Merge with original r2905

remotes/github/custis
Vitaliy Filippov 2013-07-18 19:13:28 +04:00
commit 56c2b61458
182 changed files with 13513 additions and 3575 deletions

180
CHANGES
View File

@ -1,6 +1,165 @@
Version 1.2.0 (released ??-???-????) 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 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 <title> 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) 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) * fix exception in rev-sorted remote Subversion directory views (issue #409)
* allow setting of page sizes for log and dir views individually (issue #402) * 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) Version 1.0.9 (released 11-Aug-2009)
* security fix: validate the 'view' parameter to avoid XSS attack * security fix: validate the 'view' parameter to avoid XSS attack

203
INSTALL
View File

@ -17,10 +17,13 @@ Congratulations on getting this far. :-)
Required Software And Configuration Needed To Run ViewVC: 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: For CVS Support:
* Python 1.5.2 or later
(http://www.python.org/)
* RCS, Revision Control System * RCS, Revision Control System
(http://www.cs.purdue.edu/homes/trinkle/RCS/) (http://www.cs.purdue.edu/homes/trinkle/RCS/)
* GNU-diff to replace diff implementations without the -u option * GNU-diff to replace diff implementations without the -u option
@ -30,11 +33,9 @@ Congratulations on getting this far. :-)
For Subversion Support: For Subversion Support:
* Python 2.0 or later
(http://www.python.org/)
* Subversion, Version Control System, 1.3.1 or later * Subversion, Version Control System, 1.3.1 or later
(binary installation and Python bindings) (binary installation and Python bindings)
(http://subversion.tigris.org/) (http://subversion.apache.org/)
Optional: Optional:
@ -168,72 +169,185 @@ checkin database working are below.
APACHE CONFIGURATION APACHE CONFIGURATION
-------------------- --------------------
1) Find out where the web server configuration file is kept. Typical 1) Locate your Apache configuration file(s).
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.
Either METHOD A: Typical locations are /etc/httpd/httpd.conf,
2) The ScriptAlias directive is very useful for pointing /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 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 into your httpd.conf file. Choose the location in httpd.conf where
also the other ScriptAlias lines reside. Some examples: also the other ScriptAlias lines reside. Some examples:
ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi
ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi
continue with step 3). ----------------------------------------
METHOD B: CGI mode in cgi-bin directory
or alternatively METHOD B: ----------------------------------------
2) Copy the CGI scripts from Copy the CGI scripts from
<VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi
to the /cgi-bin/ directory configured in your httpd.conf file. 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 <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi
to the directory of your choosing in the Document Root adding the following 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 Options +ExecCGI
AddHandler cgi-script .cgi 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" 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). ------------------------------------------
METHOD D: Using mod_python (if installed)
or if you've got Mod_Python installed you can use METHOD D: ------------------------------------------
2) Copy the Python scripts and .htaccess file from Copy the Python scripts and .htaccess file from
<VIEWVC_INSTALLATION_DIRECTORY>/bin/mod_python/ <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 In httpd.conf, make sure that "AllowOverride All" or at least
"AllowOverride FileInfo Options" are enabled for the directory "AllowOverride FileInfo Options" are enabled for the directory
you copied the files to. 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 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 feature may not work because it uses multithreading. This works fine
under Apache 2. 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 WSGIScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/viewvc.wsgi
"apache -k restart" are two common variants. On RedHat Linux it is WSGIScriptAlias /query <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/query.wsgi
done using the command "/sbin/service httpd restart" and on SuSE Linux
it is done with "rcapache restart"
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 WSGIApplicationGroup %{GLOBAL}
directives like this:
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>"> <Location "<url to viewvc.cgi>/<modname_you_wish_to_access_ctl>">
AllowOverride None 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/~checkout~/<module_name>
http://<server_name>/viewvc/<module_name>.tar.gz?view=tar 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 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 links to other pages and views, you can expect your server's performance

View File

@ -15,7 +15,7 @@
<blockquote> <blockquote>
<p><strong>Copyright &copy; 1999-2008 The ViewCVS Group. All rights <p><strong>Copyright &copy; 1999-2013 The ViewCVS Group. All rights
reserved.</strong></p> reserved.</strong></p>
<p>By using ViewVC, you agree to the terms and conditions set forth <p>By using ViewVC, you agree to the terms and conditions set forth
@ -59,6 +59,11 @@
<li>March 17, 2006 &mdash; software renamed from "ViewCVS"</li> <li>March 17, 2006 &mdash; software renamed from "ViewCVS"</li>
<li>April 10, 2007 &mdash; copyright years updated</li> <li>April 10, 2007 &mdash; copyright years updated</li>
<li>February 22, 2008 &mdash; copyright years updated</li> <li>February 22, 2008 &mdash; copyright years updated</li>
<li>March 18, 2009 &mdash; copyright years updated</li>
<li>March 29, 2010 &mdash; copyright years updated</li>
<li>February 18, 2011 &mdash; copyright years updated</li>
<li>January 23, 2012 &mdash; copyright years updated</li>
<li>January 04, 2013 &mdash; copyright years updated</li>
</ul> </ul>
</body> </body>

View File

@ -3,7 +3,7 @@
# -*-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 # 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 # 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) server = sapi.AspServer(Server, Request, Response, Application)
try: try:
cfg = viewvc.load_config(CONF_PATHNAME, server) 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: finally:
s.close() s.close()

View File

@ -3,7 +3,7 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 # 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 # 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 import sys
@ -47,6 +47,20 @@ else:
"../../../lib"))) "../../../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 sapi
import viewvc import viewvc
@ -54,4 +68,7 @@ import query
server = sapi.CgiServer() server = sapi.CgiServer()
cfg = viewvc.load_config(CONF_PATHNAME, server) 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)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 # 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 # 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 import sys
@ -47,12 +47,21 @@ else:
"../../../lib"))) "../../../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 sapi
import viewvc import viewvc

View File

@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -40,7 +40,6 @@ else:
######################################################################### #########################################################################
import os import os
import string
import cvsdb import cvsdb
import viewvc import viewvc
import vclib.ccvs import vclib.ccvs
@ -62,7 +61,7 @@ def UpdateFile(db, repository, path, update, latest_checkin, quiet_level, encodi
except vclib.ItemNotFound, e: except vclib.ItemNotFound, e:
return return
file = string.join(path, "/") file = '/'.join(path)
printing = 0 printing = 0
if update: if update:
if quiet_level < 1 or (quiet_level < 2 and len(commit_list)): if quiet_level < 1 or (quiet_level < 2 and len(commit_list)):

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -39,7 +39,6 @@ else:
######################################################################### #########################################################################
import os import os
import string
import getopt import getopt
import re import re
import cvsdb import cvsdb
@ -152,11 +151,11 @@ def FindLongestDirectory(s, repository):
and a file name, either of which may contain spaces. Returns the longest and a file name, either of which may contain spaces. Returns the longest
possible directory name that actually exists""" possible directory name that actually exists"""
parts = string.split(s, " ") parts = s.split()
for i in range(len(parts)-1, 0, -1): for i in range(len(parts)-1, 0, -1):
directory = string.join(parts[:i]) directory = ' '.join(parts[:i])
filename = string.join(parts[i:]) filename = ' '.join(parts[i:])
if os.path.isdir(os.path.join(repository, directory)): if os.path.isdir(os.path.join(repository, directory)):
return directory, filename return directory, filename
@ -227,7 +226,7 @@ def ProcessLoginfo(rootpath, directory, files):
cfg.utilities, 0) cfg.utilities, 0)
# split up the directory components # 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 ## build a list of Commit objects
commit_list = [] commit_list = []
@ -279,7 +278,7 @@ if __name__ == '__main__':
else: else:
# if there are no arguments, read version information from # if there are no arguments, read version information from
# first line of input like old versions of ViewCVS did # 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 len(sys.argv) > 2:
# if there is a second argument it indicates which parser # if there is a second argument it indicates which parser

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 # 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 # 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): def index(req):
server = sapi.ModPythonServer(req) server = sapi.ModPythonServer(req)
try: 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: finally:
server.close() server.close()

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 # Copyright (C) 2004-2007 James Henstridge
# #
# By using this file, you agree to the terms and conditions set forth in # 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) fsroot = self._get_root_for_rev(rev)
# find changes in the revision # 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) e_ptr, e_baton = svn.delta.make_editor(editor)
svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton) svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton)
@ -274,25 +274,32 @@ class SvnRev:
continue continue
# deal with the change types we handle # deal with the change types we handle
action = None
base_root = None base_root = None
base_path = change.base_path
if change.base_path: if change.base_path:
base_root = self._get_root_for_rev(change.base_rev) base_root = self._get_root_for_rev(change.base_rev)
# :( f*cking undocumented python-subversion! # figure out what kind of change this is, and get a diff
# change.action: modify=0, add=1, delete=2, replace=3 # object for it. note that prior to 1.4 Subversion's
if change.action == 2 or not change.path: # 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' action = 'remove'
elif change.added: elif change.added:
action = 'add' action = 'add'
else: else:
action = 'change' action = 'change'
diffobj = svn.fs.FileDiff(base_root and base_root or None, if action == 'remove':
base_root and change.base_path or None, diffobj = svn.fs.FileDiff(base_root, change.base_path, None, None, None, ['-b', '-B'])
action != 'remove' and change.path and fsroot or None, else:
action != 'remove' and change.path and change.path or None, diffobj = svn.fs.FileDiff(base_root, change.base_path, fsroot, change.path, None, ['-b', '-B'])
None,
['-b', '-B'])
diff_fp = diffobj.get_pipe() diff_fp = diffobj.get_pipe()
diff_fp = StupidBufferedReader(diff_fp) diff_fp = StupidBufferedReader(diff_fp)
plus, minus = _get_diff_counts(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) db = cvsdb.ConnectDatabase(cfg)
repository = os.path.realpath(repository) repository = os.path.realpath(repository)
# Purge what must be purged.
if command in ('rebuild', 'purge'): if command in ('rebuild', 'purge'):
if verbose: if verbose:
print "Purging commit info for repository root `%s'" % repository 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, svn_ignore_mimetype = cfg.options.svn_ignore_mimetype,
verbose = verbose, verbose = verbose,
) )
# Record what must be recorded.
if command == 'rebuild' or (command == 'update' and not revs): if command == 'rebuild' or (command == 'update' and not revs):
for rev in range(repo.rev_max+1): for rev in range(repo.rev_max+1):
handle_revision(db, command, repo, rev, verbose, force) handle_revision(db, command, repo, rev, verbose, force)
@ -456,7 +465,7 @@ def usage():
located at REPOS-PATH. located at REPOS-PATH.
Usage: 1. %s [-v] rebuild 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 3. %s [-v] purge REPOS-PATH
1. Rebuild the commit database information for the repository located 1. Rebuild the commit database information for the repository located
@ -500,17 +509,11 @@ if __name__ == '__main__':
if len(args) < 3: if len(args) < 3:
usage() usage()
command = string.lower(args[1]) command = args[1].lower()
if command not in ('rebuild', 'update', 'purge'): if command not in ('rebuild', 'update', 'purge'):
sys.stderr.write('ERROR: unknown command %s\n' % command) sys.stderr.write('ERROR: unknown command %s\n' % command)
usage() 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 = [] revs = []
if len(sys.argv) > 3: if len(sys.argv) > 3:
if command == 'rebuild': if command == 'rebuild':
@ -533,6 +536,7 @@ if __name__ == '__main__':
rev = None rev = None
try: try:
repository = vclib.svn.canonicalize_rootpath(args[2])
repository = cvsdb.CleanRepository(os.path.abspath(repository)) repository = cvsdb.CleanRepository(os.path.abspath(repository))
main(command, repository, revs, verbose, force) main(command, repository, revs, verbose, force)
except KeyboardInterrupt: except KeyboardInterrupt:

54
bin/wsgi/query.fcgi Normal file
View File

@ -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()

45
bin/wsgi/query.wsgi Normal file
View File

@ -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 []

50
bin/wsgi/viewvc.fcgi Normal file
View File

@ -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()

41
bin/wsgi/viewvc.wsgi Normal file
View File

@ -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 []

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,213 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>ViewVC: 1.1.0 Release Notes</title>
<style>
.h2, .h3 {
padding: 0.25em 0em;
background: white;
}
.warning {
font-style: italic;
}
</style>
</head>
<body>
<h1>ViewVC 1.1.0 Release Notes</h1>
<div class="h2">
<h2 id="introduction">Introduction</h2>
<p>ViewVC 1.1.0 is the superset of all previous ViewVC releases.</p>
</div> <!-- h2 -->
<div class="h2">
<h2 id="compatibility">Compatibility</h2>
<p>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).</p>
<p>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 <code>make-database</code>
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 <code>--version=1.0</code> option
to <code>make-database</code>.</p>
<p>The ViewVC configuration files and template language have changed
dramatically. See the file <code>docs/upgrading-howto.html</code>
in the release for information on porting existing versions of
those items for use with ViewVC 1.1.0.</p>
</div> <!-- h2 -->
<div class="h2">
<h2 id="compatibility">Features and Fixes</h2>
<div class="h3">
<h3 id="">Extensible path-based authorization w/ Subversion authz support</h3>
<p>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 &mdash; 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.</p>
<p>Note that ViewVC does <strong>not</strong> 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.</p>
<p class="warning">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 <code>roots</code> from the set of views
listed in the <code>allowed_views</code> configuration option.
<strong>UPDATE: This was fixed in ViewVC 1.1.3.</strong></p>
<p class="warning">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. <strong>UPDATE: This was fixed in ViewVC
1.1.15.</strong></p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Subversion versioned properties display</h3>
<p>ViewVC 1.1 displays the properties that Subversion lets you store
on files and directories
(<code>svn:mime-type</code>, <code>svn:mergeinfo</code>,
<code>svn:ignore</code>, 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.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Unified markup and annotation views</h3>
<p>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.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Unified, hassle-free Pygments-based syntax highlighting</h3>
<p>ViewVC 1.0 does syntax highlighting by working with GNU enscript, or
highlight, or php, or py2html &mdash; 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. <a href="http://www.pygments.org/">Pygments</a> (which is
also used by <a href="http://trac.edgewall.org/">Trac</a> 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.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Better MIME detection and handling</h3>
<p>ViewVC typically consults a MIME types file to determine what kind
of file a given document is, based on its filename extension
(<code>.jpg</code> = <code>image/jpeg</code>, &hellip;). But
Subversion lets you dictate a file's MIME type using
the <code>svn:mime-type</code> 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 <code>application/octet-stream</code> MIME type that
Subversion uses by default for non-human-readable files.</p>
<p>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.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Support for full content diffs</h3>
<p>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).</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Support for per-root configuration overrides</h3>
<p>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 <code>viewvc.conf.dist</code> file for more on
how to do this.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Optional email address obfuscation/mangling</h3>
<p>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).</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Pagination improvements</h3>
<p>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.</p>
</div> <!-- h3 -->
</div> <!-- h2 -->
</body>
</html>

View File

@ -0,0 +1,49 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>ViewVC: 1.2.0 Release Notes</title>
<style>
.h2, .h3 {
padding: 0.25em 0em;
background: white;
}
.warning {
font-style: italic;
}
</style>
</head>
<body>
<h1>ViewVC 1.2.0 Release Notes</h1>
<div class="h2">
<h2 id="introduction">Introduction</h2>
<p>ViewVC 1.2.0 is the superset of all previous ViewVC releases.</p>
</div> <!-- h2 -->
<div class="h2">
<h2 id="compatibility">Compatibility</h2>
<p>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).</p>
</div> <!-- h2 -->
<div class="h2">
<h2 id="compatibility">Features and Fixes</h2>
<div class="h3">
<h3 id=""></h3>
</div> <!-- h3 -->
</div> <!-- h2 -->
</body>
</html>

View File

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

View File

@ -28,12 +28,12 @@ td {
.h3 { border-width: 1px 0 0 0; } .h3 { border-width: 1px 0 0 0; }
.toc-list { font-size: 90%; } .toc-list { font-size: 90%; }
.varname { font-family: monospace; } .varname { font-family: monospace; }
.added { background: rgb(50%,75%,25%); } .added { background: rgb(60%,90%,60%); }
.unchanged { background: rgb(75%,75%,75%); } .unchanged { background: rgb(75%,75%,75%); }
.renamed { background: rgb(75%,50%,75%); } .renamed { background: rgb(80%,60%,80%); }
.changed { background: rgb(100%,100%,25%); } .changed { background: rgb(100%,100%,50%); }
.replaced { background: rgb(100%,75%,0%); } .replaced { background: rgb(100%,80%,40%); }
.removed { background: rgb(100%,25%,25%); } .removed { background: rgb(100%,70%,70%); }
</style> </style>
</head> </head>
@ -67,12 +67,21 @@ td {
<h2 id="toc">Table of Contents</h2> <h2 id="toc">Table of Contents</h2>
<ul class="toc-list"> <ul class="toc-list">
<li><a href="#introduction">Introduction</a></li> <li><a href="#introduction">Introduction</a></li>
<li><a href="#sec-from-1-1">Upgrading From ViewVC 1.1</a></li>
<li><a href="#sec-from-1-0">Upgrading From ViewVC 1.0</a></li> <li><a href="#sec-from-1-0">Upgrading From ViewVC 1.0</a></li>
<li><a href="#sec-from-0-9">Upgrading From ViewCVS 0.9</a></li> <li><a href="#sec-from-0-9">Upgrading From ViewCVS 0.9</a></li>
<li><a href="#sec-from-0-8">Upgrading From ViewCVS 0.8</a></li> <li><a href="#sec-from-0-8">Upgrading From ViewCVS 0.8</a></li>
</ul> </ul>
</div> </div>
<div class="h2">
<h2 id="sec-from-1-0">Upgrading From ViewVC 1.1</h2>
<p>This section discusses how to upgrade ViewVC 1.1.x to ViewVC 1.2.x.</p>
</div>
<div class="h2"> <div class="h2">
<h2 id="sec-from-1-0">Upgrading From ViewVC 1.0</h2> <h2 id="sec-from-1-0">Upgrading From ViewVC 1.0</h2>
@ -314,7 +323,7 @@ td {
all = viewvc.* all = viewvc.*
[all-options] [all-options]
allow_tar = 1 allowed_views = annotate, diff, markup, tar
</pre> </pre>
</blockquote> </blockquote>
@ -324,7 +333,7 @@ allow_tar = 1
all = viewvc.* all = viewvc.*
[vhost-all/options] [vhost-all/options]
allow_tar = 1 allowed_views = annotate, diff, markup, tar
</pre> </pre>
</blockquote> </blockquote>
@ -1563,13 +1572,8 @@ allow_tar = 1
<div class="h2"> <div class="h2">
<h2 id="sec-from-0-8">Upgrading From ViewCVS 0.8</h2> <h2 id="sec-from-0-8">Upgrading From ViewCVS 0.8</h2>
<p>This section discusses how to upgrade ViewCVS 0.8 to version <p>This section discusses how to upgrade ViewCVS 0.8 to ViewCVS 0.9.x.</p>
0.9 or a later version of the software.</p>
<p><strong>NOTE:</strong> these changes will bring you up to the
requirements of version 0.9. You must also follow the directions
for <a href="#sec-from-0-9">upgrading from 0.9</a>.</p>
<div class="h3"> <div class="h3">
<h3>Configuration Options</h3> <h3>Configuration Options</h3>
@ -1579,92 +1583,50 @@ allow_tar = 1
options, then you will need to make corresponding changes in the options, then you will need to make corresponding changes in the
templates.</p> templates.</p>
<dl> <dl>
<dt> <dt>Colors: <code>diff_heading</code>, <code>diff_empty</code>,
Colors: <code>diff_remove</code>, <code>diff_change</code>,
<strong>diff_heading</strong>, <code>diff_add</code>, and <code>diff_dark_change</code></dt>
<strong>diff_empty</strong>, <dd>These options have been incorporated into the
<strong>diff_remove</strong>, <code>diff.ezt</code> template.</dd>
<strong>diff_change</strong>,
<strong>diff_add</strong>,
and <strong>diff_dark_change</strong>
</dt>
<dd>
These options have been incorporated into the
<code>diff.ezt</code> template.
<p></p> <dt><code>markup_log</code></dt>
</dd> <dd>This option has been incorporated into the
<code>markup.ezt</code> template.</dd>
<dt><strong>markup_log</strong></dt> <dt>Colors: <code>nav_header</code> and
<dd> <code>alt_background</code></dt>
This option has been incorporated into the <dd>These options have been incorporated into the
<code>markup.ezt</code> template. <code>header.ezt</code> template.</dd>
<p></p> <dt>Images: <code>back_icon</code>, <code>dir_icon</code>,
</dd> and <code>file_icon</code></dt>
<dd>These options have been incorporated into the
<code>directory.ezt</code>, <code>header.ezt</code>,
<code>log.ezt</code>, <code>log_table.ezt</code>, and
<code>query.ezt</code> templates.</dd>
<dt>Colors: <strong>nav_header</strong> <dt><code>use_java_script</code>
and <strong>alt_background</strong></dt> and <code>open_extern_window</code></dt>
<dd> <dd>The templates now use JavaScript in all applicable places, and
These options have been incorporated into the open external windows for most downloading and viewing of
<code>header.ezt</code> template. files. If you wish to not use JavaScript and/or external
windows, then remove the feature(s) from the templates.</dd>
<p></p> <dt><code>show_author</code></dt>
</dd> <dd>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.</dd>
<dt> <dt><code>hide_non_readable</code></dt>
Images: <dd>This option was never used, so it has been removed.</dd>
<strong>back_icon</strong>,
<strong>dir_icon</strong>,
and <strong>file_icon</strong>
</dt>
<dd>
These options have been incorporated into the
<code>directory.ezt</code>, <code>header.ezt</code>,
<code>log.ezt</code>, <code>log_table.ezt</code>, and
<code>query.ezt</code> templates.
<p></p> <dt><code>flip_links_in_dirview</code></dt>
</dd> <dd>This option is no longer available. If you want the links in
your directory view flipped, then you may use the
<code>dir_alternate.ezt</code> template.</dd>
<dt><strong>use_java_script</strong> </dl>
and <strong>open_extern_window</strong></dt>
<dd>
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.
<p></p>
</dd>
<dt><strong>show_author</strong></dt>
<dd>
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.
<p></p>
</dd>
<dt><strong>hide_non_readable</strong></dt>
<dd>
This option was never used, so it has been removed.
<p></p>
</dd>
<dt><strong>flip_links_in_dirview</strong></dt>
<dd>
This option is no longer available. If you want the links in
your directory view flipped, then you may use the
<code>dir_alternate.ezt</code> template.
<p></p>
</dd>
</dl>
</div> </div>
@ -1675,53 +1637,65 @@ allow_tar = 1
removed in 0.9. If you have custom templates that refer to these removed in 0.9. If you have custom templates that refer to these
variables, then you will need to modify your templates.</p> variables, then you will need to modify your templates.</p>
<dl> <table>
<dt><code>directory.ezt</code>: <var>headers</var></dt> <thead>
<dd> <tr>
The headers are now listed explicitly in the template, <th>Variable</th>
rather than made available through a list. <th>Location</th>
<p></p> <th>Changes</th>
</dd> </tr>
</thead>
<dt> <tbody>
<code>directory.ezt</code>: <tr class="removed">
<var>rows.cols</var>, <td class="varname">headers</td>
and <var>rows.span</var> <td>directory.ezt</td>
</dt> <td>removed; headers are now listed explicitly in the template, rather
<dd> than made available through a list.</td>
These variables were used in conjunction with the </tr>
<var>headers</var> variable to control the column <tr class="removed">
displays. This is now controlled explicitly within the <td class="varname">rows.cols</td>
templates. <td>directory.ezt</td>
<p></p> <td>removed; was used in conjunction with the <var>headers</var>
</dd> variable to control the column displays. This is now controlled
explicitly within the templates.</td>
<dt><code>directory.ezt</code>: </tr>
<var>rev_in_front</var></dt> <tr class="removed">
<dd> <td class="varname">rows.span</td>
This was used to indicate that revision links should <td>directory.ezt</td>
be used in the first column, rather than in their <td>removed; was used in conjunction with the <var>headers</var>
standard place in the second column. Changing the variable to control the column displays. This is now controlled
links should now be done in the template, rather than explicitly within the templates.</td>
according to this variable. You may want to look at </tr>
the <code>dir_alternate.ezt</code> template, which has <tr class="removed">
the revision in front. <td class="varname">rev_in_front</td>
<p></p> <td>directory.ezt</td>
</dd> <td>removed; was used to indicate that revision links should be used in
the first column, rather than in their standard place in the
<dt><code>directory.ezt</code>: second column. Changing the links should now be done in the
<var>rows.attic</var> template, rather than according to this variable. You may want
and <var>rows.hide_attic_href</var></dt> to look at the <code>dir_alternate.ezt</code> template, which
<dd> has the revision in front.</dd>
These variable were used to manage the hide and </tr>
showing of the contents of the <code>Attic/</code> <tr class="removed">
subdirectory. Several new variables were introduced <td class="varname">rows.attic</td>
which can be used to replace this functionality: <td>directory.ezt</td>
<var>show_attic_href</var>, <td>removed; used to manage the hide and showing of the
<var>hide_attic_href</var>, and <var>rows.state</var>. contents of the <code>Attic/</code> subdirectory. Several new
<p></p> variables were introduced which can be used to replace this
</dd> functionality: <var>show_attic_href</var>,
</dl> <var>hide_attic_href</var>, and <var>rows.state</var>.</td>
</tr>
<tr class="removed">
<td class="varname">rows.hide_attic_href</td>
<td>directory.ezt</td>
<td>removed; used to manage the hide and showing of the
contents of the <code>Attic/</code> subdirectory. Several new
variables were introduced which can be used to replace this
functionality: <var>show_attic_href</var>,
<var>hide_attic_href</var>, and <var>rows.state</var>.</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>

View File

@ -654,6 +654,35 @@ th.caption {
<td>depends</td> <td>depends</td>
<td><a href="#root-param"><code>root</code> parameter</a></td> <td><a href="#root-param"><code>root</code> parameter</a></td>
</tr> </tr>
<tr>
<td><code>gflip=<var>GFLIP</var></code></td>
<td>optional</td>
<td>"1" if the revisions in the graph should run
youngest-to-oldest; "0" for the reverse</td>
</tr>
<tr>
<td><code>gbbox=<var>GBBOX</var></code></td>
<td>optional</td>
<td>"1" if the revision graph should contain branch boxes at the
tip of each branch; "0" otherwise</td>
</tr>
<tr>
<td><code>gleft=<var>GLEFT</var></code></td>
<td>optional</td>
<td>"1" if the revision graph should be orientated left-to-right;
"0" otherwise</td>
</tr>
<tr>
<td><code>gmaxtag=<var>GMAXTAG</var></code></td>
<td>optional</td>
<td>maximum number of per-revision tags to show in the revision graph</td>
</tr>
<tr>
<td><code>gshow=<var>GSHOW</var></code></td>
<td>optional</td>
<td>"all", "inittagged", or "tagged" &mdash; user-selected classes
of revision to show in the graph</td>
</tr>
</table> </table>
<h3 id="graphimg-view">Graph Image View</h3> <h3 id="graphimg-view">Graph Image View</h3>
@ -700,6 +729,35 @@ th.caption {
<td>depends</td> <td>depends</td>
<td><a href="#root-param"><code>root</code> parameter</a></td> <td><a href="#root-param"><code>root</code> parameter</a></td>
</tr> </tr>
<tr>
<td><code>gflip=<var>GFLIP</var></code></td>
<td>optional</td>
<td>"1" if the revisions in the graph should run
youngest-to-oldest; "0" for the reverse</td>
</tr>
<tr>
<td><code>gbbox=<var>GBBOX</var></code></td>
<td>optional</td>
<td>"1" if the revision graph should contain branch boxes at the
tip of each branch; "0" otherwise</td>
</tr>
<tr>
<td><code>gleft=<var>GLEFT</var></code></td>
<td>optional</td>
<td>"1" if the revision graph should be orientated left-to-right;
"0" otherwise</td>
</tr>
<tr>
<td><code>gmaxtag=<var>GMAXTAG</var></code></td>
<td>optional</td>
<td>maximum number of per-revision tags to show in the revision graph</td>
</tr>
<tr>
<td><code>gshow=<var>GSHOW</var></code></td>
<td>optional</td>
<td>"all", "inittagged", or "tagged" &mdash; user-selected classes
of revision to show in the graph</td>
</tr>
</table> </table>
<h3 id="log-view">Log View</h3> <h3 id="log-view">Log View</h3>
@ -996,7 +1054,7 @@ th.caption {
<td>file query string</td> <td>file query string</td>
</tr> </tr>
<tr> <tr>
<td><code>file_match=FILE_MATCH</code></td> <td><code>file_match=<var>FILE_MATCH</var></code></td>
<td>optional</td> <td>optional</td>
<td>"exact" "like" "glob" "regex" or "notregex" determining type <td>"exact" "like" "glob" "regex" or "notregex" determining type
of file match</td> of file match</td>
@ -1007,7 +1065,7 @@ th.caption {
<td>author query string</td> <td>author query string</td>
</tr> </tr>
<tr> <tr>
<td><code>who_match=WHO_MATCH</code></td> <td><code>who_match=<var>WHO_MATCH</var></code></td>
<td>optional</td> <td>optional</td>
<td>"exact" "like" "glob" "regex" or "notregex" determining type <td>"exact" "like" "glob" "regex" or "notregex" determining type
of author match</td> of author match</td>
@ -1024,36 +1082,36 @@ th.caption {
of log message match</td> of log message match</td>
</tr> </tr>
<tr> <tr>
<td><code>querysort=SORT</code></td> <td><code>querysort=<var>SORT</var></code></td>
<td>optional</td> <td>optional</td>
<td>"date" "author" or "file" determining order of query results</td> <td>"date" "author" or "file" determining order of query results</td>
</tr> </tr>
<tr> <tr>
<td><code>date=DATE</code></td> <td><code>date=<var>DATE</var></code></td>
<td>optional</td> <td>optional</td>
<td>"hours" "day" "week" "month" "all" or "explicit" to filter <td>"hours" "day" "week" "month" "all" or "explicit" to filter
query results by date</td> query results by date</td>
</tr> </tr>
<tr> <tr>
<td><code>hours=HOURS</code></td> <td><code>hours=<var>HOURS</var></code></td>
<td>optional</td> <td>optional</td>
<td>number of hours back to include results from when <td>number of hours back to include results from when
<code><var>DATE</var></code> is "hours"</td> <code><var>DATE</var></code> is "hours"</td>
</tr> </tr>
<tr> <tr>
<td><code>mindate=MINDATE</code></td> <td><code>mindate=<var>MINDATE</var></code></td>
<td>optional</td> <td>optional</td>
<td>earliest date to include results from when <td>earliest date to include results from when
<code><var>DATE</var></code> is "explicit"</td> <code><var>DATE</var></code> is "explicit"</td>
</tr> </tr>
<tr> <tr>
<td><code>maxdate=MAXDATE</code></td> <td><code>maxdate=<var>MAXDATE</var></code></td>
<td>optional</td> <td>optional</td>
<td>latest date to include results from when <td>latest date to include results from when
<code><var>DATE</var></code> is "explicit"</td> <code><var>DATE</var></code> is "explicit"</td>
</tr> </tr>
<tr> <tr>
<td><code>limit_changes=LIMIT_CHANGES</code></td> <td><code>limit_changes=<var>LIMIT_CHANGES</var></code></td>
<td>optional</td> <td>optional</td>
<td>maximum number of files to list per commit in query <td>maximum number of files to list per commit in query
results. Default is value of <code>limit_changes</code> results. Default is value of <code>limit_changes</code>
@ -1113,7 +1171,7 @@ th.caption {
<td>branch query string</td> <td>branch query string</td>
</tr> </tr>
<tr> <tr>
<td><code>branch_match=BRANCH_MATCH</code></td> <td><code>branch_match=<var>BRANCH_MATCH</var></code></td>
<td>optional</td> <td>optional</td>
<td>"exact" "like" "glob" "regex" or "notregex" determining type <td>"exact" "like" "glob" "regex" or "notregex" determining type
of branch match</td> of branch match</td>
@ -1129,7 +1187,7 @@ th.caption {
<td>file query string</td> <td>file query string</td>
</tr> </tr>
<tr> <tr>
<td><code>file_match=FILE_MATCH</code></td> <td><code>file_match=<var>FILE_MATCH</var></code></td>
<td>optional</td> <td>optional</td>
<td>"exact" "like" "glob" "regex" or "notregex" determining type <td>"exact" "like" "glob" "regex" or "notregex" determining type
of file match</td> of file match</td>
@ -1140,7 +1198,7 @@ th.caption {
<td>author query string</td> <td>author query string</td>
</tr> </tr>
<tr> <tr>
<td><code>who_match=WHO_MATCH</code></td> <td><code>who_match=<var>WHO_MATCH</var></code></td>
<td>optional</td> <td>optional</td>
<td>"exact" "like" "glob" "regex" or "notregex" determining type <td>"exact" "like" "glob" "regex" or "notregex" determining type
of author match</td> of author match</td>
@ -1157,50 +1215,43 @@ th.caption {
of log message match</td> of log message match</td>
</tr> </tr>
<tr> <tr>
<td><code>querysort=SORT</code></td> <td><code>querysort=<var>SORT</var></code></td>
<td>optional</td> <td>optional</td>
<td>"date" "author" or "file" determining order of query results</td> <td>"date" "author" or "file" determining order of query results</td>
</tr> </tr>
<tr> <tr>
<td><code>date=DATE</code></td> <td><code>date=<var>DATE</var></code></td>
<td>optional</td> <td>optional</td>
<td>"hours" "day" "week" "month" "all" or "explicit" to filter <td>"hours" "day" "week" "month" "all" or "explicit" to filter
query results by date</td> query results by date</td>
</tr> </tr>
<tr> <tr>
<td><code>hours=HOURS</code></td> <td><code>hours=<var>HOURS</var></code></td>
<td>optional</td> <td>optional</td>
<td>number of hours back to include results from when <td>number of hours back to include results from when
<code><var>DATE</var></code> is "hours"</td> <code><var>DATE</var></code> is "hours"</td>
</tr> </tr>
<tr> <tr>
<td><code>mindate=MINDATE</code></td> <td><code>mindate=<var>MINDATE</var></code></td>
<td>optional</td> <td>optional</td>
<td>earliest date to include results from when <td>earliest date to include results from when
<code><var>DATE</var></code> is "explicit"</td> <code><var>DATE</var></code> is "explicit"</td>
</tr> </tr>
<tr> <tr>
<td><code>maxdate=MAXDATE</code></td> <td><code>maxdate=<var>MAXDATE</var></code></td>
<td>optional</td> <td>optional</td>
<td>latest date to include results from when <td>latest date to include results from when
<code><var>DATE</var></code> is "explicit"</td> <code><var>DATE</var></code> is "explicit"</td>
</tr> </tr>
<tr> <tr>
<td><code>format=FORMAT</code></td> <td><code>format=<var>FORMAT</var></code></td>
<td>optional</td> <td>optional</td>
<td>"rss" or "backout" values to generate an rss feed or list of <td>"rss" or "backout" values to generate an rss feed or list of
commands to back out changes instead showing a normal query result commands to back out changes instead showing a normal query result
page</td> page</td>
</tr> </tr>
<tr> <tr>
<td><code>limit=LIMIT</code></td> <td><code>limit_changes=<var>LIMIT_CHANGES</var></code></td>
<td>optional</td>
<td>maximum number of file-revisions to process during a
query. Default is value of <code>row_limit</code> configuration
option</td>
</tr>
<tr>
<td><code>limit_changes=LIMIT_CHANGES</code></td>
<td>optional</td> <td>optional</td>
<td>maximum number of files to list per commit in query <td>maximum number of files to list per commit in query
results. Default is value of <code>limit_changes</code> results. Default is value of <code>limit_changes</code>
@ -1254,7 +1305,7 @@ th.caption {
<td><a href="#revision-param"><code>revision</code> parameter</a></td> <td><a href="#revision-param"><code>revision</code> parameter</a></td>
</tr> </tr>
<tr> <tr>
<td><code>limit_changes=LIMIT_CHANGES</code></td> <td><code>limit_changes=<var>LIMIT_CHANGES</var></code></td>
<td>optional</td> <td>optional</td>
<td>maximum number of files to list per commit. Default is value <td>maximum number of files to list per commit. Default is value
of <code>limit_changes</code> configuration option</td> of <code>limit_changes</code> configuration option</td>

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -15,7 +15,6 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import re import re
import string
def language(hdr): def language(hdr):
@ -40,7 +39,7 @@ def _parse(hdr, result):
name = _re_token.match(hdr, pos) name = _re_token.match(hdr, pos)
if not name: if not name:
raise AcceptLanguageParseError() raise AcceptLanguageParseError()
a = result.item_class(string.lower(name.group(1))) a = result.item_class(name.group(1).lower())
pos = name.end() pos = name.end()
while 1: while 1:
# are we looking at a parameter? # are we looking at a parameter?
@ -56,7 +55,7 @@ def _parse(hdr, result):
# the "=" was probably missing # the "=" was probably missing
continue continue
pname = string.lower(match.group(1)) pname = match.group(1).lower()
if pname == 'q' or pname == 'qs': if pname == 'q' or pname == 'qs':
try: try:
a.quality = float(match.group(2)) a.quality = float(match.group(2))
@ -70,7 +69,7 @@ def _parse(hdr, result):
# bad float literal # bad float literal
pass pass
elif pname == 'charset': elif pname == 'charset':
a.charset = string.lower(match.group(2)) a.charset = match.group(2).lower()
result.append(a) result.append(a)
if hdr[pos:pos+1] == ',': if hdr[pos:pos+1] == ',':

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 <curt@hagenlocher.org> # Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
# #
# By using this file, you agree to the terms and conditions set forth in # By using this file, you agree to the terms and conditions set forth in
@ -27,14 +27,14 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import sys import sys
import string
import os import os
import re import re
import time import time
import math import math
import cgi
import vclib
from common import _item
import vclib
import sapi
re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"') re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"')
@ -43,7 +43,7 @@ def link_includes(text, repos, path_parts, include_url):
if match: if match:
incfile = match.group(3) incfile = match.group(3)
include_path_parts = path_parts[:-1] include_path_parts = path_parts[:-1]
for part in filter(None, string.split(incfile, '/')): for part in filter(None, incfile.split('/')):
if part == "..": if part == "..":
if not include_path_parts: if not include_path_parts:
# nothing left to pop; don't bother marking up this include. # 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 include_path = None
try: try:
if repos.itemtype(include_path_parts, None) == vclib.FILE: 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: except vclib.ItemNotFound:
pass pass
if include_path: if include_path:
return '#%sinclude%s<a href="%s">"%s"</a>' % \ return '#%sinclude%s<a href="%s">"%s"</a>' % \
(match.group(1), match.group(2), (match.group(1), match.group(2),
string.replace(include_url, '/WHERE/', include_path), incfile) include_url.replace('/WHERE/', include_path), incfile)
return text return text
@ -75,14 +75,15 @@ class HTMLBlameSource:
self.path_parts = path_parts self.path_parts = path_parts
self.diff_url = diff_url self.diff_url = diff_url
self.include_url = include_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): def __getitem__(self, idx):
item = self.annotation.__getitem__(idx) item = self.annotation.__getitem__(idx)
diff_url = None diff_url = None
if item.prev_rev: if item.prev_rev:
diff_url = '%sr1=%s&amp;r2=%s' % (self.diff_url, item.prev_rev, item.rev) diff_url = '%sr1=%s&amp;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) self.path_parts, self.include_url)
return _item(text=thisline, line_number=item.line_number, return _item(text=thisline, line_number=item.line_number,
rev=item.rev, prev_rev=item.prev_rev, 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 return source, source.revision
class _item:
def __init__(self, **kw):
vars(self).update(kw)
def make_html(root, rcs_path): def make_html(root, rcs_path):
import vclib.ccvs.blame import vclib.ccvs.blame
bs = vclib.ccvs.blame.BlameSource(os.path.join(root, rcs_path)) bs = vclib.ccvs.blame.BlameSource(os.path.join(root, rcs_path))
@ -136,7 +132,7 @@ def make_html(root, rcs_path):
sys.stdout.write('<td>&nbsp;</td><td>&nbsp;</td>') sys.stdout.write('<td>&nbsp;</td><td>&nbsp;</td>')
rev_count = rev_count + 1 rev_count = rev_count + 1
sys.stdout.write('<td%s>%s</td></tr>\n' % (align % 'left', string.rstrip(thisline) or '&nbsp;')) sys.stdout.write('<td%s>%s</td></tr>\n' % (align % 'left', thisline.rstrip() or '&nbsp;'))
sys.stdout.write('</table>\n') sys.stdout.write('</table>\n')

60
lib/common.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -16,7 +16,6 @@
import sys import sys
import os import os
import string
import ConfigParser import ConfigParser
import fnmatch import fnmatch
import vclib import vclib
@ -29,99 +28,164 @@ from viewvcmagic import ContentMagic
######################################################################### #########################################################################
# #
# CONFIGURATION # CONFIGURATION
# -------------
# #
# There are three forms of configuration: # There are three forms of configuration:
# #
# 1) edit the viewvc.conf created by the viewvc-install(er) # 1. edit the viewvc.conf created by the viewvc-install(er)
# 2) as (1), but delete all unchanged entries from viewvc.conf # 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 # 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 # 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 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. # 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: class Config:
_sections = ('general', 'utilities', 'options', 'cvsdb', 'templates', 'rewritehtml') _base_sections = (
_force_multi_value = ('cvs_roots', 'svn_roots', 'languages', 'kv_files', # Base configuration sections.
'root_parents', 'allowed_views', 'mime_types_files') '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): def __init__(self):
self.__guesser = None 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()) 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.conf_path = os.path.isfile(pathname) and pathname or None
self.base = os.path.dirname(pathname) self.base = os.path.dirname(pathname)
self.parser = ConfigParser.ConfigParser() self.parser = ConfigParser.ConfigParser()
self.parser.optionxform = lambda x: x # don't case-normalize option names.
self.parser.read(self.conf_path or []) self.parser.read(self.conf_path or [])
for section in self._sections: for section in self.parser.sections():
if self.parser.has_section(section): if self._is_allowed_section(section, self._base_sections):
self._process_section(self.parser, section, section) self._process_section(self.parser, section, section)
if vhost and self.parser.has_section('vhosts'): if vhost and self.parser.has_section('vhosts'):
self._process_vhost(self.parser, vhost) 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): 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() kv = _sub_config()
for fname in self.general.kv_files: for fname in self.general.kv_files:
if fname[0] == '[': if fname[0] == '[':
idx = string.index(fname, ']') idx = fname.index(']')
parts = string.split(fname[1:idx], '.') parts = fname[1:idx].split('.')
fname = string.strip(fname[idx+1:]) fname = fname[idx+1:].strip()
else: else:
parts = [ ] parts = [ ]
fname = string.replace(fname, '%lang%', language) fname = fname.replace('%lang%', language)
parser = ConfigParser.ConfigParser() parser = ConfigParser.ConfigParser()
parser.optionxform = lambda x: x # don't case-normalize option names.
parser.read(os.path.join(self.base, fname)) parser.read(os.path.join(self.base, fname))
for section in parser.sections(): for section in parser.sections():
for option in parser.options(section): for option in parser.options(section):
@ -139,75 +203,109 @@ class Config:
return kv return kv
def path(self, path): 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) return os.path.join(self.base, path)
def _process_section(self, parser, section, subcfg_name): 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) sc = getattr(self, subcfg_name)
for opt in parser.options(section): for opt in parser.options(section):
value = parser.get(section, opt) value = parser.get(section, opt)
if opt in self._force_multi_value: 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: else:
try: try:
value = int(value) value = int(value)
except ValueError: except ValueError:
pass pass
### FIXME: This feels like unnecessary depth of knowledge for a
### semi-generic configuration object.
if opt == 'cvs_roots' or opt == 'svn_roots': if opt == 'cvs_roots' or opt == 'svn_roots':
value = _parse_roots(opt, value) value = _parse_roots(opt, value)
setattr(sc, 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): 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) canon_vhost = self._find_canon_vhost(parser, vhost)
if not canon_vhost: if not canon_vhost:
return return
# overlay any option sections associated with this vhost name # Overlay any option sections associated with this vhost name.
cv = 'vhost-%s/' % (canon_vhost)
lcv = len(cv)
for section in parser.sections(): for section in parser.sections():
if section[:lcv] == cv: base_section = self._is_allowed_override('vhost', canon_vhost, section)
base_section = section[lcv:] if base_section:
if base_section not in self._sections:
raise IllegalOverrideSection('vhost', section)
self._process_section(parser, section, base_section) self._process_section(parser, section, base_section)
def _find_canon_vhost(self, parser, vhost): 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'): for canon_vhost in parser.options('vhosts'):
value = parser.get('vhosts', canon_vhost) value = parser.get('vhosts', canon_vhost)
patterns = map(string.lower, map(string.strip, patterns = map(lambda x: x.lower().strip(),
filter(None, string.split(value, ',')))) filter(None, value.split(',')))
for pat in patterns: for pat in patterns:
if fnmatch.fnmatchcase(vhost, pat): if fnmatch.fnmatchcase(vhost, pat):
return canon_vhost return canon_vhost
return None 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): 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: if not self.conf_path:
return 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): def _get_parser_items(self, parser, section):
"""Basically implement ConfigParser.items() for pre-Python-2.3 versions.""" """Basically implement ConfigParser.items() for pre-Python-2.3 versions."""
@ -219,23 +317,66 @@ class Config:
d[option] = parser.get(section, option) d[option] = parser.get(section, option)
return d.items() return d.items()
def get_authorizer_params(self, authorizer, rootname=None): def get_authorizer_and_params_hack(self, rootname):
if not self.conf_path: """Return a 2-tuple containing the name and parameters of the
return {} 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 = {} params = {}
authz_section = 'authz-%s' % (authorizer) 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(): 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): for key, value in self._get_parser_items(self.parser, section):
params[key] = value params[key] = value
if rootname: return authorizer, params
root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer)
for section in self.parser.sections(): def get_authorizer_params(self, authorizer=None):
if section == root_authz_section: """Return a dictionary of parameter names and values which belong
for key, value in self._get_parser_items(self.parser, section): to the configured authorizer (or AUTHORIZER, if provided)."""
params[key] = value params = {}
params['__config'] = self 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 return params
def guesser(self): def guesser(self):
@ -272,11 +413,14 @@ class Config:
self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots'] self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots']
self.options.authorizer = None self.options.authorizer = None
self.options.mangle_email_addresses = 0 self.options.mangle_email_addresses = 0
self.options.custom_log_formatting = []
self.options.default_file_view = "log" self.options.default_file_view = "log"
self.options.binary_mime_types = []
self.options.http_expiration_time = 600 self.options.http_expiration_time = 600
self.options.generate_etags = 1 self.options.generate_etags = 1
self.options.svn_ignore_mimetype = 0 self.options.svn_ignore_mimetype = 0
self.options.svn_config_dir = None self.options.svn_config_dir = None
self.options.max_filesize_kbytes = 512
self.options.use_rcsparse = 0 self.options.use_rcsparse = 0
self.options.sort_by = 'file' self.options.sort_by = 'file'
self.options.sort_group_dirs = 1 self.options.sort_group_dirs = 1
@ -291,15 +435,18 @@ class Config:
self.options.hr_ignore_keyword_subst = 1 self.options.hr_ignore_keyword_subst = 1
self.options.hr_intraline = 0 self.options.hr_intraline = 0
self.options.allow_compress = 0 self.options.allow_compress = 0
self.options.template_dir = "templates" self.options.template_dir = "templates/default"
self.options.docroot = None self.options.docroot = None
self.options.show_subdir_lastmod = 0 self.options.show_subdir_lastmod = 0
self.options.show_roots_lastmod = 0
self.options.show_logs = 1 self.options.show_logs = 1
self.options.show_log_in_markup = 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.use_localtime = 0
self.options.iso8601_timestamps = 0
self.options.short_log_len = 80 self.options.short_log_len = 80
self.options.enable_syntax_coloration = 1 self.options.enable_syntax_coloration = 1
self.options.tabsize = 8
self.options.detect_encoding = 0 self.options.detect_encoding = 0
self.options.use_cvsgraph = 0 self.options.use_cvsgraph = 0
self.options.cvsgraph_conf = "cvsgraph.conf" self.options.cvsgraph_conf = "cvsgraph.conf"
@ -307,6 +454,7 @@ class Config:
self.options.use_re_search = 0 self.options.use_re_search = 0
self.options.dir_pagesize = 0 self.options.dir_pagesize = 0
self.options.log_pagesize = 0 self.options.log_pagesize = 0
self.options.log_pagesextra = 3
self.options.limit_changes = 100 self.options.limit_changes = 100
self.options.cvs_ondisk_charset = 'cp1251' self.options.cvs_ondisk_charset = 'cp1251'
self.options.binary_mime_re = '^(?!text/|.*\Wxml)' self.options.binary_mime_re = '^(?!text/|.*\Wxml)'
@ -352,17 +500,19 @@ class Config:
'after_match: </span>\n'\ 'after_match: </span>\n'\
'chunk_separator: ... \n\n' 'chunk_separator: ... \n\n'
self.query.viewvc_base_url = None
def _startswith(somestr, substr): def _startswith(somestr, substr):
return somestr[:len(substr)] == substr return somestr[:len(substr)] == substr
def _parse_roots(config_name, config_value): def _parse_roots(config_name, config_value):
roots = { } roots = { }
for root in config_value: for root in config_value:
pos = string.find(root, ':') try:
if pos < 0: name, path = root.split(':', 1)
except:
raise MalformedRoot(config_name, root) raise MalformedRoot(config_name, root)
name, path = map(string.strip, (root[:pos], root[pos+1:])) roots[name.strip()] = path.strip()
roots[name] = path
return roots return roots
class ViewVCConfigurationError(Exception): class ViewVCConfigurationError(Exception):
@ -388,10 +538,3 @@ class MalformedRoot(ViewVCConfigurationError):
class _sub_config: class _sub_config:
pass 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
+ ']*=\)\(.*\)$')

View File

@ -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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -11,7 +11,6 @@
import os import os
import sys import sys
import string
import time import time
import re import re
import cgi import cgi
@ -219,6 +218,9 @@ class CheckinDatabase:
return list return list
def GetCommitsTable(self):
return self._version >= 1 and 'commits' or 'checkins'
def GetTableList(self): def GetTableList(self):
sql = "SHOW TABLES" sql = "SHOW TABLES"
cursor = self.db.cursor() cursor = self.db.cursor()
@ -384,13 +386,11 @@ class CheckinDatabase:
'descid' : self.GetDescriptionID(commit.GetDescription()), 'descid' : self.GetDescriptionID(commit.GetDescription()),
} }
commits_table = self._version >= 1 and 'commits' or 'checkins'
cursor = self.db.cursor() cursor = self.db.cursor()
try: try:
# MySQL-specific INSERT-or-UPDATE with ID retrieval # MySQL-specific INSERT-or-UPDATE with ID retrieval
cursor.execute( 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('%s' for i in props)+') ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id), '+
', '.join(i+'=VALUES('+i+')' for i in props), ', '.join(i+'=VALUES('+i+')' for i in props),
tuple(props[i] for i in props) tuple(props[i] for i in props)
@ -447,8 +447,10 @@ class CheckinDatabase:
elif query_entry.match == "glob": elif query_entry.match == "glob":
# check if the match is exact # check if the match is exact
if not re.match(r'(\*|\?|\[.*\])', data): if not re.match(r'(\*|\?|\[.*\])', data):
# most optimal is just '=' for exact matches
match = "=" match = "="
else: else:
# LIKE is more optimal than REGEXP
data = data.replace('%', '\\%') data = data.replace('%', '\\%')
data = data.replace('_', '\\_') data = data.replace('_', '\\_')
data = data.replace('*', '%') data = data.replace('*', '%')
@ -465,7 +467,7 @@ class CheckinDatabase:
if match != '': if match != '':
sqlList.append("%s%s%s" % (field, match, self.db.literal(data))) 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): def query_ids(self, in_field, table, id_field, name_field, lst):
if not len(lst): if not len(lst):
@ -529,7 +531,7 @@ class CheckinDatabase:
) )
def CreateSQLQueryString(self, query): def CreateSQLQueryString(self, query):
commits_table = self._version >= 1 and 'commits' or 'checkins' commits_table = self.GetCommitsTable()
fields = [ fields = [
commits_table+".*", commits_table+".*",
"repositories.repository AS repository_name", "repositories.repository AS repository_name",
@ -618,17 +620,18 @@ class CheckinDatabase:
if cond is not None: joinConds.append(cond) if cond is not None: joinConds.append(cond)
fields = string.join(fields, ",") fields = string.join(fields, ",")
tables = string.join(tables, ",") tables = ",".join(tables)
conditions = string.join(joinConds + condList, " AND ") conditions = " AND ".join(joinConds + condList)
conditions = conditions and "WHERE %s" % conditions conditions = conditions and "WHERE %s" % conditions
## limit the number of rows requested or we could really slam ## apply the query's row limit, if any (so we avoid really
## a server with a large database ## slamming a server with a large database)
limit = "" limit = ""
if query.limit: if query.limit:
limit = "LIMIT %s" % (str(query.limit)) if detect_leftover:
elif self._row_limit: limit = "LIMIT %s" % (str(query.limit + 1))
limit = "LIMIT %s" % (str(self._row_limit)) else:
limit = "LIMIT %s" % (str(query.limit))
sql = "SELECT %s FROM %s %s %s %s" % ( sql = "SELECT %s FROM %s %s %s %s" % (
fields, tables, conditions, order_by, limit) fields, tables, conditions, order_by, limit)
@ -636,6 +639,7 @@ class CheckinDatabase:
return sql return sql
# Check access to dir/file in repository repos # 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): def check_commit_access(self, repos, dir, file, rev):
r = self.request.get_repo(repos) r = self.request.get_repo(repos)
if not r: if not r:
@ -799,13 +803,12 @@ class CheckinDatabase:
if file_id == None: if file_id == None:
return None return None
commits_table = self._version >= 1 and 'commits' or 'checkins'
sql = "SELECT whoid FROM %s WHERE "\ sql = "SELECT whoid FROM %s WHERE "\
" repositoryid=%%s "\ " repositoryid=%%s "\
" AND dirid=%%s"\ " AND dirid=%%s"\
" AND fileid=%%s"\ " AND fileid=%%s"\
" AND revision=%%s"\ " AND revision=%%s"\
% (commits_table) % (self.GetCommitsTable())
sql_args = (repository_id, dir_id, file_id, commit.GetRevision()) sql_args = (repository_id, dir_id, file_id, commit.GetRevision())
cursor = self.db.cursor() cursor = self.db.cursor()
@ -821,10 +824,9 @@ class CheckinDatabase:
def sql_delete(self, table, key, value, keep_fkey = None): def sql_delete(self, table, key, value, keep_fkey = None):
sql = "DELETE FROM %s WHERE %s=%%s" % (table, key) sql = "DELETE FROM %s WHERE %s=%%s" % (table, key)
sql_args = (value, ) sql_args = (value, )
commits_table = self._version >= 1 and 'commits' or 'checkins'
if keep_fkey: if keep_fkey:
sql += " AND %s NOT IN (SELECT %s FROM %s WHERE %s = %%s)" \ 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) sql_args = (value, value)
cursor = self.db.cursor() cursor = self.db.cursor()
cursor.execute(sql, sql_args) cursor.execute(sql, sql_args)
@ -842,7 +844,7 @@ class CheckinDatabase:
raise UnknownRepositoryError("Unknown repository '%s'" raise UnknownRepositoryError("Unknown repository '%s'"
% (repository)) % (repository))
checkins_table = self._version >= 1 and 'commits' or 'checkins' checkins_table = self.GetCommitsTable()
# Purge checkins # Purge checkins
cursor = self.db.cursor() cursor = self.db.cursor()
@ -1085,8 +1087,9 @@ class QueryEntry:
self.data = data self.data = data
self.match = match self.match = match
## CheckinDatabaseQueryData is a object which contains the search parameters ## CheckinDatabaseQuery is an object which contains the search
## for a query to the CheckinDatabase ## parameters for a query to the Checkin Database and -- after the
## query is executed -- the data returned by the query.
class CheckinDatabaseQuery: class CheckinDatabaseQuery:
def __init__(self): def __init__(self):
## sorting ## sorting
@ -1112,7 +1115,8 @@ class CheckinDatabaseQuery:
## limit on number of rows to return ## limit on number of rows to return
self.limit = None self.limit = None
self.limit_reached = 0
## list of commits -- filled in by CVS query ## list of commits -- filled in by CVS query
self.commit_list = [] self.commit_list = []
@ -1120,6 +1124,9 @@ class CheckinDatabaseQuery:
## are added ## are added
self.commit_cb = None self.commit_cb = None
## has this query been run?
self.executed = 0
def SetTextQuery(self, query): def SetTextQuery(self, query):
self.text_query = query self.text_query = query
@ -1180,6 +1187,20 @@ class CheckinDatabaseQuery:
def AddCommit(self, commit): def AddCommit(self, commit):
self.commit_list.append(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 ## entrypoints
@ -1190,13 +1211,15 @@ def CreateCommit():
def CreateCheckinQuery(): def CreateCheckinQuery():
return CheckinDatabaseQuery() return CheckinDatabaseQuery()
def ConnectDatabase(cfg, request=None, readonly=0): def ConnectDatabase(cfg, readonly=0):
db = CheckinDatabase( if readonly:
readonly = readonly, user = cfg.cvsdb.readonly_user
request = request, passwd = cfg.cvsdb.readonly_passwd
cfg = cfg.cvsdb, else:
guesser = cfg.guesser(), user = cfg.cvsdb.user
) passwd = cfg.cvsdb.passwd
db = CheckinDatabase(cfg.cvsdb.host, cfg.cvsdb.port, user, passwd,
cfg.cvsdb.database_name)
db.Connect() db.Connect()
return db return db
@ -1207,7 +1230,7 @@ def ConnectDatabaseReadOnly(cfg, request):
def GetCommitListFromRCSFile(repository, path_parts, revision=None): def GetCommitListFromRCSFile(repository, path_parts, revision=None):
commit_list = [] commit_list = []
directory = string.join(path_parts[:-1], "/") directory = "/".join(path_parts[:-1])
file = path_parts[-1] file = path_parts[-1]
revs = repository.itemlog(path_parts, revision, vclib.SORTBY_DEFAULT, revs = repository.itemlog(path_parts, revision, vclib.SORTBY_DEFAULT,
@ -1224,7 +1247,7 @@ def GetCommitListFromRCSFile(repository, path_parts, revision=None):
if rev.changed: if rev.changed:
# extract the plus/minus and drop the sign # extract the plus/minus and drop the sign
plus, minus = string.split(rev.changed) plus, minus = rev.changed.split()
commit.SetPlusCount(plus[1:]) commit.SetPlusCount(plus[1:])
commit.SetMinusCount(minus[1:]) commit.SetMinusCount(minus[1:])

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -14,7 +14,7 @@ import sys
import time import time
import types import types
import re import re
import compat import calendar
import MySQLdb import MySQLdb
# set to 1 to store commit times in UTC, or 0 to use the ViewVC machine's # 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() t = datetime.tuple()
if utc_time: if utc_time:
return compat.timegm(t) return calendar.timegm(t)
else: else:
return time.mktime(t[:8] + (-1,)) return time.mktime(t[:8] + (-1,))

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC

View File

@ -347,7 +347,7 @@ class Template:
for_names = [ ] for_names = [ ]
if base_format: 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)): for i in range(len(parts)):
piece = parts[i] piece = parts[i]
@ -405,13 +405,13 @@ class Template:
elif cmd == 'format': elif cmd == 'format':
if args[1][0]: if args[1][0]:
# argument is a variable reference # argument is a variable reference
printer = args[1] formatter = args[1]
else: else:
# argument is a string constant referring to built-in printer # argument is a string constant referring to built-in formatter
printer = _printers.get(args[1][1]) formatter = _formatters.get(args[1][1])
if not printer: if not formatter:
raise UnknownFormatConstantError(str(args[1:])) 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 # remember the cmd, current pos, args, and a section placeholder
stack.append([cmd, len(program), args[1:], None]) stack.append([cmd, len(program), args[1:], None])
@ -465,13 +465,13 @@ class Template:
except TypeError: except TypeError:
raise Exception("Unprintable value type for '%s'" % (str(valrefs[0][0]))) raise Exception("Unprintable value type for '%s'" % (str(valrefs[0][0])))
def _cmd_format(self, printer, ctx): def _cmd_format(self, formatter, ctx):
if type(printer) is TupleType: if type(formatter) is TupleType:
printer = _get_value(printer, ctx) formatter = _get_value(formatter, ctx)
ctx.printers.append(printer) ctx.formatters.append(formatter)
def _cmd_end_format(self, valref, ctx): def _cmd_end_format(self, valref, ctx):
ctx.printers.pop() ctx.formatters.pop()
def _cmd_include(self, (valref, reader), ctx): def _cmd_include(self, (valref, reader), ctx):
fname = _get_value(valref, ctx) fname = _get_value(valref, ctx)
@ -637,14 +637,23 @@ def _get_value((refname, start, rest), ctx):
# string or a sequence # string or a sequence
return ob 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): def _write_value(value, args, ctx):
# value is a callback function, generates its own output # value is a callback function, generates its own output
if callable(value): if callable(value):
apply(value, [ctx] + list(args)) apply(value, [ctx] + list(args))
return return
# pop printer in case it recursively calls _write_value # squirrel away formatters in case one of them recursively calls
printer = ctx.printers.pop() # _write_value() -- we'll use them (in reverse order) to format our
# output.
formatters = ctx.formatters[:]
formatters.reverse()
try: try:
# if the value has a 'read' attribute, then it is a stream: copy it # 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) chunk = value.read(16384)
if not chunk: if not chunk:
break break
printer(ctx, chunk) _print_formatted(formatters, ctx, chunk)
# value is a substitution pattern # value is a substitution pattern
elif args: elif args:
@ -666,57 +675,23 @@ def _write_value(value, args, ctx):
piece = args[idx] piece = args[idx]
else: else:
piece = '<undef>' piece = '<undef>'
printer(ctx, piece) _print_formatted(formatters, ctx, piece)
# plain old value, write to output # plain old value, write to output
else: else:
printer(ctx, value) _print_formatted(formatters, ctx, value)
finally: finally:
ctx.printers.append(printer) # restore our formatters
formatters.reverse()
ctx.formatters = formatters
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)
class Context: class Context:
"""A container for the execution context""" """A container for the execution context"""
def __init__(self, fp): def __init__(self, fp):
self.fp = fp self.fp = fp
self.printers = [] self.formatters = []
def write(self, value, args=()): def write(self, value, args=()):
_write_value(value, args, self) _write_value(value, args, self)
@ -829,26 +804,34 @@ class BaseUnavailableError(EZTException):
class UnknownFormatConstantError(EZTException): class UnknownFormatConstantError(EZTException):
"""The format specifier is an unknown value.""" """The format specifier is an unknown value."""
def _raw_printer(ctx, s): def _raw_formatter(s):
try: s = s.encode('utf-8') try: s = s.encode('utf-8')
except: pass except: pass
ctx.fp.write(s) return s
def _html_printer(ctx, s): def _html_formatter(s):
try: s = s.encode('utf-8') try: s = s.encode('utf-8')
except: pass 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') try: s = s.encode('utf-8')
except: pass except: pass
ctx.fp.write(urllib.quote(s)) s = s.replace('&', '&#x26;')
s = s.replace('<', '&#x3C;')
s = s.replace('>', '&#x3E;')
return s
_printers = { def _uri_formatter(s):
FORMAT_RAW : _raw_printer, try: s = s.encode('utf-8')
FORMAT_HTML : _html_printer, except: pass
FORMAT_XML : _html_printer, return urllib.quote(s)
FORMAT_URI : _uri_printer,
_formatters = {
FORMAT_RAW : _raw_formatter,
FORMAT_HTML : _html_formatter,
FORMAT_XML : _xml_formatter,
FORMAT_URI : _uri_formatter,
} }
# --- standard test environment --- # --- standard test environment ---

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # 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 difflib
import sys import sys
import re import re
from common import _item, _RCSDIFF_NO_CHANGES
import ezt import ezt
import cgi import sapi
def sidebyside(fromlines, tolines, context): def sidebyside(fromlines, tolines, context):
"""Generate side by side diff""" """Generate side by side diff"""
@ -29,6 +31,7 @@ def sidebyside(fromlines, tolines, context):
line_strip = lambda line: line.rstrip("\n") line_strip = lambda line: line.rstrip("\n")
fromlines = map(line_strip, fromlines) fromlines = map(line_strip, fromlines)
tolines = map(line_strip, tolines) tolines = map(line_strip, tolines)
had_changes = 0
gap = False gap = False
for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context): for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context):
@ -37,8 +40,11 @@ def sidebyside(fromlines, tolines, context):
else: else:
from_item = _mdiff_split(flag, fromdata) from_item = _mdiff_split(flag, fromdata)
to_item = _mdiff_split(flag, todata) 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 gap = False
if not had_changes:
yield _item(type=_RCSDIFF_NO_CHANGES)
_re_mdiff = re.compile("\0([+-^])(.*?)\1") _re_mdiff = re.compile("\0([+-^])(.*?)\1")
@ -49,18 +55,18 @@ def _mdiff_split(flag, (line_number, text)):
while True: while True:
m = _re_mdiff.search(text, pos) m = _re_mdiff.search(text, pos)
if not m: if not m:
segments.append(_item(text=cgi.escape(text[pos:]), type=None)) segments.append(_item(text=sapi.escape(text[pos:]), type=None))
break break
if m.start() > pos: 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) == "+": 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) == "-": 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) == "^": 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() pos = m.end()
@ -71,19 +77,26 @@ def unified(fromlines, tolines, context):
diff = difflib.Differ().compare(fromlines, tolines) diff = difflib.Differ().compare(fromlines, tolines)
lastrow = None lastrow = None
had_changes = 0
for row in _trim_context(diff, context): for row in _trim_context(diff, context):
if row[0].startswith("? "): if row[0].startswith("? "):
had_changes = 1
yield _differ_split(lastrow, row[0]) yield _differ_split(lastrow, row[0])
lastrow = None lastrow = None
else: else:
if lastrow: if lastrow:
had_changes = 1
yield _differ_split(lastrow, None) yield _differ_split(lastrow, None)
lastrow = row lastrow = row
if lastrow: if lastrow:
had_changes = 1
yield _differ_split(lastrow, None) yield _differ_split(lastrow, None)
if not had_changes:
yield _item(type=_RCSDIFF_NO_CHANGES)
def _trim_context(lines, context_size): def _trim_context(lines, context_size):
"""Trim context lines that don't surround changes from Differ results """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): for m in _re_differ.finditer(guide, pos):
if m.start() > pos: if m.start() > pos:
segments.append(_item(text=cgi.escape(line[pos:m.start()]), type=None)) segments.append(_item(text=sapi.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[m.start():m.end()]),
type="change")) type="change"))
pos = m.end() 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, return _item(gap=ezt.boolean(gap), type=type, segments=segments,
left_number=left_number, right_number=right_number) left_number=left_number, right_number=right_number)
class _item:
def __init__(self, **kw):
vars(self).update(kw)
try: try:
### Using difflib._mdiff function here was the easiest way of obtaining ### Using difflib._mdiff function here was the easiest way of obtaining
### intraline diffs for use in ViewVC, but it doesn't exist prior to ### intraline diffs for use in ViewVC, but it doesn't exist prior to

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -23,7 +23,6 @@ import os
import sys import sys
import sapi import sapi
import threading import threading
import string
if sys.platform == "win32": if sys.platform == "win32":
import win32popen import win32popen
@ -36,7 +35,7 @@ def popen(cmd, args, mode, capture_err=1):
if sys.platform == "win32": if sys.platform == "win32":
command = win32popen.CommandLine(cmd, args) command = win32popen.CommandLine(cmd, args)
if string.find(mode, 'r') >= 0: if mode.find('r') >= 0:
hStdIn = None hStdIn = None
if debug.SHOW_CHILD_PROCESSES: if debug.SHOW_CHILD_PROCESSES:
@ -85,7 +84,7 @@ def popen(cmd, args, mode, capture_err=1):
# in the parent # in the parent
# close the descriptor that we don't need and return the other one. # 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) os.close(w)
return _pipe(os.fdopen(r, mode), pid) return _pipe(os.fdopen(r, mode), pid)
os.close(r) 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 # we'll need /dev/null for the discarded I/O
null = os.open('/dev/null', os.O_RDWR) 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 # hook stdout/stderr to the "write" channel
os.dup2(w, 1) os.dup2(w, 1)
# "close" stdin; the child shouldn't use it # "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)) os.execvp(cmd, (cmd,) + tuple(args))
except: except:
# aid debugging, if the os.execvp above fails for some reason: # aid debugging, if the os.execvp above fails for some reason:
print "<h2>exec failed:</h2><pre>", cmd, string.join(args), "</pre>" print "<h2>exec failed:</h2><pre>", cmd, ' '.join(args), "</pre>"
raise raise
# crap. shouldn't be here. # crap. shouldn't be here.

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -23,6 +23,7 @@ import sys
import string import string
import time import time
from common import _item, TemplateData
import cvsdb import cvsdb
import viewvc import viewvc
import ezt import ezt
@ -57,7 +58,7 @@ class FormData:
self.valid = 1 self.valid = 1
try: try:
self.repository = string.strip(form["repository"].value) self.repository = form["repository"].value.strip()
except KeyError: except KeyError:
pass pass
except TypeError: except TypeError:
@ -66,7 +67,7 @@ class FormData:
self.valid = 1 self.valid = 1
try: try:
self.branch = string.strip(form["branch"].value) self.branch = form["branch"].value.strip()
except KeyError: except KeyError:
pass pass
except TypeError: except TypeError:
@ -75,7 +76,7 @@ class FormData:
self.valid = 1 self.valid = 1
try: try:
self.directory = string.strip(form["directory"].value) self.directory = form["directory"].value.strip()
except KeyError: except KeyError:
pass pass
except TypeError: except TypeError:
@ -84,7 +85,7 @@ class FormData:
self.valid = 1 self.valid = 1
try: try:
self.file = string.strip(form["file"].value) self.file = form["file"].value.strip()
except KeyError: except KeyError:
pass pass
except TypeError: except TypeError:
@ -93,7 +94,7 @@ class FormData:
self.valid = 1 self.valid = 1
try: try:
self.who = string.strip(form["who"].value) self.who = form["who"].value.strip()
except KeyError: except KeyError:
pass pass
except TypeError: except TypeError:
@ -102,14 +103,14 @@ class FormData:
self.valid = 1 self.valid = 1
try: try:
self.sortby = string.strip(form["sortby"].value) self.sortby = form["sortby"].value.strip()
except KeyError: except KeyError:
pass pass
except TypeError: except TypeError:
pass pass
try: try:
self.date = string.strip(form["date"].value) self.date = form["date"].value.strip()
except KeyError: except KeyError:
pass pass
except TypeError: except TypeError:
@ -168,7 +169,7 @@ def listparse_string(str):
## command; add the command and start over ## command; add the command and start over
elif c == ",": elif c == ",":
## strip ending whitespace on un-quoted data ## strip ending whitespace on un-quoted data
temp = string.rstrip(temp) temp = temp.rstrip()
return_list.append( ("", temp) ) return_list.append( ("", temp) )
temp = "" temp = ""
state = "eat leading whitespace" state = "eat leading whitespace"
@ -227,8 +228,9 @@ def decode_command(cmd):
else: else:
return "exact" return "exact"
def form_to_cvsdb_query(form_data): def form_to_cvsdb_query(cfg, form_data):
query = cvsdb.CreateCheckinQuery() query = cvsdb.CreateCheckinQuery()
query.SetLimit(cfg.cvsdb.row_limit)
if form_data.repository: if form_data.repository:
for cmd, str in listparse_string(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): def prev_rev(rev):
'''Returns a string representing the previous revision of the argument.''' '''Returns a string representing the previous revision of the argument.'''
r = string.split(rev, '.') r = rev.split('.')
# decrement final revision component # decrement final revision component
r[-1] = str(int(r[-1]) - 1) r[-1] = str(int(r[-1]) - 1)
# prune if we pass the beginning of the branch # prune if we pass the beginning of the branch
if len(r) > 2 and r[-1] == '0': if len(r) > 2 and r[-1] == '0':
r = r[:-2] r = r[:-2]
return string.join(r, '.') return '.'.join(r)
def is_forbidden(cfg, cvsroot_name, module): def is_forbidden(cfg, cvsroot_name, module):
auth_params = cfg.get_authorizer_params('forbidden', cvsroot_name) '''Return 1 if MODULE in CVSROOT_NAME is forbidden; return 0 otherwise.'''
forbidden = auth_params.get('forbidden', '')
forbidden = map(string.strip, filter(None, string.split(forbidden, ','))) # 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 default = 0
for pat in forbidden: for pat in forbidden:
if pat[0] == '!': if pat[0] == '!':
@ -304,12 +328,8 @@ def is_forbidden(cfg, cvsroot_name, module):
return default return default
def build_commit(server, cfg, desc, files, cvsroots, viewvc_link): def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
ob = _item(num_files=len(files), files=[], plus=0, minus=0) ob = _item(num_files=len(files), files=[])
ob.log = desc and server.escape(desc).replace('\n', '<br />') or '&nbsp;'
if desc:
ob.log = string.replace(server.escape(desc), '\n', '<br />')
else:
ob.log = '&nbsp;'
for commit in files: for commit in files:
repository = commit.GetRepository() repository = commit.GetRepository()
@ -318,7 +338,7 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
## find the module name (if any) ## find the module name (if any)
try: try:
module = filter(None, string.split(directory, '/'))[0] module = filter(None, directory.split('/'))[0]
except IndexError: except IndexError:
module = None module = None
@ -343,9 +363,10 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
except: except:
raise Exception, str([directory, commit.GetFile()]) raise Exception, str([directory, commit.GetFile()])
## if we couldn't find the cvsroot path configured in the ## If we couldn't find the cvsroot path configured in the
## viewvc.conf file, then don't make the link ## viewvc.conf file, or we don't have a VIEWVC_LINK, then
if cvsroot_name: ## don't make the link.
if cvsroot_name and viewvc_link:
flink = '[%s] <a href="%s/%s?root=%s">%s</a>' % ( flink = '[%s] <a href="%s/%s?root=%s">%s</a>' % (
cvsroot_name, viewvc_link, urllib.quote(file), cvsroot_name, viewvc_link, urllib.quote(file),
cvsroot_name, file) cvsroot_name, file)
@ -376,22 +397,26 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
return ob return ob
def run_query(server, cfg, db, form_data, viewvc_link): 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) db.RunQuery(query)
if not query.commit_list: commit_list = query.GetCommitList()
return [ ] if not commit_list:
return [ ], 0
row_limit_reached = query.GetLimitReached()
commits = [ ] commits = [ ]
files = [ ] files = [ ]
cvsroots = {} cvsroots = {}
viewvc.expand_root_parents(cfg)
rootitems = cfg.general.svn_roots.items() + cfg.general.cvs_roots.items() rootitems = cfg.general.svn_roots.items() + cfg.general.cvs_roots.items()
for key, value in rootitems: for key, value in rootitems:
cvsroots[cvsdb.CleanRepository(value)] = key cvsroots[cvsdb.CleanRepository(value)] = key
current_desc = query.commit_list[0].GetDescription() current_desc = commit_list[0].GetDescription()
for commit in query.commit_list: for commit in commit_list:
desc = commit.GetDescription() desc = commit.GetDescription()
if current_desc == desc: if current_desc == desc:
files.append(commit) files.append(commit)
@ -414,7 +439,7 @@ def run_query(server, cfg, db, form_data, viewvc_link):
return len(commit.files) > 0 return len(commit.files) > 0
commits = filter(_only_with_files, commits) commits = filter(_only_with_files, commits)
return commits return commits, row_limit_reached
def main(server, cfg, viewvc_link): def main(server, cfg, viewvc_link):
try: try:
@ -424,31 +449,32 @@ def main(server, cfg, viewvc_link):
db = cvsdb.ConnectDatabaseReadOnly(cfg, None) db = cvsdb.ConnectDatabaseReadOnly(cfg, None)
if form_data.valid: 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 query = None
else: else:
commits = [ ] commits = [ ]
row_limit_reached = 0
query = 'skipped' 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, 'cfg' : cfg,
'address' : cfg.general.address, 'address' : cfg.general.address,
'vsn' : viewvc.__version__, 'vsn' : viewvc.__version__,
'textquery' : server.escape(form_data.textquery),
'textquery' : server.escape(form_data.textquery, 1), 'repository' : server.escape(form_data.repository),
'repository' : server.escape(form_data.repository, 1), 'branch' : server.escape(form_data.branch),
'branch' : server.escape(form_data.branch, 1), 'directory' : server.escape(form_data.directory),
'directory' : server.escape(form_data.directory, 1), 'file' : server.escape(form_data.file),
'file' : server.escape(form_data.file, 1), 'who' : server.escape(form_data.who),
'who' : server.escape(form_data.who, 1), 'docroot' : docroot,
'docroot' : cfg.options.docroot is None \
and viewvc_link + '/' + viewvc.docroot_magic_path \
or cfg.options.docroot,
'sortby' : form_data.sortby, 'sortby' : form_data.sortby,
'date' : form_data.date, 'date' : form_data.date,
'query' : query, 'query' : query,
'row_limit_reached' : ezt.boolean(row_limit_reached),
'commits' : commits, 'commits' : commits,
'num_commits' : len(commits), 'num_commits' : len(commits),
'rss_href' : None, 'rss_href' : None,
@ -467,7 +493,3 @@ def main(server, cfg, viewvc_link):
exc_info = debug.GetExceptionData() exc_info = debug.GetExceptionData()
server.header(status=exc_info['status']) server.header(status=exc_info['status'])
debug.PrintException(server, exc_info) debug.PrintException(server, exc_info)
class _item:
def __init__(self, **kw):
vars(self).update(kw)

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -16,17 +16,30 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import types import types
import string
import os import os
import sys import sys
import re import re
import cgi
# global server object. It will be either a CgiServer or a proxy to # global server object. It will be either a CgiServer, a WsgiServer,
# an AspServer or ModPythonServer object. # or a proxy to an AspServer or ModPythonServer object.
server = None 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('&', '&amp;')
s = s.replace('>', '&gt;')
s = s.replace('<', '&lt;')
s = s.replace('"', "&quot;")
return s
class Server: class Server:
def __init__(self): def __init__(self):
self.pageGlobals = {} self.pageGlobals = {}
@ -34,6 +47,9 @@ class Server:
def self(self): def self(self):
return self return self
def escape(self, s):
return escape(s)
def close(self): def close(self):
pass pass
@ -129,9 +145,6 @@ class CgiServer(Server):
global server global server
server = self server = self
global cgi
import cgi
def addheader(self, name, value): def addheader(self, name, value):
self.headers.append((name, value)) self.headers.append((name, value))
@ -160,9 +173,6 @@ class CgiServer(Server):
self.header(status='301 Moved') self.header(status='301 Moved')
sys.stdout.write('This document is located <a href="%s">here</a>.\n' % url) sys.stdout.write('This document is located <a href="%s">here</a>.\n' % url)
def escape(self, s, quote = None):
return cgi.escape(s, quote)
def getenv(self, name, value=None): def getenv(self, name, value=None):
ret = os.environ.get(name, value) ret = os.environ.get(name, value)
if self.iis and name == 'PATH_INFO' and ret: if self.iis and name == 'PATH_INFO' and ret:
@ -175,7 +185,7 @@ class CgiServer(Server):
def FieldStorage(fp=None, headers=None, outerboundary="", def FieldStorage(fp=None, headers=None, outerboundary="",
environ=os.environ, keep_blank_values=0, strict_parsing=0): environ=os.environ, keep_blank_values=0, strict_parsing=0):
return cgi.FieldStorage(fp, headers, outerboundary, environ, return cgi.FieldStorage(fp, headers, outerboundary, environ,
keep_blank_values, strict_parsing) keep_blank_values, strict_parsing)
def write(self, s): def write(self, s):
sys.stdout.write(s) sys.stdout.write(s)
@ -187,6 +197,60 @@ class CgiServer(Server):
return sys.stdout 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 <a href="%s">here</a>.' % 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): class AspServer(ThreadedServer):
def __init__(self, Server, Request, Response, Application): def __init__(self, Server, Request, Response, Application):
ThreadedServer.__init__(self) ThreadedServer.__init__(self)
@ -219,9 +283,6 @@ class AspServer(ThreadedServer):
def redirect(self, url): def redirect(self, url):
self.response.Redirect(url) self.response.Redirect(url)
def escape(self, s, quote = None):
return self.server.HTMLEncode(str(s))
def getenv(self, name, value = None): def getenv(self, name, value = None):
ret = self.request.ServerVariables(name)() ret = self.request.ServerVariables(name)()
if not type(ret) is types.UnicodeType: if not type(ret) is types.UnicodeType:
@ -283,9 +344,6 @@ class ModPythonServer(ThreadedServer):
self.request = request self.request = request
self.headerSent = 0 self.headerSent = 0
global cgi
import cgi
def addheader(self, name, value): def addheader(self, name, value):
self.request.headers_out.add(name, value) self.request.headers_out.add(name, value)
@ -308,9 +366,6 @@ class ModPythonServer(ThreadedServer):
self.request.write("You are being redirected to <a href=\"%s\">%s</a>" self.request.write("You are being redirected to <a href=\"%s\">%s</a>"
% (url, url)) % (url, url))
def escape(self, s, quote = None):
return cgi.escape(s, quote)
def getenv(self, name, value = None): def getenv(self, name, value = None):
try: try:
return self.request.subprocess_env[name] return self.request.subprocess_env[name]

View File

@ -1,6 +1,6 @@
# -*-python-*- # -*-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 # 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 # 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.""" """Generic API for implementing authorization checks employed by ViewVC."""
import string
import vclib
class GenericViewVCAuthorizer: class GenericViewVCAuthorizer:
"""Abstract class encapsulating version control authorization routines.""" """Abstract class encapsulating version control authorization routines."""
@ -29,7 +25,15 @@ class GenericViewVCAuthorizer:
def check_root_access(self, rootname): def check_root_access(self, rootname):
"""Return 1 iff the associated username is permitted to read ROOTNAME.""" """Return 1 iff the associated username is permitted to read ROOTNAME."""
pass 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): def check_path_access(self, rootname, path_parts, pathtype, rev=None):
"""Return 1 iff the associated username is permitted to read """Return 1 iff the associated username is permitted to read
revision REV of the path PATH_PARTS (of type PATHTYPE) in revision REV of the path PATH_PARTS (of type PATHTYPE) in
@ -44,6 +48,9 @@ class ViewVCAuthorizer(GenericViewVCAuthorizer):
"""The uber-permissive authorizer.""" """The uber-permissive authorizer."""
def check_root_access(self, rootname): def check_root_access(self, rootname):
return 1 return 1
def check_universal_access(self, rootname):
return 1
def check_path_access(self, rootname, path_parts, pathtype, rev=None): def check_path_access(self, rootname, path_parts, pathtype, rev=None):
return 1 return 1

View File

@ -1,6 +1,6 @@
# -*-python-*- # -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -12,18 +12,24 @@
import vcauth import vcauth
import vclib import vclib
import fnmatch import fnmatch
import string
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
"""A simple top-level module authorizer.""" """A simple top-level module authorizer."""
def __init__(self, username, params={}): def __init__(self, username, params={}):
forbidden = params.get('forbidden', '') forbidden = params.get('forbidden', '')
self.forbidden = map(string.strip, self.forbidden = map(lambda x: x.strip(),
filter(None, string.split(forbidden, ','))) filter(None, forbidden.split(',')))
def check_root_access(self, rootname): def check_root_access(self, rootname):
return 1 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): def check_path_access(self, rootname, path_parts, pathtype, rev=None):
# No path? No problem. # No path? No problem.
if not path_parts: if not path_parts:

View File

@ -1,6 +1,6 @@
# -*-python-*- # -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -12,7 +12,6 @@
import vcauth import vcauth
import vclib import vclib
import fnmatch import fnmatch
import string
import re import re
@ -29,8 +28,8 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
"""A simple regular-expression-based authorizer.""" """A simple regular-expression-based authorizer."""
def __init__(self, username, params={}): def __init__(self, username, params={}):
forbidden = params.get('forbiddenre', '') forbidden = params.get('forbiddenre', '')
self.forbidden = map(lambda x: _split_regexp(string.strip(x)), self.forbidden = map(lambda x: _split_regexp(x.strip()),
filter(None, string.split(forbidden, ','))) filter(None, forbidden.split(',')))
def _check_root_path_access(self, root_path): def _check_root_path_access(self, root_path):
default = 1 default = 1
@ -46,10 +45,17 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
def check_root_access(self, rootname): def check_root_access(self, rootname):
return self._check_root_path_access(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): def check_path_access(self, rootname, path_parts, pathtype, rev=None):
root_path = rootname root_path = rootname
if path_parts: if path_parts:
root_path = root_path + '/' + string.join(path_parts, '/') root_path = root_path + '/' + '/'.join(path_parts)
if pathtype == vclib.DIR: if pathtype == vclib.DIR:
root_path = root_path + '/' root_path = root_path + '/'
else: else:

View File

@ -1,6 +1,6 @@
# -*-python-*- # -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -12,7 +12,6 @@
# (c) 2006 Sergey Lapin <slapin@dataart.com> # (c) 2006 Sergey Lapin <slapin@dataart.com>
import vcauth import vcauth
import string
import os.path import os.path
import debug import debug
@ -34,9 +33,9 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
# See if the admin wants us to do case normalization of usernames. # See if the admin wants us to do case normalization of usernames.
self.force_username_case = params.get('force_username_case') self.force_username_case = params.get('force_username_case')
if self.force_username_case == "upper": if self.force_username_case == "upper":
self.username = username.upper() self.username = username and username.upper() or username
elif self.force_username_case == "lower": elif self.force_username_case == "lower":
self.username = username.lower() self.username = username and username.lower() or username
elif not self.force_username_case: elif not self.force_username_case:
self.username = username self.username = username
else: else:
@ -54,7 +53,10 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
# option names. # option names.
cp = ConfigParser() cp = ConfigParser()
cp.optionxform = lambda x: x 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 # Figure out if there are any aliases for the current username
aliases = [] aliases = []
@ -94,9 +96,9 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
all_groups.append(groupname) all_groups.append(groupname)
group_member = 0 group_member = 0
groupname = groupname.strip() groupname = groupname.strip()
entries = string.split(cp.get('groups', groupname), ',') entries = cp.get('groups', groupname).split(',')
for entry in entries: for entry in entries:
entry = string.strip(entry) entry = entry.strip()
if entry == self.username: if entry == self.username:
group_member = 1 group_member = 1
break break
@ -137,13 +139,13 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
# Figure if this path is explicitly allowed or denied to USERNAME. # Figure if this path is explicitly allowed or denied to USERNAME.
allow = deny = 0 allow = deny = 0
for user in cp.options(section): for user in cp.options(section):
user = string.strip(user) user = user.strip()
if _userspec_matches_user(user): if _userspec_matches_user(user):
# See if the 'r' permission is among the ones granted to # See if the 'r' permission is among the ones granted to
# USER. If so, we can stop looking. (Entry order is not # USER. If so, we can stop looking. (Entry order is not
# relevant -- we'll use the most permissive entry, meaning # relevant -- we'll use the most permissive entry, meaning
# one 'allow' is all we need.) # 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 deny = not allow
if allow: if allow:
break break
@ -172,7 +174,7 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
if section.find(':') == -1: if section.find(':') == -1:
path = section path = section
else: else:
name, path = string.split(section, ':', 1) name, path = section.split(':', 1)
if name == rootname: if name == rootname:
root_sections.append(section) root_sections.append(section)
continue continue
@ -184,14 +186,14 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
# USERNAME, record it. # USERNAME, record it.
if allow or deny: if allow or deny:
if path != '/': if path != '/':
path = '/' + string.join(filter(None, string.split(path, '/')), '/') path = '/' + '/'.join(filter(None, path.split('/')))
paths_for_root[path] = allow paths_for_root[path] = allow
# Okay. Superimpose those root-specific values now. # Okay. Superimpose those root-specific values now.
for section in root_sections: for section in root_sections:
# Get the path again. # Get the path again.
name, path = string.split(section, ':', 1) name, path = section.split(':', 1)
# Check for a specific access determination. # Check for a specific access determination.
allow, deny = _process_access_section(section) allow, deny = _process_access_section(section)
@ -200,7 +202,7 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
# USERNAME, record it. # USERNAME, record it.
if allow or deny: if allow or deny:
if path != '/': if path != '/':
path = '/' + string.join(filter(None, string.split(path, '/')), '/') path = '/' + '/'.join(filter(None, path.split('/')))
paths_for_root[path] = allow paths_for_root[path] = allow
# If the root isn't readable, there's no point in caring about all # 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) paths = self._get_paths_for_root(rootname)
return (paths is not None) and 1 or 0 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): def check_path_access(self, rootname, path_parts, pathtype, rev=None):
# Crawl upward from the path represented by PATH_PARTS toward to # Crawl upward from the path represented by PATH_PARTS toward to
# the root of the repository, looking for an explicitly grant or # the root of the repository, looking for an explicitly grant or
@ -230,7 +262,7 @@ class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
return 0 return 0
parts = path_parts[:] parts = path_parts[:]
while parts: while parts:
path = '/' + string.join(parts, '/') path = '/' + '/'.join(parts)
if paths.has_key(path): if paths.has_key(path):
return paths[path] return paths[path]
del parts[-1] del parts[-1]

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -14,7 +14,6 @@
such as CVS. such as CVS.
""" """
import string
import types import types
@ -76,7 +75,7 @@ class Repository:
""" """
pass 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. """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 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"] of the repository. e.g. ["subdir1", "subdir2", "filename"]
rev is the revision of the file to check out 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): def listdir(self, path_parts, rev, options):
@ -168,20 +169,29 @@ class Repository:
Return value is a python file object Return value is a python file object
""" """
def annotate(self, path_parts, rev): def annotate(self, path_parts, rev, include_text=False):
"""Return a list of annotate file content lines and a revision. """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 The file path is specified as a list of components, relative to
line_number components. 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): def revinfo(self, rev):
"""Return information about a global revision """Return information about a global revision
rev is the revision of the item to return information about rev is the revision of the item to return information about
Return value is a 4-tuple containing the date, author, log Return value is a 5-tuple containing: the date, author, log
message, and a list of ChangedPath items representing paths changed 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 Raise vclib.UnsupportedFeature if the version control system
doesn't support a global revision concept. doesn't support a global revision concept.
@ -196,7 +206,21 @@ class Repository:
rev is the revision of the item to return information about 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: class DirEntry:
@ -302,7 +326,7 @@ class ItemNotFound(Error):
# use '/' rather than os.sep because this is for user consumption, and # use '/' rather than os.sep because this is for user consumption, and
# it was defined using URL separators # it was defined using URL separators
if type(path) in (types.TupleType, types.ListType): if type(path) in (types.TupleType, types.ListType):
path = string.join(path, '/') path = '/'.join(path)
Error.__init__(self, path) Error.__init__(self, path)
class InvalidRevision(Error): class InvalidRevision(Error):

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -11,28 +11,52 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os import os
import os.path 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): def canonicalize_rootpath(rootpath):
assert os.path.isabs(rootpath)
return os.path.normpath(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): def expand_root_parent(parent_path):
# Each subdirectory of PARENT_PATH that contains a child # Each subdirectory of PARENT_PATH that contains a child
# "CVSROOT/config" is added the set of returned roots. Or, if the # "CVSROOT/config" is added the set of returned roots. Or, if the
# PARENT_PATH itself contains a child "CVSROOT/config", then all its # PARENT_PATH itself contains a child "CVSROOT/config", then all its
# subdirectories are returned as roots. # subdirectories are returned as roots.
assert os.path.isabs(parent_path)
roots = {} roots = {}
subpaths = os.listdir(parent_path) subpaths = os.listdir(parent_path)
cvsroot = os.path.exists(os.path.join(parent_path, "CVSROOT", "config"))
for rootname in subpaths: for rootname in subpaths:
rootpath = os.path.join(parent_path, rootname) rootpath = os.path.join(parent_path, rootname)
if cvsroot \ if _is_cvsroot(parent_path) or _is_cvsroot(rootpath):
or (os.path.exists(os.path.join(rootpath, "CVSROOT", "config"))):
roots[rootname] = canonicalize_rootpath(rootpath) roots[rootname] = canonicalize_rootpath(rootpath)
return roots 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): def CVSRepository(name, rootpath, authorizer, utilities, use_rcsparse, charset_guesser = None):
rootpath = canonicalize_rootpath(rootpath) rootpath = canonicalize_rootpath(rootpath)
if use_rcsparse: if use_rcsparse:

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # 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 os.path
import sys import sys
import stat import stat
import string
import re import re
import time import time
import cvsdb import cvsdb
import socket import socket
import calendar
# ViewVC libs # ViewVC libs
import compat
import popen import popen
import vclib.ccvs
def _path_join(path_parts):
return '/'.join(path_parts)
class BaseCVSRepository(vclib.Repository): class BaseCVSRepository(vclib.Repository):
def __init__(self, name, rootpath, authorizer, utilities, charset_guesser = None): def __init__(self, name, rootpath, authorizer, utilities, charset_guesser = None):
if not os.path.isdir(rootpath): if not os.path.isdir(rootpath):
@ -43,6 +46,11 @@ class BaseCVSRepository(vclib.Repository):
if not vclib.check_root_access(self): if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name) 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): def rootname(self):
return self.name return self.name
@ -79,8 +87,8 @@ class BaseCVSRepository(vclib.Repository):
def listdir(self, path_parts, rev, options): def listdir(self, path_parts, rev, options):
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." 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. # Only RCS files (*,v) and subdirs are returned.
data = [ ] data = [ ]
full_name = self._getpath(path_parts) full_name = self._getpath(path_parts)
@ -136,17 +144,21 @@ class BaseCVSRepository(vclib.Repository):
if root: if root:
ret = ret_file ret = ret_file
else: else:
ret = string.join(ret_parts, "/") ret = _path_join(ret_parts)
if v: if v:
ret = ret + ",v" ret = ret + ",v"
return ret return ret
def isexecutable(self, path_parts, rev): def isexecutable(self, path_parts, rev):
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
% (string.join(path_parts, "/")))
rcsfile = self.rcsfile(path_parts, 1) rcsfile = self.rcsfile(path_parts, 1)
return os.access(rcsfile, os.X_OK) 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): class BinCVSRepository(BaseCVSRepository):
@ -165,20 +177,28 @@ class BinCVSRepository(BaseCVSRepository):
return revs[-1] return revs[-1]
return None 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 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
% (string.join(path_parts, "/")))
if not rev or rev == 'HEAD' or rev == 'MAIN': if not rev or rev == 'HEAD' or rev == 'MAIN':
rev_flag = '-p' rev_flag = '-p'
else: else:
rev_flag = '-p' + rev 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) full_name = self.rcsfile(path_parts, root=1, v=0)
used_rlog = 0 used_rlog = 0
tip_rev = None # used only if we have to fallback to using rlog tip_rev = None # used only if we have to fallback to using rlog
fp = self.rcs_popen('co', (kv_flag, rev_flag, full_name), 'rb')
fp = self.rcs_popen('co', (rev_flag, full_name), 'rb')
try: try:
filename, revision = _parse_co_header(fp) filename, revision = _parse_co_header(fp)
except COMissingRevision: except COMissingRevision:
@ -240,7 +260,7 @@ class BinCVSRepository(BaseCVSRepository):
""" """
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." raise vclib.Error("Path '%s' is not a directory."
% (string.join(path_parts, "/"))) % (_path_join(path_parts)))
subdirs = options.get('cvs_subdirs', 0) subdirs = options.get('cvs_subdirs', 0)
entries_to_fetch = [] entries_to_fetch = []
@ -277,9 +297,8 @@ class BinCVSRepository(BaseCVSRepository):
""" """
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
% (string.join(path_parts, "/")))
# Invoke rlog # Invoke rlog
rcsfile = self.rcsfile(path_parts, 1) rcsfile = self.rcsfile(path_parts, 1)
if rev and options.get('cvs_pass_rev', 0): if rev and options.get('cvs_pass_rev', 0):
@ -331,13 +350,12 @@ class BinCVSRepository(BaseCVSRepository):
args = rcs_args args = rcs_args
return popen.popen(cmd, args, mode, capture_err) 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 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
% (string.join(path_parts, "/")))
from vclib.ccvs import blame 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 return source, source.revision
def revinfo(self, rev): def revinfo(self, rev):
@ -357,12 +375,10 @@ class BinCVSRepository(BaseCVSRepository):
path_parts2 = path_parts1 path_parts2 = path_parts1
rev2 = '1.0' rev2 = '1.0'
if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1)))
% (string.join(path_parts1, "/")))
if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2)))
% (string.join(path_parts2, "/")))
args = vclib._diff_args(type, options) args = vclib._diff_args(type, options)
if options.get('ignore_keyword_subst', 0): if options.get('ignore_keyword_subst', 0):
args.append('-kk') args.append('-kk')
@ -567,7 +583,7 @@ def _remove_tag(tag):
def _revision_tuple(revision_string): def _revision_tuple(revision_string):
"""convert a revision number into a tuple of integers""" """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: if len(t) % 2 == 0:
return t return t
raise ValueError raise ValueError
@ -575,7 +591,7 @@ def _revision_tuple(revision_string):
def _tag_tuple(revision_string): def _tag_tuple(revision_string):
"""convert a revision number or branch number into a tuple of integers""" """convert a revision number or branch number into a tuple of integers"""
if revision_string: if revision_string:
t = map(int, string.split(revision_string, '.')) t = map(int, revision_string.split('.'))
l = len(t) l = len(t)
if l == 1: if l == 1:
return () return ()
@ -720,7 +736,7 @@ def _parse_log_header(fp):
if state == 1: if state == 1:
if line[0] == '\t': if line[0] == '\t':
[ tag, rev ] = map(string.strip, string.split(line, ':')) [ tag, rev ] = map(lambda x: x.strip(), line.split(':'))
taginfo[tag] = rev taginfo[tag] = rev
else: else:
# oops. this line isn't tag info. stop parsing tags. # oops. this line isn't tag info. stop parsing tags.
@ -728,7 +744,7 @@ def _parse_log_header(fp):
if state == 2: if state == 2:
if line[0] == '\t': if line[0] == '\t':
[ locker, rev ] = map(string.strip, string.split(line, ':')) [ locker, rev ] = map(lambda x: x.strip(), line.split(':'))
lockinfo[rev] = locker lockinfo[rev] = locker
else: else:
# oops. this line isn't lock info. stop parsing tags. # oops. this line isn't lock info. stop parsing tags.
@ -837,7 +853,7 @@ def _parse_log_entry(fp, guesser):
return None, eof return None, eof
# parse out a time tuple for the local time # 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" # rlog seems to assume that two-digit years are 1900-based (so, "04"
# comes out as "1904", not "2004"). # comes out as "1904", not "2004").
@ -848,7 +864,7 @@ def _parse_log_entry(fp, guesser):
tm[0] = tm[0] + 100 tm[0] = tm[0] + 100
if tm[0] < EPOCH: if tm[0] < EPOCH:
raise ValueError, 'invalid year' raise ValueError, 'invalid year'
date = compat.timegm(tm) date = calendar.timegm(tm)
if guesser: if guesser:
log = guesser.utf8(log) 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) file.errors.append("rlog error: %s" % msg)
continue continue
tag = None
if view_tag == 'MAIN' or view_tag == 'HEAD': if view_tag == 'MAIN' or view_tag == 'HEAD':
tag = Tag(None, default_branch) tag = Tag(None, default_branch)
elif taginfo.has_key(view_tag): elif taginfo.has_key(view_tag):
tag = Tag(None, taginfo[view_tag]) tag = Tag(None, taginfo[view_tag])
elif view_tag: elif view_tag and (eof != _EOF_FILE):
# the tag wasn't found, so skip this 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) _skip_file(rlog)
eof = 1 eof = _EOF_FILE
else:
tag = None
# we don't care about the specific values -- just the keys and whether # 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 # the values point to branches or revisions. this the fastest way to

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*-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 <curt@hagenlocher.org> # Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
# #
# By using this file, you agree to the terms and conditions set forth in # By using this file, you agree to the terms and conditions set forth in
@ -26,7 +26,6 @@
# #
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import string
import re import re
import time import time
import math import math
@ -101,7 +100,7 @@ class CVSParser(rcsparse.Sink):
# Split deltatext specified by rev to each line. # Split deltatext specified by rev to each line.
def deltatext_split(self, rev): def deltatext_split(self, rev):
lines = string.split(self.revision_deltatext[rev], '\n') lines = self.revision_deltatext[rev].split('\n')
if lines[-1] == '': if lines[-1] == '':
del lines[-1] del lines[-1]
return lines return lines
@ -140,16 +139,16 @@ class CVSParser(rcsparse.Sink):
adjust = adjust + 1 adjust = adjust + 1
elif dmatch: elif dmatch:
# "d" - Delete command # "d" - Delete command
start_line = string.atoi(dmatch.group(1)) start_line = int(dmatch.group(1))
count = string.atoi(dmatch.group(2)) count = int(dmatch.group(2))
begin = start_line + adjust - 1 begin = start_line + adjust - 1
del text[begin:begin + count] del text[begin:begin + count]
adjust = adjust - count adjust = adjust - count
lines_removed_now = lines_removed_now + count lines_removed_now = lines_removed_now + count
elif amatch: elif amatch:
# "a" - Add command # "a" - Add command
start_line = string.atoi(amatch.group(1)) start_line = int(amatch.group(1))
count = string.atoi(amatch.group(2)) count = int(amatch.group(2))
add_lines_remaining = count add_lines_remaining = count
lines_added_now = lines_added_now + count lines_added_now = lines_added_now + count
else: else:
@ -312,13 +311,13 @@ class CVSParser(rcsparse.Sink):
skip = skip - 1 skip = skip - 1
elif dmatch: elif dmatch:
# "d" - Delete command # "d" - Delete command
start_line = string.atoi(dmatch.group(1)) start_line = int(dmatch.group(1))
count = string.atoi(dmatch.group(2)) count = int(dmatch.group(2))
line_count = line_count - count line_count = line_count - count
elif amatch: elif amatch:
# "a" - Add command # "a" - Add command
start_line = string.atoi(amatch.group(1)) start_line = int(amatch.group(1))
count = string.atoi(amatch.group(2)) count = int(amatch.group(2))
skip = count skip = count
line_count = line_count + count line_count = line_count + count
else: else:
@ -360,8 +359,8 @@ class CVSParser(rcsparse.Sink):
dmatch = self.d_command.match(command) dmatch = self.d_command.match(command)
amatch = self.a_command.match(command) amatch = self.a_command.match(command)
if dmatch: if dmatch:
start_line = string.atoi(dmatch.group(1)) start_line = int(dmatch.group(1))
count = string.atoi(dmatch.group(2)) count = int(dmatch.group(2))
temp = [] temp = []
while count > 0: while count > 0:
temp.append(revision) temp.append(revision)
@ -369,8 +368,8 @@ class CVSParser(rcsparse.Sink):
self.revision_map = (self.revision_map[:start_line - 1] + self.revision_map = (self.revision_map[:start_line - 1] +
temp + self.revision_map[start_line - 1:]) temp + self.revision_map[start_line - 1:])
elif amatch: elif amatch:
start_line = string.atoi(amatch.group(1)) start_line = int(amatch.group(1))
count = string.atoi(amatch.group(2)) count = int(amatch.group(2))
del self.revision_map[start_line:start_line + count] del self.revision_map[start_line:start_line + count]
skip = count skip = count
else: else:
@ -389,15 +388,15 @@ class CVSParser(rcsparse.Sink):
dmatch = self.d_command.match(command) dmatch = self.d_command.match(command)
amatch = self.a_command.match(command) amatch = self.a_command.match(command)
if dmatch: if dmatch:
start_line = string.atoi(dmatch.group(1)) start_line = int(dmatch.group(1))
count = string.atoi(dmatch.group(2)) count = int(dmatch.group(2))
adj_begin = start_line + adjust - 1 adj_begin = start_line + adjust - 1
adj_end = start_line + adjust - 1 + count adj_end = start_line + adjust - 1 + count
del self.revision_map[adj_begin:adj_end] del self.revision_map[adj_begin:adj_end]
adjust = adjust - count adjust = adjust - count
elif amatch: elif amatch:
start_line = string.atoi(amatch.group(1)) start_line = int(amatch.group(1))
count = string.atoi(amatch.group(2)) count = int(amatch.group(2))
skip = count skip = count
temp = [] temp = []
while count > 0: while count > 0:
@ -415,7 +414,7 @@ class CVSParser(rcsparse.Sink):
class BlameSource: 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 # Parse the CVS file
parser = CVSParser() parser = CVSParser()
revision = parser.parse_cvs_file(rcs_file, opt_rev) revision = parser.parse_cvs_file(rcs_file, opt_rev)
@ -430,6 +429,7 @@ class BlameSource:
self.num_lines = count self.num_lines = count
self.parser = parser self.parser = parser
self.guesser = charset_guesser self.guesser = charset_guesser
self.include_text = include_text
# keep track of where we are during an iteration # keep track of where we are during an iteration
self.idx = -1 self.idx = -1
@ -449,7 +449,9 @@ class BlameSource:
line_number = idx + 1 line_number = idx + 1
author = self.parser.revision_author[rev] 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]) thisline = self.guesser.utf8(self.lines[idx])
else: else:
thisline = self.lines[idx] thisline = self.lines[idx]

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -11,7 +11,6 @@
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
import os import os
import string
import re import re
import cStringIO import cStringIO
import tempfile import tempfile
@ -23,7 +22,8 @@ import cvsdb
### The functionality shared with bincvs should probably be moved to a ### The functionality shared with bincvs should probably be moved to a
### separate module ### 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): class CCVSRepository(BaseCVSRepository):
def dirlogs(self, path_parts, rev, entries, options): 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 if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." raise vclib.Error("Path '%s' is not a directory."
% (string.join(path_parts, "/"))) % (part2path(path_parts)))
entries_to_fetch = [] entries_to_fetch = []
for entry in entries: for entry in entries:
if vclib.check_path_access(self, path_parts + [entry.name], None, rev): 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 dictionary of Tag objects for all tags encountered
""" """
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
% (string.join(path_parts, "/")))
path = self.rcsfile(path_parts, 1) path = self.rcsfile(path_parts, 1)
sink = TreeSink() sink = TreeSink()
@ -123,17 +122,15 @@ class CCVSRepository(BaseCVSRepository):
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): 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 if path_parts1 and self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1)))
% (string.join(path_parts1, "/")))
if path_parts2 and self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check if path_parts2 and self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2)))
% (string.join(path_parts2, "/")))
if not path_parts1 and not path_parts2: if not path_parts1 and not path_parts2:
raise vclib.Error("Nothing to diff.") raise vclib.Error("Nothing to diff.")
if path_parts1: if path_parts1:
temp1 = tempfile.mktemp() 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] 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) info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string)
else: else:
@ -142,7 +139,7 @@ class CCVSRepository(BaseCVSRepository):
if path_parts2: if path_parts2:
temp2 = tempfile.mktemp() 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] 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) info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string)
else: else:
@ -154,25 +151,23 @@ class CCVSRepository(BaseCVSRepository):
return vclib._diff_fp(temp1, temp2, info1, info2, return vclib._diff_fp(temp1, temp2, info1, info2,
self.utilities.diff or 'diff', diff_args) 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 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
% (string.join(path_parts, "/"))) source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser, include_text)
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser)
return source, source.revision return source, source.revision
def revinfo(self, rev): def revinfo(self, rev):
raise vclib.UnsupportedFeature 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 if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
% (string.join(path_parts, "/")))
path = self.rcsfile(path_parts, 1) path = self.rcsfile(path_parts, 1)
sink = COSink(rev) sink = COSink(rev)
rcsparse.parse(open(path, 'rb'), sink) rcsparse.parse(open(path, 'rb'), sink)
revision = sink.last and sink.last.string 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): class MatchingSink(rcsparse.Sink):
"""Superclass for sinks that search for revisions based on tag or number""" """Superclass for sinks that search for revisions based on tag or number"""
@ -212,6 +207,7 @@ class InfoSink(MatchingSink):
self.matching_rev = None self.matching_rev = None
self.perfect_match = 0 self.perfect_match = 0
self.lockinfo = { } self.lockinfo = { }
self.saw_revision = False
def define_tag(self, name, revision): def define_tag(self, name, revision):
MatchingSink.define_tag(self, name, revision) MatchingSink.define_tag(self, name, revision)
@ -225,10 +221,17 @@ class InfoSink(MatchingSink):
self.entry.absent = 1 self.entry.absent = 1
raise rcsparse.RCSStopParser 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): def set_locker(self, rev, locker):
self.lockinfo[rev] = locker self.lockinfo[rev] = locker
def define_revision(self, revision, date, author, state, branches, next): def define_revision(self, revision, date, author, state, branches, next):
self.saw_revision = True
if self.perfect_match: if self.perfect_match:
return return
@ -236,14 +239,18 @@ class InfoSink(MatchingSink):
rev = Revision(revision, date, author, state == "dead") rev = Revision(revision, date, author, state == "dead")
rev.lockinfo = self.lockinfo.get(revision) rev.lockinfo = self.lockinfo.get(revision)
# perfect match if revision number matches tag number or if revision is on # perfect match if revision number matches tag number or if
# trunk and tag points to trunk. imperfect match if tag refers to a branch # revision is on trunk and tag points to trunk. imperfect match
# and this revision is the highest revision so far found on that branch # 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 perfect = ((rev.number == tag.number) or
(not tag.number and len(rev.number) == 2)) (not tag.number and len(rev.number) == 2))
if perfect or (tag.is_branch and tag.number == rev.number[:-1] and if perfect or (tag.is_branch and \
(not self.matching_rev or ((tag.number == rev.number[:-1] and
rev.number > self.matching_rev.number)): (not self.matching_rev or
rev.number > self.matching_rev.number)) or
(rev.number == tag.number[:-1]))):
self.matching_rev = rev self.matching_rev = rev
self.perfect_match = perfect self.perfect_match = perfect
@ -299,18 +306,18 @@ class TreeSink(rcsparse.Sink):
deled = 0 deled = 0
if self.head != revision: if self.head != revision:
changed = 1 changed = 1
lines = string.split(text, '\n') lines = text.split('\n')
idx = 0 idx = 0
while idx < len(lines): while idx < len(lines):
command = lines[idx] command = lines[idx]
dmatch = self.d_command.match(command) dmatch = self.d_command.match(command)
idx = idx + 1 idx = idx + 1
if dmatch: if dmatch:
deled = deled + string.atoi(dmatch.group(2)) deled = deled + int(dmatch.group(2))
else: else:
amatch = self.a_command.match(command) amatch = self.a_command.match(command)
if amatch: if amatch:
count = string.atoi(amatch.group(2)) count = int(amatch.group(2))
added = added + count added = added + count
idx = idx + count idx = idx + count
elif command: elif command:
@ -326,12 +333,12 @@ class StreamText:
a_command = re.compile('^a(\d+)\\s(\\d+)') a_command = re.compile('^a(\d+)\\s(\\d+)')
def __init__(self, text): def __init__(self, text):
self.text = string.split(text, "\n") self.text = text.split('\n')
def command(self, cmd): def command(self, cmd):
adjust = 0 adjust = 0
add_lines_remaining = 0 add_lines_remaining = 0
diffs = string.split(cmd, "\n") diffs = cmd.split('\n')
if diffs[-1] == "": if diffs[-1] == "":
del diffs[-1] del diffs[-1]
if len(diffs) == 0: if len(diffs) == 0:
@ -349,22 +356,22 @@ class StreamText:
amatch = self.a_command.match(command) amatch = self.a_command.match(command)
if dmatch: if dmatch:
# "d" - Delete command # "d" - Delete command
start_line = string.atoi(dmatch.group(1)) start_line = int(dmatch.group(1))
count = string.atoi(dmatch.group(2)) count = int(dmatch.group(2))
begin = start_line + adjust - 1 begin = start_line + adjust - 1
del self.text[begin:begin + count] del self.text[begin:begin + count]
adjust = adjust - count adjust = adjust - count
elif amatch: elif amatch:
# "a" - Add command # "a" - Add command
start_line = string.atoi(amatch.group(1)) start_line = int(amatch.group(1))
count = string.atoi(amatch.group(2)) count = int(amatch.group(2))
add_lines_remaining = count add_lines_remaining = count
else: else:
raise RuntimeError, 'Error parsing diff commands' raise RuntimeError, 'Error parsing diff commands'
def secondnextdot(s, start): def secondnextdot(s, start):
# find the position the second dot after the start index. # 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): class COSink(MatchingSink):

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # 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 * from common import *
try: try:
@ -23,4 +32,13 @@ except ImportError:
from default import Parser from default import Parser
def parse(file, sink): 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) return Parser().parse(file, sink)

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -16,52 +16,199 @@ import calendar
import string import string
class Sink: 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): 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 pass
def set_principal_branch(self, branch_name): 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 pass
def set_access(self, accessors): 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 pass
def define_tag(self, name, revision): 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 pass
def set_locker(self, revision, locker): 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 pass
def set_locking(self, mode): 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 This is taken from the 'strict' header in the admin section of the RCS
Not called when no locking used.""" file. This function can only be called before admin_completed().
Parameters: MODE is always the string 'strict'.
"""
pass pass
def set_comment(self, comment): 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 pass
def set_expansion(self, mode): 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 pass
def admin_completed(self): def admin_completed(self):
"""Reports that the initial RCS header has been parsed. This function is
called exactly once.
"""
pass pass
def define_revision(self, revision, timestamp, author, state, def define_revision(self, revision, timestamp, author, state,
branches, next): 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 pass
def tree_completed(self): 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 pass
def set_description(self, description): 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 pass
def set_revision_info(self, revision, log, text): 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 pass
def parse_completed(self): 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 pass
@ -194,7 +341,8 @@ class _Parser:
else: else:
# Chew up "newphrase" # Chew up "newphrase"
# warn("Unexpected RCS token: $token\n") # warn("Unexpected RCS token: $token\n")
pass while self.ts.get() != ';':
pass
else: else:
if f is None: if f is None:
self.ts.unget(token) self.ts.unget(token)
@ -208,7 +356,7 @@ class _Parser:
date = self.ts.get() date = self.ts.get()
self.ts.match(';') self.ts.match(';')
# Convert date into timestamp # Convert date into standard UNIX time format (seconds since epoch)
date_fields = string.split(date, '.') date_fields = string.split(date, '.')
# According to rcsfile(5): the year "contains just the last two # According to rcsfile(5): the year "contains just the last two
# digits of the year for years from 1900 through 1999, and all the # 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) date_fields = map(string.atoi, date_fields)
EPOCH = 1970 EPOCH = 1970
if date_fields[0] < EPOCH: if date_fields[0] < EPOCH:
raise ValueError, 'invalid year' raise ValueError, 'invalid year for revision %s' % (revision,)
timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,)) 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 # Parse author
### NOTE: authors containing whitespace are violations of the ### NOTE: authors containing whitespace are violations of the
@ -255,6 +406,7 @@ class _Parser:
# group 15; # group 15;
# permissions 644; # permissions 644;
# hardlinks @configure.in@; # hardlinks @configure.in@;
# commitid mLiHw3bulRjnTDGr;
# this is "newphrase" in RCSFILE(5). we just want to skip over these. # this is "newphrase" in RCSFILE(5). we just want to skip over these.
while 1: while 1:
token = self.ts.get() token = self.ts.get()
@ -298,6 +450,15 @@ class _Parser:
self.sink.set_revision_info(revision, log, text) self.sink.set_revision_info(revision, log, text)
def parse(self, file, sink): 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.ts = self.stream_class(file)
self.sink = sink self.sink = sink

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -19,7 +19,11 @@ import string
import common import common
class _TokenStream: 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. # the algorithm is about the same speed for any CHUNK_SIZE chosen.
# grab a good-sized chunk, but not too large to overwhelm memory. # grab a good-sized chunk, but not too large to overwhelm memory.
@ -44,15 +48,17 @@ class _TokenStream:
# out more complex solutions. # out more complex solutions.
buf = self.buf buf = self.buf
lbuf = len(buf)
idx = self.idx idx = self.idx
while 1: while 1:
if idx == len(buf): if idx == lbuf:
buf = self.rcsfile.read(self.CHUNK_SIZE) buf = self.rcsfile.read(self.CHUNK_SIZE)
if buf == '': if buf == '':
# signal EOF by returning None as the token # signal EOF by returning None as the token
del self.buf # so we fail if get() is called again del self.buf # so we fail if get() is called again
return None return None
lbuf = len(buf)
idx = 0 idx = 0
if buf[idx] not in string.whitespace: if buf[idx] not in string.whitespace:
@ -60,7 +66,7 @@ class _TokenStream:
idx = idx + 1 idx = idx + 1
if buf[idx] == ';' or buf[idx] == ':': if buf[idx] in ';:':
self.buf = buf self.buf = buf
self.idx = idx + 1 self.idx = idx + 1
return buf[idx] return buf[idx]
@ -70,17 +76,18 @@ class _TokenStream:
token = '' token = ''
while 1: while 1:
# find token characters in the current buffer # 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 end = end + 1
token = token + buf[idx:end] token = token + buf[idx:end]
if end < len(buf): if end < lbuf:
# we stopped before the end, so we have a full token # we stopped before the end, so we have a full token
idx = end idx = end
break break
# we stopped at the end of the buffer, so we may have a partial token # we stopped at the end of the buffer, so we may have a partial token
buf = self.rcsfile.read(self.CHUNK_SIZE) buf = self.rcsfile.read(self.CHUNK_SIZE)
lbuf = len(buf)
idx = end = 0 idx = end = 0
self.buf = buf self.buf = buf
@ -94,22 +101,24 @@ class _TokenStream:
chunks = [ ] chunks = [ ]
while 1: while 1:
if idx == len(buf): if idx == lbuf:
idx = 0 idx = 0
buf = self.rcsfile.read(self.CHUNK_SIZE) buf = self.rcsfile.read(self.CHUNK_SIZE)
if buf == '': if buf == '':
raise RuntimeError, 'EOF' raise RuntimeError, 'EOF'
lbuf = len(buf)
i = string.find(buf, '@', idx) i = string.find(buf, '@', idx)
if i == -1: if i == -1:
chunks.append(buf[idx:]) chunks.append(buf[idx:])
idx = len(buf) idx = lbuf
continue continue
if i == len(buf) - 1: if i == lbuf - 1:
chunks.append(buf[idx:i]) chunks.append(buf[idx:i])
idx = 0 idx = 0
buf = '@' + self.rcsfile.read(self.CHUNK_SIZE) buf = '@' + self.rcsfile.read(self.CHUNK_SIZE)
if buf == '@': if buf == '@':
raise RuntimeError, 'EOF' raise RuntimeError, 'EOF'
lbuf = len(buf)
continue continue
if buf[i + 1] == '@': if buf[i + 1] == '@':
chunks.append(buf[idx:i+1]) chunks.append(buf[idx:i+1])

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -15,17 +15,50 @@
import os import os
import os.path import os.path
import re import re
import urllib
_re_url = re.compile('^(http|https|file|svn|svn\+[^:]+)://') _re_url = re.compile('^(http|https|file|svn|svn\+[^:]+)://')
def canonicalize_rootpath(rootpath): def _canonicalize_path(path):
import svn.core
try: try:
import svn.core return svn.core.svn_path_canonicalize(path)
return svn.core.svn_path_canonicalize(rootpath) except AttributeError: # svn_path_canonicalize() appeared in 1.4.0 bindings
except: # There's so much more that we *could* do here, but if we're
if re.search(_re_url, rootpath): # here at all its because there's a really old Subversion in
return rootpath[-1] == '/' and rootpath[:-1] or rootpath # place, and those older Subversion versions cared quite a bit
return os.path.normpath(rootpath) # 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): def expand_root_parent(parent_path):
@ -35,6 +68,7 @@ def expand_root_parent(parent_path):
else: else:
# Any subdirectories of PARENT_PATH which themselves have a child # Any subdirectories of PARENT_PATH which themselves have a child
# "format" are returned as roots. # "format" are returned as roots.
assert os.path.isabs(parent_path)
subpaths = os.listdir(parent_path) subpaths = os.listdir(parent_path)
for rootname in subpaths: for rootname in subpaths:
rootpath = os.path.join(parent_path, rootname) rootpath = os.path.join(parent_path, rootname)
@ -43,6 +77,20 @@ def expand_root_parent(parent_path):
return roots 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): def SubversionRepository(name, rootpath, authorizer, utilities, config_dir):
rootpath = canonicalize_rootpath(rootpath) rootpath = canonicalize_rootpath(rootpath)
if re.search(_re_url, rootpath): if re.search(_re_url, rootpath):

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -15,13 +15,14 @@
import vclib import vclib
import sys import sys
import os import os
import string
import re import re
import tempfile import tempfile
import popen2
import time import time
import urllib 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 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) props = ra.svn_ra_get_dir(ra_session, path, rev)
return props 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 ### ### END COMPATABILITY CODE ###
class LogCollector: 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 # This class uses leading slashes for paths internally
if not path: if not path:
self.path = '/' self.path = '/'
@ -67,8 +126,16 @@ class LogCollector:
self.logs = [] self.logs = []
self.show_all_logs = show_all_logs self.show_all_logs = show_all_logs
self.lockinfo = lockinfo 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 have leading slashes
changed_paths = paths.keys() changed_paths = paths.keys()
changed_paths.sort(lambda a, b: _compare_paths(a, b)) changed_paths.sort(lambda a, b: _compare_paths(a, b))
@ -82,19 +149,23 @@ class LogCollector:
if changed_path != self.path: if changed_path != self.path:
# If a parent of our path was copied, our "next previous" # If a parent of our path was copied, our "next previous"
# (huh?) path will exist elsewhere (under the copy source). # (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)] == '/': self.path[len(changed_path)] == '/':
change = paths[changed_path] change = paths[changed_path]
if change.copyfrom_path: if change.copyfrom_path:
this_path = change.copyfrom_path + self.path[len(changed_path):] this_path = change.copyfrom_path + self.path[len(changed_path):]
if self.show_all_logs or this_path: if self.show_all_logs or this_path:
entry = Revision(revision, _datestr_to_date(date), author, message, None, if self.access_check_func is None \
self.lockinfo, self.path[1:], None, None) or self.access_check_func(self.path[1:], revision):
self.logs.append(entry) 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: if this_path:
self.path = 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""" """Check out file revision to temporary file"""
temp = tempfile.mktemp() temp = tempfile.mktemp()
stream = core.svn_stream_from_aprfile(temp) stream = core.svn_stream_from_aprfile(temp)
@ -155,21 +226,8 @@ class RemoteSubversionRepository(vclib.Repository):
def open(self): def open(self):
# Setup the client context baton, complete with non-prompting authstuffs. # Setup the client context baton, complete with non-prompting authstuffs.
# TODO: svn_cmdline_setup_auth_baton() is mo' better (when available) self.ctx = setup_client_ctx(self.config_dir)
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)
ra_callbacks = ra.svn_ra_callbacks_t() ra_callbacks = ra.svn_ra_callbacks_t()
ra_callbacks.auth_baton = self.ctx.auth_baton ra_callbacks.auth_baton = self.ctx.auth_baton
self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None, 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.youngest = ra.svn_ra_get_latest_revnum(self.ra_session)
self._dirent_cache = { } self._dirent_cache = { }
self._revinfo_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): def rootname(self):
return self.name return self.name
@ -211,25 +273,23 @@ class RemoteSubversionRepository(vclib.Repository):
raise vclib.ItemNotFound(path_parts) raise vclib.ItemNotFound(path_parts)
return pathtype return pathtype
def openfile(self, path_parts, rev): def openfile(self, path_parts, rev, options):
path = self._getpath(path_parts) path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path) raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev) rev = self._getrev(rev)
url = self._geturl(path) 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 ### rev here should be the last history revision of the URL
client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev), self.ctx) fp = SelfCleanFP(cat_to_tempfile(self, path, rev))
core.svn_stream_close(stream) lh_rev, c_rev = self._get_last_history_rev(path_parts, rev)
return SelfCleanFP(tmp_file), self._get_last_history_rev(path_parts, rev) return fp, lh_rev
def listdir(self, path_parts, rev, options): def listdir(self, path_parts, rev, options):
path = self._getpath(path_parts) path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." % path) raise vclib.Error("Path '%s' is not a directory." % path)
rev = self._getrev(rev) rev = self._getrev(rev)
entries = [ ] entries = []
dirents, locks = self._get_dirents(path, rev) dirents, locks = self._get_dirents(path, rev)
for name in dirents.keys(): for name in dirents.keys():
entry = dirents[name] entry = dirents[name]
@ -237,8 +297,9 @@ class RemoteSubversionRepository(vclib.Repository):
kind = vclib.DIR kind = vclib.DIR
elif entry.kind == core.svn_node_file: elif entry.kind == core.svn_node_file:
kind = vclib.FILE kind = vclib.FILE
if vclib.check_path_access(self, path_parts + [name], kind, rev): else:
entries.append(vclib.DirEntry(name, kind)) kind = None
entries.append(vclib.DirEntry(name, kind))
return entries return entries
def dirlogs(self, path_parts, rev, entries, options): def dirlogs(self, path_parts, rev, entries, options):
@ -249,11 +310,13 @@ class RemoteSubversionRepository(vclib.Repository):
dirents, locks = self._get_dirents(path, rev) dirents, locks = self._get_dirents(path, rev)
for entry in entries: for entry in entries:
entry_path_parts = path_parts + [entry.name] 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 continue
dirent = dirents[entry.name] # Get authz-sanitized revision metadata.
entry.date, entry.author, entry.log, changes = \ entry.date, entry.author, entry.log, revprops, changes = \
self.revinfo(dirent.created_rev) self._revinfo(dirent.created_rev)
entry.rev = str(dirent.created_rev) entry.rev = str(dirent.created_rev)
entry.size = dirent.size entry.size = dirent.size
entry.lockinfo = None entry.lockinfo = None
@ -267,29 +330,51 @@ class RemoteSubversionRepository(vclib.Repository):
rev = self._getrev(rev) rev = self._getrev(rev)
url = self._geturl(path) url = self._geturl(path)
# Use ls3 to fetch the lock status for this item. # If this is a file, fetch the lock status and size (as of REV)
lockinfo = None # for this item.
basename = path_parts and path_parts[-1] or "" lockinfo = size_in_rev = None
dirents, locks = list_directory(url, _rev2optrev(rev), if path_type == vclib.FILE:
_rev2optrev(rev), 0, self.ctx) basename = path_parts[-1]
if locks.has_key(basename): list_url = self._geturl(self._getpath(path_parts[:-1]))
lockinfo = locks[basename].owner 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 # It's okay if we're told to not show all logs on a file -- all
# the revisions should match correctly anyway. # 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) cross_copies = options.get('svn_cross_copies', 0)
log_limit = 0 log_limit = 0
if limit: if limit:
log_limit = first + limit log_limit = first + limit
client.svn_client_log2([url], _rev2optrev(rev), _rev2optrev(1), client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1,
log_limit, 1, not cross_copies, cross_copies, lc.add_log, self.ctx)
lc.add_log, self.ctx)
revs = lc.logs revs = lc.logs
revs.sort() revs.sort()
prev = None prev = None
for rev in revs: 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 rev.prev = prev
prev = rev prev = rev
revs.reverse() revs.reverse()
@ -309,13 +394,25 @@ class RemoteSubversionRepository(vclib.Repository):
_rev2optrev(rev), 0, self.ctx) _rev2optrev(rev), 0, self.ctx)
return pairs and pairs[0][1] or {} 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) path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path) raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev) rev = self._getrev(rev)
url = self._geturl(path) 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 = [] blame_data = []
def _blame_cb(line_no, revision, author, date, def _blame_cb(line_no, revision, author, date,
@ -323,21 +420,27 @@ class RemoteSubversionRepository(vclib.Repository):
prev_rev = None prev_rev = None
if revision > 1: if revision > 1:
prev_rev = 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 return blame_data, rev
def revinfo(self, rev): def revinfo(self, rev):
rev = self._getrev(rev) return self._revinfo(rev, 1)
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]
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): 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) args = vclib._diff_args(type, options)
def _date_from_rev(rev): def _date_from_rev(rev):
date, author, msg, changes = self.revinfo(rev) date, author, msg, revprops, changes = self._revinfo(rev)
return date return date
try: try:
info1 = p1, _date_from_rev(r1), r1 info1 = p1, _date_from_rev(r1), r1
info2 = p2, _date_from_rev(r2), r2 info2 = p2, _date_from_rev(r2), r2
if p1: if p1:
temp1 = temp_checkout(self, p1, r1) temp1 = cat_to_tempfile(self, p1, r1)
else: else:
temp1 = '/dev/null' temp1 = '/dev/null'
if p2: if p2:
temp2 = temp_checkout(self, p2, r2) temp2 = cat_to_tempfile(self, p2, r2)
else: else:
temp2 = '/dev/null' temp2 = '/dev/null'
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args) 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 props = self.itemprops(path_parts, rev) # does authz-check
return props.has_key(core.SVN_PROP_EXECUTABLE) 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): def _getpath(self, path_parts):
return string.join(path_parts, '/') return '/'.join(path_parts)
def _getrev(self, rev): def _getrev(self, rev):
if rev is None or rev == 'HEAD': if rev is None or rev == 'HEAD':
return self.youngest return self.youngest
try: try:
if type(rev) == type(''):
while rev[0] == 'r':
rev = rev[1:]
rev = int(rev) rev = int(rev)
except ValueError: except:
raise vclib.InvalidRevision(rev) raise vclib.InvalidRevision(rev)
if (rev < 0) or (rev > self.youngest): if (rev < 0) or (rev > self.youngest):
raise vclib.InvalidRevision(rev) raise vclib.InvalidRevision(rev)
@ -403,58 +518,139 @@ class RemoteSubversionRepository(vclib.Repository):
def _geturl(self, path=None): def _geturl(self, path=None):
if not path: if not path:
return self.rootpath 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): def _get_dirents(self, path, rev):
"""Return a 2-type of dirents and locks, possibly reading/writing """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) dir_url = self._geturl(path)
path_parts = _path_parts(path)
if path: if path:
key = str(rev) + '/' + path key = str(rev) + '/' + path
else: else:
key = str(rev) key = str(rev)
# Ensure that the cache gets filled...
dirents_locks = self._dirent_cache.get(key) dirents_locks = self._dirent_cache.get(key)
if not dirents_locks: if not dirents_locks:
dirents, locks = list_directory(dir_url, _rev2optrev(rev), tmp_dirents, locks = list_directory(dir_url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx) _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] dirents_locks = [dirents, locks]
self._dirent_cache[key] = dirents_locks self._dirent_cache[key] = dirents_locks
# ...then return the goodies from the cache.
return dirents_locks[0], dirents_locks[1] return dirents_locks[0], dirents_locks[1]
def _get_last_history_rev(self, path_parts, rev): 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)) url = self._geturl(self._getpath(path_parts))
optrev = _rev2optrev(rev) optrev = _rev2optrev(rev)
# Get the last-changed-rev.
revisions = [] revisions = []
def _info_cb(path, info, pool, retval=revisions): def _info_cb(path, info, pool, retval=revisions):
revisions.append(info.last_changed_rev) revisions.append(info.last_changed_rev)
client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx) client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx)
return revisions[0] last_changed_rev = revisions[0]
def _revinfo_raw(self, rev):
# return 5-tuple (date, author, message, changes)
optrev = _rev2optrev(rev)
revs = []
def _log_cb(changed_paths, revision, author, # Now, this object might not have been directly edited since the
datestr, message, pool, retval=revs): # last-changed-rev, but it might have been the child of a copy.
date = _datestr_to_date(datestr) # 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, action_map = { 'D' : vclib.DELETED,
'A' : vclib.ADDED, 'A' : vclib.ADDED,
'R' : vclib.REPLACED, 'R' : vclib.REPLACED,
'M' : vclib.MODIFIED, '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)) 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 = [] changes = []
found_readable = found_unreadable = 0 found_readable = found_unreadable = 0
for path in paths: for path in paths:
pathtype = None
change = changed_paths[path] 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) 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: if change.copyfrom_path and change.copyfrom_rev:
is_copy = 1 is_copy = 1
base_path = change.copyfrom_path base_path = change.copyfrom_path
@ -467,31 +663,61 @@ class RemoteSubversionRepository(vclib.Repository):
base_path = path base_path = path
base_rev = revision - 1 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) parts = _path_parts(path)
if vclib.check_path_access(self, parts, vclib.FILE, revision): if vclib.check_path_access(self, parts, vclib.FILE, revision):
if is_copy and base_path and (base_path != path): if is_copy and base_path and (base_path != path):
parts = _path_parts(base_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 is_copy = 0
base_path = None base_path = None
base_rev = None base_rev = None
found_unreadable = 1
changes.append(SVNChangedPath(path, revision, pathtype, base_path, 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 found_readable = 1
else: else:
found_unreadable = 1 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: if found_unreadable:
message = None msg = None
if not found_readable: if not found_readable:
author = None author = None
date = None date = None
revs.append([date, author, message, changes])
client.svn_client_log([self.rootpath], optrev, optrev, # Drop unrequested changes.
1, 0, _log_cb, self.ctx) if not include_changed_paths:
return revs[0][0], revs[0][1], revs[0][2], revs[0][3] 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 --## ##--- custom --##
@ -510,23 +736,16 @@ class RemoteSubversionRepository(vclib.Repository):
old_path = results[old_rev] old_path = results[old_rev]
except KeyError: except KeyError:
raise vclib.ItemNotFound(path) raise vclib.ItemNotFound(path)
old_path = _cleanup_path(old_path)
return _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): def created_rev(self, path, rev):
# NOTE: We can't use svn_client_propget here because the lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev)
# interfaces in that layer strip out the properties not meant for return lh_rev
# 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))
def last_rev(self, path, peg_revision, limit_revision=None): def last_rev(self, path, peg_revision, limit_revision=None):
"""Given PATH, known to exist in PEG_REVISION, find the youngest """Given PATH, known to exist in PEG_REVISION, find the youngest
@ -561,3 +780,33 @@ class RemoteSubversionRepository(vclib.Repository):
else: else:
peg_revision = mid peg_revision = mid
return peg_revision, path 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:]

View File

@ -1,6 +1,6 @@
# -*-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 # 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 # the LICENSE.html file which can be found at the top level of the ViewVC
@ -15,15 +15,12 @@
import vclib import vclib
import os import os
import os.path import os.path
import stat
import string
import cStringIO import cStringIO
import signal
import shutil
import time import time
import tempfile import tempfile
import popen import popen
import re import re
import urllib
from svn import fs, repos, core, client, delta, diff 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): 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)" 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 ### Pre-1.5 SubversionException's might not have the .msg and .apr_err members
def _fix_subversion_exception(e): def _fix_subversion_exception(e):
if not hasattr(e, 'apr_err'): if not hasattr(e, 'apr_err'):
e.apr_err = e[1] e.apr_err = e[1]
if not hasattr(e, 'message'): if not hasattr(e, 'message'):
e.message = e[0] 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): def _allow_all(root, path, pool):
"""Generic authz_read_func that permits access to all paths""" """Generic authz_read_func that permits access to all paths"""
return 1 return 1
def _path_parts(path): def _path_parts(path):
return filter(None, string.split(path, '/')) return filter(None, path.split('/'))
def _cleanup_path(path): def _cleanup_path(path):
"""Return a cleaned-up Subversion filesystem 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): def _fs_path_join(base, relative):
@ -103,11 +114,37 @@ def _rev2optrev(rev):
def _rootpath2url(rootpath, path): def _rootpath2url(rootpath, path):
rootpath = os.path.abspath(rootpath) rootpath = os.path.abspath(rootpath)
if rootpath and rootpath[0] != '/': drive, rootpath = os.path.splitdrive(rootpath)
rootpath = '/' + rootpath
if os.sep != '/': if os.sep != '/':
rootpath = string.replace(rootpath, os.sep, '/') rootpath = rootpath.replace(os.sep, '/')
return 'file://' + string.join([rootpath, path], "/") 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): def _datestr_to_date(datestr):
@ -160,7 +197,7 @@ class NodeHistory:
test_path = path test_path = path
found = 0 found = 0
while 1: while 1:
off = string.rfind(test_path, '/') off = test_path.rfind('/')
if off < 0: if off < 0:
break break
test_path = test_path[0:off] test_path = test_path[0:off]
@ -173,64 +210,11 @@ class NodeHistory:
return return
self.histories.append([revision, _cleanup_path(path)]) self.histories.append([revision, _cleanup_path(path)])
if self.limit and len(self.histories) == self.limit: 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): def __getitem__(self, idx):
return self.histories[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): def _get_last_history_rev(fsroot, path):
history = fs.node_history(fsroot, path) history = fs.node_history(fsroot, path)
history = fs.history_prev(history, 0) history = fs.history_prev(history, 0)
@ -309,14 +293,15 @@ class FileContentsPipe:
class BlameSource: 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.idx = -1
self.first_rev = first_rev self.first_rev = first_rev
self.blame_data = [] self.blame_data = []
self.include_text = include_text
ctx = client.ctx_t() ctx = client.svn_client_create_context()
core.svn_config_ensure(None) core.svn_config_ensure(config_dir)
ctx.config = core.svn_config_get_config(None) ctx.config = core.svn_config_get_config(config_dir)
ctx.auth_baton = core.svn_auth_open([]) ctx.auth_baton = core.svn_auth_open([])
try: try:
### TODO: Is this use of FIRST_REV always what we want? Should we ### TODO: Is this use of FIRST_REV always what we want? Should we
@ -333,6 +318,8 @@ class BlameSource:
prev_rev = None prev_rev = None
if rev > self.first_rev: if rev > self.first_rev:
prev_rev = rev - 1 prev_rev = rev - 1
if not self.include_text:
text = None
self.blame_data.append(vclib.Annotation(text, line_no + 1, rev, self.blame_data.append(vclib.Annotation(text, line_no + 1, rev,
prev_rev, author, None)) prev_rev, author, None))
@ -369,31 +356,14 @@ class LocalSubversionRepository(vclib.Repository):
self.rootpath = rootpath self.rootpath = rootpath
self.name = name self.name = name
self.auth = authorizer self.auth = authorizer
self.svn_client_path = utilities.svn or 'svn'
self.diff_cmd = utilities.diff or 'diff' 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. # See if this repository is even viewable, authz-wise.
if not vclib.check_root_access(self): if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name) raise vclib.ReposNotFound(name)
def open(self): 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. # Open the repository and init some other variables.
self.repos = repos.svn_repos_open(self.rootpath) self.repos = repos.svn_repos_open(self.rootpath)
self.fs_ptr = repos.svn_repos_fs(self.repos) self.fs_ptr = repos.svn_repos_fs(self.repos)
@ -401,6 +371,10 @@ class LocalSubversionRepository(vclib.Repository):
self._fsroots = {} self._fsroots = {}
self._revinfo_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): def rootname(self):
return self.name return self.name
@ -416,19 +390,14 @@ class LocalSubversionRepository(vclib.Repository):
def itemtype(self, path_parts, rev): def itemtype(self, path_parts, rev):
rev = self._getrev(rev) rev = self._getrev(rev)
basepath = self._getpath(path_parts) basepath = self._getpath(path_parts)
kind = fs.check_path(self._getroot(rev), basepath) pathtype = self._gettype(basepath, rev)
pathtype = None if pathtype is None:
if kind == core.svn_node_dir:
pathtype = vclib.DIR
elif kind == core.svn_node_file:
pathtype = vclib.FILE
else:
raise vclib.ItemNotFound(path_parts) raise vclib.ItemNotFound(path_parts)
if not vclib.check_path_access(self, path_parts, pathtype, rev): if not vclib.check_path_access(self, path_parts, pathtype, rev):
raise vclib.ItemNotFound(path_parts) raise vclib.ItemNotFound(path_parts)
return pathtype return pathtype
def openfile(self, path_parts, rev): def openfile(self, path_parts, rev, options):
path = self._getpath(path_parts) path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path) raise vclib.Error("Path '%s' is not a file." % path)
@ -467,7 +436,7 @@ class LocalSubversionRepository(vclib.Repository):
continue continue
path = self._getpath(entry_path_parts) path = self._getpath(entry_path_parts)
entry_rev = _get_last_history_rev(fsroot, path) 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.rev = str(entry_rev)
entry.date = date entry.date = date
entry.author = author entry.author = author
@ -516,20 +485,19 @@ class LocalSubversionRepository(vclib.Repository):
# 'limit' parameter here as numeric cut-off for the depth of our # 'limit' parameter here as numeric cut-off for the depth of our
# history search. # history search.
if options.get('svn_latest_log', 0): if options.get('svn_latest_log', 0):
revision = _log_helper(self, path, rev, lockinfo) revision = self._log_helper(path, rev, lockinfo)
if revision: if revision:
revision.prev = None revision.prev = None
revs.append(revision) revs.append(revision)
else: else:
history = _get_history(self, path, rev, path_type, history = self._get_history(path, rev, path_type, first + limit, options)
first + limit, options)
if len(history) < first: if len(history) < first:
history = [] history = []
if limit: if limit:
history = history[first:first+limit] history = history[first:first+limit]
for hist_rev, hist_path in history: 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 revision:
# If we have unreadable copyfrom data, obscure it. # If we have unreadable copyfrom data, obscure it.
if revision.copy_path is not None: if revision.copy_path is not None:
@ -550,114 +518,23 @@ class LocalSubversionRepository(vclib.Repository):
fsroot = self._getroot(rev) fsroot = self._getroot(rev)
return fs.node_proplist(fsroot, path) 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 = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check path_type = self.itemtype(path_parts, rev) # does auth-check
if path_type != vclib.FILE: if path_type != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % path) raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev) rev = self._getrev(rev)
fsroot = self._getroot(rev) fsroot = self._getroot(rev)
history = _get_history(self, path, rev, path_type, 0, history = self._get_history(path, rev, path_type, 0,
{'svn_cross_copies': 1}) {'svn_cross_copies': 1})
youngest_rev, youngest_path = history[0] youngest_rev, youngest_path = history[0]
oldest_rev, oldest_path = history[-1] oldest_rev, oldest_path = history[-1]
source = BlameSource(_rootpath2url(self.rootpath, path), source = BlameSource(_rootpath2url(self.rootpath, path), youngest_rev,
youngest_rev, oldest_rev) oldest_rev, include_text, self.config_dir)
return source, youngest_rev 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): def revinfo(self, rev):
rev = self._getrev(rev) return self._revinfo(rev, 1)
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]
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}): def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
if path_parts1: if path_parts1:
@ -681,7 +558,7 @@ class LocalSubversionRepository(vclib.Repository):
args = vclib._diff_args(type, options) args = vclib._diff_args(type, options)
def _date_from_rev(rev): def _date_from_rev(rev):
date, author, msg, changes = self.revinfo(rev) date, author, msg, revprops, changes = self._revinfo(rev)
return date return date
try: try:
@ -702,21 +579,272 @@ class LocalSubversionRepository(vclib.Repository):
_fix_subversion_exception(e) _fix_subversion_exception(e)
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND: if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
raise vclib.InvalidRevision raise vclib.InvalidRevision
raise if e.apr_err != _SVN_ERR_CEASE_INVOCATION:
raise
def isexecutable(self, path_parts, rev): def isexecutable(self, path_parts, rev):
props = self.itemprops(path_parts, rev) # does authz-check props = self.itemprops(path_parts, rev) # does authz-check
return props.has_key(core.SVN_PROP_EXECUTABLE) 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): def _getpath(self, path_parts):
return string.join(path_parts, '/') return '/'.join(path_parts)
def _getrev(self, rev): def _getrev(self, rev):
if rev is None or rev == 'HEAD': if rev is None or rev == 'HEAD':
return self.youngest return self.youngest
try: try:
if type(rev) == type(''):
while rev[0] == 'r':
rev = rev[1:]
rev = int(rev) rev = int(rev)
except ValueError: except:
raise vclib.InvalidRevision(rev) raise vclib.InvalidRevision(rev)
if (rev < 0) or (rev > self.youngest): if (rev < 0) or (rev > self.youngest):
raise vclib.InvalidRevision(rev) raise vclib.InvalidRevision(rev)
@ -729,7 +857,20 @@ class LocalSubversionRepository(vclib.Repository):
r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev) r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev)
return r 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): def get_youngest_revision(self):
return self.youngest return self.youngest
@ -807,3 +948,32 @@ class LocalSubversionRepository(vclib.Repository):
return peg_revision, path return peg_revision, path
finally: finally:
pass 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:]

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# -*-python-*- # -*-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 # 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 # 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: try:
import win32api import win32api
except ImportError, e: except ImportError, e:
@ -40,9 +40,9 @@ def CommandLine(command, args):
"""Convert an executable path and a sequence of arguments into a command """Convert an executable path and a sequence of arguments into a command
line that can be passed to CreateProcess""" line that can be passed to CreateProcess"""
cmd = "\"" + string.replace(command, "\"", "\"\"") + "\"" cmd = "\"" + command.replace("\"", "\"\"") + "\""
for arg in args: for arg in args:
cmd = cmd + " \"" + string.replace(arg, "\"", "\"\"") + "\"" cmd = cmd + " \"" + arg.replace("\"", "\"\"") + "\""
return cmd return cmd
def CreateProcess(cmd, hStdInput, hStdOutput, hStdError): def CreateProcess(cmd, hStdInput, hStdOutput, hStdError):
@ -109,13 +109,13 @@ def CreatePipe(readInheritable, writeInheritable):
def File2FileObject(pipe, mode): def File2FileObject(pipe, mode):
"""Make a C stdio file object out of a win32 file handle""" """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 wmode = os.O_RDONLY
elif string.find(mode, 'w') >= 0: elif mode.find('w') >= 0:
wmode = os.O_WRONLY wmode = os.O_WRONLY
if string.find(mode, 'b') >= 0: if mode.find('b') >= 0:
wmode = wmode | os.O_BINARY wmode = wmode | os.O_BINARY
if string.find(mode, 't') >= 0: if mode.find('t') >= 0:
wmode = wmode | os.O_TEXT wmode = wmode | os.O_TEXT
return os.fdopen(msvcrt.open_osfhandle(pipe.Detach(),wmode),mode) return os.fdopen(msvcrt.open_osfhandle(pipe.Detach(),wmode),mode)

54
misc/elemx/Makefile Normal file
View File

@ -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)

110
misc/elemx/elx-common.c Normal file
View File

@ -0,0 +1,110 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#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);
}
}

54
misc/elemx/elx.h Normal file
View File

@ -0,0 +1,54 @@
#ifndef ELX_H
#define ELX_H
#include <stdio.h>
#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 */

121
misc/elemx/elx_html.py Normal file
View File

@ -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('''\
<html><head><title>ELX Output Page</title>
<style type="text/css">
.elx_C { color: firebrick; font-style: italic; }
.elx_S { color: #bc8f8f; font-weight: bold; }
.elx_K { color: purple; font-weight: bold }
.elx_F { color: blue; font-weight: bold; }
.elx_L { color: blue; font-weight: bold; }
.elx_M { color: blue; font-weight: bold; }
.elx_R { color: blue; font-weight: bold; }
</style>
</head>
<body>
''')
w.write('<pre>')
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('<span class="elx_%s">' % type)
# copy over the token
w.copy(start, length)
# and close up the formatting
w.write('</span>')
cur = start + length
# all done.
w.flush(cur)
w.write('</pre>')
if genpage:
w.write('</body></html>\n')
if __name__ == '__main__':
import sys
generate(open(sys.argv[1]), open(sys.argv[2]), sys.stdout, 1)

26
misc/elemx/elx_page.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/sh
if test "$#" != 2; then
echo "USAGE: $0 SOURCE-FILE ELX-FILE"
exit 1
fi
cat <<EOF
<html><head><title>ELX Output Page</title>
<style type="text/css">
.elx_C { color: firebrick; font-style: italic; }
.elx_S { color: #bc8f8f; font-weight: bold; }
.elx_K { color: purple; font-weight: bold }
.elx_F { color: blue; font-weight: bold; }
.elx_L { color: blue; font-weight: bold; }
.elx_M { color: blue; font-weight: bold; }
.elx_R { color: blue; font-weight: bold; }
</style>
</head>
<body>
EOF
dirname="`dirname $0`"
python2 $dirname/elx_html.py $1 $2
echo "</body></html>"

148
misc/elemx/java/elx-java.c Normal file
View File

@ -0,0 +1,148 @@
#include <stdio.h>
#include <stdlib.h>
#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;
}

View File

@ -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

135
misc/elemx/java/j_scan.y Normal file
View File

@ -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;
}

458
misc/elemx/java/java.y Normal file
View File

@ -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 | '+' | '-' | '*'

View File

@ -0,0 +1,150 @@
#include <stdio.h>
#include <stdlib.h>
#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;
}

View File

@ -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

135
misc/elemx/python/python.y Normal file
View File

@ -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]

523
misc/elemx/python/scanner.c Normal file
View File

@ -0,0 +1,523 @@
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>
#include <string.h>
#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);
}

View File

@ -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 */

50
misc/tparse/CHANGES Normal file
View File

@ -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++

26
misc/tparse/INSTALL Normal file
View File

@ -0,0 +1,26 @@
HOW TO INSTALL tparse
Quick install
-------------
$ cd <viewvcinstall>/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 <viewvcinstall>/lib/vclib/ccvs/rcsparse/
$ cd <viewvcinstall>/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!!!

15
misc/tparse/README Normal file
View File

@ -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

12
misc/tparse/Setup.py Executable file
View File

@ -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++"])]
)

398
misc/tparse/tparse.cpp Normal file
View File

@ -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 <lucas.bruand@ecl2002.ec-lyon.fr>
#
# 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 <ctime> /* 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<rcstoken> 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<rcstoken> 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<rcstoken> 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<rcstoken> 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<rcstoken> token;
token.reset(tokenstream->get(FALSE));
if (*token == ';')
break;
if ((*hstate).length)
(*hstate) += ' ';
(*hstate) += *token;
}
tokenstream->match("branches");
while (1)
{
auto_ptr<rcstoken> 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<rcstoken> 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<rcstoken> token;
tokenstream->match("desc");
token.reset(tokenstream->get(FALSE));
sink->set_description(*token);
}
void tparseParser::parse_rcs_deltatext()
{
auto_ptr<rcstoken> 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;
}

306
misc/tparse/tparse.h Normal file
View File

@ -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 <lucas.bruand@ecl2002.ec-lyon.fr>
#
# 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 <memory> /* for auto_ptr */
#include <algorithm> /* for iterator */
#include <exception> /* for exception */
#include <istream> /* for istream */
#include <list> /* for list<> */
#include <string> /* 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<length) ? b_len : length) == 0;
};
int operator!=(const char *b)
{
return (! (*this == b));
};
int operator==(const char b)
{
return (length == 1) && data && (*data == b);
};
int operator!=(const char b)
{
return (! (*this==b));
};
char operator[](size_t i)
{
return data[i];
};
void append(const char *b, size_t b_len);
void append(const char b)
{
grow(length+2);
data[length] = b;
data[length++] = 0;
};
void append(rcstoken& token)
{
append(token.data, token.length);
};
void grow(size_t new_size);
rcstoken *copy_begin_end(size_t begin, size_t end);
rcstoken *copy_begin_len(size_t begin, size_t len);
};
typedef list<rcstoken> 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<rcstoken> ptr(get(FALSE));
if (*ptr != token)
throw RCSExpected(ptr->data, token);
}
void match(const char c)
{
auto_ptr<rcstoken> 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 */

View File

@ -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 <lucas.bruand@ecl2002.ec-lyon.fr>
#
# 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 <fstream>
#include "tparsemodule.h"
#include "tparse.cpp"
#if (__GNUC__ >= 4) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1)
#include <memory> // for auto_ptr
#include <ext/stdio_filebuf.h>
typedef __gnu_cxx::stdio_filebuf<char> 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<streambuf> 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;
};

View File

@ -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 <lucas.bruand@ecl2002.ec-lyon.fr>
#
# This file was originally based on portions of the blame.py script by
# Curt Hagenlocher.
#
# -----------------------------------------------------------------------
*/
#ifdef __cplusplus
extern "C"
{
#endif
#include <Python.h>
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 <lucas.bruand@ecl2002.ec-lyon.fr>";
//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

159
notes/releases.txt Normal file
View File

@ -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.

107
notes/root-heirarchy.txt Normal file
View File

@ -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?

View File

@ -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]
<hr style="margin-top:1em;" />
<table cellpadding="10" class="auto">
<tr>
<td>
<form method="get" action="[diff_format_action]">
<div>
[for diff_format_hidden_values]<input type="hidden" name="[diff_format_hidden_values.name]" value="[diff_format_hidden_values.value]"/>[end]
<select name="diff_format" onchange="submit()">
<option value="h" [is diff_format "h"]selected="selected"[end]>Colored Diff</option>
<option value="l" [is diff_format "l"]selected="selected"[end]>Long Colored Diff</option>
<option value="f" [is diff_format "f"]selected="selected"[end]>Full Colored Diff</option>
<option value="u" [is diff_format "u"]selected="selected"[end]>Unidiff</option>
<option value="c" [is diff_format "c"]selected="selected"[end]>Context Diff</option>
<option value="s" [is diff_format "s"]selected="selected"[end]>Side by Side</option>
</select>
<input type="submit" value="Show" />
</div>
</form>
</td>
<td>
[if-any hide_legend]
&nbsp;
[else]
<table style="border:solid gray 1px;" class="auto">
<tr>
<td>Legend:<br />
<table cellspacing="0" cellpadding="1">
<tr>
<td style="text-align:center;" class="vc_diff_remove">Removed lines/characters</td>
<td class="vc_diff_empty">&nbsp;</td>
</tr>
<tr>
<td style="text-align:center;" colspan="2" class="vc_diff_change">Changed lines/characters</td>
</tr>
<tr>
<td class="vc_diff_empty">&nbsp;</td>
<td style="text-align:center;" class="vc_diff_add">Added lines/characters</td>
</tr>
</table>
</td>
</tr>
</table>
[end]
</td>
</tr>
</table>
[include "include/footer.ezt"]

View File

Before

Width:  |  Height:  |  Size: 764 B

After

Width:  |  Height:  |  Size: 764 B

View File

Before

Width:  |  Height:  |  Size: 337 B

After

Width:  |  Height:  |  Size: 337 B

View File

Before

Width:  |  Height:  |  Size: 205 B

After

Width:  |  Height:  |  Size: 205 B

View File

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 247 B

View File

Before

Width:  |  Height:  |  Size: 755 B

After

Width:  |  Height:  |  Size: 755 B

View File

Before

Width:  |  Height:  |  Size: 162 B

After

Width:  |  Height:  |  Size: 162 B

View File

Before

Width:  |  Height:  |  Size: 219 B

After

Width:  |  Height:  |  Size: 219 B

View File

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 240 B

View File

Before

Width:  |  Height:  |  Size: 228 B

After

Width:  |  Height:  |  Size: 228 B

View File

Before

Width:  |  Height:  |  Size: 167 B

After

Width:  |  Height:  |  Size: 167 B

Some files were not shown because too many files have changed in this diff Show More