Merge with original r2905
180
CHANGES
|
@ -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
|
||||||
|
|
193
INSTALL
|
@ -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,14 +169,40 @@ 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
|
||||||
|
@ -186,54 +213,141 @@ Either METHOD A:
|
||||||
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:
|
||||||
|
|
||||||
and then there's METHOD C:
|
SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf
|
||||||
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
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<blockquote>
|
<blockquote>
|
||||||
|
|
||||||
<p><strong>Copyright © 1999-2008 The ViewCVS Group. All rights
|
<p><strong>Copyright © 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 — software renamed from "ViewCVS"</li>
|
<li>March 17, 2006 — software renamed from "ViewCVS"</li>
|
||||||
<li>April 10, 2007 — copyright years updated</li>
|
<li>April 10, 2007 — copyright years updated</li>
|
||||||
<li>February 22, 2008 — copyright years updated</li>
|
<li>February 22, 2008 — copyright years updated</li>
|
||||||
|
<li>March 18, 2009 — copyright years updated</li>
|
||||||
|
<li>March 29, 2010 — copyright years updated</li>
|
||||||
|
<li>February 18, 2011 — copyright years updated</li>
|
||||||
|
<li>January 23, 2012 — copyright years updated</li>
|
||||||
|
<li>January 04, 2013 — copyright years updated</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
@ -10,19 +10,17 @@
|
||||||
# For more information, visit http://viewvc.org/
|
# For more information, visit http://viewvc.org/
|
||||||
#
|
#
|
||||||
# -----------------------------------------------------------------------
|
# -----------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# This program originally written by Peter Funk <pf@artcom-gmbh.de>, with
|
||||||
|
# contributions by Ka-Ping Yee.
|
||||||
|
#
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
|
||||||
"""Run "standalone.py -p <port>" to start an HTTP server on a given port
|
"""Run "standalone.py -p <port>" to start an HTTP server on a given port
|
||||||
on the local machine to generate ViewVC web pages.
|
on the local machine to generate ViewVC web pages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__author__ = "Peter Funk <pf@artcom-gmbh.de>"
|
#
|
||||||
__date__ = "11 November 2001"
|
|
||||||
__version__ = "$Revision: 1962 $"
|
|
||||||
__credits__ = """Guido van Rossum, for an excellent programming language.
|
|
||||||
Greg Stein, for writing ViewCVS in the first place.
|
|
||||||
Ka-Ping Yee, for the GUI code and the framework stolen from pydoc.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# INSTALL-TIME CONFIGURATION
|
# INSTALL-TIME CONFIGURATION
|
||||||
#
|
#
|
||||||
# These values will be set during the installation process. During
|
# These values will be set during the installation process. During
|
||||||
|
@ -42,6 +40,7 @@ import urllib
|
||||||
import rfc822
|
import rfc822
|
||||||
import socket
|
import socket
|
||||||
import select
|
import select
|
||||||
|
import base64
|
||||||
import BaseHTTPServer
|
import BaseHTTPServer
|
||||||
|
|
||||||
if LIBRARY_DIR:
|
if LIBRARY_DIR:
|
||||||
|
@ -51,24 +50,41 @@ else:
|
||||||
|
|
||||||
import sapi
|
import sapi
|
||||||
import viewvc
|
import viewvc
|
||||||
import compat; compat.for_standalone()
|
|
||||||
|
|
||||||
|
# The 'crypt' module is only available on Unix platforms. We'll try
|
||||||
|
# to use 'fcrypt' if it's available (for more information, see
|
||||||
|
# http://carey.geek.nz/code/python-fcrypt/).
|
||||||
|
has_crypt = False
|
||||||
|
try:
|
||||||
|
import crypt
|
||||||
|
has_crypt = True
|
||||||
|
def _check_passwd(user_passwd, real_passwd):
|
||||||
|
return real_passwd == crypt.crypt(user_passwd, real_passwd[:2])
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import fcrypt
|
||||||
|
has_crypt = True
|
||||||
|
def _check_passwd(user_passwd, real_passwd):
|
||||||
|
return real_passwd == fcrypt.crypt(user_passwd, real_passwd[:2])
|
||||||
|
except ImportError:
|
||||||
|
def _check_passwd(user_passwd, real_passwd):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class Options:
|
class Options:
|
||||||
port = 49152 # default TCP/IP port used for the server
|
port = 49152 # default TCP/IP port used for the server
|
||||||
start_gui = 0 # No GUI unless requested.
|
|
||||||
daemon = 0 # stay in the foreground by default
|
|
||||||
repositories = {} # use default repositories specified in config
|
repositories = {} # use default repositories specified in config
|
||||||
if sys.platform == 'mac':
|
host = sys.platform == 'mac' and '127.0.0.1' or 'localhost'
|
||||||
host = '127.0.0.1'
|
|
||||||
else:
|
|
||||||
host = 'localhost'
|
|
||||||
script_alias = 'viewvc'
|
script_alias = 'viewvc'
|
||||||
config_file = None
|
config_file = None
|
||||||
|
htpasswd_file = None
|
||||||
|
|
||||||
# --- web browser interface: ----------------------------------------------
|
|
||||||
|
|
||||||
class StandaloneServer(sapi.CgiServer):
|
class StandaloneServer(sapi.CgiServer):
|
||||||
|
"""Custom sapi interface that uses a BaseHTTPRequestHandler HANDLER
|
||||||
|
to generate output."""
|
||||||
|
|
||||||
def __init__(self, handler):
|
def __init__(self, handler):
|
||||||
sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32")
|
sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32")
|
||||||
self.handler = handler
|
self.handler = handler
|
||||||
|
@ -80,7 +96,7 @@ class StandaloneServer(sapi.CgiServer):
|
||||||
statusCode = 200
|
statusCode = 200
|
||||||
statusText = 'OK'
|
statusText = 'OK'
|
||||||
else:
|
else:
|
||||||
p = string.find(status, ' ')
|
p = status.find(' ')
|
||||||
if p < 0:
|
if p < 0:
|
||||||
statusCode = int(status)
|
statusCode = int(status)
|
||||||
statusText = ''
|
statusText = ''
|
||||||
|
@ -94,78 +110,138 @@ class StandaloneServer(sapi.CgiServer):
|
||||||
self.handler.end_headers()
|
self.handler.end_headers()
|
||||||
|
|
||||||
|
|
||||||
def serve(host, port, callback=None):
|
class NotViewVCLocationException(Exception):
|
||||||
"""start a HTTP server on the given port. call 'callback' when the
|
"""The request location was not aimed at ViewVC."""
|
||||||
server is ready to serve"""
|
pass
|
||||||
|
|
||||||
class ViewVC_Handler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|
||||||
|
class AuthenticationException(Exception):
|
||||||
|
"""Authentication requirements have not been met."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ViewVCHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
|
"""Custom HTTP request handler for ViewVC."""
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
"""Serve a GET request."""
|
"""Serve a GET request."""
|
||||||
if not self.path or self.path == "/":
|
self.handle_request('GET')
|
||||||
self.redirect()
|
|
||||||
elif self.is_viewvc():
|
|
||||||
try:
|
|
||||||
self.run_viewvc()
|
|
||||||
except IOError:
|
|
||||||
# ignore IOError: [Errno 32] Broken pipe
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
self.send_error(404)
|
|
||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
"""Serve a POST request."""
|
"""Serve a POST request."""
|
||||||
if self.is_viewvc():
|
self.handle_request('POST')
|
||||||
|
|
||||||
|
def handle_request(self, method):
|
||||||
|
"""Handle a request of type METHOD."""
|
||||||
|
try:
|
||||||
self.run_viewvc()
|
self.run_viewvc()
|
||||||
else:
|
except NotViewVCLocationException:
|
||||||
self.send_error(501, "Can only POST to %s"
|
# If the request was aimed at the server root, but there's a
|
||||||
% (options.script_alias))
|
# non-empty script_alias, automatically redirect to the
|
||||||
|
# script_alias. Otherwise, just return a 404 and shrug.
|
||||||
def is_viewvc(self):
|
if (not self.path or self.path == "/") and options.script_alias:
|
||||||
"""Check whether self.path is, or is a child of, the ScriptAlias"""
|
|
||||||
if self.path == '/' + options.script_alias:
|
|
||||||
return 1
|
|
||||||
if self.path[:len(options.script_alias)+2] == \
|
|
||||||
'/' + options.script_alias + '/':
|
|
||||||
return 1
|
|
||||||
if self.path[:len(options.script_alias)+2] == \
|
|
||||||
'/' + options.script_alias + '?':
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def redirect(self):
|
|
||||||
"""redirect the browser to the viewvc URL"""
|
|
||||||
new_url = self.server.url + options.script_alias + '/'
|
new_url = self.server.url + options.script_alias + '/'
|
||||||
self.send_response(301, "Moved (redirection follows)")
|
self.send_response(301, "Moved Permanently")
|
||||||
self.send_header("Content-type", "text/html")
|
self.send_header("Content-type", "text/html")
|
||||||
self.send_header("Location", new_url)
|
self.send_header("Location", new_url)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write("""<html>
|
self.wfile.write("""<html>
|
||||||
<head>
|
<head>
|
||||||
<meta http-equiv="refresh" content="1; URL=%s">
|
<meta http-equiv="refresh" content="10; url=%s" />
|
||||||
|
<title>Moved Temporarily</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Redirection to <a href="%s">ViewVC</a></h1>
|
<h1>Redirecting to ViewVC</h1>
|
||||||
Wait a second. You will be automatically redirected to <b>ViewVC</b>.
|
<p>You will be automatically redirected to <a href="%s">ViewVC</a>.
|
||||||
If this doesn't work, please click on the link above.
|
If this doesn't work, please click on the link above.</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
""" % tuple([new_url]*2))
|
""" % (new_url, new_url))
|
||||||
|
else:
|
||||||
|
self.send_error(404)
|
||||||
|
except IOError: # ignore IOError: [Errno 32] Broken pipe
|
||||||
|
pass
|
||||||
|
except AuthenticationException:
|
||||||
|
self.send_response(401, "Unauthorized")
|
||||||
|
self.send_header("WWW-Authenticate", 'Basic realm="ViewVC"')
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write("""<html>
|
||||||
|
<head>
|
||||||
|
<title>Authentication failed</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Authentication failed</h1>
|
||||||
|
<p>Authentication has failed. Please retry with the correct username
|
||||||
|
and password.</p>
|
||||||
|
</body>
|
||||||
|
</html>""")
|
||||||
|
|
||||||
|
def is_viewvc(self):
|
||||||
|
"""Check whether self.path is, or is a child of, the ScriptAlias"""
|
||||||
|
if not options.script_alias:
|
||||||
|
return 1
|
||||||
|
if self.path == '/' + options.script_alias:
|
||||||
|
return 1
|
||||||
|
alias_len = len(options.script_alias)
|
||||||
|
if self.path[:alias_len+2] == '/' + options.script_alias + '/':
|
||||||
|
return 1
|
||||||
|
if self.path[:alias_len+2] == '/' + options.script_alias + '?':
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def validate_password(self, htpasswd_file, username, password):
|
||||||
|
"""Compare USERNAME and PASSWORD against HTPASSWD_FILE."""
|
||||||
|
try:
|
||||||
|
lines = open(htpasswd_file, 'r').readlines()
|
||||||
|
for line in lines:
|
||||||
|
file_user, file_pass = line.rstrip().split(':', 1)
|
||||||
|
if username == file_user:
|
||||||
|
return _check_passwd(password, file_pass)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
def run_viewvc(self):
|
def run_viewvc(self):
|
||||||
"""This is a quick and dirty cut'n'rape from Python's
|
"""Run ViewVC to field a single request."""
|
||||||
standard library module CGIHTTPServer."""
|
|
||||||
scriptname = '/' + options.script_alias
|
### Much of this is adapter from Python's standard library
|
||||||
assert string.find(self.path, scriptname) == 0
|
### module CGIHTTPServer.
|
||||||
|
|
||||||
|
# Is this request even aimed at ViewVC? If not, complain.
|
||||||
|
if not self.is_viewvc():
|
||||||
|
raise NotViewVCLocationException()
|
||||||
|
|
||||||
|
# If htpasswd authentication is enabled, try to authenticate the user.
|
||||||
|
self.username = None
|
||||||
|
if options.htpasswd_file:
|
||||||
|
authn = self.headers.get('authorization')
|
||||||
|
if not authn:
|
||||||
|
raise AuthenticationException()
|
||||||
|
try:
|
||||||
|
kind, data = authn.split(' ', 1)
|
||||||
|
if kind == 'Basic':
|
||||||
|
data = base64.b64decode(data)
|
||||||
|
username, password = data.split(':', 1)
|
||||||
|
except:
|
||||||
|
raise AuthenticationException()
|
||||||
|
if not self.validate_password(options.htpasswd_file, username, password):
|
||||||
|
raise AuthenticationException()
|
||||||
|
self.username = username
|
||||||
|
|
||||||
|
# Setup the environment in preparation of executing ViewVC's core code.
|
||||||
|
env = os.environ
|
||||||
|
|
||||||
|
scriptname = options.script_alias and '/' + options.script_alias or ''
|
||||||
|
|
||||||
viewvc_url = self.server.url[:-1] + scriptname
|
viewvc_url = self.server.url[:-1] + scriptname
|
||||||
rest = self.path[len(scriptname):]
|
rest = self.path[len(scriptname):]
|
||||||
i = string.rfind(rest, '?')
|
i = rest.rfind('?')
|
||||||
if i >= 0:
|
if i >= 0:
|
||||||
rest, query = rest[:i], rest[i+1:]
|
rest, query = rest[:i], rest[i+1:]
|
||||||
else:
|
else:
|
||||||
query = ''
|
query = ''
|
||||||
# sys.stderr.write("Debug: '"+scriptname+"' '"+rest+"' '"+query+"'\n")
|
|
||||||
env = os.environ
|
|
||||||
# Since we're going to modify the env in the parent, provide empty
|
# Since we're going to modify the env in the parent, provide empty
|
||||||
# values to override previously set values
|
# values to override previously set values
|
||||||
for k in env.keys():
|
for k in env.keys():
|
||||||
|
@ -175,6 +251,7 @@ If this doesn't work, please click on the link above.
|
||||||
'HTTP_USER_AGENT', 'HTTP_COOKIE'):
|
'HTTP_USER_AGENT', 'HTTP_COOKIE'):
|
||||||
if env.has_key(k):
|
if env.has_key(k):
|
||||||
env[k] = ""
|
env[k] = ""
|
||||||
|
|
||||||
# XXX Much of the following could be prepared ahead of time!
|
# XXX Much of the following could be prepared ahead of time!
|
||||||
env['SERVER_SOFTWARE'] = self.version_string()
|
env['SERVER_SOFTWARE'] = self.version_string()
|
||||||
env['SERVER_NAME'] = self.server.server_name
|
env['SERVER_NAME'] = self.server.server_name
|
||||||
|
@ -192,9 +269,8 @@ If this doesn't work, please click on the link above.
|
||||||
if host != self.client_address[0]:
|
if host != self.client_address[0]:
|
||||||
env['REMOTE_HOST'] = host
|
env['REMOTE_HOST'] = host
|
||||||
env['REMOTE_ADDR'] = self.client_address[0]
|
env['REMOTE_ADDR'] = self.client_address[0]
|
||||||
# AUTH_TYPE
|
if self.username:
|
||||||
# REMOTE_USER
|
env['REMOTE_USER'] = self.username
|
||||||
# REMOTE_IDENT
|
|
||||||
if self.headers.typeheader is None:
|
if self.headers.typeheader is None:
|
||||||
env['CONTENT_TYPE'] = self.headers.type
|
env['CONTENT_TYPE'] = self.headers.type
|
||||||
else:
|
else:
|
||||||
|
@ -205,10 +281,10 @@ If this doesn't work, please click on the link above.
|
||||||
accept = []
|
accept = []
|
||||||
for line in self.headers.getallmatchingheaders('accept'):
|
for line in self.headers.getallmatchingheaders('accept'):
|
||||||
if line[:1] in string.whitespace:
|
if line[:1] in string.whitespace:
|
||||||
accept.append(string.strip(line))
|
accept.append(line.strip())
|
||||||
else:
|
else:
|
||||||
accept = accept + string.split(line[7:], ',')
|
accept = accept + line[7:].split(',')
|
||||||
env['HTTP_ACCEPT'] = string.joinfields(accept, ',')
|
env['HTTP_ACCEPT'] = ','.join(accept)
|
||||||
ua = self.headers.getheader('user-agent')
|
ua = self.headers.getheader('user-agent')
|
||||||
if ua:
|
if ua:
|
||||||
env['HTTP_USER_AGENT'] = ua
|
env['HTTP_USER_AGENT'] = ua
|
||||||
|
@ -218,8 +294,9 @@ If this doesn't work, please click on the link above.
|
||||||
etag = self.headers.getheader('if-none-match')
|
etag = self.headers.getheader('if-none-match')
|
||||||
if etag:
|
if etag:
|
||||||
env['HTTP_IF_NONE_MATCH'] = etag
|
env['HTTP_IF_NONE_MATCH'] = etag
|
||||||
|
# AUTH_TYPE
|
||||||
|
# REMOTE_IDENT
|
||||||
# XXX Other HTTP_* headers
|
# XXX Other HTTP_* headers
|
||||||
decoded_query = string.replace(query, '+', ' ')
|
|
||||||
|
|
||||||
# Preserve state, because we execute script in current process:
|
# Preserve state, because we execute script in current process:
|
||||||
save_argv = sys.argv
|
save_argv = sys.argv
|
||||||
|
@ -237,7 +314,8 @@ If this doesn't work, please click on the link above.
|
||||||
#
|
#
|
||||||
# But we no longer use pipe_cmds. So at the very least, the
|
# But we no longer use pipe_cmds. So at the very least, the
|
||||||
# comment is stale. Is the code okay, though?
|
# comment is stale. Is the code okay, though?
|
||||||
if sys.platform != "win32": save_realstdout = os.dup(1)
|
if sys.platform != "win32":
|
||||||
|
save_realstdout = os.dup(1)
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
sys.stdout = self.wfile
|
sys.stdout = self.wfile
|
||||||
|
@ -259,13 +337,15 @@ If this doesn't work, please click on the link above.
|
||||||
else:
|
else:
|
||||||
self.log_error("ViewVC exited ok")
|
self.log_error("ViewVC exited ok")
|
||||||
|
|
||||||
class ViewVC_Server(BaseHTTPServer.HTTPServer):
|
|
||||||
|
class ViewVCHTTPServer(BaseHTTPServer.HTTPServer):
|
||||||
|
"""Customized HTTP server for ViewVC."""
|
||||||
|
|
||||||
def __init__(self, host, port, callback):
|
def __init__(self, host, port, callback):
|
||||||
self.address = (host, port)
|
self.address = (host, port)
|
||||||
self.url = 'http://%s:%d/' % (host, port)
|
self.url = 'http://%s:%d/' % (host, port)
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
BaseHTTPServer.HTTPServer.__init__(self, self.address,
|
BaseHTTPServer.HTTPServer.__init__(self, self.address, self.handler)
|
||||||
self.handler)
|
|
||||||
|
|
||||||
def serve_until_quit(self):
|
def serve_until_quit(self):
|
||||||
self.quit = 0
|
self.quit = 0
|
||||||
|
@ -281,25 +361,27 @@ If this doesn't work, please click on the link above.
|
||||||
|
|
||||||
def server_bind(self):
|
def server_bind(self):
|
||||||
# set SO_REUSEADDR (if available on this platform)
|
# set SO_REUSEADDR (if available on this platform)
|
||||||
if hasattr(socket, 'SOL_SOCKET') \
|
if hasattr(socket, 'SOL_SOCKET') and hasattr(socket, 'SO_REUSEADDR'):
|
||||||
and hasattr(socket, 'SO_REUSEADDR'):
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET,
|
|
||||||
socket.SO_REUSEADDR, 1)
|
|
||||||
BaseHTTPServer.HTTPServer.server_bind(self)
|
BaseHTTPServer.HTTPServer.server_bind(self)
|
||||||
|
|
||||||
ViewVC_Server.handler = ViewVC_Handler
|
|
||||||
|
def serve(host, port, callback=None):
|
||||||
|
"""Start an HTTP server for HOST on PORT. Call CALLBACK function
|
||||||
|
when the server is ready to serve."""
|
||||||
|
|
||||||
|
ViewVCHTTPServer.handler = ViewVCHTTPRequestHandler
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# XXX Move this code out of this function.
|
# XXX Move this code out of this function.
|
||||||
# Early loading of configuration here. Used to
|
# Early loading of configuration here. Used to allow tinkering
|
||||||
# allow tinkering with some configuration settings:
|
# with some configuration settings:
|
||||||
handle_config(options.config_file)
|
handle_config(options.config_file)
|
||||||
if options.repositories:
|
if options.repositories:
|
||||||
cfg.general.default_root = "Development"
|
cfg.general.default_root = "Development"
|
||||||
for repo_name in options.repositories.keys():
|
for repo_name in options.repositories.keys():
|
||||||
repo_path = os.path.normpath(options.repositories[repo_name])
|
repo_path = os.path.normpath(options.repositories[repo_name])
|
||||||
if os.path.exists(os.path.join(repo_path, "CVSROOT",
|
if os.path.exists(os.path.join(repo_path, "CVSROOT", "config")):
|
||||||
"config")):
|
|
||||||
cfg.general.cvs_roots[repo_name] = repo_path
|
cfg.general.cvs_roots[repo_name] = repo_path
|
||||||
elif os.path.exists(os.path.join(repo_path, "format")):
|
elif os.path.exists(os.path.join(repo_path, "format")):
|
||||||
cfg.general.svn_roots[repo_name] = repo_path
|
cfg.general.svn_roots[repo_name] = repo_path
|
||||||
|
@ -322,8 +404,9 @@ If this doesn't work, please click on the link above.
|
||||||
try:
|
try:
|
||||||
while 1:
|
while 1:
|
||||||
line = fp.readline()
|
line = fp.readline()
|
||||||
if not line: break
|
if not line:
|
||||||
if string.find(line, "Concurrent Versions System (CVSNT)")>=0:
|
break
|
||||||
|
if line.find("Concurrent Versions System (CVSNT)") >= 0:
|
||||||
cvsnt_works = 1
|
cvsnt_works = 1
|
||||||
while fp.read(4096):
|
while fp.read(4096):
|
||||||
pass
|
pass
|
||||||
|
@ -335,349 +418,171 @@ If this doesn't work, please click on the link above.
|
||||||
if not cvsnt_works:
|
if not cvsnt_works:
|
||||||
cfg.utilities.cvsnt = None
|
cfg.utilities.cvsnt = None
|
||||||
|
|
||||||
ViewVC_Server(host, port, callback).serve_until_quit()
|
ViewVCHTTPServer(host, port, callback).serve_until_quit()
|
||||||
except (KeyboardInterrupt, select.error):
|
except (KeyboardInterrupt, select.error):
|
||||||
pass
|
pass
|
||||||
print 'server stopped'
|
print 'server stopped'
|
||||||
|
|
||||||
|
|
||||||
def handle_config(config_file):
|
def handle_config(config_file):
|
||||||
global cfg
|
global cfg
|
||||||
cfg = viewvc.load_config(config_file or CONF_PATHNAME)
|
cfg = viewvc.load_config(config_file or CONF_PATHNAME)
|
||||||
|
|
||||||
# --- graphical interface: --------------------------------------------------
|
|
||||||
|
|
||||||
def nogui(missing_module):
|
def usage():
|
||||||
sys.stderr.write(
|
clean_options = Options()
|
||||||
"Sorry! Your Python was compiled without the %s module"%missing_module+
|
|
||||||
" enabled.\nI'm unable to run the GUI part. Please omit the '-g'\n"+
|
|
||||||
"and '--gui' options or install another Python interpreter.\n")
|
|
||||||
raise SystemExit, 1
|
|
||||||
|
|
||||||
def gui(host, port):
|
|
||||||
"""Graphical interface (starts web server and pops up a control window)."""
|
|
||||||
class GUI:
|
|
||||||
def __init__(self, window, host, port):
|
|
||||||
self.window = window
|
|
||||||
self.server = None
|
|
||||||
self.scanner = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
import Tkinter
|
|
||||||
except ImportError:
|
|
||||||
nogui("Tkinter")
|
|
||||||
|
|
||||||
self.server_frm = Tkinter.Frame(window)
|
|
||||||
self.title_lbl = Tkinter.Label(self.server_frm,
|
|
||||||
text='Starting server...\n ')
|
|
||||||
self.open_btn = Tkinter.Button(self.server_frm,
|
|
||||||
text='open browser', command=self.open, state='disabled')
|
|
||||||
self.quit_btn = Tkinter.Button(self.server_frm,
|
|
||||||
text='quit serving', command=self.quit, state='disabled')
|
|
||||||
|
|
||||||
|
|
||||||
self.window.title('ViewVC standalone')
|
|
||||||
self.window.protocol('WM_DELETE_WINDOW', self.quit)
|
|
||||||
self.title_lbl.pack(side='top', fill='x')
|
|
||||||
self.open_btn.pack(side='left', fill='x', expand=1)
|
|
||||||
self.quit_btn.pack(side='right', fill='x', expand=1)
|
|
||||||
|
|
||||||
# Early loading of configuration here. Used to
|
|
||||||
# allow tinkering with configuration settings through the gui:
|
|
||||||
handle_config(options.config_file)
|
|
||||||
if not LIBRARY_DIR:
|
|
||||||
cfg.options.cvsgraph_conf = "../cgi/cvsgraph.conf.dist"
|
|
||||||
|
|
||||||
self.options_frm = Tkinter.Frame(window)
|
|
||||||
|
|
||||||
# cvsgraph toggle:
|
|
||||||
self.cvsgraph_ivar = Tkinter.IntVar()
|
|
||||||
self.cvsgraph_ivar.set(cfg.options.use_cvsgraph)
|
|
||||||
self.cvsgraph_toggle = Tkinter.Checkbutton(self.options_frm,
|
|
||||||
text="enable cvsgraph (needs binary)", var=self.cvsgraph_ivar,
|
|
||||||
command=self.toggle_use_cvsgraph)
|
|
||||||
self.cvsgraph_toggle.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# show_subdir_lastmod toggle:
|
|
||||||
self.subdirmod_ivar = Tkinter.IntVar()
|
|
||||||
self.subdirmod_ivar.set(cfg.options.show_subdir_lastmod)
|
|
||||||
self.subdirmod_toggle = Tkinter.Checkbutton(self.options_frm,
|
|
||||||
text="show subdir last mod (dir view)", var=self.subdirmod_ivar,
|
|
||||||
command=self.toggle_subdirmod)
|
|
||||||
self.subdirmod_toggle.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# use_re_search toggle:
|
|
||||||
self.useresearch_ivar = Tkinter.IntVar()
|
|
||||||
self.useresearch_ivar.set(cfg.options.use_re_search)
|
|
||||||
self.useresearch_toggle = Tkinter.Checkbutton(self.options_frm,
|
|
||||||
text="allow regular expr search", var=self.useresearch_ivar,
|
|
||||||
command=self.toggle_useresearch)
|
|
||||||
self.useresearch_toggle.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# use_localtime toggle:
|
|
||||||
self.use_localtime_ivar = Tkinter.IntVar()
|
|
||||||
self.use_localtime_ivar.set(cfg.options.use_localtime)
|
|
||||||
self.use_localtime_toggle = Tkinter.Checkbutton(self.options_frm,
|
|
||||||
text="use localtime (instead of UTC)",
|
|
||||||
var=self.use_localtime_ivar,
|
|
||||||
command=self.toggle_use_localtime)
|
|
||||||
self.use_localtime_toggle.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# log_pagesize integer var:
|
|
||||||
self.log_pagesize_lbl = Tkinter.Label(self.options_frm,
|
|
||||||
text='Paging (number of items per log page, 0 disables):')
|
|
||||||
self.log_pagesize_lbl.pack(side='top', anchor='w')
|
|
||||||
self.log_pagesize_ivar = Tkinter.IntVar()
|
|
||||||
self.log_pagesize_ivar.set(cfg.options.log_pagesize)
|
|
||||||
self.log_pagesize_entry = Tkinter.Entry(self.options_frm,
|
|
||||||
width=10, textvariable=self.log_pagesize_ivar)
|
|
||||||
self.log_pagesize_entry.bind('<Return>', self.set_log_pagesize)
|
|
||||||
self.log_pagesize_entry.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# dir_pagesize integer var:
|
|
||||||
self.dir_pagesize_lbl = Tkinter.Label(self.options_frm,
|
|
||||||
text='Paging (number of items per dir page, 0 disables):')
|
|
||||||
self.dir_pagesize_lbl.pack(side='top', anchor='w')
|
|
||||||
self.dir_pagesize_ivar = Tkinter.IntVar()
|
|
||||||
self.dir_pagesize_ivar.set(cfg.options.dir_pagesize)
|
|
||||||
self.dir_pagesize_entry = Tkinter.Entry(self.options_frm,
|
|
||||||
width=10, textvariable=self.dir_pagesize_ivar)
|
|
||||||
self.dir_pagesize_entry.bind('<Return>', self.set_dir_pagesize)
|
|
||||||
self.dir_pagesize_entry.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# directory view template:
|
|
||||||
self.dirtemplate_lbl = Tkinter.Label(self.options_frm,
|
|
||||||
text='Choose HTML Template for the Directory pages:')
|
|
||||||
self.dirtemplate_lbl.pack(side='top', anchor='w')
|
|
||||||
self.dirtemplate_svar = Tkinter.StringVar()
|
|
||||||
self.dirtemplate_svar.set(cfg.templates.directory)
|
|
||||||
self.dirtemplate_entry = Tkinter.Entry(self.options_frm,
|
|
||||||
width = 40, textvariable=self.dirtemplate_svar)
|
|
||||||
self.dirtemplate_entry.bind('<Return>', self.set_templates_directory)
|
|
||||||
self.dirtemplate_entry.pack(side='top', anchor='w')
|
|
||||||
self.templates_dir = Tkinter.Radiobutton(self.options_frm,
|
|
||||||
text="directory.ezt", value="templates/directory.ezt",
|
|
||||||
var=self.dirtemplate_svar, command=self.set_templates_directory)
|
|
||||||
self.templates_dir.pack(side='top', anchor='w')
|
|
||||||
self.templates_dir_alt = Tkinter.Radiobutton(self.options_frm,
|
|
||||||
text="dir_alternate.ezt", value="templates/dir_alternate.ezt",
|
|
||||||
var=self.dirtemplate_svar, command=self.set_templates_directory)
|
|
||||||
self.templates_dir_alt.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# log view template:
|
|
||||||
self.logtemplate_lbl = Tkinter.Label(self.options_frm,
|
|
||||||
text='Choose HTML Template for the Log pages:')
|
|
||||||
self.logtemplate_lbl.pack(side='top', anchor='w')
|
|
||||||
self.logtemplate_svar = Tkinter.StringVar()
|
|
||||||
self.logtemplate_svar.set(cfg.templates.log)
|
|
||||||
self.logtemplate_entry = Tkinter.Entry(self.options_frm,
|
|
||||||
width = 40, textvariable=self.logtemplate_svar)
|
|
||||||
self.logtemplate_entry.bind('<Return>', self.set_templates_log)
|
|
||||||
self.logtemplate_entry.pack(side='top', anchor='w')
|
|
||||||
self.templates_log = Tkinter.Radiobutton(self.options_frm,
|
|
||||||
text="log.ezt", value="templates/log.ezt",
|
|
||||||
var=self.logtemplate_svar, command=self.set_templates_log)
|
|
||||||
self.templates_log.pack(side='top', anchor='w')
|
|
||||||
self.templates_log_table = Tkinter.Radiobutton(self.options_frm,
|
|
||||||
text="log_table.ezt", value="templates/log_table.ezt",
|
|
||||||
var=self.logtemplate_svar, command=self.set_templates_log)
|
|
||||||
self.templates_log_table.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# query view template:
|
|
||||||
self.querytemplate_lbl = Tkinter.Label(self.options_frm,
|
|
||||||
text='Template for the database query page:')
|
|
||||||
self.querytemplate_lbl.pack(side='top', anchor='w')
|
|
||||||
self.querytemplate_svar = Tkinter.StringVar()
|
|
||||||
self.querytemplate_svar.set(cfg.templates.query)
|
|
||||||
self.querytemplate_entry = Tkinter.Entry(self.options_frm,
|
|
||||||
width = 40, textvariable=self.querytemplate_svar)
|
|
||||||
self.querytemplate_entry.bind('<Return>', self.set_templates_query)
|
|
||||||
self.querytemplate_entry.pack(side='top', anchor='w')
|
|
||||||
self.templates_query = Tkinter.Radiobutton(self.options_frm,
|
|
||||||
text="query.ezt", value="templates/query.ezt",
|
|
||||||
var=self.querytemplate_svar, command=self.set_templates_query)
|
|
||||||
self.templates_query.pack(side='top', anchor='w')
|
|
||||||
|
|
||||||
# pack and set window manager hints:
|
|
||||||
self.server_frm.pack(side='top', fill='x')
|
|
||||||
self.options_frm.pack(side='top', fill='x')
|
|
||||||
|
|
||||||
self.window.update()
|
|
||||||
self.minwidth = self.window.winfo_width()
|
|
||||||
self.minheight = self.window.winfo_height()
|
|
||||||
self.expanded = 0
|
|
||||||
self.window.wm_geometry('%dx%d' % (self.minwidth, self.minheight))
|
|
||||||
self.window.wm_minsize(self.minwidth, self.minheight)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import threading
|
|
||||||
except ImportError:
|
|
||||||
nogui("thread")
|
|
||||||
threading.Thread(target=serve,
|
|
||||||
args=(host, port, self.ready)).start()
|
|
||||||
|
|
||||||
def toggle_use_cvsgraph(self, event=None):
|
|
||||||
cfg.options.use_cvsgraph = self.cvsgraph_ivar.get()
|
|
||||||
|
|
||||||
def toggle_use_localtime(self, event=None):
|
|
||||||
cfg.options.use_localtime = self.use_localtime_ivar.get()
|
|
||||||
|
|
||||||
def toggle_subdirmod(self, event=None):
|
|
||||||
cfg.options.show_subdir_lastmod = self.subdirmod_ivar.get()
|
|
||||||
|
|
||||||
def toggle_useresearch(self, event=None):
|
|
||||||
cfg.options.use_re_search = self.useresearch_ivar.get()
|
|
||||||
|
|
||||||
def set_log_pagesize(self, event=None):
|
|
||||||
cfg.options.log_pagesize = self.log_pagesize_ivar.get()
|
|
||||||
|
|
||||||
def set_dir_pagesize(self, event=None):
|
|
||||||
cfg.options.dir_pagesize = self.dir_pagesize_ivar.get()
|
|
||||||
|
|
||||||
def set_templates_log(self, event=None):
|
|
||||||
cfg.templates.log = self.logtemplate_svar.get()
|
|
||||||
|
|
||||||
def set_templates_directory(self, event=None):
|
|
||||||
cfg.templates.directory = self.dirtemplate_svar.get()
|
|
||||||
|
|
||||||
def set_templates_query(self, event=None):
|
|
||||||
cfg.templates.query = self.querytemplate_svar.get()
|
|
||||||
|
|
||||||
def ready(self, server):
|
|
||||||
"""used as callback parameter to the serve() function"""
|
|
||||||
self.server = server
|
|
||||||
self.title_lbl.config(
|
|
||||||
text='ViewVC standalone server at\n' + server.url)
|
|
||||||
self.open_btn.config(state='normal')
|
|
||||||
self.quit_btn.config(state='normal')
|
|
||||||
|
|
||||||
def open(self, event=None, url=None):
|
|
||||||
"""opens a browser window on the local machine"""
|
|
||||||
url = url or self.server.url
|
|
||||||
try:
|
|
||||||
import webbrowser
|
|
||||||
webbrowser.open(url)
|
|
||||||
except ImportError: # pre-webbrowser.py compatibility
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
os.system('start "%s"' % url)
|
|
||||||
elif sys.platform == 'mac':
|
|
||||||
try:
|
|
||||||
import ic
|
|
||||||
ic.launchurl(url)
|
|
||||||
except ImportError: pass
|
|
||||||
else:
|
|
||||||
rc = os.system('netscape -remote "openURL(%s)" &' % url)
|
|
||||||
if rc: os.system('netscape "%s" &' % url)
|
|
||||||
|
|
||||||
def quit(self, event=None):
|
|
||||||
if self.server:
|
|
||||||
self.server.quit = 1
|
|
||||||
self.window.quit()
|
|
||||||
|
|
||||||
import Tkinter
|
|
||||||
try:
|
|
||||||
gui = GUI(Tkinter.Tk(), host, port)
|
|
||||||
Tkinter.mainloop()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# --- command-line interface: ----------------------------------------------
|
|
||||||
|
|
||||||
def cli(argv):
|
|
||||||
"""Command-line interface (looks at argv to decide what to do)."""
|
|
||||||
import getopt
|
|
||||||
class BadUsage(Exception): pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
opts, args = getopt.getopt(argv[1:], 'gdc:p:r:h:s:',
|
|
||||||
['gui', 'daemon', 'config-file=', 'host=',
|
|
||||||
'port=', 'repository=', 'script-alias='])
|
|
||||||
for opt, val in opts:
|
|
||||||
if opt in ('-g', '--gui'):
|
|
||||||
options.start_gui = 1
|
|
||||||
elif opt in ('-r', '--repository'):
|
|
||||||
if options.repositories: # option may be used more than once:
|
|
||||||
num = len(options.repositories.keys())+1
|
|
||||||
symbolic_name = "Repository"+str(num)
|
|
||||||
options.repositories[symbolic_name] = val
|
|
||||||
else:
|
|
||||||
options.repositories["Development"] = val
|
|
||||||
elif opt in ('-d', '--daemon'):
|
|
||||||
options.daemon = 1
|
|
||||||
elif opt in ('-p', '--port'):
|
|
||||||
try:
|
|
||||||
options.port = int(val)
|
|
||||||
except ValueError:
|
|
||||||
raise BadUsage, "Port '%s' is not a valid port number" \
|
|
||||||
% (val)
|
|
||||||
elif opt in ('-h', '--host'):
|
|
||||||
options.host = val
|
|
||||||
elif opt in ('-s', '--script-alias'):
|
|
||||||
options.script_alias = \
|
|
||||||
string.join(filter(None, string.split(val, '/')), '/')
|
|
||||||
elif opt in ('-c', '--config-file'):
|
|
||||||
options.config_file = val
|
|
||||||
if options.start_gui and options.config_file:
|
|
||||||
raise BadUsage, "--config-file option is not valid in GUI mode."
|
|
||||||
if not options.start_gui and not options.port:
|
|
||||||
raise BadUsage, "You must supply a valid port, or run in GUI mode."
|
|
||||||
if options.daemon:
|
|
||||||
pid = os.fork()
|
|
||||||
if pid != 0:
|
|
||||||
sys.exit()
|
|
||||||
if options.start_gui:
|
|
||||||
gui(options.host, options.port)
|
|
||||||
return
|
|
||||||
elif options.port:
|
|
||||||
def ready(server):
|
|
||||||
print 'server ready at %s%s' % (server.url,
|
|
||||||
options.script_alias)
|
|
||||||
serve(options.host, options.port, ready)
|
|
||||||
return
|
|
||||||
except (getopt.error, BadUsage), err:
|
|
||||||
cmd = os.path.basename(sys.argv[0])
|
cmd = os.path.basename(sys.argv[0])
|
||||||
port = options.port
|
port = clean_options.port
|
||||||
host = options.host
|
host = clean_options.host
|
||||||
script_alias = options.script_alias
|
script_alias = clean_options.script_alias
|
||||||
if str(err):
|
|
||||||
sys.stderr.write("ERROR: %s\n\n" % (str(err)))
|
|
||||||
sys.stderr.write("""Usage: %(cmd)s [OPTIONS]
|
sys.stderr.write("""Usage: %(cmd)s [OPTIONS]
|
||||||
|
|
||||||
Run a simple, standalone HTTP server configured to serve up ViewVC
|
Run a simple, standalone HTTP server configured to serve up ViewVC requests.
|
||||||
requests.
|
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
--config-file=PATH (-c) Use the file at PATH as the ViewVC configuration
|
--config-file=FILE (-c) Read configuration options from FILE. If not
|
||||||
file. If not specified, ViewVC will try to use
|
specified, ViewVC will look for a configuration
|
||||||
the configuration file in its installation tree;
|
file in its installation tree, falling back to
|
||||||
otherwise, built-in default values are used.
|
built-in default values.
|
||||||
(Not valid in GUI mode.)
|
|
||||||
|
|
||||||
--daemon (-d) Background the server process.
|
--daemon (-d) Background the server process.
|
||||||
|
|
||||||
--host=HOST (-h) Start the server listening on HOST. You need
|
--help Show this usage message and exit.
|
||||||
to provide the hostname if you want to
|
|
||||||
access the standalone server from a remote
|
|
||||||
machine. [default: %(host)s]
|
|
||||||
|
|
||||||
--port=PORT (-p) Start the server on the given PORT.
|
--host=HOSTNAME (-h) Listen on HOSTNAME. Required for access from a
|
||||||
[default: %(port)d]
|
remote machine. [default: %(host)s]
|
||||||
|
|
||||||
--repository=PATH (-r) Serve up the Subversion or CVS repository located
|
--htpasswd-file=FILE Authenticate incoming requests, validating against
|
||||||
|
against FILE, which is an Apache HTTP Server
|
||||||
|
htpasswd file. (CRYPT only; no DIGEST support.)
|
||||||
|
|
||||||
|
--port=PORT (-p) Listen on PORT. [default: %(port)d]
|
||||||
|
|
||||||
|
--repository=PATH (-r) Serve the Subversion or CVS repository located
|
||||||
at PATH. This option may be used more than once.
|
at PATH. This option may be used more than once.
|
||||||
|
|
||||||
--script-alias=PATH (-s) Specify the ScriptAlias, the artificial path
|
--script-alias=PATH (-s) Use PATH as the virtual script location (similar
|
||||||
location that at which ViewVC appears to be
|
to Apache HTTP Server's ScriptAlias directive).
|
||||||
located. For example, if your ScriptAlias is
|
For example, "--script-alias=repo/view" will serve
|
||||||
"cgi-bin/viewvc", then ViewVC will be accessible
|
ViewVC at "http://HOSTNAME:PORT/repo/view".
|
||||||
at "http://%(host)s:%(port)s/cgi-bin/viewvc".
|
|
||||||
[default: %(script_alias)s]
|
[default: %(script_alias)s]
|
||||||
|
|
||||||
--gui (-g) Pop up a graphical interface for serving and
|
|
||||||
testing ViewVC. NOTE: this requires a valid
|
|
||||||
X11 display connection.
|
|
||||||
""" % locals())
|
""" % locals())
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def badusage(errstr):
|
||||||
|
cmd = os.path.basename(sys.argv[0])
|
||||||
|
sys.stderr.write("ERROR: %s\n\n"
|
||||||
|
"Try '%s --help' for detailed usage information.\n"
|
||||||
|
% (errstr, cmd))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv):
|
||||||
|
"""Command-line interface (looks at argv to decide what to do)."""
|
||||||
|
import getopt
|
||||||
|
|
||||||
|
short_opts = ''.join(['c:',
|
||||||
|
'd',
|
||||||
|
'h:',
|
||||||
|
'p:',
|
||||||
|
'r:',
|
||||||
|
's:',
|
||||||
|
])
|
||||||
|
long_opts = ['daemon',
|
||||||
|
'config-file=',
|
||||||
|
'help',
|
||||||
|
'host=',
|
||||||
|
'htpasswd-file=',
|
||||||
|
'port=',
|
||||||
|
'repository=',
|
||||||
|
'script-alias=',
|
||||||
|
]
|
||||||
|
|
||||||
|
opt_daemon = False
|
||||||
|
opt_host = None
|
||||||
|
opt_port = None
|
||||||
|
opt_htpasswd_file = None
|
||||||
|
opt_config_file = None
|
||||||
|
opt_script_alias = None
|
||||||
|
opt_repositories = []
|
||||||
|
|
||||||
|
# Parse command-line options.
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
|
||||||
|
for opt, val in opts:
|
||||||
|
if opt in ['--help']:
|
||||||
|
usage()
|
||||||
|
elif opt in ['-r', '--repository']: # may be used more than once
|
||||||
|
opt_repositories.append(val)
|
||||||
|
elif opt in ['-d', '--daemon']:
|
||||||
|
opt_daemon = 1
|
||||||
|
elif opt in ['-p', '--port']:
|
||||||
|
opt_port = val
|
||||||
|
elif opt in ['-h', '--host']:
|
||||||
|
opt_host = val
|
||||||
|
elif opt in ['-s', '--script-alias']:
|
||||||
|
opt_script_alias = val
|
||||||
|
elif opt in ['-c', '--config-file']:
|
||||||
|
opt_config_file = val
|
||||||
|
elif opt in ['--htpasswd-file']:
|
||||||
|
opt_htpasswd_file = val
|
||||||
|
except getopt.error, err:
|
||||||
|
badusage(str(err))
|
||||||
|
|
||||||
|
# Validate options that need validating.
|
||||||
|
class BadUsage(Exception): pass
|
||||||
|
try:
|
||||||
|
if opt_port is not None:
|
||||||
|
try:
|
||||||
|
options.port = int(opt_port)
|
||||||
|
except ValueError:
|
||||||
|
raise BadUsage("Port '%s' is not a valid port number" % (opt_port))
|
||||||
|
if not options.port:
|
||||||
|
raise BadUsage("You must supply a valid port.")
|
||||||
|
if opt_htpasswd_file is not None:
|
||||||
|
if not os.path.isfile(opt_htpasswd_file):
|
||||||
|
raise BadUsage("'%s' does not appear to be a valid htpasswd file."
|
||||||
|
% (opt_htpasswd_file))
|
||||||
|
if not has_crypt:
|
||||||
|
raise BadUsage("Unable to locate suitable `crypt' module for use "
|
||||||
|
"with --htpasswd-file option. If your Python "
|
||||||
|
"distribution does not include this module (as is "
|
||||||
|
"the case on many non-Unix platforms), consider "
|
||||||
|
"installing the `fcrypt' module instead (see "
|
||||||
|
"http://carey.geek.nz/code/python-fcrypt/).")
|
||||||
|
options.htpasswd_file = opt_htpasswd_file
|
||||||
|
if opt_config_file is not None:
|
||||||
|
if not os.path.isfile(opt_config_file):
|
||||||
|
raise BadUsage("'%s' does not appear to be a valid configuration file."
|
||||||
|
% (opt_config_file))
|
||||||
|
options.config_file = opt_config_file
|
||||||
|
if opt_host is not None:
|
||||||
|
options.host = opt_host
|
||||||
|
if opt_script_alias is not None:
|
||||||
|
options.script_alias = '/'.join(filter(None, opt_script_alias.split('/')))
|
||||||
|
for repository in opt_repositories:
|
||||||
|
if not options.repositories.has_key('Development'):
|
||||||
|
rootname = 'Development'
|
||||||
|
else:
|
||||||
|
rootname = 'Repository%d' % (len(options.repositories.keys()) + 1)
|
||||||
|
options.repositories[rootname] = repository
|
||||||
|
except BadUsage, err:
|
||||||
|
badusage(str(err))
|
||||||
|
|
||||||
|
# Fork if we're in daemon mode.
|
||||||
|
if opt_daemon:
|
||||||
|
pid = os.fork()
|
||||||
|
if pid != 0:
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# Finaly, start the server.
|
||||||
|
def ready(server):
|
||||||
|
print 'server ready at %s%s' % (server.url, options.script_alias)
|
||||||
|
serve(options.host, options.port, ready)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
options = Options()
|
options = Options()
|
||||||
cli(sys.argv)
|
main(sys.argv)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
|
@ -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 []
|
|
@ -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()
|
|
@ -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 []
|
|
@ -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 — 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 — 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>, …). 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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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,12 +1572,7 @@ 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>
|
||||||
|
@ -1580,89 +1584,47 @@ allow_tar = 1
|
||||||
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
|
||||||
<dt>Colors: <strong>nav_header</strong>
|
|
||||||
and <strong>alt_background</strong></dt>
|
|
||||||
<dd>
|
|
||||||
These options have been incorporated into the
|
|
||||||
<code>header.ezt</code> template.
|
|
||||||
|
|
||||||
<p></p>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
<dt>
|
|
||||||
Images:
|
|
||||||
<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>directory.ezt</code>, <code>header.ezt</code>,
|
||||||
<code>log.ezt</code>, <code>log_table.ezt</code>, and
|
<code>log.ezt</code>, <code>log_table.ezt</code>, and
|
||||||
<code>query.ezt</code> templates.
|
<code>query.ezt</code> templates.</dd>
|
||||||
|
|
||||||
<p></p>
|
<dt><code>use_java_script</code>
|
||||||
</dd>
|
and <code>open_extern_window</code></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.</dd>
|
||||||
|
|
||||||
<dt><strong>use_java_script</strong>
|
<dt><code>show_author</code></dt>
|
||||||
and <strong>open_extern_window</strong></dt>
|
<dd>Changing this option would be quite strange and rare. If you
|
||||||
<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
|
do not want to show the author for the revisions, then you
|
||||||
should remove it from the various templates.
|
should remove it from the various templates.</dd>
|
||||||
|
|
||||||
<p></p>
|
<dt><code>hide_non_readable</code></dt>
|
||||||
</dd>
|
<dd>This option was never used, so it has been removed.</dd>
|
||||||
|
|
||||||
<dt><strong>hide_non_readable</strong></dt>
|
<dt><code>flip_links_in_dirview</code></dt>
|
||||||
<dd>
|
<dd>This option is no longer available. If you want the links in
|
||||||
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
|
your directory view flipped, then you may use the
|
||||||
<code>dir_alternate.ezt</code> template.
|
<code>dir_alternate.ezt</code> template.</dd>
|
||||||
|
|
||||||
<p></p>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -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" — 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" — 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>
|
||||||
|
|
|
@ -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] == ',':
|
||||||
|
|
26
lib/blame.py
|
@ -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&r2=%s' % (self.diff_url, item.prev_rev, item.rev)
|
diff_url = '%sr1=%s&r2=%s' % (self.diff_url, item.prev_rev, item.rev)
|
||||||
thisline = link_includes(cgi.escape(item.text), self.repos,
|
thisline = link_includes(sapi.escape(item.text), self.repos,
|
||||||
self.path_parts, self.include_url)
|
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> </td><td> </td>')
|
sys.stdout.write('<td> </td><td> </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 ' '))
|
sys.stdout.write('<td%s>%s</td></tr>\n' % (align % 'left', thisline.rstrip() or ' '))
|
||||||
sys.stdout.write('</table>\n')
|
sys.stdout.write('</table>\n')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
180
lib/compat.py
|
@ -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
|
|
367
lib/config.py
|
@ -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)
|
||||||
for section in self.parser.sections():
|
if hasattr(self, authz_section):
|
||||||
if section == authz_section:
|
sub_config = getattr(self, authz_section)
|
||||||
for key, value in self._get_parser_items(self.parser, section):
|
for attr in dir(sub_config):
|
||||||
params[key] = value
|
params[attr] = getattr(sub_config, attr)
|
||||||
if rootname:
|
|
||||||
root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer)
|
root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer)
|
||||||
for section in self.parser.sections():
|
for section in self.parser.sections():
|
||||||
if section == root_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
|
||||||
params['__config'] = self
|
return authorizer, params
|
||||||
|
|
||||||
|
def get_authorizer_params(self, authorizer=None):
|
||||||
|
"""Return a dictionary of parameter names and values which belong
|
||||||
|
to the configured authorizer (or AUTHORIZER, if provided)."""
|
||||||
|
params = {}
|
||||||
|
if authorizer is None:
|
||||||
|
authorizer = self.options.authorizer
|
||||||
|
if authorizer:
|
||||||
|
authz_section = 'authz-%s' % (self.options.authorizer)
|
||||||
|
if hasattr(self, authz_section):
|
||||||
|
sub_config = getattr(self, authz_section)
|
||||||
|
for attr in dir(sub_config):
|
||||||
|
params[attr] = getattr(sub_config, attr)
|
||||||
return params
|
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
|
|
||||||
+ ']*=\)\(.*\)$')
|
|
||||||
|
|
81
lib/cvsdb.py
|
@ -1,5 +1,5 @@
|
||||||
#
|
#
|
||||||
# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved.
|
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
|
||||||
#
|
#
|
||||||
# By using this file, you agree to the terms and conditions set forth in
|
# 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:
|
||||||
|
if detect_leftover:
|
||||||
|
limit = "LIMIT %s" % (str(query.limit + 1))
|
||||||
|
else:
|
||||||
limit = "LIMIT %s" % (str(query.limit))
|
limit = "LIMIT %s" % (str(query.limit))
|
||||||
elif self._row_limit:
|
|
||||||
limit = "LIMIT %s" % (str(self._row_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,6 +1115,7 @@ 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:])
|
||||||
|
|
||||||
|
|
|
@ -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,))
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
113
lib/ezt.py
|
@ -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('&', '&')
|
||||||
|
s = s.replace('<', '<')
|
||||||
|
s = s.replace('>', '>')
|
||||||
|
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 ---
|
||||||
|
|
39
lib/idiff.py
|
@ -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
|
||||||
|
|
11
lib/popen.py
|
@ -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.
|
||||||
|
|
120
lib/query.py
|
@ -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 ' '
|
||||||
if desc:
|
|
||||||
ob.log = string.replace(server.escape(desc), '\n', '<br />')
|
|
||||||
else:
|
|
||||||
ob.log = ' '
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
93
lib/sapi.py
|
@ -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('&', '&')
|
||||||
|
s = s.replace('>', '>')
|
||||||
|
s = s.replace('<', '<')
|
||||||
|
s = s.replace('"', """)
|
||||||
|
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:
|
||||||
|
@ -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]
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
||||||
|
@ -30,6 +26,14 @@ class GenericViewVCAuthorizer:
|
||||||
"""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
|
||||||
|
@ -45,5 +49,8 @@ class ViewVCAuthorizer(GenericViewVCAuthorizer):
|
||||||
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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
try:
|
||||||
cp.read(self.authz_file)
|
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]
|
||||||
|
|
|
@ -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.
|
||||||
|
@ -197,6 +207,20 @@ 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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,15 +18,18 @@ 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):
|
||||||
|
@ -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,7 +87,7 @@ 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 = [ ]
|
||||||
|
@ -136,18 +144,22 @@ 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):
|
||||||
def _get_tip_revision(self, rcs_file, rev=None):
|
def _get_tip_revision(self, rcs_file, rev=None):
|
||||||
|
@ -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,8 +297,7 @@ 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)
|
||||||
|
@ -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,11 +375,9 @@ 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):
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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 \
|
||||||
|
((tag.number == rev.number[:-1] and
|
||||||
(not self.matching_rev or
|
(not self.matching_rev or
|
||||||
rev.number > self.matching_rev.number)):
|
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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,6 +341,7 @@ class _Parser:
|
||||||
else:
|
else:
|
||||||
# Chew up "newphrase"
|
# Chew up "newphrase"
|
||||||
# warn("Unexpected RCS token: $token\n")
|
# warn("Unexpected RCS token: $token\n")
|
||||||
|
while self.ts.get() != ';':
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if f is None:
|
if f is None:
|
||||||
|
@ -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,)
|
||||||
|
try:
|
||||||
timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,))
|
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
try:
|
|
||||||
import svn.core
|
import svn.core
|
||||||
return svn.core.svn_path_canonicalize(rootpath)
|
try:
|
||||||
except:
|
return svn.core.svn_path_canonicalize(path)
|
||||||
if re.search(_re_url, rootpath):
|
except AttributeError: # svn_path_canonicalize() appeared in 1.4.0 bindings
|
||||||
return rootpath[-1] == '/' and rootpath[:-1] or rootpath
|
# There's so much more that we *could* do here, but if we're
|
||||||
return os.path.normpath(rootpath)
|
# here at all its because there's a really old Subversion in
|
||||||
|
# place, and those older Subversion versions cared quite a bit
|
||||||
|
# less about the specifics of path canonicalization.
|
||||||
|
if re.search(_re_url, path):
|
||||||
|
return path.rstrip('/')
|
||||||
|
else:
|
||||||
|
return os.path.normpath(path)
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_rootpath(rootpath):
|
||||||
|
# Try to canonicalize the rootpath using Subversion semantics.
|
||||||
|
rootpath = _canonicalize_path(rootpath)
|
||||||
|
|
||||||
|
# ViewVC's support for local repositories is more complete and more
|
||||||
|
# performant than its support for remote ones, so if we're on a
|
||||||
|
# Unix-y system and we have a file:/// URL, convert it to a local
|
||||||
|
# path instead.
|
||||||
|
if os.name == 'posix':
|
||||||
|
rootpath_lower = rootpath.lower()
|
||||||
|
if rootpath_lower in ['file://localhost',
|
||||||
|
'file://localhost/',
|
||||||
|
'file://',
|
||||||
|
'file:///'
|
||||||
|
]:
|
||||||
|
return '/'
|
||||||
|
if rootpath_lower.startswith('file://localhost/'):
|
||||||
|
rootpath = os.path.normpath(urllib.unquote(rootpath[16:]))
|
||||||
|
elif rootpath_lower.startswith('file:///'):
|
||||||
|
rootpath = os.path.normpath(urllib.unquote(rootpath[7:]))
|
||||||
|
|
||||||
|
# Ensure that we have an absolute path (or URL), and return.
|
||||||
|
if not re.search(_re_url, rootpath):
|
||||||
|
assert os.path.isabs(rootpath)
|
||||||
|
return rootpath
|
||||||
|
|
||||||
|
|
||||||
def expand_root_parent(parent_path):
|
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):
|
||||||
|
|
|
@ -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):
|
||||||
|
entry = Revision(revision, date, author, msg, None, self.lockinfo,
|
||||||
|
self.path[1:], None, None)
|
||||||
self.logs.append(entry)
|
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,
|
||||||
|
@ -178,6 +236,10 @@ class RemoteSubversionRepository(vclib.Repository):
|
||||||
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,18 +273,16 @@ 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)
|
||||||
|
@ -237,7 +297,8 @@ 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:
|
||||||
|
kind = None
|
||||||
entries.append(vclib.DirEntry(name, kind))
|
entries.append(vclib.DirEntry(name, kind))
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
basename = path_parts[-1]
|
||||||
|
list_url = self._geturl(self._getpath(path_parts[:-1]))
|
||||||
|
dirents, locks = list_directory(list_url, _rev2optrev(rev),
|
||||||
_rev2optrev(rev), 0, self.ctx)
|
_rev2optrev(rev), 0, self.ctx)
|
||||||
if locks.has_key(basename):
|
if locks.has_key(basename):
|
||||||
lockinfo = locks[basename].owner
|
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
|
||||||
|
|
||||||
|
# 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,
|
blame_data.append(vclib.Annotation(line, line_no + 1, revision, prev_rev,
|
||||||
author, None))
|
author, date))
|
||||||
|
|
||||||
client.svn_client_blame(url, _rev2optrev(1), _rev2optrev(rev),
|
|
||||||
_blame_cb, self.ctx)
|
|
||||||
|
|
||||||
|
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):
|
# Now, this object might not have been directly edited since the
|
||||||
# return 5-tuple (date, author, message, changes)
|
# last-changed-rev, but it might have been the child of a copy.
|
||||||
optrev = _rev2optrev(rev)
|
# 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 = []
|
revs = []
|
||||||
|
|
||||||
def _log_cb(changed_paths, revision, author,
|
def _log_cb(log_entry, pool, retval=revs):
|
||||||
datestr, message, pool, retval=revs):
|
# If Subversion happens to call us more than once, we choose not
|
||||||
date = _datestr_to_date(datestr)
|
# 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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 = (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:]
|
||||||
|
|
||||||
|
|
|
@ -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,6 +28,13 @@ 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'):
|
||||||
|
@ -38,18 +42,25 @@ def _fix_subversion_exception(e):
|
||||||
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,40 +518,96 @@ 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):
|
def revinfo(self, rev):
|
||||||
fsroot = self._getroot(rev)
|
return self._revinfo(rev, 1)
|
||||||
|
|
||||||
# Get the changes for the revision
|
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
||||||
|
if path_parts1:
|
||||||
|
p1 = self._getpath(path_parts1)
|
||||||
|
r1 = self._getrev(rev1)
|
||||||
|
if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1):
|
||||||
|
raise vclib.ItemNotFound(path_parts1)
|
||||||
|
else:
|
||||||
|
p1 = None
|
||||||
|
|
||||||
|
if path_parts2:
|
||||||
|
p2 = self._getpath(path_parts2)
|
||||||
|
r2 = self._getrev(rev2)
|
||||||
|
if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2):
|
||||||
|
raise vclib.ItemNotFound(path_parts2)
|
||||||
|
else:
|
||||||
|
if not p1:
|
||||||
|
raise vclib.ItemNotFound(path_parts2)
|
||||||
|
p2 = None
|
||||||
|
|
||||||
|
args = vclib._diff_args(type, options)
|
||||||
|
|
||||||
|
def _date_from_rev(rev):
|
||||||
|
date, author, msg, revprops, changes = self._revinfo(rev)
|
||||||
|
return date
|
||||||
|
|
||||||
|
try:
|
||||||
|
if p1:
|
||||||
|
temp1 = temp_checkout(self, p1, r1)
|
||||||
|
info1 = p1, _date_from_rev(r1), r1
|
||||||
|
else:
|
||||||
|
temp1 = '/dev/null'
|
||||||
|
info1 = '/dev/null', _date_from_rev(rev1), rev1
|
||||||
|
if p2:
|
||||||
|
temp2 = temp_checkout(self, p2, r2)
|
||||||
|
info2 = p2, _date_from_rev(r2), r2
|
||||||
|
else:
|
||||||
|
temp2 = '/dev/null'
|
||||||
|
info2 = '/dev/null', _date_from_rev(rev2), rev2
|
||||||
|
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
|
||||||
|
except core.SubversionException, e:
|
||||||
|
_fix_subversion_exception(e)
|
||||||
|
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
||||||
|
raise vclib.InvalidRevision
|
||||||
|
if e.apr_err != _SVN_ERR_CEASE_INVOCATION:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def isexecutable(self, path_parts, rev):
|
||||||
|
props = self.itemprops(path_parts, rev) # does authz-check
|
||||||
|
return props.has_key(core.SVN_PROP_EXECUTABLE)
|
||||||
|
|
||||||
|
def filesize(self, path_parts, rev):
|
||||||
|
path = self._getpath(path_parts)
|
||||||
|
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
|
||||||
|
raise vclib.Error("Path '%s' is not a file." % path)
|
||||||
|
fsroot = self._getroot(self._getrev(rev))
|
||||||
|
return fs.file_length(fsroot, path)
|
||||||
|
|
||||||
|
##--- helpers ---##
|
||||||
|
|
||||||
|
def _revinfo(self, rev, include_changed_paths=0):
|
||||||
|
"""Internal-use, cache-friendly revision information harvester."""
|
||||||
|
|
||||||
|
def _get_changed_paths(fsroot):
|
||||||
|
"""Return a 3-tuple: found_readable, found_unreadable, changed_paths."""
|
||||||
editor = repos.ChangeCollector(self.fs_ptr, fsroot)
|
editor = repos.ChangeCollector(self.fs_ptr, fsroot)
|
||||||
e_ptr, e_baton = delta.make_editor(editor)
|
e_ptr, e_baton = delta.make_editor(editor)
|
||||||
repos.svn_repos_replay(fsroot, e_ptr, e_baton)
|
repos.svn_repos_replay(fsroot, e_ptr, e_baton)
|
||||||
changes = editor.get_changes()
|
|
||||||
changedpaths = {}
|
changedpaths = {}
|
||||||
|
changes = editor.get_changes()
|
||||||
|
|
||||||
# Now get the revision property info. Would use
|
# Copy the Subversion changes into a new hash, checking
|
||||||
# editor.get_root_props(), but something is broken there...
|
# authorization and converting them into ChangedPath objects.
|
||||||
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
|
found_readable = found_unreadable = 0
|
||||||
for path in changes.keys():
|
for path in changes.keys():
|
||||||
change = changes[path]
|
change = changes[path]
|
||||||
|
@ -628,10 +652,12 @@ class LocalSubversionRepository(vclib.Repository):
|
||||||
if vclib.check_path_access(self, parts, pathtype, rev):
|
if vclib.check_path_access(self, parts, pathtype, rev):
|
||||||
if is_copy and change.base_path and (change.base_path != path):
|
if is_copy and change.base_path and (change.base_path != path):
|
||||||
parts = _path_parts(change.base_path)
|
parts = _path_parts(change.base_path)
|
||||||
if not vclib.check_path_access(self, parts, pathtype, change.base_rev):
|
if not vclib.check_path_access(self, parts, pathtype,
|
||||||
|
change.base_rev):
|
||||||
is_copy = 0
|
is_copy = 0
|
||||||
change.base_path = None
|
change.base_path = None
|
||||||
change.base_rev = None
|
change.base_rev = None
|
||||||
|
found_unreadable = 1
|
||||||
changedpaths[path] = SVNChangedPath(path, rev, pathtype,
|
changedpaths[path] = SVNChangedPath(path, rev, pathtype,
|
||||||
change.base_path,
|
change.base_path,
|
||||||
change.base_rev, action,
|
change.base_rev, action,
|
||||||
|
@ -640,83 +666,185 @@ class LocalSubversionRepository(vclib.Repository):
|
||||||
found_readable = 1
|
found_readable = 1
|
||||||
else:
|
else:
|
||||||
found_unreadable = 1
|
found_unreadable = 1
|
||||||
|
return found_readable, found_unreadable, changedpaths.values()
|
||||||
|
|
||||||
# Return our tuple, auth-filtered: date, author, msg, changes
|
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:
|
if found_unreadable:
|
||||||
msg = None
|
msg = None
|
||||||
if not found_readable:
|
if not found_readable:
|
||||||
author = None
|
author = None
|
||||||
datestr = None
|
date = None
|
||||||
|
return date, author, msg, revprops, changedpaths
|
||||||
|
|
||||||
date = _datestr_to_date(datestr)
|
# Consult the revinfo cache first. If we don't have cached info,
|
||||||
return date, author, msg, changedpaths.values()
|
# or our caller wants changed paths and we don't have those for
|
||||||
|
# this revision, go do the real work.
|
||||||
def revinfo(self, rev):
|
|
||||||
rev = self._getrev(rev)
|
rev = self._getrev(rev)
|
||||||
cached_info = self._revinfo_cache.get(rev)
|
cached_info = self._revinfo_cache.get(rev)
|
||||||
if not cached_info:
|
if not cached_info \
|
||||||
cached_info = self._revinfo_raw(rev)
|
or (include_changed_paths and cached_info[4] is None):
|
||||||
|
cached_info = _revinfo_helper(rev, include_changed_paths)
|
||||||
self._revinfo_cache[rev] = cached_info
|
self._revinfo_cache[rev] = cached_info
|
||||||
return cached_info[0], cached_info[1], cached_info[2], cached_info[3]
|
return tuple(cached_info)
|
||||||
|
|
||||||
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
|
def _log_helper(self, path, rev, lockinfo):
|
||||||
if path_parts1:
|
rev_root = fs.revision_root(self.fs_ptr, rev)
|
||||||
p1 = self._getpath(path_parts1)
|
copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path)
|
||||||
r1 = self._getrev(rev1)
|
date, author, msg, revprops, changes = self._revinfo(rev)
|
||||||
if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1):
|
if fs.is_file(rev_root, path):
|
||||||
raise vclib.ItemNotFound(path_parts1)
|
size = fs.file_length(rev_root, path)
|
||||||
else:
|
else:
|
||||||
p1 = None
|
size = None
|
||||||
|
return Revision(rev, date, author, msg, size, lockinfo, path,
|
||||||
|
copyfrom_path and _cleanup_path(copyfrom_path),
|
||||||
|
copyfrom_rev)
|
||||||
|
|
||||||
if path_parts2:
|
def _get_history(self, path, rev, path_type, limit=0, options={}):
|
||||||
p2 = self._getpath(path_parts2)
|
if self.youngest == 0:
|
||||||
r2 = self._getrev(rev2)
|
return []
|
||||||
if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2):
|
|
||||||
raise vclib.ItemNotFound(path_parts2)
|
|
||||||
else:
|
|
||||||
if not p1:
|
|
||||||
raise vclib.ItemNotFound(path_parts2)
|
|
||||||
p2 = None
|
|
||||||
|
|
||||||
args = vclib._diff_args(type, options)
|
rev_paths = []
|
||||||
|
fsroot = self._getroot(rev)
|
||||||
def _date_from_rev(rev):
|
show_all_logs = options.get('svn_show_all_dir_logs', 0)
|
||||||
date, author, msg, changes = self.revinfo(rev)
|
if not show_all_logs:
|
||||||
return date
|
# 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:
|
try:
|
||||||
if p1:
|
repos.svn_repos_history(self.fs_ptr, path, history.add_history,
|
||||||
temp1 = temp_checkout(self, p1, r1)
|
1, rev, options.get('svn_cross_copies', 0))
|
||||||
info1 = p1, _date_from_rev(r1), r1
|
|
||||||
else:
|
|
||||||
temp1 = '/dev/null'
|
|
||||||
info1 = '/dev/null', _date_from_rev(rev1), rev1
|
|
||||||
if p2:
|
|
||||||
temp2 = temp_checkout(self, p2, r2)
|
|
||||||
info2 = p2, _date_from_rev(r2), r2
|
|
||||||
else:
|
|
||||||
temp2 = '/dev/null'
|
|
||||||
info2 = '/dev/null', _date_from_rev(rev2), rev2
|
|
||||||
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
|
|
||||||
except core.SubversionException, e:
|
except core.SubversionException, e:
|
||||||
_fix_subversion_exception(e)
|
_fix_subversion_exception(e)
|
||||||
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
|
if e.apr_err != _SVN_ERR_CEASE_INVOCATION:
|
||||||
raise vclib.InvalidRevision
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def isexecutable(self, path_parts, rev):
|
# Now, iterate over those history items, checking for changes of
|
||||||
props = self.itemprops(path_parts, rev) # does authz-check
|
# location, pruning as necessitated by authz rules.
|
||||||
return props.has_key(core.SVN_PROP_EXECUTABLE)
|
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:]
|
||||||
|
|
||||||
|
|
1832
lib/viewvc.py
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 */
|
|
@ -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)
|
|
@ -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>"
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 | '+' | '-' | '*'
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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]
|
|
@ -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);
|
||||||
|
}
|
|
@ -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 */
|
|
@ -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++
|
|
@ -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!!!
|
|
@ -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
|
|
@ -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++"])]
|
||||||
|
)
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 */
|
|
@ -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;
|
||||||
|
};
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
|
@ -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?
|
|
@ -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]
|
||||||
|
|
||||||
|
[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"> </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"> </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"]
|
Before Width: | Height: | Size: 764 B After Width: | Height: | Size: 764 B |
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 337 B |
Before Width: | Height: | Size: 205 B After Width: | Height: | Size: 205 B |
Before Width: | Height: | Size: 247 B After Width: | Height: | Size: 247 B |
Before Width: | Height: | Size: 755 B After Width: | Height: | Size: 755 B |
Before Width: | Height: | Size: 162 B After Width: | Height: | Size: 162 B |
Before Width: | Height: | Size: 219 B After Width: | Height: | Size: 219 B |
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 228 B |
Before Width: | Height: | Size: 167 B After Width: | Height: | Size: 167 B |