diff --git a/CHANGES b/CHANGES index bc045506..d59618c8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,165 @@ Version 1.2.0 (released ??-???-????) + * bumped minimum support Python version to 2.4 + * implemented support for property diffs (issue #383) * allow user-configurable cvsgraph display (issue #336) + * allow rNNNN syntax for Subversion revision numbers (issue #441) + +Version 1.1.20 (released 24-Apr-2013) + + * fix tab-to-space handling regression in markup view + * fix regression in root lookup handling (issue #526) + +Version 1.1.19 (released 22-Apr-2013) + + * improve root lookup performance (issue #523) + * new 'max_filesize_kbytes' config option and handling (issue #524) + * tarball generation improvements: + - preserve Subversion symlinks in generated tarballs (issue #487) + - reduce memory usage of tarball generation logic + - fix double compression of generated tarballs (issue #525) + * file content handling improvements: + - expanded support for encoding detection and transcoding (issue #11) + - fix tab-to-space conversion bugs in markup, annotate, and diff views + - fix handling of trailing whitespace in diff view + * add support for timestamp display in ISO8601 format (issue #46) + +Version 1.1.18 (released 28-Feb-2013) + + * fix exception raised by BDB-backed SVN repositories (issue #519) + * hide revision-less files when rcsparse is in use + * include branchpoints in branch views using rcsparse (issue #347) + * miscellaneous cvsdb improvements: + - add --port option to make-database (issue #521) + - explicitly name columns in queries (issue #522) + - update MySQL syntax to avoid discontinued "TYPE=" terms + +Version 1.1.17 (released 25-Oct-2012) + + * fix exception caused by uninitialized variable usage (issue #516) + +Version 1.1.16 (released 24-Oct-2012) + + * security fix: escape "extra" diff info to avoid XSS attack (issue #515) + * add 'binary_mime_types' configuration option and handling (issue #510) + * fix 'select for diffs' persistence across log pages (issue #512) + * remove lock status and filesize check on directories in remote SVN views + * fix bogus 'Annotation of' page title for non-annotated view (issue #514) + +Version 1.1.15 (released 22-Jun-2012) + + * security fix: complete authz support for remote SVN views (issue #353) + * security fix: log msg leak in SVN revision view with unreadable copy source + * fix several instances of incorrect information in remote SVN views + * increase performance of some revision metadata lookups in remote SVN views + * fix RSS feed regression introduced in 1.1.14 + +Version 1.1.14 (released 12-Jun-2012) + + * fix annotation of svn files with non-URI-safe paths (issue #504) + * handle file:/// Subversion rootpaths as local roots (issue #446) + * fix bug caused by trying to case-normalize anon usernames (issue #505) + * speed up log handling by reusing tokenization results (issue #506) + * add support for custom revision log markup rules (issue #246) + +Version 1.1.13 (released 23-Jan-2012) + + * fix svndbadmin failure on deleted paths under Subversion 1.7 (issue #499) + * fix annotation of files in svn roots with non-URI-safe paths + * fix stray annotation warning in markup display of images + * more gracefully handle attempts to display binary content (issue #501) + +Version 1.1.12 (released 03-Nov-2011) + + * fix path display in patch and certain diff views (issue #485) + * fix broken cvsdb glob searching (issue 486) + * allow svn revision specifiers to have leading r's (issue #441, #448) + * allow environmental override of configuration location (issue #494) + * fix exception HTML-escaping non-string data under WSGI (issue #454) + * add links to root logs from roots view (issue #470) + * use Pygments lexer-guessing functionality (issue #495) + +Version 1.1.11 (released 17-May-2011) + + * security fix: remove user-reachable override of cvsdb row limit + * fix broken standalone.py -c and -d options handling + * add --help option to standalone.py + * fix stack trace when asked to checkout a directory (issue #478) + * improve memory usage and speed of revision log markup (issue #477) + * fix broken annotation view in CVS keyword-bearing files (issue #479) + * warn users when query results are incomplete (issue #433) + * avoid parsing errors on RCS newphrases in the admin section (issue #483) + * make rlog parsing code more robust in certain error cases (issue #444) + +Version 1.1.10 (released 15-Mar-2011) + + * fix stack trace in Subversion revision info logic (issue #475, issue #476) + +Version 1.1.9 (released 18-Feb-2011) + + * vcauth universal access determinations (issue #425) + * rework svn revision info cache for performance + * make revision log "extra pages" count configurable + * fix Subversion 1.4.x revision log compatibility code regression + * display sanitized error when authzfile is malformed + * restore markup of URLs in file contents (issue #455) + * optionally display last-committed metadata in roots view (issue #457) + +Version 1.1.8 (released 02-Dec-2010) + + * fix slowness triggered by allow_compress=1 configuration (issue #467) + * allow use of 'fcrypt' for Windows standalone.py authn support (issue #471) + * yield more useful error on directory markup/annotate request (issue #472) + +Version 1.1.7 (released 09-Sep-2010) + + * display Subversion revision properties in the revision view (issue #453) + * fix exception in 'standalone.py -r REPOS' when run without a config file + * fix standalone.py server root deployments (--script-alias='') + * add rudimentary Basic authentication support to standalone.py (issue #49) + * fix obscure "unexpected NULL parent pool" Subversion bindings error + * enable path info / link display in remote Subversion root revision view + * fix vhost name case handling inconsistency (issue #466) + * use svn:mime-type property charset param as encoding hint + * markup Subversion revision references in log messages (issue #313) + * add rudimentary support for FastCGI-based deployments (issue #464) + * fix query script WSGI deployment + * add configuration to fix query script cross-linking to ViewVC + +Version 1.1.6 (released 02-Jun-2010) + + * add rudimentary support for WSGI-based deployments (issue #397) + * fix exception caused by trying to HTML-escape non-string data (issue #454) + * fix incorrect RSS feed Content-Type header (issue #449) + * fix RSS encoding problem (issue #451) + * allow 'svndbadmin purge' to work on missing repositories (issue #452) + +Version 1.1.5 (released 29-Mar-2010) + + * security fix: escape user-provided search_re input to avoid XSS attack + +Version 1.1.4 (released 10-Mar-2010) + + * security fix: escape user-provided query form input to avoid XSS attack + * fix standalone.py failure (when per-root options aren't used) (issue #445) + * fix annotate failure caused by ignored svn_config_dir (issue #447) + +Version 1.1.3 (released 22-Dec-2009) + + * security fix: add root listing support of per-root authz config + * security fix: query.py requires 'forbidden' authorizer (or none) in config + * fix URL-ification of truncated log messages (issue #3) + * fix regexp input validation (issue #426, #427, #440) + * add support for configurable tab-to-spaces conversion + * fix not-a-sequence error in diff view + * allow viewvc-install to work when templates-contrib is absent + * minor template improvements/corrections + * expose revision metadata in diff view (issue #431) + * markup file/directory item property URLs and email addresses (issue #434) + * make ViewVC cross copies in Subversion history by default + * fix bug that caused standalone.py failure under Python 1.5.2 (issue #442) + * fix support for per-vhost overrides of authorizer parameters (issue #411) + * fix root name identification in query.py interface Version 1.1.2 (released 11-Aug-2009) @@ -62,6 +221,27 @@ Version 1.1.0 (released 13-May-2009) * fix exception in rev-sorted remote Subversion directory views (issue #409) * allow setting of page sizes for log and dir views individually (issue #402) +Version 1.0.13 (released 24-Oct-2012) + + * security fix: escape "extra" diff info to avoid XSS attack (issue #515) + * security fix: remove user-reachable override of cvsdb row limit + * fix obscure "unexpected NULL parent pool" Subversion bindings error + * fix svndbadmin failure on deleted paths under Subversion 1.7 (issue #499) + +Version 1.0.12 (released 02-Jun-2010) + + * fix exception caused by trying to HTML-escape non-string data (issue #454) + +Version 1.0.11 (released 29-Mar-2010) + + * security fix: escape user-provided search_re input to avoid XSS attack + +Version 1.0.10 (released 10-Mar-2010) + + * security fix: escape user-provided query form input to avoid XSS attack + * fix errors viewing remote Subversion paths with URI-unsafe characters + * fix regexp input validation (issue #426, #427, #440) + Version 1.0.9 (released 11-Aug-2009) * security fix: validate the 'view' parameter to avoid XSS attack diff --git a/INSTALL b/INSTALL index 1d15d9a6..d193c8d9 100644 --- a/INSTALL +++ b/INSTALL @@ -17,10 +17,13 @@ Congratulations on getting this far. :-) Required Software And Configuration Needed To Run ViewVC: + In General: + + * Python 2, version 2.4 or later (sorry, no 3.x support yet) + (http://www.python.org/) + For CVS Support: - * Python 1.5.2 or later - (http://www.python.org/) * RCS, Revision Control System (http://www.cs.purdue.edu/homes/trinkle/RCS/) * GNU-diff to replace diff implementations without the -u option @@ -30,11 +33,9 @@ Congratulations on getting this far. :-) For Subversion Support: - * Python 2.0 or later - (http://www.python.org/) * Subversion, Version Control System, 1.3.1 or later (binary installation and Python bindings) - (http://subversion.tigris.org/) + (http://subversion.apache.org/) Optional: @@ -168,72 +169,185 @@ checkin database working are below. APACHE CONFIGURATION -------------------- -1) Find out where the web server configuration file is kept. Typical - locations are /etc/httpd/httpd.conf, /etc/httpd/conf/httpd.conf, - and /etc/apache/httpd.conf. Depending on how apache was installed, - you may also look under /usr/local/etc or /etc/local. Use the vendor - documentation or the find utility if in doubt. +1) Locate your Apache configuration file(s). -Either METHOD A: -2) The ScriptAlias directive is very useful for pointing + Typical locations are /etc/httpd/httpd.conf, + /etc/httpd/conf/httpd.conf, and /etc/apache/httpd.conf. Depending + on how Apache was installed, you may also look under /usr/local/etc + or /etc/local. Use the vendor documentation or the find utility if + in doubt. + +2) Depending on how your Apache configuration is setup by default, you + might need to explicitly allow high-level access to the ViewVC + install location. + + <Directory <VIEWVC_INSTALLATION_DIRECTORY>> + Order allow,deny + Allow from all + </Directory> + + For example, if ViewVC is installed in /usr/local/viewvc-1.0 on + your system: + + <Directory /usr/local/viewvc-1.0> + Order allow,deny + Allow from all + </Directory> + +3) Configure Apache to expose ViewVC to users at the URL of your choice. + + ViewVC provides several different ways to do this. Choose one of + the following methods: + + ----------------------------------- + METHOD A: CGI mode via ScriptAlias + ----------------------------------- + The ScriptAlias directive is very useful for pointing directly to the viewvc.cgi script. Simply insert a line containing - ScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/viewvc.cgi + ScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/viewvc.cgi into your httpd.conf file. Choose the location in httpd.conf where also the other ScriptAlias lines reside. Some examples: - ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi - ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi + ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi + ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi - continue with step 3). - -or alternatively METHOD B: -2) Copy the CGI scripts from + ---------------------------------------- + METHOD B: CGI mode in cgi-bin directory + ---------------------------------------- + Copy the CGI scripts from <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi to the /cgi-bin/ directory configured in your httpd.conf file. - continue with step 3). + You can override configuration file location using: + + SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf -and then there's METHOD C: -2) Copy the CGI scripts from + ------------------------------------------ + METHOD C: CGI mode in ExecCGI'd directory + ------------------------------------------ + Copy the CGI scripts from <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi to the directory of your choosing in the Document Root adding the following - apache directives for the directory in httpd.conf or an .htaccess file: + Apache directives for the directory in httpd.conf or an .htaccess file: - Options +ExecCGI - AddHandler cgi-script .cgi + Options +ExecCGI + AddHandler cgi-script .cgi - (Note: For this to work mod_cgi has to be loaded. And for the .htaccess file + Note: For this to work mod_cgi has to be loaded. And for the .htaccess file to be effective, "AllowOverride All" or "AllowOverride Options FileInfo" - need to have been specified for the directory.) + needs to have been specified for the directory. - continue with step 3). - -or if you've got Mod_Python installed you can use METHOD D: -2) Copy the Python scripts and .htaccess file from + ------------------------------------------ + METHOD D: Using mod_python (if installed) + ------------------------------------------ + Copy the Python scripts and .htaccess file from <VIEWVC_INSTALLATION_DIRECTORY>/bin/mod_python/ - to a directory being served by apache. + to a directory being served by Apache. In httpd.conf, make sure that "AllowOverride All" or at least "AllowOverride FileInfo Options" are enabled for the directory you copied the files to. + + You can override configuration file location using: + + SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf Note: If you are using Mod_Python under Apache 1.3 the tarball generation feature may not work because it uses multithreading. This works fine under Apache 2. - continue with step 3). + ---------------------------------------- + METHOD E: Using mod_wsgi (if installed) + ---------------------------------------- + Copy the Python scripts file from + <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/ + to the directory of your choosing. Modify httpd.conf with the + following directives: -3) Restart apache. The commands to do this vary. "httpd -k restart" and - "apache -k restart" are two common variants. On RedHat Linux it is - done using the command "/sbin/service httpd restart" and on SuSE Linux - it is done with "rcapache restart" + WSGIScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/viewvc.wsgi + WSGIScriptAlias /query <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/query.wsgi -4) Optional: Add access control. + You'll probably also need the following directive because of the + not-quite-sanctioned way that ViewVC manipulates Python objects. - In your httpd.conf you can control access to certain modules by adding - directives like this: + WSGIApplicationGroup %{GLOBAL} + + Note: WSGI support in ViewVC is at this time quite rudimentary, + bordering on downright experimental. Your mileage may vary. + + ----------------------------------------- + METHOD F: Using mod_fcgid (if installed) + ----------------------------------------- + + This uses ViewVC's WSGI support (from above), but supports using FastCGI, + and is a somewhat hybrid approach of several of the above methods. + + Especially if fcgi is already being used for other purposes, e.g. PHP, + also using fcgi can prevent the need for including additional modules + (e.g. mod_python or mod_wsgi) within Apache, which may help lessen Apache's + memory usage and/or help improve performance. + + This depends on mod_fcgid: + + http://httpd.apache.org/mod_fcgid/ + + as well as the fcgi server from Python's flup package: + + http://pypi.python.org/pypi/flup + http://trac.saddi.com/flup + + The following are some example httpd.conf fragments you can use to + support this configuration: + + ScriptAlias /viewvc /usr/local/viewvc/bin/wsgi/viewvc.fcgi + ScriptAlias /query /usr/local/viewvc/bin/wsgi/query.fcgi + +4) [Optional] Provide direct access to icons, stylesheets, etc. + + ViewVC's HTML templates reference various stylesheets and icons + provided by ViewVC itself. By default, ViewVC generates URLs to + those artifacts which point back into ViewVC (using a magic + syntax); ViewVC in turn handles such magic URL requests by + streaming back the contents of the requested icon or stylesheet + file. While this simplifies the configuration and initial + deployment of ViewVC, it's not the most efficient approach to + deliver what is essentially static content. + + To improve performance, consider carving out a URL space in your + webserver's configuration solely for this static content and + instruct ViewVC to use that space when generating URLs for that + content. For example, you might add an Alias such as the following + to your httpd.conf: + + Alias /viewvc-docroot /usr/local/viewvc/templates/default/docroot + + And then, in viewvc.conf, set the 'docroot' option to the same + location: + + docroot = /viewvc-docroot + + WARNING: As always when using Alias directives, be careful that you + have them in the correct order. For example, if you use an + ordering such as the following, Apache will hand requests for your + static documents off to ViewVC as if they were versioned resources: + + ScriptAlias /viewvc /usr/local/viewvc/bin/wsgi/viewvc.fcgi + Alias /viewvc/static /usr/local/viewvc/templates/default/docroot + + The correct order would be: + + Alias /viewvc/static /usr/local/viewvc/templates/default/docroot + ScriptAlias /viewvc /usr/local/viewvc/bin/wsgi/viewvc.fcgi + + (That said, it's best to avoid such namespace nesting altogether if + you can.) + +5) [Optional] Add access control. + + In your httpd.conf you can control access to certain modules by + adding directives like this: <Location "<url to viewvc.cgi>/<modname_you_wish_to_access_ctl>"> AllowOverride None @@ -251,7 +365,14 @@ or if you've got Mod_Python installed you can use METHOD D: http://<server_name>/viewvc/~checkout~/<module_name> http://<server_name>/viewvc/<module_name>.tar.gz?view=tar -5) Optional: Protect your ViewVC instance from server-whacking webcrawlers. +6) Restart Apache. + + The commands to do this vary. "httpd -k restart" and "apache -k + restart" are two common variants. On RedHat Linux it is done using + the command "/sbin/service httpd restart" and on SuSE Linux it is + done with "rcapache restart". Other systems use "apachectl restart". + +7) [Optional] Protect your ViewVC instance from server-whacking webcrawlers. As ViewVC is a web-based application which each page containing various links to other pages and views, you can expect your server's performance diff --git a/LICENSE.html b/LICENSE.html index 2dcb6d02..d15d9b8d 100644 --- a/LICENSE.html +++ b/LICENSE.html @@ -15,7 +15,7 @@ <blockquote> -<p><strong>Copyright © 1999-2008 The ViewCVS Group. All rights +<p><strong>Copyright © 1999-2013 The ViewCVS Group. All rights reserved.</strong></p> <p>By using ViewVC, you agree to the terms and conditions set forth @@ -59,6 +59,11 @@ <li>March 17, 2006 — software renamed from "ViewCVS"</li> <li>April 10, 2007 — copyright years updated</li> <li>February 22, 2008 — copyright years updated</li> + <li>March 18, 2009 — copyright years updated</li> + <li>March 29, 2010 — copyright years updated</li> + <li>February 18, 2011 — copyright years updated</li> + <li>January 23, 2012 — copyright years updated</li> + <li>January 04, 2013 — copyright years updated</li> </ul> </body> diff --git a/bin/asp/query.asp b/bin/asp/query.asp index 314e2d8a..7eaa9467 100644 --- a/bin/asp/query.asp +++ b/bin/asp/query.asp @@ -3,7 +3,7 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -54,7 +54,10 @@ import query server = sapi.AspServer(Server, Request, Response, Application) try: cfg = viewvc.load_config(CONF_PATHNAME, server) - query.main(server, cfg, "viewvc.asp") + viewvc_base_url = cfg.query.viewvc_base_url + if viewvc_base_url is None: + viewvc_base_url = "viewvc.asp" + query.main(server, cfg, viewvc_base_url) finally: s.close() diff --git a/bin/asp/viewvc.asp b/bin/asp/viewvc.asp index 4f9b2cff..143ef045 100644 --- a/bin/asp/viewvc.asp +++ b/bin/asp/viewvc.asp @@ -3,7 +3,7 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 diff --git a/bin/cgi/query.cgi b/bin/cgi/query.cgi index 408e6dab..99eba47a 100644 --- a/bin/cgi/query.cgi +++ b/bin/cgi/query.cgi @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -34,7 +34,7 @@ CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME ######################################################################### # -# Adjust sys.path to include our library directory +# Adjust sys.path to include our library directory. # import sys @@ -47,6 +47,20 @@ else: "../../../lib"))) ######################################################################### +# +# If admins want nicer processes, here's the place to get them. +# + +#try: +# os.nice(20) # bump the nice level of this process +#except: +# pass + + +######################################################################### +# +# Go do the work. +# import sapi import viewvc @@ -54,4 +68,7 @@ import query server = sapi.CgiServer() cfg = viewvc.load_config(CONF_PATHNAME, server) -query.main(server, cfg, "viewvc.cgi") +viewvc_base_url = cfg.query.viewvc_base_url +if viewvc_base_url is None: + viewvc_base_url = "viewvc.cgi" +query.main(server, cfg, viewvc_base_url) diff --git a/bin/cgi/viewvc.cgi b/bin/cgi/viewvc.cgi index ae307323..eff18e09 100644 --- a/bin/cgi/viewvc.cgi +++ b/bin/cgi/viewvc.cgi @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -34,7 +34,7 @@ CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME ######################################################################### # -# Adjust sys.path to include our library directory +# Adjust sys.path to include our library directory. # import sys @@ -47,12 +47,21 @@ else: "../../../lib"))) ######################################################################### +# +# If admins want nicer processes, here's the place to get them. +# + +#try: +# os.nice(20) # bump the nice level of this process +#except: +# pass -### add code for checking the load average ######################################################################### +# +# Go do the work. +# -# go do the work import sapi import viewvc diff --git a/bin/cvsdbadmin b/bin/cvsdbadmin index 5b8b51f9..22f3db93 100755 --- a/bin/cvsdbadmin +++ b/bin/cvsdbadmin @@ -1,7 +1,7 @@ #!/usr/bin/python # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -40,7 +40,6 @@ else: ######################################################################### import os -import string import cvsdb import viewvc import vclib.ccvs @@ -62,7 +61,7 @@ def UpdateFile(db, repository, path, update, latest_checkin, quiet_level, encodi except vclib.ItemNotFound, e: return - file = string.join(path, "/") + file = '/'.join(path) printing = 0 if update: if quiet_level < 1 or (quiet_level < 2 and len(commit_list)): diff --git a/bin/loginfo-handler b/bin/loginfo-handler index 340bed6f..8574bb03 100755 --- a/bin/loginfo-handler +++ b/bin/loginfo-handler @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -39,7 +39,6 @@ else: ######################################################################### import os -import string import getopt import re import cvsdb @@ -152,11 +151,11 @@ def FindLongestDirectory(s, repository): and a file name, either of which may contain spaces. Returns the longest possible directory name that actually exists""" - parts = string.split(s, " ") + parts = s.split() for i in range(len(parts)-1, 0, -1): - directory = string.join(parts[:i]) - filename = string.join(parts[i:]) + directory = ' '.join(parts[:i]) + filename = ' '.join(parts[i:]) if os.path.isdir(os.path.join(repository, directory)): return directory, filename @@ -227,7 +226,7 @@ def ProcessLoginfo(rootpath, directory, files): cfg.utilities, 0) # split up the directory components - dirpath = filter(None, string.split(os.path.normpath(directory), os.sep)) + dirpath = filter(None, os.path.normpath(directory).split(os.sep)) ## build a list of Commit objects commit_list = [] @@ -279,7 +278,7 @@ if __name__ == '__main__': else: # if there are no arguments, read version information from # first line of input like old versions of ViewCVS did - arg = string.rstrip(sys.stdin.readline()) + arg = sys.stdin.readline().rstrip() if len(sys.argv) > 2: # if there is a second argument it indicates which parser diff --git a/bin/mod_python/handler.py b/bin/mod_python/handler.py index 792a9d23..e9ff22c1 100755 --- a/bin/mod_python/handler.py +++ b/bin/mod_python/handler.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 diff --git a/bin/mod_python/query.py b/bin/mod_python/query.py index dda3c55c..75a2adef 100755 --- a/bin/mod_python/query.py +++ b/bin/mod_python/query.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -66,7 +66,10 @@ cfg = viewvc.load_config(CONF_PATHNAME) def index(req): server = sapi.ModPythonServer(req) try: - query.main(server, cfg, "viewvc.py") + viewvc_base_url = cfg.query.viewvc_base_url + if viewvc_base_url is None: + viewvc_base_url = "viewvc.py" + query.main(server, cfg, viewvc_base_url) finally: server.close() diff --git a/bin/mod_python/viewvc.py b/bin/mod_python/viewvc.py index d625275a..ba13c5fa 100755 --- a/bin/mod_python/viewvc.py +++ b/bin/mod_python/viewvc.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 diff --git a/bin/standalone.py b/bin/standalone.py index 0f15d746..48522548 100755 --- a/bin/standalone.py +++ b/bin/standalone.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -10,19 +10,17 @@ # For more information, visit http://viewvc.org/ # # ----------------------------------------------------------------------- +# +# This program originally written by Peter Funk <pf@artcom-gmbh.de>, with +# contributions by Ka-Ping Yee. +# +# ----------------------------------------------------------------------- """Run "standalone.py -p <port>" to start an HTTP server on a given port on the local machine to generate ViewVC web pages. """ -__author__ = "Peter Funk <pf@artcom-gmbh.de>" -__date__ = "11 November 2001" -__version__ = "$Revision: 1962 $" -__credits__ = """Guido van Rossum, for an excellent programming language. -Greg Stein, for writing ViewCVS in the first place. -Ka-Ping Yee, for the GUI code and the framework stolen from pydoc.py. -""" - +# # INSTALL-TIME CONFIGURATION # # These values will be set during the installation process. During @@ -42,6 +40,7 @@ import urllib import rfc822 import socket import select +import base64 import BaseHTTPServer if LIBRARY_DIR: @@ -51,633 +50,539 @@ else: import sapi import viewvc -import compat; compat.for_standalone() + + +# The 'crypt' module is only available on Unix platforms. We'll try +# to use 'fcrypt' if it's available (for more information, see +# http://carey.geek.nz/code/python-fcrypt/). +has_crypt = False +try: + import crypt + has_crypt = True + def _check_passwd(user_passwd, real_passwd): + return real_passwd == crypt.crypt(user_passwd, real_passwd[:2]) +except ImportError: + try: + import fcrypt + has_crypt = True + def _check_passwd(user_passwd, real_passwd): + return real_passwd == fcrypt.crypt(user_passwd, real_passwd[:2]) + except ImportError: + def _check_passwd(user_passwd, real_passwd): + return False class Options: - port = 49152 # default TCP/IP port used for the server - start_gui = 0 # No GUI unless requested. - daemon = 0 # stay in the foreground by default - repositories = {} # use default repositories specified in config - if sys.platform == 'mac': - host = '127.0.0.1' - else: - host = 'localhost' - script_alias = 'viewvc' - config_file = None + port = 49152 # default TCP/IP port used for the server + repositories = {} # use default repositories specified in config + host = sys.platform == 'mac' and '127.0.0.1' or 'localhost' + script_alias = 'viewvc' + config_file = None + htpasswd_file = None -# --- web browser interface: ---------------------------------------------- class StandaloneServer(sapi.CgiServer): - def __init__(self, handler): - sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32") - self.handler = handler + """Custom sapi interface that uses a BaseHTTPRequestHandler HANDLER + to generate output.""" + + def __init__(self, handler): + sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32") + self.handler = handler - def header(self, content_type='text/html', status=None): - if not self.headerSent: - self.headerSent = 1 - if status is None: - statusCode = 200 - statusText = 'OK' - else: - p = string.find(status, ' ') - if p < 0: - statusCode = int(status) - statusText = '' - else: - statusCode = int(status[:p]) - statusText = status[p+1:] - self.handler.send_response(statusCode, statusText) - self.handler.send_header("Content-type", content_type) - for (name, value) in self.headers: - self.handler.send_header(name, value) - self.handler.end_headers() - + def header(self, content_type='text/html', status=None): + if not self.headerSent: + self.headerSent = 1 + if status is None: + statusCode = 200 + statusText = 'OK' + else: + p = status.find(' ') + if p < 0: + statusCode = int(status) + statusText = '' + else: + statusCode = int(status[:p]) + statusText = status[p+1:] + self.handler.send_response(statusCode, statusText) + self.handler.send_header("Content-type", content_type) + for (name, value) in self.headers: + self.handler.send_header(name, value) + self.handler.end_headers() -def serve(host, port, callback=None): - """start a HTTP server on the given port. call 'callback' when the - server is ready to serve""" - class ViewVC_Handler(BaseHTTPServer.BaseHTTPRequestHandler): - - def do_GET(self): - """Serve a GET request.""" - if not self.path or self.path == "/": - self.redirect() - elif self.is_viewvc(): - try: - self.run_viewvc() - except IOError: - # ignore IOError: [Errno 32] Broken pipe - pass - else: - self.send_error(404) +class NotViewVCLocationException(Exception): + """The request location was not aimed at ViewVC.""" + pass - def do_POST(self): - """Serve a POST request.""" - if self.is_viewvc(): - self.run_viewvc() - else: - self.send_error(501, "Can only POST to %s" - % (options.script_alias)) - def is_viewvc(self): - """Check whether self.path is, or is a child of, the ScriptAlias""" - if self.path == '/' + options.script_alias: - return 1 - if self.path[:len(options.script_alias)+2] == \ - '/' + options.script_alias + '/': - return 1 - if self.path[:len(options.script_alias)+2] == \ - '/' + options.script_alias + '?': - return 1 - return 0 +class AuthenticationException(Exception): + """Authentication requirements have not been met.""" + pass - def redirect(self): - """redirect the browser to the viewvc URL""" - new_url = self.server.url + options.script_alias + '/' - self.send_response(301, "Moved (redirection follows)") - self.send_header("Content-type", "text/html") - self.send_header("Location", new_url) - self.end_headers() - self.wfile.write("""<html> + +class ViewVCHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """Custom HTTP request handler for ViewVC.""" + + def do_GET(self): + """Serve a GET request.""" + self.handle_request('GET') + + def do_POST(self): + """Serve a POST request.""" + self.handle_request('POST') + + def handle_request(self, method): + """Handle a request of type METHOD.""" + try: + self.run_viewvc() + except NotViewVCLocationException: + # If the request was aimed at the server root, but there's a + # non-empty script_alias, automatically redirect to the + # script_alias. Otherwise, just return a 404 and shrug. + if (not self.path or self.path == "/") and options.script_alias: + new_url = self.server.url + options.script_alias + '/' + self.send_response(301, "Moved Permanently") + self.send_header("Content-type", "text/html") + self.send_header("Location", new_url) + self.end_headers() + self.wfile.write("""<html> <head> -<meta http-equiv="refresh" content="1; URL=%s"> +<meta http-equiv="refresh" content="10; url=%s" /> +<title>Moved Temporarily -

Redirection to ViewVC

-Wait a second. You will be automatically redirected to ViewVC. -If this doesn't work, please click on the link above. +

Redirecting to ViewVC

+

You will be automatically redirected to ViewVC. + If this doesn't work, please click on the link above.

-""" % tuple([new_url]*2)) - - def run_viewvc(self): - """This is a quick and dirty cut'n'rape from Python's - standard library module CGIHTTPServer.""" - scriptname = '/' + options.script_alias - assert string.find(self.path, scriptname) == 0 - viewvc_url = self.server.url[:-1] + scriptname - rest = self.path[len(scriptname):] - i = string.rfind(rest, '?') - if i >= 0: - rest, query = rest[:i], rest[i+1:] - else: - query = '' - # sys.stderr.write("Debug: '"+scriptname+"' '"+rest+"' '"+query+"'\n") - env = os.environ - # Since we're going to modify the env in the parent, provide empty - # values to override previously set values - for k in env.keys(): - if k[:5] == 'HTTP_': - del env[k] - for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', - 'HTTP_USER_AGENT', 'HTTP_COOKIE'): - if env.has_key(k): - env[k] = "" - # XXX Much of the following could be prepared ahead of time! - env['SERVER_SOFTWARE'] = self.version_string() - env['SERVER_NAME'] = self.server.server_name - env['GATEWAY_INTERFACE'] = 'CGI/1.1' - env['SERVER_PROTOCOL'] = self.protocol_version - env['SERVER_PORT'] = str(self.server.server_port) - env['REQUEST_METHOD'] = self.command - uqrest = urllib.unquote(rest) - env['PATH_INFO'] = uqrest - env['SCRIPT_NAME'] = scriptname - if query: - env['QUERY_STRING'] = query - env['HTTP_HOST'] = self.server.address[0] - host = self.address_string() - if host != self.client_address[0]: - env['REMOTE_HOST'] = host - env['REMOTE_ADDR'] = self.client_address[0] - # AUTH_TYPE - # REMOTE_USER - # REMOTE_IDENT - if self.headers.typeheader is None: - env['CONTENT_TYPE'] = self.headers.type - else: - env['CONTENT_TYPE'] = self.headers.typeheader - length = self.headers.getheader('content-length') - if length: - env['CONTENT_LENGTH'] = length - accept = [] - for line in self.headers.getallmatchingheaders('accept'): - if line[:1] in string.whitespace: - accept.append(string.strip(line)) - else: - accept = accept + string.split(line[7:], ',') - env['HTTP_ACCEPT'] = string.joinfields(accept, ',') - ua = self.headers.getheader('user-agent') - if ua: - env['HTTP_USER_AGENT'] = ua - modified = self.headers.getheader('if-modified-since') - if modified: - env['HTTP_IF_MODIFIED_SINCE'] = modified - etag = self.headers.getheader('if-none-match') - if etag: - env['HTTP_IF_NONE_MATCH'] = etag - # XXX Other HTTP_* headers - decoded_query = string.replace(query, '+', ' ') - - # Preserve state, because we execute script in current process: - save_argv = sys.argv - save_stdin = sys.stdin - save_stdout = sys.stdout - save_stderr = sys.stderr - # For external tools like enscript we also need to redirect - # the real stdout file descriptor. - # - # FIXME: This code used to carry the following comment: - # - # (On windows, reassigning the sys.stdout variable is sufficient - # because pipe_cmds makes it the standard output for child - # processes.) - # - # But we no longer use pipe_cmds. So at the very least, the - # comment is stale. Is the code okay, though? - if sys.platform != "win32": save_realstdout = os.dup(1) - try: - try: - sys.stdout = self.wfile - if sys.platform != "win32": - os.dup2(self.wfile.fileno(), 1) - sys.stdin = self.rfile - viewvc.main(StandaloneServer(self), cfg) - finally: - sys.argv = save_argv - sys.stdin = save_stdin - sys.stdout.flush() - if sys.platform != "win32": - os.dup2(save_realstdout, 1) - os.close(save_realstdout) - sys.stdout = save_stdout - sys.stderr = save_stderr - except SystemExit, status: - self.log_error("ViewVC exit status %s", str(status)) - else: - self.log_error("ViewVC exited ok") - - class ViewVC_Server(BaseHTTPServer.HTTPServer): - def __init__(self, host, port, callback): - self.address = (host, port) - self.url = 'http://%s:%d/' % (host, port) - self.callback = callback - BaseHTTPServer.HTTPServer.__init__(self, self.address, - self.handler) - - def serve_until_quit(self): - self.quit = 0 - while not self.quit: - rd, wr, ex = select.select([self.socket.fileno()], [], [], 1) - if rd: - self.handle_request() - - def server_activate(self): - BaseHTTPServer.HTTPServer.server_activate(self) - if self.callback: - self.callback(self) - - def server_bind(self): - # set SO_REUSEADDR (if available on this platform) - if hasattr(socket, 'SOL_SOCKET') \ - and hasattr(socket, 'SO_REUSEADDR'): - self.socket.setsockopt(socket.SOL_SOCKET, - socket.SO_REUSEADDR, 1) - BaseHTTPServer.HTTPServer.server_bind(self) - - ViewVC_Server.handler = ViewVC_Handler +""" % (new_url, new_url)) + else: + self.send_error(404) + except IOError: # ignore IOError: [Errno 32] Broken pipe + pass + except AuthenticationException: + self.send_response(401, "Unauthorized") + self.send_header("WWW-Authenticate", 'Basic realm="ViewVC"') + self.send_header("Content-type", "text/html") + self.end_headers() + self.wfile.write(""" + +Authentication failed + + +

Authentication failed

+

Authentication has failed. Please retry with the correct username + and password.

+ +""") + + def is_viewvc(self): + """Check whether self.path is, or is a child of, the ScriptAlias""" + if not options.script_alias: + return 1 + if self.path == '/' + options.script_alias: + return 1 + alias_len = len(options.script_alias) + if self.path[:alias_len+2] == '/' + options.script_alias + '/': + return 1 + if self.path[:alias_len+2] == '/' + options.script_alias + '?': + return 1 + return 0 + def validate_password(self, htpasswd_file, username, password): + """Compare USERNAME and PASSWORD against HTPASSWD_FILE.""" try: - # XXX Move this code out of this function. - # Early loading of configuration here. Used to - # allow tinkering with some configuration settings: - handle_config(options.config_file) - if options.repositories: - cfg.general.default_root = "Development" - for repo_name in options.repositories.keys(): - repo_path = os.path.normpath(options.repositories[repo_name]) - if os.path.exists(os.path.join(repo_path, "CVSROOT", - "config")): - cfg.general.cvs_roots[repo_name] = repo_path - elif os.path.exists(os.path.join(repo_path, "format")): - cfg.general.svn_roots[repo_name] = repo_path - elif cfg.general.cvs_roots.has_key("Development") and \ - not os.path.isdir(cfg.general.cvs_roots["Development"]): - sys.stderr.write("*** No repository found. Please use the -r option.\n") - sys.stderr.write(" Use --help for more info.\n") - raise KeyboardInterrupt # Hack! - os.close(0) # To avoid problems with shell job control + lines = open(htpasswd_file, 'r').readlines() + for line in lines: + file_user, file_pass = line.rstrip().split(':', 1) + if username == file_user: + return _check_passwd(password, file_pass) + except: + pass + return False + + def run_viewvc(self): + """Run ViewVC to field a single request.""" - # always use default docroot location - cfg.options.docroot = None + ### Much of this is adapter from Python's standard library + ### module CGIHTTPServer. - # if cvsnt isn't found, fall back to rcs - if (cfg.conf_path is None and cfg.utilities.cvsnt): - import popen - cvsnt_works = 0 - try: - fp = popen.popen(cfg.utilities.cvsnt, ['--version'], 'rt') - try: - while 1: - line = fp.readline() - if not line: break - if string.find(line, "Concurrent Versions System (CVSNT)")>=0: - cvsnt_works = 1 - while fp.read(4096): - pass - break - finally: - fp.close() - except: - pass - if not cvsnt_works: - cfg.utilities.cvsnt = None + # Is this request even aimed at ViewVC? If not, complain. + if not self.is_viewvc(): + raise NotViewVCLocationException() - ViewVC_Server(host, port, callback).serve_until_quit() - except (KeyboardInterrupt, select.error): + # If htpasswd authentication is enabled, try to authenticate the user. + self.username = None + if options.htpasswd_file: + authn = self.headers.get('authorization') + if not authn: + raise AuthenticationException() + try: + kind, data = authn.split(' ', 1) + if kind == 'Basic': + data = base64.b64decode(data) + username, password = data.split(':', 1) + except: + raise AuthenticationException() + if not self.validate_password(options.htpasswd_file, username, password): + raise AuthenticationException() + self.username = username + + # Setup the environment in preparation of executing ViewVC's core code. + env = os.environ + + scriptname = options.script_alias and '/' + options.script_alias or '' + + viewvc_url = self.server.url[:-1] + scriptname + rest = self.path[len(scriptname):] + i = rest.rfind('?') + if i >= 0: + rest, query = rest[:i], rest[i+1:] + else: + query = '' + + # Since we're going to modify the env in the parent, provide empty + # values to override previously set values + for k in env.keys(): + if k[:5] == 'HTTP_': + del env[k] + for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', + 'HTTP_USER_AGENT', 'HTTP_COOKIE'): + if env.has_key(k): + env[k] = "" + + # XXX Much of the following could be prepared ahead of time! + env['SERVER_SOFTWARE'] = self.version_string() + env['SERVER_NAME'] = self.server.server_name + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['SERVER_PROTOCOL'] = self.protocol_version + env['SERVER_PORT'] = str(self.server.server_port) + env['REQUEST_METHOD'] = self.command + uqrest = urllib.unquote(rest) + env['PATH_INFO'] = uqrest + env['SCRIPT_NAME'] = scriptname + if query: + env['QUERY_STRING'] = query + env['HTTP_HOST'] = self.server.address[0] + host = self.address_string() + if host != self.client_address[0]: + env['REMOTE_HOST'] = host + env['REMOTE_ADDR'] = self.client_address[0] + if self.username: + env['REMOTE_USER'] = self.username + if self.headers.typeheader is None: + env['CONTENT_TYPE'] = self.headers.type + else: + env['CONTENT_TYPE'] = self.headers.typeheader + length = self.headers.getheader('content-length') + if length: + env['CONTENT_LENGTH'] = length + accept = [] + for line in self.headers.getallmatchingheaders('accept'): + if line[:1] in string.whitespace: + accept.append(line.strip()) + else: + accept = accept + line[7:].split(',') + env['HTTP_ACCEPT'] = ','.join(accept) + ua = self.headers.getheader('user-agent') + if ua: + env['HTTP_USER_AGENT'] = ua + modified = self.headers.getheader('if-modified-since') + if modified: + env['HTTP_IF_MODIFIED_SINCE'] = modified + etag = self.headers.getheader('if-none-match') + if etag: + env['HTTP_IF_NONE_MATCH'] = etag + # AUTH_TYPE + # REMOTE_IDENT + # XXX Other HTTP_* headers + + # Preserve state, because we execute script in current process: + save_argv = sys.argv + save_stdin = sys.stdin + save_stdout = sys.stdout + save_stderr = sys.stderr + # For external tools like enscript we also need to redirect + # the real stdout file descriptor. + # + # FIXME: This code used to carry the following comment: + # + # (On windows, reassigning the sys.stdout variable is sufficient + # because pipe_cmds makes it the standard output for child + # processes.) + # + # But we no longer use pipe_cmds. So at the very least, the + # comment is stale. Is the code okay, though? + if sys.platform != "win32": + save_realstdout = os.dup(1) + try: + try: + sys.stdout = self.wfile + if sys.platform != "win32": + os.dup2(self.wfile.fileno(), 1) + sys.stdin = self.rfile + viewvc.main(StandaloneServer(self), cfg) + finally: + sys.argv = save_argv + sys.stdin = save_stdin + sys.stdout.flush() + if sys.platform != "win32": + os.dup2(save_realstdout, 1) + os.close(save_realstdout) + sys.stdout = save_stdout + sys.stderr = save_stderr + except SystemExit, status: + self.log_error("ViewVC exit status %s", str(status)) + else: + self.log_error("ViewVC exited ok") + + +class ViewVCHTTPServer(BaseHTTPServer.HTTPServer): + """Customized HTTP server for ViewVC.""" + + def __init__(self, host, port, callback): + self.address = (host, port) + self.url = 'http://%s:%d/' % (host, port) + self.callback = callback + BaseHTTPServer.HTTPServer.__init__(self, self.address, self.handler) + + def serve_until_quit(self): + self.quit = 0 + while not self.quit: + rd, wr, ex = select.select([self.socket.fileno()], [], [], 1) + if rd: + self.handle_request() + + def server_activate(self): + BaseHTTPServer.HTTPServer.server_activate(self) + if self.callback: + self.callback(self) + + def server_bind(self): + # set SO_REUSEADDR (if available on this platform) + if hasattr(socket, 'SOL_SOCKET') and hasattr(socket, 'SO_REUSEADDR'): + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + BaseHTTPServer.HTTPServer.server_bind(self) + + +def serve(host, port, callback=None): + """Start an HTTP server for HOST on PORT. Call CALLBACK function + when the server is ready to serve.""" + + ViewVCHTTPServer.handler = ViewVCHTTPRequestHandler + + try: + # XXX Move this code out of this function. + # Early loading of configuration here. Used to allow tinkering + # with some configuration settings: + handle_config(options.config_file) + if options.repositories: + cfg.general.default_root = "Development" + for repo_name in options.repositories.keys(): + repo_path = os.path.normpath(options.repositories[repo_name]) + if os.path.exists(os.path.join(repo_path, "CVSROOT", "config")): + cfg.general.cvs_roots[repo_name] = repo_path + elif os.path.exists(os.path.join(repo_path, "format")): + cfg.general.svn_roots[repo_name] = repo_path + elif cfg.general.cvs_roots.has_key("Development") and \ + not os.path.isdir(cfg.general.cvs_roots["Development"]): + sys.stderr.write("*** No repository found. Please use the -r option.\n") + sys.stderr.write(" Use --help for more info.\n") + raise KeyboardInterrupt # Hack! + os.close(0) # To avoid problems with shell job control + + # always use default docroot location + cfg.options.docroot = None + + # if cvsnt isn't found, fall back to rcs + if (cfg.conf_path is None and cfg.utilities.cvsnt): + import popen + cvsnt_works = 0 + try: + fp = popen.popen(cfg.utilities.cvsnt, ['--version'], 'rt') + try: + while 1: + line = fp.readline() + if not line: + break + if line.find("Concurrent Versions System (CVSNT)") >= 0: + cvsnt_works = 1 + while fp.read(4096): + pass + break + finally: + fp.close() + except: pass - print 'server stopped' + if not cvsnt_works: + cfg.utilities.cvsnt = None + + ViewVCHTTPServer(host, port, callback).serve_until_quit() + except (KeyboardInterrupt, select.error): + pass + print 'server stopped' + def handle_config(config_file): global cfg cfg = viewvc.load_config(config_file or CONF_PATHNAME) -# --- graphical interface: -------------------------------------------------- -def nogui(missing_module): - sys.stderr.write( - "Sorry! Your Python was compiled without the %s module"%missing_module+ - " enabled.\nI'm unable to run the GUI part. Please omit the '-g'\n"+ - "and '--gui' options or install another Python interpreter.\n") - raise SystemExit, 1 +def usage(): + clean_options = Options() + cmd = os.path.basename(sys.argv[0]) + port = clean_options.port + host = clean_options.host + script_alias = clean_options.script_alias + sys.stderr.write("""Usage: %(cmd)s [OPTIONS] -def gui(host, port): - """Graphical interface (starts web server and pops up a control window).""" - class GUI: - def __init__(self, window, host, port): - self.window = window - self.server = None - self.scanner = None - - try: - import Tkinter - except ImportError: - nogui("Tkinter") - - self.server_frm = Tkinter.Frame(window) - self.title_lbl = Tkinter.Label(self.server_frm, - text='Starting server...\n ') - self.open_btn = Tkinter.Button(self.server_frm, - text='open browser', command=self.open, state='disabled') - self.quit_btn = Tkinter.Button(self.server_frm, - text='quit serving', command=self.quit, state='disabled') - - - self.window.title('ViewVC standalone') - self.window.protocol('WM_DELETE_WINDOW', self.quit) - self.title_lbl.pack(side='top', fill='x') - self.open_btn.pack(side='left', fill='x', expand=1) - self.quit_btn.pack(side='right', fill='x', expand=1) - - # Early loading of configuration here. Used to - # allow tinkering with configuration settings through the gui: - handle_config(options.config_file) - if not LIBRARY_DIR: - cfg.options.cvsgraph_conf = "../cgi/cvsgraph.conf.dist" - - self.options_frm = Tkinter.Frame(window) - - # cvsgraph toggle: - self.cvsgraph_ivar = Tkinter.IntVar() - self.cvsgraph_ivar.set(cfg.options.use_cvsgraph) - self.cvsgraph_toggle = Tkinter.Checkbutton(self.options_frm, - text="enable cvsgraph (needs binary)", var=self.cvsgraph_ivar, - command=self.toggle_use_cvsgraph) - self.cvsgraph_toggle.pack(side='top', anchor='w') - - # show_subdir_lastmod toggle: - self.subdirmod_ivar = Tkinter.IntVar() - self.subdirmod_ivar.set(cfg.options.show_subdir_lastmod) - self.subdirmod_toggle = Tkinter.Checkbutton(self.options_frm, - text="show subdir last mod (dir view)", var=self.subdirmod_ivar, - command=self.toggle_subdirmod) - self.subdirmod_toggle.pack(side='top', anchor='w') - - # use_re_search toggle: - self.useresearch_ivar = Tkinter.IntVar() - self.useresearch_ivar.set(cfg.options.use_re_search) - self.useresearch_toggle = Tkinter.Checkbutton(self.options_frm, - text="allow regular expr search", var=self.useresearch_ivar, - command=self.toggle_useresearch) - self.useresearch_toggle.pack(side='top', anchor='w') - - # use_localtime toggle: - self.use_localtime_ivar = Tkinter.IntVar() - self.use_localtime_ivar.set(cfg.options.use_localtime) - self.use_localtime_toggle = Tkinter.Checkbutton(self.options_frm, - text="use localtime (instead of UTC)", - var=self.use_localtime_ivar, - command=self.toggle_use_localtime) - self.use_localtime_toggle.pack(side='top', anchor='w') - - # log_pagesize integer var: - self.log_pagesize_lbl = Tkinter.Label(self.options_frm, - text='Paging (number of items per log page, 0 disables):') - self.log_pagesize_lbl.pack(side='top', anchor='w') - self.log_pagesize_ivar = Tkinter.IntVar() - self.log_pagesize_ivar.set(cfg.options.log_pagesize) - self.log_pagesize_entry = Tkinter.Entry(self.options_frm, - width=10, textvariable=self.log_pagesize_ivar) - self.log_pagesize_entry.bind('', self.set_log_pagesize) - self.log_pagesize_entry.pack(side='top', anchor='w') - - # dir_pagesize integer var: - self.dir_pagesize_lbl = Tkinter.Label(self.options_frm, - text='Paging (number of items per dir page, 0 disables):') - self.dir_pagesize_lbl.pack(side='top', anchor='w') - self.dir_pagesize_ivar = Tkinter.IntVar() - self.dir_pagesize_ivar.set(cfg.options.dir_pagesize) - self.dir_pagesize_entry = Tkinter.Entry(self.options_frm, - width=10, textvariable=self.dir_pagesize_ivar) - self.dir_pagesize_entry.bind('', self.set_dir_pagesize) - self.dir_pagesize_entry.pack(side='top', anchor='w') - - # directory view template: - self.dirtemplate_lbl = Tkinter.Label(self.options_frm, - text='Choose HTML Template for the Directory pages:') - self.dirtemplate_lbl.pack(side='top', anchor='w') - self.dirtemplate_svar = Tkinter.StringVar() - self.dirtemplate_svar.set(cfg.templates.directory) - self.dirtemplate_entry = Tkinter.Entry(self.options_frm, - width = 40, textvariable=self.dirtemplate_svar) - self.dirtemplate_entry.bind('', self.set_templates_directory) - self.dirtemplate_entry.pack(side='top', anchor='w') - self.templates_dir = Tkinter.Radiobutton(self.options_frm, - text="directory.ezt", value="templates/directory.ezt", - var=self.dirtemplate_svar, command=self.set_templates_directory) - self.templates_dir.pack(side='top', anchor='w') - self.templates_dir_alt = Tkinter.Radiobutton(self.options_frm, - text="dir_alternate.ezt", value="templates/dir_alternate.ezt", - var=self.dirtemplate_svar, command=self.set_templates_directory) - self.templates_dir_alt.pack(side='top', anchor='w') - - # log view template: - self.logtemplate_lbl = Tkinter.Label(self.options_frm, - text='Choose HTML Template for the Log pages:') - self.logtemplate_lbl.pack(side='top', anchor='w') - self.logtemplate_svar = Tkinter.StringVar() - self.logtemplate_svar.set(cfg.templates.log) - self.logtemplate_entry = Tkinter.Entry(self.options_frm, - width = 40, textvariable=self.logtemplate_svar) - self.logtemplate_entry.bind('', self.set_templates_log) - self.logtemplate_entry.pack(side='top', anchor='w') - self.templates_log = Tkinter.Radiobutton(self.options_frm, - text="log.ezt", value="templates/log.ezt", - var=self.logtemplate_svar, command=self.set_templates_log) - self.templates_log.pack(side='top', anchor='w') - self.templates_log_table = Tkinter.Radiobutton(self.options_frm, - text="log_table.ezt", value="templates/log_table.ezt", - var=self.logtemplate_svar, command=self.set_templates_log) - self.templates_log_table.pack(side='top', anchor='w') - - # query view template: - self.querytemplate_lbl = Tkinter.Label(self.options_frm, - text='Template for the database query page:') - self.querytemplate_lbl.pack(side='top', anchor='w') - self.querytemplate_svar = Tkinter.StringVar() - self.querytemplate_svar.set(cfg.templates.query) - self.querytemplate_entry = Tkinter.Entry(self.options_frm, - width = 40, textvariable=self.querytemplate_svar) - self.querytemplate_entry.bind('', self.set_templates_query) - self.querytemplate_entry.pack(side='top', anchor='w') - self.templates_query = Tkinter.Radiobutton(self.options_frm, - text="query.ezt", value="templates/query.ezt", - var=self.querytemplate_svar, command=self.set_templates_query) - self.templates_query.pack(side='top', anchor='w') - - # pack and set window manager hints: - self.server_frm.pack(side='top', fill='x') - self.options_frm.pack(side='top', fill='x') - - self.window.update() - self.minwidth = self.window.winfo_width() - self.minheight = self.window.winfo_height() - self.expanded = 0 - self.window.wm_geometry('%dx%d' % (self.minwidth, self.minheight)) - self.window.wm_minsize(self.minwidth, self.minheight) - - try: - import threading - except ImportError: - nogui("thread") - threading.Thread(target=serve, - args=(host, port, self.ready)).start() - - def toggle_use_cvsgraph(self, event=None): - cfg.options.use_cvsgraph = self.cvsgraph_ivar.get() - - def toggle_use_localtime(self, event=None): - cfg.options.use_localtime = self.use_localtime_ivar.get() - - def toggle_subdirmod(self, event=None): - cfg.options.show_subdir_lastmod = self.subdirmod_ivar.get() - - def toggle_useresearch(self, event=None): - cfg.options.use_re_search = self.useresearch_ivar.get() - - def set_log_pagesize(self, event=None): - cfg.options.log_pagesize = self.log_pagesize_ivar.get() - - def set_dir_pagesize(self, event=None): - cfg.options.dir_pagesize = self.dir_pagesize_ivar.get() - - def set_templates_log(self, event=None): - cfg.templates.log = self.logtemplate_svar.get() - - def set_templates_directory(self, event=None): - cfg.templates.directory = self.dirtemplate_svar.get() - - def set_templates_query(self, event=None): - cfg.templates.query = self.querytemplate_svar.get() - - def ready(self, server): - """used as callback parameter to the serve() function""" - self.server = server - self.title_lbl.config( - text='ViewVC standalone server at\n' + server.url) - self.open_btn.config(state='normal') - self.quit_btn.config(state='normal') - - def open(self, event=None, url=None): - """opens a browser window on the local machine""" - url = url or self.server.url - try: - import webbrowser - webbrowser.open(url) - except ImportError: # pre-webbrowser.py compatibility - if sys.platform == 'win32': - os.system('start "%s"' % url) - elif sys.platform == 'mac': - try: - import ic - ic.launchurl(url) - except ImportError: pass - else: - rc = os.system('netscape -remote "openURL(%s)" &' % url) - if rc: os.system('netscape "%s" &' % url) - - def quit(self, event=None): - if self.server: - self.server.quit = 1 - self.window.quit() - - import Tkinter - try: - gui = GUI(Tkinter.Tk(), host, port) - Tkinter.mainloop() - except KeyboardInterrupt: - pass - -# --- command-line interface: ---------------------------------------------- - -def cli(argv): - """Command-line interface (looks at argv to decide what to do).""" - import getopt - class BadUsage(Exception): pass - - try: - opts, args = getopt.getopt(argv[1:], 'gdc:p:r:h:s:', - ['gui', 'daemon', 'config-file=', 'host=', - 'port=', 'repository=', 'script-alias=']) - for opt, val in opts: - if opt in ('-g', '--gui'): - options.start_gui = 1 - elif opt in ('-r', '--repository'): - if options.repositories: # option may be used more than once: - num = len(options.repositories.keys())+1 - symbolic_name = "Repository"+str(num) - options.repositories[symbolic_name] = val - else: - options.repositories["Development"] = val - elif opt in ('-d', '--daemon'): - options.daemon = 1 - elif opt in ('-p', '--port'): - try: - options.port = int(val) - except ValueError: - raise BadUsage, "Port '%s' is not a valid port number" \ - % (val) - elif opt in ('-h', '--host'): - options.host = val - elif opt in ('-s', '--script-alias'): - options.script_alias = \ - string.join(filter(None, string.split(val, '/')), '/') - elif opt in ('-c', '--config-file'): - options.config_file = val - if options.start_gui and options.config_file: - raise BadUsage, "--config-file option is not valid in GUI mode." - if not options.start_gui and not options.port: - raise BadUsage, "You must supply a valid port, or run in GUI mode." - if options.daemon: - pid = os.fork() - if pid != 0: - sys.exit() - if options.start_gui: - gui(options.host, options.port) - return - elif options.port: - def ready(server): - print 'server ready at %s%s' % (server.url, - options.script_alias) - serve(options.host, options.port, ready) - return - except (getopt.error, BadUsage), err: - cmd = os.path.basename(sys.argv[0]) - port = options.port - host = options.host - script_alias = options.script_alias - if str(err): - sys.stderr.write("ERROR: %s\n\n" % (str(err))) - sys.stderr.write("""Usage: %(cmd)s [OPTIONS] - -Run a simple, standalone HTTP server configured to serve up ViewVC -requests. +Run a simple, standalone HTTP server configured to serve up ViewVC requests. Options: - --config-file=PATH (-c) Use the file at PATH as the ViewVC configuration - file. If not specified, ViewVC will try to use - the configuration file in its installation tree; - otherwise, built-in default values are used. - (Not valid in GUI mode.) + --config-file=FILE (-c) Read configuration options from FILE. If not + specified, ViewVC will look for a configuration + file in its installation tree, falling back to + built-in default values. --daemon (-d) Background the server process. + + --help Show this usage message and exit. - --host=HOST (-h) Start the server listening on HOST. You need - to provide the hostname if you want to - access the standalone server from a remote - machine. [default: %(host)s] + --host=HOSTNAME (-h) Listen on HOSTNAME. Required for access from a + remote machine. [default: %(host)s] - --port=PORT (-p) Start the server on the given PORT. - [default: %(port)d] + --htpasswd-file=FILE Authenticate incoming requests, validating against + against FILE, which is an Apache HTTP Server + htpasswd file. (CRYPT only; no DIGEST support.) - --repository=PATH (-r) Serve up the Subversion or CVS repository located + --port=PORT (-p) Listen on PORT. [default: %(port)d] + + --repository=PATH (-r) Serve the Subversion or CVS repository located at PATH. This option may be used more than once. - --script-alias=PATH (-s) Specify the ScriptAlias, the artificial path - location that at which ViewVC appears to be - located. For example, if your ScriptAlias is - "cgi-bin/viewvc", then ViewVC will be accessible - at "http://%(host)s:%(port)s/cgi-bin/viewvc". + --script-alias=PATH (-s) Use PATH as the virtual script location (similar + to Apache HTTP Server's ScriptAlias directive). + For example, "--script-alias=repo/view" will serve + ViewVC at "http://HOSTNAME:PORT/repo/view". [default: %(script_alias)s] - - --gui (-g) Pop up a graphical interface for serving and - testing ViewVC. NOTE: this requires a valid - X11 display connection. """ % locals()) + sys.exit(0) + + +def badusage(errstr): + cmd = os.path.basename(sys.argv[0]) + sys.stderr.write("ERROR: %s\n\n" + "Try '%s --help' for detailed usage information.\n" + % (errstr, cmd)) + sys.exit(1) + + +def main(argv): + """Command-line interface (looks at argv to decide what to do).""" + import getopt + + short_opts = ''.join(['c:', + 'd', + 'h:', + 'p:', + 'r:', + 's:', + ]) + long_opts = ['daemon', + 'config-file=', + 'help', + 'host=', + 'htpasswd-file=', + 'port=', + 'repository=', + 'script-alias=', + ] + + opt_daemon = False + opt_host = None + opt_port = None + opt_htpasswd_file = None + opt_config_file = None + opt_script_alias = None + opt_repositories = [] + + # Parse command-line options. + try: + opts, args = getopt.getopt(argv[1:], short_opts, long_opts) + for opt, val in opts: + if opt in ['--help']: + usage() + elif opt in ['-r', '--repository']: # may be used more than once + opt_repositories.append(val) + elif opt in ['-d', '--daemon']: + opt_daemon = 1 + elif opt in ['-p', '--port']: + opt_port = val + elif opt in ['-h', '--host']: + opt_host = val + elif opt in ['-s', '--script-alias']: + opt_script_alias = val + elif opt in ['-c', '--config-file']: + opt_config_file = val + elif opt in ['--htpasswd-file']: + opt_htpasswd_file = val + except getopt.error, err: + badusage(str(err)) + + # Validate options that need validating. + class BadUsage(Exception): pass + try: + if opt_port is not None: + try: + options.port = int(opt_port) + except ValueError: + raise BadUsage("Port '%s' is not a valid port number" % (opt_port)) + if not options.port: + raise BadUsage("You must supply a valid port.") + if opt_htpasswd_file is not None: + if not os.path.isfile(opt_htpasswd_file): + raise BadUsage("'%s' does not appear to be a valid htpasswd file." + % (opt_htpasswd_file)) + if not has_crypt: + raise BadUsage("Unable to locate suitable `crypt' module for use " + "with --htpasswd-file option. If your Python " + "distribution does not include this module (as is " + "the case on many non-Unix platforms), consider " + "installing the `fcrypt' module instead (see " + "http://carey.geek.nz/code/python-fcrypt/).") + options.htpasswd_file = opt_htpasswd_file + if opt_config_file is not None: + if not os.path.isfile(opt_config_file): + raise BadUsage("'%s' does not appear to be a valid configuration file." + % (opt_config_file)) + options.config_file = opt_config_file + if opt_host is not None: + options.host = opt_host + if opt_script_alias is not None: + options.script_alias = '/'.join(filter(None, opt_script_alias.split('/'))) + for repository in opt_repositories: + if not options.repositories.has_key('Development'): + rootname = 'Development' + else: + rootname = 'Repository%d' % (len(options.repositories.keys()) + 1) + options.repositories[rootname] = repository + except BadUsage, err: + badusage(str(err)) + + # Fork if we're in daemon mode. + if opt_daemon: + pid = os.fork() + if pid != 0: + sys.exit() + + # Finaly, start the server. + def ready(server): + print 'server ready at %s%s' % (server.url, options.script_alias) + serve(options.host, options.port, ready) + if __name__ == '__main__': - options = Options() - cli(sys.argv) + options = Options() + main(sys.argv) diff --git a/bin/svndbadmin b/bin/svndbadmin index ff1df688..64a81a86 100755 --- a/bin/svndbadmin +++ b/bin/svndbadmin @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 2004-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 2004-2013 The ViewCVS Group. All Rights Reserved. # Copyright (C) 2004-2007 James Henstridge # # By using this file, you agree to the terms and conditions set forth in @@ -262,7 +262,7 @@ class SvnRev: fsroot = self._get_root_for_rev(rev) # find changes in the revision - editor = svn.repos.RevisionChangeCollector(repo.fs, rev) + editor = svn.repos.ChangeCollector(repo.fs, fsroot) e_ptr, e_baton = svn.delta.make_editor(editor) svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton) @@ -274,25 +274,32 @@ class SvnRev: continue # deal with the change types we handle + action = None base_root = None + base_path = change.base_path if change.base_path: base_root = self._get_root_for_rev(change.base_rev) - # :( f*cking undocumented python-subversion! - # change.action: modify=0, add=1, delete=2, replace=3 - if change.action == 2 or not change.path: + # figure out what kind of change this is, and get a diff + # object for it. note that prior to 1.4 Subversion's + # bindings didn't give us change.action, but that's okay + # because back then deleted paths always had a change.path + # of None. + if hasattr(change, 'action') \ + and change.action == svn.repos.CHANGE_ACTION_DELETE: + action = 'remove' + elif not change.path: action = 'remove' elif change.added: action = 'add' else: action = 'change' - diffobj = svn.fs.FileDiff(base_root and base_root or None, - base_root and change.base_path or None, - action != 'remove' and change.path and fsroot or None, - action != 'remove' and change.path and change.path or None, - None, - ['-b', '-B']) + if action == 'remove': + diffobj = svn.fs.FileDiff(base_root, change.base_path, None, None, None, ['-b', '-B']) + else: + diffobj = svn.fs.FileDiff(base_root, change.base_path, fsroot, change.path, None, ['-b', '-B']) + diff_fp = diffobj.get_pipe() diff_fp = StupidBufferedReader(diff_fp) plus, minus = _get_diff_counts(diff_fp) @@ -407,6 +414,7 @@ def main(command, repository, revs=[], verbose=0, force=0): db = cvsdb.ConnectDatabase(cfg) repository = os.path.realpath(repository) + # Purge what must be purged. if command in ('rebuild', 'purge'): if verbose: print "Purging commit info for repository root `%s'" % repository @@ -428,6 +436,7 @@ def main(command, repository, revs=[], verbose=0, force=0): svn_ignore_mimetype = cfg.options.svn_ignore_mimetype, verbose = verbose, ) + # Record what must be recorded. if command == 'rebuild' or (command == 'update' and not revs): for rev in range(repo.rev_max+1): handle_revision(db, command, repo, rev, verbose, force) @@ -456,7 +465,7 @@ def usage(): located at REPOS-PATH. Usage: 1. %s [-v] rebuild REPOS-PATH - 2. %s [-v] update REPOS-PATH [REV:[REV2]] [--force] + 2. %s [-v] update REPOS-PATH [REV[:REV2]] [--force] 3. %s [-v] purge REPOS-PATH 1. Rebuild the commit database information for the repository located @@ -500,17 +509,11 @@ if __name__ == '__main__': if len(args) < 3: usage() - command = string.lower(args[1]) + command = args[1].lower() if command not in ('rebuild', 'update', 'purge'): sys.stderr.write('ERROR: unknown command %s\n' % command) usage() - repository = args[2] - if not os.path.exists(repository): - sys.stderr.write('ERROR: could not find repository %s\n' % args[2]) - usage() - repository = vclib.svn.canonicalize_rootpath(repository) - revs = [] if len(sys.argv) > 3: if command == 'rebuild': @@ -533,6 +536,7 @@ if __name__ == '__main__': rev = None try: + repository = vclib.svn.canonicalize_rootpath(args[2]) repository = cvsdb.CleanRepository(os.path.abspath(repository)) main(command, repository, revs, verbose, force) except KeyboardInterrupt: diff --git a/bin/wsgi/query.fcgi b/bin/wsgi/query.fcgi new file mode 100644 index 00000000..5e37308e --- /dev/null +++ b/bin/wsgi/query.fcgi @@ -0,0 +1,54 @@ +#!/usr/bin/python +# -*-python-*- +# +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# viewvc: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a fcgi entry point for the query ViewVC app. It's appropriate +# for use with mod_fcgid and flup. It defines a single application function +# that is a valid fcgi entry point. +# +# mod_fcgid: http://httpd.apache.org/mod_fcgid/ +# flup: +# http://pypi.python.org/pypi/flup +# http://trac.saddi.com/flup +# +# ----------------------------------------------------------------------- + +import sys, os + +LIBRARY_DIR = None +CONF_PATHNAME = None + +if LIBRARY_DIR: + sys.path.insert(0, LIBRARY_DIR) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], + "../../../lib"))) + +import sapi +import viewvc +import query +from flup.server import fcgi + +def application(environ, start_response): + server = sapi.WsgiServer(environ, start_response) + cfg = viewvc.load_config(CONF_PATHNAME, server) + viewvc_base_url = cfg.query.viewvc_base_url + if viewvc_base_url is None: + viewvc_base_url = "viewvc.fcgi" + query.main(server, cfg, viewvc_base_url) + return [] + +fcgi.WSGIServer(application).run() diff --git a/bin/wsgi/query.wsgi b/bin/wsgi/query.wsgi new file mode 100644 index 00000000..f68382a9 --- /dev/null +++ b/bin/wsgi/query.wsgi @@ -0,0 +1,45 @@ +# -*-python-*- +# +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# viewvc: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a wsgi entry point for the query ViewVC app. It's appropriate +# for use with mod_wsgi. It defines a single application function that +# is a valid wsgi entry point. +# +# ----------------------------------------------------------------------- + +import sys, os + +LIBRARY_DIR = None +CONF_PATHNAME = None + +if LIBRARY_DIR: + sys.path.insert(0, LIBRARY_DIR) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], + "../../../lib"))) + +import sapi +import viewvc +import query + +def application(environ, start_response): + server = sapi.WsgiServer(environ, start_response) + cfg = viewvc.load_config(CONF_PATHNAME, server) + viewvc_base_url = cfg.query.viewvc_base_url + if viewvc_base_url is None: + viewvc_base_url = "viewvc.wsgi" + query.main(server, cfg, viewvc_base_url) + return [] diff --git a/bin/wsgi/viewvc.fcgi b/bin/wsgi/viewvc.fcgi new file mode 100644 index 00000000..28ba336d --- /dev/null +++ b/bin/wsgi/viewvc.fcgi @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*-python-*- +# +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# viewvc: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a fcgi entry point for the main ViewVC app. It's appropriate +# for use with mod_fcgid and flup. It defines a single application function +# that is a valid fcgi entry point. +# +# mod_fcgid: http://httpd.apache.org/mod_fcgid/ +# flup: +# http://pypi.python.org/pypi/flup +# http://trac.saddi.com/flup +# +# ----------------------------------------------------------------------- + +import sys, os + +LIBRARY_DIR = None +CONF_PATHNAME = None + +if LIBRARY_DIR: + sys.path.insert(0, LIBRARY_DIR) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], + "../../../lib"))) + +import sapi +import viewvc +from flup.server import fcgi + +def application(environ, start_response): + server = sapi.WsgiServer(environ, start_response) + cfg = viewvc.load_config(CONF_PATHNAME, server) + viewvc.main(server, cfg) + return [] + +fcgi.WSGIServer(application).run() diff --git a/bin/wsgi/viewvc.wsgi b/bin/wsgi/viewvc.wsgi new file mode 100644 index 00000000..d27d5cf5 --- /dev/null +++ b/bin/wsgi/viewvc.wsgi @@ -0,0 +1,41 @@ +# -*-python-*- +# +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# viewvc: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a wsgi entry point for the main ViewVC app. It's appropriate +# for use with mod_wsgi. It defines a single application function that +# is a valid wsgi entry point. +# +# ----------------------------------------------------------------------- + +import sys, os + +LIBRARY_DIR = None +CONF_PATHNAME = None + +if LIBRARY_DIR: + sys.path.insert(0, LIBRARY_DIR) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], + "../../../lib"))) + +import sapi +import viewvc + +def application(environ, start_response): + server = sapi.WsgiServer(environ, start_response) + cfg = viewvc.load_config(CONF_PATHNAME, server) + viewvc.main(server, cfg) + return [] diff --git a/conf/viewvc.conf.dist b/conf/viewvc.conf.dist index b36d3b02..13639d25 100644 --- a/conf/viewvc.conf.dist +++ b/conf/viewvc.conf.dist @@ -1,236 +1,305 @@ -#--------------------------------------------------------------------------- -# -# Configuration file for ViewVC (4IntraNet patched version) -# -# Information on ViewVC is located at the following web site: -# http://viewvc.org/ -# -#--------------------------------------------------------------------------- +##--------------------------------------------------------------------------- +## +## Configuration file for ViewVC (4IntraNet patched version) +## +## Information on ViewVC is located at the following web site: +## http://viewvc.org/ +## +##--------------------------------------------------------------------------- -# THE FORMAT OF THIS CONFIGURATION FILE -# -# This file is delineated by sections, specified in [brackets]. Within -# each section, are a number of configuration settings. These settings -# take the form of: name = value. Values may be continued on the -# following line by indenting the continued line. -# -# WARNING: Indentation *always* means continuation. Name=value lines -# should always start in column zero. -# -# Comments should always start in column zero, and are identified -# with "#". -# -# Certain configuration settings may have multiple values. These should -# be separated by a comma. The settings where this is allowed are noted -# below. Any other setting that requires special syntax is noted at that -# setting. -# -# -# SOME TERMINOLOGY USED HEREIN -# -# "root" - This is a CVS or Subversion repository. For Subversion, the -# meaning is pretty clear, as the virtual, versioned directory tree -# stored inside a Subversion repository looks nothing like the actual -# tree visible with shell utilities that holds the repository. For -# CVS, this is more confusing, because CVS's repository layout mimics -# (actually, defines) the layout of the stuff housed in the repository. -# But a CVS repository can be identified by the presence of a CVSROOT -# subdirectory in its root directory. -# -# "module" - A module is a top-level subdirectory of a root, usually -# associated with the concept of a single "project" among many housed -# within a single repository. -# -# -# BASIC VIEWVC CONFIGURATION HINTS -# -# While ViewVC has quite a few configuration options, you generally -# only need to change a small subset of them to get your ViewVC -# installation working properly. Here are some options that we -# recommend you pay attention to. Of course, don't try to change the -# options here -- do so in the relevant section of the configuration -# file below. -# -# For correct operation, you will probably need to change the following -# configuration variables: -# -# cvs_roots (for CVS) -# svn_roots (for Subversion) -# root_parents (for CVS or Subversion) -# default_root -# root_as_url_component -# rcs_dir -# mime_types_file -# the many options in the [utilities] section -# -# It is usually desirable to change the following variables: -# -# address -# forbidden -# -# To optimize delivery of ViewVC static files: -# -# docroot -# -# To customize the display of ViewVC for your site: -# -# template_dir -# the [templates] override section -# +## THE FORMAT OF THIS CONFIGURATION FILE +## +## This file is delineated by sections, specified in [brackets]. Within +## each section, are a number of configuration settings. These settings +## take the form of: name = value. Values may be continued on the +## following line by indenting the continued line. +## +## WARNING: Indentation *always* means continuation. Name=value lines +## should always start in column zero. +## +## Comments should always start in column zero, and are identified +## with "#". By default each of the configuration items is +## commented out, with the default value of the option shown. +## You'll need to remove the '#' that precedes configuration +## options whose values you wish to modify. +## +## Certain configuration settings may have multiple values. These should +## be separated by a comma. The settings where this is allowed are noted +## below. Any other setting that requires special syntax is noted at that +## setting. +## +## +## SOME TERMINOLOGY USED HEREIN +## +## "root" - This is a CVS or Subversion repository. For Subversion, the +## meaning is pretty clear, as the virtual, versioned directory tree +## stored inside a Subversion repository looks nothing like the actual +## tree visible with shell utilities that holds the repository. For +## CVS, this is more confusing, because CVS's repository layout mimics +## (actually, defines) the layout of the stuff housed in the repository. +## But a CVS repository can be identified by the presence of a CVSROOT +## subdirectory in its root directory. +## +## "module" - A module is a top-level subdirectory of a root, usually +## associated with the concept of a single "project" among many housed +## within a single repository. +## +## +## BASIC VIEWVC CONFIGURATION HINTS +## +## While ViewVC has quite a few configuration options, you generally +## only need to change a small subset of them to get your ViewVC +## installation working properly. Here are some options that we +## recommend you pay attention to. Of course, don't try to change the +## options here -- do so in the relevant section of the configuration +## file below. +## +## For correct operation, you will probably need to change the following +## configuration variables: +## +## cvs_roots (for CVS) +## svn_roots (for Subversion) +## root_parents (for CVS or Subversion) +## default_root +## root_as_url_component +## rcs_dir +## mime_types_files +## the many options in the [utilities] section +## +## It is usually desirable to change the following variables: +## +## address +## forbidden +## +## To optimize delivery of ViewVC static files: +## +## docroot +## +## To customize the display of ViewVC for your site: +## +## template_dir +## the [templates] override section +## -#--------------------------------------------------------------------------- +##--------------------------------------------------------------------------- [general] -# -# This setting specifies each of the CVS roots on your system and assigns -# names to them. Each root should be given by a "name: path" value. Multiple -# roots should be separated by commas and can be placed on separate lines. -# -cvs_roots = cvs: /home/cvsroot +## cvs_roots: Specifies each of the CVS roots on your system and +## assigns names to them. Each root should be given by a "name: path" +## value (where the path is an absolute filesystem path). Multiple roots +## should be separated by commas and can be placed on separate lines. +## +## Example: +## cvs_roots = cvsroot: /opt/cvs/repos1, +## anotherroot: /usr/local/cvs/repos2 +## +#cvs_roots = -# -# This setting specifies each of the Subversion roots (repositories) -# on your system and assigns names to them. Each root should be given -# by a "name: path" value. Multiple roots should be separated by -# commas and can be placed on separate lines. -# -#svn_roots = svn: /home/svnrepos +## svn_roots: Specifies each of the Subversion roots (repositories) on +## your system and assigns names to them. Each root should be given by +## a "name: path" value (where the path is an absolute filesystem path). +## Multiple roots should be separated by commas and can be placed on +## separate lines. +## +## NOTE: ViewVC offers *experimental* support for displaying remote +## Subversion repositories. Simply use the repository's URL instead +## of a local filesystem path when defining the root. +## +## Example: +## svn_roots = svnrepos: /opt/svn/, +## anotherrepos: /usr/local/svn/repos2 +## +#svn_roots = -# The 'root_parents' setting specifies a list of directories in which -# any number of repositories may reside. Rather than force you to add -# a new entry to 'cvs_roots' or 'svn_roots' each time you create a new -# repository, ViewVC rewards you for organising all your repositories -# under a few parent directories by allowing you to simply specifiy -# just those parent directories. ViewVC will then notice each -# repository in that directory as a new root whose name is the -# subdirectory of the parent path in which that repository lives. -# -# You can specify multiple parent paths separated by commas or new lines. -# -# Caution: these names can, of course, clash with names you have -# defined in your cvs_roots or svn_roots configuration items. If this -# occurs, you can either rename the offending repository on disk, or -# grant new names to the clashing item in cvs_roots or svn_roots. -# Each parent path is processed sequentially, so repositories under -# later parent paths may override earlier ones. -# -#root_parents = /home/svn-repositories : svn, -# /home/cvs-repositories : cvs +## root_parents: Specifies a list of directories under which any +## number of repositories may reside. You can specify multiple root +## parents separated by commas or new lines, each of which is of the +## form "path: type" (where the type is either "cvs" or "svn", and +## the path is an absolute filesystem path). +## +## Rather than force you to add a new entry to 'cvs_roots' or +## 'svn_roots' each time you create a new repository, ViewVC rewards +## you for organizing all your repositories under a few parent +## directories by allowing you to simply tell it about those parent +## directories. ViewVC will then notice each repository of the +## specified type in that directory as a root whose name is the +## subdirectory in which that repository lives. +## +## For example, if you have three Subversion repositories located at +## /opt/svn/projects, /opt/svn/websites, and /opt/svn/devstuff, you +## could list them individually in svn_roots like so: +## +## svn_roots = projects: /opt/svn/projects, +## websites: /opt/svn/websites, +## devstuff: /opt/svn/devstuff +## +## or you could instead use the root_parents configuration option: +## +## root_parents = /opt/svn: svn +## +## The benefit of this latter approach is that, as you add new +## repositories to your /opt/svn directory, they automatically become +## available for display in ViewVC without additional configuration. +## +## WARNING: the root names derived for repositories configured via the +## root_parents option can, of course, clash with names you have +## defined in your cvs_roots or svn_roots configuration items. If +## this occurs, you can either rename the offending repository on +## disk, or grant new names to the clashing item in cvs_roots or +## svn_roots. Each parent path is processed sequentially, so the +## names of repositories under later parent paths may override earlier +## ones. +## +## Example: +## root_parents = /opt/svn: svn, +## /opt/cvs: cvs +## +#root_parents = -# This is the name of the default root. Valid names include those -# explicitly listed in the cvs_roots and svn_roots configuration -# options, as well as those implicitly indicated by virtue of being -# the basenames of repositories found in the root_parents option -# locations. -# -# Note: This setting is ignored when root_as_url_component is enabled. -default_root = cvs +## default_root: This is the name of the default root. Valid names +## include those explicitly listed in the cvs_roots and svn_roots +## configuration options, as well as those implicitly indicated by +## virtue of being the basenames of repositories found in the +## root_parents option locations. +## +## NOTE: This setting is ignored when root_as_url_component is enabled. +## +## Example: +## default_root = cvsroot +## +#default_root = -# -# This is a pathname to a MIME types file to help viewvc to guess the -# correct MIME type on checkout. If you are having problems with the -# default guess on the MIME type, then uncomment this option and point -# it at a MIME type file. -# -# For example, you can use the mime.types provided by Apache here: -#mime_types_file = /usr/local/apache2/conf/mime.types +## mime_types_files: This is a list of pathnames to a set of MIME type +## mapping files to help ViewVC guess the correct MIME type of a +## versioned file. The pathnames listed here are specified in order of +## authoritativeness either as absolute paths or relative to this +## configuration file. +## +## As a convenience, ViewVC provides a MIME type mapping file +## (mimetypes.conf) which is, by default, the preferred provider of +## MIME type mapping answers, but which is also empty. If you find +## that ViewVC is unable to accurately guess MIME types based on the +## extensions of some of your versioned files, you can add records of +## your preferred mappings to the provided mimetypes.conf file (or to +## your system's mapping files, if you wish). +## +## You might, for example, wish to have ViewVC also consult the mapping +## files provided by your operating system and Apache. +## +## Example: +## mime_types_files = mimetypes.conf, +## /etc/mime.types, +## /usr/local/apache2/conf/mime.types +## +#mime_types_files = mimetypes.conf -# The address of the local repository maintainer. (This option is -# provided only as a convenience for ViewVC installations which are -# using the default template set, where the value of this option will -# be displayed in the footer of every ViewVC page.) -address = +## address: The address of the local repository maintainer. (This +## option is provided only as a convenience for ViewVC installations +## which are using the default template set, where the value of this +## option will be displayed in the footer of every ViewVC page.) +## +## Example: +## address = admin@server.com +## +#address = -# -# This option provides a mechanism for custom key/value pairs to be -# available to templates. These are stored in key/value (KV) files. -# -# The paths of the KV files are listed here, specified either as -# absolute paths or relative to this configuration file. The files -# use the same format as this configuration file, containing one or -# more user-defined sections, and user-defined options in those -# sections. ViewVC makes these options available to template authors -# as: -# -# kv.SECTION.OPTION -# -# Note that an option name can be dotted. For example: -# -# [my_images] -# logos.small = /images/small-logo.png -# logos.big = /images/big-logo.png -# -# Templates can use these with a directive like: [kv.my_images.logos.small] -# -# Note that section names which are common to multiple KV files will -# be merged. If two files have a [my_images] section, then the -# options in those two like-named sections will be merged together. -# If two files have the same option name in a section, then one will -# overwrite the other (and which one "wins" is unspecified). -# -# To further categorize the KV files, and how the values are provided to -# the templates, a KV file name may be annotated with an additional level -# of dotted naming. For example: -# -# kv_files = [asf]kv/images.conf -# -# Assuming the same section as above, the template would refer to an image -# using [kv.asf.my_images.logos.small] -# -# Lastly, it is possible to use %lang% in the filenames to specify a -# substitution of the selected language-tag. -# -# Example: -# kv_files = kv/file1.conf, kv/file2.conf, [i18n]kv/%lang%_data.conf -# -kv_files = +## kv_files: Provides a mechanism for custom key/value pairs to be +## available to templates. These are stored in key/value (KV) files. +## +## The paths of the KV files are listed here, specified either as +## absolute paths or relative to this configuration file. The files +## use the same format as this configuration file, containing one or +## more user-defined sections, and user-defined options in those +## sections. ViewVC makes these options available to template authors +## as: +## +## kv.SECTION.OPTION +## +## Note that an option name can be dotted. For example: +## +## [my_images] +## logos.small = /images/small-logo.png +## logos.big = /images/big-logo.png +## +## Templates can use these with a directive like: [kv.my_images.logos.small] +## +## Note that section names which are common to multiple KV files will +## be merged. If two files have a [my_images] section, then the +## options in those two like-named sections will be merged together. +## If two files have the same option name in a section, then one will +## overwrite the other (and which one "wins" is unspecified). +## +## To further categorize the KV files, and how the values are provided to +## the templates, a KV file name may be annotated with an additional level +## of dotted naming. For example: +## +## kv_files = [asf]kv/images.conf +## +## Assuming the same section as above, the template would refer to an image +## using [kv.asf.my_images.logos.small] +## +## Lastly, it is possible to use %lang% in the filenames to specify a +## substitution of the selected language-tag. +## +## Example: +## kv_files = kv/file1.conf, kv/file2.conf, [i18n]kv/%lang%_data.conf +## +#kv_files = -# -# This option is a comma-separated list of language-tag values -# available to ViewVC. The first language-tag listed is the default -# language, and will be used if an Accept-Language header is not -# present in the request, or none of the user's requested languages -# are available. If there are ties on the selection of a language, -# then the first to appear in the list is chosen. -# -# Some examples: -# -# languages = en-us, de -# languages = en-us, en-gb, de -# languages = de, fr, en-us -# -languages = en-us +## This option is a comma-separated list of language-tag values +## available to ViewVC. The first language-tag listed is the default +## language, and will be used if an Accept-Language header is not +## present in the request, or none of the user's requested languages +## are available. If there are ties on the selection of a language, +## then the first to appear in the list is chosen. +## +## Example: +## languages = en-us, en-gb, de +## +#languages = en-us -#--------------------------------------------------------------------------- +##--------------------------------------------------------------------------- [utilities] -# ViewVC uses (sometimes optionally) various third-party programs to do some -# of the heavy lifting. Generally, it will attempt to execute those utility -# programs in such a way that if they are found in ViewVC's executable -# search path ($PATH, %PATH%, etc.) all is well. But sometimes these tools -# aren't installed in the executable search path, so here's where you can -# tell ViewVC where to find them. -# -# NOTE: Options with a "_dir" suffix are for configuring the directories -# in which certain programs live; otherwise, the option value should -# point to the actual program. +## ViewVC uses (sometimes optionally) various third-party programs to do some +## of the heavy lifting. Generally, it will attempt to execute those utility +## programs in such a way that if they are found in ViewVC's executable +## search path ($PATH, %PATH%, etc.) all is well. But sometimes these tools +## aren't installed in the executable search path, so here's where you can +## tell ViewVC where to find them. +## +## NOTE: Options with a "_dir" suffix are for configuring the +## directories in which certain programs live. Note that this might +## not be the same directory into which the program's installer dumped +## the whole program package -- we want the deepest directory in which +## the executable program itself resides ("C:\rcstools\bin\win32" +## rather than just "C:\rcstools", for example). The values of options +## whose names lack the "_dir" suffix should point to the actual +## program itself (such as "C:\Program Files\cvsnt\cvs.exe"). -# RCS utilities, used for viewing CVS repositories -rcs_dir = -# rcs_dir = /usr/bin/ -# ViewVC can use CVSNT (www.cvsnt.org) instead of the RCS utilities to -# retrieve information from CVS repositories. To enable use of CVSNT, -# set the "cvsnt" value to the path of the CVSNT executable. (If CVSNT -# is on the standard path, you can also set it to the name of the -# CVSNT executable). By default "cvsnt" is set to "cvs" on Windows and -# is not set on other platforms. -cvsnt = -# cvsnt = -# cvsnt = cvs -# cvsnt = K:\Program Files\cvsnt\cvs.exe -# cvsnt = = /usr/bin/cvs +## rcs_dir: Directory in which the RCS utilities are installed, used +## for viewing CVS repositories. +## +## Example: +## rcs_dir = /usr/bin/ +## +#rcs_dir = + +## cvsnt: Location of cvsnt program. ViewVC can use CVSNT (www.cvsnt.org) +## instead of the RCS utilities to retrieve information from CVS +## repositories. To enable use of CVSNT, set the "cvsnt" value to the +## path of the CVSNT executable. (If CVSNT is on the standard path, you +## can also set it to the name of the CVSNT executable). By default +## "cvsnt" is set to "cvs" on Windows and is not set on other platforms. +## +## Examples: +## cvsnt = K:\Program Files\cvsnt\cvs.exe +## cvsnt = /usr/bin/cvs +## cvsnt = cvs +## +#cvsnt = # ViewVC can use an inetd service instead of local "cvsnt rcsfile" # This is recommended for all UNIX installations which use cvsnt, @@ -247,17 +316,21 @@ cvsnt = #rcsfile_socket = host:port # Example: rcsfile_socket = 127.0.0.1:8071 -# Subversion command-line client, used for viewing Subversion repositories -svn = -# svn = /usr/bin/svn +## diff: Location of the GNU diff program, used for showing file +## version differences. +## +## Example: +## diff = /usr/bin/diff +## +#diff = -# GNU diff, used for showing file version differences -diff = -# diff = /usr/bin/diff - -# CvsGraph, a graphical CVS version graph generator (see options.use_cvsgraph) -cvsgraph = -# cvsgraph = /usr/local/bin/cvsgraph +## cvsgraph: Location of the CvsGraph program, a graphical CVS version +## graph generator (see options.use_cvsgraph). +## +## Example: +## cvsgraph = /usr/local/bin/cvsgraph +## +#cvsgraph = # Apache Tika TCP server host and port, used to extract text from binary documents # Note that as of 2011-09-12, Tika 0.9 has a bug which leads to hangups on processing @@ -286,295 +359,492 @@ tika_mime_types = application/pdf application/rtf -#--------------------------------------------------------------------------- +##--------------------------------------------------------------------------- [options] -# root_as_url_component: Interpret the first path component in the URL -# after the script location as the root to use. This is an -# alternative to using the "root=" query key. If ViewVC is configured -# with multiple repositories, this results in more natural looking -# ViewVC URLs. -# Note: Enabling this option will break backwards compatibility with -# any old ViewCVS URL which doesn't have an explicit "root" parameter. -root_as_url_component = 1 +## root_as_url_component: Interpret the first path component in the URL +## after the script location as the root to use. This is an +## alternative to using the "root=" query key. If ViewVC is configured +## with multiple repositories, this results in more natural looking +## ViewVC URLs. +## +## NOTE: Enabling this option will break backwards compatibility with +## any old ViewCVS URL which doesn't have an explicit "root" parameter. +## +#root_as_url_component = 1 -# checkout_magic: Use checkout links with magic /*checkout*/ prefixes so -# checked out HTML pages can have working links to other repository files -# Note: This option is DEPRECATED and should not be used in new ViewVC -# installations. Setting "default_file_view = co" achieves the same effect -checkout_magic = 0 +## checkout_magic: Use checkout links with magic /*checkout*/ prefixes so +## checked out HTML pages can have working links to other repository files +## +## NOTE: This option is DEPRECATED and should not be used in new ViewVC +## installations. Setting "default_file_view = co" achieves the same effect +## +#checkout_magic = 0 -# allowed_views: List the ViewVC views which are enabled. Views not -# in this comma-delited list will not be served (or, will return an -# error on attempted access). -# Possible values: "annotate", "co", "diff", "markup", "roots", "tar" -allowed_views = annotate, diff, markup, roots +## allowed_views: List the ViewVC views which are enabled. Views not +## in this comma-delited list will not be served (or, will return an +## error on attempted access). +## +## Valid items for this list include: "annotate", "co", "diff", "markup", +## "roots", "tar". +## +## ----------+--------------------------------------------------------- +## VIEW | DESCRIPTION +## ----------+--------------------------------------------------------- +## annotate | The 'annotate' view shows the contents of a single +## | revision of a versioned file in exactly the same way as +## | the markup view, but with additional line-by-line +## | change attribution (the revision number, author, etc. +## | the most recent edit to that line of text as of the +## | displayed version). +## ----------+--------------------------------------------------------- +## co | The 'co' (aka "checkout" or "download") view isn't +## | really a branded view at all, but allows for direct +## | downloading of the contents of a single revision of a +## | versioned file. +## ----------+--------------------------------------------------------- +## diff | The 'diff' view displays line-based differences between +## | two revisions of a versioned file in a variety of +## | different user-selectable formats. +## ----------+--------------------------------------------------------- +## markup | The 'markup' view shows the contents of a single +## | revision of a versioned file, with syntax highlighting +## | where possible and enabled. It can also optionally +## | show change log information for that revision of the +## | file. +## ----------+--------------------------------------------------------- +## roots | The 'roots' view is a simple listing of the various +## | repositories which ViewVC has been configured to serve +## | to users. +## ----------+--------------------------------------------------------- +## tar | The 'tar' view isn't a branded view, but generates +## | a GNU Tar archive file containing a single versioned +## | directory and its contents (recursively). +## ----------+--------------------------------------------------------- +## +#allowed_views = annotate, diff, markup, roots -# authorizer: The name of the ViewVC authorizer plugin to use when -# authorizing access to repository contents. This value must be the -# name of a Python module addressable as vcauth.MODULENAME (most -# easily accomplished by placing it in ViewVC's lib/vcauth/ directory) -# and which implements a ViewVCAuthorizer class (as a subclass of -# vcauth.GenericViewVCAuthorizer). You can provide custom parameters -# to the authorizer module by defining configuration sections named -# authz-MODULENAME and adding the parameter keys and values there. +## Comma-delimited list of MIME content types (with support for fnmatch- +## style glob characters) which are considered not-human-readable and for +## which ViewVC will neither generate links to, nor support the direct +## display of, non-checkout views which carry the file's content (the +## 'markup', 'annotate', 'diff', and 'patch' views). +## +## NOTE: Handling of this option is given priority over ViewVC's +## longstanding support for showing web-friendly file formats -- even +## binary ones such as "image/jpeg" and "image/gif" -- in the 'markup' +## view. Thus, if you add "image/*" to this list, 'markup'-view +## display of JPEG, GIF, and PNG images will be disabled. +## +## Example: +## binary_mime_types = application/octet-stream, image/*, application/pdf, +## application/vnd*, application/msword, audio/* # -# ViewVC provides the following modules: -# svnauthz - based on Subversion authz files -# forbidden - simple path glob matches against top-level root directories -# forbiddenre - root and path matches against regular expressions +#binary_mime_types = + +## authorizer: The name of the ViewVC authorizer plugin to use when +## authorizing access to repository contents. This value must be the +## name of a Python module addressable as vcauth.MODULENAME (most +## easily accomplished by placing it in ViewVC's lib/vcauth/ directory) +## and which implements a ViewVCAuthorizer class (as a subclass of +## vcauth.GenericViewVCAuthorizer). You can provide custom parameters +## to the authorizer module by defining configuration sections named +## authz-MODULENAME and adding the parameter keys and values there. +## +## ViewVC provides the following modules: +## svnauthz - based on Subversion authz files +## forbidden - simple path glob matches against top-level root directories +## forbiddenre - root and path matches against regular expressions +## +## NOTE: Only one authorizer may be in use for a given ViewVC request. +## It doesn't matter if you configure the parameters of multiple +## authorizer plugins -- only the authorizer whose name is configured +## here (or effectively configured here via per-vhost or per-root +## configuration) will be activated. +## +#authorizer = + +## hide_cvsroot: Don't show the CVSROOT directory +## 1 Hide CVSROOT directory +## 0 Show CVSROOT directory +## +## NOTE: Someday this option may be removed in favor of letting +## individual authorizer plugin hide the CVSROOT. +## +#hide_cvsroot = 1 + +## mangle_email_addresses: Mangle email addresses in marked-up output. +## There are various levels of mangling available: +## 0 - No mangling; markup un-mangled email addresses as hyperlinks +## 1 - Obfuscation (using entity encoding); no hyperlinking +## 2 - Data-dropping address truncation; no hyperlinking +## +## NOTE: this will not effect the display of versioned file contents, only +## addresses that appear in version control metadata (e.g. log messages). +## +#mangle_email_addresses = 0 + +## custom_log_formatting: Specifies mappings of regular expressions to +## substitution format strings used to URL-ize strings found in +## revision log messages. Multiple mappings (specified as +## REGEXP:FORMATSTRING) may be defined, separated by commas. +## +## NOTE: Due to a limitation of the configuration format, commas may +## not be used in the regular expression portion of each mapping. +## Commas "in the raw" may not be used in the format string portion, +## either, but you can probably use the URI-encoded form of the comma +## ("%2C") instead with no ill side-effects. If you must specify a +## colon character in either the regular expression or the format +## string, escape it with a preceding backslash ("\:"). +## +## Example: +## custom_log_formatting = +## artf[0-9]+ : http://example.com/tracker?id=\0, +## issue ([0-9]+) : http://example.com/bug?id=\1&opts=full%2csortby=id +## +#custom_log_formatting = + +## default_file_view: "log", "co", or "markup" +## Controls whether the default view for file URLs is a checkout view or +## a log view. "log" is the default for backwards compatibility with old +## ViewCVS URLs, but "co" has the advantage that it allows ViewVC to serve +## static HTML pages directly from a repository with working links +## to other repository files +## +## NOTE: Changing this option may break compatibility with existing +## bookmarked URLs. +## +## ALSO NOTE: If you choose one of the "co" or "markup" views, be sure +## to enable it (via the allowed_views option) +## +#default_file_view = log + +## http_expiration_time: Expiration time (in seconds) for cacheable +## pages served by ViewVC. Note that in most cases, a cache aware +## client will only revalidate the page after it expires (using the +## If-Modified-Since and/or If-None-Match headers) and that browsers +## will also revalidate the page when the reload button is pressed. +## Set to 0 to disable the transmission of these caching headers. +## +#http_expiration_time = 600 + +## generate_etags: Generate Etag headers for relevant pages to assist +## in browser caching. +## 1 Generate Etags +## 0 Don't generate Etags +## +#generate_etags = 1 + +## svn_ignore_mimetype: Don't consult the svn:mime-type property to +## determine how to display a file in the markup view. This is +## especially helpful when versioned images carry the default +## Subversion-calculated MIME type of "application/octet-stream" (which +## isn't recognized as viewable type by browsers). +## +#svn_ignore_mimetype = 0 + +## max_filesize_kbytes: Limit ViewVC's processing of file contents in +## "markup" and "annotate" views to only those files which are smaller +## than this setting, expressed in kilobytes. Set to 0 to disable +## this safeguard. +## +## NOTE: The "co" and "tar" views are unaffected by this setting. +## +#max_filesize_kbytes = 512 + +## svn_config_dir: Path of the Subversion runtime configuration +## directory ViewVC should consult for various things, including cached +## remote authentication credentials. If unset, Subversion will use +## the default location(s) ($HOME/.subversion, etc.) +## +#svn_config_dir = + +## use_rcsparse: Use the rcsparse Python module to retrieve CVS +## repository information instead of invoking rcs utilities [EXPERIMENTAL] +## +#use_rcsparse = 0 + +## sort_by: File sort order +## file Sort by filename +## rev Sort by revision number +## date Sort by commit date +## author Sort by author +## log Sort by log message +## +#sort_by = file + +## sort_group_dirs: Group directories when sorting +## 1 Group directories together +## 0 No grouping -- sort directories as any other item would be sorted +## +#sort_group_dirs = 1 + +## hide_attic: Hide or show the contents of the Attic subdirectory +## 1 Hide dead files inside Attic subdir +## 0 Show the files which are inside the Attic subdir +## +#hide_attic = 1 + +## hide_errorful_entries: Hide or show errorful directory entries +## (perhaps due to not being readable, or some other rlog parsing +## error, etc.) +## 1 Hide errorful entries from the directory display +## 0 Show errorful entries (with their errors) in the directory display +## +#hide_errorful_entries = 0 + +## log_sort: Sort order for log messages +## date Sort revisions by date +## rev Sort revision by revision number +## none Use the version control system's ordering +## +#log_sort = date + +## diff_format: Default diff format +## h Human readable +## u Unified diff +## c Context diff +## s Side by side +## l Long human readable (more context) +## f Full human readable (entire file) +## +#diff_format = h + +## hr_breakable: Diff view line breaks +## 1 lines break at spaces +## 0 no line breaking +## Or, use a positive integer > 1 to cut lines after that many characters +## +#hr_breakable = 1 + +## hr_funout: Give out function names in human readable diffs. +## (Only works well for C source files, otherwise diff's heuristic falls short.) +## ('-p' option to diff) +## +#hr_funout = 1 + +## hr_ignore_white: Ignore whitespace (indendation and stuff) for human +## readable diffs. +## ('-w' option to diff) +## +#hr_ignore_white = 0 + +## hr_ignore_keyword_subst: Ignore diffs which are caused by keyword +## substitution (such as "$Id - Stuff"). +## ('-kk' option to rcsdiff) +## +#hr_ignore_keyword_subst = 1 + +## hr_intraline: Enable highlighting of intraline changes in human +## readable diffs. [Requires Python 2.4] +## +#hr_intraline = 0 + +## allow_compress: Allow compression via gzip of output if the Browser +## accepts it (HTTP_ACCEPT_ENCODING contains "gzip"). +## +## NOTE: this relies on Python's gzip module, which has proven to be +## not-so-performant. Enabling this feature should reduce the overall +## transfer size of ViewVC's responses to the client's request, but +## will do so with a speed penalty. +## +#allow_compress = 0 + +## template_dir: The directory which contains the EZT templates used by +## ViewVC to customize the display of the various output views. ViewVC +## looks in this directory for files with names that match the name of +## the view ("log", "directory", etc.) plus the ".ezt" extension. If +## specified as a relative path, it is relative to the directory where +## this config file resides; absolute paths may be used as well. If +## %lang% occurs in the pathname, then the selected language will be +## substituted. +## +## SEE ALSO: the [templates] configuration section, where you can +## override templates on a per-view basis. +## +## Example: +## template_dir = templates/classic +## template_dir = templates/default +## template_dir = templates-contrib/custom/templates # -# NOTE: Only one authorizer may be in use for a given ViewVC request. -# It doesn't matter if you configure the parameters of multiple -# authorizer plugins -- only the authorizer whose name is configured -# here (or effectively configured here via per-vhost or per-root -# configuration) will be activated. -authorizer = +#template_dir = templates/default -# hide_cvsroot: Don't show the CVSROOT directory -# 1 Hide CVSROOT directory -# 0 Show CVSROOT directory -# NOTE: Someday this option may be removed in favor of letting -# individual authorizer plugin hide the CVSROOT. -hide_cvsroot = 1 +## docroot: Web path to a directory that contains ViewVC static files +## (stylesheets, images, etc.) If set, static files will get +## downloaded directory from this location. If unset, static files +## will be served by the ViewVC script (at a likely performance +## penalty, and from the "docroot" subdirectory of the directory +## specified by the "template_dir" option). +## +## NOTE: This option is evaluated outside the context of a particular +## root. Be careful when using per-root configuration to select an +## alternate template set as the default value for this option will +## still be based on the global default template set per 'template_dir' +## above, not on 'template_dir' as overridden for a given root. +## +#docroot = -# mangle_email_addresses: Mangle email addresses in marked-up output. -# There are various levels of mangling available: -# 0 - No mangling; markup un-mangled email addresses as hyperlinks -# 1 - Obfuscation (using entity encoding); no hyperlinking -# 2 - Data-dropping address truncation; no hyperlinking -# Note: this will not effect the display of versioned file contents, only -# addresses that appear in version control metadata (e.g. log messages). -mangle_email_addresses = 0 +## show_subdir_lastmod: Show last changelog message for CVS subdirectories +## +## NOTE: The current implementation makes many assumptions and may show +## the incorrect file at some times. The main assumption is that the +## last modified file has the newest filedate. But some CVS operations +## touches the file without even when a new version is not checked in, +## and TAG based browsing essentially puts this out of order, unless +## the last checkin was on the same tag as you are viewing. Enable +## this if you like the feature, but don't rely on correct results. +## +## SECURITY WARNING: Enabling this will currently leak unauthorized +## path names. +## +#show_subdir_lastmod = 0 -# default_file_view: "log", "co", or "markup" -# Controls whether the default view for file URLs is a checkout view or -# a log view. "log" is the default for backwards compatibility with old -# ViewCVS URLs, but "co" has the advantage that it allows ViewVC to serve -# static HTML pages directly from a repository with working links -# to other repository files -# Note: Changing this option may break compatibility with existing -# bookmarked URLs. -# Also note: If you choose one of the "co" or "markup" views, be sure -# to enable it (via the allowed_views option) -default_file_view = log +## show_roots_lastmod: In the root listing view, show the most recent +## modifications made to the root. (Subversion roots only.) +## +## NOTE: Enabling this feature will significantly reduce the +## performance of the root listing view. +## +#show_roots_lastmod = 0 -# http_expiration_time: Expiration time (in seconds) for cacheable -# pages served by ViewVC. Note that in most cases, a cache aware -# client will only revalidate the page after it expires (using the -# If-Modified-Since and/or If-None-Match headers) and that browsers -# will also revalidate the page when the reload button is pressed. -# Set to 0 to disable the transmission of these caching headers. -http_expiration_time = 600 +## show_logs: Show the most recent log entry in directory listings. +## +#show_logs = 1 -# generate_etags: Generate Etag headers for relevant pages to assist -# in browser caching. -# 1 Generate Etags -# 0 Don't generate Etags -generate_etags = 1 +## show_log_in_markup: Show log when viewing file contents. +## +#show_log_in_markup = 1 -# Don't use the svn:mime-type property to determine how to display a -# file in the markup view. This is especially helpful when versioned -# images carry the default Subversion-calculated MIME type of -# "application/octet-stream" (which isn't recognized as viewable type -# by browsers). -svn_ignore_mimetype = 0 +## cross_copies: Cross filesystem copies when traversing Subversion +## file revision histories. +## +#cross_copies = 1 -# svn_config_dir: Path of the Subversion runtime configuration -# directory ViewVC should consult for various things, including cached -# remote authentication credentials. If unset, Subversion will use -# the default location(s) ($HOME/.subversion, etc.) -svn_config_dir = +## use_localtime: Display dates as UTC or in local time zone. +## +#use_localtime = 0 -# use the rcsparse Python module to retrieve CVS repository -# information instead of invoking rcs utilities [EXPERIMENTAL] -use_rcsparse = 0 +## iso8601_dates: Display timestamps using a standard ISO-8601 format. +## +#iso8601_timestamps = 0 -# sort_by: File sort order -# file Sort by filename -# rev Sort by revision number -# date Sort by commit date -# author Sort by author -# log Sort by log message -sort_by = file +## short_log_len: The length (in characters) to which the most recent +## log entry should be truncated when shown in the directory view. +## +#short_log_len = 80 -# sort_group_dirs: Group directories when sorting -# 1 Group directories together -# 0 No grouping -- sort directories as any other item would be sorted -sort_group_dirs = 1 +## enable_syntax_coloration: Should we colorize known file content +## syntaxes? +## +## NOTE: This feature requires the Pygments Python module +## (http://pygments.org) and works only when ViewVC can determine the +## MIME content type of the file whose contents it wishes to colorize. +## Use the 'mime_types_files' configuration option to specify MIME +## type mapping files useful for making that determination. +## +#enable_syntax_coloration = 1 -# hide_attic: Hide or show the contents of the Attic subdirectory -# 1 Hide dead files inside Attic subdir -# 0 Show the files which are inside the Attic subdir -hide_attic = 1 +## tabsize: The number of spaces into which horizontal tab characters +## are converted when viewing file contents. Set to 0 to preserve +## tab characters. +## +#tabsize = 8 -# hide_errorful_entries: Hide or show errorful directory entries -# (perhaps due to not being readable, or some other rlog parsing -# error, etc.) -# 1 Hide errorful entries from the directory display -# 0 Show errorful entries (with their errors) in the directory display -hide_errorful_entries = 0 - -# log_sort: Sort order for log messages -# date Sort revisions by date -# rev Sort revision by revision number -# none Use the version control system's ordering -log_sort = date - -# diff_format: Default diff format -# h Human readable -# u Unified diff -# c Context diff -# s Side by side -# l Long human readable (more context) -# f Full human readable (entire file) -diff_format = h - -# hr_breakable: Diff view line breaks -# 1 lines break at spaces -# 0 no line breaking -# Or, use a positive integer > 1 to cut lines after that many characters -hr_breakable = 1 - -# give out function names in human readable diffs -# this just makes sense if we have C-files, otherwise -# diff's heuristic doesn't work well .. -# ( '-p' option to diff) -hr_funout = 0 - -# ignore whitespaces for human readable diffs -# (indendation and stuff ..) -# ( '-w' option to diff) -hr_ignore_white = 0 - -# ignore diffs which are caused by -# keyword-substitution like $Id - Stuff -# ( '-kk' option to rcsdiff) -hr_ignore_keyword_subst = 1 - -# Enable highlighting of intraline changes in human readable diffs -# this feature is experimental and currently requires python 2.4 -hr_intraline = 0 - -# allow compression with gzip of output if the Browser accepts it -# (HTTP_ACCEPT_ENCODING contains "gzip") -#allow_compress = 1 - -# The directory which contains the EZT templates used by ViewVC to -# customize the display of the various output views. ViewVC looks in -# this directory for files with names that match the name of the view -# ("log", "directory", etc.) plus the ".ezt" extension. If specified -# as a relative path, it is relative to the directory where this config -# file resides; absolute paths may be used as well. -# -# If %lang% occurs in the pathname, then the selected language will be -# substituted. -# -# See also the [templates] configuration section, where you can -# override templates on a per-view basis. -# -template_dir = templates - -# Web path to a directory that contains ViewVC static files -# (stylesheets, images, etc.) If set, static files will get -# downloaded directory from this location. If unset, static files -# will be served by the ViewVC script (at a likely performance -# penalty, and from the "docroot" subdirectory of the directory -# specified by the "template_dir" option). -#docroot = /docroot - -# Show last changelog message for CVS subdirectories -# NOTE: The current implementation makes many assumptions and may show -# the incorrect file at some times. The main assumption is that the -# last modified file has the newest filedate. But some CVS operations -# touches the file without even when a new version is not checked in, -# and TAG based browsing essentially puts this out of order, unless -# the last checkin was on the same tag as you are viewing. Enable -# this if you like the feature, but don't rely on correct results. -# -# ** WARNING: Enabling this will currently leak unauthorized path names ** -show_subdir_lastmod = 0 - -# Show the most recent log entry in directory listings. -show_logs = 1 - -# Show log when viewing file contents -show_log_in_markup = 1 - -# Cross filesystem copies when traversing Subversion file revision histories. -cross_copies = 1 - -# Display dates as UTC or in local time zone -use_localtime = 0 -#use_localtime = 1 - -### CONFIGURATION DEFAULTS ### -### -### Defaults for configuration variables that shouldn't need -### to be configured.. - -# the length to which the most recent log entry should be truncated when -# shown in the directory view -short_log_len = 80 - -# should we colorize known file content syntaxes? (requires Pygments module) -enable_syntax_coloration = 1 - -# detect_encoding: Should we attempt to detect versioned file -# character encodings? [Requires 'chardet' module] -# Used in file list, file content display and indexing -# See also options.encodings for naive guessing. +## detect_encoding: Should we attempt to detect versioned file +## character encodings? [Requires 'chardet' module, and is currently +## used only for the 'markup' and 'annotate' views.] +## detect_encoding = 1 -# Use CvsGraph. See http://www.akhphd.au.dk/~bertho/cvsgraph/ for -# documentation and download. -use_cvsgraph = 0 -#use_cvsgraph = 1 +## use_cvsgraph: Use CvsGraph to offer visual graphs of CVS revision history. +## +#use_cvsgraph = 0 -# Location of the customized cvsgraph configuration file. -cvsgraph_conf = cvsgraph.conf +## cvsgraph_conf: Location of the customized cvsgraph configuration file. +## May be specified as an absolute path or as a path relative to this +## configuration file. +## +#cvsgraph_conf = cvsgraph.conf -# -# Set to enable regular expression search of all files in a directory -# -# WARNING: -# -# Enabling this option can consume HUGE amounts of server time. A -# "checkout" must be performed on *each* file in a directory, and -# the result needs to be searched for a match against the regular -# expression. -# -# -# SECURITY WARNING: Denial Of Service -# -# Since a user can enter the regular expression, it is possible for -# them to enter an expression with many alternatives and a lot of -# backtracking. Executing that search over thousands of lines over -# dozens of files can easily tie up a server for a long period of -# time. -# -# This option should only be used on sites with trusted users. It is -# highly inadvisable to use this on a public site. -# -use_re_search = 0 -# use_re_search = 1 +## allowed_cvsgraph_useropts: A list of settings used for cvsgraph's +## user-modifiable behavior which can be changed in the graph display. +## This value is a comma-delimited list of features, taken from the +## following set: +## invert - Display graph upside down +## branchbox - Add a branchbox at the tip of each branch +## show - Display user-selected classes of revisions +## rotate - Draw the tree left-to-right or top-to-bottom [*] +## limittags - Allow limit on the maximum number of tags displayed +## +## If this option is left unset, users will not be permitted to modify +## the graph display behavior. +## +## [*] WARNING: The 'rotate' option is known to cause some browsers to +## crash due, presumably, to the display of excessively wide images. +## +## Example: +## allowed_cvsgraph_useropts = limittags, show +## +## +#allowed_cvsgraph_useropts = -# -# Split directories and logs into pages. -# Allows ViewVC to present discrete pages to the users instead of the -# entire log or directory. -# Set use_pagesize to the number of entries you want displayed on a page. -# -use_pagesize = 0 -# use_pagesize = 20 +## use_re_search: Enable regular expression search of files in a directory. +## +## WARNING: Enabling this option can consume HUGE amounts of server +## time. A "checkout" must be performed on *each* file in a directory, +## and the result needs to be searched for a match against the regular +## expression. +## +## SECURITY WARNING: Since a user can enter the regular expression, it +## is possible for them to enter an expression with many alternatives +## and a lot of backtracking. Executing that search over thousands of +## lines over dozens of files can easily tie up a server for a long +## period of time. This option should only be used on sites with +## trusted users. It is highly inadvisable to use this on a public site. +## +#use_re_search = 0 -# Limit number of changed paths shown per commit in the Subversion revision -# view and in query results. This is not a hard limit (the UI provides -# options to show all changed paths), but it prevents ViewVC from generating -# enormous and hard to read pages by default when they happen to contain -# import or merge commits affecting hundreds or thousands of files. -# Set to 0 to disable the limit. -limit_changes = 100 +## dir_pagesize: Maximum number of directory entries on a given page. +## This allows ViewVC to present discrete pages to the users instead of +## the entire directory. Set to 0 to disable pagination. +## +#dir_pagesize = 0 + +## log_pagesize: Maximum number of revision log entries on a given page. +## This allows ViewVC to present discrete pages to the users instead of +## the entire revision log. Set to 0 to disable pagination. +## +#log_pagesize = 0 + +## log_pagesextra: Maximum number of extra pages (based on +## log_pagesize) of revision log data to fetch and present to the user +## as additional options for display. Revision log information +## "beyond" this window is still accessible, but must be navigated to +## in multiple steps. +## +## Example: +## log_pagesize = 100 +## log_pagesextra = 3 +## +## For a versioned file with 1000 revisions, the above settings would +## present to the user the first 100 of those 1000 revisions, with +## links to three additional pages (the 200-299th revisions, 300-399th +## revisions, and 400-499th revisions) plus a link to the 500th +## revision. Following these links slides the display "window", +## showing the requested set of revisions plus links to three +## additional pages beyond those, and so on. +## +#log_pagesextra = 3 + +## limit_changes: Maximum number of changed paths shown per commit in +## the Subversion revision view and in query results. This is not a +## hard limit (the UI provides options to show all changed paths), but +## it prevents ViewVC from generating enormous and hard to read pages +## by default when they happen to contain import or merge commits +## affecting hundreds or thousands of files. Set to 0 to disable the +## limit. +## +#limit_changes = 100 # You can also use primitive charset guessing instead of chardet (options.detect_encoding) # Just set this to the list of possible charsets in your repository. @@ -587,74 +857,116 @@ limit_changes = 100 # Just set to cp1251 if you work with your CVS from Windowz. #cvs_ondisk_charset = cp1251 -#--------------------------------------------------------------------------- +##--------------------------------------------------------------------------- [templates] -# You can override the templates used by various ViewVC views in this -# section. By default, ViewVC will look for templates in the -# directory specified by the "template_dir" configuration option (see -# the documentation for that option for details). But if you want to -# use a different template for a particular view, simply uncomment the -# appropriate option below and specify the currect location of the EZT -# template file you wish to use for that view. -# -# Templates are specified relative to the configured template -# directory (see the "template_dir" option), but absolute paths may -# also be used as well. -# -# If %lang% occurs in the pathname, then the selected language will be -# substituted. -# -# Note: the selected language is defined by the "languages" item in the -# [general] section, and based on the request's Accept-Language -# header. -# -#diff = diff.ezt -#directory = directory.ezt -### an alternative directory view -#directory = dir_new.ezt -#error = error.ezt -#file = file.ezt -#graph = graph.ezt -#log = log.ezt -### a table-based alternative log view -#log = log_table.ezt -#query = query.ezt -#query_form = query_form.ezt -#query_results = query_results.ezt -#revision = revision.ezt -#roots = roots.ezt +## You can override the templates used by various ViewVC views in this +## section. By default, ViewVC will look for templates in the +## directory specified by the "template_dir" configuration option (see +## the documentation for that option for details). But if you want to +## use a different template for a particular view, simply uncomment the +## appropriate option below and specify the currect location of the EZT +## template file you wish to use for that view. +## +## Templates are specified relative to the configured template +## directory (see the "template_dir" option), but absolute paths may +## also be used as well. +## +## If %lang% occurs in the pathname, then the selected language will be +## substituted. +## +## NOTE: the selected language is defined by the "languages" item in the +## [general] section, and based on the request's Accept-Language +## header. +## -#--------------------------------------------------------------------------- +## diff: Template used for the file differences view. +## +#diff = + +## directory: Template used for the directory listing view. +## +#directory = + +## error: Template used for the ViewVC error display view. +## +#error = + +## file: Template used for the file contents/annotation view. +## +#file = + +## graph: Template used for the revision graph view. +## +#graph = + +## log: Template used for the revision log view. +## +#log = + +## query: Template used for the non-integrated query interface. +## +#query = + +## query_form: Template used for the query form view. +## +#query_form = + +## query_results: Template used for the query results view. +## +#query_results = + +## revision: Template used for the revision/changeset view. +## +#revision = + +## roots: Template used for the root listing view. +## +#roots = + +##--------------------------------------------------------------------------- [cvsdb] -# Set to 1 to enable the database integration feature, 0 otherwise. -enabled = 0 +## enabled: Enable database integration feature. +## +#enabled = 0 -# Set to 1 to enable indexing of file contents using Sphinx and Tika -index_content = 0 +## host: Database hostname. Leave unset to use a local Unix socket +## connection. +## +#host = -# Set to limit stored text file content size (max. 4MB - Sphinx limit) -#content_max_size = 4194304 - -# Database hostname, port, and socket -#host = localhost +## post: Database listening port. +## #port = 3306 -# On Debian Linux, enable this: + +## socket: Database listening socket. +## On Debian Linux, enable this: #socket = /var/run/mysqld/mysqld.sock -# ViewVC database name. +## database_name: ViewVC database name. +## #database_name = ViewVC -# Username and password of user with read/write privileges to the ViewVC -# database. -#user = -#passwd = +## user: Username of user with read/write privileges to the database +## specified by the 'database_name' configuration option. +## +#user = -# Username and password of user with read privileges to the ViewVC -# database. -#readonly_user = -#readonly_passwd = +## passwd: Password of user with read/write privileges to the database +## specified by the 'database_name' configuration option. +## +#passwd = + +## readonly_user: Username of user with read privileges to the database +## specified by the 'database_name' configuration option. +## +#readonly_user = + +## readonly_passwd: Password of user with read privileges to the database +## specified by the 'database_name' configuration option. +## +#readonly_passwd = # ViewVC can use Sphinx (http://sphinxsearch.com) full-text search engine # to index file contents with full history and then search over them. @@ -673,6 +985,12 @@ index_content = 0 # rt_attr_uint = revision # rt_attr_uint = branchid +# Set to 1 to enable indexing of file contents using Sphinx and Tika +index_content = 0 + +# Set to limit stored text file content size (max. 4MB - Sphinx limit) +#content_max_size = 4194304 + # Sphinx connection parameters: #sphinx_host = #sphinx_port = @@ -695,202 +1013,292 @@ sphinx_snippet_options = after_match: chunk_separator: ... \n -# Limit the number of rows returned by a given query to this number. +## row_limit: Maximum number of rows returned by a given normal query +## to the database. +## +## NOTE: This limits the amount of data provided to ViewVC by the +## database. It is from this already-reduced data set that ViewVC +## builds the query response it presents to the user, which may or may +## not include still more limiting via the query form's 'limit' +## parameter. In other words, there is no value which the user can use +## in the query form's 'limit' parameter which will cause more data to +## be returned by the database for ViewVC to process. +## #row_limit = 1000 -# Limit the number of rows returned by a given query made as part an -# RSS feed request to this number. (Keeping in mind that RSS readers -# tend to poll regularly for new data, you might want to keep this set -# to a conservative number.) +## rss_row_limit: Maximum number of rows returned by a given query to +## the database made as part of an RSS feed request. (Keeping in mind +## that RSS readers tend to poll regularly for new data, you might want +## to keep this set to a conservative number.) +## +## See also the `NOTE' for the 'row_limit' option, which applies here +## as well. +## #rss_row_limit = 100 -# Check if the repository is found in the database before showing -# the query link and RSS feeds. Set to 1 to enable check. -# -# WARNING: Enabling this check adds the cost of a database connection -# and query to most ViewVC requests. If all your roots are represented -# in the commits database, or if you don't care about the creation of -# RSS and query links that might lead ultimately to error pages for -# certain of your roots, or if you simply don't want to add this extra -# cost to your ViewVC requests, leave this disabled. +## check_database_for_root: Check if the repository is found in the +## database before showing the query link and RSS feeds. +## +## WARNING: Enabling this check adds the cost of a database connection +## and query to most ViewVC requests. If all your roots are represented +## in the commits database, or if you don't care about the creation of +## RSS and query links that might lead ultimately to error pages for +## certain of your roots, or if you simply don't want to add this extra +## cost to your ViewVC requests, leave this disabled. +## #check_database_for_root = 0 -#--------------------------------------------------------------------------- +##--------------------------------------------------------------------------- [vhosts] -# Virtual hosts are individual logical servers accessible via -# different hostnames, but which are all really the same physical -# computer. For example, you might have your web server configured to -# accept incoming traffic for both http://www.yourdomain.com/ and -# http://viewvc.yourdomain.com/. Users pointing their web browsers at -# each of those two URLs might see entirely different content via one -# URL versus the other, but all that content actually lives on the -# same computer, is served up via the same web server, and so -# on. It just *looks* like its coming from multiple servers. -# -# ViewVC allows you to customize its configuration options for -# individual virtual hosts. You might, for example, wish to expose -# all of your Subversion repositories at http://svn.yourdomain.com/viewvc/ -# and all your CVS ones at http://cvs.yourdomain.com/viewvc/, with no -# cross-exposure. Using ViewVC's virtual host (vhost) configuration -# support, you can do this. Simply create two vhost configurations -# (one for each of your hostnames), then configure the cvs_roots -# option only for the vhost associated with cvs.yourdomain.com, and -# configure the svn_roots option only for the vhost associated with -# svn.yourdomain.com. -# -# This section is a freeform configuration section, where you create -# both the option names and their values. The names of the options -# are then treated as canonical names of virtual hosts, and their -# values are defined to be comma-delimited lists of hostname globs -# against which incoming ViewVC requests will be matched to figure out -# which vhost they apply to. -# -# After you've named and defined your vhosts, you may then create new -# configuration sections whose names are of the form -# vhost-VHOSTNAME/CONFIGSECTION. VHOSTNAME here is the canonical name -# of one of the virtual hosts you defined under the [vhosts] section. -# Inside those configuration sections, you override the standard -# ViewVC options typically found in the base configuration section -# named CONFIGSECTION ("general", "option", etc.) -# -# Here is an example: -# -# [vhosts] -# libs = libs.yourdomain.*, *.yourlibs.* -# gui = guiproject.yourdomain.* -# -# [vhost-libs/general] -# cvs_roots = -# svn_roots = svnroot: /var/svn/libs-repos -# default_root = svnroot -# -# [vhost-libs/options] -# show_logs = 1 -# -# [vhost-gui/general] -# cvs_roots = cvsroot: /var/cvs/guiproject -# svn_roots = -# default_root = cvsroot -# +## Virtual hosts are individual logical servers accessible via +## different hostnames, but which are all really the same physical +## computer. For example, you might have your web server configured to +## accept incoming traffic for both http://www.yourdomain.com/ and +## http://viewvc.yourdomain.com/. Users pointing their web browsers at +## each of those two URLs might see entirely different content via one +## URL versus the other, but all that content actually lives on the +## same computer, is served up via the same web server, and so +## on. It just *looks* like its coming from multiple servers. +## +## ViewVC allows you to customize its configuration options for +## individual virtual hosts. You might, for example, wish to expose +## all of your Subversion repositories at http://svn.yourdomain.com/viewvc/ +## and all your CVS ones at http://cvs.yourdomain.com/viewvc/, with no +## cross-exposure. Using ViewVC's virtual host (vhost) configuration +## support, you can do this. Simply create two vhost configurations +## (one for each of your hostnames), then configure the cvs_roots +## option only for the vhost associated with cvs.yourdomain.com, and +## configure the svn_roots option only for the vhost associated with +## svn.yourdomain.com. +## +## This section is a freeform configuration section, where you create +## both the option names and their values. The names of the options +## are then treated as canonical names of virtual hosts, and their +## values are defined to be comma-delimited lists of hostname globs +## against which incoming ViewVC requests will be matched to figure out +## which vhost they apply to. +## +## After you've named and defined your vhosts, you may then create new +## configuration sections whose names are of the form +## vhost-VHOSTNAME/CONFIGSECTION. VHOSTNAME here is the canonical name +## of one of the virtual hosts you defined under the [vhosts] section. +## Inside those configuration sections, you override the standard +## ViewVC options typically found in the base configuration section +## named CONFIGSECTION ("general", "option", etc.) +## +## NOTE: Per-vhost overrides may only be applied to the following +## sections: +## +## general +## options +## utilities +## templates +## cvsdb +## authz-* +## +## Here is an example: +## +## [vhosts] +## libs = libs.yourdomain.*, *.yourlibs.* +## gui = guiproject.yourdomain.* +## +## [vhost-libs/general] +## cvs_roots = +## svn_roots = svnroot: /var/svn/libs-repos +## default_root = svnroot +## +## [vhost-libs/options] +## show_logs = 1 +## +## [vhost-gui/general] +## cvs_roots = cvsroot: /var/cvs/guiproject +## svn_roots = +## default_root = cvsroot +## -#--------------------------------------------------------------------------- -# ViewVC recognizes per-root configuration overrides, too. To -# override the value of a configuration parameter only for a single -# root, create a configuration section whose names is of the form -# root-ROOTNAME/CONFIGSECTION. ROOTNAME here is the name of the root -# as defined explicitly in cvs_roots or svn_roots or implicitly as the -# basename of a root path in root_parents. Options found in this new -# configuration section override for this one root the corresponding -# options found in the base configuration section CONFIGSECTION -# ("options", "authz-*", etc.) -# -# Here is an example showing how to enable Subversion authz-based -# authorization for only the single root named "svnroot": -# -# [root-svnroot/options] -# authorizer = svnauthz -# -# [root-svnroot/authz-svnauthz] -# authzfile = /path/to/authzfile -# +##--------------------------------------------------------------------------- +## ViewVC recognizes per-root configuration overrides, too. To +## override the value of a configuration parameter only for a single +## root, create a configuration section whose names is of the form +## root-ROOTNAME/CONFIGSECTION. ROOTNAME here is the name of the root +## as defined explicitly in cvs_roots or svn_roots or implicitly as the +## basename of a root path in root_parents. Options found in this new +## configuration section override for this one root the corresponding +## options found in the base configuration section CONFIGSECTION +## ("options", "authz-*", etc.) as interpreted after per-vhost +## overrides (if any) have been applied. +## +## NOTE: Per-root overrides may only be applied to the following +## sections: +## +## options +## utilities +## authz-* +## +## WARNING: Do not use per-root overrides if your ViewVC instance is +## served via the standalone.py server option! Doing so could cause +## ViewVC to be unable to function properly (or at all). +## +## Here is an example showing how to enable Subversion authz-based +## authorization for only the single root named "svnroot": +## +## [root-svnroot/options] +## authorizer = svnauthz +## +## [root-svnroot/authz-svnauthz] +## authzfile = /path/to/authzfile +## -#--------------------------------------------------------------------------- +##--------------------------------------------------------------------------- [authz-forbidden] -# The "forbidden" authorizer forbids access to repository modules, -# defined to be top-level subdirectories in a repository. You can use -# a simple list of modules, or something more complex: -# -# *) The "!" can be used before a module to explicitly state that it -# is NOT forbidden. Whenever this form is seen, then all modules will -# be forbidden unless one of the "!" modules match. -# -# *) Shell-style "glob" expressions may be used. "*" will match any -# sequence of zero or more characters, "?" will match any single -# character, "[seq]" will match any character in seq, and "[!seq]" -# will match any character not in seq. -# -# *) Tests are performed in sequence. The first match will terminate the -# testing. This allows for more complex allow/deny patterns. -# -# Tests are case-sensitive. -# -# NOTE: Again, this is for the hiding of modules within repositories, *not* -# for the hiding of repositories (roots) themselves. -# -# Some examples: -# -# Disallow "example" but allow all others: -# forbidden = example -# -# Disallow "example1" and "example2" but allow all others: -# forbidden = example1, example2 -# -# Allow *only* "example1" and "example2": -# forbidden = !example1, !example2 -# -# Forbid modules starting with "x": -# forbidden = x* -# -# Allow modules starting with "x" but no others: -# forbidden = !x* -# -# Allow "xml", forbid other modules starting with "x", and allow the rest: -# forbidden = !xml, x*, !* -# -forbidden = +## The "forbidden" authorizer forbids access to repository modules, +## defined to be top-level subdirectories in a repository. +## +## NOTE: The options in this section apply only when the 'authorizer' +## option (in the [options] section) is set to 'forbidden'. -#--------------------------------------------------------------------------- +## forbidden: A comma-delimited list of patterns which match modules +## that ViewVC should hide from users. +## +## You can use a simple list of modules, or something more complex: +## +## *) The "!" can be used before a module to explicitly state that it +## is NOT forbidden. Whenever this form is seen, then all modules will +## be forbidden unless one of the "!" modules match. +## +## *) Shell-style "glob" expressions may be used. "*" will match any +## sequence of zero or more characters, "?" will match any single +## character, "[seq]" will match any character in seq, and "[!seq]" +## will match any character not in seq. +## +## *) Tests are performed in sequence. The first match will terminate the +## testing. This allows for more complex allow/deny patterns. +## +## Tests are case-sensitive. +## +## NOTE: Again, this is for the hiding of modules within repositories, *not* +## for the hiding of repositories (roots) themselves. +## +## Some examples: +## +## Disallow "example" but allow all others: +## forbidden = example +## +## Disallow "example1" and "example2" but allow all others: +## forbidden = example1, example2 +## +## Allow *only* "example1" and "example2": +## forbidden = !example1, !example2 +## +## Forbid modules starting with "x": +## forbidden = x* +## +## Allow modules starting with "x" but no others: +## forbidden = !x* +## +## Allow "xml", forbid other modules starting with "x", and allow the rest: +## forbidden = !xml, x*, !* +## +#forbidden = + +##--------------------------------------------------------------------------- [authz-forbiddenre] -# The "forbiddenre" authorizer forbids access to repositories and -# repository paths by comparing a list of regular expressions -# (separated by commas) against paths consisting of the repository (or -# root) name plus the path of the versioned file or directory to be -# tested. For example, to see if the user is authorized to see the -# path "/trunk/www/index.html" in the repository whose root name is -# "svnrepos", this authorizer will check the path -# "svnrepos/trunk/www/index.html" against the list of forbidden -# regular expressions. Directory paths will be terminated by a forward -# slash. -# -# Like the "forbidden" authorizer... -# -# *) The "!" can be used before a module to explicitly state that it -# is NOT forbidden. Whenever this form is seen, then all modules will -# be forbidden unless one of the "!" modules match. -# -# *) Tests are performed in sequence. The first match will terminate the -# testing. This allows for more complex allow/deny patterns. -# -# Unlike the "forbidden" authorizer, you can can use this to hide roots, too. -# -# Some examples: -# -# Disallow files named "PRIVATE", but allow all others: -# forbiddenre = /PRIVATE$ -# -# Disallow the "hidden" repository, allowing all others: -# forbiddenre = ^hidden(/|$) -# -# Allow only the "example1" and "example2" roots and the paths inside them, -# disallowing all others (which can be done in multiple ways): -# forbiddenre = !^example1(/|$), !^example2(/|$)/ -# forbiddenre = !^example[12](/|$) -# -# Only allow visibility of HTML files and the directories that hold them: -# forbiddenre = !^([^/]+|.*(/|\.html))$ -# -forbiddenre = +## The "forbiddenre" authorizer forbids access to repositories and +## repository paths by comparing a list of regular expressions +## (separated by commas) against paths consisting of the repository (or +## root) name plus the path of the versioned file or directory to be +## tested. For example, to see if the user is authorized to see the +## path "/trunk/www/index.html" in the repository whose root name is +## "svnrepos", this authorizer will check the path +## "svnrepos/trunk/www/index.html" against the list of forbidden +## regular expressions. Directory paths will be terminated by a forward +## slash. +## +## NOTE: The options in this section apply only when the 'authorizer' +## option (in the [options] section) is set to 'forbiddenre'. -#--------------------------------------------------------------------------- +## forbiddenre: A comma-delimited list of regular expressions which +## match paths that ViewVC should hide from users. +## +## Like the "forbidden" authorizer... +## +## *) The "!" can be used before a module to explicitly state that it +## is NOT forbidden. Whenever this form is seen, then all modules will +## be forbidden unless one of the "!" modules match. +## +## *) Tests are performed in sequence. The first match will terminate the +## testing. This allows for more complex allow/deny patterns. +## +## Unlike the "forbidden" authorizer, you can can use this to hide roots, too. +## +## Some examples: +## +## Disallow files named "PRIVATE", but allow all others: +## forbiddenre = /PRIVATE$ +## +## Disallow the "hidden" repository, allowing all others: +## forbiddenre = ^hidden(/|$) +## +## Allow only the "example1" and "example2" roots and the paths inside them, +## disallowing all others (which can be done in multiple ways): +## forbiddenre = !^example1(/|$), !^example2(/|$)/ +## forbiddenre = !^example[12](/|$) +## +## Only allow visibility of HTML files and the directories that hold them: +## forbiddenre = !^([^/]+|.*(/|\.html))$ +## +#forbiddenre = + +##--------------------------------------------------------------------------- [authz-svnauthz] -# The "svnauthz" authorizer uses a Subversion authz configuration file -# to determine access to repository paths. This option specifies the -# location of that file using an absolute path. -# -authzfile = +## The "svnauthz" authorizer uses a Subversion authz configuration file +## to determine access to repository paths. +## +## NOTE: The options in this section apply only when the 'authorizer' +## option (in the [options] section) is set to 'svnauthz'. -#--------------------------------------------------------------------------- +## authzfile: Specifies the location of the authorization rules file +## (using an absolute path). +## +#authzfile = + +## force_username_case: Like the AuthzForceUsernameCase httpd.conf +## directive, set this to "upper" or "lower" to force the normalization +## to upper- or lower-case, respectively, of incoming usernames prior +## to comparison against the authorization rules files. Leave the +## option unset to preserve the username case. +## +#force_username_case = + +##--------------------------------------------------------------------------- +[query] + +## The configuration items in this section are used exclusively by the +## 'query' script, a separate script from ViewVC itself that ships +## with ViewVC and allows for queries into the ViewVC commits +## database. If you aren't using this separate script (which was made +## largely irrelevant by the introduction of an integrated "query" +## view in ViewVC itself, or aren't using the ViewVC commits database +## functionality at all, you can ignore these configurations items +## altogether. + +## viewvc_base_url: Base URL at which ViewVC may be accessed on this +## server. The default value for this option is determined at +## run-time by the various front-ends to the query script. +## +## Examples: +## viewvc_base_url = /viewvc.py +## viewvc_base_url = /viewvc.wsgi +## viewvc_base_url = /cgi-bin/viewvc +## viewvc_base_url = viewvc +## +## To disable cross-linking between the query script and ViewVC, +## uncomment this option and leave its value empty. +## +#viewvc_base_url = + +##--------------------------------------------------------------------------- diff --git a/docs/release-notes/1.1.0.html b/docs/release-notes/1.1.0.html new file mode 100644 index 00000000..97ddab28 --- /dev/null +++ b/docs/release-notes/1.1.0.html @@ -0,0 +1,213 @@ + + + +ViewVC: 1.1.0 Release Notes + + + + + +

ViewVC 1.1.0 Release Notes

+ +
+

Introduction

+ +

ViewVC 1.1.0 is the superset of all previous ViewVC releases.

+ +
+ +
+

Compatibility

+ +

Each ViewVC release strives to maintain URL stability with previous + releases, and 1.1.0 is no exception. All URLs considered valid for + previous ViewVC releases should continue to work correctly in this + release, though possibly only via the use of HTTP redirects + (generated by ViewVC itself).

+ +

The commits database functionality has changed in ViewVC 1.1.0 in + way that breaks compatibility with prior ViewVC releases, but only + for new database instantiations. ViewVC 1.1.0 will continue to + understand (for both read and write operations) the previous + schema, so you are not required to rebuild your commits database + for ViewVC 1.1.0 compatibility. By default, new commits databases + created using the 1.1.0 version of the make-database + script will use a new database schema that is unreadable by + previous ViewVC versions. However, if you need a database which + can co-exist with a previous ViewVC version, you can use + the --version=1.0 option + to make-database.

+ +

The ViewVC configuration files and template language have changed + dramatically. See the file docs/upgrading-howto.html + in the release for information on porting existing versions of + those items for use with ViewVC 1.1.0.

+ +
+ +
+

Features and Fixes

+ +
+

Extensible path-based authorization w/ Subversion authz support

+ +

In a nutshell, ViewVC is now able to do path-based authorization. + ViewVC 1.0 has a configuration option for naming 'forbidden' + modules, but it is really limited — it basically just makes a + universal decision about which top-level directories in every + hosted repository should be hidden by ViewVC. People want + more, and specifically requested that ViewVC learn how to honor + Subversion's authz files and semantics. So, along with some other + types of authorization approaches, that's what ViewVC 1.1 can now + do. If you are using mod_authz_svn with Apache today, or + svnserve's built-in authorization support, then you can now point + ViewVC to the same authz configuration file and have it honor the + access rules you've defined for your repositories.

+ +

Note that ViewVC does not handle authentication, + though. You'll need to configure your web server to demand login + credentials from users, which the web server itself can then hand + off to ViewVC for application against the authorization rules + you've defined.

+ +

WARNING: The root listing view does not consult the + authorization subsystem when deciding what roots to display to a + given user. If you need to protect your root names, consider + disabling it by removing roots from the set of views + listed in the allowed_views configuration option. + UPDATE: This was fixed in ViewVC 1.1.3.

+ +

WARNING: Support for path-based authorization is + incomplete in the experimental version control backend modules, + including the one that permits display of remote Subversion + repositories. UPDATE: This was fixed in ViewVC + 1.1.15.

+ +
+ +
+

Subversion versioned properties display

+ +

ViewVC 1.1 displays the properties that Subversion lets you store + on files and directories + (svn:mime-type, svn:mergeinfo, + svn:ignore, etc.). Directory properties are shown by + default at the bottom of that directory's entry listing. File + properties are displayed at the bottom of that file's + markup/annotate view.

+ +
+ +
+

Unified markup and annotation views

+ +

The "markup" and "annotate" views in ViewVC now have a unified look + and feel (driven by a single EZT template). Both views support + syntax highlighting and Subversion file property display.

+ +
+ +
+

Unified, hassle-free Pygments-based syntax highlighting

+ +

ViewVC 1.0 does syntax highlighting by working with GNU enscript, or + highlight, or php, or py2html — all these external tools just + to accomplish a single task. But they all do things in slightly + different ways. And if you configure them wrongly, you get strange + errors. Pygments (which is + also used by Trac for + syntax highlighting) is a Python package that requires no + configuration, is easier to use inside ViewVC, and so on. So + ViewVC 1.1 drops support for all those various old integrations, + and just uses Pygments for everything now. This change was about + developer and administrator sanity. There will be complaints, to + be sure, about how various color schemes differ and what file types + now are and aren't understood by the syntax highlighting engine, + but this change should vastly simplify the discussions of such + things.

+ +
+ +
+

Better MIME detection and handling

+ +

ViewVC typically consults a MIME types file to determine what kind + of file a given document is, based on its filename extension + (.jpg = image/jpeg, …). But + Subversion lets you dictate a file's MIME type using + the svn:mime-type property. ViewVC now recognizes and + honors that property as the preferred source of a file's MIME type. + This can be disabled in the configuration, though, which might be + desirable if many of your Subversion-versioned files carry the + generic application/octet-stream MIME type that + Subversion uses by default for non-human-readable files.

+ +

Also, ViewVC now allows you to specify multiple MIME type mapping + files that you'd like it to consult when determine the MIME type of + files based on their extensions. This allows administrators to + easily define their own custom mappings for ViewVC's benefit + without potentially affecting the mappings used by other site + services.

+ +
+ +
+

Support for full content diffs

+ +

ViewVC 1.1 expands the previously existing options of "colored + diff" and "long colored diff" with a new "full colored diff", which + shows the full contents of the changed file (instead of only the 3 + or 15 lines of context shown via the older diff display types).

+ +
+ +
+

Support for per-root configuration overrides

+ +

In ViewVC 1.1, you can setup configuration option overrides on a + per-root (per-repository) basis (if you need/care to do so). See + the comments in the viewvc.conf.dist file for more on + how to do this.

+ +
+ +
+

Optional email address obfuscation/mangling

+ +

ViewVC can, when displaying revision metadata, munge strings that + look like email addresses to protect them from screen-scraping + spammers. For example, a log message that says, "Patch by: + cmpilato@red-bean.com" can optionally be displayed by ViewVC using + HTML entity encoding for the characters (a trick that causes no + visible change to the output, but that might confuse + unsophisticated spam bot crawlers) or as "Patch by: cmpilato@..." + (which isn't a complete email address at all, but might be enough + information for the human reading the log message to know who to + blame for the patch).

+ +
+ +
+

Pagination improvements

+ +

The way that ViewVC splits directory and log views across pages has + been reworked. The old way was "Fetch all the information you can + find, then display only one page's worth." The new way is "Fetch + only what you need to display the page requested, plus a little bit + of border information." This provides a large performance + enhancement for the default sort orderings.

+ +
+ +
+ + + diff --git a/docs/release-notes/1.2.0.html b/docs/release-notes/1.2.0.html new file mode 100644 index 00000000..23cd7830 --- /dev/null +++ b/docs/release-notes/1.2.0.html @@ -0,0 +1,49 @@ + + + +ViewVC: 1.2.0 Release Notes + + + + + +

ViewVC 1.2.0 Release Notes

+ +
+

Introduction

+ +

ViewVC 1.2.0 is the superset of all previous ViewVC releases.

+ +
+ +
+

Compatibility

+ +

Each ViewVC release strives to maintain URL stability with previous + releases, and 1.2.0 is no exception. All URLs considered valid for + previous ViewVC releases should continue to work correctly in this + release, though possibly only via the use of HTTP redirects + (generated by ViewVC itself).

+ +
+ +
+

Features and Fixes

+ +
+

+ +
+ +
+ + + diff --git a/docs/template-authoring-guide.html b/docs/template-authoring-guide.html index efdcf247..73812990 100644 --- a/docs/template-authoring-guide.html +++ b/docs/template-authoring-guide.html @@ -1,6 +1,6 @@ -ViewVC 1.0 Template Authoring Guide +ViewVC 1.2 Template Authoring Guide -

ViewVC 1.0 Template Authoring Guide

+

ViewVC 1.2 Template Authoring Guide

Introduction

This document represents an (unfinished) attempt at providing - documentation for how to customize ViewVC 1.0-dev's HTML output via + instructions for how to customize ViewVC's HTML output via modification of its templates.

@@ -161,12 +165,6 @@ td { resource. Valid only when pathtype is file or (for Subversion roots) dir. - - log_rev_href - String - Revision number of the file-revision currently being viewed, or - None. - nav_path List @@ -321,7 +319,7 @@ td { pathrev_hidden_values List - Hidden value name/value pairs for the revision/tag selection form. + Hidden field name/value pairs for the revision/tag selection form. pathrev_clear_action @@ -331,7 +329,7 @@ td { pathrev_clear_hidden_values List - Hidden value name/value pairs for the path revision clear button. + Hidden field name/value pairs for the path revision clear button. @@ -450,10 +448,10 @@ td { annotation String - If set, indicates that annotations were requested. Valid values - are "annotated" (annotation was successful), "binary" (file contents - are not line-based and human-readable), and "error" (something went - wrong during annotation). + Valid values are "none" (no annotations were attempted), + "annotated" (annotation was successful), "binary" (file contents + are not line-based and human-readable), and "error" (something + went wrong during annotation). author @@ -604,6 +602,45 @@ td { Includes all variables from the COMMON variable set + + gbbox + Boolean + Toggle generation of a branch box at the tip of all branches in + the revision graph. + + + gflip + Boolean + Toggle the direction of the revision graph. + + + gleft + Boolean + Toggle the orientation of the revision graph. + + + gmaxtag + String + Number of tags per revision to display in the revision graph. + + + graph_action + String + Form action URL for the graph customization form. + + + graph_hidden_values + String + Hidden value name/value pairs for the graph customization form. + + + gshow + String + Classes of revisions to show in the revision graph. Valid values + are all (all revision), inittagged (initial + revision(s) and tagged revisions), and tagged (tagged + revisions only). + imagemap String @@ -616,6 +653,37 @@ td { URL of the ViewVC revision graph image for the current resource. + + opt_gbbox + Boolean + Specifies whether the user is allowed to toggle the generation + of branch boxes at the tip of all branches in the revision + graph. + + + opt_gflip + Boolean + Specifies whether the user is allowed to toggle the direction + of the revision graph. + + + opt_gleft + Boolean + Specifies whether the user is allowed to toggle the orientation + of the revision graph. + + + opt_gmaxtag + Boolean + Specifies whether the user is allowed to configure the maximum + number of tags per revision show in the revision graph. + + + opt_gshow + Boolean + Specifies whether the user is allowed to configure which + classes of revisions are shown in the revision graph. + @@ -636,30 +704,64 @@ td { COMMON variable set - changes + diffs List - Set of objects which contain information about a single line of - file difference data. Valid only when diff_format is - h or l. + List of all blocks of differences between the two sides, including content + and property differences. - changes.have_left - Boolean - Specifies whether the left file has a line of content relevant - to the difference data line. Valid only when - changes.type is change. - - - changes.have_right - Boolean - Specifies whether the right file has a line of content relevant - to the difference data line. Valid only when - changes.type is change. - - - changes.left + diffs.diff_block_format String - Textual contents of the relevant line in the left file. Valid + Indicates the type of this block. One of the anchor (no display, + create an anchor), raw (non-colored diff, display as produced), + sidebyside-1 (traditional side-by-side diff), + sidebyside-2 (newer side-by-side diff with intraline changes), + unified (colored unified diff). + + + diffs.anchor + String + If diffs.diff_block_format is anchor, this variable specifies + the anchor name. + + + diffs.changes + List/Container + Set of objects which contain information about a change in a single + object (file or property). Not present if diffs.diff_block_format is + anchor, otherwise has different format depending on + diffs.diff_block_format (applicable as indicated in brackets below). + + + diffs.changes.raw + String + [raw] Diff text. Valid only if diffs.changes.type is + raw. + + + diffs.changes.type + String + [raw] The type of change. Values: binary-diff, + error, no-changes, raw. + + + diffs.changes.have_left + Boolean + [sidebyside-1] Specifies whether the left file has a line of content relevant + to the difference data line. Valid only when + changes.type is change. + + + diffs.changes.have_right + Boolean + [sidebyside-1] Specifies whether the right file has a line of content relevant + to the difference data line. Valid only when + changes.type is change. + + + diffs.changes.left + String + [sidebyside-1] Textual contents of the relevant line in the left file. Valid only when changes.type is change, context, or remove. When changes.type is change, valid only when @@ -667,10 +769,10 @@ td { between missing lines and empty lines, which EZT does not support). - - changes.right + + diffs.changes.right String - Textual contents of the relevant line in the right file. Valid + [sidebyside-1] Textual contents of the relevant line in the right file. Valid only when changes.type is add, change, or context. When changes.type is change, valid only when @@ -678,40 +780,264 @@ td { between missing lines and empty lines, which EZT does not support). - - changes.line_info_extra + + diffs.changes.line_info_extra String - Additional line information for the current difference hunk. + [sidebyside-1] Additional line information for the current difference hunk. Valid only when changes.type is header. - - changes.line_info_left + + diffs.changes.line_info_left String - First line number represented by the current hunk in the left + [sidebyside-1] First line number represented by the current hunk in the left file. Valid only when changes.type is header. - - changes.line_info_right + + diffs.changes.line_info_right String - First line number represented by the current hunk in the right + [sidebyside-1] First line number represented by the current hunk in the right file. Valid only when changes.type is header. - - changes.line_number + + diffs.changes.line_number String - Line number (1-based) of the line. + [sidebyside-1] Line number (1-based) of the line. - - changes.type + + diffs.changes.type String - The type of change. Value values: add, - change, context, header, + [sidebyside-1] The type of change. Values: add, binary-diff, + change, context, error, header, no-changes, remove. + + diffs.changes.columns + List + [sidebyside-2] List of two columns for left and right parts of the diff. + + + diffs.changes.columns.line_number + String + [sidebyside-2] Line number in the left/right column. + + + diffs.changes.columns.segments + List + [sidebyside-2] Left/right line, broken into change segments. + + + diffs.changes.columns.segments.text + String + [sidebyside-2] Text of this segment. + + + diffs.changes.columns.segments.type + String + [sidebyside-2] Not set if the segment is the same in both left and right sides; + otherwise, one of the add, remove or change. + + + diffs.changes.gap + Boolean + [sidebyside-2] If true, indicates that change blocks are non-contiguous + and that the template should display some sort of ellipsis before the + current block. + + + diffs.changes.type + String + [sidebyside-2] The type of change. Values: binary-diff, + error, intraline, no-changes. + + + diffs.changes.segments + List + [unified] Left/right line, broken into change segments. + + + diffs.changes.segments.text + String + [unified] Text of this segment. + + + diffs.changes.segments.type + String + [unified] Not set if the segment is the same in both left and right sides; + otherwise, one of the add, remove or change. + + + diffs.changes.type + String + [unified] The type of change. Values: add, binary-diff, + error, no-changes, remove or empty string + if the line was not changed (context line). + + + diffs.left + Container + Container object for grouping information about the left file. + + + diffs.left.ago + String + Text description of the time elapsed since left.date. + + + diffs.left.annotate_href + String + URL of the ViewVC annotation view for the left file. + Valid only when entries.pathtype is file. + + + diffs.left.author + String + Author of the revision of the left file. + + + diffs.left.date + String + Date (in UTC if not otherwise configured) in which the left file + revision was created. + + + diffs.left.download_href + String + URL to download the HEAD revision of the left file. + + + diffs.left.download_text_href + String + URL to download the HEAD revision of the left file as + text/plain. + + + diffs.left.log + String + Log message of the left file revision. + + + diffs.left.path + String + Path of the left file. + + + diffs.left.prefer_markup + Boolean + Indicates whether to make the default file link a link to the markup + page instead of the checkout page. + + + diffs.left.rev + String + Revision of the left file. + + + diffs.left.revision_href + String + URL of the Subversion revision view for the left file's + current revision. Valid only when roottype is + svn. + + + diffs.left.size + String + Size of the left file revision, in bytes. Subversion only. + + + diffs.left.tag + String + Tag of the left file. + + + diffs.left.view_href + String + This is a URL for the markup view of the left file. + + + diffs.right + Container + Container object for grouping information about the right file. + + + diffs.right.ago + String + Text description of the time elapsed since right.date. + + + diffs.right.annotate_href + String + URL of the ViewVC annotation view for the right file. + Valid only when entries.pathtype is file. + + + diffs.right.author + String + Author of the revision of the right file. + + + diffs.right.date + String + Date (in UTC if not otherwise configured) in which the right file + revision was created. + + + diffs.right.download_href + String + URL to download the HEAD revision of the right file. + + + diffs.right.download_text_href + String + URL to download the HEAD revision of the right file as + text/plain. + + + diffs.right.log + String + Log message of the right file revision. + + + diffs.right.path + String + Path of the right file. + + + diffs.right.prefer_markup + Boolean + Indicates whether to make the default file link a link to the markup + page instead of the checkout page. + + + diffs.right.rev + String + Revision of the right file. + + + diffs.right.revision_href + String + URL of the Subversion revision view for the right file's + current revision. Valid only when roottype is + svn. + + + diffs.right.size + String + Size of the right file revision, in bytes. Subversion only. + + + diffs.right.tag + String + Tag of the right file. + + + diffs.right.view_href + String + This is a URL for the markup view of the right file. + diff_format String - Difference dislay format: Valid values are c + Difference display format: Valid values are c (context), f (full human-readable), h (human-readable, or colored), l (long human-readable), s (side-by-side), u @@ -725,135 +1051,17 @@ td { diff_format_hidden_values List - Hidden value name/value pairs for the diff format selection form. + Hidden field name/value pairs for the diff format selection form. - left - Container - Container object for grouping information about the left file. - - - left.annotate_href - String - URL of the ViewVC annotation view for the left file. - Valid only when entries.pathtype is file. - - - left.date - String - Date (in UTC if not otherwise configured) in which the left file - revision was created. - - - left.download_href - String - URL to download the HEAD revision of the left file. - - - left.download_text_href - String - URL to download the HEAD revision of the left file as - text/plain. - - - left.path - String - Path of the left file. - - - left.prefer_markup + hide_legend Boolean - Indicates whether to make the default file link a link to the markup - page instead of the checkout page. - - - left.rev - String - Revision of the left file. - - - left.revision_href - String - URL of the Subversion revision view for the left file's - current revision. Valid only when roottype is - svn. - - - left.tag - String - Tag of the left file. - - - left.view_href - String - This is a URL for the markup view of the left file. + Indicates whether the display format requires displaying a legend - raw_diff + patch_href String - Raw difference text. Valid only when diff_format is - c, s, or u. - - - right - Container - Container object for grouping information about the right file. - - - right.annotate_href - String - URL of the ViewVC annotation view for the right file. - Valid only when entries.pathtype is file. - - - right.date - String - Date (in UTC if not otherwise configured) in which the right file - revision was created. - - - right.download_href - String - URL to download the HEAD revision of the right file. - - - right.download_text_href - String - URL to download the HEAD revision of the right file as - text/plain. - - - right.path - String - Path of the right file. - - - right.prefer_markup - Boolean - Indicates whether to make the default file link a link to the markup - page instead of the checkout page. - - - right.rev - String - Revision of the right file. - - - right.revision_href - String - URL of the Subversion revision view for the right file's - current revision. Valid only when roottype is - svn. - - - right.tag - String - Tag of the right file. - - - right.view_href - String - This is a URL for the markup view of the right file. + URL of the patch view for the file. @@ -905,7 +1113,7 @@ td { dir_paging_hidden_values List - Hidden value name/value pairs for the page selection form. + Hidden field name/value pairs for the page selection form. entries @@ -1087,23 +1295,16 @@ td { String Current search expression, if any. - - search_re_form - Boolean - Indicates whether or not to display the regular expression search - form. Value depends on the whether searching is enabled in the - configuration and whether or not the current directory is - empty. - search_re_action String - Form action URL for the regular expression search form. + Form action URL for the regular expression search form, + if searching is available. search_re_hidden_values List - Hidden value name/value pairs for the regular expression search form. + Hidden field name/value pairs for the regular expression search form. show_attic_href @@ -1249,7 +1450,7 @@ td { diff_select_hidden_values List - Hidden value name/value pairs for the diff selection form. + Hidden field name/value pairs for the diff selection form. entries @@ -1530,7 +1731,7 @@ td { log_paging_hidden_values List - Hidden value name/value pairs for the page selection form. + Hidden field name/value pairs for the page selection form. logsort @@ -1546,7 +1747,7 @@ td { logsort_hidden_values List - Hidden value name/value pairs for the log sort drop down box + Hidden field name/value pairs for the log sort drop down box mime_type @@ -1794,6 +1995,14 @@ td { Indicates how query results are being sorted. Possible values: date, author, and file. + + row_limit_reached + Boolean + Indicates whether the internal database row limit threshold (set + via the cvsdb.row_limit + and cvsdb.rss_row_limit configuration options) was + reached by the query. + show_branch Boolean @@ -1910,7 +2119,7 @@ td { query_hidden_values List - Hidden value name/value pairs for query form. + Hidden field name/value pairs for query form. querysort @@ -2050,7 +2259,7 @@ td { jump_rev_hidden_values List - Hidden value name/value pairs for revision jump form. + Hidden field name/value pairs for revision jump form. limit_changes @@ -2073,6 +2282,11 @@ td { String URL for the current view but with limit_changes disabled. + + num_changes + String + Number of paths changed in this revision. + next_href String @@ -2112,6 +2326,38 @@ td { List Set of configured viewable repositories. + + roots.ago + String + Textual description of the time since roots.date. + + + roots.author + String + Username of the last modifier of the root. + + + root.date + String + Date (in UTC if not otherwise configured) of the last + modification of the root. + + + roots.href + String + URL of root directory view for a configured repository. + + + roots.log + String + Log message of last modification to the root. + + + roots.log_href + String + URL of log revision view for the top-most (root) directory of + the root (repository). + roots.name String @@ -2125,17 +2371,24 @@ td { configuration can have negative security implications. Use this token at your own risk. + + roots.rev + String + Youngest revision of the root. + + + roots.short_log + String + Log message of last modification to the root, truncated to + contain no more than the number of characters specified by + the short_log_len configuration option. + roots.type String Version control type of a configured repository. Valid values: cvs, svn. - - roots.href - String - URL of root directory view for a configured repository. - diff --git a/docs/upgrading-howto.html b/docs/upgrading-howto.html index 0d81e1ca..91dafe00 100644 --- a/docs/upgrading-howto.html +++ b/docs/upgrading-howto.html @@ -28,12 +28,12 @@ td { .h3 { border-width: 1px 0 0 0; } .toc-list { font-size: 90%; } .varname { font-family: monospace; } -.added { background: rgb(50%,75%,25%); } +.added { background: rgb(60%,90%,60%); } .unchanged { background: rgb(75%,75%,75%); } -.renamed { background: rgb(75%,50%,75%); } -.changed { background: rgb(100%,100%,25%); } -.replaced { background: rgb(100%,75%,0%); } -.removed { background: rgb(100%,25%,25%); } +.renamed { background: rgb(80%,60%,80%); } +.changed { background: rgb(100%,100%,50%); } +.replaced { background: rgb(100%,80%,40%); } +.removed { background: rgb(100%,70%,70%); } @@ -67,12 +67,21 @@ td {

Table of Contents

+
+

Upgrading From ViewVC 1.1

+ +

This section discusses how to upgrade ViewVC 1.1.x to ViewVC 1.2.x.

+ +
+ +

Upgrading From ViewVC 1.0

@@ -314,7 +323,7 @@ td { all = viewvc.* [all-options] -allow_tar = 1 +allowed_views = annotate, diff, markup, tar @@ -324,7 +333,7 @@ allow_tar = 1 all = viewvc.* [vhost-all/options] -allow_tar = 1 +allowed_views = annotate, diff, markup, tar @@ -1563,13 +1572,8 @@ allow_tar = 1

Upgrading From ViewCVS 0.8

-

This section discusses how to upgrade ViewCVS 0.8 to version - 0.9 or a later version of the software.

+

This section discusses how to upgrade ViewCVS 0.8 to ViewCVS 0.9.x.

-

NOTE: these changes will bring you up to the - requirements of version 0.9. You must also follow the directions - for upgrading from 0.9.

-

Configuration Options

@@ -1579,92 +1583,50 @@ allow_tar = 1 options, then you will need to make corresponding changes in the templates.

-
-
- Colors: - diff_heading, - diff_empty, - diff_remove, - diff_change, - diff_add, - and diff_dark_change -
-
- These options have been incorporated into the - diff.ezt template. +
+
Colors: diff_heading, diff_empty, + diff_remove, diff_change, + diff_add, and diff_dark_change
+
These options have been incorporated into the + diff.ezt template.
-

-
+
markup_log
+
This option has been incorporated into the + markup.ezt template.
-
markup_log
-
- This option has been incorporated into the - markup.ezt template. +
Colors: nav_header and + alt_background
+
These options have been incorporated into the + header.ezt template.
-

- +
Images: back_icon, dir_icon, + and file_icon
+
These options have been incorporated into the + directory.ezt, header.ezt, + log.ezt, log_table.ezt, and + query.ezt templates.
-
Colors: nav_header - and alt_background
-
- These options have been incorporated into the - header.ezt template. +
use_java_script + and open_extern_window
+
The templates now use JavaScript in all applicable places, and + open external windows for most downloading and viewing of + files. If you wish to not use JavaScript and/or external + windows, then remove the feature(s) from the templates.
-

- +
show_author
+
Changing this option would be quite strange and rare. If you + do not want to show the author for the revisions, then you + should remove it from the various templates.
-
- Images: - back_icon, - dir_icon, - and file_icon -
-
- These options have been incorporated into the - directory.ezt, header.ezt, - log.ezt, log_table.ezt, and - query.ezt templates. +
hide_non_readable
+
This option was never used, so it has been removed.
-

- +
flip_links_in_dirview
+
This option is no longer available. If you want the links in + your directory view flipped, then you may use the + dir_alternate.ezt template.
-
use_java_script - and open_extern_window
-
- The templates now use JavaScript in all applicable places, - and open external windows for most downloading and viewing - of files. If you wish to not use JavaScript and/or external - windows, then remove the feature(s) from the templates. - -

-
- -
show_author
-
- Changing this option would be quite strange and rare. If you - do not want to show the author for the revisions, then you - should remove it from the various templates. - -

-
- -
hide_non_readable
-
- This option was never used, so it has been removed. - -

-
- -
flip_links_in_dirview
-
- This option is no longer available. If you want the links in - your directory view flipped, then you may use the - dir_alternate.ezt template. - -

-
- -
+
@@ -1675,53 +1637,65 @@ allow_tar = 1 removed in 0.9. If you have custom templates that refer to these variables, then you will need to modify your templates.

-
-
directory.ezt: headers
-
- The headers are now listed explicitly in the template, - rather than made available through a list. -

-
- -
- directory.ezt: - rows.cols, - and rows.span -
-
- These variables were used in conjunction with the - headers variable to control the column - displays. This is now controlled explicitly within the - templates. -

-
- -
directory.ezt: - rev_in_front
-
- This was used to indicate that revision links should - be used in the first column, rather than in their - standard place in the second column. Changing the - links should now be done in the template, rather than - according to this variable. You may want to look at - the dir_alternate.ezt template, which has - the revision in front. -

-
- -
directory.ezt: - rows.attic - and rows.hide_attic_href
-
- These variable were used to manage the hide and - showing of the contents of the Attic/ - subdirectory. Several new variables were introduced - which can be used to replace this functionality: - show_attic_href, - hide_attic_href, and rows.state. -

-
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableLocationChanges
headersdirectory.eztremoved; headers are now listed explicitly in the template, rather + than made available through a list.
rows.colsdirectory.eztremoved; was used in conjunction with the headers + variable to control the column displays. This is now controlled + explicitly within the templates.
rows.spandirectory.eztremoved; was used in conjunction with the headers + variable to control the column displays. This is now controlled + explicitly within the templates.
rev_in_frontdirectory.eztremoved; was used to indicate that revision links should be used in + the first column, rather than in their standard place in the + second column. Changing the links should now be done in the + template, rather than according to this variable. You may want + to look at the dir_alternate.ezt template, which + has the revision in front. +
rows.atticdirectory.eztremoved; used to manage the hide and showing of the + contents of the Attic/ subdirectory. Several new + variables were introduced which can be used to replace this + functionality: show_attic_href, + hide_attic_href, and rows.state.
rows.hide_attic_hrefdirectory.eztremoved; used to manage the hide and showing of the + contents of the Attic/ subdirectory. Several new + variables were introduced which can be used to replace this + functionality: show_attic_href, + hide_attic_href, and rows.state.
diff --git a/docs/url-reference.html b/docs/url-reference.html index ebea1630..2a037022 100644 --- a/docs/url-reference.html +++ b/docs/url-reference.html @@ -654,6 +654,35 @@ th.caption { depends root parameter + + gflip=GFLIP + optional + "1" if the revisions in the graph should run + youngest-to-oldest; "0" for the reverse + + + gbbox=GBBOX + optional + "1" if the revision graph should contain branch boxes at the + tip of each branch; "0" otherwise + + + gleft=GLEFT + optional + "1" if the revision graph should be orientated left-to-right; + "0" otherwise + + + gmaxtag=GMAXTAG + optional + maximum number of per-revision tags to show in the revision graph + + + gshow=GSHOW + optional + "all", "inittagged", or "tagged" — user-selected classes + of revision to show in the graph +

Graph Image View

@@ -700,6 +729,35 @@ th.caption { depends root parameter + + gflip=GFLIP + optional + "1" if the revisions in the graph should run + youngest-to-oldest; "0" for the reverse + + + gbbox=GBBOX + optional + "1" if the revision graph should contain branch boxes at the + tip of each branch; "0" otherwise + + + gleft=GLEFT + optional + "1" if the revision graph should be orientated left-to-right; + "0" otherwise + + + gmaxtag=GMAXTAG + optional + maximum number of per-revision tags to show in the revision graph + + + gshow=GSHOW + optional + "all", "inittagged", or "tagged" — user-selected classes + of revision to show in the graph +

Log View

@@ -996,7 +1054,7 @@ th.caption { file query string - file_match=FILE_MATCH + file_match=FILE_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of file match @@ -1007,7 +1065,7 @@ th.caption { author query string - who_match=WHO_MATCH + who_match=WHO_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of author match @@ -1024,36 +1082,36 @@ th.caption { of log message match - querysort=SORT + querysort=SORT optional "date" "author" or "file" determining order of query results - date=DATE + date=DATE optional "hours" "day" "week" "month" "all" or "explicit" to filter query results by date - hours=HOURS + hours=HOURS optional number of hours back to include results from when DATE is "hours" - mindate=MINDATE + mindate=MINDATE optional earliest date to include results from when DATE is "explicit" - maxdate=MAXDATE + maxdate=MAXDATE optional latest date to include results from when DATE is "explicit" - limit_changes=LIMIT_CHANGES + limit_changes=LIMIT_CHANGES optional maximum number of files to list per commit in query results. Default is value of limit_changes @@ -1113,7 +1171,7 @@ th.caption { branch query string - branch_match=BRANCH_MATCH + branch_match=BRANCH_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of branch match @@ -1129,7 +1187,7 @@ th.caption { file query string - file_match=FILE_MATCH + file_match=FILE_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of file match @@ -1140,7 +1198,7 @@ th.caption { author query string - who_match=WHO_MATCH + who_match=WHO_MATCH optional "exact" "like" "glob" "regex" or "notregex" determining type of author match @@ -1157,50 +1215,43 @@ th.caption { of log message match - querysort=SORT + querysort=SORT optional "date" "author" or "file" determining order of query results - date=DATE + date=DATE optional "hours" "day" "week" "month" "all" or "explicit" to filter query results by date - hours=HOURS + hours=HOURS optional number of hours back to include results from when DATE is "hours" - mindate=MINDATE + mindate=MINDATE optional earliest date to include results from when DATE is "explicit" - maxdate=MAXDATE + maxdate=MAXDATE optional latest date to include results from when DATE is "explicit" - format=FORMAT + format=FORMAT optional "rss" or "backout" values to generate an rss feed or list of commands to back out changes instead showing a normal query result page - limit=LIMIT - optional - maximum number of file-revisions to process during a - query. Default is value of row_limit configuration - option - - - limit_changes=LIMIT_CHANGES + limit_changes=LIMIT_CHANGES optional maximum number of files to list per commit in query results. Default is value of limit_changes @@ -1254,7 +1305,7 @@ th.caption { revision parameter - limit_changes=LIMIT_CHANGES + limit_changes=LIMIT_CHANGES optional maximum number of files to list per commit. Default is value of limit_changes configuration option diff --git a/lib/accept.py b/lib/accept.py index 3c8ae4e6..01076b4d 100644 --- a/lib/accept.py +++ b/lib/accept.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -15,7 +15,6 @@ # ----------------------------------------------------------------------- import re -import string def language(hdr): @@ -40,7 +39,7 @@ def _parse(hdr, result): name = _re_token.match(hdr, pos) if not name: raise AcceptLanguageParseError() - a = result.item_class(string.lower(name.group(1))) + a = result.item_class(name.group(1).lower()) pos = name.end() while 1: # are we looking at a parameter? @@ -56,7 +55,7 @@ def _parse(hdr, result): # the "=" was probably missing continue - pname = string.lower(match.group(1)) + pname = match.group(1).lower() if pname == 'q' or pname == 'qs': try: a.quality = float(match.group(2)) @@ -70,7 +69,7 @@ def _parse(hdr, result): # bad float literal pass elif pname == 'charset': - a.charset = string.lower(match.group(2)) + a.charset = match.group(2).lower() result.append(a) if hdr[pos:pos+1] == ',': diff --git a/lib/blame.py b/lib/blame.py index 28dbf6da..ab023b80 100644 --- a/lib/blame.py +++ b/lib/blame.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # Copyright (C) 2000 Curt Hagenlocher # # By using this file, you agree to the terms and conditions set forth in @@ -27,14 +27,14 @@ # ----------------------------------------------------------------------- import sys -import string import os import re import time import math -import cgi -import vclib +from common import _item +import vclib +import sapi re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"') @@ -43,7 +43,7 @@ def link_includes(text, repos, path_parts, include_url): if match: incfile = match.group(3) include_path_parts = path_parts[:-1] - for part in filter(None, string.split(incfile, '/')): + for part in filter(None, incfile.split('/')): if part == "..": if not include_path_parts: # nothing left to pop; don't bother marking up this include. @@ -55,14 +55,14 @@ def link_includes(text, repos, path_parts, include_url): include_path = None try: if repos.itemtype(include_path_parts, None) == vclib.FILE: - include_path = string.join(include_path_parts, '/') + include_path = '/'.join(include_path_parts) except vclib.ItemNotFound: pass if include_path: return '#%sinclude%s"%s"' % \ (match.group(1), match.group(2), - string.replace(include_url, '/WHERE/', include_path), incfile) + include_url.replace('/WHERE/', include_path), incfile) return text @@ -75,14 +75,15 @@ class HTMLBlameSource: self.path_parts = path_parts self.diff_url = diff_url self.include_url = include_url - self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev) + self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev, + True) def __getitem__(self, idx): item = self.annotation.__getitem__(idx) diff_url = None if item.prev_rev: diff_url = '%sr1=%s&r2=%s' % (self.diff_url, item.prev_rev, item.rev) - thisline = link_includes(cgi.escape(item.text), self.repos, + thisline = link_includes(sapi.escape(item.text), self.repos, self.path_parts, self.include_url) return _item(text=thisline, line_number=item.line_number, rev=item.rev, prev_rev=item.prev_rev, @@ -94,11 +95,6 @@ def blame(repos, path_parts, diff_url, include_url, opt_rev=None): return source, source.revision -class _item: - def __init__(self, **kw): - vars(self).update(kw) - - def make_html(root, rcs_path): import vclib.ccvs.blame bs = vclib.ccvs.blame.BlameSource(os.path.join(root, rcs_path)) @@ -136,7 +132,7 @@ def make_html(root, rcs_path): sys.stdout.write('  ') rev_count = rev_count + 1 - sys.stdout.write('%s\n' % (align % 'left', string.rstrip(thisline) or ' ')) + sys.stdout.write('%s\n' % (align % 'left', thisline.rstrip() or ' ')) sys.stdout.write('\n') diff --git a/lib/common.py b/lib/common.py new file mode 100644 index 00000000..66d5a71a --- /dev/null +++ b/lib/common.py @@ -0,0 +1,60 @@ +# -*-python-*- +# +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# common: common definitions for the viewvc library +# +# ----------------------------------------------------------------------- + +# Special type indicators for diff header processing and idiff return codes +_RCSDIFF_IS_BINARY = 'binary-diff' +_RCSDIFF_ERROR = 'error' +_RCSDIFF_NO_CHANGES = "no-changes" + + +class _item: + def __init__(self, **kw): + vars(self).update(kw) + + +class TemplateData: + """A custom dictionary-like object that allows one-time definition + of keys, and only value fetches and changes, and key deletions, + thereafter. + + EZT doesn't require the use of this special class -- a normal + dict-type data dictionary works fine. But use of this class will + assist those who want the data sent to their templates to have a + consistent set of keys.""" + + def __init__(self, initial_data={}): + self._items = initial_data + + def __getitem__(self, key): + return self._items.__getitem__(key) + + def __setitem__(self, key, item): + assert self._items.has_key(key) + return self._items.__setitem__(key, item) + + def __delitem__(self, key): + return self._items.__delitem__(key) + + def keys(self): + return self._items.keys() + + def merge(self, template_data): + """Merge the data in TemplataData instance TEMPLATA_DATA into this + instance. Avoid the temptation to use this conditionally in your + code -- it rather defeats the purpose of this class.""" + + assert isinstance(template_data, TemplateData) + self._items.update(template_data._items) diff --git a/lib/compat.py b/lib/compat.py deleted file mode 100644 index 3b220f8d..00000000 --- a/lib/compat.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*-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/ -# -# ----------------------------------------------------------------------- -# -# compat.py: compatibility functions for operation across Python 1.5.x to 2.2.x -# -# ----------------------------------------------------------------------- - -import urllib -import string -import time -import calendar -import re -import os -import rfc822 -import tempfile -import errno - -# -# urllib.urlencode() is new to Python 1.5.2 -# -try: - urlencode = urllib.urlencode -except AttributeError: - def urlencode(dict): - "Encode a dictionary as application/x-url-form-encoded." - if not dict: - return '' - quote = urllib.quote_plus - keyvalue = [ ] - for key, value in dict.items(): - keyvalue.append(quote(key) + '=' + quote(str(value))) - return string.join(keyvalue, '&') - -# -# time.strptime() is new to Python 1.5.2 -# -if hasattr(time, 'strptime'): - def cvs_strptime(timestr): - 'Parse a CVS-style date/time value.' - return time.strptime(timestr, '%Y/%m/%d %H:%M:%S')[:-1] + (0,) -else: - _re_rev_date = re.compile('([0-9]{4})/([0-9][0-9])/([0-9][0-9]) ' - '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])') - def cvs_strptime(timestr): - 'Parse a CVS-style date/time value.' - match = _re_rev_date.match(timestr) - if match: - return tuple(map(int, match.groups())) + (0, 1, 0) - else: - raise ValueError('date is not in cvs format') - -# -# os.makedirs() is new to Python 1.5.2 -# -try: - makedirs = os.makedirs -except AttributeError: - def makedirs(path, mode=0777): - head, tail = os.path.split(path) - if head and tail and not os.path.exists(head): - makedirs(head, mode) - os.mkdir(path, mode) - -# -# rfc822.formatdate() is new to Python 1.6 -# -try: - formatdate = rfc822.formatdate -except AttributeError: - def formatdate(timeval): - if timeval is None: - timeval = time.time() - timeval = time.gmtime(timeval) - return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( - ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][timeval[6]], - timeval[2], - ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][timeval[1]-1], - timeval[0], timeval[3], timeval[4], timeval[5]) - -# -# calendar.timegm() is new to Python 2.x and -# calendar.leapdays() was wrong in Python 1.5.2 -# -try: - timegm = calendar.timegm -except AttributeError: - def leapdays(year1, year2): - """Return number of leap years in range [year1, year2). - Assume year1 <= year2.""" - year1 = year1 - 1 - year2 = year2 - 1 - return (year2/4 - year1/4) - (year2/100 - - year1/100) + (year2/400 - year1/400) - - EPOCH = 1970 - def timegm(tuple): - """Unrelated but handy function to calculate Unix timestamp from GMT.""" - year, month, day, hour, minute, second = tuple[:6] - # assert year >= EPOCH - # assert 1 <= month <= 12 - days = 365*(year-EPOCH) + leapdays(EPOCH, year) - for i in range(1, month): - days = days + calendar.mdays[i] - if month > 2 and calendar.isleap(year): - days = days + 1 - days = days + day - 1 - hours = days*24 + hour - minutes = hours*60 + minute - seconds = minutes*60 + second - return seconds - -# -# tempfile.mkdtemp() is new to Python 2.3 -# -try: - mkdtemp = tempfile.mkdtemp -except AttributeError: - def mkdtemp(suffix="", prefix="tmp", dir=None): - # mktemp() only took a single suffix argument until Python 2.3. - # We'll do the best we can. - oldtmpdir = os.environ.get('TMPDIR') - try: - for i in range(10): - if dir: - os.environ['TMPDIR'] = dir - dir = tempfile.mktemp(suffix) - if prefix: - parent, base = os.path.split(dir) - dir = os.path.join(parent, prefix + base) - try: - os.mkdir(dir, 0700) - return dir - except OSError, e: - if e.errno == errno.EEXIST: - continue # try again - raise - finally: - if oldtmpdir: - os.environ['TMPDIR'] = oldtmpdir - elif os.environ.has_key('TMPDIR'): - del(os.environ['TMPDIR']) - - raise IOError, (errno.EEXIST, "No usable temporary directory name found") - -# -# the following stuff is *ONLY* needed for standalone.py. -# For that reason I've encapsulated it into a function. -# - -def for_standalone(): - import SocketServer - if not hasattr(SocketServer.TCPServer, "close_request"): - # - # method close_request() was missing until Python 2.1 - # - class TCPServer(SocketServer.TCPServer): - def process_request(self, request, client_address): - """Call finish_request. - - Overridden by ForkingMixIn and ThreadingMixIn. - - """ - self.finish_request(request, client_address) - self.close_request(request) - - def close_request(self, request): - """Called to clean up an individual request.""" - request.close() - - SocketServer.TCPServer = TCPServer diff --git a/lib/config.py b/lib/config.py index 8debb970..e9a93acf 100644 --- a/lib/config.py +++ b/lib/config.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -16,7 +16,6 @@ import sys import os -import string import ConfigParser import fnmatch import vclib @@ -29,99 +28,164 @@ from viewvcmagic import ContentMagic ######################################################################### # # CONFIGURATION +# ------------- # # There are three forms of configuration: # -# 1) edit the viewvc.conf created by the viewvc-install(er) -# 2) as (1), but delete all unchanged entries from viewvc.conf -# 3) do not use viewvc.conf and just edit the defaults in this file +# 1. edit the viewvc.conf created by the viewvc-install(er) +# 2. as (1), but delete all unchanged entries from viewvc.conf +# 3. do not use viewvc.conf and just edit the defaults in this file # # Most users will want to use (1), but there are slight speed advantages # to the other two options. Note that viewvc.conf values are a bit easier # to work with since it is raw text, rather than python literal values. # +# +# A WORD ABOUT OPTION LAYERING/OVERRIDES +# -------------------------------------- +# +# ViewVC has three "layers" of configuration options: +# +# 1. base configuration options - very basic configuration bits +# found in sections like 'general', 'options', etc. +# 2. vhost overrides - these options overlay/override the base +# configuration on a per-vhost basis. +# 3. root overrides - these options overlay/override the base +# configuration and vhost overrides on a per-root basis. +# +# Here's a diagram of the valid overlays/overrides: +# +# PER-ROOT PER-VHOST BASE +# +# ,-----------. ,-----------. +# | vhost-*/ | | | +# | general | --> | general | +# | | | | +# `-----------' `-----------' +# ,-----------. ,-----------. ,-----------. +# | root-*/ | | vhost-*/ | | | +# | options | --> | options | --> | options | +# | | | | | | +# `-----------' `-----------' `-----------' +# ,-----------. ,-----------. ,-----------. +# | root-*/ | | vhost-*/ | | | +# | templates | --> | templates | --> | templates | +# | | | | | | +# `-----------' `-----------' `-----------' +# ,-----------. ,-----------. ,-----------. +# | root-*/ | | vhost-*/ | | | +# | utilities | --> | utilities | --> | utilities | +# | | | | | | +# `-----------' `-----------' `-----------' +# ,-----------. ,-----------. +# | vhost-*/ | | | +# | cvsdb | --> | cvsdb | +# | | | | +# `-----------' `-----------' +# ,-----------. ,-----------. ,-----------. +# | root-*/ | | vhost-*/ | | | +# | authz-* | --> | authz-* | --> | authz-* | +# | | | | | | +# `-----------' `-----------' `-----------' +# ,-----------. +# | | +# | vhosts | +# | | +# `-----------' +# ,-----------. +# | | +# | query | +# | | +# `-----------' +# +# ### TODO: Figure out what this all means for the 'kv' stuff. +# ######################################################################### class Config: - _sections = ('general', 'utilities', 'options', 'cvsdb', 'templates', 'rewritehtml') - _force_multi_value = ('cvs_roots', 'svn_roots', 'languages', 'kv_files', - 'root_parents', 'allowed_views', 'mime_types_files') + _base_sections = ( + # Base configuration sections. + 'authz-*', + 'cvsdb', + 'general', + 'options', + 'query', + 'templates', + 'utilities', + ) + _force_multi_value = ( + # Configuration values with multiple, comma-separated values. + 'allowed_views', + 'binary_mime_types', + 'custom_log_formatting', + 'cvs_roots', + 'kv_files', + 'languages', + 'mime_types_files', + 'root_parents', + 'svn_roots', + ) + _allowed_overrides = { + # Mapping of override types to allowed overridable sections. + 'vhost' : ('authz-*', + 'cvsdb', + 'general', + 'options', + 'templates', + 'utilities', + ), + 'root' : ('authz-*', + 'options', + 'templates', + 'utilities', + ) + } def __init__(self): self.__guesser = None - for section in self._sections: + self.root_options_overlayed = 0 + for section in self._base_sections: + if section[-1] == '*': + continue setattr(self, section, _sub_config()) - def load_config(self, pathname, vhost=None, rootname=None): + def load_config(self, pathname, vhost=None): + """Load the configuration file at PATHNAME, applying configuration + settings there as overrides to the built-in default values. If + VHOST is provided, also process the configuration overrides + specific to that virtual host.""" + self.conf_path = os.path.isfile(pathname) and pathname or None self.base = os.path.dirname(pathname) self.parser = ConfigParser.ConfigParser() + self.parser.optionxform = lambda x: x # don't case-normalize option names. self.parser.read(self.conf_path or []) - - for section in self._sections: - if self.parser.has_section(section): + + for section in self.parser.sections(): + if self._is_allowed_section(section, self._base_sections): self._process_section(self.parser, section, section) if vhost and self.parser.has_section('vhosts'): self._process_vhost(self.parser, vhost) - if rootname: - self._process_root_options(self.parser, rootname) - self.expand_root_parents() - r = {} - for i in self.rewritehtml.__dict__.keys(): - if i[-8:] == '.replace': - if r.get(i[:-8], None) is None: - r[i[:-8]] = ['',''] - r[i[:-8]][1] = self.rewritehtml.__dict__[i] - if i[-5:] == '.find': - if r.get(i[:-5], None) is None: - r[i[:-5]] = ['',''] - r[i[:-5]][0] = self.rewritehtml.__dict__[i] - for i in r.keys(): - if r[i][0] != '': - viewvc.add_rewrite_html(r[i][0], r[i][1]) - - def expand_root_parents(self): - """Expand the configured root parents into individual roots.""" - - # Each item in root_parents is a "directory : repo_type" string. - for pp in self.general.root_parents: - pos = string.rfind(pp, ':') - if pos < 0: - raise debug.ViewVCException( - "The path '%s' in 'root_parents' does not include a " - "repository type." % (pp)) - - repo_type = string.strip(pp[pos+1:]) - pp = os.path.normpath(string.strip(pp[:pos])) - - if repo_type == 'cvs': - roots = vclib.ccvs.expand_root_parent(pp) - if self.options.hide_cvsroot and roots.has_key('CVSROOT'): - del roots['CVSROOT'] - self.general.cvs_roots.update(roots) - elif repo_type == 'svn': - roots = vclib.svn.expand_root_parent(pp) - self.general.svn_roots.update(roots) - else: - raise debug.ViewVCException( - "The path '%s' in 'root_parents' has an unrecognized " - "repository type." % (pp)) - def load_kv_files(self, language): + """Process the key/value (kv) files specified in the + configuration, merging their values into the configuration as + dotted heirarchical items.""" + kv = _sub_config() for fname in self.general.kv_files: if fname[0] == '[': - idx = string.index(fname, ']') - parts = string.split(fname[1:idx], '.') - fname = string.strip(fname[idx+1:]) + idx = fname.index(']') + parts = fname[1:idx].split('.') + fname = fname[idx+1:].strip() else: parts = [ ] - fname = string.replace(fname, '%lang%', language) + fname = fname.replace('%lang%', language) parser = ConfigParser.ConfigParser() + parser.optionxform = lambda x: x # don't case-normalize option names. parser.read(os.path.join(self.base, fname)) for section in parser.sections(): for option in parser.options(section): @@ -139,75 +203,109 @@ class Config: return kv def path(self, path): - """Return path relative to the config file directory""" + """Return PATH relative to the config file directory.""" return os.path.join(self.base, path) def _process_section(self, parser, section, subcfg_name): + if not hasattr(self, subcfg_name): + setattr(self, subcfg_name, _sub_config()) sc = getattr(self, subcfg_name) for opt in parser.options(section): value = parser.get(section, opt) if opt in self._force_multi_value: - value = map(string.strip, filter(None, string.split(value, ','))) + value = map(lambda x: x.strip(), filter(None, value.split(','))) else: try: value = int(value) except ValueError: pass + ### FIXME: This feels like unnecessary depth of knowledge for a + ### semi-generic configuration object. if opt == 'cvs_roots' or opt == 'svn_roots': value = _parse_roots(opt, value) setattr(sc, opt, value) + def _is_allowed_section(self, section, allowed_sections): + """Return 1 iff SECTION is an allowed section, defined as being + explicitly present in the ALLOWED_SECTIONS list or present in the + form 'someprefix-*' in that list.""" + + for allowed_section in allowed_sections: + if allowed_section[-1] == '*': + if _startswith(section, allowed_section[:-1]): + return 1 + elif allowed_section == section: + return 1 + return 0 + + def _is_allowed_override(self, sectype, secspec, section): + """Test if SECTION is an allowed override section for sections of + type SECTYPE ('vhosts' or 'root', currently) and type-specifier + SECSPEC (a rootname or vhostname, currently). If it is, return + the overridden base section name. If it's not an override section + at all, return None. And if it's an override section but not an + allowed one, raise IllegalOverrideSection.""" + + cv = '%s-%s/' % (sectype, secspec) + lcv = len(cv) + if section[:lcv] != cv: + return None + base_section = section[lcv:] + if self._is_allowed_section(base_section, + self._allowed_overrides[sectype]): + return base_section + raise IllegalOverrideSection(sectype, section) + def _process_vhost(self, parser, vhost): - # find a vhost name for this vhost, if any (if not, we've nothing to do) + # Find a vhost name for this VHOST, if any (else, we've nothing to do). canon_vhost = self._find_canon_vhost(parser, vhost) if not canon_vhost: return - # overlay any option sections associated with this vhost name - cv = 'vhost-%s/' % (canon_vhost) - lcv = len(cv) + # Overlay any option sections associated with this vhost name. for section in parser.sections(): - if section[:lcv] == cv: - base_section = section[lcv:] - if base_section not in self._sections: - raise IllegalOverrideSection('vhost', section) + base_section = self._is_allowed_override('vhost', canon_vhost, section) + if base_section: self._process_section(parser, section, base_section) def _find_canon_vhost(self, parser, vhost): - vhost = string.split(string.lower(vhost), ':')[0] # lower-case, no port + vhost = vhost.lower().split(':')[0] # lower-case, no port for canon_vhost in parser.options('vhosts'): value = parser.get('vhosts', canon_vhost) - patterns = map(string.lower, map(string.strip, - filter(None, string.split(value, ',')))) + patterns = map(lambda x: x.lower().strip(), + filter(None, value.split(','))) for pat in patterns: if fnmatch.fnmatchcase(vhost, pat): return canon_vhost return None - def _process_root_options(self, parser, rootname): - rn = 'root-%s/' % (rootname) - lrn = len(rn) - for section in parser.sections(): - if section[:lrn] == rn: - base_section = section[lrn:] - if base_section in self._sections: - if base_section == 'general': - raise IllegalOverrideSection('root', section) - self._process_section(parser, section, base_section) - elif _startswith(base_section, 'authz-'): - pass - else: - raise IllegalOverrideSection('root', section) - def overlay_root_options(self, rootname): - "Overly per-root options atop the existing option set." + """Overlay per-root options for ROOTNAME atop the existing option + set. This is a destructive change to the configuration.""" + + did_overlay = 0 + if not self.conf_path: return - self._process_root_options(self.parser, rootname) + + for section in self.parser.sections(): + base_section = self._is_allowed_override('root', rootname, section) + if base_section: + # We can currently only deal with root overlays happening + # once, so check that we've not yet done any overlaying of + # per-root options. + assert(self.root_options_overlayed == 0) + self._process_section(self.parser, section, base_section) + did_overlay = 1 + + # If we actually did any overlaying, remember this fact so we + # don't do it again later. + if did_overlay: + self.root_options_overlayed = 1 def _get_parser_items(self, parser, section): """Basically implement ConfigParser.items() for pre-Python-2.3 versions.""" @@ -219,23 +317,66 @@ class Config: d[option] = parser.get(section, option) return d.items() - def get_authorizer_params(self, authorizer, rootname=None): - if not self.conf_path: - return {} + def get_authorizer_and_params_hack(self, rootname): + """Return a 2-tuple containing the name and parameters of the + authorizer configured for use with ROOTNAME. + ### FIXME: This whole thing is a hack caused by our not being able + ### to non-destructively overlay root options when trying to do + ### something like a root listing (which might need to get + ### different authorizer bits for each and every root in the list). + ### Until we have a good way to do that, we expose this function, + ### which assumes that base and per-vhost configuration has been + ### absorbed into this object and that per-root options have *not* + ### been overlayed. See issue #371.""" + + # We assume that per-root options have *not* been overlayed. + assert(self.root_options_overlayed == 0) + + if not self.conf_path: + return None, {} + + # Figure out the authorizer by searching first for a per-root + # override, then falling back to the base/vhost configuration. + authorizer = None + root_options_section = 'root-%s/options' % (rootname) + if self.parser.has_section(root_options_section) \ + and self.parser.has_option(root_options_section, 'authorizer'): + authorizer = self.parser.get(root_options_section, 'authorizer') + if not authorizer: + authorizer = self.options.authorizer + + # No authorizer? Get outta here. + if not authorizer: + return None, {} + + # Dig up the parameters for the authorizer, starting with the + # base/vhost items, then overlaying any root-specific ones we find. params = {} authz_section = 'authz-%s' % (authorizer) + if hasattr(self, authz_section): + sub_config = getattr(self, authz_section) + for attr in dir(sub_config): + params[attr] = getattr(sub_config, attr) + root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer) for section in self.parser.sections(): - if section == authz_section: + if section == root_authz_section: for key, value in self._get_parser_items(self.parser, section): params[key] = value - if rootname: - root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer) - for section in self.parser.sections(): - 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): + """Return a dictionary of parameter names and values which belong + to the configured authorizer (or AUTHORIZER, if provided).""" + params = {} + if authorizer is None: + authorizer = self.options.authorizer + if authorizer: + authz_section = 'authz-%s' % (self.options.authorizer) + if hasattr(self, authz_section): + sub_config = getattr(self, authz_section) + for attr in dir(sub_config): + params[attr] = getattr(sub_config, attr) return params def guesser(self): @@ -272,11 +413,14 @@ class Config: self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots'] self.options.authorizer = None self.options.mangle_email_addresses = 0 + self.options.custom_log_formatting = [] self.options.default_file_view = "log" + self.options.binary_mime_types = [] self.options.http_expiration_time = 600 self.options.generate_etags = 1 self.options.svn_ignore_mimetype = 0 self.options.svn_config_dir = None + self.options.max_filesize_kbytes = 512 self.options.use_rcsparse = 0 self.options.sort_by = 'file' self.options.sort_group_dirs = 1 @@ -291,15 +435,18 @@ class Config: self.options.hr_ignore_keyword_subst = 1 self.options.hr_intraline = 0 self.options.allow_compress = 0 - self.options.template_dir = "templates" + self.options.template_dir = "templates/default" self.options.docroot = None self.options.show_subdir_lastmod = 0 + self.options.show_roots_lastmod = 0 self.options.show_logs = 1 self.options.show_log_in_markup = 1 - self.options.cross_copies = 0 + self.options.cross_copies = 1 self.options.use_localtime = 0 + self.options.iso8601_timestamps = 0 self.options.short_log_len = 80 self.options.enable_syntax_coloration = 1 + self.options.tabsize = 8 self.options.detect_encoding = 0 self.options.use_cvsgraph = 0 self.options.cvsgraph_conf = "cvsgraph.conf" @@ -307,6 +454,7 @@ class Config: self.options.use_re_search = 0 self.options.dir_pagesize = 0 self.options.log_pagesize = 0 + self.options.log_pagesextra = 3 self.options.limit_changes = 100 self.options.cvs_ondisk_charset = 'cp1251' self.options.binary_mime_re = '^(?!text/|.*\Wxml)' @@ -352,17 +500,19 @@ class Config: 'after_match: \n'\ 'chunk_separator: ... \n\n' + self.query.viewvc_base_url = None + def _startswith(somestr, substr): return somestr[:len(substr)] == substr def _parse_roots(config_name, config_value): roots = { } for root in config_value: - pos = string.find(root, ':') - if pos < 0: + try: + name, path = root.split(':', 1) + except: raise MalformedRoot(config_name, root) - name, path = map(string.strip, (root[:pos], root[pos+1:])) - roots[name] = path + roots[name.strip()] = path.strip() return roots class ViewVCConfigurationError(Exception): @@ -388,10 +538,3 @@ class MalformedRoot(ViewVCConfigurationError): class _sub_config: pass - -if not hasattr(sys, 'hexversion'): - # Python 1.5 or 1.5.1. fix the syntax for ConfigParser options. - import regex - ConfigParser.option_cre = regex.compile('^\([-A-Za-z0-9._]+\)\(:\|[' - + string.whitespace - + ']*=\)\(.*\)$') diff --git a/lib/cvsdb.py b/lib/cvsdb.py index 25516fac..e350115c 100644 --- a/lib/cvsdb.py +++ b/lib/cvsdb.py @@ -1,5 +1,5 @@ # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -11,7 +11,6 @@ import os import sys -import string import time import re import cgi @@ -219,6 +218,9 @@ class CheckinDatabase: return list + def GetCommitsTable(self): + return self._version >= 1 and 'commits' or 'checkins' + def GetTableList(self): sql = "SHOW TABLES" cursor = self.db.cursor() @@ -384,13 +386,11 @@ class CheckinDatabase: 'descid' : self.GetDescriptionID(commit.GetDescription()), } - commits_table = self._version >= 1 and 'commits' or 'checkins' - cursor = self.db.cursor() try: # MySQL-specific INSERT-or-UPDATE with ID retrieval cursor.execute( - 'INSERT INTO '+commits_table+'('+','.join(i for i in props)+') VALUES ('+ + 'INSERT INTO '+self.GetCommitsTable()+'('+','.join(i for i in props)+') VALUES ('+ ', '.join('%s' for i in props)+') ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id), '+ ', '.join(i+'=VALUES('+i+')' for i in props), tuple(props[i] for i in props) @@ -447,8 +447,10 @@ class CheckinDatabase: elif query_entry.match == "glob": # check if the match is exact if not re.match(r'(\*|\?|\[.*\])', data): + # most optimal is just '=' for exact matches match = "=" else: + # LIKE is more optimal than REGEXP data = data.replace('%', '\\%') data = data.replace('_', '\\_') data = data.replace('*', '%') @@ -465,7 +467,7 @@ class CheckinDatabase: if match != '': sqlList.append("%s%s%s" % (field, match, self.db.literal(data))) - return "(%s)" % (string.join(sqlList, " OR ")) + return "(%s)" % (" OR ".join(sqlList)) def query_ids(self, in_field, table, id_field, name_field, lst): if not len(lst): @@ -529,7 +531,7 @@ class CheckinDatabase: ) def CreateSQLQueryString(self, query): - commits_table = self._version >= 1 and 'commits' or 'checkins' + commits_table = self.GetCommitsTable() fields = [ commits_table+".*", "repositories.repository AS repository_name", @@ -618,17 +620,18 @@ class CheckinDatabase: if cond is not None: joinConds.append(cond) fields = string.join(fields, ",") - tables = string.join(tables, ",") - conditions = string.join(joinConds + condList, " AND ") + tables = ",".join(tables) + conditions = " AND ".join(joinConds + condList) conditions = conditions and "WHERE %s" % conditions - ## limit the number of rows requested or we could really slam - ## a server with a large database + ## apply the query's row limit, if any (so we avoid really + ## slamming a server with a large database) limit = "" if query.limit: - limit = "LIMIT %s" % (str(query.limit)) - elif self._row_limit: - limit = "LIMIT %s" % (str(self._row_limit)) + if detect_leftover: + limit = "LIMIT %s" % (str(query.limit + 1)) + else: + limit = "LIMIT %s" % (str(query.limit)) sql = "SELECT %s FROM %s %s %s %s" % ( fields, tables, conditions, order_by, limit) @@ -636,6 +639,7 @@ class CheckinDatabase: return sql # Check access to dir/file in repository repos + # FIXME Should probably be moved outside of CheckinDatabase, but here by now def check_commit_access(self, repos, dir, file, rev): r = self.request.get_repo(repos) if not r: @@ -799,13 +803,12 @@ class CheckinDatabase: if file_id == None: return None - commits_table = self._version >= 1 and 'commits' or 'checkins' sql = "SELECT whoid FROM %s WHERE "\ " repositoryid=%%s "\ " AND dirid=%%s"\ " AND fileid=%%s"\ " AND revision=%%s"\ - % (commits_table) + % (self.GetCommitsTable()) sql_args = (repository_id, dir_id, file_id, commit.GetRevision()) cursor = self.db.cursor() @@ -821,10 +824,9 @@ class CheckinDatabase: def sql_delete(self, table, key, value, keep_fkey = None): sql = "DELETE FROM %s WHERE %s=%%s" % (table, key) sql_args = (value, ) - commits_table = self._version >= 1 and 'commits' or 'checkins' if keep_fkey: sql += " AND %s NOT IN (SELECT %s FROM %s WHERE %s = %%s)" \ - % (key, keep_fkey, commits_table, keep_fkey) + % (key, keep_fkey, self.GetCommitsTable(), keep_fkey) sql_args = (value, value) cursor = self.db.cursor() cursor.execute(sql, sql_args) @@ -842,7 +844,7 @@ class CheckinDatabase: raise UnknownRepositoryError("Unknown repository '%s'" % (repository)) - checkins_table = self._version >= 1 and 'commits' or 'checkins' + checkins_table = self.GetCommitsTable() # Purge checkins cursor = self.db.cursor() @@ -1085,8 +1087,9 @@ class QueryEntry: self.data = data self.match = match -## CheckinDatabaseQueryData is a object which contains the search parameters -## for a query to the CheckinDatabase +## CheckinDatabaseQuery is an object which contains the search +## parameters for a query to the Checkin Database and -- after the +## query is executed -- the data returned by the query. class CheckinDatabaseQuery: def __init__(self): ## sorting @@ -1112,7 +1115,8 @@ class CheckinDatabaseQuery: ## limit on number of rows to return self.limit = None - + self.limit_reached = 0 + ## list of commits -- filled in by CVS query self.commit_list = [] @@ -1120,6 +1124,9 @@ class CheckinDatabaseQuery: ## are added self.commit_cb = None + ## has this query been run? + self.executed = 0 + def SetTextQuery(self, query): self.text_query = query @@ -1180,6 +1187,20 @@ class CheckinDatabaseQuery: def AddCommit(self, commit): self.commit_list.append(commit) + def SetExecuted(self): + self.executed = 1 + + def SetLimitReached(self): + self.limit_reached = 1 + + def GetLimitReached(self): + assert self.executed + return self.limit_reached + + def GetCommitList(self): + assert self.executed + return self.commit_list + ## ## entrypoints @@ -1190,13 +1211,15 @@ def CreateCommit(): def CreateCheckinQuery(): return CheckinDatabaseQuery() -def ConnectDatabase(cfg, request=None, readonly=0): - db = CheckinDatabase( - readonly = readonly, - request = request, - cfg = cfg.cvsdb, - guesser = cfg.guesser(), - ) +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) db.Connect() return db @@ -1207,7 +1230,7 @@ def ConnectDatabaseReadOnly(cfg, request): def GetCommitListFromRCSFile(repository, path_parts, revision=None): commit_list = [] - directory = string.join(path_parts[:-1], "/") + directory = "/".join(path_parts[:-1]) file = path_parts[-1] revs = repository.itemlog(path_parts, revision, vclib.SORTBY_DEFAULT, @@ -1224,7 +1247,7 @@ def GetCommitListFromRCSFile(repository, path_parts, revision=None): if rev.changed: # extract the plus/minus and drop the sign - plus, minus = string.split(rev.changed) + plus, minus = rev.changed.split() commit.SetPlusCount(plus[1:]) commit.SetMinusCount(minus[1:]) diff --git a/lib/dbi.py b/lib/dbi.py index 6c56282c..8ac76f97 100644 --- a/lib/dbi.py +++ b/lib/dbi.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -14,7 +14,7 @@ import sys import time import types import re -import compat +import calendar import MySQLdb # set to 1 to store commit times in UTC, or 0 to use the ViewVC machine's @@ -55,7 +55,7 @@ def TicksFromDateTime(datetime): t = datetime.tuple() if utc_time: - return compat.timegm(t) + return calendar.timegm(t) else: return time.mktime(t[:8] + (-1,)) diff --git a/lib/debug.py b/lib/debug.py index df7e07b0..c17fac15 100644 --- a/lib/debug.py +++ b/lib/debug.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 diff --git a/lib/ezt.py b/lib/ezt.py index f27cd171..cc714fc6 100644 --- a/lib/ezt.py +++ b/lib/ezt.py @@ -347,7 +347,7 @@ class Template: for_names = [ ] if base_format: - program.append((self._cmd_format, _printers[base_format])) + program.append((self._cmd_format, _formatters[base_format])) for i in range(len(parts)): piece = parts[i] @@ -405,13 +405,13 @@ class Template: elif cmd == 'format': if args[1][0]: # argument is a variable reference - printer = args[1] + formatter = args[1] else: - # argument is a string constant referring to built-in printer - printer = _printers.get(args[1][1]) - if not printer: + # argument is a string constant referring to built-in formatter + formatter = _formatters.get(args[1][1]) + if not formatter: raise UnknownFormatConstantError(str(args[1:])) - program.append((self._cmd_format, printer)) + program.append((self._cmd_format, formatter)) # remember the cmd, current pos, args, and a section placeholder stack.append([cmd, len(program), args[1:], None]) @@ -465,13 +465,13 @@ class Template: except TypeError: raise Exception("Unprintable value type for '%s'" % (str(valrefs[0][0]))) - def _cmd_format(self, printer, ctx): - if type(printer) is TupleType: - printer = _get_value(printer, ctx) - ctx.printers.append(printer) + def _cmd_format(self, formatter, ctx): + if type(formatter) is TupleType: + formatter = _get_value(formatter, ctx) + ctx.formatters.append(formatter) def _cmd_end_format(self, valref, ctx): - ctx.printers.pop() + ctx.formatters.pop() def _cmd_include(self, (valref, reader), ctx): fname = _get_value(valref, ctx) @@ -637,14 +637,23 @@ def _get_value((refname, start, rest), ctx): # string or a sequence return ob +def _print_formatted(formatters, ctx, chunk): + # print chunk to ctx.fp after running it sequentially through formatters + for formatter in formatters: + chunk = formatter(chunk) + ctx.fp.write(chunk) + def _write_value(value, args, ctx): # value is a callback function, generates its own output if callable(value): apply(value, [ctx] + list(args)) return - # pop printer in case it recursively calls _write_value - printer = ctx.printers.pop() + # squirrel away formatters in case one of them recursively calls + # _write_value() -- we'll use them (in reverse order) to format our + # output. + formatters = ctx.formatters[:] + formatters.reverse() try: # if the value has a 'read' attribute, then it is a stream: copy it @@ -653,7 +662,7 @@ def _write_value(value, args, ctx): chunk = value.read(16384) if not chunk: break - printer(ctx, chunk) + _print_formatted(formatters, ctx, chunk) # value is a substitution pattern elif args: @@ -666,57 +675,23 @@ def _write_value(value, args, ctx): piece = args[idx] else: piece = '' - printer(ctx, piece) + _print_formatted(formatters, ctx, piece) # plain old value, write to output else: - printer(ctx, value) + _print_formatted(formatters, ctx, value) finally: - ctx.printers.append(printer) - - -class TemplateData: - """A custom dictionary-like object that allows one-time definition - of keys, and only value fetches and changes, and key deletions, - thereafter. - - EZT doesn't require the use of this special class -- a normal - dict-type data dictionary works fine. But use of this class will - assist those who want the data sent to their templates to have a - consistent set of keys.""" - - def __init__(self, initial_data={}): - initial_data['env_user_url'] = os.environ.get('user_url', '') - self._items = initial_data - - def __getitem__(self, key): - return self._items.__getitem__(key) - - def __setitem__(self, key, item): - assert self._items.has_key(key) - return self._items.__setitem__(key, item) - - def __delitem__(self, key): - return self._items.__delitem__(key) - - def keys(self): - return self._items.keys() - - def merge(self, template_data): - """Merge the data in TemplataData instance TEMPLATA_DATA into this - instance. Avoid the temptation to use this conditionally in your - code -- it rather defeats the purpose of this class.""" - - assert isinstance(template_data, TemplateData) - self._items.update(template_data._items) + # restore our formatters + formatters.reverse() + ctx.formatters = formatters class Context: """A container for the execution context""" def __init__(self, fp): self.fp = fp - self.printers = [] + self.formatters = [] def write(self, value, args=()): _write_value(value, args, self) @@ -829,26 +804,34 @@ class BaseUnavailableError(EZTException): class UnknownFormatConstantError(EZTException): """The format specifier is an unknown value.""" -def _raw_printer(ctx, s): +def _raw_formatter(s): try: s = s.encode('utf-8') except: pass - ctx.fp.write(s) + return s -def _html_printer(ctx, s): +def _html_formatter(s): try: s = s.encode('utf-8') except: pass - ctx.fp.write(cgi.escape(s)) + return cgi.escape(s) -def _uri_printer(ctx, s): +def _xml_formatter(s): try: s = s.encode('utf-8') except: pass - ctx.fp.write(urllib.quote(s)) + s = s.replace('&', '&') + s = s.replace('<', '<') + s = s.replace('>', '>') + return s -_printers = { - FORMAT_RAW : _raw_printer, - FORMAT_HTML : _html_printer, - FORMAT_XML : _html_printer, - FORMAT_URI : _uri_printer, +def _uri_formatter(s): + try: s = s.encode('utf-8') + except: pass + return urllib.quote(s) + +_formatters = { + FORMAT_RAW : _raw_formatter, + FORMAT_HTML : _html_formatter, + FORMAT_XML : _xml_formatter, + FORMAT_URI : _uri_formatter, } # --- standard test environment --- diff --git a/lib/idiff.py b/lib/idiff.py index b4cf36e6..0dfd60d1 100644 --- a/lib/idiff.py +++ b/lib/idiff.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -19,8 +19,10 @@ from __future__ import generators import difflib import sys import re + +from common import _item, _RCSDIFF_NO_CHANGES import ezt -import cgi +import sapi def sidebyside(fromlines, tolines, context): """Generate side by side diff""" @@ -29,6 +31,7 @@ def sidebyside(fromlines, tolines, context): line_strip = lambda line: line.rstrip("\n") fromlines = map(line_strip, fromlines) tolines = map(line_strip, tolines) + had_changes = 0 gap = False for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context): @@ -37,8 +40,11 @@ def sidebyside(fromlines, tolines, context): else: from_item = _mdiff_split(flag, fromdata) to_item = _mdiff_split(flag, todata) - yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item)) + had_changes = 1 + yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item), type="intraline") gap = False + if not had_changes: + yield _item(type=_RCSDIFF_NO_CHANGES) _re_mdiff = re.compile("\0([+-^])(.*?)\1") @@ -49,18 +55,18 @@ def _mdiff_split(flag, (line_number, text)): while True: m = _re_mdiff.search(text, pos) if not m: - segments.append(_item(text=cgi.escape(text[pos:]), type=None)) + segments.append(_item(text=sapi.escape(text[pos:]), type=None)) break if m.start() > pos: - segments.append(_item(text=cgi.escape(text[pos:m.start()]), type=None)) + segments.append(_item(text=sapi.escape(text[pos:m.start()]), type=None)) if m.group(1) == "+": - segments.append(_item(text=cgi.escape(m.group(2)), type="add")) + segments.append(_item(text=sapi.escape(m.group(2)), type="add")) elif m.group(1) == "-": - segments.append(_item(text=cgi.escape(m.group(2)), type="remove")) + segments.append(_item(text=sapi.escape(m.group(2)), type="remove")) elif m.group(1) == "^": - segments.append(_item(text=cgi.escape(m.group(2)), type="change")) + segments.append(_item(text=sapi.escape(m.group(2)), type="change")) pos = m.end() @@ -71,19 +77,26 @@ def unified(fromlines, tolines, context): diff = difflib.Differ().compare(fromlines, tolines) lastrow = None + had_changes = 0 for row in _trim_context(diff, context): if row[0].startswith("? "): + had_changes = 1 yield _differ_split(lastrow, row[0]) lastrow = None else: if lastrow: + had_changes = 1 yield _differ_split(lastrow, None) lastrow = row if lastrow: + had_changes = 1 yield _differ_split(lastrow, None) + if not had_changes: + yield _item(type=_RCSDIFF_NO_CHANGES) + def _trim_context(lines, context_size): """Trim context lines that don't surround changes from Differ results @@ -166,20 +179,16 @@ def _differ_split(row, guide): for m in _re_differ.finditer(guide, pos): if m.start() > pos: - segments.append(_item(text=cgi.escape(line[pos:m.start()]), type=None)) - segments.append(_item(text=cgi.escape(line[m.start():m.end()]), + segments.append(_item(text=sapi.escape(line[pos:m.start()]), type=None)) + segments.append(_item(text=sapi.escape(line[m.start():m.end()]), type="change")) pos = m.end() - segments.append(_item(text=cgi.escape(line[pos:]), type=None)) + segments.append(_item(text=sapi.escape(line[pos:]), type=None)) return _item(gap=ezt.boolean(gap), type=type, segments=segments, left_number=left_number, right_number=right_number) -class _item: - def __init__(self, **kw): - vars(self).update(kw) - try: ### Using difflib._mdiff function here was the easiest way of obtaining ### intraline diffs for use in ViewVC, but it doesn't exist prior to diff --git a/lib/popen.py b/lib/popen.py index 3664010d..e6ab1328 100644 --- a/lib/popen.py +++ b/lib/popen.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -23,7 +23,6 @@ import os import sys import sapi import threading -import string if sys.platform == "win32": import win32popen @@ -36,7 +35,7 @@ def popen(cmd, args, mode, capture_err=1): if sys.platform == "win32": command = win32popen.CommandLine(cmd, args) - if string.find(mode, 'r') >= 0: + if mode.find('r') >= 0: hStdIn = None if debug.SHOW_CHILD_PROCESSES: @@ -85,7 +84,7 @@ def popen(cmd, args, mode, capture_err=1): # in the parent # close the descriptor that we don't need and return the other one. - if string.find(mode, 'r') >= 0: + if mode.find('r') >= 0: os.close(w) return _pipe(os.fdopen(r, mode), pid) os.close(r) @@ -96,7 +95,7 @@ def popen(cmd, args, mode, capture_err=1): # we'll need /dev/null for the discarded I/O null = os.open('/dev/null', os.O_RDWR) - if string.find(mode, 'r') >= 0: + if mode.find('r') >= 0: # hook stdout/stderr to the "write" channel os.dup2(w, 1) # "close" stdin; the child shouldn't use it @@ -125,7 +124,7 @@ def popen(cmd, args, mode, capture_err=1): os.execvp(cmd, (cmd,) + tuple(args)) except: # aid debugging, if the os.execvp above fails for some reason: - print "

exec failed:

", cmd, string.join(args), "
" + print "

exec failed:

", cmd, ' '.join(args), "
" raise # crap. shouldn't be here. diff --git a/lib/query.py b/lib/query.py index faa605be..ec248044 100644 --- a/lib/query.py +++ b/lib/query.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -23,6 +23,7 @@ import sys import string import time +from common import _item, TemplateData import cvsdb import viewvc import ezt @@ -57,7 +58,7 @@ class FormData: self.valid = 1 try: - self.repository = string.strip(form["repository"].value) + self.repository = form["repository"].value.strip() except KeyError: pass except TypeError: @@ -66,7 +67,7 @@ class FormData: self.valid = 1 try: - self.branch = string.strip(form["branch"].value) + self.branch = form["branch"].value.strip() except KeyError: pass except TypeError: @@ -75,7 +76,7 @@ class FormData: self.valid = 1 try: - self.directory = string.strip(form["directory"].value) + self.directory = form["directory"].value.strip() except KeyError: pass except TypeError: @@ -84,7 +85,7 @@ class FormData: self.valid = 1 try: - self.file = string.strip(form["file"].value) + self.file = form["file"].value.strip() except KeyError: pass except TypeError: @@ -93,7 +94,7 @@ class FormData: self.valid = 1 try: - self.who = string.strip(form["who"].value) + self.who = form["who"].value.strip() except KeyError: pass except TypeError: @@ -102,14 +103,14 @@ class FormData: self.valid = 1 try: - self.sortby = string.strip(form["sortby"].value) + self.sortby = form["sortby"].value.strip() except KeyError: pass except TypeError: pass try: - self.date = string.strip(form["date"].value) + self.date = form["date"].value.strip() except KeyError: pass except TypeError: @@ -168,7 +169,7 @@ def listparse_string(str): ## command; add the command and start over elif c == ",": ## strip ending whitespace on un-quoted data - temp = string.rstrip(temp) + temp = temp.rstrip() return_list.append( ("", temp) ) temp = "" state = "eat leading whitespace" @@ -227,8 +228,9 @@ def decode_command(cmd): else: return "exact" -def form_to_cvsdb_query(form_data): +def form_to_cvsdb_query(cfg, form_data): query = cvsdb.CreateCheckinQuery() + query.SetLimit(cfg.cvsdb.row_limit) if form_data.repository: for cmd, str in listparse_string(form_data.repository): @@ -281,18 +283,40 @@ def form_to_cvsdb_query(form_data): def prev_rev(rev): '''Returns a string representing the previous revision of the argument.''' - r = string.split(rev, '.') + r = rev.split('.') # decrement final revision component r[-1] = str(int(r[-1]) - 1) # prune if we pass the beginning of the branch if len(r) > 2 and r[-1] == '0': r = r[:-2] - return string.join(r, '.') + return '.'.join(r) def is_forbidden(cfg, cvsroot_name, module): - auth_params = cfg.get_authorizer_params('forbidden', cvsroot_name) - forbidden = auth_params.get('forbidden', '') - forbidden = map(string.strip, filter(None, string.split(forbidden, ','))) + '''Return 1 if MODULE in CVSROOT_NAME is forbidden; return 0 otherwise.''' + + # CVSROOT_NAME might be None here if the data comes from an + # unconfigured root. This interfaces doesn't care that the root + # isn't configured, but if that's the case, it will consult only + # the base and per-vhost configuration for authorizer and + # authorizer parameters. + if cvsroot_name: + authorizer, params = cfg.get_authorizer_and_params_hack(cvsroot_name) + else: + authorizer = cfg.options.authorizer + params = cfg.get_authorizer_params() + + # If CVSROOT_NAME isn't configured to use an authorizer, nothing + # is forbidden. If it's configured to use something other than + # the 'forbidden' authorizer, complain. Otherwise, check for + # forbiddenness per the PARAMS as expected. + if not authorizer: + return 0 + if authorizer != 'forbidden': + raise Exception("The 'forbidden' authorizer is the only one supported " + "by this interface. The '%s' root is configured to " + "use a different one." % (cvsroot_name)) + forbidden = params.get('forbidden', '') + forbidden = map(lambda x: x.strip(), filter(None, forbidden.split(','))) default = 0 for pat in forbidden: if pat[0] == '!': @@ -304,12 +328,8 @@ def is_forbidden(cfg, cvsroot_name, module): return default def build_commit(server, cfg, desc, files, cvsroots, viewvc_link): - ob = _item(num_files=len(files), files=[], plus=0, minus=0) - - if desc: - ob.log = string.replace(server.escape(desc), '\n', '
') - else: - ob.log = ' ' + ob = _item(num_files=len(files), files=[]) + ob.log = desc and server.escape(desc).replace('\n', '
') or ' ' for commit in files: repository = commit.GetRepository() @@ -318,7 +338,7 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link): ## find the module name (if any) try: - module = filter(None, string.split(directory, '/'))[0] + module = filter(None, directory.split('/'))[0] except IndexError: module = None @@ -343,9 +363,10 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link): except: raise Exception, str([directory, commit.GetFile()]) - ## if we couldn't find the cvsroot path configured in the - ## viewvc.conf file, then don't make the link - if cvsroot_name: + ## If we couldn't find the cvsroot path configured in the + ## viewvc.conf file, or we don't have a VIEWVC_LINK, then + ## don't make the link. + if cvsroot_name and viewvc_link: flink = '[%s] %s' % ( cvsroot_name, viewvc_link, urllib.quote(file), cvsroot_name, file) @@ -376,22 +397,26 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link): return ob def run_query(server, cfg, db, form_data, viewvc_link): - query = form_to_cvsdb_query(form_data) + query = form_to_cvsdb_query(cfg, form_data) db.RunQuery(query) - if not query.commit_list: - return [ ] + commit_list = query.GetCommitList() + if not commit_list: + return [ ], 0 + + row_limit_reached = query.GetLimitReached() commits = [ ] files = [ ] cvsroots = {} + viewvc.expand_root_parents(cfg) rootitems = cfg.general.svn_roots.items() + cfg.general.cvs_roots.items() for key, value in rootitems: cvsroots[cvsdb.CleanRepository(value)] = key - current_desc = query.commit_list[0].GetDescription() - for commit in query.commit_list: + current_desc = commit_list[0].GetDescription() + for commit in commit_list: desc = commit.GetDescription() if current_desc == desc: files.append(commit) @@ -414,7 +439,7 @@ def run_query(server, cfg, db, form_data, viewvc_link): return len(commit.files) > 0 commits = filter(_only_with_files, commits) - return commits + return commits, row_limit_reached def main(server, cfg, viewvc_link): try: @@ -424,31 +449,32 @@ def main(server, cfg, viewvc_link): db = cvsdb.ConnectDatabaseReadOnly(cfg, None) if form_data.valid: - commits = run_query(server, cfg, db, form_data, viewvc_link) + commits, row_limit_reached = run_query(server, cfg, db, form_data, viewvc_link) query = None else: commits = [ ] + row_limit_reached = 0 query = 'skipped' - data = ezt.TemplateData({ + docroot = cfg.options.docroot + if docroot is None and viewvc_link: + docroot = viewvc_link + '/' + viewvc.docroot_magic_path + + data = TemplateData({ 'cfg' : cfg, 'address' : cfg.general.address, 'vsn' : viewvc.__version__, - - 'textquery' : server.escape(form_data.textquery, 1), - 'repository' : server.escape(form_data.repository, 1), - 'branch' : server.escape(form_data.branch, 1), - 'directory' : server.escape(form_data.directory, 1), - 'file' : server.escape(form_data.file, 1), - 'who' : server.escape(form_data.who, 1), - 'docroot' : cfg.options.docroot is None \ - and viewvc_link + '/' + viewvc.docroot_magic_path \ - or cfg.options.docroot, - + 'textquery' : server.escape(form_data.textquery), + 'repository' : server.escape(form_data.repository), + 'branch' : server.escape(form_data.branch), + 'directory' : server.escape(form_data.directory), + 'file' : server.escape(form_data.file), + 'who' : server.escape(form_data.who), + 'docroot' : docroot, 'sortby' : form_data.sortby, 'date' : form_data.date, - 'query' : query, + 'row_limit_reached' : ezt.boolean(row_limit_reached), 'commits' : commits, 'num_commits' : len(commits), 'rss_href' : None, @@ -467,7 +493,3 @@ def main(server, cfg, viewvc_link): exc_info = debug.GetExceptionData() server.header(status=exc_info['status']) debug.PrintException(server, exc_info) - -class _item: - def __init__(self, **kw): - vars(self).update(kw) diff --git a/lib/sapi.py b/lib/sapi.py index e36774e7..585f3ca0 100644 --- a/lib/sapi.py +++ b/lib/sapi.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -16,17 +16,30 @@ # ----------------------------------------------------------------------- import types -import string import os import sys import re +import cgi -# global server object. It will be either a CgiServer or a proxy to -# an AspServer or ModPythonServer object. +# global server object. It will be either a CgiServer, a WsgiServer, +# or a proxy to an AspServer or ModPythonServer object. server = None +# Simple HTML string escaping. Note that we always escape the +# double-quote character -- ViewVC shouldn't ever need to preserve +# that character as-is, and sometimes needs to embed escaped values +# into HTML attributes. +def escape(s): + s = str(s) + s = s.replace('&', '&') + s = s.replace('>', '>') + s = s.replace('<', '<') + s = s.replace('"', """) + return s + + class Server: def __init__(self): self.pageGlobals = {} @@ -34,6 +47,9 @@ class Server: def self(self): return self + def escape(self, s): + return escape(s) + def close(self): pass @@ -129,9 +145,6 @@ class CgiServer(Server): global server server = self - global cgi - import cgi - def addheader(self, name, value): self.headers.append((name, value)) @@ -160,9 +173,6 @@ class CgiServer(Server): self.header(status='301 Moved') sys.stdout.write('This document is located here.\n' % url) - def escape(self, s, quote = None): - return cgi.escape(s, quote) - def getenv(self, name, value=None): ret = os.environ.get(name, value) if self.iis and name == 'PATH_INFO' and ret: @@ -175,7 +185,7 @@ class CgiServer(Server): def FieldStorage(fp=None, headers=None, outerboundary="", environ=os.environ, keep_blank_values=0, strict_parsing=0): return cgi.FieldStorage(fp, headers, outerboundary, environ, - keep_blank_values, strict_parsing) + keep_blank_values, strict_parsing) def write(self, s): sys.stdout.write(s) @@ -187,6 +197,60 @@ class CgiServer(Server): return sys.stdout +class WsgiServer(Server): + def __init__(self, environ, start_response): + Server.__init__(self) + + self._environ = environ + self._start_response = start_response; + self._headers = [] + self._wsgi_write = None + self.headerSent = False + + global server + server = self + + def addheader(self, name, value): + self._headers.append((name, value)) + + def header(self, content_type='text/html; charset=UTF-8', status=None): + if not status: + status = "200 OK" + if not self.headerSent: + self.headerSent = True + self._headers.insert(0, ("Content-Type", content_type),) + self._wsgi_write = self._start_response("%s" % status, self._headers) + + def redirect(self, url): + """Redirect client to url. This discards any data that has been queued + to be sent to the user. But there should never by any anyway. + """ + self.addheader('Location', url) + self.header(status='301 Moved') + self._wsgi_write('This document is located here.' % url) + + def getenv(self, name, value=None): + return self._environ.get(name, value) + + def params(self): + return cgi.parse(environ=self._environ, fp=self._environ["wsgi.input"]) + + def FieldStorage(self, fp=None, headers=None, outerboundary="", + environ=os.environ, keep_blank_values=0, strict_parsing=0): + return cgi.FieldStorage(self._environ["wsgi.input"], headers, + outerboundary, self._environ, keep_blank_values, + strict_parsing) + + def write(self, s): + self._wsgi_write(s) + + def flush(self): + pass + + def file(self): + return File(self) + + class AspServer(ThreadedServer): def __init__(self, Server, Request, Response, Application): ThreadedServer.__init__(self) @@ -219,9 +283,6 @@ class AspServer(ThreadedServer): def redirect(self, url): self.response.Redirect(url) - def escape(self, s, quote = None): - return self.server.HTMLEncode(str(s)) - def getenv(self, name, value = None): ret = self.request.ServerVariables(name)() if not type(ret) is types.UnicodeType: @@ -283,9 +344,6 @@ class ModPythonServer(ThreadedServer): self.request = request self.headerSent = 0 - global cgi - import cgi - def addheader(self, name, value): self.request.headers_out.add(name, value) @@ -308,9 +366,6 @@ class ModPythonServer(ThreadedServer): self.request.write("You are being redirected to %s" % (url, url)) - def escape(self, s, quote = None): - return cgi.escape(s, quote) - def getenv(self, name, value = None): try: return self.request.subprocess_env[name] diff --git a/lib/vcauth/__init__.py b/lib/vcauth/__init__.py index 5261415a..2f78571f 100644 --- a/lib/vcauth/__init__.py +++ b/lib/vcauth/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 2006-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 2006-2013 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 @@ -12,10 +12,6 @@ """Generic API for implementing authorization checks employed by ViewVC.""" -import string -import vclib - - class GenericViewVCAuthorizer: """Abstract class encapsulating version control authorization routines.""" @@ -29,7 +25,15 @@ class GenericViewVCAuthorizer: def check_root_access(self, rootname): """Return 1 iff the associated username is permitted to read ROOTNAME.""" pass - + + def check_universal_access(self, rootname): + """Return 1 if the associated username is permitted to read every + path in the repository at every revision, 0 if the associated + username is prohibited from reading any path in the repository, or + None if no such determination can be made (perhaps because the + cost of making it is too great).""" + pass + def check_path_access(self, rootname, path_parts, pathtype, rev=None): """Return 1 iff the associated username is permitted to read revision REV of the path PATH_PARTS (of type PATHTYPE) in @@ -44,6 +48,9 @@ class ViewVCAuthorizer(GenericViewVCAuthorizer): """The uber-permissive authorizer.""" def check_root_access(self, rootname): return 1 + + def check_universal_access(self, rootname): + return 1 def check_path_access(self, rootname, path_parts, pathtype, rev=None): return 1 diff --git a/lib/vcauth/forbidden/__init__.py b/lib/vcauth/forbidden/__init__.py index 30dfa45b..46b52f7a 100644 --- a/lib/vcauth/forbidden/__init__.py +++ b/lib/vcauth/forbidden/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 2006-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 2006-2013 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 @@ -12,18 +12,24 @@ import vcauth import vclib import fnmatch -import string class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): """A simple top-level module authorizer.""" def __init__(self, username, params={}): forbidden = params.get('forbidden', '') - self.forbidden = map(string.strip, - filter(None, string.split(forbidden, ','))) + self.forbidden = map(lambda x: x.strip(), + filter(None, forbidden.split(','))) def check_root_access(self, rootname): return 1 - + + def check_universal_access(self, rootname): + # If there aren't any forbidden paths, we can grant universal read + # access. Otherwise, we make no claim. + if not self.forbidden: + return 1 + return None + def check_path_access(self, rootname, path_parts, pathtype, rev=None): # No path? No problem. if not path_parts: diff --git a/lib/vcauth/forbiddenre/__init__.py b/lib/vcauth/forbiddenre/__init__.py index 61d54e47..a50d37d5 100644 --- a/lib/vcauth/forbiddenre/__init__.py +++ b/lib/vcauth/forbiddenre/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 2008-2013 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 @@ -12,7 +12,6 @@ import vcauth import vclib import fnmatch -import string import re @@ -29,8 +28,8 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): """A simple regular-expression-based authorizer.""" def __init__(self, username, params={}): forbidden = params.get('forbiddenre', '') - self.forbidden = map(lambda x: _split_regexp(string.strip(x)), - filter(None, string.split(forbidden, ','))) + self.forbidden = map(lambda x: _split_regexp(x.strip()), + filter(None, forbidden.split(','))) def _check_root_path_access(self, root_path): default = 1 @@ -46,10 +45,17 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): def check_root_access(self, rootname): return self._check_root_path_access(rootname) + def check_universal_access(self, rootname): + # If there aren't any forbidden regexps, we can grant universal + # read access. Otherwise, we make no claim. + if not self.forbidden: + return 1 + return None + def check_path_access(self, rootname, path_parts, pathtype, rev=None): root_path = rootname if path_parts: - root_path = root_path + '/' + string.join(path_parts, '/') + root_path = root_path + '/' + '/'.join(path_parts) if pathtype == vclib.DIR: root_path = root_path + '/' else: diff --git a/lib/vcauth/svnauthz/__init__.py b/lib/vcauth/svnauthz/__init__.py index c3701222..7c7bceba 100644 --- a/lib/vcauth/svnauthz/__init__.py +++ b/lib/vcauth/svnauthz/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 2006-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 2006-2013 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 @@ -12,7 +12,6 @@ # (c) 2006 Sergey Lapin import vcauth -import string import os.path import debug @@ -34,9 +33,9 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): # See if the admin wants us to do case normalization of usernames. self.force_username_case = params.get('force_username_case') if self.force_username_case == "upper": - self.username = username.upper() + self.username = username and username.upper() or username elif self.force_username_case == "lower": - self.username = username.lower() + self.username = username and username.lower() or username elif not self.force_username_case: self.username = username else: @@ -54,7 +53,10 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): # option names. cp = ConfigParser() cp.optionxform = lambda x: x - cp.read(self.authz_file) + try: + cp.read(self.authz_file) + except: + raise debug.ViewVCException("Unable to parse configured authzfile file") # Figure out if there are any aliases for the current username aliases = [] @@ -94,9 +96,9 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): all_groups.append(groupname) group_member = 0 groupname = groupname.strip() - entries = string.split(cp.get('groups', groupname), ',') + entries = cp.get('groups', groupname).split(',') for entry in entries: - entry = string.strip(entry) + entry = entry.strip() if entry == self.username: group_member = 1 break @@ -137,13 +139,13 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): # Figure if this path is explicitly allowed or denied to USERNAME. allow = deny = 0 for user in cp.options(section): - user = string.strip(user) + user = user.strip() if _userspec_matches_user(user): # See if the 'r' permission is among the ones granted to # USER. If so, we can stop looking. (Entry order is not # relevant -- we'll use the most permissive entry, meaning # one 'allow' is all we need.) - allow = string.find(cp.get(section, user), 'r') != -1 + allow = cp.get(section, user).find('r') != -1 deny = not allow if allow: break @@ -172,7 +174,7 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): if section.find(':') == -1: path = section else: - name, path = string.split(section, ':', 1) + name, path = section.split(':', 1) if name == rootname: root_sections.append(section) continue @@ -184,14 +186,14 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): # USERNAME, record it. if allow or deny: if path != '/': - path = '/' + string.join(filter(None, string.split(path, '/')), '/') + path = '/' + '/'.join(filter(None, path.split('/'))) paths_for_root[path] = allow # Okay. Superimpose those root-specific values now. for section in root_sections: # Get the path again. - name, path = string.split(section, ':', 1) + name, path = section.split(':', 1) # Check for a specific access determination. allow, deny = _process_access_section(section) @@ -200,7 +202,7 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): # USERNAME, record it. if allow or deny: if path != '/': - path = '/' + string.join(filter(None, string.split(path, '/')), '/') + path = '/' + '/'.join(filter(None, path.split('/'))) paths_for_root[path] = allow # If the root isn't readable, there's no point in caring about all @@ -221,6 +223,36 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): paths = self._get_paths_for_root(rootname) return (paths is not None) and 1 or 0 + def check_universal_access(self, rootname): + paths = self._get_paths_for_root(rootname) + if not paths: # None or empty. + return 0 + + # Search the access determinations. If there's a mix, we can't + # claim a universal access determination. + found_allow = 0 + found_deny = 0 + for access in paths.values(): + if access: + found_allow = 1 + else: + found_deny = 1 + if found_allow and found_deny: + return None + + # We didn't find both allowances and denials, so we must have + # found one or the other. Denials only is a universal denial. + if found_deny: + return 0 + + # ... but allowances only is only a universal allowance if read + # access is granted to the root directory. + if found_allow and paths.has_key('/'): + return 1 + + # Anything else is indeterminable. + return None + def check_path_access(self, rootname, path_parts, pathtype, rev=None): # Crawl upward from the path represented by PATH_PARTS toward to # the root of the repository, looking for an explicitly grant or @@ -230,7 +262,7 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): return 0 parts = path_parts[:] while parts: - path = '/' + string.join(parts, '/') + path = '/' + '/'.join(parts) if paths.has_key(path): return paths[path] del parts[-1] diff --git a/lib/vclib/__init__.py b/lib/vclib/__init__.py index 325580d4..bf1151a3 100644 --- a/lib/vclib/__init__.py +++ b/lib/vclib/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -14,7 +14,6 @@ such as CVS. """ -import string import types @@ -76,7 +75,7 @@ class Repository: """ pass - def openfile(self, path_parts, rev): + def openfile(self, path_parts, rev, options): """Open a file object to read file contents at a given path and revision. The return value is a 2-tuple of containg the file object and revision @@ -86,6 +85,8 @@ class Repository: of the repository. e.g. ["subdir1", "subdir2", "filename"] rev is the revision of the file to check out + + options is a dictionary of implementation specific options """ def listdir(self, path_parts, rev, options): @@ -168,20 +169,29 @@ class Repository: Return value is a python file object """ - def annotate(self, path_parts, rev): - """Return a list of annotate file content lines and a revision. + def annotate(self, path_parts, rev, include_text=False): + """Return a list of Annotation object, sorted by their + "line_number" components, which describe the lines of given + version of a file. - The result is a list of Annotation objects, sorted by their - line_number components. - """ + The file path is specified as a list of components, relative to + the root of the repository. e.g. ["subdir1", "subdir2", "filename"] + + rev is the revision of the item to return information about. + + If include_text is true, populate the Annotation objects' "text" + members with the corresponding line of file content; otherwise, + leave that member set to None.""" def revinfo(self, rev): """Return information about a global revision rev is the revision of the item to return information about - Return value is a 4-tuple containing the date, author, log - message, and a list of ChangedPath items representing paths changed + Return value is a 5-tuple containing: the date, author, log + message, a list of ChangedPath items representing paths changed, + and a dictionary mapping property names to property values for + properties stored on an item. Raise vclib.UnsupportedFeature if the version control system doesn't support a global revision concept. @@ -196,7 +206,21 @@ class Repository: rev is the revision of the item to return information about """ + + def filesize(self, path_parts, rev): + """Return the size of a versioned file's contents if it can be + obtained without a brute force measurement; -1 otherwise. + + NOTE: Callers that require a filesize answer when this function + returns -1 may obtain it by measuring the data returned via + openfile(). + The path is specified as a list of components, relative to the root + of the repository. e.g. ["subdir1", "subdir2", "filename"] + + rev is the revision of the item to return information about + """ + # ====================================================================== class DirEntry: @@ -302,7 +326,7 @@ class ItemNotFound(Error): # use '/' rather than os.sep because this is for user consumption, and # it was defined using URL separators if type(path) in (types.TupleType, types.ListType): - path = string.join(path, '/') + path = '/'.join(path) Error.__init__(self, path) class InvalidRevision(Error): diff --git a/lib/vclib/ccvs/__init__.py b/lib/vclib/ccvs/__init__.py index f54c47b8..1842f01d 100644 --- a/lib/vclib/ccvs/__init__.py +++ b/lib/vclib/ccvs/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -11,28 +11,52 @@ # ----------------------------------------------------------------------- import os import os.path +import time + + +def cvs_strptime(timestr): + return time.strptime(timestr, '%Y/%m/%d %H:%M:%S')[:-1] + (0,) def canonicalize_rootpath(rootpath): + assert os.path.isabs(rootpath) return os.path.normpath(rootpath) +def _is_cvsroot(path): + return os.path.exists(os.path.join(path, "CVSROOT", "config")) + + def expand_root_parent(parent_path): # Each subdirectory of PARENT_PATH that contains a child # "CVSROOT/config" is added the set of returned roots. Or, if the # PARENT_PATH itself contains a child "CVSROOT/config", then all its # subdirectories are returned as roots. + assert os.path.isabs(parent_path) roots = {} subpaths = os.listdir(parent_path) - cvsroot = os.path.exists(os.path.join(parent_path, "CVSROOT", "config")) for rootname in subpaths: rootpath = os.path.join(parent_path, rootname) - if cvsroot \ - or (os.path.exists(os.path.join(rootpath, "CVSROOT", "config"))): + if _is_cvsroot(parent_path) or _is_cvsroot(rootpath): roots[rootname] = canonicalize_rootpath(rootpath) return roots +def find_root_in_parent(parent_path, rootname): + """Search PARENT_PATH for a root named ROOTNAME, returning the + canonicalized ROOTPATH of the root if found; return None if no such + root is found.""" + # Is PARENT_PATH itself a CVS repository? If so, we allow ROOTNAME + # to be any subdir within it. Otherwise, we expect + # PARENT_PATH/ROOTNAME to be a CVS repository. + assert os.path.isabs(parent_path) + rootpath = os.path.join(parent_path, rootname) + if (_is_cvsroot(parent_path) and os.path.exists(rootpath)) \ + or _is_cvsroot(rootpath): + return canonicalize_rootpath(rootpath) + return None + + def CVSRepository(name, rootpath, authorizer, utilities, use_rcsparse, charset_guesser = None): rootpath = canonicalize_rootpath(rootpath) if use_rcsparse: diff --git a/lib/vclib/ccvs/bincvs.py b/lib/vclib/ccvs/bincvs.py index ca5ba1f7..46d3d073 100644 --- a/lib/vclib/ccvs/bincvs.py +++ b/lib/vclib/ccvs/bincvs.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -18,16 +18,19 @@ import os import os.path import sys import stat -import string import re import time import cvsdb import socket +import calendar # ViewVC libs -import compat import popen +import vclib.ccvs +def _path_join(path_parts): + return '/'.join(path_parts) + class BaseCVSRepository(vclib.Repository): def __init__(self, name, rootpath, authorizer, utilities, charset_guesser = None): if not os.path.isdir(rootpath): @@ -43,6 +46,11 @@ class BaseCVSRepository(vclib.Repository): if not vclib.check_root_access(self): raise vclib.ReposNotFound(name) + def open(self): + # See if a universal read access determination can be made. + if self.auth and self.auth.check_universal_access(self.name) == 1: + self.auth = None + def rootname(self): return self.name @@ -79,8 +87,8 @@ class BaseCVSRepository(vclib.Repository): def listdir(self, path_parts, rev, options): if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." - % (string.join(path_parts, "/"))) - + % (_path_join(path_parts))) + # Only RCS files (*,v) and subdirs are returned. data = [ ] full_name = self._getpath(path_parts) @@ -136,17 +144,21 @@ class BaseCVSRepository(vclib.Repository): if root: ret = ret_file else: - ret = string.join(ret_parts, "/") + ret = _path_join(ret_parts) if v: ret = ret + ",v" return ret def isexecutable(self, path_parts, rev): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts, "/"))) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) rcsfile = self.rcsfile(path_parts, 1) return os.access(rcsfile, os.X_OK) + + def filesize(self, path_parts, rev): + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) + return -1 class BinCVSRepository(BaseCVSRepository): @@ -165,20 +177,28 @@ class BinCVSRepository(BaseCVSRepository): return revs[-1] return None - def openfile(self, path_parts, rev): + def openfile(self, path_parts, rev, options): + """see vclib.Repository.openfile docstring + + Option values recognized by this implementation: + + cvs_oldkeywords + boolean. true to use the original keyword substitution values. + """ if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts, "/"))) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) if not rev or rev == 'HEAD' or rev == 'MAIN': rev_flag = '-p' else: rev_flag = '-p' + rev + if options.get('cvs_oldkeywords', 0): + kv_flag = '-ko' + else: + kv_flag = '-kkv' full_name = self.rcsfile(path_parts, root=1, v=0) - used_rlog = 0 tip_rev = None # used only if we have to fallback to using rlog - - fp = self.rcs_popen('co', (rev_flag, full_name), 'rb') + fp = self.rcs_popen('co', (kv_flag, rev_flag, full_name), 'rb') try: filename, revision = _parse_co_header(fp) except COMissingRevision: @@ -240,7 +260,7 @@ class BinCVSRepository(BaseCVSRepository): """ if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." - % (string.join(path_parts, "/"))) + % (_path_join(path_parts))) subdirs = options.get('cvs_subdirs', 0) entries_to_fetch = [] @@ -277,9 +297,8 @@ class BinCVSRepository(BaseCVSRepository): """ if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts, "/"))) - + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) + # Invoke rlog rcsfile = self.rcsfile(path_parts, 1) if rev and options.get('cvs_pass_rev', 0): @@ -331,13 +350,12 @@ class BinCVSRepository(BaseCVSRepository): args = rcs_args return popen.popen(cmd, args, mode, capture_err) - def annotate(self, path_parts, rev=None): + def annotate(self, path_parts, rev=None, include_text=False): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts, "/"))) - + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) + from vclib.ccvs import blame - source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser) + source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser, include_text) return source, source.revision def revinfo(self, rev): @@ -357,12 +375,10 @@ class BinCVSRepository(BaseCVSRepository): path_parts2 = path_parts1 rev2 = '1.0' if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts1, "/"))) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1))) if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts2, "/"))) - + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2))) + args = vclib._diff_args(type, options) if options.get('ignore_keyword_subst', 0): args.append('-kk') @@ -567,7 +583,7 @@ def _remove_tag(tag): def _revision_tuple(revision_string): """convert a revision number into a tuple of integers""" - t = tuple(map(int, string.split(revision_string, '.'))) + t = tuple(map(int, revision_string.split('.'))) if len(t) % 2 == 0: return t raise ValueError @@ -575,7 +591,7 @@ def _revision_tuple(revision_string): def _tag_tuple(revision_string): """convert a revision number or branch number into a tuple of integers""" if revision_string: - t = map(int, string.split(revision_string, '.')) + t = map(int, revision_string.split('.')) l = len(t) if l == 1: return () @@ -720,7 +736,7 @@ def _parse_log_header(fp): if state == 1: if line[0] == '\t': - [ tag, rev ] = map(string.strip, string.split(line, ':')) + [ tag, rev ] = map(lambda x: x.strip(), line.split(':')) taginfo[tag] = rev else: # oops. this line isn't tag info. stop parsing tags. @@ -728,7 +744,7 @@ def _parse_log_header(fp): if state == 2: if line[0] == '\t': - [ locker, rev ] = map(string.strip, string.split(line, ':')) + [ locker, rev ] = map(lambda x: x.strip(), line.split(':')) lockinfo[rev] = locker else: # oops. this line isn't lock info. stop parsing tags. @@ -837,7 +853,7 @@ def _parse_log_entry(fp, guesser): return None, eof # parse out a time tuple for the local time - tm = compat.cvs_strptime(match.group(1)) + tm = vclib.ccvs.cvs_strptime(match.group(1)) # rlog seems to assume that two-digit years are 1900-based (so, "04" # comes out as "1904", not "2004"). @@ -848,7 +864,7 @@ def _parse_log_entry(fp, guesser): tm[0] = tm[0] + 100 if tm[0] < EPOCH: raise ValueError, 'invalid year' - date = compat.timegm(tm) + date = calendar.timegm(tm) if guesser: log = guesser.utf8(log) @@ -1042,16 +1058,16 @@ def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs, guesser): file.errors.append("rlog error: %s" % msg) continue + tag = None if view_tag == 'MAIN' or view_tag == 'HEAD': tag = Tag(None, default_branch) elif taginfo.has_key(view_tag): tag = Tag(None, taginfo[view_tag]) - elif view_tag: - # the tag wasn't found, so skip this file + elif view_tag and (eof != _EOF_FILE): + # the tag wasn't found, so skip this file (unless we already + # know there's nothing left of it to read) _skip_file(rlog) - eof = 1 - else: - tag = None + eof = _EOF_FILE # we don't care about the specific values -- just the keys and whether # the values point to branches or revisions. this the fastest way to diff --git a/lib/vclib/ccvs/blame.py b/lib/vclib/ccvs/blame.py index f602bc41..6660061f 100644 --- a/lib/vclib/ccvs/blame.py +++ b/lib/vclib/ccvs/blame.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved. # Copyright (C) 2000 Curt Hagenlocher # # By using this file, you agree to the terms and conditions set forth in @@ -26,7 +26,6 @@ # # ----------------------------------------------------------------------- -import string import re import time import math @@ -101,7 +100,7 @@ class CVSParser(rcsparse.Sink): # Split deltatext specified by rev to each line. def deltatext_split(self, rev): - lines = string.split(self.revision_deltatext[rev], '\n') + lines = self.revision_deltatext[rev].split('\n') if lines[-1] == '': del lines[-1] return lines @@ -140,16 +139,16 @@ class CVSParser(rcsparse.Sink): adjust = adjust + 1 elif dmatch: # "d" - Delete command - start_line = string.atoi(dmatch.group(1)) - count = string.atoi(dmatch.group(2)) + start_line = int(dmatch.group(1)) + count = int(dmatch.group(2)) begin = start_line + adjust - 1 del text[begin:begin + count] adjust = adjust - count lines_removed_now = lines_removed_now + count elif amatch: # "a" - Add command - start_line = string.atoi(amatch.group(1)) - count = string.atoi(amatch.group(2)) + start_line = int(amatch.group(1)) + count = int(amatch.group(2)) add_lines_remaining = count lines_added_now = lines_added_now + count else: @@ -312,13 +311,13 @@ class CVSParser(rcsparse.Sink): skip = skip - 1 elif dmatch: # "d" - Delete command - start_line = string.atoi(dmatch.group(1)) - count = string.atoi(dmatch.group(2)) + start_line = int(dmatch.group(1)) + count = int(dmatch.group(2)) line_count = line_count - count elif amatch: # "a" - Add command - start_line = string.atoi(amatch.group(1)) - count = string.atoi(amatch.group(2)) + start_line = int(amatch.group(1)) + count = int(amatch.group(2)) skip = count line_count = line_count + count else: @@ -360,8 +359,8 @@ class CVSParser(rcsparse.Sink): dmatch = self.d_command.match(command) amatch = self.a_command.match(command) if dmatch: - start_line = string.atoi(dmatch.group(1)) - count = string.atoi(dmatch.group(2)) + start_line = int(dmatch.group(1)) + count = int(dmatch.group(2)) temp = [] while count > 0: temp.append(revision) @@ -369,8 +368,8 @@ class CVSParser(rcsparse.Sink): self.revision_map = (self.revision_map[:start_line - 1] + temp + self.revision_map[start_line - 1:]) elif amatch: - start_line = string.atoi(amatch.group(1)) - count = string.atoi(amatch.group(2)) + start_line = int(amatch.group(1)) + count = int(amatch.group(2)) del self.revision_map[start_line:start_line + count] skip = count else: @@ -389,15 +388,15 @@ class CVSParser(rcsparse.Sink): dmatch = self.d_command.match(command) amatch = self.a_command.match(command) if dmatch: - start_line = string.atoi(dmatch.group(1)) - count = string.atoi(dmatch.group(2)) + start_line = int(dmatch.group(1)) + count = int(dmatch.group(2)) adj_begin = start_line + adjust - 1 adj_end = start_line + adjust - 1 + count del self.revision_map[adj_begin:adj_end] adjust = adjust - count elif amatch: - start_line = string.atoi(amatch.group(1)) - count = string.atoi(amatch.group(2)) + start_line = int(amatch.group(1)) + count = int(amatch.group(2)) skip = count temp = [] while count > 0: @@ -415,7 +414,7 @@ class CVSParser(rcsparse.Sink): class BlameSource: - def __init__(self, rcs_file, opt_rev=None, charset_guesser=None): + def __init__(self, rcs_file, opt_rev=None, charset_guesser=None, include_text=False): # Parse the CVS file parser = CVSParser() revision = parser.parse_cvs_file(rcs_file, opt_rev) @@ -430,6 +429,7 @@ class BlameSource: self.num_lines = count self.parser = parser self.guesser = charset_guesser + self.include_text = include_text # keep track of where we are during an iteration self.idx = -1 @@ -449,7 +449,9 @@ class BlameSource: line_number = idx + 1 author = self.parser.revision_author[rev] - if self.guesser: + if not self.include_text: + thisline = None + elif self.guesser: thisline = self.guesser.utf8(self.lines[idx]) else: thisline = self.lines[idx] diff --git a/lib/vclib/ccvs/ccvs.py b/lib/vclib/ccvs/ccvs.py index 42a8bf7d..f11af2f3 100644 --- a/lib/vclib/ccvs/ccvs.py +++ b/lib/vclib/ccvs/ccvs.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -11,7 +11,6 @@ # ----------------------------------------------------------------------- import os -import string import re import cStringIO import tempfile @@ -23,7 +22,8 @@ import cvsdb ### The functionality shared with bincvs should probably be moved to a ### separate module -from bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp +from bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp, _path_join + class CCVSRepository(BaseCVSRepository): def dirlogs(self, path_parts, rev, entries, options): @@ -45,7 +45,7 @@ class CCVSRepository(BaseCVSRepository): """ if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." - % (string.join(path_parts, "/"))) + % (part2path(path_parts))) entries_to_fetch = [] for entry in entries: if vclib.check_path_access(self, path_parts + [entry.name], None, rev): @@ -97,8 +97,7 @@ class CCVSRepository(BaseCVSRepository): dictionary of Tag objects for all tags encountered """ if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts, "/"))) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) path = self.rcsfile(path_parts, 1) sink = TreeSink() @@ -123,17 +122,15 @@ class CCVSRepository(BaseCVSRepository): def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): if path_parts1 and self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts1, "/"))) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1))) if path_parts2 and self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts2, "/"))) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2))) if not path_parts1 and not path_parts2: raise vclib.Error("Nothing to diff.") - + if path_parts1: temp1 = tempfile.mktemp() - open(temp1, 'wb').write(self.openfile(path_parts1, rev1)[0].getvalue()) + open(temp1, 'wb').write(self.openfile(path_parts1, rev1, {})[0].getvalue()) r1 = self.itemlog(path_parts1, rev1, vclib.SORTBY_DEFAULT, 0, 0, {})[-1] info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string) else: @@ -142,7 +139,7 @@ class CCVSRepository(BaseCVSRepository): if path_parts2: temp2 = tempfile.mktemp() - open(temp2, 'wb').write(self.openfile(path_parts2, rev2)[0].getvalue()) + open(temp2, 'wb').write(self.openfile(path_parts2, rev2, {})[0].getvalue()) r2 = self.itemlog(path_parts2, rev2, vclib.SORTBY_DEFAULT, 0, 0, {})[-1] info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string) else: @@ -154,25 +151,23 @@ class CCVSRepository(BaseCVSRepository): return vclib._diff_fp(temp1, temp2, info1, info2, self.utilities.diff or 'diff', diff_args) - def annotate(self, path_parts, rev=None): + def annotate(self, path_parts, rev=None, include_text=False): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts, "/"))) - source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) + source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser, include_text) return source, source.revision def revinfo(self, rev): raise vclib.UnsupportedFeature - def openfile(self, path_parts, rev=None): + def openfile(self, path_parts, rev, options): if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check - raise vclib.Error("Path '%s' is not a file." - % (string.join(path_parts, "/"))) + raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts))) path = self.rcsfile(path_parts, 1) sink = COSink(rev) rcsparse.parse(open(path, 'rb'), sink) revision = sink.last and sink.last.string - return cStringIO.StringIO(string.join(sink.sstext.text, "\n")), revision + return cStringIO.StringIO('\n'.join(sink.sstext.text)), revision class MatchingSink(rcsparse.Sink): """Superclass for sinks that search for revisions based on tag or number""" @@ -212,6 +207,7 @@ class InfoSink(MatchingSink): self.matching_rev = None self.perfect_match = 0 self.lockinfo = { } + self.saw_revision = False def define_tag(self, name, revision): MatchingSink.define_tag(self, name, revision) @@ -225,10 +221,17 @@ class InfoSink(MatchingSink): self.entry.absent = 1 raise rcsparse.RCSStopParser + def parse_completed(self): + if not self.saw_revision: + #self.entry.errors.append("No revisions exist on %s" % (view_tag or "MAIN")) + self.entry.absent = 1 + def set_locker(self, rev, locker): self.lockinfo[rev] = locker def define_revision(self, revision, date, author, state, branches, next): + self.saw_revision = True + if self.perfect_match: return @@ -236,14 +239,18 @@ class InfoSink(MatchingSink): rev = Revision(revision, date, author, state == "dead") rev.lockinfo = self.lockinfo.get(revision) - # perfect match if revision number matches tag number or if revision is on - # trunk and tag points to trunk. imperfect match if tag refers to a branch - # and this revision is the highest revision so far found on that branch + # perfect match if revision number matches tag number or if + # revision is on trunk and tag points to trunk. imperfect match + # if tag refers to a branch and either a) this revision is the + # highest revision so far found on that branch, or b) this + # revision is the branchpoint. perfect = ((rev.number == tag.number) or (not tag.number and len(rev.number) == 2)) - if perfect or (tag.is_branch and tag.number == rev.number[:-1] and - (not self.matching_rev or - rev.number > self.matching_rev.number)): + if perfect or (tag.is_branch and \ + ((tag.number == rev.number[:-1] and + (not self.matching_rev or + rev.number > self.matching_rev.number)) or + (rev.number == tag.number[:-1]))): self.matching_rev = rev self.perfect_match = perfect @@ -299,18 +306,18 @@ class TreeSink(rcsparse.Sink): deled = 0 if self.head != revision: changed = 1 - lines = string.split(text, '\n') + lines = text.split('\n') idx = 0 while idx < len(lines): command = lines[idx] dmatch = self.d_command.match(command) idx = idx + 1 if dmatch: - deled = deled + string.atoi(dmatch.group(2)) + deled = deled + int(dmatch.group(2)) else: amatch = self.a_command.match(command) if amatch: - count = string.atoi(amatch.group(2)) + count = int(amatch.group(2)) added = added + count idx = idx + count elif command: @@ -326,12 +333,12 @@ class StreamText: a_command = re.compile('^a(\d+)\\s(\\d+)') def __init__(self, text): - self.text = string.split(text, "\n") + self.text = text.split('\n') def command(self, cmd): adjust = 0 add_lines_remaining = 0 - diffs = string.split(cmd, "\n") + diffs = cmd.split('\n') if diffs[-1] == "": del diffs[-1] if len(diffs) == 0: @@ -349,22 +356,22 @@ class StreamText: amatch = self.a_command.match(command) if dmatch: # "d" - Delete command - start_line = string.atoi(dmatch.group(1)) - count = string.atoi(dmatch.group(2)) + start_line = int(dmatch.group(1)) + count = int(dmatch.group(2)) begin = start_line + adjust - 1 del self.text[begin:begin + count] adjust = adjust - count elif amatch: # "a" - Add command - start_line = string.atoi(amatch.group(1)) - count = string.atoi(amatch.group(2)) + start_line = int(amatch.group(1)) + count = int(amatch.group(2)) add_lines_remaining = count else: raise RuntimeError, 'Error parsing diff commands' def secondnextdot(s, start): # find the position the second dot after the start index. - return string.find(s, '.', string.find(s, '.', start) + 1) + return s.find('.', s.find('.', start) + 1) class COSink(MatchingSink): diff --git a/lib/vclib/ccvs/rcsparse/__init__.py b/lib/vclib/ccvs/rcsparse/__init__.py index 829c1170..10fe244c 100644 --- a/lib/vclib/ccvs/rcsparse/__init__.py +++ b/lib/vclib/ccvs/rcsparse/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -10,8 +10,17 @@ # # ----------------------------------------------------------------------- -"""This package provides parsing tools for RCS files.""" +"""This package provides parsing tools for RCS files. +To use this package, first create a subclass of Sink. This should +declare all the callback methods you care about. Create an instance +of your class, and open() the RCS file you want to read. Then call +parse() to parse the file. +""" + +# Make the "Sink" class and the various exception classes visible in this +# scope. That way, applications never need to import any of the +# sub-packages. from common import * try: @@ -23,4 +32,13 @@ except ImportError: from default import Parser def parse(file, sink): + """Parse an RCS file. + + Parameters: FILE is the file object to parse. (I.e. an object of the + built-in Python type "file", usually created using Python's built-in + "open()" function). It should be opened in binary mode. + SINK is an instance of (some subclass of) Sink. It's methods will be + called as the file is parsed; see the definition of Sink for the + details. + """ return Parser().parse(file, sink) diff --git a/lib/vclib/ccvs/rcsparse/common.py b/lib/vclib/ccvs/rcsparse/common.py index 55b9c83c..4df15e5a 100644 --- a/lib/vclib/ccvs/rcsparse/common.py +++ b/lib/vclib/ccvs/rcsparse/common.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -16,52 +16,199 @@ import calendar import string class Sink: + """Interface to be implemented by clients. The RCS parser calls this as + it parses the RCS file. + + All these methods have stub implementations that do nothing, so you only + have to override the callbacks that you care about. + """ def set_head_revision(self, revision): + """Reports the head revision for this RCS file. + + This is the value of the 'head' header in the admin section of the RCS + file. This function can only be called before admin_completed(). + + Parameter: REVISION is a string containing a revision number. This is + an actual revision number, not a branch number. + """ pass def set_principal_branch(self, branch_name): + """Reports the principal branch for this RCS file. This is only called + if the principal branch is not trunk. + + This is the value of the 'branch' header in the admin section of the RCS + file. This function can only be called before admin_completed(). + + Parameter: BRANCH_NAME is a string containing a branch number. If this + function is called, the parameter is typically "1.1.1", indicating the + vendor branch. + """ pass def set_access(self, accessors): + """Reports the access control list for this RCS file. This function is + only called if the ACL is set. If this function is not called then + there is no ACL and all users are allowed access. + + This is the value of the 'access' header in the admin section of the RCS + file. This function can only be called before admin_completed(). + + Parameter: ACCESSORS is a list of strings. Each string is a username. + The user is allowed access if and only if their username is in the list, + OR the user owns the RCS file on disk, OR the user is root. + + Note that CVS typically doesn't use this field. + """ pass def define_tag(self, name, revision): + """Reports a tag or branch definition. This function will be called + once for each tag or branch. + + This is taken from the 'symbols' header in the admin section of the RCS + file. This function can only be called before admin_completed(). + + Parameters: NAME is a string containing the tag or branch name. + REVISION is a string containing a revision number. This may be + an actual revision number (for a tag) or a branch number. + + The revision number consists of a number of decimal components separated + by dots. There are three common forms. If there are an odd number of + components, it's a branch. Otherwise, if the next-to-last component is + zero, it's a branch (and the next-to-last component is an artifact of + CVS and should not be shown to the user). Otherwise, it's a tag. + + This function is called in the order that the tags appear in the RCS + file header. For CVS, this appears to be in reverse chronological + order of tag/branch creation. + """ pass def set_locker(self, revision, locker): + """Reports a lock on this RCS file. This function will be called once + for each lock. + + This is taken from the 'locks' header in the admin section of the RCS + file. This function can only be called before admin_completed(). + + Parameters: REVISION is a string containing a revision number. This is + an actual revision number, not a branch number. + LOCKER is a string containing a username. + """ pass def set_locking(self, mode): - """Used to signal locking mode. + """Signals strict locking mode. This function will be called if and + only if the RCS file is in strict locking mode. - Called with mode argument 'strict' if strict locking - Not called when no locking used.""" + This is taken from the 'strict' header in the admin section of the RCS + file. This function can only be called before admin_completed(). + Parameters: MODE is always the string 'strict'. + """ pass def set_comment(self, comment): + """Reports the comment for this RCS file. + + This is the value of the 'comment' header in the admin section of the + RCS file. This function can only be called before admin_completed(). + + Parameter: COMMENT is a string containing the comment. This may be + multi-line. + + This field does not seem to be used by CVS. + """ pass def set_expansion(self, mode): + """Reports the keyword expansion mode for this RCS file. + + This is the value of the 'expand' header in the admin section of the + RCS file. This function can only be called before admin_completed(). + + Parameter: MODE is a string containing the keyword expansion mode. + Possible values include 'o' and 'b', amongst others. + """ pass def admin_completed(self): + """Reports that the initial RCS header has been parsed. This function is + called exactly once. + """ pass def define_revision(self, revision, timestamp, author, state, branches, next): + """Reports metadata about a single revision. + + This function is called for each revision. It is called later than + admin_completed() and earlier than tree_completed(). + + Parameter: REVISION is a revision number, as a string. This is an + actual revision number, not a branch number. + TIMESTAMP is the date and time that the revision was created, as an + integer number of seconds since the epoch. (I.e. "UNIX time" format). + AUTHOR is the author name, as a string. + STATE is the state of the revision, as a string. Common values are + "Exp" and "dead". + BRANCHES is a list of strings, with each string being an actual + revision number (not a branch number). For each branch which is based + on this revision and has commits, the revision number of the first + branch commit is listed here. + NEXT is either None or a string representing an actual revision number + (not a branch number). + + When on trunk, NEXT points to what humans might consider to be the + 'previous' revision number. For example, 1.3's NEXT is 1.2. + However, on a branch, NEXT really does point to what humans would + consider to be the 'next' revision number. For example, 1.1.2.1's + NEXT would be 1.1.2.2. + In other words, NEXT always means "where to find the next deltatext + that you need this revision to retrieve". + """ pass def tree_completed(self): + """Reports that the RCS revision tree has been parsed. This function is + called exactly once. This function will be called later than + admin_completed(). + """ pass def set_description(self, description): + """Reports the description from the RCS file. This is set using the + "-m" flag to "cvs add". However, many CVS users don't use that option, + so this is often empty. + + This function is called once, after tree_completed(). + + Parameter: DESCRIPTION is a string containing the description. This may + be multi-line. + """ pass def set_revision_info(self, revision, log, text): + """Reports the log message and contents of a CVS revision. + + This function is called for each revision. It is called later than + set_description(). + + Parameters: REVISION is a string containing the actual revision number. + LOG is a string containing the log message. This may be multi-line. + TEXT is the contents of the file in this revision, either as full-text or + as a diff. This is usually multi-line, and often quite large and/or + binary. + """ pass def parse_completed(self): + """Reports that parsing an RCS file is complete. + + This function is called once. After it is called, no more calls will be + made via this interface. + """ pass @@ -194,7 +341,8 @@ class _Parser: else: # Chew up "newphrase" # warn("Unexpected RCS token: $token\n") - pass + while self.ts.get() != ';': + pass else: if f is None: self.ts.unget(token) @@ -208,7 +356,7 @@ class _Parser: date = self.ts.get() self.ts.match(';') - # Convert date into timestamp + # Convert date into standard UNIX time format (seconds since epoch) date_fields = string.split(date, '.') # According to rcsfile(5): the year "contains just the last two # digits of the year for years from 1900 through 1999, and all the @@ -218,8 +366,11 @@ class _Parser: date_fields = map(string.atoi, date_fields) EPOCH = 1970 if date_fields[0] < EPOCH: - raise ValueError, 'invalid year' - timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,)) + raise ValueError, 'invalid year for revision %s' % (revision,) + try: + timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,)) + except ValueError, e: + raise ValueError, 'invalid date for revision %s: %s' % (revision, e,) # Parse author ### NOTE: authors containing whitespace are violations of the @@ -255,6 +406,7 @@ class _Parser: # group 15; # permissions 644; # hardlinks @configure.in@; + # commitid mLiHw3bulRjnTDGr; # this is "newphrase" in RCSFILE(5). we just want to skip over these. while 1: token = self.ts.get() @@ -298,6 +450,15 @@ class _Parser: self.sink.set_revision_info(revision, log, text) def parse(self, file, sink): + """Parse an RCS file. + + Parameters: FILE is the file object to parse. (I.e. an object of the + built-in Python type "file", usually created using Python's built-in + "open()" function). + SINK is an instance of (some subclass of) Sink. It's methods will be + called as the file is parsed; see the definition of Sink for the + details. + """ self.ts = self.stream_class(file) self.sink = sink diff --git a/lib/vclib/ccvs/rcsparse/debug.py b/lib/vclib/ccvs/rcsparse/debug.py index cfeaf2b6..f3687e28 100644 --- a/lib/vclib/ccvs/rcsparse/debug.py +++ b/lib/vclib/ccvs/rcsparse/debug.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 diff --git a/lib/vclib/ccvs/rcsparse/default.py b/lib/vclib/ccvs/rcsparse/default.py index cb117141..942aab59 100644 --- a/lib/vclib/ccvs/rcsparse/default.py +++ b/lib/vclib/ccvs/rcsparse/default.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -19,7 +19,11 @@ import string import common class _TokenStream: - token_term = string.whitespace + ';:' + token_term = string.whitespace + ";:" + try: + token_term = frozenset(token_term) + except NameError: + pass # the algorithm is about the same speed for any CHUNK_SIZE chosen. # grab a good-sized chunk, but not too large to overwhelm memory. @@ -44,15 +48,17 @@ class _TokenStream: # out more complex solutions. buf = self.buf + lbuf = len(buf) idx = self.idx while 1: - if idx == len(buf): + if idx == lbuf: buf = self.rcsfile.read(self.CHUNK_SIZE) if buf == '': # signal EOF by returning None as the token del self.buf # so we fail if get() is called again return None + lbuf = len(buf) idx = 0 if buf[idx] not in string.whitespace: @@ -60,7 +66,7 @@ class _TokenStream: idx = idx + 1 - if buf[idx] == ';' or buf[idx] == ':': + if buf[idx] in ';:': self.buf = buf self.idx = idx + 1 return buf[idx] @@ -70,17 +76,18 @@ class _TokenStream: token = '' while 1: # find token characters in the current buffer - while end < len(buf) and buf[end] not in self.token_term: + while end < lbuf and buf[end] not in self.token_term: end = end + 1 token = token + buf[idx:end] - if end < len(buf): + if end < lbuf: # we stopped before the end, so we have a full token idx = end break # we stopped at the end of the buffer, so we may have a partial token buf = self.rcsfile.read(self.CHUNK_SIZE) + lbuf = len(buf) idx = end = 0 self.buf = buf @@ -94,22 +101,24 @@ class _TokenStream: chunks = [ ] while 1: - if idx == len(buf): + if idx == lbuf: idx = 0 buf = self.rcsfile.read(self.CHUNK_SIZE) if buf == '': raise RuntimeError, 'EOF' + lbuf = len(buf) i = string.find(buf, '@', idx) if i == -1: chunks.append(buf[idx:]) - idx = len(buf) + idx = lbuf continue - if i == len(buf) - 1: + if i == lbuf - 1: chunks.append(buf[idx:i]) idx = 0 buf = '@' + self.rcsfile.read(self.CHUNK_SIZE) if buf == '@': raise RuntimeError, 'EOF' + lbuf = len(buf) continue if buf[i + 1] == '@': chunks.append(buf[idx:i+1]) diff --git a/lib/vclib/ccvs/rcsparse/texttools.py b/lib/vclib/ccvs/rcsparse/texttools.py index 03387194..65c4b167 100644 --- a/lib/vclib/ccvs/rcsparse/texttools.py +++ b/lib/vclib/ccvs/rcsparse/texttools.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 diff --git a/lib/vclib/svn/__init__.py b/lib/vclib/svn/__init__.py index 51ef3d4f..16edf805 100644 --- a/lib/vclib/svn/__init__.py +++ b/lib/vclib/svn/__init__.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2008 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -15,17 +15,50 @@ import os import os.path import re +import urllib _re_url = re.compile('^(http|https|file|svn|svn\+[^:]+)://') -def canonicalize_rootpath(rootpath): +def _canonicalize_path(path): + import svn.core try: - import svn.core - return svn.core.svn_path_canonicalize(rootpath) - except: - if re.search(_re_url, rootpath): - return rootpath[-1] == '/' and rootpath[:-1] or rootpath - return os.path.normpath(rootpath) + return svn.core.svn_path_canonicalize(path) + except AttributeError: # svn_path_canonicalize() appeared in 1.4.0 bindings + # There's so much more that we *could* do here, but if we're + # here at all its because there's a really old Subversion in + # place, and those older Subversion versions cared quite a bit + # less about the specifics of path canonicalization. + if re.search(_re_url, path): + return path.rstrip('/') + else: + return os.path.normpath(path) + + +def canonicalize_rootpath(rootpath): + # Try to canonicalize the rootpath using Subversion semantics. + rootpath = _canonicalize_path(rootpath) + + # ViewVC's support for local repositories is more complete and more + # performant than its support for remote ones, so if we're on a + # Unix-y system and we have a file:/// URL, convert it to a local + # path instead. + if os.name == 'posix': + rootpath_lower = rootpath.lower() + if rootpath_lower in ['file://localhost', + 'file://localhost/', + 'file://', + 'file:///' + ]: + return '/' + if rootpath_lower.startswith('file://localhost/'): + rootpath = os.path.normpath(urllib.unquote(rootpath[16:])) + elif rootpath_lower.startswith('file:///'): + rootpath = os.path.normpath(urllib.unquote(rootpath[7:])) + + # Ensure that we have an absolute path (or URL), and return. + if not re.search(_re_url, rootpath): + assert os.path.isabs(rootpath) + return rootpath def expand_root_parent(parent_path): @@ -35,6 +68,7 @@ def expand_root_parent(parent_path): else: # Any subdirectories of PARENT_PATH which themselves have a child # "format" are returned as roots. + assert os.path.isabs(parent_path) subpaths = os.listdir(parent_path) for rootname in subpaths: rootpath = os.path.join(parent_path, rootname) @@ -43,6 +77,20 @@ def expand_root_parent(parent_path): return roots +def find_root_in_parent(parent_path, rootname): + """Search PARENT_PATH for a root named ROOTNAME, returning the + canonicalized ROOTPATH of the root if found; return None if no such + root is found.""" + + if not re.search(_re_url, parent_path): + assert os.path.isabs(parent_path) + rootpath = os.path.join(parent_path, rootname) + format_path = os.path.join(rootpath, "format") + if os.path.exists(format_path): + return canonicalize_rootpath(rootpath) + return None + + def SubversionRepository(name, rootpath, authorizer, utilities, config_dir): rootpath = canonicalize_rootpath(rootpath) if re.search(_re_url, rootpath): diff --git a/lib/vclib/svn/svn_ra.py b/lib/vclib/svn/svn_ra.py index 1411f137..77d370f3 100644 --- a/lib/vclib/svn/svn_ra.py +++ b/lib/vclib/svn/svn_ra.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -15,13 +15,14 @@ import vclib import sys import os -import string import re import tempfile -import popen2 import time import urllib -from svn_repos import Revision, SVNChangedPath, _datestr_to_date, _compare_paths, _path_parts, _cleanup_path, _rev2optrev, _fix_subversion_exception +from svn_repos import Revision, SVNChangedPath, _datestr_to_date, \ + _compare_paths, _path_parts, _cleanup_path, \ + _rev2optrev, _fix_subversion_exception, \ + _split_revprops, _canonicalize_path from svn import core, delta, client, wc, ra @@ -52,13 +53,71 @@ def get_directory_props(ra_session, path, rev): props = ra.svn_ra_get_dir(ra_session, path, rev) return props +def client_log(url, start_rev, end_rev, log_limit, include_changes, + cross_copies, cb_func, ctx): + include_changes = include_changes and 1 or 0 + cross_copies = cross_copies and 1 or 0 + try: + client.svn_client_log4([url], start_rev, start_rev, end_rev, + log_limit, include_changes, not cross_copies, + 0, None, cb_func, ctx) + except AttributeError: + # Wrap old svn_log_message_receiver_t interface with a + # svn_log_entry_t one. + def cb_convert(paths, revision, author, date, message, pool): + class svn_log_entry_t: + pass + log_entry = svn_log_entry_t() + log_entry.changed_paths = paths + log_entry.revision = revision + log_entry.revprops = { core.SVN_PROP_REVISION_LOG : message, + core.SVN_PROP_REVISION_AUTHOR : author, + core.SVN_PROP_REVISION_DATE : date, + } + cb_func(log_entry, pool) + client.svn_client_log2([url], start_rev, end_rev, log_limit, + include_changes, not cross_copies, cb_convert, ctx) + + +def setup_client_ctx(config_dir): + # Ensure that the configuration directory exists. + core.svn_config_ensure(config_dir) + + # Fetch the configuration (and 'config' bit thereof). + cfg = core.svn_config_get_config(config_dir) + config = cfg.get(core.SVN_CONFIG_CATEGORY_CONFIG) + + # Here's the compat-sensitive part: try to use + # svn_cmdline_create_auth_baton(), and fall back to making our own + # if that fails. + try: + auth_baton = core.svn_cmdline_create_auth_baton(1, None, None, config_dir, + 1, 1, config, None) + except AttributeError: + auth_baton = core.svn_auth_open([ + client.svn_client_get_simple_provider(), + client.svn_client_get_username_provider(), + client.svn_client_get_ssl_server_trust_file_provider(), + client.svn_client_get_ssl_client_cert_file_provider(), + client.svn_client_get_ssl_client_cert_pw_file_provider(), + ]) + if config_dir is not None: + core.svn_auth_set_parameter(auth_baton, + core.SVN_AUTH_PARAM_CONFIG_DIR, + config_dir) + + # Create, setup, and return the client context baton. + ctx = client.svn_client_create_context() + ctx.config = cfg + ctx.auth_baton = auth_baton + return ctx + ### END COMPATABILITY CODE ### class LogCollector: - ### TODO: Make this thing authz-aware - def __init__(self, path, show_all_logs, lockinfo): + def __init__(self, path, show_all_logs, lockinfo, access_check_func): # This class uses leading slashes for paths internally if not path: self.path = '/' @@ -67,8 +126,16 @@ class LogCollector: self.logs = [] self.show_all_logs = show_all_logs self.lockinfo = lockinfo + self.access_check_func = access_check_func + self.done = False + + def add_log(self, log_entry, pool): + if self.done: + return + paths = log_entry.changed_paths + revision = log_entry.revision + msg, author, date, revprops = _split_revprops(log_entry.revprops) - def add_log(self, paths, revision, author, date, message, pool): # Changed paths have leading slashes changed_paths = paths.keys() changed_paths.sort(lambda a, b: _compare_paths(a, b)) @@ -82,19 +149,23 @@ class LogCollector: if changed_path != self.path: # If a parent of our path was copied, our "next previous" # (huh?) path will exist elsewhere (under the copy source). - if (string.rfind(self.path, changed_path) == 0) and \ + if (self.path.rfind(changed_path) == 0) and \ self.path[len(changed_path)] == '/': change = paths[changed_path] if change.copyfrom_path: this_path = change.copyfrom_path + self.path[len(changed_path):] if self.show_all_logs or this_path: - entry = Revision(revision, _datestr_to_date(date), author, message, None, - self.lockinfo, self.path[1:], None, None) - self.logs.append(entry) + if self.access_check_func is None \ + or self.access_check_func(self.path[1:], revision): + entry = Revision(revision, date, author, msg, None, self.lockinfo, + self.path[1:], None, None) + self.logs.append(entry) + else: + self.done = True if this_path: self.path = this_path -def temp_checkout(svnrepos, path, rev): +def cat_to_tempfile(svnrepos, path, rev): """Check out file revision to temporary file""" temp = tempfile.mktemp() stream = core.svn_stream_from_aprfile(temp) @@ -155,21 +226,8 @@ class RemoteSubversionRepository(vclib.Repository): def open(self): # Setup the client context baton, complete with non-prompting authstuffs. - # TODO: svn_cmdline_setup_auth_baton() is mo' better (when available) - core.svn_config_ensure(self.config_dir) - self.ctx = client.svn_client_ctx_t() - self.ctx.auth_baton = core.svn_auth_open([ - client.svn_client_get_simple_provider(), - client.svn_client_get_username_provider(), - client.svn_client_get_ssl_server_trust_file_provider(), - client.svn_client_get_ssl_client_cert_file_provider(), - client.svn_client_get_ssl_client_cert_pw_file_provider(), - ]) - self.ctx.config = core.svn_config_get_config(self.config_dir) - if self.config_dir is not None: - core.svn_auth_set_parameter(self.ctx.auth_baton, - core.SVN_AUTH_PARAM_CONFIG_DIR, - self.config_dir) + self.ctx = setup_client_ctx(self.config_dir) + ra_callbacks = ra.svn_ra_callbacks_t() ra_callbacks.auth_baton = self.ctx.auth_baton self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None, @@ -177,6 +235,10 @@ class RemoteSubversionRepository(vclib.Repository): self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session) self._dirent_cache = { } self._revinfo_cache = { } + + # See if a universal read access determination can be made. + if self.auth and self.auth.check_universal_access(self.name) == 1: + self.auth = None def rootname(self): return self.name @@ -211,25 +273,23 @@ class RemoteSubversionRepository(vclib.Repository): raise vclib.ItemNotFound(path_parts) return pathtype - def openfile(self, path_parts, rev): + def openfile(self, path_parts, rev, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) url = self._geturl(path) - tmp_file = tempfile.mktemp() - stream = core.svn_stream_from_aprfile(tmp_file) ### rev here should be the last history revision of the URL - client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev), self.ctx) - core.svn_stream_close(stream) - return SelfCleanFP(tmp_file), self._get_last_history_rev(path_parts, rev) + fp = SelfCleanFP(cat_to_tempfile(self, path, rev)) + lh_rev, c_rev = self._get_last_history_rev(path_parts, rev) + return fp, lh_rev def listdir(self, path_parts, rev, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check raise vclib.Error("Path '%s' is not a directory." % path) rev = self._getrev(rev) - entries = [ ] + entries = [] dirents, locks = self._get_dirents(path, rev) for name in dirents.keys(): entry = dirents[name] @@ -237,8 +297,9 @@ class RemoteSubversionRepository(vclib.Repository): kind = vclib.DIR elif entry.kind == core.svn_node_file: kind = vclib.FILE - if vclib.check_path_access(self, path_parts + [name], kind, rev): - entries.append(vclib.DirEntry(name, kind)) + else: + kind = None + entries.append(vclib.DirEntry(name, kind)) return entries def dirlogs(self, path_parts, rev, entries, options): @@ -249,11 +310,13 @@ class RemoteSubversionRepository(vclib.Repository): dirents, locks = self._get_dirents(path, rev) for entry in entries: entry_path_parts = path_parts + [entry.name] - if not vclib.check_path_access(self, entry_path_parts, entry.kind, rev): + dirent = dirents.get(entry.name, None) + # dirents is authz-sanitized, so ensure the entry is found therein. + if dirent is None: continue - dirent = dirents[entry.name] - entry.date, entry.author, entry.log, changes = \ - self.revinfo(dirent.created_rev) + # Get authz-sanitized revision metadata. + entry.date, entry.author, entry.log, revprops, changes = \ + self._revinfo(dirent.created_rev) entry.rev = str(dirent.created_rev) entry.size = dirent.size entry.lockinfo = None @@ -267,29 +330,51 @@ class RemoteSubversionRepository(vclib.Repository): rev = self._getrev(rev) url = self._geturl(path) - # Use ls3 to fetch the lock status for this item. - lockinfo = None - basename = path_parts and path_parts[-1] or "" - dirents, locks = list_directory(url, _rev2optrev(rev), - _rev2optrev(rev), 0, self.ctx) - if locks.has_key(basename): - lockinfo = locks[basename].owner + # If this is a file, fetch the lock status and size (as of REV) + # for this item. + lockinfo = size_in_rev = None + if path_type == vclib.FILE: + basename = path_parts[-1] + list_url = self._geturl(self._getpath(path_parts[:-1])) + dirents, locks = list_directory(list_url, _rev2optrev(rev), + _rev2optrev(rev), 0, self.ctx) + if locks.has_key(basename): + lockinfo = locks[basename].owner + if dirents.has_key(basename): + size_in_rev = dirents[basename].size + + # Special handling for the 'svn_latest_log' scenario. + ### FIXME: Don't like this hack. We should just introduce + ### something more direct in the vclib API. + if options.get('svn_latest_log', 0): + dir_lh_rev, dir_c_rev = self._get_last_history_rev(path_parts, rev) + date, author, log, revprops, changes = self._revinfo(dir_lh_rev) + return [vclib.Revision(dir_lh_rev, str(dir_lh_rev), date, author, + None, log, size_in_rev, lockinfo)] + def _access_checker(check_path, check_rev): + return vclib.check_path_access(self, _path_parts(check_path), + path_type, check_rev) + # It's okay if we're told to not show all logs on a file -- all # the revisions should match correctly anyway. - lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0), lockinfo) + lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0), + lockinfo, _access_checker) cross_copies = options.get('svn_cross_copies', 0) log_limit = 0 if limit: log_limit = first + limit - client.svn_client_log2([url], _rev2optrev(rev), _rev2optrev(1), - log_limit, 1, not cross_copies, - lc.add_log, self.ctx) + client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1, + cross_copies, lc.add_log, self.ctx) revs = lc.logs revs.sort() prev = None for rev in revs: + # Swap out revision info with stuff from the cache (which is + # authz-sanitized). + rev.date, rev.author, rev.log, revprops, changes \ + = self._revinfo(rev.number) rev.prev = prev prev = rev revs.reverse() @@ -309,13 +394,25 @@ class RemoteSubversionRepository(vclib.Repository): _rev2optrev(rev), 0, self.ctx) return pairs and pairs[0][1] or {} - def annotate(self, path_parts, rev): + def annotate(self, path_parts, rev, include_text=False): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) url = self._geturl(path) + # Examine logs for the file to determine the oldest revision we are + # permitted to see. + log_options = { + 'svn_cross_copies' : 1, + 'svn_show_all_dir_logs' : 1, + } + revs = self.itemlog(path_parts, rev, vclib.SORTBY_REV, 0, 0, log_options) + oldest_rev = revs[-1].number + + # Now calculate the annotation data. Note that we'll not + # inherently trust the provided author and date, because authz + # rules might necessitate that we strip that information out. blame_data = [] def _blame_cb(line_no, revision, author, date, @@ -323,21 +420,27 @@ class RemoteSubversionRepository(vclib.Repository): prev_rev = None if revision > 1: prev_rev = revision - 1 - blame_data.append(vclib.Annotation(line, line_no+1, revision, prev_rev, - author, None)) - - client.svn_client_blame(url, _rev2optrev(1), _rev2optrev(rev), - _blame_cb, self.ctx) + # If we have an invalid revision, clear the date and author + # values. Otherwise, if we have authz filtering to do, use the + # revinfo cache to do so. + if revision < 0: + date = author = None + elif self.auth: + date, author, msg, revprops, changes = self._revinfo(revision) + + # Strip text if the caller doesn't want it. + if not include_text: + line = None + blame_data.append(vclib.Annotation(line, line_no + 1, revision, prev_rev, + author, date)) + + client.blame2(url, _rev2optrev(rev), _rev2optrev(oldest_rev), + _rev2optrev(rev), _blame_cb, self.ctx) return blame_data, rev def revinfo(self, rev): - rev = self._getrev(rev) - cached_info = self._revinfo_cache.get(rev) - if not cached_info: - cached_info = self._revinfo_raw(rev) - self._revinfo_cache[rev] = cached_info - return cached_info[0], cached_info[1], cached_info[2], cached_info[3] + return self._revinfo(rev, 1) def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): @@ -362,18 +465,18 @@ class RemoteSubversionRepository(vclib.Repository): args = vclib._diff_args(type, options) def _date_from_rev(rev): - date, author, msg, changes = self.revinfo(rev) + date, author, msg, revprops, changes = self._revinfo(rev) return date try: info1 = p1, _date_from_rev(r1), r1 info2 = p2, _date_from_rev(r2), r2 if p1: - temp1 = temp_checkout(self, p1, r1) + temp1 = cat_to_tempfile(self, p1, r1) else: temp1 = '/dev/null' if p2: - temp2 = temp_checkout(self, p2, r2) + temp2 = cat_to_tempfile(self, p2, r2) else: temp2 = '/dev/null' return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args) @@ -386,15 +489,27 @@ class RemoteSubversionRepository(vclib.Repository): props = self.itemprops(path_parts, rev) # does authz-check return props.has_key(core.SVN_PROP_EXECUTABLE) + def filesize(self, path_parts, rev): + path = self._getpath(path_parts) + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." % path) + rev = self._getrev(rev) + dirents, locks = self._get_dirents(self._getpath(path_parts[:-1]), rev) + dirent = dirents.get(path_parts[-1], None) + return dirent.size + def _getpath(self, path_parts): - return string.join(path_parts, '/') + return '/'.join(path_parts) def _getrev(self, rev): if rev is None or rev == 'HEAD': return self.youngest try: + if type(rev) == type(''): + while rev[0] == 'r': + rev = rev[1:] rev = int(rev) - except ValueError: + except: raise vclib.InvalidRevision(rev) if (rev < 0) or (rev > self.youngest): raise vclib.InvalidRevision(rev) @@ -403,58 +518,139 @@ class RemoteSubversionRepository(vclib.Repository): def _geturl(self, path=None): if not path: return self.rootpath - return self.rootpath + '/' + urllib.quote(path, "/*~") + path = self.rootpath + '/' + urllib.quote(path) + return _canonicalize_path(path) def _get_dirents(self, path, rev): """Return a 2-type of dirents and locks, possibly reading/writing - from a local cache of that information.""" + from a local cache of that information. This functions performs + authz checks, stripping out unreadable dirents.""" dir_url = self._geturl(path) + path_parts = _path_parts(path) if path: key = str(rev) + '/' + path else: key = str(rev) + + # Ensure that the cache gets filled... dirents_locks = self._dirent_cache.get(key) if not dirents_locks: - dirents, locks = list_directory(dir_url, _rev2optrev(rev), - _rev2optrev(rev), 0, self.ctx) + tmp_dirents, locks = list_directory(dir_url, _rev2optrev(rev), + _rev2optrev(rev), 0, self.ctx) + dirents = {} + for name, dirent in tmp_dirents.items(): + dirent_parts = path_parts + [name] + kind = dirent.kind + if (kind == core.svn_node_dir or kind == core.svn_node_file) \ + and vclib.check_path_access(self, dirent_parts, + kind == core.svn_node_dir \ + and vclib.DIR or vclib.FILE, rev): + lh_rev, c_rev = self._get_last_history_rev(dirent_parts, rev) + dirent.created_rev = lh_rev + dirents[name] = dirent dirents_locks = [dirents, locks] self._dirent_cache[key] = dirents_locks + + # ...then return the goodies from the cache. return dirents_locks[0], dirents_locks[1] def _get_last_history_rev(self, path_parts, rev): + """Return the a 2-tuple which contains: + - the last interesting revision equal to or older than REV in + the history of PATH_PARTS. + - the created_rev of of PATH_PARTS as of REV.""" + + path = self._getpath(path_parts) url = self._geturl(self._getpath(path_parts)) optrev = _rev2optrev(rev) + + # Get the last-changed-rev. revisions = [] def _info_cb(path, info, pool, retval=revisions): revisions.append(info.last_changed_rev) client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx) - return revisions[0] - - def _revinfo_raw(self, rev): - # return 5-tuple (date, author, message, changes) - optrev = _rev2optrev(rev) - revs = [] + last_changed_rev = revisions[0] - def _log_cb(changed_paths, revision, author, - datestr, message, pool, retval=revs): - date = _datestr_to_date(datestr) + # Now, this object might not have been directly edited since the + # last-changed-rev, but it might have been the child of a copy. + # To determine this, we'll run a potentially no-op log between + # LAST_CHANGED_REV and REV. + lc = LogCollector(path, 1, None, None) + client_log(url, optrev, _rev2optrev(last_changed_rev), 1, 1, 0, + lc.add_log, self.ctx) + revs = lc.logs + if revs: + revs.sort() + return revs[0].number, last_changed_rev + else: + return last_changed_rev, last_changed_rev + + def _revinfo_fetch(self, rev, include_changed_paths=0): + need_changes = include_changed_paths or self.auth + revs = [] + + def _log_cb(log_entry, pool, retval=revs): + # If Subversion happens to call us more than once, we choose not + # to care. + if retval: + return + + revision = log_entry.revision + msg, author, date, revprops = _split_revprops(log_entry.revprops) action_map = { 'D' : vclib.DELETED, 'A' : vclib.ADDED, 'R' : vclib.REPLACED, 'M' : vclib.MODIFIED, } - paths = (changed_paths or {}).keys() + + # Easy out: if we won't use the changed-path info, just return a + # changes-less tuple. + if not need_changes: + return revs.append([date, author, msg, revprops, None]) + + # Subversion 1.5 and earlier didn't offer the 'changed_paths2' + # hash, and in Subversion 1.6, it's offered but broken. + try: + changed_paths = log_entry.changed_paths2 + paths = (changed_paths or {}).keys() + except: + changed_paths = log_entry.changed_paths + paths = (changed_paths or {}).keys() paths.sort(lambda a, b: _compare_paths(a, b)) + + # If we get this far, our caller needs changed-paths, or we need + # them for authz-related sanitization. changes = [] found_readable = found_unreadable = 0 for path in paths: - pathtype = None change = changed_paths[path] + + # svn_log_changed_path_t (which we might get instead of the + # svn_log_changed_path2_t we'd prefer) doesn't have the + # 'node_kind' member. + pathtype = None + if hasattr(change, 'node_kind'): + if change.node_kind == core.svn_node_dir: + pathtype = vclib.DIR + elif change.node_kind == core.svn_node_file: + pathtype = vclib.FILE + + # svn_log_changed_path2_t only has the 'text_modified' and + # 'props_modified' bits in Subversion 1.7 and beyond. And + # svn_log_changed_path_t is without. + text_modified = props_modified = 0 + if hasattr(change, 'text_modified'): + if change.text_modified == core.svn_tristate_true: + text_modified = 1 + if hasattr(change, 'props_modified'): + if change.props_modified == core.svn_tristate_true: + props_modified = 1 + + # Wrong, diddily wrong wrong wrong. Can you say, + # "Manufacturing data left and right because it hurts to + # figure out the right stuff?" action = action_map.get(change.action, vclib.MODIFIED) - ### Wrong, diddily wrong wrong wrong. Can you say, - ### "Manufacturing data left and right because it hurts to - ### figure out the right stuff?" if change.copyfrom_path and change.copyfrom_rev: is_copy = 1 base_path = change.copyfrom_path @@ -467,31 +663,61 @@ class RemoteSubversionRepository(vclib.Repository): base_path = path base_rev = revision - 1 - ### Check authz rules (we lie about the path type) + # Check authz rules (sadly, we have to lie about the path type) parts = _path_parts(path) if vclib.check_path_access(self, parts, vclib.FILE, revision): if is_copy and base_path and (base_path != path): parts = _path_parts(base_path) - if vclib.check_path_access(self, parts, vclib.FILE, base_rev): + if not vclib.check_path_access(self, parts, vclib.FILE, base_rev): is_copy = 0 base_path = None base_rev = None + found_unreadable = 1 changes.append(SVNChangedPath(path, revision, pathtype, base_path, - base_rev, action, is_copy, 0, 0)) + base_rev, action, is_copy, + text_modified, props_modified)) found_readable = 1 else: found_unreadable = 1 + # If our caller doesn't want changed-path stuff, and we have + # the info we need to make an authz determination already, + # quit this loop and get on with it. + if (not include_changed_paths) and found_unreadable and found_readable: + break + + # Filter unreadable information. if found_unreadable: - message = None + msg = None if not found_readable: author = None date = None - revs.append([date, author, message, changes]) - client.svn_client_log([self.rootpath], optrev, optrev, - 1, 0, _log_cb, self.ctx) - return revs[0][0], revs[0][1], revs[0][2], revs[0][3] + # Drop unrequested changes. + if not include_changed_paths: + changes = None + + # Add this revision information to the "return" array. + retval.append([date, author, msg, revprops, changes]) + + optrev = _rev2optrev(rev) + client_log(self.rootpath, optrev, optrev, 1, need_changes, 0, + _log_cb, self.ctx) + return tuple(revs[0]) + + def _revinfo(self, rev, include_changed_paths=0): + """Internal-use, cache-friendly revision information harvester.""" + + # Consult the revinfo cache first. If we don't have cached info, + # or our caller wants changed paths and we don't have those for + # this revision, go do the real work. + rev = self._getrev(rev) + cached_info = self._revinfo_cache.get(rev) + if not cached_info \ + or (include_changed_paths and cached_info[4] is None): + cached_info = self._revinfo_fetch(rev, include_changed_paths) + self._revinfo_cache[rev] = cached_info + return cached_info ##--- custom --## @@ -510,23 +736,16 @@ class RemoteSubversionRepository(vclib.Repository): old_path = results[old_rev] except KeyError: raise vclib.ItemNotFound(path) - - return _cleanup_path(old_path) + old_path = _cleanup_path(old_path) + old_path_parts = _path_parts(old_path) + # Check access (lying about path types) + if not vclib.check_path_access(self, old_path_parts, vclib.FILE, old_rev): + raise vclib.ItemNotFound(path) + return old_path def created_rev(self, path, rev): - # NOTE: We can't use svn_client_propget here because the - # interfaces in that layer strip out the properties not meant for - # human consumption (such as svn:entry:committed-rev, which we are - # using here to get the created revision of PATH@REV). - kind = ra.svn_ra_check_path(self.ra_session, path, rev) - if kind == core.svn_node_none: - raise vclib.ItemNotFound(_path_parts(path)) - elif kind == core.svn_node_dir: - props = get_directory_props(self.ra_session, path, rev) - elif kind == core.svn_node_file: - fetched_rev, props = ra.svn_ra_get_file(self.ra_session, path, rev, None) - return int(props.get(core.SVN_PROP_ENTRY_COMMITTED_REV, - SVN_INVALID_REVNUM)) + lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev) + return lh_rev def last_rev(self, path, peg_revision, limit_revision=None): """Given PATH, known to exist in PEG_REVISION, find the youngest @@ -561,3 +780,33 @@ class RemoteSubversionRepository(vclib.Repository): else: peg_revision = mid return peg_revision, path + + def get_symlink_target(self, path_parts, rev): + """Return the target of the symbolic link versioned at PATH_PARTS + in REV, or None if that object is not a symlink.""" + + path = self._getpath(path_parts) + path_type = self.itemtype(path_parts, rev) # does auth-check + rev = self._getrev(rev) + url = self._geturl(path) + + # Symlinks must be files with the svn:special property set on them + # and with file contents which read "link SOME_PATH". + if path_type != vclib.FILE: + return None + pairs = client.svn_client_proplist2(url, _rev2optrev(rev), + _rev2optrev(rev), 0, self.ctx) + props = pairs and pairs[0][1] or {} + if not props.has_key(core.SVN_PROP_SPECIAL): + return None + pathspec = '' + ### FIXME: We're being a touch sloppy here, first by grabbing the + ### whole file and then by checking only the first line + ### of it. + fp = SelfCleanFP(cat_to_tempfile(self, path, rev)) + pathspec = fp.readline() + fp.close() + if pathspec[:5] != 'link ': + return None + return pathspec[5:] + diff --git a/lib/vclib/svn/svn_repos.py b/lib/vclib/svn/svn_repos.py index 3e110f6b..c96d4cb1 100644 --- a/lib/vclib/svn/svn_repos.py +++ b/lib/vclib/svn/svn_repos.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -15,15 +15,12 @@ import vclib import os import os.path -import stat -import string import cStringIO -import signal -import shutil import time import tempfile import popen import re +import urllib from svn import fs, repos, core, client, delta, diff @@ -31,25 +28,39 @@ from svn import fs, repos, core, client, delta, diff if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1): raise Exception, "Version requirement not met (needs 1.3.1 or better)" + +### Pre-1.5 Subversion doesn't have SVN_ERR_CEASE_INVOCATION +try: + _SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CEASE_INVOCATION +except: + _SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CANCELLED + ### Pre-1.5 SubversionException's might not have the .msg and .apr_err members def _fix_subversion_exception(e): if not hasattr(e, 'apr_err'): e.apr_err = e[1] if not hasattr(e, 'message'): e.message = e[0] - + +### Pre-1.4 Subversion doesn't have svn_path_canonicalize() +def _canonicalize_path(path): + try: + return core.svn_path_canonicalize(path) + except AttributeError: + return path + def _allow_all(root, path, pool): """Generic authz_read_func that permits access to all paths""" return 1 def _path_parts(path): - return filter(None, string.split(path, '/')) + return filter(None, path.split('/')) def _cleanup_path(path): """Return a cleaned-up Subversion filesystem path""" - return string.join(_path_parts(path), '/') + return '/'.join(_path_parts(path)) def _fs_path_join(base, relative): @@ -103,11 +114,37 @@ def _rev2optrev(rev): def _rootpath2url(rootpath, path): rootpath = os.path.abspath(rootpath) - if rootpath and rootpath[0] != '/': - rootpath = '/' + rootpath + drive, rootpath = os.path.splitdrive(rootpath) if os.sep != '/': - rootpath = string.replace(rootpath, os.sep, '/') - return 'file://' + string.join([rootpath, path], "/") + rootpath = rootpath.replace(os.sep, '/') + rootpath = urllib.quote(rootpath) + path = urllib.quote(path) + if drive: + url = 'file:///' + drive + rootpath + '/' + path + else: + url = 'file://' + rootpath + '/' + path + return _canonicalize_path(url) + + +# Given a dictionary REVPROPS of revision properties, pull special +# ones out of them and return a 4-tuple containing the log message, +# the author, the date (converted from the date string property), and +# a dictionary of any/all other revprops. +def _split_revprops(revprops): + if not revprops: + return None, None, None, {} + special_props = [] + for prop in core.SVN_PROP_REVISION_LOG, \ + core.SVN_PROP_REVISION_AUTHOR, \ + core.SVN_PROP_REVISION_DATE: + if revprops.has_key(prop): + special_props.append(revprops[prop]) + del(revprops[prop]) + else: + special_props.append(None) + msg, author, datestr = tuple(special_props) + date = _datestr_to_date(datestr) + return msg, author, date, revprops def _datestr_to_date(datestr): @@ -160,7 +197,7 @@ class NodeHistory: test_path = path found = 0 while 1: - off = string.rfind(test_path, '/') + off = test_path.rfind('/') if off < 0: break test_path = test_path[0:off] @@ -173,64 +210,11 @@ class NodeHistory: return self.histories.append([revision, _cleanup_path(path)]) if self.limit and len(self.histories) == self.limit: - raise core.SubversionException("", core.SVN_ERR_CEASE_INVOCATION) + raise core.SubversionException("", _SVN_ERR_CEASE_INVOCATION) def __getitem__(self, idx): return self.histories[idx] - -def _get_history(svnrepos, path, rev, path_type, limit=0, options={}): - if svnrepos.youngest == 0: - return [] - - rev_paths = [] - fsroot = svnrepos._getroot(rev) - show_all_logs = options.get('svn_show_all_dir_logs', 0) - if not show_all_logs: - # See if the path is a file or directory. - kind = fs.check_path(fsroot, path) - if kind is core.svn_node_file: - show_all_logs = 1 - - # Instantiate a NodeHistory collector object, and use it to collect - # history items for PATH@REV. - history = NodeHistory(svnrepos.fs_ptr, show_all_logs, limit) - try: - repos.svn_repos_history(svnrepos.fs_ptr, path, history.add_history, - 1, rev, options.get('svn_cross_copies', 0)) - except core.SubversionException, e: - _fix_subversion_exception(e) - if e.apr_err != core.SVN_ERR_CEASE_INVOCATION: - raise - - # Now, iterate over those history items, checking for changes of - # location, pruning as necessitated by authz rules. - for hist_rev, hist_path in history: - path_parts = _path_parts(hist_path) - if not vclib.check_path_access(svnrepos, path_parts, path_type, hist_rev): - break - rev_paths.append([hist_rev, hist_path]) - return rev_paths - - -def _log_helper(svnrepos, path, rev, lockinfo): - rev_root = fs.revision_root(svnrepos.fs_ptr, rev) - - # Was this path@rev the target of a copy? - copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path) - - # Assemble our LogEntry - date, author, msg, changes = svnrepos.revinfo(rev) - if fs.is_file(rev_root, path): - size = fs.file_length(rev_root, path) - else: - size = None - entry = Revision(rev, date, author, msg, size, lockinfo, path, - copyfrom_path and _cleanup_path(copyfrom_path), - copyfrom_rev) - return entry - - def _get_last_history_rev(fsroot, path): history = fs.node_history(fsroot, path) history = fs.history_prev(history, 0) @@ -309,14 +293,15 @@ class FileContentsPipe: class BlameSource: - def __init__(self, local_url, rev, first_rev): + def __init__(self, local_url, rev, first_rev, include_text, config_dir): self.idx = -1 self.first_rev = first_rev self.blame_data = [] + self.include_text = include_text - ctx = client.ctx_t() - core.svn_config_ensure(None) - ctx.config = core.svn_config_get_config(None) + ctx = client.svn_client_create_context() + core.svn_config_ensure(config_dir) + ctx.config = core.svn_config_get_config(config_dir) ctx.auth_baton = core.svn_auth_open([]) try: ### TODO: Is this use of FIRST_REV always what we want? Should we @@ -333,6 +318,8 @@ class BlameSource: prev_rev = None if rev > self.first_rev: prev_rev = rev - 1 + if not self.include_text: + text = None self.blame_data.append(vclib.Annotation(text, line_no + 1, rev, prev_rev, author, None)) @@ -369,31 +356,14 @@ class LocalSubversionRepository(vclib.Repository): self.rootpath = rootpath self.name = name self.auth = authorizer - self.svn_client_path = utilities.svn or 'svn' self.diff_cmd = utilities.diff or 'diff' - self.config_dir = config_dir + self.config_dir = config_dir or None # See if this repository is even viewable, authz-wise. if not vclib.check_root_access(self): raise vclib.ReposNotFound(name) def open(self): - # Register a handler for SIGTERM so we can have a chance to - # cleanup. If ViewVC takes too long to start generating CGI - # output, Apache will grow impatient and SIGTERM it. While we - # don't mind getting told to bail, we want to gracefully close the - # repository before we bail. - def _sigterm_handler(signum, frame, self=self): - sys.exit(-1) - try: - signal.signal(signal.SIGTERM, _sigterm_handler) - except ValueError: - # This is probably "ValueError: signal only works in main - # thread", which will get thrown by the likes of mod_python - # when trying to install a signal handler from a thread that - # isn't the main one. We'll just not care. - pass - # Open the repository and init some other variables. self.repos = repos.svn_repos_open(self.rootpath) self.fs_ptr = repos.svn_repos_fs(self.repos) @@ -401,6 +371,10 @@ class LocalSubversionRepository(vclib.Repository): self._fsroots = {} self._revinfo_cache = {} + # See if a universal read access determination can be made. + if self.auth and self.auth.check_universal_access(self.name) == 1: + self.auth = None + def rootname(self): return self.name @@ -416,19 +390,14 @@ class LocalSubversionRepository(vclib.Repository): def itemtype(self, path_parts, rev): rev = self._getrev(rev) basepath = self._getpath(path_parts) - kind = fs.check_path(self._getroot(rev), basepath) - pathtype = None - if kind == core.svn_node_dir: - pathtype = vclib.DIR - elif kind == core.svn_node_file: - pathtype = vclib.FILE - else: + pathtype = self._gettype(basepath, rev) + if pathtype is None: raise vclib.ItemNotFound(path_parts) if not vclib.check_path_access(self, path_parts, pathtype, rev): raise vclib.ItemNotFound(path_parts) return pathtype - def openfile(self, path_parts, rev): + def openfile(self, path_parts, rev, options): path = self._getpath(path_parts) if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check raise vclib.Error("Path '%s' is not a file." % path) @@ -467,7 +436,7 @@ class LocalSubversionRepository(vclib.Repository): continue path = self._getpath(entry_path_parts) entry_rev = _get_last_history_rev(fsroot, path) - date, author, msg, changes = self.revinfo(entry_rev) + date, author, msg, revprops, changes = self._revinfo(entry_rev) entry.rev = str(entry_rev) entry.date = date entry.author = author @@ -516,20 +485,19 @@ class LocalSubversionRepository(vclib.Repository): # 'limit' parameter here as numeric cut-off for the depth of our # history search. if options.get('svn_latest_log', 0): - revision = _log_helper(self, path, rev, lockinfo) + revision = self._log_helper(path, rev, lockinfo) if revision: revision.prev = None revs.append(revision) else: - history = _get_history(self, path, rev, path_type, - first + limit, options) + history = self._get_history(path, rev, path_type, first + limit, options) if len(history) < first: history = [] if limit: history = history[first:first+limit] for hist_rev, hist_path in history: - revision = _log_helper(self, hist_path, hist_rev, lockinfo) + revision = self._log_helper(hist_path, hist_rev, lockinfo) if revision: # If we have unreadable copyfrom data, obscure it. if revision.copy_path is not None: @@ -550,114 +518,23 @@ class LocalSubversionRepository(vclib.Repository): fsroot = self._getroot(rev) return fs.node_proplist(fsroot, path) - def annotate(self, path_parts, rev): + def annotate(self, path_parts, rev, include_text=False): path = self._getpath(path_parts) path_type = self.itemtype(path_parts, rev) # does auth-check if path_type != vclib.FILE: raise vclib.Error("Path '%s' is not a file." % path) rev = self._getrev(rev) fsroot = self._getroot(rev) - history = _get_history(self, path, rev, path_type, 0, - {'svn_cross_copies': 1}) + history = self._get_history(path, rev, path_type, 0, + {'svn_cross_copies': 1}) youngest_rev, youngest_path = history[0] oldest_rev, oldest_path = history[-1] - source = BlameSource(_rootpath2url(self.rootpath, path), - youngest_rev, oldest_rev) + source = BlameSource(_rootpath2url(self.rootpath, path), youngest_rev, + oldest_rev, include_text, self.config_dir) return source, youngest_rev - def _revinfo_raw(self, rev): - fsroot = self._getroot(rev) - - # Get the changes for the revision - editor = repos.ChangeCollector(self.fs_ptr, fsroot) - e_ptr, e_baton = delta.make_editor(editor) - repos.svn_repos_replay(fsroot, e_ptr, e_baton) - changes = editor.get_changes() - changedpaths = {} - - # Now get the revision property info. Would use - # editor.get_root_props(), but something is broken there... - revprops = fs.revision_proplist(self.fs_ptr, rev) - msg = revprops.get(core.SVN_PROP_REVISION_LOG) - author = revprops.get(core.SVN_PROP_REVISION_AUTHOR) - datestr = revprops.get(core.SVN_PROP_REVISION_DATE) - - # Copy the Subversion changes into a new hash, converting them into - # ChangedPath objects. - found_readable = found_unreadable = 0 - for path in changes.keys(): - change = changes[path] - if change.path: - change.path = _cleanup_path(change.path) - if change.base_path: - change.base_path = _cleanup_path(change.base_path) - is_copy = 0 - if not hasattr(change, 'action'): # new to subversion 1.4.0 - action = vclib.MODIFIED - if not change.path: - action = vclib.DELETED - elif change.added: - action = vclib.ADDED - replace_check_path = path - if change.base_path and change.base_rev: - replace_check_path = change.base_path - if changedpaths.has_key(replace_check_path) \ - and changedpaths[replace_check_path].action == vclib.DELETED: - action = vclib.REPLACED - else: - if change.action == repos.CHANGE_ACTION_ADD: - action = vclib.ADDED - elif change.action == repos.CHANGE_ACTION_DELETE: - action = vclib.DELETED - elif change.action == repos.CHANGE_ACTION_REPLACE: - action = vclib.REPLACED - else: - action = vclib.MODIFIED - if (action == vclib.ADDED or action == vclib.REPLACED) \ - and change.base_path \ - and change.base_rev: - is_copy = 1 - if change.item_kind == core.svn_node_dir: - pathtype = vclib.DIR - elif change.item_kind == core.svn_node_file: - pathtype = vclib.FILE - else: - pathtype = None - - parts = _path_parts(path) - if vclib.check_path_access(self, parts, pathtype, rev): - if is_copy and change.base_path and (change.base_path != path): - parts = _path_parts(change.base_path) - if not vclib.check_path_access(self, parts, pathtype, change.base_rev): - is_copy = 0 - change.base_path = None - change.base_rev = None - changedpaths[path] = SVNChangedPath(path, rev, pathtype, - change.base_path, - change.base_rev, action, - is_copy, change.text_changed, - change.prop_changes) - found_readable = 1 - else: - found_unreadable = 1 - - # Return our tuple, auth-filtered: date, author, msg, changes - if found_unreadable: - msg = None - if not found_readable: - author = None - datestr = None - - date = _datestr_to_date(datestr) - return date, author, msg, changedpaths.values() - def revinfo(self, rev): - rev = self._getrev(rev) - cached_info = self._revinfo_cache.get(rev) - if not cached_info: - cached_info = self._revinfo_raw(rev) - self._revinfo_cache[rev] = cached_info - return cached_info[0], cached_info[1], cached_info[2], cached_info[3] + return self._revinfo(rev, 1) def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): if path_parts1: @@ -681,7 +558,7 @@ class LocalSubversionRepository(vclib.Repository): args = vclib._diff_args(type, options) def _date_from_rev(rev): - date, author, msg, changes = self.revinfo(rev) + date, author, msg, revprops, changes = self._revinfo(rev) return date try: @@ -702,21 +579,272 @@ class LocalSubversionRepository(vclib.Repository): _fix_subversion_exception(e) if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: raise vclib.InvalidRevision - raise + if e.apr_err != _SVN_ERR_CEASE_INVOCATION: + raise def isexecutable(self, path_parts, rev): props = self.itemprops(path_parts, rev) # does authz-check return props.has_key(core.SVN_PROP_EXECUTABLE) + + def filesize(self, path_parts, rev): + path = self._getpath(path_parts) + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." % path) + fsroot = self._getroot(self._getrev(rev)) + return fs.file_length(fsroot, path) + + ##--- helpers ---## + + def _revinfo(self, rev, include_changed_paths=0): + """Internal-use, cache-friendly revision information harvester.""" + + def _get_changed_paths(fsroot): + """Return a 3-tuple: found_readable, found_unreadable, changed_paths.""" + editor = repos.ChangeCollector(self.fs_ptr, fsroot) + e_ptr, e_baton = delta.make_editor(editor) + repos.svn_repos_replay(fsroot, e_ptr, e_baton) + changedpaths = {} + changes = editor.get_changes() + + # Copy the Subversion changes into a new hash, checking + # authorization and converting them into ChangedPath objects. + found_readable = found_unreadable = 0 + for path in changes.keys(): + change = changes[path] + if change.path: + change.path = _cleanup_path(change.path) + if change.base_path: + change.base_path = _cleanup_path(change.base_path) + is_copy = 0 + if not hasattr(change, 'action'): # new to subversion 1.4.0 + action = vclib.MODIFIED + if not change.path: + action = vclib.DELETED + elif change.added: + action = vclib.ADDED + replace_check_path = path + if change.base_path and change.base_rev: + replace_check_path = change.base_path + if changedpaths.has_key(replace_check_path) \ + and changedpaths[replace_check_path].action == vclib.DELETED: + action = vclib.REPLACED + else: + if change.action == repos.CHANGE_ACTION_ADD: + action = vclib.ADDED + elif change.action == repos.CHANGE_ACTION_DELETE: + action = vclib.DELETED + elif change.action == repos.CHANGE_ACTION_REPLACE: + action = vclib.REPLACED + else: + action = vclib.MODIFIED + if (action == vclib.ADDED or action == vclib.REPLACED) \ + and change.base_path \ + and change.base_rev: + is_copy = 1 + if change.item_kind == core.svn_node_dir: + pathtype = vclib.DIR + elif change.item_kind == core.svn_node_file: + pathtype = vclib.FILE + else: + pathtype = None + + parts = _path_parts(path) + if vclib.check_path_access(self, parts, pathtype, rev): + if is_copy and change.base_path and (change.base_path != path): + parts = _path_parts(change.base_path) + if not vclib.check_path_access(self, parts, pathtype, + change.base_rev): + is_copy = 0 + change.base_path = None + change.base_rev = None + found_unreadable = 1 + changedpaths[path] = SVNChangedPath(path, rev, pathtype, + change.base_path, + change.base_rev, action, + is_copy, change.text_changed, + change.prop_changes) + found_readable = 1 + else: + found_unreadable = 1 + return found_readable, found_unreadable, changedpaths.values() + + def _get_change_copyinfo(fsroot, path, change): + # If we know the copyfrom info, return it... + if hasattr(change, 'copyfrom_known') and change.copyfrom_known: + copyfrom_path = change.copyfrom_path + copyfrom_rev = change.copyfrom_rev + # ...otherwise, if this change could be a copy (that is, it + # contains an add action), query the copyfrom info ... + elif (change.change_kind == fs.path_change_add or + change.change_kind == fs.path_change_replace): + copyfrom_rev, copyfrom_path = fs.copied_from(fsroot, path) + # ...else, there's no copyfrom info. + else: + copyfrom_rev = core.SVN_INVALID_REVNUM + copyfrom_path = None + return copyfrom_path, copyfrom_rev + + def _simple_auth_check(fsroot): + """Return a 2-tuple: found_readable, found_unreadable.""" + found_unreadable = found_readable = 0 + if hasattr(fs, 'paths_changed2'): + changes = fs.paths_changed2(fsroot) + else: + changes = fs.paths_changed(fsroot) + paths = changes.keys() + for path in paths: + change = changes[path] + pathtype = None + if hasattr(change, 'node_kind'): + if change.node_kind == core.svn_node_file: + pathtype = vclib.FILE + elif change.node_kind == core.svn_node_dir: + pathtype = vclib.DIR + parts = _path_parts(path) + if pathtype is None: + # Figure out the pathtype so we can query the authz subsystem. + if change.change_kind == fs.path_change_delete: + # Deletions are annoying, because they might be underneath + # copies (make their previous location non-trivial). + prev_parts = parts + prev_rev = rev - 1 + parent_parts = parts[:-1] + while parent_parts: + parent_path = '/' + self._getpath(parent_parts) + parent_change = changes.get(parent_path) + if not (parent_change and \ + (parent_change.change_kind == fs.path_change_add or + parent_change.change_kind == fs.path_change_replace)): + del(parent_parts[-1]) + continue + copyfrom_path, copyfrom_rev = \ + _get_change_copyinfo(fsroot, parent_path, parent_change) + if copyfrom_path: + prev_rev = copyfrom_rev + prev_parts = _path_parts(copyfrom_path) + \ + parts[len(parent_parts):] + break + del(parent_parts[-1]) + pathtype = self._gettype(self._getpath(prev_parts), prev_rev) + else: + pathtype = self._gettype(self._getpath(parts), rev) + if vclib.check_path_access(self, parts, pathtype, rev): + found_readable = 1 + copyfrom_path, copyfrom_rev = \ + _get_change_copyinfo(fsroot, path, change) + if copyfrom_path and copyfrom_path != path: + parts = _path_parts(copyfrom_path) + if not vclib.check_path_access(self, parts, pathtype, + copyfrom_rev): + found_unreadable = 1 + else: + found_unreadable = 1 + if found_readable and found_unreadable: + break + return found_readable, found_unreadable + + def _revinfo_helper(rev, include_changed_paths): + # Get the revision property info. (Would use + # editor.get_root_props(), but something is broken there...) + revprops = fs.revision_proplist(self.fs_ptr, rev) + msg, author, date, revprops = _split_revprops(revprops) + + # Optimization: If our caller doesn't care about the changed + # paths, and we don't need them to do authz determinations, let's + # get outta here. + if self.auth is None and not include_changed_paths: + return date, author, msg, revprops, None + + # If we get here, then we either need the changed paths because we + # were asked for them, or we need them to do authorization checks. + # + # If we only need them for authorization checks, though, we + # won't bother generating fully populated ChangedPath items (the + # cost is too great). + fsroot = self._getroot(rev) + if include_changed_paths: + found_readable, found_unreadable, changedpaths = \ + _get_changed_paths(fsroot) + else: + changedpaths = None + found_readable, found_unreadable = _simple_auth_check(fsroot) + + # Filter our metadata where necessary, and return the requested data. + if found_unreadable: + msg = None + if not found_readable: + author = None + date = None + return date, author, msg, revprops, changedpaths + + # Consult the revinfo cache first. If we don't have cached info, + # or our caller wants changed paths and we don't have those for + # this revision, go do the real work. + rev = self._getrev(rev) + cached_info = self._revinfo_cache.get(rev) + if not cached_info \ + or (include_changed_paths and cached_info[4] is None): + cached_info = _revinfo_helper(rev, include_changed_paths) + self._revinfo_cache[rev] = cached_info + return tuple(cached_info) + + def _log_helper(self, path, rev, lockinfo): + rev_root = fs.revision_root(self.fs_ptr, rev) + copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path) + date, author, msg, revprops, changes = self._revinfo(rev) + if fs.is_file(rev_root, path): + size = fs.file_length(rev_root, path) + else: + size = None + return Revision(rev, date, author, msg, size, lockinfo, path, + copyfrom_path and _cleanup_path(copyfrom_path), + copyfrom_rev) + + def _get_history(self, path, rev, path_type, limit=0, options={}): + if self.youngest == 0: + return [] + + rev_paths = [] + fsroot = self._getroot(rev) + show_all_logs = options.get('svn_show_all_dir_logs', 0) + if not show_all_logs: + # See if the path is a file or directory. + kind = fs.check_path(fsroot, path) + if kind is core.svn_node_file: + show_all_logs = 1 + + # Instantiate a NodeHistory collector object, and use it to collect + # history items for PATH@REV. + history = NodeHistory(self.fs_ptr, show_all_logs, limit) + try: + repos.svn_repos_history(self.fs_ptr, path, history.add_history, + 1, rev, options.get('svn_cross_copies', 0)) + except core.SubversionException, e: + _fix_subversion_exception(e) + if e.apr_err != _SVN_ERR_CEASE_INVOCATION: + raise + + # Now, iterate over those history items, checking for changes of + # location, pruning as necessitated by authz rules. + for hist_rev, hist_path in history: + path_parts = _path_parts(hist_path) + if not vclib.check_path_access(self, path_parts, path_type, hist_rev): + break + rev_paths.append([hist_rev, hist_path]) + return rev_paths def _getpath(self, path_parts): - return string.join(path_parts, '/') + return '/'.join(path_parts) def _getrev(self, rev): if rev is None or rev == 'HEAD': return self.youngest try: + if type(rev) == type(''): + while rev[0] == 'r': + rev = rev[1:] rev = int(rev) - except ValueError: + except: raise vclib.InvalidRevision(rev) if (rev < 0) or (rev > self.youngest): raise vclib.InvalidRevision(rev) @@ -729,7 +857,20 @@ class LocalSubversionRepository(vclib.Repository): r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev) return r - ##--- custom --## + def _gettype(self, path, rev): + # Similar to itemtype(), but without the authz check. Returns + # None for missing paths. + try: + kind = fs.check_path(self._getroot(rev), path) + except: + return None + if kind == core.svn_node_dir: + return vclib.DIR + if kind == core.svn_node_file: + return vclib.FILE + return None + + ##--- custom ---## def get_youngest_revision(self): return self.youngest @@ -807,3 +948,32 @@ class LocalSubversionRepository(vclib.Repository): return peg_revision, path finally: pass + + def get_symlink_target(self, path_parts, rev): + """Return the target of the symbolic link versioned at PATH_PARTS + in REV, or None if that object is not a symlink.""" + + path = self._getpath(path_parts) + rev = self._getrev(rev) + path_type = self.itemtype(path_parts, rev) # does auth-check + fsroot = self._getroot(rev) + + # Symlinks must be files with the svn:special property set on them + # and with file contents which read "link SOME_PATH". + if path_type != vclib.FILE: + return None + props = fs.node_proplist(fsroot, path) + if not props.has_key(core.SVN_PROP_SPECIAL): + return None + pathspec = '' + ### FIXME: We're being a touch sloppy here, only checking the first line + ### of the file. + stream = fs.file_contents(fsroot, path) + try: + pathspec, eof = core.svn_stream_readline(stream, '\n') + finally: + core.svn_stream_close(stream) + if pathspec[:5] != 'link ': + return None + return pathspec[5:] + diff --git a/lib/viewvc.py b/lib/viewvc.py index 50e3603c..763966b0 100644 --- a/lib/viewvc.py +++ b/lib/viewvc.py @@ -1,4 +1,6 @@ -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# -*-python-*- +# +# Copyright (C) 1999-2013 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 @@ -22,13 +24,14 @@ debug.t_start('imports') # standard modules that we know are in the path or builtin import sys import os -import cgi +import calendar +import copy +import fnmatch import gzip import mimetypes import re import rfc822 import stat -import string import struct import tempfile import time @@ -38,8 +41,8 @@ import datetime import locale # 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 import accept -import compat import config import cvsdb import ezt @@ -82,17 +85,9 @@ _sticky_vars = [ 'limit_changes', ] -# number of extra pages of information on either side of the current -# page to fetch (see dir_pagesize/log_pagesize configuration option) -EXTRA_PAGES = 3 - # for reading/writing between a couple descriptors CHUNK_SIZE = 8192 -# for rcsdiff processing of header -_RCSDIFF_IS_BINARY = 'binary-diff' -_RCSDIFF_ERROR = 'error' - # special characters that don't need to be URL encoded _URL_SAFE_CHARS = "/*~" @@ -134,8 +129,8 @@ class Request: if cfg.options.allow_compress: http_accept_encoding = os.environ.get("HTTP_ACCEPT_ENCODING", "") if "gzip" in filter(None, - map(lambda x: string.strip(x), - string.split(http_accept_encoding, ","))): + map(lambda x: x.strip(), + http_accept_encoding.split(','))): self.gzip_compress_level = 9 # make this configurable? def utf8(self, value): @@ -293,11 +288,37 @@ class Request: self.auth = rcr.auth # Overlay root-specific options. cfg.overlay_root_options(self.rootname) - if self.repos.roottype() == vclib.CVS: - # required so that spawned rcs programs correctly expand - # $CVSHeader$ - os.environ['CVSROOT'] = self.rootpath + + # Setup an Authorizer for this rootname and username + debug.t_start('setup-authorizer') + self.auth = setup_authorizer(cfg, self.username) + debug.t_end('setup-authorizer') + # Create the repository object + debug.t_start('select-repos') + try: + if self.repos.roottype() == 'cvs': + self.rootpath = vclib.ccvs.canonicalize_rootpath(rootpath) + self.repos = vclib.ccvs.CVSRepository(self.rootname, + self.rootpath, + self.auth, + cfg.utilities, + cfg.options.use_rcsparse) + # required so that spawned rcs programs correctly expand + # $CVSHeader$ + os.environ['CVSROOT'] = self.rootpath + elif self.repos.roottype() == 'svn': + self.rootpath = vclib.svn.canonicalize_rootpath(rootpath) + self.repos = vclib.svn.SubversionRepository(self.rootname, + self.rootpath, + self.auth, + cfg.utilities, + cfg.options.svn_config_dir) + else: + raise vclib.ReposNotFound() + except vclib.ReposNotFound: + pass + debug.t_end('select-repos') if self.repos is None: raise debug.ViewVCException( 'The root "%s" is unknown. If you believe the value is ' @@ -305,6 +326,9 @@ class Request: % self.rootname, "404 Not Found") if self.repos: + debug.t_start('select-repos') + self.repos.open() + debug.t_end('select-repos') type = self.repos.roottype() if type == vclib.SVN: self.roottype = 'svn' @@ -312,7 +336,8 @@ class Request: self.roottype = 'cvs' else: raise debug.ViewVCException( - 'The root "%s" has an unknown type (%s).' % (self.rootname, type), + 'The root "%s" has an unknown type ("%s"). Expected "cvs" or "svn".' + % (self.rootname, type), "500 Internal Server Error") # If this is using an old-style 'rev' parameter, redirect to new hotness. @@ -388,6 +413,8 @@ class Request: # ViewCVS 0.9.2 used to put ?tarball=1 at the end of tarball urls if self.query_dict.has_key('tarball'): self.view_func = download_tarball + elif self.query_dict.has_key('r1') and self.query_dict.has_key('r2'): + self.view_func = view_diff else: self.view_func = view_directory elif self.pathtype == vclib.FILE: @@ -433,7 +460,9 @@ class Request: if needs_redirect: self.server.redirect(self.get_url()) else: + debug.t_start('view-func') self.view_func(self) + debug.t_end('view-func') def get_url(self, escape=0, partial=0, prefix=0, **args): """Constructs a link to another ViewVC page just like the get_link @@ -442,7 +471,7 @@ class Request: server name portions of the URL.""" url, params = apply(self.get_link, (), args) - qs = compat.urlencode(params) + qs = urllib.urlencode(params) if qs: result = urllib.quote(url, _URL_SAFE_CHARS) + '?' + qs else: @@ -470,7 +499,8 @@ class Request: action = self.server.escape(urllib.quote(url, _URL_SAFE_CHARS)) hidden_values = [] for name, value in params.items(): - hidden_values.append(_item(name=name, value=value)) + hidden_values.append(_item(name=self.server.escape(name), + value=self.server.escape(value))) return action, hidden_values def get_link(self, view_func=None, where=None, pathtype=None, params=None, root=None): @@ -617,7 +647,7 @@ def _path_parts(path): """Split up a repository path into a list of path components""" # clean it up. this removes duplicate '/' characters and any that may # exist at the front or end of the path. - return filter(None, string.split(path, '/')) + return filter(None, path.split('/')) def _normalize_path(path): """Collapse leading slashes in the script name @@ -671,12 +701,14 @@ def _validate_param(name, value): '400 Bad Request') 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. - return True + try: + re.compile(value) + return True + except: + return None def _validate_view(value): # Return true iff VALUE is one of our allowed views. @@ -741,16 +773,16 @@ _legal_params = { 'gmaxtag' : _re_validate_number, # for query - 'repos' : _validate_regex, + 'repos' : None, 'repos_match' : _re_validate_alpha, 'repos_type' : None, - 'branch' : _validate_regex, + 'branch' : None, 'branch_match' : _re_validate_alpha, 'dir' : None, - 'file' : _validate_regex, + 'file' : None, 'file_match' : _re_validate_alpha, 'query_revision': None, - 'who' : _validate_regex, + 'who' : None, 'who_match' : _re_validate_alpha, 'comment' : None, 'comment_match' : _re_validate_alpha, @@ -761,7 +793,6 @@ _legal_params = { 'mindate' : _re_validate_datetime, 'maxdate' : _re_validate_datetime, 'format' : _re_validate_alpha, - 'limit' : _re_validate_number, # for redirect_pathrev 'orig_path' : None, @@ -780,7 +811,7 @@ _legal_params = { } def _path_join(path_parts): - return string.join(path_parts, '/') + return '/'.join(path_parts) def _strip_suffix(suffix, path_parts, rev, pathtype, repos, view_func): """strip the suffix from a repository path if the resulting path @@ -851,32 +882,37 @@ def _orig_path(request, rev_param='revision', path_param=None): return _path_parts(request.repos.get_location(path, pathrev, rev)), rev return _path_parts(path), rev -def setup_authorizer(cfg, username, rootname): - import imp +def setup_authorizer(cfg, username, rootname=None): + """Setup the authorizer. If ROOTNAME is provided, assume that + per-root options have not been overlayed. Otherwise, assume they + have (and fetch the authorizer for the configured root).""" + if rootname is None: + authorizer = cfg.options.authorizer + params = cfg.get_authorizer_params() + else: + authorizer, params = cfg.get_authorizer_and_params_hack(rootname) + # No configured authorizer? No problem. - if not cfg.options.authorizer: + if not authorizer: return None # First, try to load a module with the configured name. + import imp fp = None try: try: - fp, path, desc = imp.find_module("%s" % (cfg.options.authorizer), - vcauth.__path__) + fp, path, desc = imp.find_module("%s" % (authorizer), vcauth.__path__) my_auth = imp.load_module('viewvc', fp, path, desc) except ImportError: raise debug.ViewVCException( 'Invalid authorizer (%s) specified for root "%s"' \ - % (cfg.options.authorizer, rootname), + % (authorizer, rootname), '500 Internal Server Error') finally: if fp: fp.close() - # Now we'll get custom parameters for our particular root. - params = cfg.get_authorizer_params(cfg.options.authorizer, rootname) - # Finally, instantiate our Authorizer. return my_auth.ViewVCAuthorizer(username, params) @@ -913,7 +949,7 @@ def check_freshness(request, mtime=None, etag=None, weak=0): # require revalidation after the configured amount of time if cfg and cfg.options.http_expiration_time >= 0: - expiration = compat.formatdate(time.time() + + expiration = rfc822.formatdate(time.time() + cfg.options.http_expiration_time) request.server.addheader('Expires', expiration) request.server.addheader('Cache-Control', @@ -925,7 +961,7 @@ def check_freshness(request, mtime=None, etag=None, weak=0): if etag is not None: request.server.addheader('ETag', etag) if mtime is not None: - request.server.addheader('Last-Modified', compat.formatdate(mtime)) + request.server.addheader('Last-Modified', rfc822.formatdate(mtime)) return isfresh def get_view_template(cfg, view_name, language="en"): @@ -938,7 +974,7 @@ def get_view_template(cfg, view_name, language="en"): tname = os.path.join(cfg.options.template_dir or "templates", tname) # Allow per-language template selection. - tname = string.replace(tname, '%lang%', language) + tname = tname.replace('%lang%', language) # Finally, construct the whole template path. tname = cfg.path(tname) @@ -949,26 +985,45 @@ def get_view_template(cfg, view_name, language="en"): return template -def get_writeready_server_file(request, content_type=None): +def get_writeready_server_file(request, content_type=None, encoding=None, + content_length=None, allow_compress=True): """Return a file handle to a response body stream, after outputting any queued special headers (on REQUEST.server) and (optionally) a - 'Content-Type' header whose value is CONTENT_TYPE. After this is - called, it is too late to add new headers to the response.""" - if request.gzip_compress_level: + 'Content-Type' header whose value is CONTENT_TYPE and character set + is ENCODING. + + If CONTENT_LENGTH is provided and compression is not in use, also + generate a 'Content-Length' header for this response. + + Callers my use ALLOW_COMPRESS to disable compression where it would + otherwise be allowed. (Such as when transmitting an + already-compressed response.) + + After this function is called, it is too late to add new headers to + the response.""" + + if allow_compress and request.gzip_compress_level: request.server.addheader('Content-Encoding', 'gzip') - if content_type: + elif content_length is not None: + request.server.addheader('Content-Length', content_length) + + if content_type and encoding: + request.server.header("%s; charset=%s" % (content_type, encoding)) + elif content_type: request.server.header(content_type) else: request.server.header() - if request.gzip_compress_level: + + if allow_compress and request.gzip_compress_level: fp = gzip.GzipFile('', 'wb', request.gzip_compress_level, request.server.file()) else: fp = request.server.file() + return fp def generate_page(request, view_name, data, content_type=None): - server_fp = get_writeready_server_file(request) + server_fp = get_writeready_server_file(request, content_type) template = get_view_template(request.cfg, view_name, request.language) template.generate(server_fp, data) @@ -1020,7 +1075,7 @@ def nav_path(request): def prep_tags(request, tags): url, params = request.get_link(params={'pathrev': None}) - params = compat.urlencode(params) + params = urllib.urlencode(params) if params: url = urllib.quote(url, _URL_SAFE_CHARS) + '?' + params + '&pathrev=' else: @@ -1065,6 +1120,15 @@ def default_view(mime_type, cfg): return view_markup return view_checkout +def is_binary_file_mime_type(mime_type, cfg): + """Return True iff MIME_TYPE is set and matches one of the binary + file mime type patterns in CFG.""" + if mime_type: + for pattern in cfg.options.binary_mime_types: + if fnmatch.fnmatch(mime_type, pattern): + return True + return False + def get_file_view_info(request, where, rev=None, mime_type=None, pathrev=-1): """Return an object holding common hrefs and a viewability flag used for various views of FILENAME at revision REV whose MIME type is @@ -1125,7 +1189,12 @@ def get_file_view_info(request, where, rev=None, mime_type=None, pathrev=-1): params={'revision': rev}, escape=1) - prefer_markup = default_view(mime_type, request.cfg) == view_markup + is_binary_file = is_binary_file_mime_type(mime_type, request.cfg) + if is_binary_file: + download_text_href = annotate_href = view_href = None + prefer_markup = False + else: + prefer_markup = default_view(mime_type, request.cfg) == view_markup return _item(view_href=view_href, download_href=download_href, @@ -1135,65 +1204,314 @@ def get_file_view_info(request, where, rev=None, mime_type=None, pathrev=-1): prefer_markup=ezt.boolean(prefer_markup)) -# Regular expressions for location text that looks like URLs and email -# addresses. Note that the regexps assume the text is already HTML-encoded. +# Matches URLs _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%.~:_])+)*' '(#([-a-zA-Z0-9%.~:_]+)?)?)') +# 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' ] ] -def mangle_email_addresses(text, style=0): - # style=2: truncation mangling - if style == 2: - return re.sub(_re_rewrite_email, r'\1@…', text) +# Matches revision references +_re_rewrite_svnrevref = re.compile(r'\b(r|rev #?|revision #?)([0-9]+)\b') - # style=1: entity-encoding and at-wrapping - if style == 1: - def _match_replace(matchobj): - return string.join(map(lambda x: '&#%d;' % (ord(x)), - matchobj.group(1)), '') \ - + ' {at} ' + \ - string.join(map(lambda x: '&#%d;' % (ord(x)), - matchobj.group(2)), '') - return re.sub(_re_rewrite_email, _match_replace, text) +class ViewVCHtmlFormatterTokens: + def __init__(self, tokens): + self.tokens = tokens - # otherwise, no mangling - return text + def get_result(self, maxlen=0): + """Format the tokens per the registered set of formatters, and + limited to MAXLEN visible characters (or unlimited if MAXLEN is + 0). Return a 3-tuple containing the formatted result string, the + number of visible characters in the result string, and a boolean + flag indicating whether or not S was truncated.""" + out = '' + out_len = 0 + for token in self.tokens: + chunk, chunk_len = token.converter(token.match, token.userdata, maxlen) + out = out + chunk + out_len = out_len + chunk_len + if maxlen: + maxlen = maxlen - chunk_len + if maxlen <= 0: + return out, out_len, 1 + return out, out_len, 0 -def htmlify(html, mangle_email_addrs=0): - if not html: - return html - html = cgi.escape(html) - global _re_rewrites_html - for i in _re_rewrites_html: - html = re.sub(i[0], i[1], html) - html = mangle_email_addresses(html, mangle_email_addrs) - return html + +class ViewVCHtmlFormatter: + """Format a string as HTML-encoded output with customizable markup + rules, for example turning strings that look like URLs into anchor links. -def add_rewrite_html(regex, replace): - global _re_rewrites_html - if type(regex) == 'str' or type(regex) == 'unicode': - regex = re.compile(regex) - _re_rewrites_html.append([ regex, replace ]) + NOTE: While there might appear to be some unused portions of this + interface, there is a good chance that there are consumers outside + of ViewVC itself that make use of these things. + """ + + def __init__(self): + self._formatters = [] -def format_log(log, cfg, htmlize=1): - if not log: - return log - try: log = unicode(log.decode('utf-8')) - except: pass - if htmlize: - s = htmlify(log[:cfg.options.short_log_len], - cfg.options.mangle_email_addresses) - else: - s = cgi.escape(log[:cfg.options.short_log_len]) - if cfg.options.mangle_email_addresses == 2: - s = re.sub(_re_rewrite_email, r'\1@...', s) - if len(log) > cfg.options.short_log_len: - s = s + '...' - return s + def format_url(self, mobj, userdata, maxlen=0): + """Return a 2-tuple containing: + - the text represented by MatchObject MOBJ, formatted as + linkified URL, with no more than MAXLEN characters in the + non-HTML-tag bits. If MAXLEN is 0, there is no maximum. + - the number of non-HTML-tag characters returned. + """ + s = mobj.group(0) + trunc_s = maxlen and s[:maxlen] or s + return '%s' % (sapi.escape(s), + sapi.escape(trunc_s)), \ + len(trunc_s) + + def format_email(self, mobj, userdata, maxlen=0): + """Return a 2-tuple containing: + - the text represented by MatchObject MOBJ, formatted as + linkified email address, with no more than MAXLEN characters + in the non-HTML-tag bits. If MAXLEN is 0, there is no maximum. + - the number of non-HTML-tag characters returned. + """ + s = mobj.group(0) + trunc_s = maxlen and s[:maxlen] or s + return '%s' % (urllib.quote(s), + self._entity_encode(trunc_s)), \ + len(trunc_s) + + def format_email_obfuscated(self, mobj, userdata, maxlen=0): + """Return a 2-tuple containing: + - the text represented by MatchObject MOBJ, formatted as an + entity-encoded email address, with no more than MAXLEN characters + in the non-HTML-tag bits. If MAXLEN is 0, there is no maximum. + - the number of non-HTML-tag characters returned. + """ + s = mobj.group(0) + trunc_s = maxlen and s[:maxlen] or s + return self._entity_encode(trunc_s), len(trunc_s) + + def format_email_truncated(self, mobj, userdata, maxlen=0): + """Return a 2-tuple containing: + - the text represented by MatchObject MOBJ, formatted as an + HTML-escaped truncated email address of no more than MAXLEN + characters. If MAXLEN is 0, there is no maximum. + - the number of characters returned. + """ + s = mobj.group(1) + s_len = len(s) + if (maxlen == 0) or (s_len < (maxlen - 1)): + return self._entity_encode(s) + '@…', s_len + 2 + elif s_len < maxlen: + return self._entity_encode(s) + '@', s_len + 1 + else: + 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 + linkified URL to a ViewVC Subversion revision view, with no + more than MAXLEN characters in the non-HTML-tag portions. + If MAXLEN is 0, there is no maximum. + - the number of characters returned. + + USERDATA is a function that accepts a revision reference + and returns a URL to that revision. + """ + s = mobj.group(0) + revref = mobj.group(2) + trunc_s = maxlen and s[:maxlen] or s + revref_url = userdata(revref) + return '%s' % (sapi.escape(revref_url), + sapi.escape(trunc_s)), \ + len(trunc_s) + + def format_custom_url(self, mobj, userdata, maxlen=0): + """Return a 2-tuple containing: + - the text represented by MatchObject MOBJ, formatted as an + linkified URL created by substituting match groups 0-9 into + USERDATA (which is a format string that uses \N to + represent the substitution locations) and with no more than + MAXLEN characters in the non-HTML-tag portions. If MAXLEN + is 0, there is no maximum. + - the number of characters returned. + """ + format = userdata + text = mobj.group(0) + url = format + for i in range(9): + try: + repl = mobj.group(i) + except: + repl = '' + url = url.replace('\%d' % (i), repl) + trunc_s = maxlen and text[:maxlen] or text + return '%s' % (sapi.escape(url), + sapi.escape(trunc_s)), \ + len(trunc_s) + + def format_text(self, s, unused, maxlen=0): + """Return a 2-tuple containing: + - the text S, HTML-escaped, containing no more than MAXLEN + characters. If MAXLEN is 0, there is no maximum. + - the number of characters returned. + """ + trunc_s = maxlen and s[:maxlen] or s + return sapi.escape(trunc_s), len(trunc_s) + + def add_formatter(self, regexp, conv, userdata=None): + """Register a formatter which finds instances of strings matching + REGEXP, and using the function CONV and USERDATA to format them. + + CONV is a function which accepts three parameters: + - the MatchObject which holds the string portion to be formatted, + - the USERDATA object, + - the maximum number of characters from that string to use for + human-readable output (or 0 to indicate no maximum). + """ + if type(regexp) == type(''): + regexp = re.compile(regexp) + self._formatters.append([regexp, conv, userdata]) + + def get_result(self, s, maxlen=0): + """Format S per the set of formatters registered with this object, + and limited to MAXLEN visible characters (or unlimited if MAXLEN + is 0). Return a 3-tuple containing the formatted result string, + the number of visible characters in the result string, and a + boolean flag indicating whether or not S was truncated. + """ + return self.tokenize_text(s).get_result(maxlen) + + def tokenize_text(self, s): + """Return a ViewVCHtmlFormatterTokens object containing the tokens + created when parsing the string S. Callers can use that object's + get_result() function to retrieve HTML-formatted text. + """ + tokens = [] + # We could just have a "while s:" here instead of "for line: while + # line:", but for really large log messages with heavy + # tokenization, the cost in both performance and memory + # consumption of the approach taken was atrocious. + for line in s.replace('\r\n', '\n').split('\n'): + line = line + '\n' + while line: + best_match = best_conv = best_userdata = None + for test in self._formatters: + match = test[0].search(line) + # If we find and match and (a) its our first one, or (b) it + # matches text earlier than our previous best match, or (c) it + # matches text at the same location as our previous best match + # but extends to cover more text than that match, then this is + # our new best match. + # + # Implied here is that when multiple formatters match exactly + # the same text, the first formatter in the registration list wins. + if match \ + and ((best_match is None) \ + or (match.start() < best_match.start()) + or ((match.start() == best_match.start()) \ + and (match.end() > best_match.end()))): + best_match = match + best_conv = test[1] + best_userdata = test[2] + # If we found a match... + if best_match: + # ... add any non-matching stuff first, then the matching bit. + start = best_match.start() + end = best_match.end() + if start > 0: + tokens.append(_item(match=line[:start], + converter=self.format_text, + userdata=None)) + tokens.append(_item(match=best_match, + converter=best_conv, + userdata=best_userdata)) + line = line[end:] + else: + # Otherwise, just add the rest of the string. + tokens.append(_item(match=line, + converter=self.format_text, + userdata=None)) + line = '' + return ViewVCHtmlFormatterTokens(tokens) + + def _entity_encode(self, s): + return ''.join(map(lambda x: '&#%d;' % (ord(x)), s)) + + +class LogFormatter: + def __init__(self, request, log): + self.request = request + self.log = log or '' + self.tokens = None + self.cache = {} # (maxlen, htmlize) => resulting_log + + 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)] + + # If we are HTML-izing... + if htmlize: + # ...and we don't yet have ViewVCHtmlFormatter() object tokens... + if not self.tokens: + # ... 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) + + # Rewrite Subversion revision references. + if self.request.roottype == 'svn': + def revision_to_url(rev): + return self.request.get_url(view_func=view_revision, + params={'revision': rev}, + escape=1) + lf.add_formatter(_re_rewrite_svnrevref, lf.format_svnrevref, + revision_to_url) + + # Rewrite email addresses. + if cfg.options.mangle_email_addresses == 2: + lf.add_formatter(_re_rewrite_email, lf.format_email_truncated) + elif cfg.options.mangle_email_addresses == 1: + lf.add_formatter(_re_rewrite_email, lf.format_email_obfuscated) + else: + lf.add_formatter(_re_rewrite_email, lf.format_email) + + # Add custom rewrite handling per configuration. + for rule in cfg.options.custom_log_formatting: + rule = rule.replace('\\:', '\x01') + regexp, format = map(lambda x: x.strip(), rule.split(':', 1)) + regexp = regexp.replace('\x01', ':') + format = format.replace('\x01', ':') + lf.add_formatter(re.compile(regexp), lf.format_custom_url, format) + + # Tokenize the log message. + self.tokens = lf.tokenize_text(self.log) + + # Use our formatter to ... you know ... format. + log, log_len, truncated = self.tokens.get_result(maxlen) + result_log = log + (truncated and '…' or '') + + # But if we're not HTML-izing... + else: + # ...then do much more simplistic transformations as necessary. + log = self.log + if cfg.options.mangle_email_addresses == 2: + log = re.sub(_re_rewrite_email, r'\1@...', log) + result_log = maxlen and log[:maxlen] or log + + # In either case, populate the cache and return the results. + self.cache[(maxlen, htmlize)] = result_log + return result_log _time_desc = { 1 : 'second', @@ -1254,7 +1572,7 @@ def html_time(request, secs, extended=0): return s def common_template_data(request, revision=None, mime_type=None): - """Return a ezt.TemplateData instance with data dictionary items + """Return a TemplateData instance with data dictionary items common to most ViewVC views.""" cfg = request.cfg @@ -1264,7 +1582,7 @@ def common_template_data(request, revision=None, mime_type=None): where = request.server.escape(where) # Initialize data dictionary members (sorted alphanumerically) - data = ezt.TemplateData({ + data = TemplateData({ 'annotate_href' : None, 'cfg' : cfg, 'vsn' : __version__, @@ -1287,7 +1605,7 @@ def common_template_data(request, revision=None, mime_type=None): 'rootname' : request.rootname \ and request.server.escape(request.rootname) or None, 'rootpath' : request.rootpath, - 'roots_href' : request.get_url(view_func=view_roots, escape=1, params={}), + 'roots_href' : None, 'roottype' : request.roottype, 'rss_href' : None, 'tarball_href' : None, @@ -1323,6 +1641,10 @@ def common_template_data(request, revision=None, mime_type=None): where=dir, pathtype=vclib.DIR, params={}, escape=1) + if 'roots' in cfg.options.allowed_views: + data['roots_href'] = request.get_url(view_func=view_roots, + escape=1, params={}) + if request.pathtype == vclib.FILE: fvi = get_file_view_info(request, request.where, data['rev'], mime_type) data['view_href'] = fvi.view_href @@ -1397,7 +1719,7 @@ def retry_read(src, reqlen=CHUNK_SIZE): continue return chunk -def copy_stream(src, dst, cfg, htmlize=0): +def copy_stream(src, dst, htmlize=0): nch = 0 while 1: chunk = retry_read(src) @@ -1405,7 +1727,7 @@ def copy_stream(src, dst, cfg, htmlize=0): break nch = nch+1 if htmlize: - chunk = htmlify(chunk, mangle_email_addrs=0) + chunk = sapi.escape(chunk) dst.write(chunk) return nch @@ -1413,9 +1735,8 @@ class MarkupPipeWrapper: """An EZT callback that outputs a filepointer, plus some optional pre- and post- text.""" - def __init__(self, cfg, fp, pretext=None, posttext=None, htmlize=1): + def __init__(self, fp, pretext=None, posttext=None, htmlize=0): self.fp = fp - self.cfg = cfg self.pretext = pretext self.posttext = posttext self.htmlize = htmlize @@ -1423,80 +1744,152 @@ class MarkupPipeWrapper: def __call__(self, ctx): if self.pretext: ctx.fp.write(self.pretext) - copy_stream(self.fp, ctx.fp, self.cfg, self.htmlize) + copy_stream(self.fp, ctx.fp, self.htmlize) self.fp.close() if self.posttext: ctx.fp.write(self.posttext) -def markup_stream_pygments(request, cfg, blame_data, fp, filename, mime_type): - # Determine if we should use Pygments to highlight our output. - # Reasons not to include a) being told not to by the configuration, - # b) not being able to import the Pygments modules, and c) Pygments - # not having a lexer for our file's format. +_re_rewrite_escaped_url = re.compile('((http|https|ftp|file|svn|svn\+ssh)' + '(://[-a-zA-Z0-9%.~:_/]+)' + '((\?|\&amp;|\&|\&)' + '([-a-zA-Z0-9%.~:_]+)=([-a-zA-Z0-9%.~:_])+)*' + '(#([-a-zA-Z0-9%.~:_]+)?)?)') - blame_source = [] - if blame_data: - for i in blame_data: - i.text = cgi.escape(i.text) - i.diff_href = None - if i.prev_rev: - i.diff_href = request.get_url(view_func=view_diff, - params={'r1': i.prev_rev, - 'r2': i.rev}, - escape=1, partial=1) - blame_source.append(i) - blame_data = blame_source - lexer = None - use_pygments = cfg.options.enable_syntax_coloration +def markup_escaped_urls(s): + # Return a copy of S with all URL references -- which are expected + # to be already HTML-escaped -- wrapped in . + def _url_repl(match_obj): + url = match_obj.group(0) + unescaped_url = url.replace("&amp;", "&") + return "%s" % (unescaped_url, url) + return re.sub(_re_rewrite_escaped_url, _url_repl, s) + + +def detect_encoding(text_block): + # Does the TEXT_BLOCK start with a BOM? + for bom, encoding in [('\xef\xbb\xbf', 'utf-8'), + ('\xff\xfe', 'utf-16'), + ('\xfe\xff', 'utf-16be'), + ('\xff\xfe\0\0', 'utf-32'), + ('\0\0\xfe\xff', 'utf-32be'), + ]: + if text_block.startswith(bom): + return encoding + + # If no recognized BOM, see if chardet can help us. try: + import chardet + return chardet.detect(text_block).get('encoding') + except: + pass + + # By default ... we have no idea. + return None + +def transcode_text(text, encoding=None): + """If ENCODING is provided and not 'utf-8', transcode TEXT from + ENCODING to UTF-8.""" + + if not encoding or encoding == 'utf-8': + return text + try: + return unicode(text, encoding, 'replace').encode('utf-8', 'replace') + except: + pass + return text + +def markup_stream(request, cfg, blame_data, file_lines, filename, + mime_type, encoding, colorize): + """Return the contents of a versioned file as a list of + vclib.Annotation objects, each representing one line of the file's + contents. Use BLAME_DATA as the annotation information for the file + if provided. Use FILE_LINES as the lines of file content text + themselves. MIME_TYPE is the MIME content type of the file; + ENCODING is its character encoding. If COLORIZE is true, attempt to + apply syntax coloration to the file contents, and use the + HTML-marked-up results as the text in the return vclib.Annotation + objects.""" + + # Nothing to mark up? So be it. + if not file_lines: + return [] + + # Determine if we should (and can) use Pygments to highlight our + # output. Reasons not to include a) being told not to by the + # configuration, b) not being able to import the Pygments modules, + # and c) Pygments not having a lexer for our file's format. + pygments_lexer = None + if colorize: from pygments import highlight from pygments.formatters import HtmlFormatter from pygments.lexers import ClassNotFound, \ get_lexer_by_name, \ get_lexer_for_mimetype, \ - get_lexer_for_filename + get_lexer_for_filename, \ + guess_lexer from pygments.lexers._mapping import LEXERS # Hack for shell mime types: LEXERS['BashLexer'] = ('pygments.lexers.other', 'Bash', ('bash', 'sh'), ('*.sh',), ('application/x-sh', 'application/x-shellscript', 'text/x-sh', 'text/x-shellscript')) - try: - lexer = get_lexer_for_mimetype(mime_type, - encoding='utf-8', - stripnl=False) - except ClassNotFound: + + # First, see if there's a Pygments lexer associated with MIME_TYPE. + if mime_type: try: - lexer = get_lexer_for_filename(filename, - encoding='utf-8', - stripnl=False) + pygments_lexer = get_lexer_for_mimetype(mime_type, + encoding=encoding, + tabsize=cfg.options.tabsize, + stripnl=False) except ClassNotFound: - use_pygments = 0 - except ImportError: - use_pygments = 0 + pygments_lexer = None + + # If we've no lexer thus far, try to find one based on the FILENAME. + if not pygments_lexer: + try: + pygments_lexer = get_lexer_for_filename(filename, + encoding=encoding, + tabsize=cfg.options.tabsize, + stripnl=False) + except ClassNotFound: + pygments_lexer = None + + # Still no lexer? If we've reason to believe this is a text + # file, try to guess the lexer based on the file's content. + if not pygments_lexer and is_text(mime_type) and file_lines: + try: + pygments_lexer = guess_lexer(file_lines[0]) + except ClassNotFound: + pygments_lexer = None # Detect encoding by calling chardet ourselves, # to support it in non-highlighting mode - content = fp.read() - c, encoding = cfg.guesser().guess_charset(content) - if encoding: - content = c - else: - encoding = 'unknown' - - # If we aren't going to be highlighting anything, just return the - # BLAME_SOURCE. If there's no blame_source, we'll generate a fake - # one from the file contents we fetch with PATH and REV. - if not use_pygments: - if blame_source: - return blame_source, encoding + if not encoding and cfg.options.detect_encoding: + content = ''.join(file_lines) + c, encoding = cfg.guesser().guess_charset(content) + if encoding: + file_lines = c.rstrip('\n').split('\n') else: - lines = [] - line_no = 0 - for line in content.split('\n'): - line_no = line_no + 1 - item = vclib.Annotation(cgi.escape(line), line_no, - None, None, None, None) - item.diff_href = None - lines.append(item) - return lines, encoding + encoding = 'unknown' + + # If we aren't highlighting, just return an amalgamation of the + # BLAME_DATA (if any) and the FILE_LINES. + if not pygments_lexer: + + # Built output data comprised of marked-up and possibly-transcoded + # source text lines wrapped in (possibly dummy) vclib.Annotation + # objects. + lines = [] + for i in range(len(file_lines)): + line = file_lines[i] + if cfg.options.tabsize > 0: + line = line.expandtabs(cfg.options.tabsize) + line = markup_escaped_urls(sapi.escape(line)) + if blame_data: + blame_item = blame_data[i] + blame_item.text = line + else: + blame_item = vclib.Annotation(line, i + 1, None, None, None, None) + blame_item.diff_href = None + lines.append(blame_item) + return lines # If we get here, we're highlighting something. class PygmentsSink: @@ -1510,6 +1903,7 @@ def markup_stream_pygments(request, cfg, blame_data, fp, filename, mime_type): self.line_no = 0 def write(self, buf): ### FIXME: Don't bank on write() being called once per line + buf = markup_escaped_urls(buf.rstrip('\n\r')) if self.has_blame_data: self.blame_data[self.line_no].text = buf else: @@ -1518,8 +1912,9 @@ def markup_stream_pygments(request, cfg, blame_data, fp, filename, mime_type): item.diff_href = None self.blame_data.append(item) self.line_no = self.line_no + 1 - ps = PygmentsSink(blame_source) - highlight(content, lexer, + + ps = PygmentsSink(blame_data) + highlight(''.join(file_lines), pygments_lexer, HtmlFormatter(nowrap=True, classprefix='pygments-', encoding='utf-8'), ps) @@ -1534,10 +1929,23 @@ def make_time_string(date, cfg): if date is None: return None if cfg.options.use_localtime: - localtime = time.localtime(date) - return time.asctime(localtime) + ' ' + time.tzname[localtime[8]] + tm = time.localtime(date) else: - return time.asctime(time.gmtime(date)) + ' UTC' + tm = time.gmtime(date) + if cfg.options.iso8601_timestamps: + if cfg.options.use_localtime: + if tm[8] and time.daylight: + tz = time.altzone + else: + tz = time.timezone + tz = float(tz) / 3600.0 + tz = '{0:+06.2f}'.format(tz).replace('.', ':') + else: + tz = 'Z' + return time.strftime('%Y-%m-%dT%H:%M:%S', tm) + tz + else: + return time.asctime(tm) + ' ' + \ + (cfg.options.use_localtime and time.tzname[tm[8]] or 'UTC') def make_rss_time_string(date, cfg): """Returns formatted date string in UTC, formatted for RSS. @@ -1559,54 +1967,78 @@ def make_comma_sep_list_string(items): return string.join(map(lambda x: x.name, items), ', ') def make_comma_sep_list_string(items): - return string.join(map(lambda x: x.name, items), ', ') + return ', '.join(map(lambda x: x.name, items)) + +def is_undisplayable(val): + try: + unicode(val) + return 0 + except: + return 1 def get_itemprops(request, path_parts, rev): itemprops = request.repos.itemprops(path_parts, rev) propnames = itemprops.keys() propnames.sort() props = [] - has_binary_props = 0 for name in propnames: - value = itemprops[name] - undisplayable = ezt.boolean(0) # skip non-utf8 property names - try: - unicode(name, 'utf8') - except: + if is_undisplayable(name): continue - # note non-utf8 property values - try: - unicode(value, 'utf8') - except: + lf = LogFormatter(request, itemprops[name]) + value = lf.get(maxlen=0, htmlize=1) + undisplayable = is_undisplayable(value) + if undisplayable: value = None - undisplayable = ezt.boolean(1) - props.append(_item(name=name, value=value, undisplayable=undisplayable)) + props.append(_item(name=name, value=value, + undisplayable=ezt.boolean(undisplayable))) return props +def parse_mime_type(mime_type): + mime_parts = map(lambda x: x.strip(), mime_type.split(';')) + type_subtype = mime_parts[0].lower() + parameters = {} + for part in mime_parts[1:]: + name, value = part.split('=', 1) + parameters[name] = value + return type_subtype, parameters + def calculate_mime_type(request, path_parts, rev): - mime_type = None + """Return a 2-tuple carrying the MIME content type and character + encoding for the file represented by PATH_PARTS in REV. Use REQUEST + for repository access as necessary.""" if not path_parts: - return None + return None, None + mime_type = encoding = None if request.roottype == 'svn' \ and (not request.cfg.options.svn_ignore_mimetype): try: itemprops = request.repos.itemprops(path_parts, rev) mime_type = itemprops.get('svn:mime-type') if mime_type: - return mime_type + mime_type, parameters = parse_mime_type(mime_type) + return mime_type, parameters.get('charset') except: pass # FIXME rewrite to use viewvcmagic - return guess_mime(path_parts[-1]) + return guess_mime(path_parts[-1]), None +def assert_viewable_filesize(cfg, filesize): + if cfg.options.max_filesize_kbytes \ + and filesize != -1 \ + and filesize > (1024 * cfg.options.max_filesize_kbytes): + raise debug.ViewVCException('Display of files larger than %d KB ' + 'disallowed by configuration' + % (cfg.options.max_filesize_kbytes), + '403 Forbidden') + def markup_or_annotate(request, is_annotate): cfg = request.cfg path, rev = _orig_path(request, is_annotate and 'annotate' or 'revision') lines = fp = image_src_href = None annotation = 'none' revision = None - mime_type = calculate_mime_type(request, path, rev) + mime_type, encoding = calculate_mime_type(request, path, rev) if not mime_type or mime_type == default_mime_type: try: @@ -1616,49 +2048,106 @@ def markup_or_annotate(request, is_annotate): except: raise - # Is this a binary type? - if is_binary(request.cfg, mime_type): - fp, revision = request.repos.openfile(path, rev) + # Is this display blocked by 'binary_mime_types' configuration? + if is_binary_file_mime_type(mime_type, cfg): + raise debug.ViewVCException('Display of binary file content disabled ' + 'by configuration', '403 Forbidden') + + # Is this a viewable image type? + if is_viewable_image(mime_type) \ + and 'co' in cfg.options.allowed_views: + fp, revision = request.repos.openfile(path, rev, {}) fp.close() if check_freshness(request, None, revision, weak=1): return - annotation = 'binary' - if 'co' in cfg.options.allowed_views: - # Is this a viewable image type? - if is_viewable_image(mime_type) \ - and 'co' in cfg.options.allowed_views: - image_src_href = request.get_url(view_func=view_checkout, - params={'revision': rev}, escape=1) - else: - download_href = request.get_url(view_func=view_checkout, - params={'revision': rev}, escape=1) + if is_annotate: + annotation = 'binary' + image_src_href = request.get_url(view_func=view_checkout, + params={'revision': rev}, escape=1) # Text type else: - blame_source = None + filesize = request.repos.filesize(path, rev) + + # If configuration disallows display of large files, try to honor + # that request. + assert_viewable_filesize(cfg, filesize) + + # If this was an annotation request, try to annotate this file. + # If something goes wrong, that's okay -- we'll gracefully revert + # to a plain markup display. + blame_data = None if is_annotate: - # Try to annotate this file, but don't croak if we fail. try: - blame_source, revision = request.repos.annotate(path, rev) - annotation = 'annotated' + blame_source, revision = request.repos.annotate(path, rev, False) if check_freshness(request, None, revision, weak=1): return + # Create BLAME_DATA list from BLAME_SOURCE, adding diff_href + # items to each relevant "line". + blame_data = [] + for item in blame_source: + item.diff_href = None + if item.prev_rev: + item.diff_href = request.get_url(view_func=view_diff, + params={'r1': item.prev_rev, + 'r2': item.rev}, + escape=1, partial=1) + blame_data.append(item) + annotation = 'annotated' except vclib.NonTextualFileContents: annotation = 'binary' except: annotation = 'error' - fp, revision = request.repos.openfile(path, rev) + # Grab the file contents. + fp, revision = request.repos.openfile(path, rev, {'cvs_oldkeywords' : 1}) if check_freshness(request, None, revision, weak=1): fp.close() return - lines, charset = markup_stream_pygments(request, cfg, blame_source, fp, path[-1], mime_type) - fp.close() - if mime_type.find(';') < 0: - mime_type = mime_type+'; charset='+charset - data = common_template_data(request, revision) - data.merge(ezt.TemplateData({ + # If we're limiting by filesize but couldn't pull off the cheap + # check above, we'll try to do so line by line here (while + # building our file_lines array). + if cfg.options.max_filesize_kbytes and filesize == -1: + file_lines = [] + filesize = 0 + while 1: + line = fp.readline() + if not line: + break + filesize = filesize + len(line) + assert_viewable_filesize(cfg, filesize) + file_lines.append(line) + else: + file_lines = fp.readlines() + fp.close() + + # Do we have a differing number of file content lines and + # annotation items? That's no good. Call it an error and don't + # bother attempting the annotation display. + if blame_data and (len(file_lines) != len(blame_data)): + annotation = 'error' + blame_data = None + + # Try to markup the file contents/annotation. If we get an error + # and we were colorizing the stream, try once more without the + # colorization enabled. + colorize = cfg.options.enable_syntax_coloration + try: + lines, charset = markup_stream(request, cfg, blame_data, file_lines, + path[-1], mime_type, encoding, colorize) + if mime_type.find(';') < 0: + mime_type = mime_type+'; charset='+charset + except: + if colorize: + lines = markup_stream(request, cfg, blame_data, file_lines, + path[-1], mime_type, encoding, False) + else: + raise debug.ViewVCException('Error displaying file contents', + '500 Internal Server Error') + + data = common_template_data(request, revision, mime_type) + data.merge(TemplateData({ 'mime_type' : mime_type, 'log' : None, 'date' : None, @@ -1681,14 +2170,19 @@ def markup_or_annotate(request, is_annotate): })) if cfg.options.show_log_in_markup: - options = {'svn_latest_log': 1} ### FIXME: No longer needed? + options = { + 'svn_latest_log': 1, ### FIXME: Use of this magical value is uncool. + 'svn_cross_copies': 1, + } revs = request.repos.itemlog(path, revision, vclib.SORTBY_REV, 0, 1, options) entry = revs[-1] + lf = LogFormatter(request, entry.log) + data['date'] = make_time_string(entry.date, cfg) data['author'] = entry.author data['changed'] = entry.changed - data['log'] = htmlify(entry.log, cfg.options.mangle_email_addresses) + data['log'] = lf.get(maxlen=0, htmlize=1) data['size'] = entry.size if entry.date is not None: @@ -1721,17 +2215,23 @@ def view_markup(request): if 'markup' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Markup view is disabled', '403 Forbidden') + if request.pathtype != vclib.FILE: + raise debug.ViewVCException('Unsupported feature: markup view on ' + 'directory', '400 Bad Request') markup_or_annotate(request, 0) def view_annotate(request): if 'annotate' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Annotation view is disabled', '403 Forbidden') + if request.pathtype != vclib.FILE: + raise debug.ViewVCException('Unsupported feature: annotate view on ' + 'directory', '400 Bad Request') markup_or_annotate(request, 1) def revcmp(rev1, rev2): - rev1 = map(int, string.split(rev1, '.')) - rev2 = map(int, string.split(rev2, '.')) + rev1 = map(int, rev1.split('.')) + rev2 = map(int, rev2.split('.')) return cmp(rev1, rev2) def sort_file_data(file_data, roottype, sortdir, sortby, group_dirs): @@ -1783,7 +2283,7 @@ def sort_file_data(file_data, roottype, sortdir, sortby, group_dirs): def icmp(x, y): """case insensitive comparison""" - return cmp(string.lower(x), string.lower(y)) + return cmp(x.lower(), y.lower()) def view_roots(request): if 'roots' not in request.cfg.options.allowed_views: @@ -1797,16 +2297,30 @@ def view_roots(request): rootnames = allroots.keys() rootnames.sort(icmp) for rootname in rootnames: + root_path, root_type, lastmod = allroots[rootname] href = request.get_url(view_func=view_directory, where='', pathtype=vclib.DIR, params={'root': rootname}, escape=1) + if root_type == vclib.SVN: + log_href = request.get_url(view_func=view_log, + where='', pathtype=vclib.DIR, + params={'root': rootname}, escape=1) + else: + log_href = None roots.append(_item(name=request.server.escape(rootname), - type=allroots[rootname][1], - path=allroots[rootname][0], - href=href)) + type=root_type, + path=root_path, + author=lastmod and lastmod.author or None, + ago=lastmod and lastmod.ago or None, + date=lastmod and lastmod.date or None, + log=lastmod and lastmod.log or None, + short_log=lastmod and lastmod.short_log or None, + rev=lastmod and lastmod.rev or None, + href=href, + log_href=log_href)) data = common_template_data(request) - data.merge(ezt.TemplateData({ + data.merge(TemplateData({ 'roots' : roots, })) generate_page(request, "roots", data) @@ -1833,8 +2347,10 @@ def view_directory(request): cfg.options.hide_attic)) options["cvs_subdirs"] = (cfg.options.show_subdir_lastmod and cfg.options.show_logs) + debug.t_start("listdir") file_data = request.repos.listdir(request.path_parts, request.pathrev, options) + debug.t_end("listdir") # sort with directories first, and using the "sortby" criteria sortby = request.query_dict.get('sortby', cfg.options.sort_by) or 'file' @@ -1886,6 +2402,7 @@ def view_directory(request): where = request.where where_prefix = where and where + '/' + debug.t_start("row-building") for file in file_data: row = _item(author=None, log=None, short_log=None, state=None, size=None, log_file=None, log_rev=None, graph_href=None, mime_type=None, @@ -1903,8 +2420,11 @@ def view_directory(request): row.date = make_time_string(file.date, cfg) row.ago = html_time(request, file.date) if cfg.options.show_logs: - row.short_log = format_log(file.log, cfg) - row.log = htmlify(file.log, cfg.options.mangle_email_addresses) + debug.t_start("dirview_logformat") + lf = LogFormatter(request, file.log) + row.log = lf.get(maxlen=0, htmlize=1) + row.short_log = lf.get(maxlen=cfg.options.short_log_len, htmlize=1) + debug.t_end("dirview_logformat") row.lockinfo = file.lockinfo row.name = request.server.escape(request.utf8(file.name)) row.anchor = row.name @@ -1960,8 +2480,9 @@ def view_directory(request): if request.roottype == 'svn': row.size = file.size - row.mime_type = calculate_mime_type(request, _path_parts(file_where), - file.rev) + row.mime_type, encoding = calculate_mime_type(request, + _path_parts(file_where), + file.rev) fvi = get_file_view_info(request, file_where, file.rev, row.mime_type) row.view_href = fvi.view_href row.download_href = fvi.download_href @@ -1982,17 +2503,16 @@ def view_directory(request): escape=1) rows.append(row) + debug.t_end("row-building") # Prepare the data that will be passed to the template, based on the # common template data. data = common_template_data(request) - data.merge(ezt.TemplateData({ + data.merge(TemplateData({ 'entries' : rows, 'sortby' : sortby, 'sortdir' : sortdir, - 'search_re' : search_re \ - and htmlify(search_re, cfg.options.mangle_email_addresses) \ - or None, + 'search_re' : request.server.escape(search_re), 'dir_pagestart' : None, 'sortby_file_href' : request.get_url(params={'sortby': 'file', 'sortdir': None}, @@ -2127,10 +2647,11 @@ def paging(data, key, pagestart, local_name, pagesize): # Slice return data[key][pagestart:pageend] -def paging_sws(data, key, pagestart, local_name, pagesize, offset): +def paging_sws(data, key, pagestart, local_name, pagesize, + extra_pages, offset): """Implement sliding window-style paging.""" # Create the picklist - last_requested = pagestart + (EXTRA_PAGES * pagesize) + last_requested = pagestart + (extra_pages * pagesize) picklist = data['picklist'] = [] has_more = ezt.boolean(0) for i in range(0, len(data[key]), pagesize): @@ -2234,9 +2755,11 @@ def view_log(request): if request.roottype == 'cvs': raise debug.ViewVCException('Unsupported feature: log view on CVS ' 'directory', '400 Bad Request') - mime_type = None + mime_type = encoding = None else: - mime_type = calculate_mime_type(request, request.path_parts, request.pathrev) + mime_type, encoding = calculate_mime_type(request, + request.path_parts, + request.pathrev) options = {} options['svn_show_all_dir_logs'] = 1 ### someday make this optional? @@ -2255,11 +2778,12 @@ def view_log(request): sortby = vclib.SORTBY_DEFAULT first = last = 0 + log_pagestart = None if cfg.options.log_pagesize: log_pagestart = int(request.query_dict.get('log_pagestart', 0)) - first = log_pagestart - min(log_pagestart, - (EXTRA_PAGES * cfg.options.log_pagesize)) - last = log_pagestart + ((EXTRA_PAGES + 1) * cfg.options.log_pagesize) + 1 + total = cfg.options.log_pagesextra * cfg.options.log_pagesize + first = log_pagestart - min(log_pagestart, total) + last = log_pagestart + (total + cfg.options.log_pagesize) + 1 show_revs = request.repos.itemlog(request.path_parts, request.pathrev, sortby, first, last - first, options) @@ -2268,7 +2792,6 @@ def view_log(request): entries = [ ] name_printed = { } - cvs = request.roottype == 'cvs' for rev in show_revs: entry = _item() entry.rev = rev.string @@ -2279,10 +2802,6 @@ def view_log(request): entry.ago = None if rev.date is not None: entry.ago = html_time(request, rev.date, 1) - entry.log = rev.log or "" - if cvs: - entry.log = request.utf8(entry.log) - entry.log = htmlify(entry.log, cfg.options.mangle_email_addresses) entry.size = rev.size entry.lockinfo = rev.lockinfo entry.branch_point = None @@ -2290,6 +2809,9 @@ def view_log(request): entry.orig_path = None entry.copy_path = None + lf = LogFormatter(request, rev.log or '') + entry.log = lf.get(maxlen=0, htmlize=1) + entry.view_href = None entry.download_href = None entry.download_text_href = None @@ -2379,7 +2901,8 @@ def view_log(request): if selected_rev != entry.rev: entry.sel_for_diff_href = \ request.get_url(view_func=view_log, - params={'r1': entry.rev}, + params={'r1': entry.rev, + 'log_pagestart': log_pagestart}, escape=1) if entry.prev is not None: entry.diff_to_prev_href = \ @@ -2431,7 +2954,7 @@ def view_log(request): data = common_template_data(request) - data.merge(ezt.TemplateData({ + data.merge(TemplateData({ 'default_branch' : None, 'mime_type' : mime_type, 'rev_selected' : selected_rev, @@ -2520,10 +3043,13 @@ def view_log(request): if cfg.options.log_pagesize: data['log_paging_action'], data['log_paging_hidden_values'] = \ - request.get_form(params={'log_pagestart': None}) + request.get_form(params={'log_pagestart': None, + 'r1': selected_rev, + }) data['log_pagestart'] = int(request.query_dict.get('log_pagestart',0)) data['entries'] = paging_sws(data, 'entries', data['log_pagestart'], - 'rev', cfg.options.log_pagesize, first) + 'rev', cfg.options.log_pagesize, + cfg.options.log_pagesextra, first) generate_page(request, "log", data) @@ -2534,17 +3060,21 @@ def view_checkout(request): if 'co' not in cfg.options.allowed_views: raise debug.ViewVCException('Checkout view is disabled', '403 Forbidden') - + if request.pathtype != vclib.FILE: + raise debug.ViewVCException('Unsupported feature: checkout view on ' + 'directory', '400 Bad Request') + path, rev = _orig_path(request) - fp, revision = request.repos.openfile(path, rev) + fp, revision = request.repos.openfile(path, rev, {}) # The revision number acts as a strong validator. if not check_freshness(request, None, revision): + mime_type, encoding = calculate_mime_type(request, path, rev) mime_type = request.query_dict.get('content-type') \ - or calculate_mime_type(request, path, rev) \ + or mime_type \ or 'text/plain' - server_fp = get_writeready_server_file(request, mime_type) - copy_stream(fp, server_fp, cfg) + server_fp = get_writeready_server_file(request, mime_type, encoding) + copy_stream(fp, server_fp) fp.close() def cvsgraph_make_reqopt(request, cfgname, queryparam, optvalue): @@ -2620,7 +3150,7 @@ def view_cvsgraph_image(request): cvsgraph_extraopts(request), rcsfile), 'rb', 0) - copy_stream(fp, get_writeready_server_file(request, 'image/png'), cfg) + copy_stream(fp, get_writeready_server_file(request, 'image/png')) fp.close() def view_cvsgraph(request): @@ -2667,7 +3197,7 @@ def view_cvsgraph(request): request.get_form(view_func=view_cvsgraph, params={}) data = common_template_data(request) - data.merge(ezt.TemplateData({ + data.merge(TemplateData({ 'imagemap' : fp, 'imagesrc' : imagesrc, 'graph_action' : graph_action, @@ -2691,7 +3221,7 @@ def search_file(repos, path_parts, rev, search_re): # Read in each line of a checked-out file, and then use re.search to # search line. - fp = repos.openfile(path_parts, rev)[0] + fp = repos.openfile(path_parts, rev, {})[0] matches = 0 while 1: line = fp.readline() @@ -2733,7 +3263,6 @@ def view_doc(request): raise debug.ViewVCException('Static file "%s" not available (%s)' % (document, str(v)), '404 Not Found') - request.server.addheader('Content-Length', content_length) if document[-3:] == 'png': mime_type = 'image/png' elif document[-3:] == 'jpg': @@ -2744,17 +3273,18 @@ def view_doc(request): mime_type = 'text/css' else: # assume HTML: mime_type = None - copy_stream(fp, get_writeready_server_file(request, mime_type), cfg) + copy_stream(fp, get_writeready_server_file(request, mime_type, + content_length=content_length)) fp.close() def rcsdiff_date_reformat(date_str, cfg): if date_str is None: return None try: - date = compat.cvs_strptime(date_str) + date = vclib.ccvs.cvs_strptime(date_str) except ValueError: return date_str - return make_time_string(compat.timegm(date), cfg) + return make_time_string(calendar.timegm(date), cfg) _re_extract_rev = re.compile(r'^[-+*]{3} [^\t]+\t([^\t]+)\t((\d+\.)*\d+)$') _re_extract_info = re.compile(r'@@ \-([0-9]+).*\+([0-9]+).*@@(.*)') @@ -2793,24 +3323,25 @@ class DiffSource: return item def _format_text(self, text): - text = string.expandtabs(string.rstrip(text)) + text = text.rstrip('\r\n') + if self.cfg.options.tabsize > 0: + text = text.expandtabs(self.cfg.options.tabsize) hr_breakable = self.cfg.options.hr_breakable # 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, + # to insert "&" because it would get escaped by sapi.escape(). Similarly, # we use "\x02" as a stand-in for "
" if hr_breakable > 1 and len(text) > hr_breakable: text = re.sub('(' + ('.' * hr_breakable) + ')', '\\1\x02', text) if hr_breakable: # make every other space "breakable" - text = string.replace(text, ' ', ' \x01nbsp;') + text = text.replace(' ', ' \x01nbsp;') else: - text = string.replace(text, ' ', '\x01nbsp;') - text = htmlify(text, mangle_email_addrs=0) - text = string.replace(text, '\x01', '&') - text = string.replace(text, '\x02', - '\
') + text = text.replace(' ', '\x01nbsp;') + text = sapi.escape(text) + text = text.replace('\x01', '&') + text = text.replace('\x02', '\
') return text def _get_row(self): @@ -2829,7 +3360,7 @@ class DiffSource: if not line: if self.state == 'no-changes': self.state = 'done' - return _item(type='no-changes') + return _item(type=_RCSDIFF_NO_CHANGES) # see if there are lines to flush if self.left_col or self.right_col: @@ -2851,14 +3382,12 @@ class DiffSource: return _item(type='header', line_info_left=match.group(1), line_info_right=match.group(2), - line_info_extra=match.group(3)) - + line_info_extra=self._format_text(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 + # Just skip. This code used to move to flush state, but that resulted in + # changes being displayed as removals-and-readditions. return None diff_code = line[0] @@ -2919,7 +3448,8 @@ class DiffSource: class DiffSequencingError(Exception): pass -def diff_parse_headers(fp, diff_type, rev1, rev2, sym1=None, sym2=None): +def diff_parse_headers(fp, diff_type, path1, path2, rev1, rev2, + sym1=None, sym2=None): date1 = date2 = log_rev1 = log_rev2 = flag = None header_lines = [] @@ -2936,6 +3466,7 @@ def diff_parse_headers(fp, diff_type, rev1, rev2, sym1=None, sym2=None): # collecting them in an array until we've read and handled them all. if f1 and f2: parsing = 1 + flag = _RCSDIFF_NO_CHANGES len_f1 = len(f1) len_f2 = len(f2) while parsing: @@ -2943,26 +3474,29 @@ def diff_parse_headers(fp, diff_type, rev1, rev2, sym1=None, sym2=None): if not line: break + # Saw at least one line in the stream + flag = None + if line[:len(f1)] == f1: match = _re_extract_rev.match(line) if match: date1 = match.group(1) log_rev1 = match.group(2) - if sym1: - line = line[:-1] + ' %s\n' % sym1 + line = '%s%s\t%s\t%s%s\n' % (f1, path1, date1, log_rev1, + sym1 and ' ' + sym1 or '') elif line[:len(f2)] == f2: match = _re_extract_rev.match(line) if match: date2 = match.group(1) log_rev2 = match.group(2) - if sym2: - line = line[:-1] + ' %s\n' % sym2 + line = '%s%s\t%s\t%s%s\n' % (f2, path2, date2, log_rev2, + sym2 and ' ' + sym2 or '') parsing = 0 elif line[:3] == 'Bin': flag = _RCSDIFF_IS_BINARY parsing = 0 - elif (string.find(line, 'not found') != -1 or - string.find(line, 'illegal option') != -1): + elif (line.find('not found') != -1 or + line.find('illegal option') != -1): flag = _RCSDIFF_ERROR parsing = 0 header_lines.append(line) @@ -2976,7 +3510,7 @@ def diff_parse_headers(fp, diff_type, rev1, rev2, sym1=None, sym2=None): 'revision %s' % (log_rev2, rev2), '500 Internal Server Error') - return date1, date2, flag, string.join(header_lines, '') + return date1, date2, flag, ''.join(header_lines) def _get_diff_path_parts(request, query_key, rev, base_rev): @@ -3013,7 +3547,7 @@ def setup_diff(request): raise debug.ViewVCException('Missing revision from the diff ' 'form text field', '400 Bad Request') else: - idx = string.find(r1, ':') + idx = r1.find(':') if idx == -1: rev1 = r1 else: @@ -3027,7 +3561,7 @@ def setup_diff(request): 'form text field', '400 Bad Request') sym2 = '' else: - idx = string.find(r2, ':') + idx = r2.find(':') if idx == -1: rev2 = r2 else: @@ -3067,6 +3601,13 @@ def view_patch(request): query_dict = request.query_dict p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request) + mime_type1, encoding1 = calculate_mime_type(request, p1, rev1) + mime_type2, encoding2 = calculate_mime_type(request, p2, rev2) + if is_binary_file_mime_type(mime_type1, cfg) or \ + is_binary_file_mime_type(mime_type2, cfg): + raise debug.ViewVCException('Display of binary file content disabled ' + 'by configuration', '403 Forbidden') + # In the absence of a format dictation in the CGI params, we'll let # use the configured diff format, allowing 'c' to mean 'c' and # anything else to mean 'u'. @@ -3081,154 +3622,327 @@ def view_patch(request): % format, '400 Bad Request') try: - fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type) + fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type, diff_options) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' 'to diff', '400 Bad Request') - date1, date2, flag, headers = diff_parse_headers(fp, diff_type, rev1, rev2, - sym1, sym2) + path_left = _path_join(p1) + path_right = _path_join(p2) + date1, date2, flag, headers = diff_parse_headers(fp, diff_type, + path_left, path_right, + rev1, rev2, sym1, sym2) server_fp = get_writeready_server_file(request, 'text/plain') server_fp.write(headers) - copy_stream(fp, server_fp, cfg) + copy_stream(fp, server_fp) fp.close() +def diff_side_item(request, path_comp, rev, sym): + '''Prepare information about left/right side of the diff. Prepare two flavors, + for content and for property diffs.''' + + # TODO: Is the slice necessary, or is limit enough? + options = {'svn_show_all_dir_logs': 1} + log_entry = request.repos.itemlog(path_comp, rev, vclib.SORTBY_REV, + 0, 1, options)[-1] + ago = log_entry.date is not None \ + and html_time(request, log_entry.date, 1) or None + path_joined = _path_join(path_comp) + + lf = LogFormatter(request, log_entry.log) + + # Item for property diff: no hrefs, there's no view + # to download/annotate property + i_prop = _item(log_entry=log_entry, + date=make_time_string(log_entry.date, request.cfg), + author=log_entry.author, + log = lf.get(maxlen=0, htmlize=1), + size=log_entry.size, + ago=ago, + path=path_joined, + path_comp=path_comp, + rev=rev, + tag=sym, + view_href=None, + download_href=None, + download_text_href=None, + annotate_href=None, + revision_href=None, + prefer_markup=ezt.boolean(0)) + + # Content diff item is based on property diff, with URIs added + fvi = get_file_view_info(request, path_joined, rev) + i_content = copy.copy(i_prop) + i_content.view_href = fvi.view_href + i_content.download_href = fvi.download_href + i_content.download_text_href = fvi.download_text_href + i_content.annotate_href = fvi.annotate_href + i_content.revision_href = fvi.revision_href + i_content.prefer_markup = fvi.prefer_markup + + # Property diff item has properties hash, naturally. Content item doesn't. + i_content.properties = None + i_prop.properties = request.repos.itemprops(path_comp, rev) + return i_content, i_prop + + +class DiffDescription: + def __init__(self, request): + cfg = request.cfg + query_dict = request.query_dict + + self.diff_format = query_dict.get('diff_format', cfg.options.diff_format) + self.human_readable = 0 + self.hide_legend = 0 + self.line_differ = None + self.fp_differ = None + self.request = request + self.context = -1 + self.changes = [] + + if self.diff_format == 'c': + self.diff_type = vclib.CONTEXT + self.hide_legend = 1 + elif self.diff_format == 's': + self.diff_type = vclib.SIDE_BY_SIDE + self.hide_legend = 1 + elif self.diff_format == 'l': + self.diff_type = vclib.UNIFIED + self.context = 15 + self.human_readable = 1 + elif self.diff_format == 'f': + self.diff_type = vclib.UNIFIED + self.context = None + self.human_readable = 1 + elif self.diff_format == 'h': + self.diff_type = vclib.UNIFIED + self.human_readable = 1 + elif self.diff_format == 'u': + self.diff_type = vclib.UNIFIED + self.hide_legend = 1 + else: + raise debug.ViewVCException('Diff format %s not understood' + % self.diff_format, '400 Bad Request') + + # Determine whether idiff is avaialble and whether it could be used. + # idiff only supports side-by-side (conditionally) and unified formats, + # and is only used if intra-line diffs are requested. + if (cfg.options.hr_intraline and idiff + and ((self.human_readable and idiff.sidebyside) + or (not self.human_readable and self.diff_type == vclib.UNIFIED))): + # Override hiding legend for unified format. It is not marked 'human + # readable', and it is displayed differently depending on whether + # hr_intraline is disabled (displayed as raw diff) or enabled + # (displayed as colored). What a royal mess... Issue #301 should + # at some time address it; at that time, human_readable and hide_legend + # controls should both be merged into one, 'is_colored' or something. + self.hide_legend = 0 + if self.human_readable: + self.line_differ = self._line_idiff_sidebyside + self.diff_block_format = 'sidebyside-2' + else: + self.line_differ = self._line_idiff_unified + self.diff_block_format = 'unified' + else: + if self.human_readable: + self.diff_block_format = 'sidebyside-1' + self.fp_differ = self._fp_vclib_hr + else: + self.diff_block_format = 'raw' + self.fp_differ = self._fp_vclib_raw + + def anchor(self, anchor_name): + self.changes.append(_item(diff_block_format='anchor', anchor=anchor_name)) + + def get_content_diff(self, left, right): + diff_options = {} + if self.context != -1: + diff_options['context'] = self.context + if self.human_readable: + cfg = self.request.cfg + diff_options['funout'] = cfg.options.hr_funout + diff_options['ignore_white'] = cfg.options.hr_ignore_white + diff_options['ignore_keyword_subst'] = \ + cfg.options.hr_ignore_keyword_subst + self._get_diff(left, right, self._content_lines, self._content_fp, + diff_options, None) + + def get_prop_diff(self, left, right): + diff_options = {} + if self.context != -1: + diff_options['context'] = self.context + if self.human_readable: + cfg = self.request.cfg + diff_options['ignore_white'] = cfg.options.hr_ignore_white + for name in self._uniq(left.properties.keys() + right.properties.keys()): + # Skip non-utf8 property names + if is_undisplayable(name): + continue + val_left = left.properties.get(name, '') + val_right = right.properties.get(name, '') + # Skip non-changed properties + if val_left == val_right: + continue + # Check for binary properties + if is_undisplayable(val_left) or is_undisplayable(val_right): + self.changes.append(_item(left=left, + right=right, + diff_block_format=self.diff_block_format, + changes=[ _item(type=_RCSDIFF_IS_BINARY) ], + propname=name)) + continue + self._get_diff(left, right, self._prop_lines, self._prop_fp, + diff_options, name) + + def _get_diff(self, left, right, get_lines, get_fp, diff_options, propname): + if self.fp_differ is not None: + fp = get_fp(left, right, propname, diff_options) + changes = self.fp_differ(left, right, fp, propname) + else: + lines_left = get_lines(left, propname) + lines_right = get_lines(right, propname) + changes = self.line_differ(lines_left, lines_right, diff_options) + self.changes.append(_item(left=left, + right=right, + changes=changes, + diff_block_format=self.diff_block_format, + propname=propname)) + + def _line_idiff_sidebyside(self, lines_left, lines_right, diff_options): + return idiff.sidebyside(lines_left, lines_right, + diff_options.get("context", 5)) + + def _line_idiff_unified(self, lines_left, lines_right, diff_options): + return idiff.unified(lines_left, lines_right, + diff_options.get("context", 2)) + + def _fp_vclib_hr(self, left, right, fp, propname): + date1, date2, flag, headers = \ + diff_parse_headers(fp, self.diff_type, + self._property_path(left, propname), + self._property_path(right, propname), + left.rev, right.rev, left.tag, right.tag) + if flag is not None: + return [ _item(type=flag) ] + else: + return DiffSource(fp, self.request.cfg) + + def _fp_vclib_raw(self, left, right, fp, propname): + date1, date2, flag, headers = \ + diff_parse_headers(fp, self.diff_type, + self._property_path(left, propname), + self._property_path(right, propname), + left.rev, right.rev, left.tag, right.tag) + if flag is not None: + return _item(type=flag) + else: + return _item(type='raw', raw=MarkupPipeWrapper(fp, + self.request.server.escape(headers), None, 1)) + + def _content_lines(self, side, propname): + f = self.request.repos.openfile(side.path_comp, side.rev, {})[0] + try: + lines = f.readlines() + finally: + f.close() + return lines + + def _content_fp(self, left, right, propname, diff_options): + return self.request.repos.rawdiff(left.path_comp, left.rev, + right.path_comp, right.rev, + self.diff_type, diff_options) + + def _prop_lines(self, side, propname): + val = side.properties.get(propname, '') + return val.splitlines() + + def _prop_fp(self, left, right, propname, diff_options): + fn_left = self._temp_file(left.properties.get(propname)) + fn_right = self._temp_file(right.properties.get(propname)) + diff_args = vclib._diff_args(self.diff_type, diff_options) + info_left = self._property_path(left, propname), \ + left.log_entry.date, left.rev + info_right = self._property_path(right, propname), \ + right.log_entry.date, right.rev + return vclib._diff_fp(fn_left, fn_right, info_left, info_right, + self.request.cfg.utilities.diff or 'diff', diff_args) + + def _temp_file(self, val): + '''Create a temporary file with content from val''' + fn = tempfile.mktemp() + fp = open(fn, "wb") + if val: + fp.write(val) + fp.close() + return fn + + def _uniq(self, lst): + '''Determine unique set of list elements''' + h = {} + for e in lst: + h[e] = 1 + return sorted(h.keys()) + + def _property_path(self, side, propname): + '''Return path to be displayed in raw diff - possibly augmented with + property name''' + if propname is None: + return side.path + else: + return "%s:property(%s)" % (side.path, propname) + + def view_diff(request): if 'diff' not in request.cfg.options.allowed_views: raise debug.ViewVCException('Diff generation is disabled', '403 Forbidden') cfg = request.cfg - query_dict = request.query_dict p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request) + + mime_type1, encoding1 = calculate_mime_type(request, p1, rev1) + mime_type2, encoding2 = calculate_mime_type(request, p2, rev2) + if is_binary_file_mime_type(mime_type1, cfg) or \ + is_binary_file_mime_type(mime_type2, cfg): + raise debug.ViewVCException('Display of binary file content disabled ' + 'by configuration', '403 Forbidden') # since templates are in use and subversion allows changes to the dates, # we can't provide a strong etag if check_freshness(request, None, '%s-%s' % (rev1, rev2), weak=1): return - diff_type = None - diff_options = {} - human_readable = 0 + left_side_content, left_side_prop = diff_side_item(request, p1, rev1, sym1) + right_side_content, right_side_prop = diff_side_item(request, p2, rev2, sym2) - format = query_dict.get('diff_format', cfg.options.diff_format) - if format == 'c': - diff_type = vclib.CONTEXT - elif format == 's': - diff_type = vclib.SIDE_BY_SIDE - elif format == 'l': - diff_type = vclib.UNIFIED - diff_options['context'] = 15 - human_readable = 1 - elif format == 'f': - diff_type = vclib.UNIFIED - diff_options['context'] = None - human_readable = 1 - elif format == 'h': - diff_type = vclib.UNIFIED - diff_options['context'] = 5 - human_readable = 1 - elif format == 'u': - diff_type = vclib.UNIFIED - else: - raise debug.ViewVCException('Diff format %s not understood' - % format, '400 Bad Request') + desc = DiffDescription(request) - if human_readable: - diff_options['funout'] = cfg.options.hr_funout - diff_options['ignore_white'] = cfg.options.hr_ignore_white - diff_options['ignore_keyword_subst'] = cfg.options.hr_ignore_keyword_subst try: - fp = sidebyside = unified = None - if (cfg.options.hr_intraline and idiff - and ((human_readable and idiff.sidebyside) - or (not human_readable and diff_type == vclib.UNIFIED))): - f1 = request.repos.openfile(p1, rev1)[0] - try: - lines_left = f1.readlines() - finally: - f1.close() + if request.pathtype == vclib.FILE: + # Get file content diff + desc.anchor("content") + desc.get_content_diff(left_side_content, right_side_content) - f2 = request.repos.openfile(p2, rev2)[0] - try: - lines_right = f2.readlines() - finally: - f2.close() + # Get property list and diff each property + desc.anchor("properties") + desc.get_prop_diff(left_side_prop, right_side_prop) - if human_readable: - sidebyside = idiff.sidebyside(lines_left, lines_right, - diff_options.get("context", 5)) - else: - unified = idiff.unified(lines_left, lines_right, - diff_options.get("context", 2)) - else: - fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type, diff_options) except vclib.InvalidRevision: raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' - 'to diff', '400 Bad Request') - path_left = _path_join(p1) - path_right = _path_join(p2) - if fp: - date1, date2, flag, headers = diff_parse_headers(fp, diff_type, - rev1, rev2, - sym1, sym2) - else: - date1 = date2 = flag = headers = None - - raw_diff_fp = changes = None - if fp: - if human_readable: - if flag is not None: - changes = [ _item(type=flag) ] - else: - changes = DiffSource(fp, cfg) - else: - raw_diff_fp = MarkupPipeWrapper(cfg, fp, - htmlify(headers, mangle_email_addrs=0), - None, 1) + 'to diff', '400 Bad Request') no_format_params = request.query_dict.copy() no_format_params['diff_format'] = None - - fvi = get_file_view_info(request, path_left, rev1) - left = _item(date=rcsdiff_date_reformat(date1, cfg), - path=path_left, - rev=rev1, - tag=sym1, - view_href=fvi.view_href, - download_href=fvi.download_href, - download_text_href=fvi.download_text_href, - annotate_href=fvi.annotate_href, - revision_href=fvi.revision_href, - prefer_markup=fvi.prefer_markup) - - fvi = get_file_view_info(request, path_right, rev2) - right = _item(date=rcsdiff_date_reformat(date2, cfg), - path=path_right, - rev=rev2, - tag=sym2, - view_href=fvi.view_href, - download_href=fvi.download_href, - download_text_href=fvi.download_text_href, - annotate_href=fvi.annotate_href, - revision_href=fvi.revision_href, - prefer_markup=fvi.prefer_markup) - diff_format_action, diff_format_hidden_values = \ request.get_form(params=no_format_params) data = common_template_data(request) - data.merge(ezt.TemplateData({ - 'left' : left, - 'right' : right, - 'raw_diff' : raw_diff_fp, - 'changes' : changes, - 'sidebyside': sidebyside, - 'unified': unified, - 'diff_format' : request.query_dict.get('diff_format', - cfg.options.diff_format), + data.merge(TemplateData({ + 'diffs' : desc.changes, + 'diff_format' : desc.diff_format, + 'hide_legend' : ezt.boolean(desc.hide_legend), 'patch_href' : request.get_url(view_func=view_patch, params=no_format_params, escape=1), @@ -3239,7 +3953,7 @@ def view_diff(request): def generate_tarball_header(out, name, size=0, mode=None, mtime=0, - uid=0, gid=0, typefrag=None, linkname='', + uid=0, gid=0, typeflag=None, linkname='', uname='viewvc', gname='viewvc', devmajor=1, devminor=0, prefix=None, magic='ustar', version='00', chksum=None): @@ -3249,40 +3963,49 @@ def generate_tarball_header(out, name, size=0, mode=None, mtime=0, else: mode = 0644 - if not typefrag: - if name[-1:] == '/': - typefrag = '5' # directory + if not typeflag: + if linkname: + typeflag = '2' # symbolic link + elif name[-1:] == '/': + typeflag = '5' # directory else: - typefrag = '0' # regular file + typeflag = '0' # regular file if not prefix: prefix = '' - # generate a GNU tar extension header for long names. + # generate a GNU tar extension header for a long name. if len(name) >= 100: generate_tarball_header(out, '././@LongLink', len(name), - 0644, 0, 0, 0, 'L') + 0, 0, 0, 0, 'L') out.write(name) out.write('\0' * (511 - ((len(name) + 511) % 512))) + # generate a GNU tar extension header for a long symlink name. + if len(linkname) >= 100: + generate_tarball_header(out, '././@LongLink', len(linkname), + 0, 0, 0, 0, 'K') + out.write(linkname) + out.write('\0' * (511 - ((len(linkname) + 511) % 512))) + block1 = struct.pack('100s 8s 8s 8s 12s 12s', - name, - '%07o' % mode, - '%07o' % uid, - '%07o' % gid, - '%011o' % size, - '%011o' % mtime) + 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) + typeflag, + linkname, + magic, + version, + uname, + gname, + '%07o' % devmajor, + '%07o' % devminor, + prefix) if not chksum: dummy_chksum = ' ' @@ -3360,17 +4083,50 @@ def generate_tarball(out, request, reldir, stack, dir_mtime=None): else: mode = 0644 - ### FIXME: Read the whole file into memory? Bad... better to do - ### 2 passes. - fp = request.repos.openfile(rep_path + [file.name], request.pathrev)[0] - contents = fp.read() - fp.close() + # Is this thing a symlink? + # + ### FIXME: A better solution would be to have vclib returning + ### symlinks with a new vclib.SYMLINK path type. + symlink_target = None + if hasattr(request.repos, 'get_symlink_target'): + symlink_target = request.repos.get_symlink_target(rep_path + [file.name], + request.pathrev) - generate_tarball_header(out, tar_dir + file.name, - len(contents), mode, - file.date is not None and file.date or 0) - out.write(contents) - out.write('\0' * (511 - ((len(contents) + 511) % 512))) + # If the object is a symlink, generate the appropriate header. + # Otherwise, we're dealing with a regular file. + if symlink_target: + generate_tarball_header(out, tar_dir + file.name, 0, mode, + file.date is not None and file.date or 0, + typeflag='2', linkname=symlink_target) + else: + filesize = request.repos.filesize(rep_path + [file.name], request.pathrev) + + if filesize == -1: + # Bummer. We have to calculate the filesize manually. + fp = request.repos.openfile(rep_path + [file.name], request.pathrev, {})[0] + filesize = 0 + while 1: + chunk = retry_read(fp) + if not chunk: + break + filesize = filesize + len(chunk) + fp.close() + + # Write the tarball header... + generate_tarball_header(out, tar_dir + file.name, filesize, mode, + file.date is not None and file.date or 0) + + # ...the file's contents ... + fp = request.repos.openfile(rep_path + [file.name], request.pathrev, {})[0] + while 1: + chunk = retry_read(fp) + if not chunk: + break + out.write(chunk) + fp.close() + + # ... and then add the block padding. + out.write('\0' * (511 - (filesize + 511) % 512)) # Recurse into subdirectories, skipping busted and unauthorized (or # configured-to-be-hidden) ones. @@ -3394,6 +4150,10 @@ def download_tarball(request): raise debug.ViewVCException('Tarball generation is disabled', '403 Forbidden') + # If debugging, we just need to open up the specified tar path for + # writing. Otherwise, we get a writeable server output stream -- + # disabling any default compression thereupon -- and wrap that in + # our own gzip stream wrapper. if debug.TARFILE_PATH: fp = open(debug.TARFILE_PATH, 'w') else: @@ -3402,11 +4162,9 @@ def download_tarball(request): tarfile = "%s-%s" % (tarfile, request.path_parts[-1]) request.server.addheader('Content-Disposition', 'attachment; filename="%s.tar.gz"' % (tarfile)) - server_fp = get_writeready_server_file(request, 'application/x-gzip') + server_fp = get_writeready_server_file(request, 'application/x-gzip', + allow_compress=False) request.server.flush() - - # Try to use the Python gzip module, if available; otherwise, - # we'll use the configured 'gzip' binary. fp = gzip.GzipFile('', 'wb', 9, server_fp) ### FIXME: For Subversion repositories, we can get the real mtime of the @@ -3427,7 +4185,7 @@ def download_tarball(request): def view_revision(request): - if request.roottype == "cvs": + if request.roottype != "svn": raise debug.ViewVCException("Revision view not supported for CVS " "repositories at this time.", "400 Bad Request") @@ -3446,9 +4204,26 @@ def view_revision(request): return # Fetch the revision information. - date, author, msg, changes = request.repos.revinfo(rev) + date, author, msg, revprops, changes = request.repos.revinfo(rev) date_str = make_time_string(date, cfg) + # Fix up the revprops list (rather like get_itemprops()). + propnames = revprops.keys() + propnames.sort() + props = [] + for name in propnames: + # skip non-utf8 property names + if is_undisplayable(name): + continue + lf = LogFormatter(request, revprops[name]) + value = lf.get(maxlen=0, htmlize=1) + # note non-utf8 property values + undisplayable = is_undisplayable(value) + if undisplayable: + value = None + props.append(_item(name=name, value=value, + undisplayable=ezt.boolean(undisplayable))) + # Sort the changes list by path. def changes_sort_by_path(a, b): return cmp(a.path_parts, b.path_parts) @@ -3514,7 +4289,8 @@ def view_revision(request): params={'pathrev' : link_rev}, escape=1) - if change.pathtype is vclib.FILE and change.text_changed: + if (change.pathtype is vclib.FILE and change.text_changed) \ + or change.props_changed: change.diff_href = request.get_url(view_func=view_diff, where=path, pathtype=change.pathtype, @@ -3557,13 +4333,15 @@ def view_revision(request): escape=1) jump_rev_action, jump_rev_hidden_values = \ request.get_form(params={'revision': None}) - + + lf = LogFormatter(request, msg) data = common_template_data(request) - data.merge(ezt.TemplateData({ + data.merge(TemplateData({ 'rev' : str(rev), 'author' : author, 'date' : date_str, - 'log' : msg and htmlify(msg, cfg.options.mangle_email_addresses) or None, + 'log' : lf.get(maxlen=0, htmlize=1), + 'properties' : props, 'ago' : date is not None and html_time(request, date, 1) or None, 'changes' : changes, 'prev_href' : prev_rev_href, @@ -3576,6 +4354,11 @@ def view_revision(request): 'first_changes_href': first_changes_href, 'jump_rev_action' : jump_rev_action, 'jump_rev_hidden_values' : jump_rev_hidden_values, + 'revision_href' : request.get_url(view_func=view_revision, + where=None, + pathtype=None, + params={'revision': str(rev)}, + escape=1), })) if rev == youngest_rev: request.server.addheader("Cache-control", "must-revalidate, no-store") @@ -3601,12 +4384,42 @@ def is_querydb_nonempty_for_root(request): return 1 return 0 +def validate_query_args(request): + # Do some additional input validation of query form arguments beyond + # what is offered by the CGI param validation loop in Request.run_viewvc(). + + for arg_base in ['branch', 'file', 'comment', 'who']: + # 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'): + raise debug.ViewVCException( + 'An illegal value was provided for the "%s" parameter.' + % (arg_match), + '400 Bad Request') + + # Now, for those args which are supposed to be regular expressions (per + # their corresponding XXX_match values), make sure they are. + if arg_match_value == 'regex' or arg_match_value == 'notregex': + arg_base_value = request.query_dict.get(arg_base) + if arg_base_value: + try: + re.compile(arg_base_value) + except: + raise debug.ViewVCException( + 'An illegal value was provided for the "%s" parameter.' + % (arg_base), + '400 Bad Request') + def view_queryform(request): if not is_query_supported(request): raise debug.ViewVCException('Can not query project root "%s" at "%s".' % (request.rootname, request.where), '403 Forbidden') + # Do some more precise input validation. + validate_query_args(request) + query_action, query_hidden_values = \ request.get_form(view_func=view_query, params={'limit_changes': None}) limit_changes = \ @@ -3624,27 +4437,30 @@ def view_queryform(request): else: dir_href = request.get_url(view_func=view_roots, params={}, escape=1) + def escaped_query_dict_get(itemname, itemdefault=''): + return request.server.escape(request.query_dict.get(itemname, itemdefault)) + data = common_template_data(request) - data.merge(ezt.TemplateData({ - 'repos' : repos, - 'repos_match' : repos_match, - 'repos_type' : request.query_dict.get('repos_type', ''), - 'branch' : request.query_dict.get('branch', ''), - 'branch_match' : request.query_dict.get('branch_match', 'exact'), - 'dir' : request.query_dict.get('dir', ''), - 'file' : request.query_dict.get('file', ''), - 'file_match' : request.query_dict.get('file_match', 'exact'), - 'query_revision' : request.query_dict.get('query_revision', ''), - 'who' : request.query_dict.get('who', ''), - 'who_match' : request.query_dict.get('who_match', 'exact'), - 'comment' : request.query_dict.get('comment', ''), - 'comment_match' : request.query_dict.get('comment_match', 'fulltext'), - 'search_content' : request.query_dict.get('search_content', ''), - 'querysort' : request.query_dict.get('querysort', 'date'), - 'date' : request.query_dict.get('date', 'week'), - 'hours' : request.query_dict.get('hours', '2'), - 'mindate' : request.query_dict.get('mindate', ''), - 'maxdate' : request.query_dict.get('maxdate', ''), + data.merge(TemplateData({ + 'repos' : request.server.escape(repos), + 'repos_match' : request.server.escape(repos_match), + 'repos_type' : escaped_query_dict_get('repos_type', ''), + 'query_revision' : escaped_query_dict_get('query_revision', ''), + 'search_content' : escaped_query_dict_get('search_content', ''), + 'branch' : escaped_query_dict_get('branch', ''), + 'branch_match' : escaped_query_dict_get('branch_match', 'exact'), + 'dir' : escaped_query_dict_get('dir', ''), + 'file' : escaped_query_dict_get('file', ''), + 'file_match' : escaped_query_dict_get('file_match', 'exact'), + 'who' : escaped_query_dict_get('who', ''), + 'who_match' : escaped_query_dict_get('who_match', 'exact'), + 'comment' : escaped_query_dict_get('comment', ''), + 'comment_match' : escaped_query_dict_get('comment_match', 'exact'), + 'querysort' : escaped_query_dict_get('querysort', 'date'), + 'date' : escaped_query_dict_get('date', 'week'), + 'hours' : escaped_query_dict_get('hours', '2'), + 'mindate' : escaped_query_dict_get('mindate', ''), + 'maxdate' : escaped_query_dict_get('maxdate', ''), 'query_action' : query_action, 'query_hidden_values' : query_hidden_values, 'limit_changes' : limit_changes, @@ -3680,7 +4496,7 @@ def parse_date(datestr): second = 0 # return a "seconds since epoch" value assuming date given in UTC tm = (year, month, day, hour, minute, second, 0, 0, 0) - return compat.timegm(tm) + return calendar.timegm(tm) else: return None @@ -3709,8 +4525,7 @@ def english_query(request): ret.append('on all branches ') comment = request.query_dict.get('comment', '') if comment: - ret.append('with comment %s ' - % htmlify(comment, mangle_email_addrs=0)) + ret.append('with comment %s ' % request.server.escape(comment)) if who: ret.append('by %s ' % request.server.escape(who)) date = request.query_dict.get('date', 'hours') @@ -3738,17 +4553,17 @@ def english_query(request): if maxdate: maxdate = make_time_string(parse_date(maxdate), cfg) ret.append('%s %s ' % (w2, maxdate)) - return string.join(ret, '') + return ''.join(ret) def prev_rev(rev): """Returns a string representing the previous revision of the argument.""" - r = string.split(rev, '.') + r = rev.split('.') # decrement final revision component r[-1] = str(int(r[-1]) - 1) # prune if we pass the beginning of the branch if len(r) > 2 and r[-1] == '0': r = r[:-2] - return string.join(r, '.') + return '.'.join(r) def rev_cmp(rev1, rev2): """Compares two revision numbers rev1 and rev2""" @@ -3887,7 +4702,7 @@ def build_commit(request, files, max_files, dir_strip, format): diff_href = request.get_url(root=my_repos.rootname, view_func=view_diff, where=where, pathtype=vclib.FILE, params=diff_href_params, escape=1) - mime_type = calculate_mime_type(request, path_parts, exam_rev) + mime_type, encoding = calculate_mime_type(request, path_parts, exam_rev) prefer_markup = ezt.boolean(default_view(mime_type, cfg) == view_markup) # Update plus/minus line change count. @@ -3945,8 +4760,10 @@ def build_commit(request, files, max_files, dir_strip, format): commit.log = None commit.short_log = None else: - commit.log = htmlify(desc) - commit.short_log = format_log(desc, cfg, format != 'rss') + lf = LogFormatter(request, desc) + htmlize = (format != 'rss') + commit.log = lf.get(maxlen=0, htmlize=htmlize) + commit.short_log = lf.get(maxlen=cfg.options.short_log_len, htmlize=htmlize) commit.author = request.server.escape(author) commit.rss_date = make_rss_time_string(date, request.cfg) if my_repos.roottype == 'svn': @@ -3963,27 +4780,29 @@ def build_commit(request, files, max_files, dir_strip, format): return commit def query_backout(request, commits): - request.server.header('text/plain') - if commits: - print '# This page can be saved as a shell script and executed.' - print '# It should be run at the top of your work area. It will update' - print '# your working copy to back out the changes selected by the' - print '# query.' - print - else: - print '# No changes were selected by the query.' - print '# There is nothing to back out.' + server_fp = get_writeready_server_file(request, 'text/plain') + if not commits: + server_fp.write("""\ +# No changes were selected by the query. +# There is nothing to back out. +""") return + server_fp.write("""\ +# This page can be saved as a shell script and executed. +# It should be run at the top of your work area. It will update +# your working copy to back out the changes selected by the +# query. +""") for commit in commits: for fileinfo in commit.files: if request.roottype == 'cvs': - print 'cvs update -j %s -j %s %s/%s' \ - % (fileinfo.rev, prev_rev(fileinfo.rev), - fileinfo.dir, fileinfo.file) + server_fp.write('cvs update -j %s -j %s %s/%s\n' + % (fileinfo.rev, prev_rev(fileinfo.rev), + fileinfo.dir, fileinfo.file)) elif request.roottype == 'svn': - print 'svn merge -r %s:%s %s/%s' \ - % (fileinfo.rev, prev_rev(fileinfo.rev), - fileinfo.dir, fileinfo.file) + server_fp.write('svn merge -r %s:%s %s/%s\n' + % (fileinfo.rev, prev_rev(fileinfo.rev), + fileinfo.dir, fileinfo.file)) def query_is_unsecure_patch(request, commits): if not commits: @@ -4096,6 +4915,9 @@ def view_query(request): cfg = request.cfg + # Do some more precise input validation. + validate_query_args(request) + # get form data repos_root = request.query_dict.get('repos', '') repos_root_t = repos_root @@ -4118,7 +4940,6 @@ def view_query(request): mindate = request.query_dict.get('mindate', '') maxdate = request.query_dict.get('maxdate', '') format = request.query_dict.get('format') - limit = int(request.query_dict.get('limit', 0)) limit_changes = int(request.query_dict.get('limit_changes', cfg.options.limit_changes)) @@ -4169,9 +4990,9 @@ def view_query(request): elif branch: query.SetBranch(branch, branch_match) if dir: - for subdir in string.split(dir, ','): + for subdir in dir.split(','): path = (_path_join(repos_dir + request.path_parts - + _path_parts(string.strip(subdir)))) + + _path_parts(subdir.strip()))) query.SetDirectory(path, 'exact') query.SetDirectory('%s/%%' % cvsdb.EscapeLike(path), 'like') else: @@ -4208,30 +5029,33 @@ def view_query(request): query.SetFromDateObject(mindate) if maxdate is not None: query.SetToDateObject(maxdate) - if limit: - query.SetLimit(limit) - elif format == 'rss': + + # Set the admin-defined (via configuration) row limits. This is to avoid + # slamming the database server with a monster query. + if format == 'rss': query.SetLimit(cfg.cvsdb.rss_row_limit) + else: + query.SetLimit(cfg.cvsdb.row_limit) # run the query db.RunQuery(query) - - sql = request.server.escape(db.CreateSQLQueryString(query)) - + commit_list = query.GetCommitList() + row_limit_reached = query.GetLimitReached() + # gather commits commits = [] plus_count = 0 minus_count = 0 mod_time = -1 - if query.commit_list: + if commit_list: files = [] limited_files = 0 - current_desc = query.commit_list[0].GetDescriptionID() - current_rev = query.commit_list[0].GetRevision() - current_repo = query.commit_list[0].GetRepository() + current_desc = commit_list[0].GetDescriptionID() + current_rev = commit_list[0].GetRevision() + current_repo = commit_list[0].GetRepository() dir_strip = _path_join(repos_dir) - for commit in query.commit_list: + for commit in commit_list: commit_desc = commit.GetDescriptionID() commit_rev = commit.GetRevision() commit_repo = commit.GetRepository() @@ -4331,10 +5155,10 @@ def view_query(request): return data = common_template_data(request) - data.merge(ezt.TemplateData({ - 'sql': sql, - 'repos_root': repos_root_t, - 'repos_type': repos_type, + data.merge(TemplateData({ + 'repos_root': request.server.escape(repos_root_t), + 'repos_type': request.server.escape(repos_type), + 'sql': request.server.escape(db.CreateSQLQueryString(query)), 'english_query': english_query(request), 'queryform_href': request.get_url(view_func=view_queryform, escape=1), 'querycvs_href': lookcvs_href, @@ -4349,6 +5173,7 @@ def view_query(request): 'show_branch': show_branch, 'querysort': querysort, 'commits': commits, + 'row_limit_reached' : ezt.boolean(row_limit_reached), 'limit_changes': limit_changes, 'limit_changes_href': limit_changes_href, 'rss_link_href': rss_link_href, @@ -4388,11 +5213,27 @@ def list_roots(request): for root in cfg.general.svn_roots.keys(): auth = setup_authorizer(cfg, request.username, root) try: - vclib.svn.SubversionRepository(root, cfg.general.svn_roots[root], auth, - cfg.utilities, cfg.options.svn_config_dir) + repos = vclib.svn.SubversionRepository(root, cfg.general.svn_roots[root], + auth, cfg.utilities, + cfg.options.svn_config_dir) + lastmod = None + if cfg.options.show_roots_lastmod: + try: + repos.open() + youngest_rev = repos.youngest + date, author, msg, revprops, changes = repos.revinfo(youngest_rev) + date_str = make_time_string(date, cfg) + ago = html_time(request, date) + lf = LogFormatter(request, msg) + log = lf.get(maxlen=0, htmlize=1) + short_log = lf.get(maxlen=cfg.options.short_log_len, htmlize=1) + lastmod = _item(ago=ago, author=author, date=date_str, log=log, + short_log=short_log, rev=str(youngest_rev)) + except: + lastmod = None except vclib.ReposNotFound: continue - allroots[root] = [cfg.general.svn_roots[root], 'svn'] + allroots[root] = [cfg.general.svn_roots[root], 'svn', lastmod] # Add the viewable CVS roots for root in cfg.general.cvs_roots.keys(): @@ -4402,10 +5243,38 @@ def list_roots(request): cfg.utilities, cfg.options.use_rcsparse) except vclib.ReposNotFound: continue - allroots[root] = [cfg.general.cvs_roots[root], 'cvs'] - + allroots[root] = [cfg.general.cvs_roots[root], 'cvs', None] + return allroots +def expand_root_parents(cfg): + """Expand the configured root parents into individual roots.""" + + # Each item in root_parents is a "directory : repo_type" string. + for pp in cfg.general.root_parents: + pos = pp.rfind(':') + if pos < 0: + raise debug.ViewVCException( + 'The path "%s" in "root_parents" does not include a ' + 'repository type. Expected "cvs" or "svn".' % (pp)) + + repo_type = pp[pos+1:].strip() + pp = os.path.normpath(pp[:pos].strip()) + + if repo_type == 'cvs': + roots = vclib.ccvs.expand_root_parent(pp) + if cfg.options.hide_cvsroot and roots.has_key('CVSROOT'): + del roots['CVSROOT'] + cfg.general.cvs_roots.update(roots) + elif repo_type == 'svn': + roots = vclib.svn.expand_root_parent(pp) + cfg.general.svn_roots.update(roots) + else: + raise debug.ViewVCException( + 'The path "%s" in "root_parents" has an unrecognized ' + 'repository type ("%s"). Expected "cvs" or "svn".' + % (pp, repo_type)) + def find_root_in_parents(cfg, rootname, roottype): """Return the rootpath for configured ROOTNAME of ROOTTYPE.""" @@ -4414,26 +5283,23 @@ def find_root_in_parents(cfg, rootname, roottype): return None, None for pp in cfg.general.root_parents: - pos = string.rfind(pp, ':') + pos = pp.rfind(':') if pos < 0: continue - repo_type = string.strip(pp[pos+1:]) + repo_type = pp[pos+1:].strip() if repo_type != roottype: continue - pp = os.path.normpath(string.strip(pp[:pos])) - + pp = os.path.normpath(pp[:pos].strip()) + + rootpath = None if roottype == 'cvs': - roots = vclib.ccvs.expand_root_parent(pp) + rootpath = vclib.ccvs.find_root_in_parent(pp, rootname) 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 + rootpath = vclib.svn.find_root_in_parent(pp, rootname) + + if rootpath is not None: + return rootpath + return None def locate_root(cfg, rootname): """Return a 3-tuple ROOTTYPE, ROOTPATH, ROOTNAME for configured ROOTNAME. @@ -4459,14 +5325,35 @@ def locate_root(cfg, rootname): return None, None, None def load_config(pathname=None, server=None): + """Load the ViewVC configuration file. SERVER is the server object + that will be using this configuration. Consult the environment for + the variable VIEWVC_CONF_PATHNAME and VIEWCVS_CONF_PATHNAME (its + legacy name) and, if set, use its value as the path of the + configuration file; otherwise, use PATHNAME (if provided). Failing + all else, use a hardcoded default configuration path.""" + debug.t_start('load-config') - if pathname is None: - pathname = (os.environ.get("VIEWVC_CONF_PATHNAME") - or os.environ.get("VIEWCVS_CONF_PATHNAME") - or os.path.join(os.path.dirname(os.path.dirname(__file__)), - "viewvc.conf")) + # See if the environment contains overrides to the configuration + # path. If we have a SERVER object, consult its environment; use + # the OS environment otherwise. + env_pathname = None + if server is not None: + env_pathname = (server.getenv("VIEWVC_CONF_PATHNAME") + or server.getenv("VIEWCVS_CONF_PATHNAME")) + else: + env_pathname = (os.environ.get("VIEWVC_CONF_PATHNAME") + or os.environ.get("VIEWCVS_CONF_PATHNAME")) + # Try to find the configuration pathname by searching these ordered + # locations: the environment, the passed-in PATHNAME, the hard-coded + # default. + pathname = (env_pathname + or pathname + or os.path.join(os.path.dirname(os.path.dirname(__file__)), + "viewvc.conf")) + + # Load the configuration! cfg = config.Config() cfg.set_defaults() cfg.load_config(pathname, server and server.getenv("HTTP_HOST")) @@ -4477,7 +5364,7 @@ def load_config(pathname=None, server=None): if cfg.general.mime_types_files: files = cfg.general.mime_types_files[:] files.reverse() - files = map(lambda x: os.path.join(os.path.dirname(pathname), x), files) + files = map(lambda x, y=pathname: os.path.join(os.path.dirname(y), x), files) mimetypes.init(files) debug.t_end('load-config') @@ -4524,8 +5411,3 @@ def main(server, cfg): debug.t_end('main') debug.t_dump(server.file()) debug.DumpChildren(server) - - -class _item: - def __init__(self, **kw): - vars(self).update(kw) diff --git a/lib/win32popen.py b/lib/win32popen.py index 281b43cc..f210008e 100644 --- a/lib/win32popen.py +++ b/lib/win32popen.py @@ -1,6 +1,6 @@ # -*-python-*- # -# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -14,7 +14,7 @@ # # ----------------------------------------------------------------------- -import os, sys, traceback, string, thread +import os, sys, traceback, thread try: import win32api except ImportError, e: @@ -40,9 +40,9 @@ def CommandLine(command, args): """Convert an executable path and a sequence of arguments into a command line that can be passed to CreateProcess""" - cmd = "\"" + string.replace(command, "\"", "\"\"") + "\"" + cmd = "\"" + command.replace("\"", "\"\"") + "\"" for arg in args: - cmd = cmd + " \"" + string.replace(arg, "\"", "\"\"") + "\"" + cmd = cmd + " \"" + arg.replace("\"", "\"\"") + "\"" return cmd def CreateProcess(cmd, hStdInput, hStdOutput, hStdError): @@ -109,13 +109,13 @@ def CreatePipe(readInheritable, writeInheritable): def File2FileObject(pipe, mode): """Make a C stdio file object out of a win32 file handle""" - if string.find(mode, 'r') >= 0: + if mode.find('r') >= 0: wmode = os.O_RDONLY - elif string.find(mode, 'w') >= 0: + elif mode.find('w') >= 0: wmode = os.O_WRONLY - if string.find(mode, 'b') >= 0: + if mode.find('b') >= 0: wmode = wmode | os.O_BINARY - if string.find(mode, 't') >= 0: + if mode.find('t') >= 0: wmode = wmode | os.O_TEXT return os.fdopen(msvcrt.open_osfhandle(pipe.Detach(),wmode),mode) diff --git a/lib/PyFontify.py b/misc/PyFontify.py similarity index 100% rename from lib/PyFontify.py rename to misc/PyFontify.py diff --git a/misc/elemx/Makefile b/misc/elemx/Makefile new file mode 100644 index 00000000..cb8a8f38 --- /dev/null +++ b/misc/elemx/Makefile @@ -0,0 +1,54 @@ +TARGETS = python/elx-python java/elx-java + +all : $(TARGETS) + +CFLAGS = -g -Wpointer-arith -Wwrite-strings -Wshadow -Wall +CPPFLAGS = -Ipython -Ijava -I. + +# the scanner depends on tokens generated from python.y +python/scanner.c : python/python.c + +# the keywords also need the tokens in python.h +python/py_keywords.c : python/python.c + +# we need the scanner tokens in py_scan.h and keywords in py_keywords.h +python/elx-python.o : python/elx-python.c python/py_keywords.c + +# we need java.[ch] generated first to get the tokens in java.h +java/j_scan.c : java/java.c java/j_keywords.c + +# the keywords also need the tokens in java.h +java/j_keywords.c : java/java.c + +# we need the scanner tokens in j_scan.h and keywords in j_keywords.h +java/elx-java.o : java/elx-java.c java/j_keywords.c java/java.c + + +python/elx-python : python/elx-python.o python/scanner.o python/python.o \ + python/py_keywords.o elx-common.o + $(CC) -o $@ $^ + +java/elx-java : java/elx-java.o java/j_scan.o java/java.o java/j_keywords.o \ + elx-common.o + $(CC) -o $@ $^ + +clean : + rm -f *.o + rm -f python/*.o python/python.[ch] python/py_keywords.[ch] + rm -f java/*.o java/java.[ch] java/j_keywords.[ch] java/j_scan.[ch] + rm -f python/*.output java/*.output + rm -f $(TARGETS) + +.SUFFIXES: +.SUFFIXES: .c .y .shilka .o + +%.c : %.y + @d="`echo $@ | sed 's/\.c//'`" ; \ + echo msta -d -enum -v -o $$d $< ; \ + msta -d -enum -v -o $$d $< + +%.c : %.shilka + @d="`echo $@ | sed 's,/[^/]*$$,,'`" ; \ + f="`echo $< | sed 's,.*/,,'`" ; \ + echo shilka -length -no-definitions -interface $$f ; \ + (cd $$d && shilka -length -no-definitions -interface $$f) diff --git a/misc/elemx/elx-common.c b/misc/elemx/elx-common.c new file mode 100644 index 00000000..2392f4ac --- /dev/null +++ b/misc/elemx/elx-common.c @@ -0,0 +1,110 @@ +#include +#include +#include +#include + +#include "elx.h" + +#define ELX_ELEMS_EXT ".elx" +#define ELX_SYMBOLS_EXT ".els" + + +static void usage(const char *progname) +{ + fprintf(stderr, "USAGE: %s FILENAME\n", progname); + exit(1); +} + +static const char * build_one(const char *base, int len, const char *suffix) +{ + int slen = strlen(suffix); + char *fn; + + fn = malloc(len + slen + 1); + memcpy(fn, base, len); + memcpy(fn + len, suffix, slen); + fn[len + slen] = '\0'; + + return fn; +} + +elx_context_t *elx_process_args(int argc, const char **argv) +{ + elx_context_t *ec; + const char *input_fn; + const char *p; + int len; + + /* ### in the future, we can expand this for more options */ + + if (argc != 2) + { + usage(argv[0]); + /* NOTREACHED */ + } + + input_fn = argv[1]; + + p = strrchr(input_fn, '.'); + if (p == NULL) + len = strlen(input_fn); + else + len = p - argv[1]; + + ec = malloc(sizeof(*ec)); + ec->input_fn = input_fn; + ec->elx_fn = build_one(input_fn, len, ELX_ELEMS_EXT); + ec->sym_fn = build_one(input_fn, len, ELX_SYMBOLS_EXT); + + return ec; +} + +void elx_open_files(elx_context_t *ec) +{ + const char *fn; + const char *op; + + if ((ec->input_fp = fopen(ec->input_fn, "r")) == NULL) + { + fn = ec->input_fn; + op = "reading"; + goto error; + } + if ((ec->elx_fp = fopen(ec->elx_fn, "w")) == NULL) + { + fn = ec->elx_fn; + op = "writing"; + goto error; + } + if ((ec->sym_fp = fopen(ec->sym_fn, "w")) == NULL) + { + fn = ec->sym_fn; + op = "writing"; + goto error; + } + return; + + error: + fprintf(stderr, "ERROR: file \"%s\" could not be opened for %s.\n %s\n", + fn, op, strerror(errno)); + exit(2); +} + +void elx_close_files(elx_context_t *ec) +{ + fclose(ec->input_fp); + fclose(ec->elx_fp); + fclose(ec->sym_fp); +} + +void elx_issue_token(elx_context_t *ec, + char which, int start, int len, + const char *symbol) +{ + fprintf(ec->elx_fp, "%c %d %d\n", which, start, len); + + if (ELX_DEFINES_SYM(which)) + { + fprintf(ec->sym_fp, "%s %d %s\n", symbol, start, ec->input_fn); + } +} diff --git a/misc/elemx/elx.h b/misc/elemx/elx.h new file mode 100644 index 00000000..7f519689 --- /dev/null +++ b/misc/elemx/elx.h @@ -0,0 +1,54 @@ +#ifndef ELX_H +#define ELX_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +#define ELX_COMMENT 'C' /* a comment */ +#define ELX_STRING 'S' /* a string constant */ +#define ELX_KEYWORD 'K' /* a language keyword */ +#define ELX_GLOBAL_FDEF 'F' /* function defn in global (visible) scope */ +#define ELX_LOCAL_FDEF 'L' /* function defn in local (hidden) scope */ +#define ELX_METHOD_DEF 'M' /* method definition */ +#define ELX_FUNC_REF 'R' /* function reference / call */ + +#define ELX_DEFINES_SYM(c) ((c) == ELX_GLOBAL_FDEF || (c) == ELX_LOCAL_FDEF \ + || (c) == ELX_METHOD_DEF) + + +typedef struct +{ + /* input filename */ + const char *input_fn; + + /* output filenames: element extractions, and symbols */ + const char *elx_fn; + const char *sym_fn; + + /* file pointers for each of the input/output files */ + FILE *input_fp; + FILE *elx_fp; + FILE *sym_fp; + +} elx_context_t; + +elx_context_t *elx_process_args(int argc, const char **argv); + +void elx_open_files(elx_context_t *ec); +void elx_close_files(elx_context_t *ec); + + +void elx_issue_token(elx_context_t *ec, + char which, int start, int len, + const char *symbol); + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* ELX_H */ diff --git a/misc/elemx/elx_html.py b/misc/elemx/elx_html.py new file mode 100644 index 00000000..ecfd3341 --- /dev/null +++ b/misc/elemx/elx_html.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# +# generate HTML given an input file and an element file +# + +import re +import string +import cgi +import struct + +_re_elem = re.compile('([a-zA-Z]) ([0-9]+) ([0-9]+)\n') + +CHUNK_SIZE = 98304 # 4096*24 + + +class ElemParser: + "Parse an elements file, extracting the token type, start, and length." + + def __init__(self, efile): + self.efile = efile + + def get(self): + line = self.efile.readline() + if not line: + return None, None, None + t, s, e = string.split(line) + return t, int(s)-1, int(e) + + def unused_get(self): + record = self.efile.read(9) + if not record: + return None, None, None + return struct.unpack('>cii', record) + + +class Writer: + "Generate output, including copying from another input." + + def __init__(self, ifile, ofile): + self.ifile = ifile + self.ofile = ofile + + self.buf = ifile.read(CHUNK_SIZE) + self.offset = 0 + + def write(self, data): + self.ofile.write(data) + + def copy(self, pos, amt): + "Copy 'amt' bytes from position 'pos' of input to output." + idx = pos - self.offset + self.ofile.write(cgi.escape(buffer(self.buf, idx, amt))) + amt = amt - (len(self.buf) - idx) + while amt > 0: + self._more() + self.ofile.write(cgi.escape(buffer(self.buf, 0, amt))) + amt = amt - len(self.buf) + + def flush(self, pos): + "Flush the rest of the input to the output." + idx = pos - self.offset + self.ofile.write(cgi.escape(buffer(self.buf, idx))) + while 1: + buf = self.ifile.read(CHUNK_SIZE) + if not buf: + break + self.ofile.write(cgi.escape(buf)) + + def _more(self): + self.offset = self.offset + len(self.buf) + self.buf = self.ifile.read(CHUNK_SIZE) + + +def generate(input, elems, output, genpage=0): + ep = ElemParser(elems) + w = Writer(input, output) + cur = 0 + if genpage: + w.write('''\ +ELX Output Page + + + +''') + w.write('
')
+  while 1:
+    type, start, length = ep.get()
+    if type is None:
+      break
+    if cur < start:
+      # print out some plain text up to 'cur'
+      w.copy(cur, start - cur)
+
+    # wrap a bit o' formatting here
+    w.write('' % type)
+
+    # copy over the token
+    w.copy(start, length)
+
+    # and close up the formatting
+    w.write('')
+
+    cur = start + length
+
+  # all done.
+  w.flush(cur)
+  w.write('
') + if genpage: + w.write('\n') + +if __name__ == '__main__': + import sys + generate(open(sys.argv[1]), open(sys.argv[2]), sys.stdout, 1) diff --git a/misc/elemx/elx_page.sh b/misc/elemx/elx_page.sh new file mode 100755 index 00000000..7608bedd --- /dev/null +++ b/misc/elemx/elx_page.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +if test "$#" != 2; then + echo "USAGE: $0 SOURCE-FILE ELX-FILE" + exit 1 +fi + +cat <ELX Output Page + + + +EOF + +dirname="`dirname $0`" +python2 $dirname/elx_html.py $1 $2 + +echo "" diff --git a/misc/elemx/java/elx-java.c b/misc/elemx/java/elx-java.c new file mode 100644 index 00000000..e007a89e --- /dev/null +++ b/misc/elemx/java/elx-java.c @@ -0,0 +1,148 @@ +#include +#include + +#include "java.h" +#include "j_keywords.h" +#include "elx.h" + +/* from j_scan.c */ +extern int yylex(void); +extern void yylex_start(int *error_flag); +extern void yylex_finish(void); +extern const char *get_identifier(void); + +static const char *fname; +static int saw_error = 0; + +static int lineno = 1; +static int hpos = 1; +static int fpos = 0; + +static int token_start = 0; +static int start_lineno; +static int start_hpos; + +static elx_context_t *ectx; + + +//#define DEBUG_SCANNER + +/* if we're debugging, then the scanner looks for this var */ +int yysdebug = 0; + +/* and the parser looks for this */ +int yydebug = 1; + + +void yyserror(const char *msg) +{ + fprintf(stderr, "%s:%d:%d: lex error: %s\n", + fname, start_lineno, start_hpos, msg); + saw_error = 1; +} + +void yyerror(const char *msg) +{ + fprintf(stderr, "%s:%d:%d: parse error: %s\n", + fname, start_lineno, start_hpos, msg); + saw_error = 1; +} + +int yyslex(void) +{ + int c = fgetc(ectx->input_fp); + + if (c == EOF) + return -1; /* tell lexer we're done */ + + ++fpos; + if (c == '\n') + { + hpos = 1; + ++lineno; + } + else + ++hpos; + +// printf("char: '%c'\n", c); + + return c; +} + +void issue_token(char which) +{ + const char *ident = NULL; + + if (ELX_DEFINES_SYM(which)) + ident = get_identifier(); + else + ident = NULL; + + elx_issue_token(ectx, which, token_start, fpos - token_start + 1, ident); +} + +void mark_token_start(void) +{ + token_start = fpos; + start_lineno = lineno; + start_hpos = hpos; +} + +#ifdef DEBUG_SCANNER + +void gen_scan_tokens(void) +{ + while (1) + { + int v = yylex(); + + if (v == TK_IDENTIFIER) + printf("%d-%d: %d '%s'\n", + token_start, fpos-1, v, get_identifier()); + else + printf("%d-%d: %d\n", token_start, fpos-1, v); + + /* end of parse? */ + if (v <= 0) + break; + } +} + +#else /* DEBUG_SCANNER */ + +static void gen_elx_tokens(void) +{ + /* ### what to do with the result? should have seen/set saw_error */ + (void) yyparse(); +} + +#endif /* DEBUG_SCANNER */ + +int main(int argc, const char **argv) +{ + int errcode; + + ectx = elx_process_args(argc, argv); + + yylex_start(&errcode); + if (errcode) + { + fprintf(stderr, "error: yylex_start: %d\n", errcode); + return EXIT_FAILURE; + } + + elx_open_files(ectx); + +#ifdef DEBUG_SCANNER + gen_scan_tokens(); +#else + gen_elx_tokens(); +#endif + + yylex_finish(); + elx_close_files(ectx); + + if (saw_error) + return EXIT_FAILURE; + return EXIT_SUCCESS; +} diff --git a/misc/elemx/java/j_keywords.shilka b/misc/elemx/java/j_keywords.shilka new file mode 100644 index 00000000..d872402d --- /dev/null +++ b/misc/elemx/java/j_keywords.shilka @@ -0,0 +1,65 @@ +%local { +/* get the KR_* values */ +#include "java.h" +} + +%% +abstract +boolean +break +byte +/* byvalue */ +case +/* cast */ +catch +char +class +/* const */ +continue +default +do +double +else +extends +false +final +finally +float +for +/* future */ +/* generic */ +/* goto */ +if +implements +import +/* inner */ +instanceof +int +interface +long +native +new +null +/* operator */ +/* outer */ +package +private +protected +public +/* rest */ +return +short +static +super +switch +synchronized +this +throw +throws +transient +true +try +/* var */ +void +volatile +while diff --git a/misc/elemx/java/j_scan.y b/misc/elemx/java/j_scan.y new file mode 100644 index 00000000..116ab0ce --- /dev/null +++ b/misc/elemx/java/j_scan.y @@ -0,0 +1,135 @@ +%start token +%scanner + +%local { +#include "elx.h" + +/* from elx-java.c */ +void yyserror(const char *msg); +int yyslex(void); + +/* for the TK_ symbols, generated from java.y */ +#include "java.h" + +/* for keyword recognition */ +#include "j_keywords.h" + +extern void issue_token(char which); +extern void mark_token_start(void); + +#define MAX_IDENT 200 +static int idlen; +static char identifier[MAX_IDENT+1]; +#define INIT_IDENT(c) (identifier[0] = (c), idlen = 1) +#define ADD_IDENT(c) if (idlen == MAX_IDENT) return E_IDENT_TOO_LONG; \ + else identifier[idlen++] = (c) + +/* ### is there a better place? */ +#define E_IDENT_TOO_LONG (-100) + +static int lookup(void); +} + + +%% + +token : pure_ws* { mark_token_start(); } slash_op + +slash_op : "/=" { return TK_OPERATOR; } + | comment token + | '/' { return TK_OPERATOR; } + | one_token + | + ; + +one_token : t_identifier { return lookup(); } + | t_literal { return TK_LITERAL; } + | t_operator { return TK_OPERATOR; } + | t_chars { return yysprev_char; } + | t_inc_dec { return TK_INC_DEC; } + | t_bracket + ; + +t_identifier : alpha { INIT_IDENT(yysprev_char); } + ( alphanum { ADD_IDENT(yysprev_char); } )* + +alpha : 'a' - 'z' | 'A' - 'Z' | '_' | '$' +alphanum : alpha | digit + +digit : '0' - '9' +hexdigit : digit | 'a' - 'f' | 'A' - 'F' +octal : '0' - '7' + +t_literal : number | string | char_constant + +number : ('1' - '9') digit* decimal_suffix + | '.' digit+ [exponent] [float_suffix] + | '0' (('x' | 'X') hexdigit+ | octal+) decimal_suffix + ; +decimal_suffix : ('.' digit* [exponent] [float_suffix]) + | 'l' | 'L' + | /* nothing */ + ; +exponent : ('e' | 'E') ['+' | '-'] digit+ +float_suffix : 'f' | 'F' | 'd' | 'D' + +string : '"' string_char* '"' { issue_token(ELX_STRING); } +string_char : '\1' -> '"' | '"' <-> '\\' | '\\' <- '\377' | '\\' '\1' - '\377' + +char_constant : '\'' one_char '\'' +one_char : '\1' -> '\'' | '\'' <-> '\\' | '\\' <- '\377' | '\\' '\1' - '\377' + +comment : ( "//" line_comment_char* '\n' + | "/*" (block_comment_char | '*' block_non_term_char)* "*/" + ) { issue_token(ELX_COMMENT); } + ; +line_comment_char : '\1' -> '\n' | '\n' <- '\377' +block_comment_char : '\1' -> '*' | '*' <- '\377' +block_non_term_char : '\1' -> '/' | '/' <- '\377' + +t_operator : "<<" | ">>" | ">>>" + | ">=" | "<=" | "==" | "!=" | "&&" | "||" + | "*=" | "%=" | "+=" | "-=" | "<<=" | ">>=" + | ">>>=" | "&=" | "^=" | "|=" + | '<' | '>' | '%' | '^' | '&' | '|' + ; +t_inc_dec : "++" | "--" + +/* note: could not use ws* ; the '[' form would only reduce on $end + rather than "any" character. that meant we could not recognize '[' + within the program text. separating out the cases Does The Right + Thing */ +t_bracket : '[' { return '['; } + | '[' ']' { return TK_DIM; } + | '[' ws+ ']' { return TK_DIM; } + ; + +t_chars : ',' | ';' | '.' | '{' | '}' | '=' | '(' | ')' | ':' + | ']' | '!' | '~' | '+' | '-' | '*' | '?' + ; + +ws : pure_ws | comment + +pure_ws : ' ' | '\t' | '\n' | '\f' + +%% + +static int lookup(void) +{ + int kw = KR_find_keyword(identifier, idlen); + + if (kw == KR__not_found) + { + /* terminate so user can grab an identifier string */ + identifier[idlen] = '\0'; + return TK_IDENTIFIER; + } + + issue_token(ELX_KEYWORD); + return kw; +} + +const char *get_identifier(void) +{ + return identifier; +} diff --git a/misc/elemx/java/java.y b/misc/elemx/java/java.y new file mode 100644 index 00000000..1e23177a --- /dev/null +++ b/misc/elemx/java/java.y @@ -0,0 +1,458 @@ +%token KR_abstract +%token KR_boolean KR_break KR_byte /* KR_byvalue */ +%token KR_case /* KR_cast */ KR_catch KR_char KR_class /* KR_const */ KR_continue +%token KR_default KR_do KR_double +%token KR_else KR_extends +%token KR_false KR_final KR_finally KR_float KR_for /* KR_future */ +/* %token KR_generic KR_goto */ +%token KR_if KR_implements KR_import /* KR_inner */ KR_instanceof KR_int KR_interface +%token KR_long +%token KR_native KR_new KR_null +/* %token KR_operator KR_outer */ +%token KR_package KR_private KR_protected KR_public +%token /* KR_rest */ KR_return +%token KR_short KR_static KR_super KR_switch KR_synchronized +%token KR_this KR_throw KR_throws KR_transient KR_true KR_try +%token /* KR_var */ KR_void KR_volatile +%token KR_while + +%token TK_OP_ASSIGN TK_OPERATOR TK_IDENTIFIER TK_LITERAL +%token TK_DIM TK_INC_DEC + +%start CompilationUnit + +/* the standard if/then/else conflict */ +/* %expect 1 */ + +%{ +#include "elx.h" + +void yyerror(const char *msg); +int yylex(void); + +/* ### should come from an elx-python.h or something */ +void issue_token(char which); +%} + +%export { +/* the main parsing function */ +int yyparse(void); + +/* need to define the 'not found' in addition to the regular keywords */ +#define KR__not_found 0 +} + +%% + +TypeSpecifier + : TypeName + | TypeNameDims + ; + +TypeNameDims + : TypeName TK_DIM+ + ; + +TypeNameDot + : NamePeriod + | PrimitiveType '.' + ; + +TypeName + : PrimitiveType + | NamePeriod TK_IDENTIFIER + | TK_IDENTIFIER + ; + +NamePeriod + : TK_IDENTIFIER '.' + | NamePeriod TK_IDENTIFIER '.' + ; + +TypeNameList + : TypeName / ',' + ; + +PrimitiveType + : KR_boolean + | KR_byte + | KR_char + | KR_double + | KR_float + | KR_int + | KR_long + | KR_short + | KR_void + ; + +CompilationUnit + : PackageStatement [ImportStatements] [TypeDeclarations] + | ImportStatements [TypeDeclarations] + | TypeDeclarations + ; + +PackageStatement + : KR_package (TK_IDENTIFIER / '.') ';' + ; + +TypeDeclarations + : TypeDeclaration+ + ; + +TypeDeclaration + : ClassDeclaration + | InterfaceDeclaration + ; + +ImportStatements + : ImportStatement+ + ; + +ImportStatement + : KR_import TK_IDENTIFIER ('.' TK_IDENTIFIER)* [".*"] ';' + ; +/* +QualifiedName + : TK_IDENTIFIER / '.' + ; +*/ +ClassDeclaration + : [Modifiers] KR_class TK_IDENTIFIER [Super] [Interfaces] ClassBody + ; + +Modifiers + : Modifier+ + ; + +Modifier + : KR_abstract + | KR_final + | KR_public + | KR_protected + | KR_private + | KR_static + | KR_transient + | KR_volatile + | KR_native + | KR_synchronized + ; + +Super + : KR_extends TypeNameList + ; + +Interfaces + : KR_implements TypeNameList + ; + +ClassBody + : '{' FieldDeclaration* '}' + ; + +FieldDeclaration + : FieldVariableDeclaration + | MethodDeclaration + | ConstructorDeclaration + | StaticInitializer + ; + +FieldVariableDeclaration + : [Modifiers] TypeSpecifier VariableDeclarators ';' + ; + +VariableDeclarators + : VariableDeclarator / ',' + ; + +VariableDeclarator + : DeclaratorName ['=' VariableInitializer] + ; + +VariableInitializer + : Expression + | '{' [ArrayInitializers] '}' + ; + +ArrayInitializers + : VariableInitializer ( ',' [VariableInitializer] )* + ; + +MethodDeclaration + : [Modifiers] TypeSpecifier MethodDeclarator [Throws] MethodBody + ; + +MethodDeclarator + : DeclaratorName '(' [ParameterList] ')' TK_DIM* + ; + +ParameterList + : Parameter / ',' + ; + +Parameter + : TypeSpecifier DeclaratorName + ; + +DeclaratorName + : TK_IDENTIFIER TK_DIM* + ; + +Throws + : KR_throws TypeNameList + ; + +MethodBody + : Block + | ';' + ; + +ConstructorDeclaration + : [Modifiers] ConstructorDeclarator [Throws] Block + ; + +ConstructorDeclarator + : TypeName '(' [ParameterList] ')' + ; + +StaticInitializer + : KR_static Block + ; + +InterfaceDeclaration + : [Modifiers] KR_interface TK_IDENTIFIER [ExtendsInterfaces] InterfaceBody + ; + +ExtendsInterfaces + : KR_extends TypeNameList + ; + +InterfaceBody + : '{' FieldDeclaration+ '}' + ; + +Block + : '{' LocalVariableDeclarationOrStatement* '}' + ; + +LocalVariableDeclarationOrStatement + : LocalVariableDeclarationStatement + | Statement + ; + +LocalVariableDeclarationStatement + : TypeSpecifier VariableDeclarators ';' + ; + +Statement + : EmptyStatement + | LabeledStatement + | ExpressionStatement ';' + | SelectionStatement + | IterationStatement + | JumpStatement + | GuardingStatement + | Block + ; + +EmptyStatement + : ';' + ; + +LabeledStatement + : TK_IDENTIFIER ':' LocalVariableDeclarationOrStatement + | KR_case ConstantExpression ':' LocalVariableDeclarationOrStatement + | KR_default ':' LocalVariableDeclarationOrStatement + ; + +ExpressionStatement + : Expression + ; + +SelectionStatement + : KR_if '(' Expression ')' Statement [KR_else Statement] + | KR_switch '(' Expression ')' Block + ; + +IterationStatement + : KR_while '(' Expression ')' Statement + | KR_do Statement KR_while '(' Expression ')' ';' + | KR_for '(' ForInit ForExpr [ForIncr] ')' Statement + ; + +ForInit + : ExpressionStatements ';' + | LocalVariableDeclarationStatement + | ';' + ; + +ForExpr + : [Expression] ';' + ; + +ForIncr + : ExpressionStatements + ; + +ExpressionStatements + : ExpressionStatement / ',' + ; + +JumpStatement + : KR_break [TK_IDENTIFIER] ';' + | KR_continue [TK_IDENTIFIER] ';' + | KR_return [Expression] ';' + | KR_throw Expression ';' + ; + +GuardingStatement + : KR_synchronized '(' Expression ')' Statement + | KR_try Block Finally + | KR_try Block Catches + | KR_try Block Catches Finally + ; + +Catches + : Catch+ + ; + +Catch + : KR_catch '(' TypeSpecifier [TK_IDENTIFIER] ')' Block + ; + +Finally + : KR_finally Block + ; + +ArgumentList + : Expression / ',' + ; + +PrimaryExpression + : TK_LITERAL + | KR_true | KR_false + | KR_this + | KR_null + | KR_super + | '(' Expression ')' + ; + +PostfixExpression + : PrimaryExpression Trailers + + | TypeName AltTrailers + | TypeNameDot FollowsPeriod Trailers + | TypeNameDot DimAllocation TypeTrailers + | TypeNameDims TypeTrailers + + | KR_new TypeName AltTrailers + | KR_new TypeNameDims TypeTrailers + ; + +DimAllocation + : KR_new TypeNameDims + ; + +PostfixDims + : TK_DIM+ '.' KR_class + ; + +FollowsPeriod + : KR_this + | KR_class + | KR_super + | KR_new TypeName NoPeriodsTrailer + ; + +NoPeriodsTrailer + : '[' Expression ']' + | '(' [ArgumentList] ')' + ; + +NoDimTrailer + : NoPeriodsTrailer + | '.' (FollowsPeriod | TK_IDENTIFIER) + ; + +AnyTrailer + : NoDimTrailer + | PostfixDims + ; + +Trailers + : (AnyTrailer | DimAllocation NoDimTrailer)* [DimAllocation] + ; + +AltTrailers + : NoPeriodsTrailer Trailers + | + ; + +TypeTrailers + : NoDimTrailer Trailers + | + ; + +CastablePrefixExpression + : PostfixExpression [TK_INC_DEC] + | LogicalUnaryOperator CastExpression + ; + +LogicalUnaryOperator + : '~' + | '!' + ; + +UnaryOperator + : '+' + | '-' + | TK_INC_DEC + ; + +/* note: we don't actually have grammar for a cast. we just rely on: + (expr) (argument) + as our parse match */ +CastExpression + : /* '(' PrimitiveType ')' CastExpression + | '(' NamePeriod TK_IDENTIFIER TK_DIM TK_DIM* ')' CastablePrefixExpression + | '(' NamePeriod TK_IDENTIFIER ')' CastablePrefixExpression + | '(' TK_IDENTIFIER TK_DIM TK_DIM* ')' CastablePrefixExpression + | '(' TK_IDENTIFIER ')' CastablePrefixExpression + | */ UnaryOperator CastExpression + | CastablePrefixExpression + ; + +BinaryExpression + : CastExpression + | BinaryExpression TK_BINARY CastExpression + | BinaryExpression KR_instanceof TypeSpecifier + ; + +ConditionalExpression + : BinaryExpression + | BinaryExpression '?' Expression ':' ConditionalExpression + ; + +AssignmentExpression + : ConditionalExpression [AssignmentOperator AssignmentExpression] + ; + +AssignmentOperator + : '=' + | TK_OP_ASSIGN + ; + +Expression + : AssignmentExpression + ; + +ConstantExpression + : ConditionalExpression + ; + +/* +TK_OPERATOR : OP_LOR | OP_LAND + | OP_EQ | OP_NE | OP_LE | OP_GE + | OP_SHL | OP_SHR | OP_SHRR + ; +*/ +TK_BINARY : TK_OPERATOR | '+' | '-' | '*' diff --git a/misc/elemx/python/elx-python.c b/misc/elemx/python/elx-python.c new file mode 100644 index 00000000..ef31eea0 --- /dev/null +++ b/misc/elemx/python/elx-python.c @@ -0,0 +1,150 @@ +#include +#include + +#include "scanner.h" +#include "python.h" +#include "py_keywords.h" +#include "elx.h" + +extern int yylex(void); + +static const char *fname; +static int saw_error = 0; +static void *scan_ctx; +static elx_context_t *ectx; + +void yyerror(const char *msg) +{ + int sl, sc, el, ec; + + scanner_token_linecol(scan_ctx, &sl, &sc, &el, &ec); + fprintf(stderr, "%s:%d:%d: parse error: %s\n", fname, sl, sc, msg); + saw_error = 1; +} + +int reader(void *user_ctx) +{ + FILE *inf = user_ctx; + int c = fgetc(inf); + + if (c == EOF) + return SCANNER_EOF; + +// printf("char: '%c'\n", c); + + return c; +} + +void issue_token(char which) +{ + int start; + int end; + const char *ident = NULL; + + scanner_token_range(scan_ctx, &start, &end); + + if (ELX_DEFINES_SYM(which)) + { + int length; + + scanner_identifier(scan_ctx, &ident, &length); + } + + elx_issue_token(ectx, which, start, end - start + 1, ident); +} + +int yylex(void) +{ + int v; + + do { + v = scanner_get_token(scan_ctx); + + if (v == TK_COMMENT) + { + issue_token(ELX_COMMENT); + } + } while (v == TK_COMMENT); + + /* is this identifier a keyword? */ + if (v == TK_IDENTIFIER) + { + const char *ident; + int length; + int kw; + + scanner_identifier(scan_ctx, &ident, &length); +#if 0 + printf("id=%s\n", ident); +#endif + + kw = KR_find_keyword(ident, length); + if (kw != KR__not_found) + { + v = kw; + issue_token(ELX_KEYWORD); + } + } + else if (v == TK_STRING) + { + issue_token(ELX_STRING); + } + +// printf("token=%d\n", v); + + return v; +} + +#ifdef DEBUG_SCANNER + +void gen_scan_tokens(void) +{ + while (1) + { + int v = scanner_get_token(scan_ctx); + int sl, sc, el, ec; + + scanner_token_linecol(scan_ctx, &sl, &sc, &el, &ec); + if (v == TK_NEWLINE) + printf("%d,%d: NEWLINE\n", sl, sc); + else if (v == TK_INDENT) + printf("%d,%d: INDENT\n", el, ec); + else if (v == TK_DEDENT) + printf("%d,%d: DEDENT\n", el, ec); + else + printf("%d,%d-%d,%d: %d\n", sl, sc, el, ec, v); + + /* end of parse? */ + if (v <= 0) + break; + } +} + +#endif /* DEBUG_SCANNER */ + +static void gen_elx_tokens(void) +{ + /* ### what to do with the result? should have seen/set saw_error */ + (void) yyparse(); +} + +int main(int argc, const char **argv) +{ + ectx = elx_process_args(argc, argv); + elx_open_files(ectx); + + scan_ctx = scanner_begin(reader, ectx->input_fp); + +#ifdef DEBUG_SCANNER + gen_scan_tokens(); +#else + gen_elx_tokens(); +#endif + + scanner_end(scan_ctx); + elx_close_files(ectx); + + if (saw_error) + return EXIT_FAILURE; + return EXIT_SUCCESS; +} diff --git a/misc/elemx/python/py_keywords.shilka b/misc/elemx/python/py_keywords.shilka new file mode 100644 index 00000000..fc6e7381 --- /dev/null +++ b/misc/elemx/python/py_keywords.shilka @@ -0,0 +1,36 @@ +%local { +/* get the KR_* values */ +#include "python.h" +} + +%% +and +/* as */ +assert +break +class +continue +def +del +elif +else +except +exec +finally +for +from +global +if +import +in +is +lambda +not +or +pass +print +raise +return +try +while +yield diff --git a/misc/elemx/python/python.y b/misc/elemx/python/python.y new file mode 100644 index 00000000..000cd8a9 --- /dev/null +++ b/misc/elemx/python/python.y @@ -0,0 +1,135 @@ + +%token TK_COMMENT TK_IDENTIFIER TK_NUMBER +%token TK_OPERATOR TK_STRING +%token TK_INDENT TK_DEDENT TK_NEWLINE + +%token KR_and KR_assert KR_break KR_class KR_continue KR_def +%token KR_del KR_elif KR_else KR_except KR_exec KR_finally +%token KR_for KR_from KR_global KR_if KR_import KR_in KR_is +%token KR_lambda KR_not KR_or KR_pass KR_print KR_raise +%token KR_return KR_try KR_while KR_yield + +%start file_input + +%{ +#include "elx.h" + +void yyerror(const char *msg); +int yylex(void); + +/* ### should come from an elx-python.h or something */ +void issue_token(char which); +%} + +%export { +/* the main parsing function */ +int yyparse(void); + +/* need to define the 'not found' in addition to the regular keywords */ +#define KR__not_found 0 +} + +%% + +file_input: (TK_NEWLINE | stmt)* + +NAME: TK_IDENTIFIER + +funcdef: KR_def NAME { issue_token(ELX_LOCAL_FDEF); } parameters ':' suite +parameters: '(' [varargslist] ')' +varargslist: paramdef (',' paramdef)* [',' [varargsdef]] + | varargsdef + ; +/* the TK_OPERATOR represents '*' or '**' */ +varargsdef: TK_OPERATOR NAME [',' TK_OPERATOR NAME] +paramdef: fpdef [TK_OPERATOR test] +fpdef: NAME | '(' fplist ')' +fplist: fpdef (',' fpdef)* [','] + +stmt: simple_stmt | compound_stmt +simple_stmt: small_stmt (';' small_stmt)* [';'] TK_NEWLINE +small_stmt: expr_stmt | print_stmt | raise_stmt + | import_stmt | global_stmt | exec_stmt | assert_stmt + | KR_del exprlist + | KR_pass + | KR_break + | KR_continue + | KR_return [testlist] + | KR_yield testlist + ; + +/* expr_stmt is normally assignment, which we get thru TK_OPERATOR in 'expr' */ +expr_stmt: testlist + +/* a print normally allows '>> test'; since that is a TK_OPERATOR, we + get it as part of 'factor'. this rule also allows for a trailing + comma in '>> test,' which the normal print doesn't */ +print_stmt: KR_print [test (',' test)* [',']] + +raise_stmt: KR_raise [test [',' test [',' test]]] + +/* the TK_OPERATOR represents '*' */ +import_stmt: KR_import dotted_as_name (',' dotted_as_name)* + | KR_from dotted_name KR_import (TK_OPERATOR | import_as_name (',' import_as_name)*) +import_as_name: NAME [NAME NAME] +dotted_as_name: dotted_name [NAME NAME] +dotted_name: NAME ('.' NAME)* +global_stmt: KR_global NAME (',' NAME)* +exec_stmt: KR_exec expr [KR_in test [',' test]] +assert_stmt: KR_assert test [',' test] + +compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef +if_stmt: KR_if test ':' suite (KR_elif test ':' suite)* [KR_else ':' suite] +while_stmt: KR_while test ':' suite [KR_else ':' suite] +for_stmt: KR_for exprlist KR_in testlist ':' suite [KR_else ':' suite] +try_stmt: KR_try ':' suite (except_clause ':' suite)+ + [KR_else ':' suite] | KR_try ':' suite KR_finally ':' suite +/* NB compile.c makes sure that the default except clause is last */ +except_clause: KR_except [test [',' test]] +suite: simple_stmt | TK_NEWLINE TK_INDENT stmt+ TK_DEDENT + +test: test_factor (test_op test_factor | KR_is [KR_not] factor)* + [TK_OPERATOR lambdef] | lambdef +test_op: bin_op | KR_in +test_factor: KR_not* factor + +expr: factor (expr_op factor)* +expr_op: bin_op | KR_is [KR_not] + +factor: TK_OPERATOR* atom trailer* + +bin_op: TK_OPERATOR | KR_or | KR_and | KR_not KR_in + +atom: '(' [testlist] ')' | '[' [listmaker] ']' | '{' [dictmaker] '}' + | '`' testlist_no_trailing '`' | TK_IDENTIFIER | TK_NUMBER | TK_STRING+ +listmaker: test ( list_for | (',' test)* [','] ) +lambdef: KR_lambda [varargslist] ':' test +trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME +subscriptlist: subscript (',' subscript)* [','] +subscript: '.' '.' '.' | test | [test] ':' [test] [sliceop] +sliceop: ':' [test] +exprlist: expr (',' expr)* [','] +testlist: test (',' test)* [','] +testlist_no_trailing: test (',' test)* +testlist_safe: test [(',' test)+ [',']] /* doesn't match: test, */ +dictmaker: test ':' test (',' test ':' test)* [','] + +classdef: KR_class NAME ['(' testlist ')'] ':' suite + +/* arguments are normally 'keyword = test; since '=' is TK_OPERATOR, we + match keyword arguments as part of 'test' (in 'expr'). + + the vararg portion is normally '* test' or '** test'; since '*' and + '**' are TK_OPERATOR, we match varargs as part of 'test' (in + 'factor') + + thus, all argument forms are simply 'test' + + varargs does not normally allow a trailing comma, but we can + simplify things and allow a match +*/ +arglist: test (',' test)* [','] + +list_iter: list_for | list_if +list_for: KR_for exprlist KR_in testlist_safe [list_iter] +list_if: KR_if test [list_iter] diff --git a/misc/elemx/python/scanner.c b/misc/elemx/python/scanner.c new file mode 100644 index 00000000..6c9e8b7d --- /dev/null +++ b/misc/elemx/python/scanner.c @@ -0,0 +1,523 @@ + +#include +#include +#include +#include + +#include "python.h" /* get the TK_ values */ +#include "scanner.h" + +#define SCANNER_EMPTY (SCANNER_EOF - 1) /* -2 */ +#define SCANNER_TABSIZE 8 +#define SCANNER_MAXINDENT 100 +#define SCANNER_MAXIDLEN 200 + +typedef struct +{ + get_char_t getfunc; + void *user_ctx; + + char saved; + int was_newline; /* was previous character a newline? */ + + int start; /* start position of last token returned */ + int start_col; + int start_line; + + int fpos; /* file position */ + int lineno; /* file line number */ + int line_pos; /* file position of current line's first char */ + + int nesting_level; + + int indent; /* which indent */ + int indents[SCANNER_MAXINDENT]; /* the set of indents */ + + int dedent_count; /* how many DEDENTs to issue */ + + int skip_newline; /* skip the newline after a blank_line + comment */ + + int idlen; + char identifier[SCANNER_MAXIDLEN]; /* accumulated identifier */ + +} scanner_ctx; + + +static int next_char(scanner_ctx *ctx) +{ + int c; + + ++ctx->fpos; + + if (ctx->saved == SCANNER_EMPTY) + { + return (*ctx->getfunc)(ctx->user_ctx); + } + + c = ctx->saved; + ctx->saved = SCANNER_EMPTY; + return c; +} + +static void backup_char(scanner_ctx *ctx, int c) +{ + assert(ctx->saved == SCANNER_EMPTY); + ctx->saved = c; + ctx->was_newline = 0; /* we may have put it back */ + --ctx->fpos; +} + +/* called to note that we've moved on to another line */ +static void on_next_line(scanner_ctx *ctx) +{ + ctx->line_pos = ctx->fpos; + ++ctx->lineno; +} + +void *scanner_begin(get_char_t getfunc, void *user_ctx) +{ + scanner_ctx *ctx = malloc(sizeof(*ctx)); + + memset(ctx, 0, sizeof(*ctx)); + ctx->getfunc = getfunc; + ctx->user_ctx = user_ctx; + ctx->saved = SCANNER_EMPTY; + ctx->lineno = 1; + + return ctx; +} + +int scanner_get_token(void *opaque_ctx) +{ + scanner_ctx *ctx = opaque_ctx; + int c; + int c2; + int blank_line; + + if (ctx->dedent_count) + { + --ctx->dedent_count; + return TK_DEDENT; + } + + nextline: + blank_line = 0; + /* if we're at the start of the line, then get the indentation level */ + if (ctx->fpos == ctx->line_pos) + { + int col = 0; + + while (1) + { + c = next_char(ctx); + if (c == ' ') + ++col; + else if (c == '\t') + col = (col / SCANNER_TABSIZE + 1) * SCANNER_TABSIZE; + else if (c == '\f') /* ^L / formfeed */ + col = 0; + else + break; + } + backup_char(ctx, c); + + if (c == '#' || c == '\n') + { + /* this is a "blank" line and doesn't count towards indentation, + and it doesn't produce NEWLINE tokens */ + blank_line = 1; + } + + /* if it isn't blank, and we aren't inside nesting expressions, then + we need to handle INDENT/DEDENT */ + if (!blank_line && ctx->nesting_level == 0) + { + int last_indent = ctx->indents[ctx->indent]; + + if (col == last_indent) + { + /* no change */ + } + else if (col > last_indent) + { + if (ctx->indent == SCANNER_MAXINDENT - 1) + { + /* oops. too deep. */ + return E_TOO_MANY_INDENTS; + } + ctx->indents[++ctx->indent] = col; + return TK_INDENT; + } + else /* col < last_indent */ + { + /* find the previous indentation that matches this one */ + while (ctx->indent > 0 + && col < ctx->indents[ctx->indent]) + { + ++ctx->dedent_count; + --ctx->indent; + } + if (col != ctx->indents[ctx->indent]) + { + /* oops. dedent doesn't match any indent. */ + return E_DEDENT_MISMATCH; + } + + /* deliver one dedent now */ + --ctx->dedent_count; + return TK_DEDENT; + } + } /* !blank_line ... */ + } /* start of line */ + + /* start here if we see a line continuation */ + read_more: + + do { + c = next_char(ctx); + } while (c == ' ' || c == '\t' || c == '\f'); + + /* here is where the token starts */ + ctx->start = ctx->fpos; + ctx->start_line = ctx->lineno; + ctx->start_col = ctx->fpos - ctx->line_pos; + + /* comment? */ + if (c == '#') + { + do { + c = next_char(ctx); + } while (c != SCANNER_EOF && c != '\n'); + + /* if we are suppressing newlines because this is a blank line, then + leave a marker to skip the newline, next time through. */ + if (blank_line && c == '\n') + ctx->skip_newline = 1; + + /* put back whatever we sucked up */ + backup_char(ctx, c); + + return TK_COMMENT; + } + + /* Look for an identifier */ + if (isalpha(c) || c == '_') + { + ctx->idlen = 0; + + /* is this actually a string? */ + if (c == 'r' || c == 'R') + { + ctx->identifier[ctx->idlen++] = c; + c = next_char(ctx); + if (c == '"' || c == '\'') + goto parse_string; + } + else if (c == 'u' || c == 'U') + { + ctx->identifier[ctx->idlen++] = c; + c = next_char(ctx); + if (c == 'r' || c == 'R') + { + ctx->identifier[ctx->idlen++] = c; + c = next_char(ctx); + } + if (c == '"' || c == '\'') + goto parse_string; + } + + while (isalnum(c) || c == '_') { + /* store the character if there is room for it, and room left + for a null-terminator. */ + if (ctx->idlen < SCANNER_MAXIDLEN-1) + ctx->identifier[ctx->idlen++] = c; + c = next_char(ctx); + } + backup_char(ctx, c); + + /* ### check for a keyword */ + return TK_IDENTIFIER; + } + + if (c == '\n') + { + on_next_line(ctx); + + /* don't report NEWLINE tokens for blank lines or nested exprs */ + if (blank_line || ctx->nesting_level > 0 || ctx->skip_newline) + { + ctx->skip_newline = 0; + goto nextline; + } + + return TK_NEWLINE; + } + + if (c == '.') + { + c = next_char(ctx); + if (isdigit(c)) + goto parse_fraction; + backup_char(ctx, c); + return '.'; + } + + if (isdigit(c)) + { + if (c == '0') + { + c = next_char(ctx); + if (c == 'x' || c == 'X') + { + do { + c = next_char(ctx); + } while (isxdigit(c)); + goto skip_fp; + } + else if (isdigit(c)) + { + do { + c = next_char(ctx); + } while (isdigit(c)); + } + if (c == '.') + goto parse_fraction; + if (c == 'e' || c == 'E') + goto parse_exponent; + if (c == 'j' || c == 'J') + goto parse_imaginary; + skip_fp: + /* this point: parsed an octal, decimal, or hexadecimal */ + + if (c == 'l' || c == 'L') + { + /* we consumed just enough. stop and return a NUMBER */ + return TK_NUMBER; + } + + /* consumed too much. backup and return a NUMBER */ + backup_char(ctx, c); + return TK_NUMBER; + } + + /* decimal number */ + do { + c = next_char(ctx); + } while (isdigit(c)); + + if (c == 'l' || c == 'L') + { + /* we consumed just enogh. stop and return a NUMBER */ + return TK_NUMBER; + } + + if (c == '.') + { + parse_fraction: + do { + c = next_char(ctx); + } while (isdigit(c)); + } + + if (c == 'e' || c == 'E') + { + parse_exponent: + c = next_char(ctx); + if (c == '+' || c == '-') + c = next_char(ctx); + if (!isdigit(c)) + { + backup_char(ctx, c); + return E_BAD_NUMBER; + } + do { + c = next_char(ctx); + } while (isdigit(c)); + } + + if (c == 'j' || c == 'J') + { + parse_imaginary: + c = next_char(ctx); + } + + /* one too far. backup and return a NUMBER */ + backup_char(ctx, c); + return TK_NUMBER; + + } /* isdigit */ + +parse_string: + if (c == '\'' || c == '"') + { + int second_quote_pos = ctx->fpos + 1; + int which_quote = c; + int is_triple = 0; + int quote_count = 0; + + while (1) + { + c = next_char(ctx); + if (c == '\n') + { + on_next_line(ctx); + + if (!is_triple) + return E_UNTERM_STRING; + quote_count = 0; + } + else if (c == SCANNER_EOF) + { + return E_UNTERM_STRING; + } + else if (c == which_quote) + { + ++quote_count; + if (ctx->fpos == second_quote_pos) + { + c = next_char(ctx); + if (c == which_quote) + { + is_triple = 1; + quote_count = 0; + continue; + } + /* we just read one past the empty string. back up. */ + backup_char(ctx, c); + } + + /* this quote may have terminated the string */ + if (!is_triple || quote_count == 3) + return TK_STRING; + } + else if (c == '\\') + { + c = next_char(ctx); + if (c == SCANNER_EOF) + return E_UNTERM_STRING; + if (c == '\n') + on_next_line(ctx); + quote_count = 0; + } + else + { + quote_count = 0; + } + } + + /* NOTREACHED */ + } + + /* line continuation */ + if (c == '\\') + { + c = next_char(ctx); + if (c != '\n') + return E_BAD_CONTINUATION; + + on_next_line(ctx); + goto read_more; + } + + /* look for operators */ + + /* the nesting operators */ + if (c == '(' || c == '[' || c == '{') + { + ++ctx->nesting_level; + return c; + } + if (c == ')' || c == ']' || c == '}') + { + --ctx->nesting_level; + return c; + } + + /* look for up-to-3-char ops */ + if (c == '<' || c == '>' || c == '*' || c == '/') + { + c2 = next_char(ctx); + if (c == c2) + { + c2 = next_char(ctx); + if (c2 != '=') + { + /* oops. one too far. */ + backup_char(ctx, c2); + } + return TK_OPERATOR; + } + + if (c == '<' && c2 == '>') + return TK_OPERATOR; + + if (c2 != '=') + { + /* one char too far. */ + backup_char(ctx, c2); + } + return TK_OPERATOR; + } + + /* look for 2-char ops */ + if (c == '=' || c == '!' || c == '+' || c == '-' + || c == '|' || c == '%' || c == '&' || c == '^') + { + c2 = next_char(ctx); + if (c2 == '=') + return TK_OPERATOR; + + /* oops. too far. */ + backup_char(ctx, c2); + return TK_OPERATOR; + } + + /* ### should all of these return 'c' ? */ + if (c == ':' || c == ',' || c == ';' || c == '`') + return c; + + /* as a unary operator, this must be a TK_OPERATOR */ + if (c == '~') + return TK_OPERATOR; + + /* if we have an EOF, then just return it */ + if (c == SCANNER_EOF) + return SCANNER_EOF; + + /* unknown input */ + return E_UNKNOWN_TOKEN; +} + +void scanner_identifier(void *opaque_ctx, const char **ident, int *len) +{ + scanner_ctx *ctx = opaque_ctx; + + ctx->identifier[ctx->idlen] = '\0'; + *ident = ctx->identifier; + *len = ctx->idlen; +} + +void scanner_token_range(void *opaque_ctx, int *start, int *end) +{ + scanner_ctx *ctx = opaque_ctx; + + *start = ctx->start; + *end = ctx->fpos; +} + +void scanner_token_linecol(void *opaque_ctx, + int *sline, int *scol, int *eline, int *ecol) +{ + scanner_ctx *ctx = opaque_ctx; + + *sline = ctx->start_line; + *scol = ctx->start_col; + + *eline = ctx->lineno; + *ecol = ctx->fpos - ctx->line_pos; +} + +void scanner_end(void *ctx) +{ + free(ctx); +} diff --git a/misc/elemx/python/scanner.h b/misc/elemx/python/scanner.h new file mode 100644 index 00000000..864d51c8 --- /dev/null +++ b/misc/elemx/python/scanner.h @@ -0,0 +1,42 @@ +#ifndef SCANNER_H +#define SCANNER_H + + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +/* constants and errors returned by the scanner */ +enum +{ + SCANNER_EOF = -1, /* returned by get_char_t and + scanner_get_token to symbolize EOF */ + + E_TOO_MANY_INDENTS = -100, /* too many indents */ + E_DEDENT_MISMATCH, /* no matching indent */ + E_BAD_CONTINUATION, /* character occurred after \ */ + E_BAD_NUMBER, /* parse error in a number */ + E_UNKNOWN_TOKEN, /* dunno what we found */ + E_UNTERM_STRING /* unterminated string constant */ +}; + +typedef int (*get_char_t)(void *user_ctx); + +void *scanner_begin(get_char_t getfunc, void *user_ctx); + +int scanner_get_token(void *ctx); + +void scanner_identifier(void *ctx, const char **ident, int *len); +void scanner_token_range(void *ctx, int *start, int *end); +void scanner_token_linecol(void *ctx, + int *sline, int *scol, int *eline, int *ecol); + +void scanner_end(void *ctx); + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* SCANNER_H */ diff --git a/lib/py2html.py b/misc/py2html.py similarity index 100% rename from lib/py2html.py rename to misc/py2html.py diff --git a/misc/tparse/CHANGES b/misc/tparse/CHANGES new file mode 100644 index 00000000..b19f00e0 --- /dev/null +++ b/misc/tparse/CHANGES @@ -0,0 +1,50 @@ +Bugfix ( 19-Feb-2002 - Daniel Berlin) + * tparsemodule.cpp (tparse): Change tparseParser object to + non-pointer. + +Modif ( 13-Feb-2002) + * Removed debugging statments from the (C++) Sink class. + * Changed INSTALL to reflect the move of rcsparse to lib/vclib/ccvs/rcsparse. + * Now when tparse cannot import common.py, it fails. + * Added check for sink being of common.Sink class. + +Modif ( 11-Feb-2002) + * Support for parser exceptions ( RCS* ): + tparse will try import those exceptions from the "common" module. + If it fails, tparse create those exceptions inside itself. + * Changed the INSTALL file to make installation clearer. + +Modif ( 08-Feb-2002) + * Added Daniel Berlin's patch: + Use a buffer stdiobufstream to access a python File in C++. + Much faster. + +Modif ( 30-Jan-2002) + * Fixed compilation problem of revision 1.2 of tparser.cpp + * Streamlined some part of the code. ( Added returns here and there...) + +Modif ( 29-Jan-2002) + * Greg Fixed a problem with revisions' date. + * Greg Fixed a problem with the state ostrstream. ( added a space in the end) + +Modif ( 27-Jan-2002) + * Added Install file + * Added the list of the platforms on which tparse has been compiled ( compile farm of sourceforge.) + * Added lots of little variable ( __version__, etc...) +Modif ( 25-Jan-2002) + * renamed module as tparse + * wrote distutils Setup.py + * added inline (__doc__) documentation in python module. + +Modif ( 24-Jan-2002) + * Implementation of the python exceptions in the C++ code. + * Added an exception to stop the parser. + * Fixed bug that added a "@" in the end of string in certain cases. + +Modif ( 21-Jan-2002) + * Extensive testing of the memory leaks + * Started to write the python wrapper. (tparsemodule.cpp & tparsemodule.h) + +Creation ( 20-Jan-2002 ) + * Implementation of the Token parser in C++ ( tparse.cpp & tparse.h) + * Implementation of the parser itself in C++ \ No newline at end of file diff --git a/misc/tparse/INSTALL b/misc/tparse/INSTALL new file mode 100644 index 00000000..a15542d2 --- /dev/null +++ b/misc/tparse/INSTALL @@ -0,0 +1,26 @@ + HOW TO INSTALL tparse + + Quick install + ------------- + $ cd /tparse/ + $ python Setup.py build_ext + +Normally, you can find a tparse.so dynamic library, in a directory called +build/lib.****/ +where **** depends on OS. +cd into that directory. +now move the tparse.so in here into your lib/vclib/ccvs/rcsparse directory: + $ mv tparse.so /lib/vclib/ccvs/rcsparse/ + $ cd /lib/vclib/ccvs/rcsparse/ + + Check your install + ------------------ + +Then check if tparse has been correctly installed: +$ python +Python 2.1.2 (#1, Jan 21 2002, 04:12:22) +[GCC 2.96 20000731 (Mandrake Linux 8.1 2.96-0.62mdk)] on linux2 +Type "copyright", "credits" or "license" for more information. +>>> import tparse +>>> + Tparse is correctly installed!!! diff --git a/misc/tparse/README b/misc/tparse/README new file mode 100644 index 00000000..305b05f1 --- /dev/null +++ b/misc/tparse/README @@ -0,0 +1,15 @@ + TPARSE + + What is tparse ? + ---------------- +TPARSE is a C++ coded RCS file format parser with bindings for the Python scripting language. +It was originally designed after rcsparser.py from Greg Stein and blame.py from Curt Hagenlocher. + + What platforms does tparse support ? + ------------------------------------ +TPARSE has been successfully compiled and tested on the following platforms: + -freebsd-4.4-STABLE-i386-2.1 + -linux-alpha-1.5 + -linux-i686-1.5 + -linux-ppc-1.5 + -linux-sparc64-1.5 diff --git a/misc/tparse/Setup.py b/misc/tparse/Setup.py new file mode 100755 index 00000000..fa1b6fed --- /dev/null +++ b/misc/tparse/Setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from distutils.core import setup,Extension + +setup(name="tparse", + version="1.0", + description="A quick RCS file format parser", + author="Lucas Bruand", + author_email="lbruand@users.sourceforge.net", + url="http://viewvc.org", + ext_modules=[Extension("tparse", ["tparsemodule.cpp"],libraries=["stdc++"])] + ) diff --git a/misc/tparse/tparse.cpp b/misc/tparse/tparse.cpp new file mode 100644 index 00000000..2bb08220 --- /dev/null +++ b/misc/tparse/tparse.cpp @@ -0,0 +1,398 @@ +/* +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# This file has been rewritten in C++ from the rcsparse.py file by +# Lucas Bruand +# +# This file was originally based on portions of the blame.py script by +# Curt Hagenlocher. +# +# ----------------------------------------------------------------------- +*/ + +/* + This C++ library offers an API to a performance oriented RCSFILE parser. + It does little syntax checking. + + Version: $Id$ +*/ + +#include "tparse.h" + +#ifndef __USE_XOPEN +#define __USE_XOPEN +#endif +#include /* for strptime */ + + +using namespace std; + +#define Whitespace(c) (c == ' ' || c == '\t' || c == '\014' || c == '\n' || \ + c == '\r') +#define Token_term(c) (c == ' ' || c == '\t' || c == '\014' || c == '\n' || \ + c == '\r' || c == ';' || c == ':') +#define isdigit(c) ((c-'0')<10) + + + +void rcstoken::init(const char *mydata, size_t mylen) +{ + size = DEFAULT_TOKEN_SIZE; + length = 0; + delta = DEFAULT_TOKEN_DELTA; + data = NULL; + if (mydata && mylen) + append(mydata, mylen); +}; + +void rcstoken::append(const char *b, size_t b_len) +{ + if (b || b_len) + { + grow(length + b_len + 1); + memcpy(&data[length], b, b_len); + length += b_len; + data[length] = 0; + } +}; + +void rcstoken::grow(size_t new_size) +{ + if ((! data) || (new_size > size)) + { + while (new_size > size) + size += delta; + + data = (char*) realloc(data, size); + }; +}; + +rcstoken *rcstoken::copy_begin_end(size_t begin, size_t end) +{ + return new rcstoken(&data[begin], end - begin); +}; + +rcstoken *rcstoken::copy_begin_len(size_t begin, size_t len) +{ + return new rcstoken(&data[begin], len); +}; + + +/*--------- Tokenparser class -----------*/ +rcstoken *TokenParser::get(int allow_eof) +{ + auto_ptr token; + + if (backget) + { + token.reset(backget); + backget = NULL; + + return token.release(); + } + + token.reset(new rcstoken()); + while (1) + { + if (idx == buflength) + { + input->read(buf, CHUNK_SIZE); + if ( (buflength = input->gcount()) == 0 ) + { + if (allow_eof) + return token.release(); + else + throw RCSParseError("Unexpected end of file."); + }; + + idx = 0; + } + if (!Whitespace(buf[idx])) + break; + idx++; + } + + if (buf[idx] == ';' || buf[idx] == ':') + { + idx++; + (*token) = buf[idx]; + return token.release(); + } + + if (buf[idx] != '@') + { + int end = idx + 1; + + while (1) + { + while ( (end < buflength) && !(Token_term(buf[end])) ) + end++; + token->append(buf + idx, end - idx); + if (end < buflength) + { + idx = end; + return token.release(); + } + input->read(buf, CHUNK_SIZE); + buflength = input->gcount(); + idx = 0; + end = 0; + } + } + idx++; + + while (1) + { + int i; + + if (idx == buflength) + { + idx = 0; + input->read(buf, CHUNK_SIZE); + if ( (buflength = input->gcount()) == 0 ) + throw RCSIllegalCharacter("Unterminated string: @ missing!"); + } + //i=strchr(buf+idx,'@'); + for (i = idx;i < buflength && (buf[i] != '@');i++) + ; + if (i == buflength) + { + if ((buflength - idx) > 0) + token->append(buf + idx, buflength - idx); + idx = buflength; + continue; + } + if ( i == buflength - 1) + { + token->append(buf + idx, i - idx); + idx = 0; + buf[0] = '@'; + input->read(buf + 1, CHUNK_SIZE - 1); + if ( (buflength = input->gcount()) == 0 ) + throw RCSIllegalCharacter("Unterminated string: @ missing!"); + buflength++; + continue; + } + if (buf[i + 1] == '@') + { + token->append(buf + idx, i - idx + 1); + idx = i + 2; + continue; + } + if ((i - idx) > 0) + token->append(buf + idx, i - idx); + idx = i + 1; + return token.release(); + } +}; + +void TokenParser::unget(rcstoken *token) +{ + if (backget) + { + throw RCSParseError("Ungetting a token while already having " + "an ungetted token."); + } + backget = token; +} + +/*--------- tparseParser class -----------*/ +void tparseParser::parse_rcs_admin() +{ + while (1) + { + auto_ptr token(tokenstream->get(FALSE)); + + if (isdigit((*token)[0])) + { + tokenstream->unget(token.release()); + return; + } + if (*token == "head") + { + token.reset(tokenstream->get(FALSE)); + sink->set_head_revision(*token); + + tokenstream->match(';'); + continue; + } + if (*token == "branch") + { + token.reset(tokenstream->get(FALSE)); + if (*token != ';') + { + sink->set_principal_branch(*token); + + tokenstream->match(';'); + } + continue; + } + if (*token == "symbols") + { + while (1) + { + auto_ptr rev; + token.reset(tokenstream->get(FALSE)); + if (*token == ';') + break; + + tokenstream->match(':'); + rev.reset(tokenstream->get(FALSE)); + sink->define_tag(*token, *rev); + } + continue; + } + if (*token == "comment") + { + token.reset(tokenstream->get(FALSE)); + sink->set_comment((*token)); + + tokenstream->match(';'); + continue; + } + if (*token == "locks" || + *token == "strict" || + *token == "expand" || + *token == "access") + { + while (1) + { + token.reset(tokenstream->get(FALSE)); + if (*token == ';') + break; + } + continue; + } + } +}; + +void tparseParser::parse_rcs_tree() +{ + while (1) + { + auto_ptr revision, date, author, hstate, next; + long timestamp; + tokenlist branches; + struct tm tm; + + revision.reset(tokenstream->get(FALSE)); + if (*revision == "desc") + { + tokenstream->unget(revision.release()); + return; + } + + // Parse date + tokenstream->match("date"); + date.reset(tokenstream->get(FALSE)); + tokenstream->match(";"); + + memset ((void *) &tm, 0, sizeof(struct tm)); + if (strptime((*date).data, "%y.%m.%d.%H.%M.%S", &tm) == NULL) + strptime((*date).data, "%Y.%m.%d.%H.%M.%S", &tm); + timestamp = mktime(&tm); + + + tokenstream->match("author"); + author.reset(tokenstream->get(FALSE)); + tokenstream->match(';'); + + tokenstream->match("state"); + hstate.reset(new rcstoken()); + while (1) + { + auto_ptr token; + token.reset(tokenstream->get(FALSE)); + if (*token == ';') + break; + + if ((*hstate).length) + (*hstate) += ' '; + (*hstate) += *token; + } + + tokenstream->match("branches"); + while (1) + { + auto_ptr token; + token.reset(tokenstream->get(FALSE)); + if (*token == ';') + break; + + branches.push_front((*token)); + } + + tokenstream->match("next"); + next.reset(tokenstream->get(FALSE)); + if (*next == ';') + /* generate null token */ + next.reset(new rcstoken()); + else + tokenstream->match(';'); + + /* + * there are some files with extra tags in them. for example: + * owner 640; + * group 15; + * permissions 644; + * hardlinks @configure.in@; + * this is "newphrase" in RCSFILE(5). we just want to skip over these. + */ + while (1) + { + auto_ptr token; + token.reset(tokenstream->get(FALSE)); + + if ((*token == "desc") || isdigit((*token)[0]) ) + { + tokenstream->unget(token.release()); + break; + }; + + while (*token != ";") + token.reset(tokenstream->get(FALSE)); + } + + sink->define_revision(*revision, timestamp, *author, + *hstate, branches, *next); + } + return; +} + +void tparseParser::parse_rcs_description() +{ + auto_ptr token; + tokenstream->match("desc"); + + token.reset(tokenstream->get(FALSE)); + sink->set_description(*token); +} + +void tparseParser::parse_rcs_deltatext() +{ + auto_ptr revision, log, text; + + while (1) + { + revision.reset(tokenstream->get(TRUE)); + if ((*revision).null_token()) + break; + + tokenstream->match("log"); + log.reset(tokenstream->get(FALSE)); + + tokenstream->match("text"); + text.reset(tokenstream->get(FALSE)); + + sink->set_revision_info(*revision, *log, *text); + } + return; +} diff --git a/misc/tparse/tparse.h b/misc/tparse/tparse.h new file mode 100644 index 00000000..a98a90cc --- /dev/null +++ b/misc/tparse/tparse.h @@ -0,0 +1,306 @@ +/* +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# This file has been rewritten in C++ from the rcsparse.py file by +# Lucas Bruand +# +# This file was originally based on portions of the blame.py script by +# Curt Hagenlocher. +# +# ----------------------------------------------------------------------- +*/ + +/* + This C++ library offers an API to a performance-oriented RCSFILE parser. + It does little syntax checking. + + Version: $Id$ + */ + +#ifndef __PARSE_H +#define __PARSE_H +#include /* for auto_ptr */ +#include /* for iterator */ +#include /* for exception */ +#include /* for istream */ +#include /* for list<> */ +#include /* for string */ + + +#define CHUNK_SIZE 30000 +#define DEFAULT_TOKEN_SIZE 512 +#define DEFAULT_TOKEN_DELTA 10240 + +#ifndef FALSE +#define FALSE (0 != 0) +#endif + +#ifndef TRUE +#define TRUE (0 == 0) +#endif + +using namespace std; + +/* This class represents a exception that occured during the parsing + of a file */ + +class RCSParseError : public exception +{ + public: + string value; + RCSParseError() {}; + RCSParseError(const char *myvalue) + { + value = myvalue; + }; + virtual ~RCSParseError() throw() {}; +}; + +class RCSIllegalCharacter : public RCSParseError +{ + public: + RCSIllegalCharacter(const char *myvalue) + { + value = myvalue; + }; + virtual ~RCSIllegalCharacter() throw() {}; +}; + +class RCSExpected : public RCSParseError +{ + public: + string got; + string wanted; + RCSExpected(const char *mygot, const char *mywanted) + { + got = mygot; + wanted = mywanted; + }; + RCSExpected(const char *mygot, const char c) + { + got = mygot; + wanted = c; + }; + virtual ~RCSExpected() throw() {}; +}; + +class rcstoken +{ + public: + size_t length, size, delta; + char *data; + + public: + rcstoken(const char *mydata, size_t mylen) + { + init(mydata, mylen); + }; + rcstoken(const char *mydata) + { + init(mydata, strlen(mydata)); + }; + rcstoken(size_t mysize = DEFAULT_TOKEN_SIZE, + size_t mydelta = DEFAULT_TOKEN_DELTA) + { + data = NULL; + size = mysize; + length = 0; + delta = mydelta; + }; + ~rcstoken() + { + if (data) + free(data); + data = NULL; + }; + void init(const char *mydata, size_t mylen); + int null_token() + { + return data == NULL; + }; + rcstoken& operator=(const char b) + { + grow(2); + length = 1; + data[0] = b; + data[1] = 0; + + return *this; + }; + rcstoken& operator+=(const char b) + { + append(b); + + return *this; + }; + rcstoken& operator+=(rcstoken& token) + { + append(token); + + return *this; + }; + int operator==(const char *b) + { + size_t b_len; + return data && b && length == (b_len = strlen(b)) && + memcmp(data, b, (b_len tokenlist; +typedef tokenlist::iterator tokenlist_iter; + + + +/* This class is a handler that receive the event generated by the parser + i.e.: When we reach the head revision tag, etc... */ +class Sink +{ + public: + Sink() {}; + virtual ~Sink() throw () {}; + virtual void set_head_revision(rcstoken &revision) = 0; + virtual void set_principal_branch(rcstoken &branch_name) = 0; + virtual void define_tag(rcstoken &name, rcstoken &revision) = 0; + virtual void set_comment(rcstoken &comment) = 0; + virtual void set_description(rcstoken &description) = 0; + virtual void define_revision(rcstoken &revision, long timestamp, + rcstoken &author, rcstoken &state, + tokenlist &branches, rcstoken &next) = 0; + virtual void set_revision_info(rcstoken &revision, + rcstoken &log, rcstoken &text) = 0; + virtual void tree_completed() = 0; + virtual void parse_completed() = 0; +}; + +/* The class is used to get one by one every token in the file. */ +class TokenParser +{ + private: + istream *input; + char buf[CHUNK_SIZE]; + int buflength; + int idx; + rcstoken *backget; + public: + rcstoken *get(int allow_eof); + void unget(rcstoken *token); + int eof() + { + return (input->gcount() == 0); + }; + void match(const char *token) + { + auto_ptr ptr(get(FALSE)); + if (*ptr != token) + throw RCSExpected(ptr->data, token); + } + void match(const char c) + { + auto_ptr token(get(FALSE)); + + if ((*token) != c) + throw RCSExpected(token->data, c); + }; + TokenParser(istream *myinput) + { + input = myinput; + backget = NULL; + idx = 0; + input->read(buf, CHUNK_SIZE); + if ( (buflength = input->gcount()) == 0 ) + throw RCSParseError("Non-existing file or empty file"); + }; + ~TokenParser() + { + if (input != NULL) + { + delete input; + input = NULL; + }; + if (backget != NULL) + { + delete backget; + backget = NULL; + }; + }; +}; + +/* this is the class that does the actual job: by reading each part of + the file and thus generate events to a sink event-handler*/ +class tparseParser +{ + private: + TokenParser *tokenstream; + Sink *sink; + void parse_rcs_admin(); + void parse_rcs_tree(); + void parse_rcs_description(); + void parse_rcs_deltatext(); + public: + tparseParser(istream *myinput, Sink* mysink) + { + sink = mysink; + tokenstream = new TokenParser(myinput); + } + void parse() + { + parse_rcs_admin(); + parse_rcs_tree(); + + // many sinks want to know when the tree has been completed so they can + // do some work to prepare for the arrival of the deltatext + sink->tree_completed(); + + parse_rcs_description(); + parse_rcs_deltatext(); + // easiest for us to tell the sink it is done, rather than worry about + // higher level software doing it. + sink->parse_completed(); + } + ~tparseParser() + { + delete tokenstream; + delete sink; + } +}; + +#endif /* __PARSE_H */ diff --git a/misc/tparse/tparsemodule.cpp b/misc/tparse/tparsemodule.cpp new file mode 100644 index 00000000..8bc28755 --- /dev/null +++ b/misc/tparse/tparsemodule.cpp @@ -0,0 +1,296 @@ +/* +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# This file has been rewritten in C++ from the rcsparse.py file by +# Lucas Bruand +# +# This file was originally based on portions of the blame.py script by +# Curt Hagenlocher. +# +# ----------------------------------------------------------------------- +*/ + +/* + This python extension module is a binding to the tparse library. + tparse is a C++ library that offers an API to a performance-oriented + RCSFILE parser. It does little syntax checking. + + Version: $Id$ +*/ + +#include + +#include "tparsemodule.h" +#include "tparse.cpp" + +#if (__GNUC__ >= 4) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1) +#include // for auto_ptr +#include +typedef __gnu_cxx::stdio_filebuf stdio_filebuf; +#define GNUC_STDIO_FILEBUF_AVAILABLE +#endif + +using namespace std; + +class PythonException +{ + public: + PythonException() {}; +}; + + +class pyobject +{ + private: + PyObject *obj; + public: + pyobject(PyObject *myobj) + { + obj = myobj; + } + ~pyobject() + { + Py_XDECREF(obj); + }; + PyObject *operator*() + { + return obj; + }; +}; + + +class pystring : public pyobject +{ +public: + pystring(const char *s) : + pyobject(PyString_FromString(s)) + {}; + pystring(rcstoken& t) : + pyobject(PyString_FromStringAndSize(t.data, t.length)) + {}; +}; + + +static +void chkpy(PyObject *obj) +{ + Py_XDECREF(obj); + if (!obj) + throw PythonException(); +}; + + + +static PyMethodDef tparseMethods[] = { + {"parse", tparse, METH_VARARGS, tparse__doc__}, + {NULL, NULL} /* Sentinel */ +}; + +void inittparse() +{ + PyObject *m, *d, *common, *commondict; + pystring ver(__version__), + dat(__date__), + aut(__author__); + m = Py_InitModule3("tparse", tparseMethods, __doc__); + + common = PyImport_ImportModule("common"); + if (!common) + return ; // Common not imported ? + + commondict = PyModule_GetDict(common); + pyRCSStopParser = PyDict_GetItemString(commondict, "RCSStopParser"); + Py_INCREF(pyRCSStopParser); + + pyRCSParseError = PyDict_GetItemString(commondict, "RCSParseError"); + Py_INCREF(pyRCSParseError); + + pyRCSIllegalCharacter = PyDict_GetItemString(commondict, + "RCSIllegalCharacter"); + Py_INCREF(pyRCSIllegalCharacter); + + pyRCSExpected = PyDict_GetItemString(commondict, "RCSExpected"); + Py_INCREF(pyRCSExpected); + + PySink = PyDict_GetItemString(commondict, "Sink"); + Py_INCREF(PySink); + + d = PyModule_GetDict(m); + + PyDict_SetItemString(d, "__version__", *ver); + PyDict_SetItemString(d, "__date__", *dat); + PyDict_SetItemString(d, "__author__", *aut); +} + +class PythonSink : public Sink +{ + public: + PyObject *sink; + PythonSink(PyObject *mysink) + { + sink = mysink; + Py_INCREF(sink); + }; + virtual ~PythonSink() throw () + { + Py_DECREF(sink); + }; + virtual void set_head_revision(rcstoken &revision) + { + chkpy(PyObject_CallMethod(sink, "set_head_revision", "s", + revision.data)); + }; + virtual void set_principal_branch(rcstoken &branch_name) + { + chkpy(PyObject_CallMethod(sink, "set_principal_branch", + "s", branch_name.data)); + }; + virtual void define_tag(rcstoken &name, rcstoken &revision) + { + chkpy(PyObject_CallMethod(sink, "define_tag", "ss", + name.data, revision.data)); + }; + virtual void set_comment(rcstoken &comment) + { + pystring c(comment); + chkpy(PyObject_CallMethod(sink, "set_comment", "S", *c)); + }; + virtual void set_description(rcstoken &description) + { + pystring d(description); + chkpy(PyObject_CallMethod(sink, "set_description", "S", *d)); + }; + virtual void define_revision(rcstoken &revision, long timestamp, + rcstoken &author, rcstoken &state, + tokenlist &branches, rcstoken &next) + { + pyobject branchlist(PyList_New(0)); + tokenlist_iter branch; + + for (branch = branches.begin(); branch != branches.end(); branch++) + { + pystring str(*branch); + PyList_Append(*branchlist, *str); + } + + chkpy(PyObject_CallMethod(sink, "define_revision", "slssOs", + revision.data,timestamp, + author.data,state.data,*branchlist, + next.data)); + }; + virtual void set_revision_info(rcstoken& revision, + rcstoken& log, rcstoken& text) + { + pystring l(log), txt(text); + chkpy(PyObject_CallMethod(sink, "set_revision_info", "sSS", + revision.data, *l, *txt)); + }; + virtual void tree_completed() + { + chkpy(PyObject_CallMethod(sink, "tree_completed", NULL)); + }; + virtual void parse_completed() + { + chkpy(PyObject_CallMethod(sink, "parse_completed", NULL)); + }; +}; + +static PyObject * tparse( PyObject *self, PyObject *args) +{ + char *filename; + istream *input = NULL; + PyObject *file = NULL; + PyObject *hsink; + PyObject *rv = Py_None; +#ifdef GNUC_STDIO_FILEBUF_AVAILABLE + auto_ptr rdbuf; +#endif + + if (PyArg_ParseTuple(args, "sO!", &filename, &PyInstance_Type, &hsink)) + input = new ifstream(filename, ios::in); + else if (PyArg_ParseTuple(args, "O!O!", &PyFile_Type, &file, + &PyInstance_Type, &hsink)) + { + PyErr_Clear(); // Reset the exception PyArg_ParseTuple has raised. +#ifdef GNUC_STDIO_FILEBUF_AVAILABLE + rdbuf.reset(new stdio_filebuf(PyFile_AsFile(file), ios::in | ios::binary)); + input = new istream(rdbuf.get()); +#else + PyErr_SetString(PyExc_NotImplementedError, + "tparse only implements the parsing of filehandles " + "when compiled with GNU C++ version 3.1 or later - " + "please pass a filename instead"); + return NULL; +#endif + } + else + return NULL; + + + + if (!PyObject_IsInstance(hsink, PySink)) + { + PyErr_SetString(PyExc_TypeError, + "Sink has to be an instance of class Sink."); + return NULL; + } + + try + { + tparseParser tp(input, new PythonSink(hsink)); + tp.parse(); + } + catch (RCSExpected e) + { + const char *got = e.got.c_str(); + const char *wanted = e.wanted.c_str(); + + pyobject arg(Py_BuildValue("(ss)", got, wanted)), + exp(PyInstance_New(pyRCSExpected, *arg, NULL)); + PyErr_SetObject(pyRCSExpected, *exp); + + delete [] got; + delete [] wanted; + rv = NULL; + } + catch (RCSIllegalCharacter e) + { + const char *value = e.value.c_str(); + + pyobject arg(Py_BuildValue("(s)", value)), + exp(PyInstance_New(pyRCSIllegalCharacter,*arg, NULL)); + PyErr_SetObject(pyRCSIllegalCharacter, *exp); + + delete [] value; + rv = NULL; + } + catch (RCSParseError e) + { + const char *value = e.value.c_str(); + + pyobject arg(Py_BuildValue("(s)", value)), + exp(PyInstance_New(pyRCSParseError, *arg, NULL)); + PyErr_SetObject(pyRCSParseError, *exp); + + delete [] value; + rv = NULL; + } + catch (PythonException e) + { + if (! PyErr_ExceptionMatches(pyRCSStopParser)) + rv = NULL; + else + PyErr_Clear(); + } + + Py_XINCREF(rv); + return rv; +}; diff --git a/misc/tparse/tparsemodule.h b/misc/tparse/tparsemodule.h new file mode 100644 index 00000000..8236610a --- /dev/null +++ b/misc/tparse/tparsemodule.h @@ -0,0 +1,75 @@ +/* +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# This file has been rewritten in C++ from the rcsparse.py file by +# Lucas Bruand +# +# This file was originally based on portions of the blame.py script by +# Curt Hagenlocher. +# +# ----------------------------------------------------------------------- +*/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include + + static char *__doc__ = \ + "This python extension module is a binding to the tparse library.\n" \ + "tparse is a C++ library that offers an API to a performance-oriented\n" \ + "RCSFILE parser.\n" \ + "It does little syntax checking.\n" \ + "\n" \ + "Version: $Id$\n"; + static char *__version__ = "0.14"; + static char *__date__ = "2002/02/11"; + static char *__author__ = "Lucas Bruand "; + + //static char *pyRCSStopParser__doc__ = + // "Stop parser exception: to be raised from the sink to abort parsing."; + static PyObject *pyRCSStopParser; + + //static char *pyRCSParseError__doc__ = + // "Ancestor Exception"; + static PyObject *pyRCSParseError; + + //static char *pyRCSIllegalCharacter__doc__ = + // "Parser has encountered an Illegal Character."; + static PyObject *pyRCSIllegalCharacter; + + //static char *pyRCSExpected__doc__ = + // "Parse has found something but the expected."; + static PyObject *pyRCSExpected; + static PyObject *PySink; // Sink Class from the common module. + + static char *tparse__doc__ = \ + "Main function: Parse a file and send the result to the sink.\n" \ + "Two ways of invoking this function from python:\n" \ + "* tparse.parse(filename, sink)\n" \ + "where filename is a string and sink is an instance of the class Sink\n" \ + "defined in the common.py module.\n" \ + "* tparse.parse(file, sink)\n" \ + "where file is a python file and sink is an instance of the class Sink\n" \ + "defined in the common.py module.\n"; + static PyObject * tparse( PyObject *self, PyObject *args); + + /* Init function for this module: Invoked when the module is + imported from Python Load the stopparser expression into the + tparser's namespace */ + void inittparse(); +#ifdef __cplusplus +} +#endif + + diff --git a/notes/releases.txt b/notes/releases.txt new file mode 100644 index 00000000..8663efff --- /dev/null +++ b/notes/releases.txt @@ -0,0 +1,159 @@ + RELEASE MANAGEMENT + +ViewVC rolls releases from release branches associate with each minor +version of the software. For example, the 1.1.0 is rolled from the +1.1.x branch. The same is true for the 1.1.1, 1.1.2, ... releases. + + +A. Creating Release Branches +============================ + +Primary ViewVC development occurs on the trunk, with bugfixes and +compatible features being backported to release branches as +appropriate. When, however, the need arises to create a new release +branch, here's the process (M, N, X, and Y below represent integral +major, minor, and patch version numbers, and are not literal): + +1. Create the release branch as a copy of the trunk@HEAD (the + lower-case "x" in the branch name is literal): + + svn cp -m "Branch for X.Y release stabilization." . ^/branches/X.Y.x + +2. On the trunk, update the following files to reflect the new + version which trunk will be progressing towards: + + CHANGES: Add stub section for new release. + INSTALL: Update example configuration. + lib/viewvc.py: Update "__version__" value. + docs/upgrading-howto.html: Add stub section for new release. + docs/template-authoring-guide.html: Update to reflect new release. + docs/release-notes/M.N.0.html: Add a new stub file. + + Commit these changes: + + svn ci -m "Trunk is now progressing toward version M.N." + + +B. Publishing Releases +====================== + +There is a script, `tools/make-release', which creates a release +directory and the various archive files that we distribute. All other +steps required to get a ViewVC release out of the door require manual +execution (currently by C. Michael Pilato). Those steps are as +follows: + +Checkout a working copy of the release branch for the release you +intend to roll, and in that working copy, perform the following steps +(X, Y, and Z below represent integral major, minor, and patch version +numbers, and are not literal): + +1. Review any open bug reports: + + http://viewvc.tigris.org/servlets/ProjectIssues + +2. Ensure that the file 'docs/upgrading.html' describes all user + visible changes for users of previous releases of ViewVC. (Any + changes here should be made on the trunk and backported to the + branch.) NOTE: This step should not be necessary for patch + releases. + +3. Verify that copyright years are correct in both the license-1.html + file and the source code. + +4. Update and commit the 'CHANGES' file, using any available crystal + balls or other forward-looking devices to take a stab at the + release date. + +5. Test, test, test! There is no automatic testsuite available. So + just run with permuting different `viewvc.conf' settings... and + pray. Fix what needs fixin', keeping the CHANGES file in sync + with the branch. + +6. At this point, the source code committed to the release branch + should exactly reflect what you wish to distribute and dub "the + release". + +7. Update your release branch working copy to HEAD. + + svn up + +8. Edit the file 'lib/viewvc.py' and remove the "-dev" suffix from + __version__. The remainder should be of the form "X.Y.Z", where X, + Y, and Z are positive integers. + + *** Do NOT commit this change. *** + +9. "Peg" the contributed templates externals definition to the + current HEAD revision: + + svn pedit svn:externals . + + (squeeze "-rBASE_REV", where BASE_REV is the current HEAD revision + number, between 'templates-contrib' and the target URL). + + *** Do NOT commit this change. *** + +10. Tag the release: + + svn cp -m "Tag the X.Y.Z final release." . ^/tags/X.Y.Z + + This will create a copy of the release branch, plus your local + modifications to the svn:externals property and lib/viewvc.py + file, to the tag location. + +11. Revert the changes in your working copy. + + svn revert -R . + +12. Go into an empty directory and run the 'make-release' script: + + tools/make-release viewvc-X.Y.Z tags/X.Y.Z + +13. Verify the archive files: + + - do they have a LICENSE.html file? + - do they have necessary include documentation? + - do they *not* have unnecessary stuff? + - do they install and work correctly? + +14. Upload the created archive files (tar.gz and zip) into the Files + and Documents section of the Tigris.org project, and modify the + CHECKSUMS document there accordingly: + + http://viewvc.tigris.org/servlets/ProjectDocumentList?folderID=6004 + + Also, drop a copy of the archive files into the root directory of + the viewvc.org website (unversioned). + +15. Update the Tigris.org website (^/trunk/www/index.html) to refer to + the new release files and commit. + + svn ci -m "Bump latest advertised release." + +16. Back on the release branch, edit the file 'lib/viewvc.py' again, + incrementing the patch number assigned to the __version__ + variable. Add a new empty block in the branch's CHANGES file. + Commit your changes: + + svn ci -m "Begin a new release cycle." + +17. Edit the Issue Tracker configuration options, adding a new Version + for the just-released one, and a new Milestone for the next patch + (and possibly, minor or major) release. (For the Milestone sort + key, use a packed integer XXYYZZ: 1.0.3 == 10003, 2.11.4 == 21104.) + + http://viewvc.tigris.org/issues/editversions.cgi?component=viewvc&action=add + http://viewvc.tigris.org/issues/editmilestones.cgi?component=viewvc&action=add + +18. Send to the announce@ list a message explaining all the cool new + features. + + http://viewvc.tigris.org/ds/viewForumSummary.do?dsForumId=4253 + +19. Post a new release notification at Freecode. + + https://freecode.com/projects/viewvc/releases/new + +20. Merge CHANGES for this release into the CHANGES file for newer + release lines and commit. diff --git a/notes/root-heirarchy.txt b/notes/root-heirarchy.txt new file mode 100644 index 00000000..1935ddd0 --- /dev/null +++ b/notes/root-heirarchy.txt @@ -0,0 +1,107 @@ + -*- text -*- + +This document carries some design thoughts regarding the solution of +Issue #439[1] ("allow svn repositories to reside in web-navigable +subdirectories") + +[1] http://viewvc.tigris.org/issues/show_bug.cgi?id=439 + + +INTRODUCTION +============ + +Many folks have expressed the desire that ViewVC expose its configured +roots at more or less arbitrary virtual paths below the ViewVC root +URL. An example might explain this better. + +Say you have a ViewVC instance configured with roots like so: + + # path vc + # ------------ --- + root_parents = /var/cvs/old : cvs, + /var/svn/dev : svn, + /var/svn/qa : svn, + +and say that each of these root parents has a few roots whose names +begin with the basenames of the parent directory ("dev-tools" lives in +"/var/svn/dev", "old-docs" lives in "/var/cvs/old", etc.) + +Today, ViewVC will display all those roots in the "root listing" view +as if they are siblings: + + old-docs/ + old-src/ + dev-build/ + dev-libs/ + dev-tools/ + qa-scripts/ + qa-utils/ + +In other words, any heirarchy which might exist in the on-disk +locations of the roots, or (in the Subversion case) in the +version-control system itself, is flattened. + +But sometimes you might want to preserve -- or even introduce -- some +heirarchy in those roots, exposed to the users. For example, you +might wish that instead of a single "root listing" view, ViewVC +instead presented users with a navigable tree constructed from paths +configured by the admin. For example, imagine if each root_parent +item also carried an "exposure path" bit of configuration: + + # path vc exposure-path + # ------------ --- ------------- + root_parents = /var/cvs/old : cvs : old, + /var/svn/dev : svn : current/dev, + /var/svn/qa : svn : current/qa, + +A visit to ViewVC's root URL would then show: + + old/ + current/ + +Clicking into "old/", you'd see: + + old-docs/ + old-src/ + +Alternatively, clicking into "current/" would show: + + dev/ + qa/ + +...and so on. + +In fact, you'd have a virtual heirarchy like so: + + / + old/ + old-docs/ => CVS root at /var/cvs/old/docs + old-src/ => CVS root at /var/cvs/old/src + current/ + dev/ + dev-build/ => SVN root at /var/svn/dev/dev-build + dev-libs/ => SVN root at /var/svn/dev/dev-libs + dev-tools/ => SVN root at /var/svn/dev/dev-tools + qa/ + qa-scripts/ => SVN root at /var/svn/qa/qa-scripts + qa-utils/ => SVN root at /var/svn/qa/qa-utils + + +CHALLENGES +========== + +* Merely tacking on a new "exposure path" item in the definition of + the root_parents, cvs_roots, and svn_roots seems clunky. It also + limits the the granularity of control: you couldn't assign + different locations to the various roots that live within a single + root parent path. Finally, the current code is banking on there + being only a single colon (:) separator (since those might legally + appear in Windows on-disk paths). That might create a bit of a + compatibility annoyance. + +* What do you do about root_as_url_component=0? I guess this feature + is just simply unavailable in that case. + +* Do you continue to allow cvs_roots and svn_roots members to specify + a root name, or does the root name concept go away entirely in light + of the new exposure path concept? diff --git a/templates/classic/diff.ezt b/templates/classic/diff.ezt new file mode 100644 index 00000000..f6df9e39 --- /dev/null +++ b/templates/classic/diff.ezt @@ -0,0 +1,63 @@ +[# setup page definitions] + [define page_title]Diff of /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "diff"] +[include "include/file_header.ezt"] + +[if-any diffs] + [for diffs] + [include "include/diff_display.ezt"] + [end] +[end] + +
+ + + + + + +
+
+
+ [for diff_format_hidden_values][end] + + +
+
+
+[if-any hide_legend] +   +[else] + + + + +
Legend:
+ + + + + + + + + + + + +
Removed lines/characters 
Changed lines/characters
 Added lines/characters
+
+[end] +
+ +[include "include/footer.ezt"] diff --git a/templates/dir_new.ezt b/templates/classic/dir_new.ezt similarity index 100% rename from templates/dir_new.ezt rename to templates/classic/dir_new.ezt diff --git a/templates/directory.ezt b/templates/classic/directory.ezt similarity index 100% rename from templates/directory.ezt rename to templates/classic/directory.ezt diff --git a/templates/docroot/help.css b/templates/classic/docroot/help.css similarity index 100% rename from templates/docroot/help.css rename to templates/classic/docroot/help.css diff --git a/templates/docroot/help_dirview.html b/templates/classic/docroot/help_dirview.html similarity index 100% rename from templates/docroot/help_dirview.html rename to templates/classic/docroot/help_dirview.html diff --git a/templates/docroot/help_log.html b/templates/classic/docroot/help_log.html similarity index 100% rename from templates/docroot/help_log.html rename to templates/classic/docroot/help_log.html diff --git a/templates/docroot/help_query.html b/templates/classic/docroot/help_query.html similarity index 100% rename from templates/docroot/help_query.html rename to templates/classic/docroot/help_query.html diff --git a/templates/docroot/help_rootview.html b/templates/classic/docroot/help_rootview.html similarity index 100% rename from templates/docroot/help_rootview.html rename to templates/classic/docroot/help_rootview.html diff --git a/templates/docroot/images/annotate.png b/templates/classic/docroot/images/annotate.png similarity index 100% rename from templates/docroot/images/annotate.png rename to templates/classic/docroot/images/annotate.png diff --git a/templates/docroot/images/back.png b/templates/classic/docroot/images/back.png similarity index 100% rename from templates/docroot/images/back.png rename to templates/classic/docroot/images/back.png diff --git a/templates/docroot/images/back_small.png b/templates/classic/docroot/images/back_small.png similarity index 100% rename from templates/docroot/images/back_small.png rename to templates/classic/docroot/images/back_small.png diff --git a/templates/docroot/images/broken.png b/templates/classic/docroot/images/broken.png similarity index 100% rename from templates/docroot/images/broken.png rename to templates/classic/docroot/images/broken.png diff --git a/templates/docroot/images/chalk.jpg b/templates/classic/docroot/images/chalk.jpg similarity index 100% rename from templates/docroot/images/chalk.jpg rename to templates/classic/docroot/images/chalk.jpg diff --git a/templates/docroot/images/cvsgraph_16x16.png b/templates/classic/docroot/images/cvsgraph_16x16.png similarity index 100% rename from templates/docroot/images/cvsgraph_16x16.png rename to templates/classic/docroot/images/cvsgraph_16x16.png diff --git a/templates/docroot/images/cvsgraph_32x32.png b/templates/classic/docroot/images/cvsgraph_32x32.png similarity index 100% rename from templates/docroot/images/cvsgraph_32x32.png rename to templates/classic/docroot/images/cvsgraph_32x32.png diff --git a/templates/docroot/images/diff.png b/templates/classic/docroot/images/diff.png similarity index 100% rename from templates/docroot/images/diff.png rename to templates/classic/docroot/images/diff.png diff --git a/templates/docroot/images/dir.png b/templates/classic/docroot/images/dir.png similarity index 100% rename from templates/docroot/images/dir.png rename to templates/classic/docroot/images/dir.png diff --git a/templates/docroot/images/down.png b/templates/classic/docroot/images/down.png similarity index 100% rename from templates/docroot/images/down.png rename to templates/classic/docroot/images/down.png diff --git a/templates/docroot/images/download.png b/templates/classic/docroot/images/download.png similarity index 100% rename from templates/docroot/images/download.png rename to templates/classic/docroot/images/download.png diff --git a/templates/docroot/images/favicon.ico b/templates/classic/docroot/images/favicon.ico similarity index 100% rename from templates/docroot/images/favicon.ico rename to templates/classic/docroot/images/favicon.ico diff --git a/templates/docroot/images/feed-icon-16x16.jpg b/templates/classic/docroot/images/feed-icon-16x16.jpg similarity index 100% rename from templates/docroot/images/feed-icon-16x16.jpg rename to templates/classic/docroot/images/feed-icon-16x16.jpg diff --git a/templates/docroot/images/forward.png b/templates/classic/docroot/images/forward.png similarity index 100% rename from templates/docroot/images/forward.png rename to templates/classic/docroot/images/forward.png diff --git a/templates/docroot/images/list.png b/templates/classic/docroot/images/list.png similarity index 100% rename from templates/docroot/images/list.png rename to templates/classic/docroot/images/list.png diff --git a/templates/docroot/images/lock.png b/templates/classic/docroot/images/lock.png similarity index 100% rename from templates/docroot/images/lock.png rename to templates/classic/docroot/images/lock.png diff --git a/templates/docroot/images/log.png b/templates/classic/docroot/images/log.png similarity index 100% rename from templates/docroot/images/log.png rename to templates/classic/docroot/images/log.png diff --git a/templates/docroot/images/text.png b/templates/classic/docroot/images/text.png similarity index 100% rename from templates/docroot/images/text.png rename to templates/classic/docroot/images/text.png diff --git a/templates/docroot/images/up.png b/templates/classic/docroot/images/up.png similarity index 100% rename from templates/docroot/images/up.png rename to templates/classic/docroot/images/up.png diff --git a/templates/docroot/images/view.png b/templates/classic/docroot/images/view.png similarity index 100% rename from templates/docroot/images/view.png rename to templates/classic/docroot/images/view.png diff --git a/templates/docroot/images/viewvc-logo.png b/templates/classic/docroot/images/viewvc-logo.png similarity index 100% rename from templates/docroot/images/viewvc-logo.png rename to templates/classic/docroot/images/viewvc-logo.png diff --git a/templates/docroot/styles.css b/templates/classic/docroot/styles.css similarity index 97% rename from templates/docroot/styles.css rename to templates/classic/docroot/styles.css index c6cdba40..91a1e8f6 100644 --- a/templates/docroot/styles.css +++ b/templates/classic/docroot/styles.css @@ -94,9 +94,6 @@ form { margin: 0; } .vc_properties { margin: 1em 0; } -.vc_properties h2 { - font-size: 115%; -} /*** File Content Markup Styles ***/ @@ -278,3 +275,14 @@ table.vc_idiff tbody th { /*** Graph Display Form ***/ .vc_graph_form { } + + +/*** Warning! ***/ +.vc_warning { + border-width: 1px 2px 2px 2px; + border-color: black; + border-style: solid; + background-color: red; + color: white; + padding: 0.5em; +} diff --git a/templates/error.ezt b/templates/classic/error.ezt similarity index 100% rename from templates/error.ezt rename to templates/classic/error.ezt diff --git a/templates/file.ezt b/templates/classic/file.ezt similarity index 91% rename from templates/file.ezt rename to templates/classic/file.ezt index 9972f7e4..94bc4356 100644 --- a/templates/file.ezt +++ b/templates/classic/file.ezt @@ -9,7 +9,11 @@ [# ------------------------------------------------------------------------- ] [# setup page definitions] - [define page_title]Contents of /[where][end] + [is annotation "annotated"] + [define page_title]Annotation of /[where][end] + [else] + [define page_title]Contents of /[where][end] + [end] [define help_href][docroot]/help_rootview.html[end] [# end] @@ -45,7 +49,7 @@ Revision [if-any revision_href][rev]< [end] [end] [if-any mime_type] -
File MIME type: [mime_type] +
Content type: [mime_type] [end] [is roottype "svn"][if-any size]
File size: [size] byte(s) @@ -71,9 +75,9 @@ Revision [if-any revision_href]
[rev]< [if-any image_src_href][define hide_binary_garbage]0[end][end] [is hide_binary_garbage "1"] -

This file's contents are not viewable. Please - download this version of the - file in order to view it.

+

This file's contents are not viewable. + [if-any download_href]Please download + this version of the file in order to view it.[end]

[else] [define last_rev]0[end] diff --git a/templates/graph.ezt b/templates/classic/graph.ezt similarity index 100% rename from templates/graph.ezt rename to templates/classic/graph.ezt diff --git a/templates/classic/include/diff_display.ezt b/templates/classic/include/diff_display.ezt new file mode 100644 index 00000000..08293ea4 --- /dev/null +++ b/templates/classic/include/diff_display.ezt @@ -0,0 +1,235 @@ +[is diffs.diff_block_format "anchor"] +

+[else] + [define msg_no_changes]
- No changes -
 [end] + [define msg_binary]
- Binary content differs -
 [end] + [define msg_error]
- ViewVC depends on rcsdiff and GNU diff to create this page. ViewVC cannot find GNU diff. + Even if you have GNU diff installed, the rcsdiff program must be configured and compiled with the GNU + diff location. -
 [end] + + [define left_view_href][if-any diffs.left.prefer_markup][diffs.left.view_href][else][if-any diffs.left.download_href][diffs.left.download_href][end][end][end] + [define right_view_href][if-any diffs.right.prefer_markup][diffs.right.view_href][else][if-any diffs.right.download_href][diffs.right.download_href][end][end][end] + + [define left_header][is diffs.left.path diffs.right.path][else][diffs.left.path]
[end] + [if-any diffs.propname]Property: [diffs.propname]
[end] + Revision [if-any left_view_href][end][diffs.left.rev][if-any left_view_href][end][if-any diffs.left.author] by [diffs.left.author][end], + [diffs.left.date] + [if-any diffs.left.tag]
Tag: [diffs.left.tag][end][end] + + [define right_header][is diffs.right.path diffs.left.path][else][diffs.right.path]
[end] + [if-any diffs.propname]Property: [diffs.propname]
[end] + Revision [if-any right_view_href][end][diffs.right.rev][if-any right_view_href][end][if-any diffs.right.author] by [diffs.right.author][end], + [diffs.right.date] + [if-any diffs.right.tag]
Tag: [diffs.right.tag][end][end] +[end] + +[is diffs.diff_block_format "raw"] + + + + + + + + + + + [is diffs.changes.type "no-changes"] + + + + [else] + [is diffs.changes.type "binary-diff"] + + + + [else] + + [end] + [end] +
 [left_header]
vs.
 [right_header]
[msg_no_changes]
[msg_binary]
[diffs.changes.raw]
+[end] + +[is diffs.diff_block_format "sidebyside-1"] + + + + + + + + [for diffs.changes] + [is diffs.changes.type "header"] + + + + + + [else] + [is diffs.changes.type "add"] + + + + + + [else] + [is diffs.changes.type "remove"] + + + + + + [else] + [is diffs.changes.type "change"] + + [if-any diffs.changes.have_right] + + [else] + + [end] + [if-any diffs.changes.have_left] + + [else] + + [end] + [if-any diffs.changes.have_right] + + [else] + + [end] + + [else] + [is diffs.changes.type "no-changes"] + + + + + + + [else] + [is diffs.changes.type "binary-diff"] + + + + + + + [else] + [is diffs.changes.type "error"] + + + + + + + [else] + + + + + + [end] + [end] + [end] + [end] + [end] + [end] + [end] + [end] +
[left_header][right_header]
# + Line [diffs.changes.line_info_left]  + [diffs.changes.line_info_extra] + + Line [diffs.changes.line_info_right]  + [diffs.changes.line_info_extra] +
[if-any diffs.right.annotate_href][diffs.changes.line_number][else][diffs.changes.line_number][end]  [diffs.changes.right]
 [diffs.changes.left] 
[if-any diffs.right.annotate_href][diffs.changes.line_number][else][diffs.changes.line_number][end] [diffs.changes.left]  [diffs.changes.right] 
 
[msg_no_changes]
 
[msg_binary]
 
[msg_error]
[if-any diffs.right.annotate_href][diffs.changes.line_number][else][diffs.changes.line_number][end] [diffs.changes.left] [diffs.changes.right]
+[end] + +[is diffs.diff_block_format "sidebyside-2"] + + + + + + + + + + + [for diffs.changes] + [is diffs.changes.type "no-changes"] + + + + [else] + [is diffs.changes.type "binary-diff"] + + + + [else] + [if-any diffs.changes.gap] + + + + + [end] + + [for diffs.changes.columns] + [for diffs.changes.columns.segments][if-any diffs.changes.columns.segments.type][diffs.changes.columns.segments.text][else][diffs.changes.columns.segments.text][end][end] + [end] + + [end] + [end] + [end] + +
[left_header][right_header]
[msg_no_changes]
[msg_binary]
[diffs.changes.columns.line_number]
+[end] + +[is diffs.diff_block_format "unified"] + + + + + + + + + + + + + + + + + + + [for diffs.changes] + [is diffs.changes.type "no-changes"] + + + + [else] + [is diffs.changes.type "binary-diff"] + + + + [else] + [if-any diffs.changes.gap] + + + + + + [end] + + + + [for diffs.changes.segments][if-any diffs.changes.segments.type][diffs.changes.segments.text][else][diffs.changes.segments.text][end][end] + + [end] + [end] + [end] + +
 [left_header]
vs.
 [right_header]
[msg_no_changes]
[msg_binary]
[diffs.changes.left_number][diffs.changes.right_number]
+[end] diff --git a/templates/include/diff_form.ezt b/templates/classic/include/diff_form.ezt similarity index 100% rename from templates/include/diff_form.ezt rename to templates/classic/include/diff_form.ezt diff --git a/templates/include/dir_footer.ezt b/templates/classic/include/dir_footer.ezt similarity index 100% rename from templates/include/dir_footer.ezt rename to templates/classic/include/dir_footer.ezt diff --git a/templates/include/dir_header.ezt b/templates/classic/include/dir_header.ezt similarity index 100% rename from templates/include/dir_header.ezt rename to templates/classic/include/dir_header.ezt diff --git a/templates/include/file_header.ezt b/templates/classic/include/file_header.ezt similarity index 100% rename from templates/include/file_header.ezt rename to templates/classic/include/file_header.ezt diff --git a/templates/include/footer.ezt b/templates/classic/include/footer.ezt similarity index 80% rename from templates/include/footer.ezt rename to templates/classic/include/footer.ezt index 9ad673bb..af1c1f62 100644 --- a/templates/include/footer.ezt +++ b/templates/classic/include/footer.ezt @@ -5,7 +5,7 @@ - + diff --git a/templates/include/header.ezt b/templates/classic/include/header.ezt similarity index 98% rename from templates/include/header.ezt rename to templates/classic/include/header.ezt index 13e7c1e8..bfb9019d 100644 --- a/templates/include/header.ezt +++ b/templates/classic/include/header.ezt @@ -5,7 +5,7 @@ [if-any rootname][[][rootname]][else]ViewVC[end] [page_title] - + [if-any rss_href][end] diff --git a/templates/include/log_footer.ezt b/templates/classic/include/log_footer.ezt similarity index 100% rename from templates/include/log_footer.ezt rename to templates/classic/include/log_footer.ezt diff --git a/templates/include/log_header.ezt b/templates/classic/include/log_header.ezt similarity index 100% rename from templates/include/log_header.ezt rename to templates/classic/include/log_header.ezt diff --git a/templates/include/paging.ezt b/templates/classic/include/paging.ezt similarity index 100% rename from templates/include/paging.ezt rename to templates/classic/include/paging.ezt diff --git a/templates/include/pathrev_form.ezt b/templates/classic/include/pathrev_form.ezt similarity index 100% rename from templates/include/pathrev_form.ezt rename to templates/classic/include/pathrev_form.ezt diff --git a/templates/include/props.ezt b/templates/classic/include/props.ezt similarity index 100% rename from templates/include/props.ezt rename to templates/classic/include/props.ezt diff --git a/templates/include/sort.ezt b/templates/classic/include/sort.ezt similarity index 100% rename from templates/include/sort.ezt rename to templates/classic/include/sort.ezt diff --git a/templates/log.ezt b/templates/classic/log.ezt similarity index 92% rename from templates/log.ezt rename to templates/classic/log.ezt index 2332ee71..cd6111c5 100644 --- a/templates/log.ezt +++ b/templates/classic/log.ezt @@ -31,13 +31,11 @@ [if-any entries.download_text_href](as text)[end] [if-any entries.annotate_href](annotate)[end] - [is pathtype "file"] - [# if you don't want to allow select for diffs then remove this section] - [is entries.rev rev_selected] - - [[]selected] - [else] - - [[]select for diffs] - [end] + [# if you don't want to allow select for diffs then remove this section] + [is entries.rev rev_selected] + - [[]selected] + [else] + - [[]select for diffs] [end] [end] @@ -105,8 +103,7 @@ [is entries.state "dead"]
FILE REMOVED [else] - [is pathtype "file"] - [if-any entries.prev] + [if-any entries.diff_to_prev_href]
Diff to previous [entries.prev] [if-any human_readable] [else] @@ -140,7 +137,6 @@ (colored) [end] [end] - [end] [end]
[entries.log]
diff --git a/templates/log_table.ezt b/templates/classic/log_table.ezt similarity index 100% rename from templates/log_table.ezt rename to templates/classic/log_table.ezt diff --git a/templates/classic/query.ezt b/templates/classic/query.ezt new file mode 100644 index 00000000..3549de8a --- /dev/null +++ b/templates/classic/query.ezt @@ -0,0 +1,249 @@ + + + + + Checkin Database Query + [if-any docroot][end] + + + + +[# setup page definitions] + [define help_href][if-any docroot][docroot]/help_query.html[end][end] +[# end] + +

+ Select your parameters for querying the CVS commit database. You + can search for multiple matches by typing a comma-seperated list + into the text fields. Regular expressions, and wildcards are also + supported. Blank text input fields are treated as wildcards. +

+

+ Any of the text entry fields can take a comma-seperated list of + search arguments. For example, to search for all commits from + authors jpaint and gstein, just type: jpaint, + gstein in the Author input box. If you are searching + for items containing spaces or quotes, you will need to quote your + request. For example, the same search above with quotes is: + "jpaint", "gstein". +

+

+ + Wildcard and regular expression searches are entered in a similar + way to the quoted requests. You must quote any wildcard or + regular expression request, and a command charactor preceeds the + first quote. The command charactor l is for wildcard + searches, and the wildcard charactor is a percent (%). The + command charactor for regular expressions is r, and is + passed directly to MySQL, so you'll need to refer to the MySQL + manual for the exact regex syntax. It is very similar to Perl. A + wildard search for all files with a .py extention is: + l"%.py" in the File input box. The same search done + with a regular expression is: r".*\.py". +

+

+ All search types can be mixed, as long as they are seperated by + commas. +

+ + + +
+
[if-any cfg.general.address]
[cfg.general.address]
[else] [end]
ViewVC Help[if-any help_href]ViewVC Help[else] [end]
Powered by ViewVC [vsn]
+ + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
CVS Repository: + +
CVS Branch: + +
Directory: + +
File: + +
Author: + +
+ +
+ + + + + + + + + +
Sort By: + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Date:
In the last + hours +
In the last day
In the last week
In the last month
Since the beginning of time
+
+ +
+
+ +
+ + + + +[is query "skipped"] +[else] +

[num_commits] matches found.

+[if-any row_limit_reached] +

WARNING: These query results have been + artificially limited by an administrative threshold value and do + not represent the entirety of the data set which matches + the query. Consider modifying your query to be more specific, + using your version control tool's query capabilities, or asking + your administrator to raise the database response size + threshold.

+[end] +[if-any commits] + + + + + + + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + + [for commits.files] + + + + + + + + +[# uncommment, if you want a separate Description column: + {if-index commits.files first} + + {end} + + (substitute brackets for the braces) +] + +[# and also take the following out in the "Description column" case:] + [if-index commits.files last] + + + + + [end] +[# ---] + [end] + +[end] + + + + + + + + +[# uncommment, if you want a separate Description column: + +] + +
RevisionFileBranch+/-DateAuthorDescription
+ [if-any commits.files.rev][commits.files.rev][else] [end] + [commits.files.link] + [if-any commits.files.branch][commits.files.branch][else] [end] + + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] + + {if-any commits.log}{commits.log}{else} {end} +
 Log:
+
[if-any commits.log][commits.log][else] [end]
       
+[end] +[end] +[include "include/footer.ezt"] diff --git a/templates/query_form.ezt b/templates/classic/query_form.ezt similarity index 100% rename from templates/query_form.ezt rename to templates/classic/query_form.ezt diff --git a/templates/query_results.ezt b/templates/classic/query_results.ezt similarity index 89% rename from templates/query_results.ezt rename to templates/classic/query_results.ezt index dee029fe..1953ead2 100644 --- a/templates/query_results.ezt +++ b/templates/classic/query_results.ezt @@ -7,6 +7,15 @@

[english_query]

[# ] +[if-any row_limit_reached] +

WARNING: These query results have been + artificially limited by an administrative threshold value and do + not represent the entirety of the data set which matches + the query. Consider modifying your + query to be more specific, using your version control tool's + query capabilities, or asking your administrator to raise the + database response size threshold.

+[end]

Modify query[if-any repos_root] [else] [if-any repos_type] [is repos_type "cvs"]| Look only in SVN | Look in all repos [end] diff --git a/templates/revision.ezt b/templates/classic/revision.ezt similarity index 88% rename from templates/revision.ezt rename to templates/classic/revision.ezt index 2732a805..abeeeae3 100644 --- a/templates/revision.ezt +++ b/templates/classic/revision.ezt @@ -6,6 +6,7 @@ [include "include/header.ezt" "revision"]


+
@@ -42,7 +43,7 @@
-

Changed paths:

+

Changed paths

@@ -59,8 +60,8 @@ [if-any changes.is_copy]
(Copied from [changes.copy_path], r[changes.copy_rev])[end] [end] @@ -77,4 +78,5 @@
[if-any changes.log_href][end][changes.action][if-any changes.log_href][end] - [if-any changes.text_mods], [if-any changes.diff_href][end]text changed[if-any changes.diff_href][end][end] - [if-any changes.prop_mods], props changed[end] + [if-any changes.text_mods], [if-any changes.diff_href][end]text changed[if-any changes.diff_href][end][end] + [if-any changes.prop_mods], [if-any changes.diff_href][end]props changed[if-any changes.diff_href][end][end]
+[include "include/props.ezt"] [include "include/footer.ezt"] diff --git a/templates/roots.ezt b/templates/classic/roots.ezt similarity index 56% rename from templates/roots.ezt rename to templates/classic/roots.ezt index d64f7f6c..ece8c829 100644 --- a/templates/roots.ezt +++ b/templates/classic/roots.ezt @@ -13,6 +13,12 @@ Name +[is cfg.options.show_roots_lastmod "1"] + Revision + Age + Author + Log +[end] @@ -24,6 +30,12 @@ [roots.name] +[is cfg.options.show_roots_lastmod "1"] +  [if-any roots.log_href][roots.rev][else][roots.rev][end] +  [roots.ago] +  [roots.author] +  [roots.short_log] +[end] [end] diff --git a/templates/classic/rss.ezt b/templates/classic/rss.ezt new file mode 100644 index 00000000..c3c9ae00 --- /dev/null +++ b/templates/classic/rss.ezt @@ -0,0 +1,17 @@ + + + + [rss_link_href] + [rootname] checkins[if-any where] (in [where])[end] + + [is roottype "svn"]Subversion[else]CVS[end] commits to the[if-any where] [where] directory of the[end] [rootname] repository + + [for commits] + [if-any commits.rev][commits.rev]: [end][[commits.author]] [format "xml"][commits.short_log][end] + [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> + [end] + + diff --git a/templates/default/diff.ezt b/templates/default/diff.ezt new file mode 100644 index 00000000..391d14a5 --- /dev/null +++ b/templates/default/diff.ezt @@ -0,0 +1,58 @@ +[# Setup page definitions] + [define page_title]Diff of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "diff"] + + +
+ [for diff_format_hidden_values][end] + + + (Generate patch) +
+
+ +
+ + +[if-any diffs] + [for diffs] + [include "include/diff_display.ezt"] + [end] +[end] + +[if-any hide_legend] +[else] +

Diff Legend

+ + + + + + + + + + + + + + + + + +
Removed lines
+Added lines
<Changed lines
>Changed lines
+[end] + + +
+ +[include "include/footer.ezt"] diff --git a/templates/default/directory.ezt b/templates/default/directory.ezt new file mode 100644 index 00000000..fd93059b --- /dev/null +++ b/templates/default/directory.ezt @@ -0,0 +1,140 @@ +[# setup page definitions] + [define page_title]Index of:[end] + [define help_href][docroot]/help_[if-any where]dir[else]root[end]view.html[end] +[# end] +[include "include/header.ezt" "directory"] + +[if-any where][else] + +[end] + +
+ + +
+
+[include "include/pathrev_form.ezt"] +
+[is roottype "svn"] +
+[if-any rev]r[rev][end] +
+[end] +
+ + + + + + + + + + +[if-any up_href] + + + + [end] +[for entries] + [define click_href][is entries.pathtype "dir"][entries.view_href][else][if-any entries.prefer_markup][entries.view_href][else][entries.download_href][end][end][end] + + + + + + +[end] + + +
+ File + [is sortby "file"] + [is sortdir + [end] + + + Last Change + [is sortby "rev"] + [is sortdir + [end] + +
+ +  ../ +
+ [if-any click_href][end] + + [entries.name][is entries.pathtype "dir"]/[end][if-any click_href][end][if-any entries.lockinfo]locked[end] + [is entries.state "dead"](dead)[end] + + [if-any entries.rev] + [if-any entries.log_href][entries.rev][else][entries.rev][end] + ([entries.ago] ago) + by [entries.author]: + [entries.log] + [is entries.pathtype "dir"][is roottype "cvs"] + (from [entries.log_file]/[entries.log_rev]) + [end][end] + [end] +
+ +
+[is picklist_len "0"][else][is picklist_len "1"][else] +
+
+[for dir_paging_hidden_values][end] + + +
+
+[end][end] +[if-any search_re_action] +
+
+ [for search_re_hidden_values][end] + + +
+
+[if-any search_re] +
+
+ [for search_re_hidden_values][end] + +
+
+
+[end] +[end] +[is num_dead "0"] +[else] +
+ [if-any attic_showing] + Hide + [else] + Show + [end] + dead files +
+[end] +
+ [files_shown] file[is files_shown "1"][else]s[end] shown +
+
+ +[include "include/props.ezt"] + + + + +[include "include/footer.ezt"] diff --git a/templates/default/docroot/help.css b/templates/default/docroot/help.css new file mode 100644 index 00000000..6680d84c --- /dev/null +++ b/templates/default/docroot/help.css @@ -0,0 +1,8 @@ +/************************************/ +/*** ViewVC Help CSS Stylesheet ***/ +/************************************/ +body { margin: 0.5em; } +img { border: none; } +table { width: 100%; } +td { vertical-align: top; } +col.menu { width:12em; } diff --git a/templates/default/docroot/help_dirview.html b/templates/default/docroot/help_dirview.html new file mode 100644 index 00000000..ea471d54 --- /dev/null +++ b/templates/default/docroot/help_dirview.html @@ -0,0 +1,126 @@ + + + + ViewVC Help: Directory View + + + + + + + + + +
+

ViewVC Help: Directory View

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

The directory listing view should be a familiar sight to any + computer user. It shows the path of the current directory being viewed + at the top of the page. Below that is a table summarizing the + directory contents, and then comes actual contents, a sortable list of + all files and subdirectories inside the current directory.

+ +

The summary table is made up of some or all + of the following rows:

+
    +
  • Files Shown + - Number of files shown in the directory listing. This might be less + than the actual number of files in the directory if a + regular expression search is in place, + hiding files which don't meet the search criteria. In CVS directory + listings, this row will also have a link to toggle display of + dead files, if any are + present.
  • + +
  • Directory + Revision - For Subversion directories only. + Shown as "# of #" where the first number is the most recent + repository revision where the directory (or a path underneath it) + was modified. The second number is just the latest repository + revision. Both numbers are links to + revision views
  • + +
  • Sticky + Revision/Tag - shows the current + sticky revision or + tag and contains form fields to set or clear it.
  • + +
  • Current Search - + If a regular expression search is in place, + shows the search string.
  • + +
  • Query - Provides + a link to a query form + for the directory
  • +
+ +

The actual directory list is a table with + filenames and directory names in one column and information about the + most recent revisions where each file or directory was modified in the + other columns. Column headers can be clicked to sort the directory + entries in order by a column, and clicked again to reverse the sort + order.

+ +

+ + File names are links to log views + showing a list of revisions where a file was modified. Revision + numbers are links to either + view + or download a file + (depending on its file type). The links are reversed for directories. + Directory revision numbers are links to log + views, while directory names are links showing the contents of those + directories. + + + + + Also, in CVS repositories with the graph view enabled, there + will be small icons next to file names which are links to revision + graphs.

+ +

Depending on how ViewVC is configured, there may be more options + at the bottom of directory pages:

+ +
    +
  • Regular expression + search - If enabled, will show a form field accepting + a search string (a + python regular + expression). Once submitted, only files that have at least + one occurance of the expression will show up in directory listings. +
  • +
  • Tarball download - + If enabled, will show a link to download a gzipped tar archive of + the directory contents.
  • +
+ +
+
+
ViewVC Users Mailinglist
+ + diff --git a/templates/default/docroot/help_log.html b/templates/default/docroot/help_log.html new file mode 100644 index 00000000..33000943 --- /dev/null +++ b/templates/default/docroot/help_log.html @@ -0,0 +1,71 @@ + + + + ViewVC Help: Log View + + + + + + + + + + +
+

ViewVC Help: Log View

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+

+ The log view displays the revision history of the selected source + file or directory. For each revision the following information is + displayed: + +

    +
  • The revision number. In Subversion repositories, this is a + link to the revision + view
  • +
  • For files, links to + view, + download, and + annotate the + revision. For directories, a link to + list directory contents
  • +
  • A link to select the revision for diffs (see below)
  • +
  • The date and age of the change
  • +
  • The author of the modification
  • +
  • The CVS branch (usually MAIN, if not on a branch)
  • +
  • Possibly a list of CVS tags bound to the revision (if any)
  • +
  • The size of the change measured in added and removed lines of + code. (CVS only)
  • +
  • The size of the file in bytes at the time of the revision + (Subversion only)
  • +
  • Links to view diffs to the previous revision or possibly to + an arbitrary selected revision (if any, see above)
  • +
  • If the revision is the result of a copy, the path and revision + copied from
  • +
  • If the revision precedes a copy or rename, the path at the + time of the revision
  • +
  • And last but not least, the commit log message which should tell + about the reason for the change.
  • +
+

+ At the bottom of the page you will find a form which allows + to request diffs between arbitrary revisions. +

+
+
+
ViewVC Users Mailinglist
+ + diff --git a/templates/default/docroot/help_query.html b/templates/default/docroot/help_query.html new file mode 100644 index 00000000..96e6e438 --- /dev/null +++ b/templates/default/docroot/help_query.html @@ -0,0 +1,66 @@ + + + + ViewVC Help: Query The Commit Database + + + + + + + + + +
+

ViewVC Help: Query The Commit Database

+
+

Other Help:

+ General
+ Directory View
+ Classic Log View
+ Alternative Log View
+ Query Database + +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

+ Select your parameters for querying the CVS commit database in the + form at the top of the page. You + can search for multiple matches by typing a comma-seperated list + into the text fields. Regular expressions, and wildcards are also + supported. Blank text input fields are treated as wildcards. +

+

+ Any of the text entry fields can take a comma-seperated list of + search arguments. For example, to search for all commits from + authors jpaint and gstein, just type: jpaint, + gstein in the Author input box. If you are searching + for items containing spaces or quotes, you will need to quote your + request. For example, the same search above with quotes is: + "jpaint", "gstein". +

+

+ Wildcard and regular expression searches are entered in a similar + way to the quoted requests. You must quote any wildcard or + regular expression request, and a command character preceeds the + first quote. The command character l(lowercase L) is for wildcard + searches, and the wildcard character is a percent (%). The + command character for regular expressions is r, and is + passed directly to MySQL, so you'll need to refer to the MySQL + manual for the exact regex syntax. It is very similar to Perl. A + wildard search for all files with a .py extention is: + l"%.py" in the File input box. The same search done + with a regular expression is: r".*\.py". +

+

+ All search types can be mixed, as long as they are seperated by + commas. +

+
+ diff --git a/templates/default/docroot/help_rootview.html b/templates/default/docroot/help_rootview.html new file mode 100644 index 00000000..1e15ccad --- /dev/null +++ b/templates/default/docroot/help_rootview.html @@ -0,0 +1,166 @@ + + + + ViewVC Help: General + + + + + + + + + +
+

ViewVC Help: General

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

ViewVC is a WWW interface for CVS and Subversion + repositories. It allows you to browse the files and directories in a + repository while showing you metadata from the repository history: log + messages, modification dates, author names, revision numbers, copy + history, and so on. It provides several different views of repository + data to help you find the information you are looking for:

+ + + +

Multiple Repositories

+ +

A single installation of ViewVC is often used to provide access to + more than one repository. In these installations, ViewVC shows a + Project Root drop down box in the top right corner of every + generated page to allow for quick access to any repository.

+ +

Sticky Revision and Tag

+ +

By default, ViewVC will show the files and directories and revisions + that currently exist in the repository. But it's also possible to browse + the contents of a repository at a point in its past history by choosing + a "sticky tag" (in CVS) or a "sticky revision" (in Subversion) from the + forms at the top of directory and log pages. They're called sticky + because once they're chosen, they stick around when you navigate to + other pages, until you reset them. When they're set, directory and log + pages only show revisions preceding the specified point in history. In + CVS, when a tag refers to a branch or a revision on a branch, only + revisions from the branch history are shown, including branch points and + their preceding revisions.

+ +

Dead Files

+ +

In CVS directory listings, ViewVC can optionally display dead files. + Dead files are files which used to be in a directory but are currently + deleted, or files which just don't exist in the currently selected + sticky tag. Dead files cannot be + shown in Subversion repositories. The only way to see a deleted file in + a Subversion directory is to navigate to a sticky revision where the + file previously existed.

+ +

Artificial Tags

+ +

In CVS Repositories, ViewVC adds artificial tags HEAD and + MAIN to tag listings and accepts them in place of revision + numbers and real tag names in all URLs. MAIN acts like a branch + tag pointing at the default branch, while HEAD acts like a + revision tag pointing to the latest revision on the default branch. The + default branch is usually just the trunk, but may be set to other + branches inside individual repository files. CVS will always check out + revisions from a file's default branch when no other branch is specified + on the command line.

+ +

More Information

+ +

More information about ViewVC is available from + viewvc.org. + See the links below for guides to CVS and Subversion

+ +

Documentation about CVS

+
+

+ Open Source + Development with CVS
+ CVS + User's Guide
+ Another CVS tutorial
+ Yet another CVS tutorial (a little old, but nice)
+ An old but very useful FAQ about CVS +

+
+ +

Documentation about Subversion

+
+

+ Version Control with + Subversion
+

+
+ +
+
+
ViewVC Users Mailinglist
+ + diff --git a/templates/default/docroot/images/back.png b/templates/default/docroot/images/back.png new file mode 100644 index 00000000..65f46318 Binary files /dev/null and b/templates/default/docroot/images/back.png differ diff --git a/templates/default/docroot/images/back_small.png b/templates/default/docroot/images/back_small.png new file mode 100644 index 00000000..a057c3f8 Binary files /dev/null and b/templates/default/docroot/images/back_small.png differ diff --git a/templates/default/docroot/images/broken.png b/templates/default/docroot/images/broken.png new file mode 100644 index 00000000..cdaf2362 Binary files /dev/null and b/templates/default/docroot/images/broken.png differ diff --git a/templates/default/docroot/images/cvs-logo.png b/templates/default/docroot/images/cvs-logo.png new file mode 100644 index 00000000..c00e24c4 Binary files /dev/null and b/templates/default/docroot/images/cvs-logo.png differ diff --git a/templates/default/docroot/images/dir.png b/templates/default/docroot/images/dir.png new file mode 100644 index 00000000..a11e7eb1 Binary files /dev/null and b/templates/default/docroot/images/dir.png differ diff --git a/templates/default/docroot/images/down.png b/templates/default/docroot/images/down.png new file mode 100644 index 00000000..5644d63b Binary files /dev/null and b/templates/default/docroot/images/down.png differ diff --git a/templates/default/docroot/images/feed-icon-16x16.jpg b/templates/default/docroot/images/feed-icon-16x16.jpg new file mode 100644 index 00000000..0c72133f Binary files /dev/null and b/templates/default/docroot/images/feed-icon-16x16.jpg differ diff --git a/templates/default/docroot/images/forward.png b/templates/default/docroot/images/forward.png new file mode 100644 index 00000000..d8185ac9 Binary files /dev/null and b/templates/default/docroot/images/forward.png differ diff --git a/templates/default/docroot/images/lock-icon.gif b/templates/default/docroot/images/lock-icon.gif new file mode 100644 index 00000000..56e3fca9 Binary files /dev/null and b/templates/default/docroot/images/lock-icon.gif differ diff --git a/templates/default/docroot/images/svn-logo.png b/templates/default/docroot/images/svn-logo.png new file mode 100644 index 00000000..eaa0766a Binary files /dev/null and b/templates/default/docroot/images/svn-logo.png differ diff --git a/templates/default/docroot/images/text.png b/templates/default/docroot/images/text.png new file mode 100644 index 00000000..6e050cd6 Binary files /dev/null and b/templates/default/docroot/images/text.png differ diff --git a/templates/default/docroot/images/up.png b/templates/default/docroot/images/up.png new file mode 100644 index 00000000..625819f9 Binary files /dev/null and b/templates/default/docroot/images/up.png differ diff --git a/templates/default/docroot/images/viewvc-logo.png b/templates/default/docroot/images/viewvc-logo.png new file mode 100644 index 00000000..6e16f3b1 Binary files /dev/null and b/templates/default/docroot/images/viewvc-logo.png differ diff --git a/templates/default/docroot/scripts.js b/templates/default/docroot/scripts.js new file mode 100644 index 00000000..7469fe1b --- /dev/null +++ b/templates/default/docroot/scripts.js @@ -0,0 +1,4 @@ +function jumpTo(url) +{ + window.location = url; +} \ No newline at end of file diff --git a/templates/default/docroot/styles.css b/templates/default/docroot/styles.css new file mode 100644 index 00000000..e4371bbd --- /dev/null +++ b/templates/default/docroot/styles.css @@ -0,0 +1,385 @@ +/*******************************/ +/*** ViewVC CSS Stylesheet ***/ +/*******************************/ + +/*** Standard Tags ***/ +html, body { + background-color: white; + color: black; + font-family: sans-serif; + font-size: 100%; + margin: 5px; +} +h2 { + border-top: 3px solid #d0d775; + font-size: 115%; + font-weight: bold; +} +a { + text-decoration: none; + color: rgb(30%,30%,60%); +} +img { border: none; } +table { + width: 100%; + margin: 0; + border: none; +} +td, th { + vertical-align: top; +} +th { white-space: nowrap; } +table.auto { + width: auto; +} +table.fixed { + width: 100%; + table-layout: fixed; +} +table.fixed td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +form { margin: 0; } +address { font-style: normal; display: inline; } +.inline { display: inline; } +.lpad { margin-left: 2em; } + +/*** Icons ***/ +.vc_icon { + width: 16px; + height: 16px; + border: none; + padding: 0 1px; +} +.vc_overlay { + width: 12px; + height: 12px; + border: none; + padding: 0 1px; + vertical-align: top; +} +#vc_header { + padding: 0 0 10px 0; + border-bottom: 10px solid #94bd5e; +} + +#vc_footer { + text-align: right; + font-size: 85%; + padding: 10px 0 0 0; + border-top: 10px solid #94bd5e; +} + +#vc_topmatter { + float: right; + text-align: right; + font-size: 85%; +} + +#vc_current_path { + color: rgb(50%,50%,50%); + padding: 10px 0; + font-size: 140%; + font-weight: bold; +} + +#vc_current_path a { + color: rgb(60%,60%,60%); +} + +#vc_current_path a:hover { + background-color: rgb(90%,90%,90%); +} + +#vc_current_path .thisitem { + color: #94bd5e; +} + +#vc_current_path .pathdiv { + padding: 0 0.1em; +} + +#vc_view_selection_group { + background: black; + color: white; + margin: 0 0 5px 0; + padding: 5px; + text-align: right; +} + +#vc_view_selection_group a { + padding: 5px; + font-size: 100%; + color: white; + text-decoration: none; +} + +#vc_view_selection_group a.vc_view_link_this, #vc_view_selection_group a.vc_view_link:hover { + color: #94bd5e; +} + +#vc_view_selection_group a:hover { + background-color: black; +} + +#vc_view_main { + border-top: 1px solid black; + border-bottom: 1px solid black; +} + +#vc_togglables { + text-align: right; + font-size: 85%; +} + +#vc_main_body { + background: white; + padding: 5px 0 20px 0; +} + +#vc_view_summary { + font-size: 85%; + text-align: right; + margin-top: 5px; +} + + +/*** Table Headers ***/ +.vc_header, .vc_header_sort { + padding: 2px 5px; + text-align: left; + vertical-align: top; + border-bottom: 1px solid black; + background-color: rgb(80%,80%,80%); +} +.vc_header_sort { + background-color: rgb(85%,85%,85%); +} + + +/*** Table Rows ***/ +.vc_row_even { + background-color: rgb(95%,95%,95%); +} +.vc_row_odd { + background-color: rgb(90%,90%,90%); +} + + +/*** Directory View ***/ +#dirlist td, #dirlist th { + padding: 0.2em; + vertical-align: middle; +} +#dirlist tr:hover { + background-color: white; +} + + +/*** Log messages ***/ +.vc_log { + /* unfortunately, white-space: pre-wrap isn't widely supported ... */ + white-space: -moz-pre-wrap; /* Mozilla based browsers */ + white-space: -pre-wrap; /* Opera 4 - 6 */ + white-space: -o-pre-wrap; /* Opera >= 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ +} + + +/*** Revision Log View ***/ +#vc_revision_log { +} +.vc_log_block_even { + border-left: 10px solid #d0d775; + padding-left: 1em; + background-color: rgb(95%,95%,95%); +} +.vc_log_block_odd { + border-left: 10px solid #94bd5e; + padding-left: 1em; + background-color: rgb(90%,90%,90%); +} + + + +/*** Changed Paths Listing ***/ +.vc_changedpaths { + margin: 1em 0 1em 2em; +} + + +/*** Properties Listing ***/ +.vc_properties { + margin: 1em 0 1em 2em; +} +.vc_properties td, .vc_properties th { + padding: 0.2em; +} + + +/*** File Content Markup Styles ***/ +.vc_summary { + background-color: #eeeeee; +} +#vc_file { + margin: 1em 0 1em 2em; +} +#vc_file td { + border-right-style: solid; + border-right-color: #505050; + text-decoration: none; + font-weight: normal; + font-style: normal; + padding: 1px 5px; +} +.vc_file_line_number { + border-right-width: 1px; + background-color: #eeeeee; + color: #505050; + text-align: right; +} +.vc_file_line_author, .vc_file_line_rev { + border-right-width: 1px; + text-align: right; +} +.vc_file_line_text { + border-right-width: 0px; + background-color: white; + font-family: monospace; + text-align: left; + white-space: pre; + width: 100%; +} +.pygments-c { color: #408080; font-style: italic } /* Comment */ +.pygments-err { border: 1px solid #FF0000 } /* Error */ +.pygments-k { color: #008000; font-weight: bold } /* Keyword */ +.pygments-o { color: #666666 } /* Operator */ +.pygments-cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.pygments-cp { color: #BC7A00 } /* Comment.Preproc */ +.pygments-c1 { color: #408080; font-style: italic } /* Comment.Single */ +.pygments-cs { color: #408080; font-style: italic } /* Comment.Special */ +.pygments-gd { color: #A00000 } /* Generic.Deleted */ +.pygments-ge { font-style: italic } /* Generic.Emph */ +.pygments-gr { color: #FF0000 } /* Generic.Error */ +.pygments-gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.pygments-gi { color: #00A000 } /* Generic.Inserted */ +.pygments-go { color: #808080 } /* Generic.Output */ +.pygments-gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.pygments-gs { font-weight: bold } /* Generic.Strong */ +.pygments-gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.pygments-gt { color: #0040D0 } /* Generic.Traceback */ +.pygments-kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.pygments-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.pygments-kp { color: #008000 } /* Keyword.Pseudo */ +.pygments-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.pygments-kt { color: #B00040 } /* Keyword.Type */ +.pygments-m { color: #666666 } /* Literal.Number */ +.pygments-s { color: #BA2121 } /* Literal.String */ +.pygments-na { color: #7D9029 } /* Name.Attribute */ +.pygments-nb { color: #008000 } /* Name.Builtin */ +.pygments-nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.pygments-no { color: #880000 } /* Name.Constant */ +.pygments-nd { color: #AA22FF } /* Name.Decorator */ +.pygments-ni { color: #999999; font-weight: bold } /* Name.Entity */ +.pygments-ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.pygments-nf { color: #0000FF } /* Name.Function */ +.pygments-nl { color: #A0A000 } /* Name.Label */ +.pygments-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.pygments-nt { color: #008000; font-weight: bold } /* Name.Tag */ +.pygments-nv { color: #19177C } /* Name.Variable */ +.pygments-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.pygments-w { color: #bbbbbb } /* Text.Whitespace */ +.pygments-mf { color: #666666 } /* Literal.Number.Float */ +.pygments-mh { color: #666666 } /* Literal.Number.Hex */ +.pygments-mi { color: #666666 } /* Literal.Number.Integer */ +.pygments-mo { color: #666666 } /* Literal.Number.Oct */ +.pygments-sb { color: #BA2121 } /* Literal.String.Backtick */ +.pygments-sc { color: #BA2121 } /* Literal.String.Char */ +.pygments-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.pygments-s2 { color: #BA2121 } /* Literal.String.Double */ +.pygments-se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.pygments-sh { color: #BA2121 } /* Literal.String.Heredoc */ +.pygments-si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.pygments-sx { color: #008000 } /* Literal.String.Other */ +.pygments-sr { color: #BB6688 } /* Literal.String.Regex */ +.pygments-s1 { color: #BA2121 } /* Literal.String.Single */ +.pygments-ss { color: #19177C } /* Literal.String.Symbol */ +.pygments-bp { color: #008000 } /* Name.Builtin.Pseudo */ +.pygments-vc { color: #19177C } /* Name.Variable.Class */ +.pygments-vg { color: #19177C } /* Name.Variable.Global */ +.pygments-vi { color: #19177C } /* Name.Variable.Instance */ +.pygments-il { color: #666666 } /* Literal.Number.Integer.Long */ + + +/*** Diff Styles ***/ +.vc_diff_plusminus { width: 1em; } +.vc_diff_remove, .vc_diff_add, .vc_diff_changes1, .vc_diff_changes2 { + font-family: monospace; + white-space: pre; +} +.vc_diff_remove { background: rgb(100%,60%,60%); } +.vc_diff_add { background: rgb(60%,100%,60%); } +.vc_diff_changes1 { background: rgb(100%,100%,70%); color: rgb(50%,50%,50%); text-decoration: line-through; } +.vc_diff_changes2 { background: rgb(100%,100%,0%); } +.vc_diff_nochange, .vc_diff_binary, .vc_diff_error { + font-family: sans-serif; + font-size: smaller; +} + +/*** Intraline Diff Styles ***/ +.vc_idiff_add { + background-color: #aaffaa; +} +.vc_idiff_change { + background-color:#ffff77; +} +.vc_idiff_remove { + background-color:#ffaaaa; +} +.vc_idiff_empty { + background-color:#e0e0e0; +} + +table.vc_idiff col.content { + width: 50%; +} +table.vc_idiff tbody { + font-family: monospace; + /* unfortunately, white-space: pre-wrap isn't widely supported ... */ + white-space: -moz-pre-wrap; /* Mozilla based browsers */ + white-space: -pre-wrap; /* Opera 4 - 6 */ + white-space: -o-pre-wrap; /* Opera >= 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ +} +table.vc_idiff tbody th { + background-color:#e0e0e0; + text-align:right; +} + + +/*** Query Form ***/ +.vc_query_form { +} + + +/*** Admonishments ***/ +.vc_notice { + border-width: 1px 2px 2px 2px; + border-color: black; + border-style: solid; + background-color: yellow; + color: black; + padding: 0.5em; +} +.vc_warning { + border-width: 1px 2px 2px 2px; + border-color: black; + border-style: solid; + background-color: red; + color: white; + padding: 0.5em; +} diff --git a/templates/default/error.ezt b/templates/default/error.ezt new file mode 100644 index 00000000..e1d61af7 --- /dev/null +++ b/templates/default/error.ezt @@ -0,0 +1,51 @@ + + + + +ViewVC Exception + + +

An Exception Has Occurred

+ +[if-any msg] +

[msg]

+[end] + +[if-any status] +

HTTP Response Status

+

[status]

+
+[end] + +[if-any msg][else] +

Python Traceback

+

+[stacktrace]
+

+[end] + +[# Here follows a bunch of space characters, present to ensure that + our error message is larger than 512 bytes so that IE's "Friendly + Error Message" won't show. For more information, see + http://oreillynet.com/onjava/blog/2002/09/internet_explorer_subverts_err.html] + + + + + + + + + + + + + + + + + + + + diff --git a/templates/default/file.ezt b/templates/default/file.ezt new file mode 100644 index 00000000..c9c1b27c --- /dev/null +++ b/templates/default/file.ezt @@ -0,0 +1,168 @@ +[# ------------------------------------------------------------------------- ] +[# CUSTOMIZE ME: To avoid displaying "binary garbage" -- the contents of ] +[# files with non-human-readable file formats -- change the value of the ] +[# hide_binary_garbage variable below to 1. ] +[# ------------------------------------------------------------------------- ] + +[define hide_binary_garbage]0[end] + +[# ------------------------------------------------------------------------- ] + +[# setup page definitions] + [is annotation "annotated"] + [define page_title]Annotation of:[end] + [else] + [define page_title]Contents of:[end] + [end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "annotate"] + + + + + + + + + + + +[if-any mime_type] + + + + +[end] +[if-any orig_path] + + + + +[end] +[if-any branches] + + + + +[end] +[if-any tags] + + + + +[end] +[if-any branch_points] + + + + +[end] +[is roottype "cvs"][if-any changed] + + + + +[end][end] +[is roottype "svn"][if-any size] + + + + +[end][end] +[if-any lockinfo] + + +[end] +[is state "dead"] + + + + +[end] +[if-any annotation] +[is annotation "binary"] + + + +[end] +[is annotation "error"] + + + +[end] +[end] +[if-any log] + + + + +[end] +
Revision:[if-any revision_href][rev][else][rev][end] [if-any vendor_branch] (vendor branch)[end]
Committed:[if-any date][date] [end][if-any ago]([ago] ago) [end][if-any author]by [author][end]
Content type:[mime_type]
Original Path:[orig_path]
Branch:[branches]
CVS Tags:[tags]
Branch point for:[branch_points]
Changes since [prev]:[changed] lines
File size:[size] byte(s)
Lock status:[lockinfo]
State:FILE REMOVED
Unable to calculate annotation data on binary file contents.
Error occurred while calculating annotation data.
Log Message:
[log]
+ +
+ + +

File Contents

+ +[if-any prefer_markup][define hide_binary_garbage]0[end][end] +[if-any image_src_href][define hide_binary_garbage]0[end][end] + +[is hide_binary_garbage "1"] +

This file's contents are not viewable. + [if-any download_href]Please download + this version of the file in order to view it.[end]

+[else] + +[define last_rev]0[end] +[define rowclass]vc_row_odd[end] + +[if-any lines] +
+ + + +[is annotation "annotated"] + + +[end] + + +[for lines] + [is lines.rev last_rev] + [else] + [is rowclass "vc_row_even"] + [define rowclass]vc_row_odd[end] + [else] + [define rowclass]vc_row_even[end] + [end] + [end] + + + +[is annotation "annotated"] + + +[end] + + + [define last_rev][lines.rev][end] +[end] +
#UserRevContent
[lines.line_number][is lines.rev last_rev] [else][lines.author][end][is lines.rev last_rev] [else][if-any lines.diff_href][end][lines.rev][if-any lines.diff_href][end][end][lines.text]
+
+ +[else] +[if-any image_src_href] +
+ +
+[end] +[end] +[end] + +[include "include/props.ezt"] + + +
+ +[include "include/footer.ezt"] diff --git a/templates/default/graph.ezt b/templates/default/graph.ezt new file mode 100644 index 00000000..6a1bddb9 --- /dev/null +++ b/templates/default/graph.ezt @@ -0,0 +1,89 @@ +[# setup page definitions] + [define page_title]Graph of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "graph"] + +
+[imagemap] +Revisions of [where] +
+ +[define graph_disp_opts][end] +[if-any opt_gflip][define graph_disp_opts]1[end][end] +[if-any opt_gbbox][define graph_disp_opts]1[end][end] +[if-any opt_gleft][define graph_disp_opts]1[end][end] + +[define graph_user_opts][end] +[if-any graph_disp_opts][define graph_user_opts]1[end][end] +[if-any opt_gshow][define graph_user_opts]1[end][end] +[if-any opt_gmaxtag][define graph_user_opts]1[end][end] + +[if-any graph_user_opts] + +
+
+[for graph_hidden_values][end] + +[is graph_disp_opts "1"] +

Graph display options:

+ +[if-any opt_gflip] + + + + +[end] +[if-any opt_gbbox] + + + + +[end] +[if-any opt_gleft] + + + + +[end] +
+[end] + +[if-any opt_gshow] +

Revision display options:

+ + + + + + + + + + + + + +
+[end] + +[if-any opt_gmaxtag] +

Tag limitation:

+ + + + + +
+[end] + + + +
+
+[end] + +[include "include/footer.ezt"] diff --git a/templates/default/include/diff_display.ezt b/templates/default/include/diff_display.ezt new file mode 100644 index 00000000..ca52891c --- /dev/null +++ b/templates/default/include/diff_display.ezt @@ -0,0 +1,189 @@ +[is diffs.diff_block_format "anchor"] +

+[else] + [define msg_no_changes]- No changes -[end] + [define msg_binary]- Binary content differs -[end] + [define msg_error]- ViewVC depends on rcsdiff and GNU diff to create + this page. ViewVC cannot find GNU diff. Even if you have GNU diff + installed, the rcsdiff program must be configured and compiled with + the GNU diff location. -[end] + + [define left_view_href][if-any diffs.left.prefer_markup][diffs.left.view_href][else][if-any diffs.left.download_href][diffs.left.download_href][end][end][end] + [define right_view_href][if-any diffs.right.prefer_markup][diffs.right.view_href][else][if-any diffs.right.download_href][diffs.right.download_href][end][end][end] + + [define left_item][diffs.left.path] ([if-any diffs.propname]property [diffs.propname][else]file contents[end])[end] + [define right_item][diffs.right.path] ([if-any diffs.propname]property [diffs.propname][else]file contents[end])[end] + + [is left_item right_item] + [define common_header][left_item][end] + [define left_header]Revision [if-any left_view_href][end][diffs.left.rev][if-any left_view_href][end][if-any diffs.left.author] by [diffs.left.author][end], [diffs.left.date] [if-any diffs.left.tag]
Tag: [diffs.left.tag][end][end] + [define right_header]Revision [if-any right_view_href][end][diffs.right.rev][if-any right_view_href][end][if-any diffs.right.author] by [diffs.right.author][end], [diffs.right.date] [if-any diffs.right.tag]
Tag: [diffs.right.tag][end][end] + [else] + [define common_header][end] + [define left_header][left_item], Revision [if-any left_view_href][end][diffs.left.rev][if-any left_view_href][end][if-any diffs.left.author] by [diffs.left.author][end], [diffs.left.date] [if-any diffs.left.tag]
Tag: [diffs.left.tag][end][end] + [define right_header][right_item], Revision [if-any right_view_href][end][diffs.right.rev][if-any right_view_href][end][if-any diffs.right.author] by [diffs.right.author][end], [diffs.right.date] [if-any diffs.right.tag]
Tag: [diffs.right.tag][end][end] + [end] + +

Comparing[if-any common_header] [common_header][else][end]:
+ [left_header] vs.
+ [right_header]

+[end] + +[is diffs.diff_block_format "raw"] + + [is diffs.changes.type "no-changes"] + + + + [else] + [is diffs.changes.type "binary-diff"] + + + + [else] + + [end][end] +
[msg_no_changes]
[msg_binary]
[diffs.changes.raw]
+[end] + +[is diffs.diff_block_format "sidebyside-1"] + [define change_right][end] + [define last_change_type][end] + + + [for diffs.changes] + [is diffs.changes.type "change"][else][if-any change_right][change_right][define change_right][end][end][end] + [is diffs.changes.type "header"] + + + + + [else] + [is diffs.changes.type "add"] + + + + + + [else] + [is diffs.changes.type "remove"] + + + + + + [else] + [is diffs.changes.type "change"] + [if-any diffs.changes.have_left] + + + + + + [end] + [define change_right][change_right] + [if-any diffs.changes.have_right] + + + + + + [end] + [end] + [else] + [is diffs.changes.type "no-changes"] + + [else] + [is diffs.changes.type "binary-diff"] + + [else] + [is diffs.changes.type "error"] + + [else][# a line of context] + + + + + + [end][end][end][end][end][end][end] + [define last_change_type][diffs.changes.type][end] + [end] + [if-any change_right][change_right][end] +
# + Line [diffs.changes.line_info_left] | + Line [diffs.changes.line_info_right] + [if-any diffs.changes.line_info_extra]| [diffs.changes.line_info_extra][end] +
[if-any diffs.right.annotate_href][diffs.changes.line_number][else][diffs.changes.line_number][end]+[diffs.changes.right]
[diffs.changes.line_number][diffs.changes.left]
[diffs.changes.line_number]<[diffs.changes.left]
[if-any diffs.right.annotate_href][diffs.changes.line_number][else][diffs.changes.line_number][end]>[diffs.changes.right]
[msg_no_changes]
[msg_binary]
[msg_error]
[if-any diffs.right.annotate_href][diffs.changes.line_number][else][diffs.changes.line_number][end] [diffs.changes.right]
+[end] + +[is diffs.diff_block_format "sidebyside-2"] + + + + + [for diffs.changes] + [is diffs.changes.type "no-changes"] + + + + [else] + [is diffs.changes.type "binary-diff"] + + + + [else] + [if-any diffs.changes.gap] + + + + + [end] + + [for diffs.changes.columns] + [for diffs.changes.columns.segments][if-any diffs.changes.columns.segments.type][diffs.changes.columns.segments.text][else][diffs.changes.columns.segments.text][end][end] + [end] + + [end] + [end] + [end] + +
[msg_no_changes]
[msg_binary]
[diffs.changes.columns.line_number]
+[end] + +[is diffs.diff_block_format "unified"] + + + + + + + + [for diffs.changes] + [is diffs.changes.type "no-changes"] + + + + [else] + [is diffs.changes.type "binary-diff"] + + + + [else] + [if-any diffs.changes.gap] + + + + + + [end] + + + + [for diffs.changes.segments][if-any diffs.changes.segments.type][diffs.changes.segments.text][else][diffs.changes.segments.text][end][end] + + [end] + [end] + [end] + +
[msg_no_changes]
[msg_binary]
[diffs.changes.left_number][diffs.changes.right_number]
+[end] diff --git a/templates/default/include/diff_form.ezt b/templates/default/include/diff_form.ezt new file mode 100644 index 00000000..239e95d8 --- /dev/null +++ b/templates/default/include/diff_form.ezt @@ -0,0 +1,70 @@ + +
+

This form allows you to request diffs between any two + revisions of this file. + For each of the two "sides" of the diff, +[if-any tags] + select a symbolic revision name using the selection box, or choose + 'Use Text Field' and enter a numeric revision. +[else] + enter a numeric revision. +[end] +

+ +
+ + + + + + + + + + +
  + [for diff_select_hidden_values][end] + Diffs between +[if-any tags] + + +[else] + +[end] + + and +[if-any tags] + + +[else] + +[end] +
  + Type of Diff should be a + + +
+
+
diff --git a/templates/default/include/footer.ezt b/templates/default/include/footer.ezt new file mode 100644 index 00000000..753ddf89 --- /dev/null +++ b/templates/default/include/footer.ezt @@ -0,0 +1,10 @@ + + + + + + diff --git a/templates/default/include/header.ezt b/templates/default/include/header.ezt new file mode 100644 index 00000000..3b08eaff --- /dev/null +++ b/templates/default/include/header.ezt @@ -0,0 +1,71 @@ + + + + + + + [[]ViewVC] [page_title] [if-any rootname][rootname][if-any where]/[where][end][end] + + + + [if-any rss_href] + + [end] + + + + +
+ +
+[if-any username]Logged in as: [username] |[end] +ViewVC Help +
+ + + +
+[is pathtype "dir"] + View Directory + [if-any log_href] + | Revision Log + [end] + [if-any queryform_href] + | Commit Query + [end] + [if-any tarball_href] + | Download Tarball + [end] +[end] +[is pathtype "file"] + [if-any view_href] + View File | + [end] + Revision Log + [if-any annotate_href] + | Show Annotations + [end] + [if-any graph_href] + | Revision Graph + [end] + [if-any download_href] + | Download File + [end] +[end] +[if-any revision_href] + | View Changeset +[end] +[if-any roots_href] + | Root Listing +[end] +
+ +
+[if-any roots_href]root[end][if-any nav_path]/[for nav_path][if-any nav_path.href][end][if-index nav_path last][end][nav_path.name][if-index nav_path last][end][if-any nav_path.href][end][if-index nav_path last][else]/[end][end][end] +
+ +
+ +
diff --git a/templates/default/include/pathrev_form.ezt b/templates/default/include/pathrev_form.ezt new file mode 100644 index 00000000..4625dec8 --- /dev/null +++ b/templates/default/include/pathrev_form.ezt @@ -0,0 +1,53 @@ +
+
+[for pathrev_hidden_values][end] +[is roottype "cvs"] + [define pathrev_selected][pathrev][end] + +[else] + +[end] + +
+
+ +[if-any pathrev] +
+
+[for pathrev_clear_hidden_values][end] +[if-any lastrev] + [is pathrev lastrev][else][end] + (Current path doesn't exist after revision [lastrev]) +[else] + +[end] +
+
+[end] diff --git a/templates/default/include/props.ezt b/templates/default/include/props.ezt new file mode 100644 index 00000000..b8e0ea58 --- /dev/null +++ b/templates/default/include/props.ezt @@ -0,0 +1,25 @@ +[if-any properties] +

Properties

+
+ + + + + + + + +[for properties] + + + [if-any properties.undisplayable] + + [else] + + [end] + +[end] + +
NameValue
[properties.name]Property value is undisplayable.[properties.value]
+
+[end] diff --git a/templates/default/log.ezt b/templates/default/log.ezt new file mode 100644 index 00000000..247caf36 --- /dev/null +++ b/templates/default/log.ezt @@ -0,0 +1,229 @@ +[# setup page definitions] + [define page_title]Log of:[end] + [define help_href][docroot]/help_log.html[end] +[# end] +[include "include/header.ezt" "log"] + +
+ + +
+ +[define first_revision][end] +[define last_revision][end] +[for entries] +[if-index entries first][define first_revision][entries.rev][end][end] +[if-index entries last][define last_revision][entries.rev][end][end] + +
+ +[is entries.state "dead"] + Revision [entries.rev] +[else] + + [for entries.tag_names][end] + [for entries.branch_names][end] + + Revision [is roottype "svn"][entries.rev][else][entries.rev][end] - + [if-any entries.view_href] + [is pathtype "file"] + (view) + [else] + Directory Listing + [end] + [end] + [if-any entries.download_href](download)[end] + [if-any entries.download_text_href](as text)[end] + [if-any entries.annotate_href](annotate)[end] + + [# if you don't want to allow select for diffs then remove this section] + [is entries.rev rev_selected] + - [[]selected] + [else] + - [[]select for diffs] + [end] + [end] + + [if-any entries.vendor_branch] + (vendor branch) + [end] + +
+ + [is roottype "svn"] + [if-index entries last]Added[else]Modified[end] + [end] + + [if-any entries.date][entries.date][else](unknown date)[end] + [if-any entries.ago]([entries.ago] ago)[end] + by [if-any entries.author][entries.author][else](unknown author)[end] + + [if-any entries.orig_path] +
Original Path: [entries.orig_path] + [end] + + [if-any entries.branches] +
Branch: + [for entries.branches] + [entries.branches.name][if-index entries.branches last][else],[end] + [end] + [end] + + [if-any entries.tags] +
CVS Tags: + [for entries.tags] + [entries.tags.name][if-index entries.tags last][else],[end] + [end] + [end] + + [if-any entries.branch_points] +
Branch point for: + [for entries.branch_points] + [entries.branch_points.name][if-index entries.branch_points last][else],[end] + [end] + [end] + + [if-any entries.prev] + [if-any entries.changed] + [is roottype "cvs"] +
Changes since [entries.prev]: [entries.changed] lines + [end] + [end] + [end] + + [if-any entries.lockinfo] +
Lock status: [entries.lockinfo] + [end] + + [is roottype "svn"] + [if-any entries.size] +
File length: [entries.size] byte(s) + [end] + + [if-any entries.copy_path] +
Copied from: [entries.copy_path] revision [entries.copy_rev] + [end] + [end] + + [is entries.state "dead"] +
FILE REMOVED + [else] + [if-any entries.diff_to_prev_href] +
Diff to previous [entries.prev] + [if-any human_readable] + [else] + (colored) + [end] + [end] + + [is roottype "cvs"] + [if-any entries.branch_point] + , to branch point [entries.branch_point] + [if-any human_readable] + [else] + (colored) + [end] + [end] + + [if-any entries.next_main] + , to next main [entries.next_main] + [if-any human_readable] + [else] + (colored) + [end] + [end] + [end] + + [if-any entries.diff_to_sel_href] + [if-any entries.prev], [else]
Diff[end] + to selected [rev_selected] + [if-any human_readable] + [else] + (colored) + [end] + [end] + [end] + +
[entries.log]
+
+[end] + +
+ +
+[is picklist_len "0"][else][is picklist_len "1"][else] +
+
+[for log_paging_hidden_values][end] + + +
+
+[end][end] +
+
+[for logsort_hidden_values][end] + + +
+
+
+[include "include/pathrev_form.ezt"] +
+
+ + +
+ +

Convenience Links

+ +[if-any default_branch] + + Default branch: + [for default_branch][default_branch.name][if-index default_branch last][else], [end] +[end] + +[end] + +[is pathtype "file"] +[if-any view_href] + + Links to HEAD: + + (view) + [if-any download_href](download)[end] + [if-any download_text_href](as text)[end] + [if-any annotate_href](annotate)[end] + + +[end] + +[if-any tag_view_href] + + Links to [pathrev]: + + (view) + [if-any tag_download_href](download)[end] + [if-any tag_download_text_href](as text)[end] + [if-any tag_annotate_href](annotate)[end] + + +[end] +[end] + + + +[is pathtype "file"] +

Compare Revisions

+ [include "include/diff_form.ezt"] +[end] + +[include "include/footer.ezt"] diff --git a/templates/query.ezt b/templates/default/query.ezt similarity index 100% rename from templates/query.ezt rename to templates/default/query.ezt diff --git a/templates/default/query_form.ezt b/templates/default/query_form.ezt new file mode 100644 index 00000000..8060d0ba --- /dev/null +++ b/templates/default/query_form.ezt @@ -0,0 +1,201 @@ +[# setup page definitions] + [define page_title]Query on:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "query"] + +
+ +
+ [for query_hidden_values][end] + + [is roottype "cvs"] + [# For subversion, the branch field is not used ] + + + + + [end] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Branch: + + + + + +
Subdirectory: + + (You can list multiple directories separated by commas.) +
File: + + + + + +
Who: + + + + + +
Comment: + + + + + +
Sort By: + +
Date: + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + hours +
+ + + and + +
+ (use the form yyyy-mm-dd hh:mm:ss) +
+
Limit: + Show at most + + changed files per commit. (Use 0 to show all files.) +
+
+ +
+ +[include "include/footer.ezt"] diff --git a/templates/default/query_results.ezt b/templates/default/query_results.ezt new file mode 100644 index 00000000..5f8b85ba --- /dev/null +++ b/templates/default/query_results.ezt @@ -0,0 +1,95 @@ +[# setup page definitions] + [define page_title]Query results in:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt"] + +

[english_query]

+[# ] +[if-any row_limit_reached] +

WARNING: These query results have been + artificially limited by an administrative threshold value and do + not represent the entirety of the data set which matches + the query. Consider modifying your + query to be more specific, using your version control tool's + query capabilities, or asking your administrator to raise the + database response size threshold.

+[end] +

Modify query

+

Show commands which could be used to back out these changes

+ +

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

+ +[if-any commits] + + + + + +[if-any show_branch] + +[end] + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + [for commits.files] + + + + +[if-any show_branch] + +[end] + + + + + [end] + [if-any commits.limited_files] + + + + [end] + + + + + +[end] +
RevisionFileBranch+/-DateAuthorDescription
+ [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][end][commits.files.rev][if-any rev_href][end][else] [end] + + [commits.files.dir]/ + [commits.files.file] + + [if-any commits.files.branch][commits.files.branch][else] [end] + + [# only show a diff link for changes ] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] +
  + Only first [commits.num_files] files shown. + Show all files or + adjust limit. +
 Log:
+
[commits.log]
+[end] + +[include "include/footer.ezt"] diff --git a/templates/default/revision.ezt b/templates/default/revision.ezt new file mode 100644 index 00000000..23f47c21 --- /dev/null +++ b/templates/default/revision.ezt @@ -0,0 +1,81 @@ +[# setup page definitions] + [define page_title]Revision [rev] of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "revision"] + + +
+ + + + + + + + + + + + + + + + + +
Jump to revision: + [for jump_rev_hidden_values][end] + + + [if-any prev_href] + Previous[end] + [if-any next_href] Next[end] +
Author:[if-any author][author][else](unknown author)[end]
Date:[if-any date][date][else](unknown date)[end] + [if-any ago]([ago] ago)[end]
Log Message:
[log]
+
+ + + +[include "include/footer.ezt"] diff --git a/templates/default/roots.ezt b/templates/default/roots.ezt new file mode 100644 index 00000000..08cf1779 --- /dev/null +++ b/templates/default/roots.ezt @@ -0,0 +1,40 @@ +[# setup page definitions] + [define page_title]Repository Listing[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "directory"] + +
+ + + + + + +[is cfg.options.show_roots_lastmod "1"] + +[end] + + + + +[if-any roots] +[for roots] + + +[is cfg.options.show_roots_lastmod "1"] + +[end] + +[end] +[end] + + +
NameLast Change
[roots.name][if-any roots.rev][if-any roots.log_href][roots.rev][else][roots.rev][end] ([roots.ago] ago) + by [roots.author]: [roots.log][end]
+ + +
+ +[include "include/footer.ezt"] diff --git a/templates/rss.ezt b/templates/default/rss.ezt similarity index 100% rename from templates/rss.ezt rename to templates/default/rss.ezt diff --git a/templates/diff.ezt b/templates/diff.ezt deleted file mode 100644 index 069ae4c0..00000000 --- a/templates/diff.ezt +++ /dev/null @@ -1,240 +0,0 @@ -[# setup page definitions] - [define page_title]Diff of /[where][end] - [define help_href][docroot]/help_rootview.html[end] -[# end] - -[include "include/header.ezt" "diff"] -[include "include/file_header.ezt"] - -

- -[if-any raw_diff] -
[raw_diff]
-[end] - -[define left_view_href][if-any left.prefer_markup][left.view_href][else][if-any left.download_href][left.download_href][end][end][end] -[define right_view_href][if-any right.prefer_markup][right.view_href][else][if-any right.download_href][right.download_href][end][end][end] - -[if-any changes] - - - - - - - - [for changes] - [is changes.type "header"] - - - - - - [else] - [is changes.type "add"] - - - - - - [else] - [is changes.type "remove"] - - - - - - [else] - [is changes.type "change"] - - [if-any changes.have_right] - - [else] - - [end] - [if-any changes.have_left] - - [else] - - [end] - [if-any changes.have_right] - - [else] - - [end] - - [else] - [is changes.type "no-changes"] - - - - - - - [else] - [is changes.type "binary-diff"] - - - - - - - [else] - [is changes.type "error"] - - - - - - - [else] - - - - - - [end] - [end] - [end] - [end] - [end] - [end] - [end] - [end] -
- [is left.path right.path][else][left.path][end] - revision [if-any left_view_href][end][left.rev][if-any left_view_href][end], - [left.date] - [if-any left.tag]
Tag: [left.tag][end] -
- [is left.path right.path][else][right.path][end] - revision [if-any right_view_href][end][right.rev][if-any right_view_href][end], - [right.date] - [if-any right.tag]
Tag: [right.tag][end] -
# - Line [changes.line_info_left]  - [changes.line_info_extra] - - Line [changes.line_info_right]  - [changes.line_info_extra] -
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]  [changes.right]
 [changes.left] 
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.left]  [changes.right] 
 

- - No changes -
 
 

- - Binary file revisions differ -
 
 

- - ViewVC depends on rcsdiff and GNU diff to create - this page. ViewVC cannot find GNU diff. Even if you - have GNU diff installed, the rcsdiff program must be - configured and compiled with the GNU diff location. - -
 
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.left] [changes.right]
-[end] - -[if-any sidebyside] - - - - - - - - - - - [for sidebyside] - [if-any sidebyside.gap] - - - - - [end] - - [for sidebyside.columns] - [for sidebyside.columns.segments][if-any sidebyside.columns.segments.type][sidebyside.columns.segments.text][else][sidebyside.columns.segments.text][end][end] - [end] - - [end] - -
- [is left.path right.path][else][left.path][end] - Revision [left.rev] - - [is left.path right.path][else][right.path][end] - Revision [right.rev] -
[sidebyside.columns.line_number]
-[end] - -[if-any unified] - - - - - - - - - - [for unified] - [if-any unified.gap] - - - - - - [end] - - - - [for unified.segments][if-any unified.segments.type][unified.segments.text][else][unified.segments.text][end][end] - - [end] - -
r[left.rev]r[right.rev]
[unified.left_number][unified.right_number]
-[end] - -
- - - - - - -
-
-
- [for diff_format_hidden_values][end] - - -
-
-
-[if-any raw_diff] -   -[else] - - - - -
Legend:
- - - - - - - - - - - - -
Removed from v.[left.rev] 
changed lines
 Added in v.[right.rev]
-
-[end] -
- -[include "include/footer.ezt"] diff --git a/tests/timelog.py b/tests/timelog.py new file mode 100644 index 00000000..5eafe280 --- /dev/null +++ b/tests/timelog.py @@ -0,0 +1,179 @@ + +import time +import profile + +from vclib.ccvs import rcsparse +import viewvc + +try: + import tparse +except ImportError: + tparse = None + +def lines_changed(delta): + idx = 0 + added = deleted = 0 + while idx < len(delta): + op = delta[idx] + i = delta.find(' ', idx + 1) + j = delta.find('\n', i + 1) + line = int(delta[idx+1:i]) + count = int(delta[i+1:j]) + idx = j + 1 + if op == 'd': + deleted = deleted + count + else: # 'a' for adding text + added = added + count + # skip new text + while count > 0: + nl = delta.find('\n', idx) + assert nl > 0, 'missing a newline in the delta in the RCS file' + idx = nl + 1 + count = count - 1 + return added, deleted + +class FetchSink(rcsparse.Sink): + def __init__(self, which_rev=None): + self.head = self.branch = '' + self.tags = { } + self.meta = { } + self.revs = [ ] + self.base = { } + self.entries = { } + self.which = which_rev + + def set_head_revision(self, revision): + self.head = revision + + def set_principal_branch(self, branch_name): + self.branch = branch_name + + def define_tag(self, name, revision): + self.tags[name] = revision + + def define_revision(self, revision, timestamp, author, state, + branches, next): + self.meta[revision] = (timestamp, author, state) + self.base[next] = revision + for b in branches: + self.base[b] = revision + + def set_revision_info(self, revision, log, text): + timestamp, author, state = self.meta[revision] + entry = viewvc.LogEntry(revision, int(timestamp) - time.timezone, author, + state, None, log) + + # .revs is "order seen" and .entries is for random access + self.revs.append(entry) + self.entries[revision] = entry + + if revision != self.head: + added, deleted = lines_changed(text) + if revision.count('.') == 1: + # on the trunk. reverse delta. + changed = '+%d -%d' % (deleted, added) + self.entries[self.base[revision]].changed = changed + else: + # on a branch. forward delta. + changed = '+%d -%d' % (added, deleted) + self.entries[revision].changed = changed + + def parse_completed(self): + if self.which: + self.revs = [ self.entries[self.which] ] + +def fetch_log2(full_name, which_rev=None): + sink = FetchSink(which_rev) + rcsparse.parse(open(full_name, 'rb'), sink) + return sink.head, sink.branch, sink.tags, sink.revs + +def fetch_log3(full_name, which_rev=None): + sink = FetchSink(which_rev) + tparse.parse(full_name, sink) + return sink.head, sink.branch, sink.tags, sink.revs + +def compare_data(d1, d2): + if d1[:3] != d2[:3]: + print 'd1:', d1[:3] + print 'd2:', d2[:3] + return + if len(d1[3]) != len(d2[3]): + print 'len(d1[3])=%d len(d2[3])=%d' % (len(d1[3]), len(d2[3])) + return + def sort_func(e, f): + return cmp(e.rev, f.rev) + d1[3].sort(sort_func) + d2[3].sort(sort_func) + import pprint + for i in range(len(d1[3])): + if vars(d1[3][i]) != vars(d2[3][i]): + pprint.pprint((i, vars(d1[3][i]), vars(d2[3][i]))) + +def compare_fetch(full_name, which_rev=None): + # d1 and d2 are: + # ( HEAD revision, branch name, TAGS { name : revision }, [ LogEntry ] ) + d1 = viewvc.fetch_log(full_name, which_rev) + d2 = fetch_log2(full_name, which_rev) + + print 'comparing external tools vs a parser module:' + compare_data(d1, d2) + + if tparse: + d2 = fetch_log3(full_name, which_rev) + print 'comparing external tools vs the tparse module:' + compare_data(d1, d2) + +def compare_many(files): + for file in files: + print file, '...' + compare_fetch(file) + +def time_stream(stream_class, filename, n=10): + d1 = d2 = d3 = d4 = 0 + t = time.time() + for i in range(n): + ts = stream_class(open(filename, 'rb')) + while ts.get() is not None: + pass + t = time.time() - t + print t/n + +def time_fetch(full_name, which_rev=None, n=1): + times1 = [ None ] * n + times2 = [ None ] * n + for i in range(n): + t = time.time() + viewvc.fetch_log(full_name, which_rev) + times1[i] = time.time() - t + for i in range(n): + t = time.time() + fetch_log2(full_name, which_rev) + times2[i] = time.time() - t + times1.sort() + times2.sort() + i1 = int(n*.05) + i2 = int(n*.95)+1 + times1 = times1[i1:i2] + times2 = times2[i1:i2] + t1 = reduce(lambda x,y: x+y, times1, 0) / len(times1) + t2 = reduce(lambda x,y: x+y, times2, 0) / len(times2) + print "t1=%.4f (%.4f .. %.4f) t2=%.4f (%.4f .. %.4f)" % \ + (t1, times1[0], times1[-1], t2, times2[0], times2[-1]) + +def profile_stream(stream_class, filename, n=20): + p = profile.Profile() + def many_calls(filename, n): + for i in xrange(n): + ts = stream_class(open(filename, 'rb')) + while ts.get() is not None: + pass + p.runcall(many_calls, filename, n) + p.print_stats() + +def profile_fetch(full_name, which_rev=None, n=10): + p = profile.Profile() + def many_calls(full_name, which_rev, n): + for i in xrange(n): + fetch_log2(full_name, which_rev) + p.runcall(many_calls, full_name, which_rev, n) + p.print_stats() diff --git a/tools/bump-copyright-years b/tools/bump-copyright-years new file mode 100755 index 00000000..895d1584 --- /dev/null +++ b/tools/bump-copyright-years @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# +# Copyright (C) 2012-2013 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/ +# +# ----------------------------------------------------------------------- +# +# bump-copyright-years: internal tool for bumping copyright years +# +# ----------------------------------------------------------------------- +# +import sys +import os +import time +import re + +_copyright_re = re.compile(r'Copyright (\(C\)|©) ([0-9]{4}-)?([0-9]{4}) The ViewCVS Group') + +def replace_end_year(path, year): + updated = False + lines = open(path, 'r').readlines() + for i in range(len(lines)): + line = lines[i] + new_line = None + m = _copyright_re.search(line) + if not m: + continue + if m.group(2): + lines[i] = line[:m.start(3)] + year + line[m.end(3):] + open(path, 'w').write(''.join(lines)) + break + elif m.group(3) != year: + lines[i] = line[:m.end(3)] + '-' + year + line[m.end(3):] + open(path, 'w').write(''.join(lines)) + break + +def bump_years_recursive(target_dir, year): + children = os.listdir(target_dir) + for child in children: + child_path = os.path.join(target_dir, child) + if os.path.isfile(child_path): + replace_end_year(child_path, year) + elif os.path.isdir(child_path): + bump_years_recursive(child_path, year) + +def bump_years(target_dir): + year = time.strftime('%Y') + long_date = time.strftime('%B %d, %Y') + bump_years_recursive(target_dir, year) + sys.stdout.write("""\ +Copyright years bumped. Don't forget to add the following line to +the primary license file at '%s': + +
  • %s — copyright years updated
  • + +""" % (os.path.join(target_dir, 'LICENSE.html'), long_date)) + +if __name__ == "__main__": + try: + target_dir = sys.argv[1] + except: + sys.stderr.write("""\ +Usage: bump-copyright-years VIEWVC_DIRECTORY + +Recursively update the copyright years associated with files carrying +'The ViewCVS Group' copyright in and under VIEWVC_DIRECTORY to include +the current year. +""") + sys.exit(1) + bump_years(target_dir) diff --git a/tools/make-release b/tools/make-release new file mode 100755 index 00000000..a3029f34 --- /dev/null +++ b/tools/make-release @@ -0,0 +1,91 @@ +#!/bin/sh +# +# Copyright (C) 1999-2013 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/ +# +# ----------------------------------------------------------------------- +# +# make-release: internal tool for creating ViewVC releases +# +# ----------------------------------------------------------------------- +# + +### Validate input +if test $# != 2 && test $# != 1; then + echo "Usage: $0 TARGET-DIRECTORY [BRANCH]" + echo "" + echo "If BRANCH (i.e. \"tags/1.1.0\" or \"branches/1.0.x\") is not provided," + echo "the release will be rolled from trunk." + exit 1 +fi + +TARGET=${1} +if test $# = 1; then + ROOT=trunk +else + ROOT=${2} +fi + +if test -e ${TARGET}; then + echo "ERROR: must remove ${TARGET} first." + exit 1 +fi + +### Grab an export from the Subversion repository. +EXPORT_URL="http://viewvc.tigris.org/svn/viewvc/${ROOT}" +echo "Exporting '${EXPORT_URL}' into '${TARGET}'" + +for PLATFORM in unix windows; do + if test ${PLATFORM} = windows; then + EOL="--native-eol CRLF" + else + EOL="--native-eol LF" + fi + + echo "Beginning build for ${PLATFORM}:" + + echo " Exporting source code..." + svn export --quiet ${EOL} ${EXPORT_URL} ${TARGET} + + ### Various shifting, cleanup. + + # Remove some not useful directories + for JUNK in elemx \ + misc \ + notes \ + tests \ + tools \ + tparse \ + viewcvs.sourceforge.net \ + viewvc.org \ + www; do + if [ -d ${TARGET}/${JUNK} ]; then + echo " Removing ${TARGET}/${JUNK}..." + rm -r ${TARGET}/${JUNK} + fi + done + + # Make sure permissions are reasonable: + echo " Normalizing permissions..." + find ${TARGET} -print | xargs chmod uoa+r + find ${TARGET} -type d -print | xargs chmod uoa+x + + if test ${PLATFORM} = windows; then + # Create also a ZIP file for those poor souls :-) still using Windows: + echo " Creating ZIP archive..." + zip -qor9 ${TARGET}.zip ${TARGET} + else + # Cut the tarball: + echo " Creating tarball archive..." + tar cf - ${TARGET} | gzip -9 > ${TARGET}.tar.gz + fi + + # remove target directory + rm -r ${TARGET} +done +echo "Done." diff --git a/viewvc-install b/viewvc-install index 73739a5f..c52b05aa 100755 --- a/viewvc-install +++ b/viewvc-install @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- Mode: python -*- # -# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 1999-2013 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 @@ -17,7 +17,6 @@ import os import sys -import string import re import traceback import py_compile @@ -27,7 +26,6 @@ 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__ @@ -49,6 +47,10 @@ CLEAN_MODE = None 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/wsgi/viewvc.wsgi", "bin/wsgi/viewvc.wsgi", 0755, 1, 0, 0), + ("bin/wsgi/viewvc.fcgi", "bin/wsgi/viewvc.fcgi", 0755, 1, 0, 0), + ("bin/wsgi/query.wsgi", "bin/wsgi/query.wsgi", 0755, 1, 0, 0), + ("bin/wsgi/query.fcgi", "bin/wsgi/query.fcgi", 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), @@ -75,11 +77,12 @@ if sys.platform == "win32": ## List of directories for installation. ## type (source path, ## destination path, +## boolean -- optional item?, ## boolean -- prompt before replacing?) TREE_LIST = [ - ("lib", "lib", 0), - ("templates", "templates", 1), - ("templates-contrib", "templates-contrib", 1), + ("lib", "lib", 0, 0), + ("templates", "templates", 0, 1), + ("templates-contrib", "templates-contrib", 1, 1), ] @@ -97,14 +100,14 @@ def _escape(str): 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, "\\", "\\\\") + return str.replace("\\", "\\\\") 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)) + path.replace('/', os.sep)) def error(text, etype=None, evalue=None): @@ -148,7 +151,7 @@ def install_file(src_path, dst_path, mode, subst_path_vars, 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)) + dst_path = os.path.join(ROOT_DIR, dst_path.replace('/', os.sep)) destdir_path = DESTDIR + dst_path overwrite = None @@ -172,7 +175,7 @@ def install_file(src_path, dst_path, mode, subst_path_vars, # Collect the '+ ' and '- ' lines. diff_lines = [] looking_at_diff_lines = 0 - for line in string.split(ndiff_output, '\n'): + for line in ndiff_output.split('\n'): # Print line if it is a difference line if line[:2] == "+ " or line[:2] == "- " or line[:2] == "? ": diff_lines.append(line) @@ -204,11 +207,13 @@ def install_file(src_path, dst_path, mode, subst_path_vars, temp = raw_input("Do you want to [O]verwrite, [D]o " "not overwrite, or [V]iew " "differences? ") - temp = string.lower(temp[0]) + if len(temp) == 0: + continue + temp = temp[0].lower() if temp == "v" and ext not in BINARY_FILE_EXTS: print """ ---------------------------------------------------------------------------""" - print string.join(diff_lines, '\n') + '\n' + print '\n'.join(diff_lines) + '\n' print """ LEGEND A leading '- ' indicates line to remove from installed file @@ -730,7 +735,7 @@ LEGEND dst_parent = os.path.dirname(destdir_path) if not os.path.exists(dst_parent): try: - compat.makedirs(dst_parent) + os.makedirs(dst_parent) print " created %s%s" % (dst_parent, os.sep) except os.error, e: if e.errno == 17: # EEXIST: file exists @@ -761,18 +766,22 @@ LEGEND py_compile.compile(destdir_path, destdir_path + "c" , dst_path) -def install_tree(src_path, dst_path, prompt_replace): +def install_tree(src_path, dst_path, is_optional, 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.""" + files that differ from the to-be-installed version. If + IS_OPTIONAL is set, don't fuss about a missing source item.""" 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)) + dst_path = os.path.join(ROOT_DIR, dst_path.replace('/', os.sep)) + if not os.path.isdir(src_path): + print " skipping %s" % (dst_path) + return destdir_path = os.path.join(DESTDIR + dst_path) # Get a list of items in the directory. @@ -792,7 +801,7 @@ def install_tree(src_path, dst_path, prompt_replace): # 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) + install_tree(orig_src_child, orig_dst_child, 0, prompt_replace) else: set_paths = 0 compile_it = fname[-3:] == '.py' @@ -818,7 +827,7 @@ def install_tree(src_path, dst_path, prompt_replace): while 1: temp = raw_input("Do you want to [D]elete it, or [L]eave " "it as is? ") - temp = string.lower(temp[0]) + temp = temp[0].lower() if temp == "l": delete = 0 elif temp == "d": @@ -907,8 +916,7 @@ Just hit [Enter] if a default is okay. default = os.path.join(pf, "viewvc-" + version) else: default = "/usr/local/viewvc-" + version - temp = string.strip(raw_input("Installation path [%s]: " \ - % default)) + temp = raw_input("Installation path [%s]: " % (default)).strip() print if len(temp): ROOT_DIR = temp @@ -918,10 +926,8 @@ Just hit [Enter] if a default is okay. # 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)) + temp = raw_input("DESTDIR path (generally only used by package " + "maintainers) [%s]: " % (default)).strip() print if len(temp): DESTDIR = temp diff --git a/www/index.html b/www/index.html new file mode 100644 index 00000000..abd54d91 --- /dev/null +++ b/www/index.html @@ -0,0 +1,139 @@ + + +ViewVC - Version Control Repository Browser + + + + + + + +
    + +
    + +

    ViewVC — Web-based Version Control Repository Browsing

    + +
    + +

    Latest Release(s)

    + +

    The most recent stable release of ViewVC is: 1.1.20

    + +
    + +
    + +
    +

    What Is ViewVC?

    + +

    ViewVC is a browser interface for CVS and Subversion version + control repositories. It generates templatized HTML to present + navigable directory, revision, and change log listings. It can + display specific versions of files as well as diffs between those + versions. Basically, ViewVC provides the bulk of the report-like + functionality you expect out of your version control tool, but much + more prettily than the average textual command-line program + output.

    + +

    Here are some of the additional features of ViewVC:

    + +
      +
    • Support for filesystem-accessible CVS and Subversion repositories
    • +
    • Individually configurable virtual host support
    • +
    • Line-based annotation/blame display
    • +
    • Revision graph capabilities (CVS only)
    • +
    • Syntax highlighting support
    • +
    • Path-based authorization
    • +
    • Commit metadata query facilities
    • +
    • Template-driven output generation
    • +
    • Colorized, side-by-side differences
    • +
    • Tarball generation (by tag for CVS, by revision for Subversion)
    • +
    • Ability to run as CGI script, under mod_python, or as a + standalone server application
    • +
    • Regexp-based file searching
    • +
    • INI-like configuration file (no code tweaking required)
    • +
    • Localization support based on the Accept-Language request header
    • +
    + +

    For a complete list of changes present in each release, see + ViewVC's CHANGES file.

    + +
    + +
    +

    Wanna Talk About ViewVC?

    + + + + + + + + + + + + + + +
    User/Admin DiscussionDeveloper Discussion
    If you have questions about ViewVC — how to configure it, if + some behavior you are seeing is expected or not, and so on — + send email to users@viewvc.tigris.org or use your favorite IRC client to + pop into #viewvc on irc.freenode.net.If you'd like to discuss the actual development of ViewVC itself, + or submit a patch to ViewVC's sources, you can do so on our + development list, dev@viewvc.tigris.org.
    + +
    + +
    +

    Screenshots

    + +

    Who needs screenshots when you can visit and interact with running + ViewVC instances? The following sites are running various versions + of ViewVC:

    + +
      +
    • ViewVC has been integrated into CollabNet Enterprise Edition, + and is used by Tigris.org + to serve up displays of both CVS- and Subversion-backed projects + hosted there (such as the ViewVC project itself).
    • +
    • ViewVC is also deployed at SourceForge.net, + where it again serves up both CVS and Subversion repositories.
    • +
    • The Apache Software Foundation uses ViewVC on svn.apache.org.
    • +
    + +
    + +
    + + diff --git a/www/project_tools.html b/www/project_tools.html new file mode 100644 index 00000000..7324fb6c --- /dev/null +++ b/www/project_tools.html @@ -0,0 +1,43 @@ + + + + +
    + + + +
    Useful links
    + +
    + +
    diff --git a/www/viewvc-logo.png b/www/viewvc-logo.png new file mode 100644 index 00000000..6e16f3b1 Binary files /dev/null and b/www/viewvc-logo.png differ