1
0
mirror of https://github.com/vitalif/viewvc-4intranet synced 2019-04-16 04:14:59 +03:00

Compare commits

..

24 Commits

Author SHA1 Message Date
cmpilato
51298ea2b8 Tag the 1.0.2 final release.
git-svn-id: http://viewvc.tigris.org/svn/viewvc/tags/1.0.2@1433 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-09-29 16:36:38 +00:00
cmpilato
51011abec8 Backport to the 1.0.x branch r1428 from trunk, whose log message read
thusly:

   Update the PHP CGI hack and add a comment to the config file pointing
   out the CLI/CGI distinction. Thanks to Mark <mark@mitsein.net> and
   Brian G. Peterson <brian@braverock.com> for bringing this issue up
   on the users' list.
   
   * lib/viewvc.py
     (markup_stream_php): update the cgi hack
   
   * viewvc.conf.dist
     (utilities.php): add comment

Additionally:

* CHANGES
  Note this change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1429 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-09-28 01:13:36 +00:00
cmpilato
0bbe7f5751 Port r1422 to the 1.0.x branch.
* lib/viewvc.py
  (view_markup): Correct the syntax highlighter choosing logic.

* CHANGES
  Note this change.

git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1423 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-08-16 16:32:09 +00:00
cmpilato
1df169ab24 Backport r1416 to the 1.0.x branch.
* lib/vclib/svn/__init__.py
  (BlameSource.__init__):  Pass the --non-interactive flag to 'svn blame'
    so it doesn't wedge when cache authstuffs aren't available or sufficient.

* CHANGES
  Note this change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1419 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-08-01 18:17:39 +00:00
cmpilato
98e7612420 Backport fix for issue #245 (r1417) to the 1.0.x branch.
* lib/viewvc.py
  (build_commit): If the environment variable HTTPS is set to "on", then
    construct the rss_url data dictionary item with an "https" method instead
    of an "http" one.

* CHANGES
  Note this change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1418 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-08-01 18:15:32 +00:00
cmpilato
70b0076d88 Backport fix for issue #208 (r1414) to the 1.0.x branch.
* lib/blame.py
  (link_includes): Calculate include paths in such a way that '.' is
    dropped and '..' is normalized.  This should prevent us from
    sending paths through Subversion's APIs that aren't legal (and
    cause assertions to fire).

* CHANGES
  Note this change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1415 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-08-01 16:48:48 +00:00
cmpilato
e23b88d389 * CHANGES
Update with recent fixes.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1398 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-21 09:51:27 +00:00
cmpilato
336ee6b95d Backport to the 1.0.x branch r1396, whose log message readly thusly:
Fix issue #18 (Annotate not working in windows for subversion).
   Thanks to Hernán Martínez Foffani <hernan.martinez@ecc.es> for testing
   this patch on Windows.
   
   * lib/vclib/svn/__init__.py
     (BlameSource.__init__): Patch up the URLs passed to 'svn blame' on
       systems that don't use forward-slashes for path separators.

git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1397 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-21 09:51:13 +00:00
cmpilato
642130cf93 Merge the corrections to the INSTALL file made in r1386 to the 1.0.x branch.
Wish I'd remembered to do this *before* rolling 1.0.1...

* INSTALL
  Tweaky, tweaky...


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1395 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-21 09:40:36 +00:00
cmpilato
1e3c57f2e0 * lib/viewvc.py
(__version__): Bump to 1.0.2-dev.

git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1393 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-21 01:04:12 +00:00
cmpilato
9693f2b981 * CHANGES
Note recent change that uses real modification times for Subversion
  directories in generated tarballs.

git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1387 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-20 22:08:53 +00:00
cmpilato
69ab22922f Backport fix for issue #250 (r1384) to the 1.0.x branch.
* lib/viewvc.py
  (generate_tarball): Add new (optional) dir_mtime parameter, used to specify
    the modification time of the current directory while generating tarball
    data.  Now, allow empty directories in Subversion tarballs (while
    preserving the "pruning" behavior for CVS tarballs).

* CHANGES
  Note the change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1385 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-14 20:06:34 +00:00
cmpilato
d422c3abc9 Backport fix for issue #249 (r1382) to the 1.0.x branch.
* lib/query.py
  (main): Add 'rss_href' to the data dictionary so the template
    doesn't freak out.

* CHANGES
  Note this change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1383 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-14 18:35:29 +00:00
cmpilato
ded0015f5c Backport fix for issue #241 (r1377) to the 1.0.x branch.
* lib/viewvc.py
  (get_file_view_info): Fix an oops which caused some whack URL
    generation for download-as-text links on files whose path has changed
    over time.

* CHANGES
  Update to reflect this change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1378 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-14 15:53:09 +00:00
cmpilato
6232555f79 Backport fix for issue #237 (r1375) to the 1.0.x branch.
* lib/vclib/bincvs/__init__.py
  (_tag_tuple): Return () instead of raising a ValueError for
    single-slot revisions.

* CHANGES
  Update to reflect this change.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1376 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-07-14 15:31:44 +00:00
cmpilato
81fa4ce6fd Syncronize the 1.0.x CHANGES file with recent formatting fixes from trunk.
git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1369 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-05-26 15:49:25 +00:00
cmpilato
98378e7560 Backport the patch from r1367 (from issue #239), and not the change in
the CHANGES file.
   
* templates/include/footer.ezt
  Fix an XHTML validation error caused by not closing an <A> tag.

* CHANGES
  Note the bugfix.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1368 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-05-26 15:48:00 +00:00
cmpilato
8da2897df2 Backport r1357 to the 1.0.x release branch. That log message read:
Fix exception on log page when use_pagesize is enabled. Patch from
   Jay Rossiter / Signe <signe@cothlamadh.net> on users' list.
   
   * lib/viewvc.py
     (view_log): call paging() with right arguments


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1361 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-05-18 17:31:55 +00:00
cmpilato
45bca25fbc * CHANGES
Add change for r1357's bugfix.

git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1359 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-05-18 17:27:57 +00:00
cmpilato
a54b3bcbbe * lib/viewvc.py
(__version__): Bump to 1.0.1-dev.

* CHANGES
  Give a date for the 1.0.0 release.


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1352 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-05-01 21:20:24 +00:00
cmpilato
2612b3bd0a Merge r1339 from trunk into the 1.0.x branch.
git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1340 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-04-20 06:55:11 +00:00
rey4
588f19cb0b Merge trunk revision 1333 into 1.0 branch
* branches/1.0.x


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1336 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-04-14 14:33:45 +00:00
rey4
6d540deba2 Merge trunk doc updates from 1330 and 1331 onto 1.0 branch
* CHANGES
* INSTALL
* viewvc.org/index.html
* viewvc.org/upgrading.html
* windows/README
* www/index.html


git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1332 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-04-09 16:09:09 +00:00
cmpilato
9f612d3b29 Branch for 1.0.0 release.
git-svn-id: http://viewvc.tigris.org/svn/viewvc/branches/1.0.x@1327 8cb11bc2-c004-0410-86c3-e597b4017df7
2006-04-06 19:53:43 +00:00
443 changed files with 8835 additions and 31312 deletions

337
CHANGES
View File

@@ -1,339 +1,3 @@
4Intra.net/CUSTIS improvements
* Support for full-text search over file contents, including binary
documents like *.doc and so on using Sphinx Search and Apache Tika
server. Patched Tika with fixes for #TIKA709 and #TIKA964 is highly
recommended:
http://wiki.4intra.net/public/tika-app-1.2-fix-TIKA709-TIKA964.jar
(SHA1 efef722a5e2322f7c2616d096552a48134dc5faa)
* Access right checks in query results.
* Access right checks for repository root directories.
* New query parameters: repository, repo type, revision number.
* Authorizer for CVSnt ACLs.
* InnoDB, additional database indexes and some search query
optimisations.
* Support for specifying path to MySQL UNIX socket.
* Asynchronous hook examples for updating SVN and CVS repos.
* Slightly more correct charset guessing, especially for Russian.
* Support for diffing added/removed files.
* File lists in RSS feeds for 'classic' template.
* Path configuration via a single 'viewvcinstallpath.py' file,
not via editing multiple bin/* files.
* Link to repository list instead of viewvc.org from the logo.
* "rcsfile service" support used to workaround command execution
problems (fork/signal problems) from Apache mod_python.
* Some new fields in query form: revision number, repository,
repository type, and the branch field visible not only in
CVS roots, but also without the selected repository.
* Correct per-root overriding of configuration options.
Version 1.2.0 (released ??-???-????)
* bumped minimum support Python version to 2.4
* implemented support for property diffs (issue #383)
* allow user-configurable cvsgraph display (issue #336)
* allow rNNNN syntax for Subversion revision numbers (issue #441)
Version 1.1.20 (released 24-Apr-2013)
* fix tab-to-space handling regression in markup view
* fix regression in root lookup handling (issue #526)
Version 1.1.19 (released 22-Apr-2013)
* improve root lookup performance (issue #523)
* new 'max_filesize_kbytes' config option and handling (issue #524)
* tarball generation improvements:
- preserve Subversion symlinks in generated tarballs (issue #487)
- reduce memory usage of tarball generation logic
- fix double compression of generated tarballs (issue #525)
* file content handling improvements:
- expanded support for encoding detection and transcoding (issue #11)
- fix tab-to-space conversion bugs in markup, annotate, and diff views
- fix handling of trailing whitespace in diff view
* add support for timestamp display in ISO8601 format (issue #46)
Version 1.1.18 (released 28-Feb-2013)
* fix exception raised by BDB-backed SVN repositories (issue #519)
* hide revision-less files when rcsparse is in use
* include branchpoints in branch views using rcsparse (issue #347)
* miscellaneous cvsdb improvements:
- add --port option to make-database (issue #521)
- explicitly name columns in queries (issue #522)
- update MySQL syntax to avoid discontinued "TYPE=" terms
Version 1.1.17 (released 25-Oct-2012)
* fix exception caused by uninitialized variable usage (issue #516)
Version 1.1.16 (released 24-Oct-2012)
* security fix: escape "extra" diff info to avoid XSS attack (issue #515)
* add 'binary_mime_types' configuration option and handling (issue #510)
* fix 'select for diffs' persistence across log pages (issue #512)
* remove lock status and filesize check on directories in remote SVN views
* fix bogus 'Annotation of' page title for non-annotated view (issue #514)
Version 1.1.15 (released 22-Jun-2012)
* security fix: complete authz support for remote SVN views (issue #353)
* security fix: log msg leak in SVN revision view with unreadable copy source
* fix several instances of incorrect information in remote SVN views
* increase performance of some revision metadata lookups in remote SVN views
* fix RSS feed regression introduced in 1.1.14
Version 1.1.14 (released 12-Jun-2012)
* fix annotation of svn files with non-URI-safe paths (issue #504)
* handle file:/// Subversion rootpaths as local roots (issue #446)
* fix bug caused by trying to case-normalize anon usernames (issue #505)
* speed up log handling by reusing tokenization results (issue #506)
* add support for custom revision log markup rules (issue #246)
Version 1.1.13 (released 23-Jan-2012)
* fix svndbadmin failure on deleted paths under Subversion 1.7 (issue #499)
* fix annotation of files in svn roots with non-URI-safe paths
* fix stray annotation warning in markup display of images
* more gracefully handle attempts to display binary content (issue #501)
Version 1.1.12 (released 03-Nov-2011)
* fix path display in patch and certain diff views (issue #485)
* fix broken cvsdb glob searching (issue 486)
* allow svn revision specifiers to have leading r's (issue #441, #448)
* allow environmental override of configuration location (issue #494)
* fix exception HTML-escaping non-string data under WSGI (issue #454)
* add links to root logs from roots view (issue #470)
* use Pygments lexer-guessing functionality (issue #495)
Version 1.1.11 (released 17-May-2011)
* security fix: remove user-reachable override of cvsdb row limit
* fix broken standalone.py -c and -d options handling
* add --help option to standalone.py
* fix stack trace when asked to checkout a directory (issue #478)
* improve memory usage and speed of revision log markup (issue #477)
* fix broken annotation view in CVS keyword-bearing files (issue #479)
* warn users when query results are incomplete (issue #433)
* avoid parsing errors on RCS newphrases in the admin section (issue #483)
* make rlog parsing code more robust in certain error cases (issue #444)
Version 1.1.10 (released 15-Mar-2011)
* fix stack trace in Subversion revision info logic (issue #475, issue #476)
Version 1.1.9 (released 18-Feb-2011)
* vcauth universal access determinations (issue #425)
* rework svn revision info cache for performance
* make revision log "extra pages" count configurable
* fix Subversion 1.4.x revision log compatibility code regression
* display sanitized error when authzfile is malformed
* restore markup of URLs in file contents (issue #455)
* optionally display last-committed metadata in roots view (issue #457)
Version 1.1.8 (released 02-Dec-2010)
* fix slowness triggered by allow_compress=1 configuration (issue #467)
* allow use of 'fcrypt' for Windows standalone.py authn support (issue #471)
* yield more useful error on directory markup/annotate request (issue #472)
Version 1.1.7 (released 09-Sep-2010)
* display Subversion revision properties in the revision view (issue #453)
* fix exception in 'standalone.py -r REPOS' when run without a config file
* fix standalone.py server root deployments (--script-alias='')
* add rudimentary Basic authentication support to standalone.py (issue #49)
* fix obscure "unexpected NULL parent pool" Subversion bindings error
* enable path info / link display in remote Subversion root revision view
* fix vhost name case handling inconsistency (issue #466)
* use svn:mime-type property charset param as encoding hint
* markup Subversion revision references in log messages (issue #313)
* add rudimentary support for FastCGI-based deployments (issue #464)
* fix query script WSGI deployment
* add configuration to fix query script cross-linking to ViewVC
Version 1.1.6 (released 02-Jun-2010)
* add rudimentary support for WSGI-based deployments (issue #397)
* fix exception caused by trying to HTML-escape non-string data (issue #454)
* fix incorrect RSS feed Content-Type header (issue #449)
* fix RSS <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)
* security fix: validate the 'view' parameter to avoid XSS attack
* security fix: avoid printing illegal parameter names and values
* add optional support for character encoding detection (issue #400)
* fix username case handling in svnauthz module (issue #419)
* fix cvsdbadmin/svnadmin rebuild error on missing repos (issue #420)
* don't drop leading blank lines from colorized file contents (issue #422)
* add file.ezt template logic for optionally hiding binary file contents
Version 1.1.1 (released 03-Jun-2009)
* fix broken query form (missing required template variables) (issue #416)
* fix bug in cvsdb which caused rebuild operations to lose data (issue #417)
* fix cvsdb purge/rebuild repos lookup to error on missing repos
* fix misleading file contents view page title
Version 1.1.0 (released 13-May-2009)
* add support for full content diffs (issue #153)
* make many more data dictionary items available to all views
* various rcsparse and tparse module fixes
* add daemon mode to standalone.py (issue #235)
* rework helper application configuration options (issues #229, #62)
* teach standalone.py to recognize Subversion repositories via -r option
* now interpret relative paths in "viewvc.conf" as relative to that file
* add 'purge' subcommand to cvsdbadmin and svndbadmin (issue #271)
* fix orphaned data bug in cvsdbadmin/svndbadmin rebuild (issue #271)
* add support for query by log message (issues #22, #121)
* fix bug parsing 'svn blame' output with too-long author names (issue #221)
* fix default standalone.py port to be within private IANA range (issue #234)
* add unified configury of allowed views; checkout view disabled by default
* add support for ranges of revisions to svndbadmin (issue #224)
* make the query handling more forgiving of malformatted subdirs (issue #244)
* add support for per-root configuration overrides (issue #371)
* add support for optional email address mangling (issue #290)
* extensible path-based authorization subsystem (issue #268), supporting:
- Subversion authz files (new)
- regexp-based path hiding (for compat with 1.0.x)
- file glob top-level directory hiding (for compat with 1.0.x)
* allow default file view to be "markup" (issue #305)
* add support for displaying file/directory properties (issue #39)
* pagination improvements
* add gzip output encoding support for template-driven pages
* fix cache control bugs (issue #259)
* add RSS feed URL generation for file history
* add support for remote creation of ViewVC checkins database
* add integration with Pygments for syntax highlighting
* preserve executability of Subversion files in tarballs (issue #233)
* add ability to set Subversion runtime config dir (issue #351, issue #339)
* show RSS/query links only for roots found in commits database (issue #357)
* recognize Subversion svn:mime-type property values (issue #364)
* hide CVS files when viewing tags/branches on which they don't exist
* allow hiding of errorful entries from the directory view (issue #105)
* fix directory view sorting UI
* tolerate malformed Accept-Language headers (issue #396)
* allow MIME type mapping overrides in ViewVC configuration (issue #401)
* fix exception in rev-sorted remote Subversion directory views (issue #409)
* allow setting of page sizes for log and dir views individually (issue #402)
Version 1.0.13 (released 24-Oct-2012)
* security fix: escape "extra" diff info to avoid XSS attack (issue #515)
* security fix: remove user-reachable override of cvsdb row limit
* fix obscure "unexpected NULL parent pool" Subversion bindings error
* fix svndbadmin failure on deleted paths under Subversion 1.7 (issue #499)
Version 1.0.12 (released 02-Jun-2010)
* fix exception caused by trying to HTML-escape non-string data (issue #454)
Version 1.0.11 (released 29-Mar-2010)
* security fix: escape user-provided search_re input to avoid XSS attack
Version 1.0.10 (released 10-Mar-2010)
* security fix: escape user-provided query form input to avoid XSS attack
* fix errors viewing remote Subversion paths with URI-unsafe characters
* fix regexp input validation (issue #426, #427, #440)
Version 1.0.9 (released 11-Aug-2009)
* security fix: validate the 'view' parameter to avoid XSS attack
* security fix: avoid printing illegal parameter names and values
Version 1.0.8 (released 05-May-2009)
* fix directory view sorting UI
* tolerate malformed Accept-Language headers (issue #396)
* fix directory log views in revision-less Subversion repositories
* fix exception in rev-sorted remote Subversion directory views (issue #409)
Version 1.0.7 (released 14-Oct-2008)
* fix regression in the 'as text' download view (issue #373)
Version 1.0.6 (released 16-Sep-2008)
* security fix: ignore arbitrary user-provided MIME types (issue #354)
* fix bug in regexp search filter when used with sticky tag (issue #346)
* fix bug in handling of certain 'co' output (issue #348)
* fix regexp search filter template bug
* fix annotate code syntax error
* fix mod_python import cycle (issue #369)
Version 1.0.5 (released 28-Feb-2008)
* security fix: omit commits of all-forbidden files from query results
* security fix: disallow direct URL navigation to hidden CVSROOT folder
* security fix: strip forbidden paths from revision view
* security fix: don't traverse log history thru forbidden locations
* security fix: honor forbiddenness via diff view path parameters
* new 'forbiddenre' regexp-based path authorization feature
* fix root name conflict resolution inconsistencies (issue #287)
* fix an oversight in the CVS 1.12.9 loginfo-handler support
* fix RSS feed content type to be more specific (issue #306)
* fix entity escaping problems in RSS feed data (issue #238)
* fix bug in tarball generation for remote Subversion repositories
* fix query interface file-count-limiting logic
* fix query results plus/minus count to ignore forbidden files
* fix blame error caused by 'svn' unable to create runtime config dir
Version 1.0.4 (released 10-Apr-2007)
* fix some markup bugs in query views (issue #266)
* fix loginfo-handler's support for CVS 1.12.9 (issues #151, #257)
* make viewvc-install able to run from an arbitrary location
* update viewvc-install's output for readability
* fix bug writing commits to non-MyISAM databases (issue #262)
* allow long paths in generated tarballs (issue #12)
* fix bug interpreting EZT substitute patterns
* fix broken markup view disablement
* fix broken directory view link generation in directory log view
* fix Windows-specific viewvc-install bugs
* fix broke query result links for Subversion deleted items (issue #296)
* fix some output XHTML validation buglets
* fix database query cache staleness problems (issue #180)
Version 1.0.3 (released 13-Oct-2006)
* fix bug in path shown for Subversion deleted-under-copy items (issue #265)
* security fix: declare charset for views to avoid IE UTF7 XSS attack
Version 1.0.2 (released 29-Sep-2006)
* minor documentation fixes
@@ -486,6 +150,7 @@ Version 0.9 (released 23-Dec-2001)
* create dir_alternate.ezt for the flipped rev/name links
* various UI tweaks for the directory pages
Version 0.8 (released 10-Dec-2001)
* add EZT templating mechanism for generating output pages

View File

@@ -20,7 +20,6 @@ directly.
jamesh James Henstridge <???>
maxb Max Bowsher <maxb1@ukf.net>
eh Erik Hülsmann <e.huelsmann@gmx.net>
mhagger Michael Haggerty <mhagger@alum.mit.edu>
## Local Variables:
## coding:utf-8

560
INSTALL
View File

@@ -1,12 +1,11 @@
CONTENTS
--------
TO THE IMPATIENT
SECURITY INFORMATION
INSTALLING VIEWVC
APACHE CONFIGURATION
UPGRADING VIEWVC
SQL CHECKIN DATABASE
ENABLING SYNTAX COLORATION
ENSCRIPT AND HIGHLIGHT CONFIGURATION
CVSGRAPH CONFIGURATION
IF YOU HAVE PROBLEMS...
@@ -17,13 +16,10 @@ Congratulations on getting this far. :-)
Required Software And Configuration Needed To Run ViewVC:
In General:
* Python 2, version 2.4 or later (sorry, no 3.x support yet)
(http://www.python.org/)
For CVS Support:
* Python 1.5.2 or later
(http://www.python.org/)
* RCS, Revision Control System
(http://www.cs.purdue.edu/homes/trinkle/RCS/)
* GNU-diff to replace diff implementations without the -u option
@@ -33,9 +29,11 @@ Congratulations on getting this far. :-)
For Subversion Support:
* Subversion, Version Control System, 1.3.1 or later
* Python 2.0 or later
(http://www.python.org/)
* Subversion, Version Control System, 1.2.0 or later
(binary installation and Python bindings)
(http://subversion.apache.org/)
(http://subversion.tigris.org/)
Optional:
@@ -44,59 +42,34 @@ Congratulations on getting this far. :-)
* MySQL 3.22 and MySQLdb 0.9.0 or later to create a commit database
(http://www.mysql.com/)
(http://sourceforge.net/projects/mysql-python)
* Pygments 0.9 or later, syntax highlighting engine
(http://pygments.org)
* Enscript, code colorizer
(http://www.codento.com/people/mtr/genscript/)
* Highlight, code colorizer, 2.2.10 or later required, 2.4.5 or
later recommended for reliable line numbering
(http://www.andre-simon.de/)
* CvsGraph 1.5.0 or later, graphical CVS revision tree generator
(http://www.akhphd.au.dk/~bertho/cvsgraph/)
Quick sanity check:
GUI Operation:
If you just want to see what your repository looks like when seen
through ViewVC, type:
If you just want to see what your CVS repository looks like with
ViewVC, type "bin/standalone.py -g -r /PATH/TO/CVS/ROOT". This
will start a tiny webserver serving at http://localhost:7467/.
PLEASE NOTE: This requires Python with thread support enabled and
the Tkinter GUI. If you don't have one of these, omit the '-g' option.
$ bin/standalone.py -r /PATH/TO/REPOSITORY
This will start a tiny ViewVC server at http://localhost:49152/viewvc/,
to which you can connect with your browser.
Standard operation:
To start installing right away (on UNIX): type "./viewvc-install"
in the current directory and answer the prompts. When it
finishes, edit the file viewvc.conf in the installation directory
to tell ViewVC the paths to your CVS and Subversion repositories.
to tell viewvc the paths to your CVS and Subversion repositories.
Next, configure your web server (in the way appropriate to that browser)
to run <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/viewvc.cgi. The section
`INSTALLING VIEWVC' below is still recommended reading.
SECURITY INFORMATION
--------------------
ViewVC provides a feature which allows version controlled content to
be served to web browsers just like static web server content. So, if
you have a directory full of interrelated HTML files that is housed in
your version control repository, ViewVC can serve those files as HTML.
You'll see in your web browser what you'd see if the files were part
of your website, with working references to stylesheets and images and
links to other pages.
It is important to realize, however, that as useful as that feature
is, there is some risk security-wise in its use. Essentially, anyone
with commit access to the CVS or Subversion repositories served by
ViewVC has the ability to affect site content. If a discontented or
ignorant user commits malicious HTML to a version controlled file
(perhaps just by way of documenting examples of such), that malicious
HTML is effectively published and live on your ViewVC instance.
Visitors viewing those versioned controlled documents get the
malicious code, too, which might not be what the original author
intended.
For this reason, ViewVC's "checkout" view is disabled by default. If
you wish to enable it, simply add "co" to the list of views enabled in
the allowed_views configuration option.
INSTALLING VIEWVC
------------------
@@ -134,13 +107,10 @@ installation instructions.
3) Edit <VIEWVC_INSTALLATION_DIRECTORY>/viewvc.conf for your specific
configuration. In particular, examine the following configuration options:
cvs_roots (for CVS)
svn_roots (for Subversion)
root_parents (for CVS or Subversion)
cvs_roots
default_root
root_as_url_component
rcs_dir
mime_types_files
rcs_path
mime_types_file
There are some other options that are usually nice to change. See
viewvc.conf for more information. ViewVC provides a working,
@@ -155,238 +125,109 @@ installation instructions.
scripts after the install (unlike the other files in ViewVC, the scripts
under bin/ can be moved).
If you are using Apache, then see below at the section titled
APACHE CONFIGURATION.
If you are using Apache, then see below at the section
titled APACHE CONFIGURATION.
NOTE: for security reasons, it is not advisable to install ViewVC
directly into your published HTTP directory tree (due to the MySQL
passwords in viewvc.conf).
That's it for repository browsing. Instructions for getting the SQL
checkin database working are below.
5) That's it for repository browsing. Instructions for getting the
SQL checkin database working are below.
APACHE CONFIGURATION
--------------------
1) Locate your Apache configuration file(s).
1) Find out where the web server configuration file is kept. Typical
locations are /etc/httpd/httpd.conf, /etc/httpd/conf/httpd.conf,
and /etc/apache/httpd.conf. Depending on how apache was installed,
you may also look under /usr/local/etc or /etc/local. Use the vendor
documentation or the find utility if in doubt.
Typical locations are /etc/httpd/httpd.conf,
/etc/httpd/conf/httpd.conf, and /etc/apache/httpd.conf. Depending
on how Apache was installed, you may also look under /usr/local/etc
or /etc/local. Use the vendor documentation or the find utility if
in doubt.
2) Depending on how your Apache configuration is setup by default, you
might need to explicitly allow high-level access to the ViewVC
install location.
<Directory <VIEWVC_INSTALLATION_DIRECTORY>>
Order allow,deny
Allow from all
</Directory>
For example, if ViewVC is installed in /usr/local/viewvc-1.0 on
your system:
<Directory /usr/local/viewvc-1.0>
Order allow,deny
Allow from all
</Directory>
3) Configure Apache to expose ViewVC to users at the URL of your choice.
ViewVC provides several different ways to do this. Choose one of
the following methods:
-----------------------------------
METHOD A: CGI mode via ScriptAlias
-----------------------------------
The ScriptAlias directive is very useful for pointing
Either METHOD A:
2) The ScriptAlias directive is very useful for pointing
directly to the viewvc.cgi script. Simply insert a line containing
ScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/viewvc.cgi
ScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/viewvc.cgi
into your httpd.conf file. Choose the location in httpd.conf where
also the other ScriptAlias lines reside. Some examples:
ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi
ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi
ScriptAlias /viewvc /usr/local/viewvc-1.0/bin/cgi/viewvc.cgi
ScriptAlias /query /usr/local/viewvc-1.0/bin/cgi/query.cgi
----------------------------------------
METHOD B: CGI mode in cgi-bin directory
----------------------------------------
Copy the CGI scripts from
continue with step 3).
or alternatively METHOD B:
2) Copy the CGI scripts from
<VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi
to the /cgi-bin/ directory configured in your httpd.conf file.
You can override configuration file location using:
SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf
continue with step 3).
------------------------------------------
METHOD C: CGI mode in ExecCGI'd directory
------------------------------------------
Copy the CGI scripts from
and then there's METHOD C:
2) Copy the CGI scripts from
<VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/*.cgi
to the directory of your choosing in the Document Root adding the following
Apache directives for the directory in httpd.conf or an .htaccess file:
apache directives for the directory in httpd.conf or an .htaccess file:
Options +ExecCGI
AddHandler cgi-script .cgi
Options +ExecCGI
AddHandler cgi-script .cgi
Note: For this to work mod_cgi has to be loaded. And for the .htaccess file
(Note: For this to work mod_cgi has to be loaded. And for the .htaccess file
to be effective, "AllowOverride All" or "AllowOverride Options FileInfo"
needs to have been specified for the directory.
need to have been specified for the directory.)
------------------------------------------
METHOD D: Using mod_python (if installed)
------------------------------------------
Copy the Python scripts and .htaccess file from
continue with step 3).
or if you've got Mod_Python installed you can use METHOD D:
2) Copy the Python scripts and .htaccess file from
<VIEWVC_INSTALLATION_DIRECTORY>/bin/mod_python/
to a directory being served by Apache.
to a directory being served by apache.
In httpd.conf, make sure that "AllowOverride All" or at least
"AllowOverride FileInfo Options" are enabled for the directory
you copied the files to.
You can override configuration file location using:
SetEnv VIEWVC_CONF_PATHNAME /etc/viewvc.conf
Note: If you are using Mod_Python under Apache 1.3 the tarball generation
feature may not work because it uses multithreading. This works fine
under Apache 2.
and enscript colorizing features may not work because they use
multithreading. They do work fine with Apache 2.
----------------------------------------
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:
continue with step 3).
WSGIScriptAlias /viewvc <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/viewvc.wsgi
WSGIScriptAlias /query <VIEWVC_INSTALLATION_DIRECTORY>/bin/wsgi/query.wsgi
3) Restart apache. The commands to do this vary. "httpd -k restart" and
"apache -k restart" are two common variants. On RedHat Linux it is
done using the command "/sbin/service httpd restart" and on SuSE Linux
it is done with "rcapache restart"
You'll probably also need the following directive because of the
not-quite-sanctioned way that ViewVC manipulates Python objects.
4) Optional: Add access control.
WSGIApplicationGroup %{GLOBAL}
In your httpd.conf you can control access to certain modules by adding
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>">
AllowOverride None
AuthUserFile /path/to/passwd/file
AuthName "Client Access"
AuthType Basic
require valid-user
</Location>
<Location "<url to viewvc.cgi>/<modname_you_wish_to_access_ctl>">
AllowOverride None
AuthUserFile /path/to/passwd/file
AuthName "Client Access"
AuthType Basic
require valid-user
</Location>
WARNING: If you enable the "checkout_magic" or "allow_tar" options, you
will need to add additional location directives to prevent people
from sneaking in with URLs like:
http://<server_name>/viewvc/*checkout*/<module_name>
http://<server_name>/viewvc/~checkout~/<module_name>
http://<server_name>/viewvc/<module_name>.tar.gz?view=tar
6) Restart Apache.
The commands to do this vary. "httpd -k restart" and "apache -k
restart" are two common variants. On RedHat Linux it is done using
the command "/sbin/service httpd restart" and on SuSE Linux it is
done with "rcapache restart". Other systems use "apachectl restart".
7) [Optional] Protect your ViewVC instance from server-whacking webcrawlers.
As ViewVC is a web-based application which each page containing various
links to other pages and views, you can expect your server's performance
to suffer if a webcrawler finds your ViewVC instance and begins
traversing those links. We highly recommend that you add your ViewVC
location to a site-wide robots.txt file. Visit the Wikipedia page
for Robots.txt (http://en.wikipedia.org/wiki/Robots.txt) for more
information.
http://<server_name>/viewvc/*checkout*/<module_name>
http://<server_name>/viewvc/~checkout~/<module_name>
http://<server_name>/viewvc/<module_name>.tar.gz?view=tar
UPGRADING VIEWVC
-----------------
Please read the file upgrading-howto.html in the docs/ subdirectory.
Please read the file upgrading.html in the viewvc.org/ subdirectory or
at <http://viewvc.org/upgrading.html>.
SQL CHECKIN DATABASE
@@ -417,106 +258,91 @@ there are some additional steps required to get the database working.
enabled = 1 # Whether to enable query support in viewvc.cgi
host = # MySQL database server host
port = # MySQL database server port (default is 3306)
database_name = # name of database you created with make-database
user = # read/write database user
database_name = # the name of the database you created with
# make-database
user = # the read/write database user
passwd = # password for read/write database user
readonly_user = # read-only database user
readonly_passwd = # password for the read-only user
readonly_user = # the readonly database user -- it's pretty
# safe to use the read/write user here
readonly_passwd = # password for the readonly user
Note that it's pretty safe in this instance for your read-only user
and your read-write user to be the same.
5) Two programs are provided for updating the checkin database from a
CVS repository, cvsdbadmin and loginfo-handler. They serve two
different purposes. The cvsdbadmin program walks through your CVS
repository and adds every commit in every file. This is commonly
used for initializing the database from a repository which has been
in use. The loginfo-handler script is executed by the CVS server's
CVSROOT/loginfo system upon each commit. It makes real-time
updates to the checkin database as commits are made to the
repository.
5) At this point, you need to tell your version control system(s) to
publish their commit information to the database. This is done
using utilities that ViewVC provides.
To build a database of all the commits in the CVS repository /home/cvs,
invoke: "./cvsdbadmin rebuild /home/cvs". If you want to update
the checkin database, invoke: "./cvsdbadmin update /home/cvs". The
update mode checks to see if a commit is already in the database,
and only adds it if it is absent.
To publish CVS commits into the database:
To get real-time updates, you'll want to checkout the CVSROOT module
from your CVS repository and edit CVSROOT/loginfo. Add the line:
Two programs are provided for updating the checkin database from
a CVS repository, cvsdbadmin and loginfo-handler. They serve
two different purposes. The cvsdbadmin program walks through
your CVS repository and adds every commit in every file. This
is commonly used for initializing the database from a repository
which has been in use. The loginfo-handler script is executed
by the CVS server's CVSROOT/loginfo system upon each commit. It
makes real-time updates to the checkin database as commits are
made to the repository.
ALL <VIEWVC_INSTALLATION_DIRECTORY>/bin/loginfo-handler %{sVv}
To build a database of all the commits in the CVS repository
/home/cvs, invoke: "./cvsdbadmin rebuild /home/cvs". If you
want to update the checkin database, invoke: "./cvsdbadmin
update /home/cvs". The update mode checks to see if a commit is
already in the database, and only adds it if it is absent.
If you have other scripts invoked by CVSROOT/loginfo, you will want
to make sure to change any running under the "DEFAULT" keyword to
"ALL" like the loginfo handler, and probably carefully read the
execution rules for CVSROOT/loginfo from the CVS manual.
To get real-time updates, you'll want to checkout the CVSROOT
module from your CVS repository and edit CVSROOT/loginfo. For
folks running CVS 1.12 or better, add this line:
ALL <VIEWVC_INSTALLATION_DIRECTORY>/bin/loginfo-handler %p %{sVv}
If you are running CVS 1.11 or earlier, you'll want a slightly
different command line in CVSROOT/loginfo:
ALL <VIEWVC_INSTALLATION_DIRECTORY>/bin/loginfo-handler %{sVv}
If you are running the Unix port of CVS-NT, you'll need to use a
slightly different command line:
If you have other scripts invoked by CVSROOT/loginfo, you will
want to make sure to change any running under the "DEFAULT"
keyword to "ALL" like the loginfo handler, and probably
carefully read the execution rules for CVSROOT/loginfo from the
CVS manual.
If you are running the Unix port of CVS-NT, the handler script
need to know about it. CVS-NT delivers commit information to
loginfo scripts differently than the way mainstream CVS does.
Your command line should look like this:
ALL <VIEWVC_INSTALLATION_DIRECTORY>/bin/loginfo-handler %{sVv} cvsnt
ALL <VIEWVC_INSTALLATION_DIRECTORY>/bin/loginfo-handler %{sVv} cvsnt
The extra 'cvsnt' parameter tells the handler script to parse the
commit information in a different way.
To publish Subversion commits into the database:
To build a database of all the commits in the Subversion
repository /home/svn, invoke: "./svndbadmin rebuild /home/svn".
If you want to update the checkin database, invoke:
"./svndbadmin update /home/svn".
To get real time updates, you will need to add a post-commit
hook (for the repository example above, the script should go in
/home/svn/hooks/post-commit). The script should look something
like this:
#!/bin/sh
REPOS="$1"
REV="$2"
<VIEWVC_INSTALLATION_DIRECTORY>/bin/svndbadmin update \
"$REPOS" "$REV"
If you allow revision property changes in your repository,
create a post-revprop-change hook script which uses the same
'svndbadmin update' command as the post-commit script, except
with the addition of the --force option:
#!/bin/sh
REPOS="$1"
REV="$2"
<VIEWVC_INSTALLATION_DIRECTORY>/bin/svndbadmin update --force \
"$REPOS" "$REV"
This will make sure that the checkin database stays consistent
when you change the svn:log, svn:author or svn:date revision
properties.
For Subversion repositories, there is a single script called
svndbadmin that performs both of the above tasks.
You should be ready to go. Click one of the "Query revision history"
links in ViewVC directory listings and give it a try.
To build a database of all the commits in the Subversion repository
/home/svn, invoke: "./svndbadmin rebuild /home/svn". If you want
to update the checkin database, invoke: "./svndbadmin update
/home/svn".
To get real time updates, you will need to add a post-commit hook
(for the repository example above, the script should go in
/home/svn/hooks/post-commit). The script should look something
like this:
#!/bin/sh
REPOS="$1"
REV="$2"
<VIEWVC_INSTALLATION_DIRECTORY>/bin/svndbadmin rebuild "$REPOS" "$REV"
If you allow revision property changes in your repository, create a
post-revprop-change hook script containing the same commands as the
post-commit one. This will make sure that the checkin database
stays consistent when you change the svn:log, svn:author or
svn:date revision properties.
6) You should be ready to go. Click one of the "Query revision history"
links in ViewVC directory listings and give it a try.
ENABLING SYNTAX COLORATION
--------------------------
ENSCRIPT AND HIGHLIGHT CONFIGURATION
------------------------------------
ViewVC uses Pygments (http://pygments.org) for syntax coloration. You
need only install a suitable version of that module, and if ViewVC
finds it in your Python module path, it will use it (unless you
specifically disable the feature by setting use_pygments = 0 in your
viewvc.conf file).
Enscript and Highlight are programs that can colorize source code for
a lot of languages. ViewVC can be configured to use either one.
1) Install Enscript or Highlight using your system's package manager
or downloading from the project home pages.
2) Set the 'use_enscript' or 'use_highlight' options in viewvc.conf to 1.
3) You may also need to set 'enscript_path' and 'highlight_path' options
if the executables are not located on the system PATH.
4) That's it!
CVSGRAPH CONFIGURATION
@@ -545,52 +371,35 @@ a reasonable number of revisions and branches.
4) There is a file <VIEWVC_INSTALLATION_DIRECTORY>/cvsgraph.conf that
you may want to edit if desired to set color and font characteristics.
See the cvsgraph.conf documentation. No edits are required in
cvsgraph.conf for operation with ViewVC.
cvsgraph.conf for operation with viewvc.
SUBVERSION INTEGRATION
----------------------
Unlike the CVS integration, which simply wraps the RCS and CVS utility
programs, the Subversion integration requires additional Python
libraries. To use ViewVC with Subversion, make sure you have both
Subversion itself and the Subversion Python bindings installed. These
can be obtained through typical package distribution mechanisms, or
may be build from source. (See the files 'INSTALL' and
'subversion/bindings/swig/INSTALL' in the Subversion source tree for
more details on how to build and install Subversion and its Python
bindings.)
ViewVC supports browsing of Subversion repositories. To use ViewVC
with Subversion, make sure you have both Subversion itself and
the Subversion Python bindings installed. See Subversion's
installation notes for more details on how to build and install these
items.
Generally speaking, you'll know when your installation of Subversion's
bindings has been successful if you can import the 'svn.core' module
from within your Python interpreter. Here's an example of doing so
which doubles as a quick way to check what version of the Subversion
Python binding you have:
bindings has been successful if you can import the 'svn.repos' module
from within your Python interpreter:
% python
Python 2.2.2 (#1, Oct 29 2002, 02:47:30)
[GCC 2.96 20000731 (Red Hat Linux 7.2 2.96-108.7.2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from svn.core import *
>>> "%s.%s.%s" % (SVN_VER_MAJOR, SVN_VER_MINOR, SVN_VER_PATCH)
'1.3.1'
>>>
>>> import svn.repos
>>>
Note that by default, Subversion installs its bindings in a location
that is not in Python's default module search path (for example, on
Linux systems the default is usually /usr/local/lib/svn-python). You
need to remedy this, either by adding this path to Python's module
search path, or by relocating the bindings to some place in that
search path.
For example, you might want to create .pth file in your Python
installation directory's site-packages area which tells Python where
to find additional modules (in this case, you Subversion Python
bindings). You would do this as follows (and as root):
$ echo "/path/to/svn/bindings" > /path/to/python/site-packages/svn.pth
(Though, obviously, with the correct paths specified.)
search path, or by relocation the bindings to some place in that
search path.
Configuration of the Subversion repositories happens in much the same
way as with CVS repositories, only with the 'svn_roots' configuration
@@ -600,42 +409,39 @@ variable instead of the 'cvs_roots' one.
IF YOU HAVE PROBLEMS ...
------------------------
If nothing seems to work:
If you've trouble to make viewvc.cgi work:
* Check if you can execute CGI-scripts (Apache needs to have an
ScriptAlias /cgi-bin or cgi-script Handler defined). Try to
execute a simple CGI-script that often comes with the distribution
of the webserver; locate the logfiles and try to find hints which
explain the malfunction
=== If nothing seems to work:
* View the entries in the webserver's error.log
o check if you can execute CGI-scripts (Apache needs to have an
ScriptAlias /cgi-bin or cgi-script Handler defined). Try to
execute a simple CGI-script that often comes with the distribution
of the webserver; locate the logfiles and try to find hints
which explain the malfunction
o view the entries in the webserver's error.log
If ViewVC seems to work but doesn't show the expected result (Typical
error: you can't see any files)
=== If viewvc seems to work but doesn't show the expected result
(Typical error: you can't see any files)
* Check whether the CGI-script has read-permissions to your
CVS-Repository. The CGI-script generally runs as the same user
that the web server does, often user 'nobody' or 'httpd'.
o check whether the CGI-script has read-permissions to your
CVS-Repository. The CGI-script often runs as the user 'nobody'
or 'httpd' ..
* Does ViewVC find your RCS utilities? (edit rcs_dir)
o does viewvc find your RCS utilities? (edit rcs_path)
If something else happens or you can't get it to work:
=== If something else happens or you can't get it to work:
* Check the ViewVC home page:
o check the ViewVC home page:
http://viewvc.org/
http://viewvc.org/
* Review the ViewVC mailing list archive to see if somebody else had
the same problem, and it was solved:
o review the ViewVC mailing list archive to see if somebody else had
the same problem, and it was solved:
http://viewvc.tigris.org/servlets/SummarizeList?listName=users
http://viewvc.tigris.org/servlets/SummarizeList?listName=users
* Check the ViewVC issue database to see if the problem you are
seeing is the result of a known bug:
o send mail to the ViewVC mailing list: users@viewvc.tigris.org
http://viewvc.tigris.org/issues/query.cgi
* Send mail to the ViewVC mailing list, users@viewvc.tigris.org.
NOTE: make sure you provide an accurate description of the problem
-- including the version of ViewVC you are using -- and any
relevant tracebacks or error logs.
NOTE: make sure you provide an accurate description of the problem
and any relevant tracebacks or error logs.

3
README
View File

@@ -1,9 +1,6 @@
ViewVC -- Viewing the content of CVS/SVN repositories with a Webbrowser.
This is the 4Intra.net patched version with some extra features.
Please read the file INSTALL for more information.
And see windows/README for more information on running ViewVC on
Microsoft Windows.

View File

View File

@@ -3,7 +3,7 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 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
@@ -24,15 +24,15 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
#########################################################################
#
@@ -54,10 +54,7 @@ import query
server = sapi.AspServer(Server, Request, Response, Application)
try:
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.asp"
query.main(server, cfg, viewvc_base_url)
query.main(server, cfg, "viewvc.asp")
finally:
s.close()

View File

@@ -3,7 +3,7 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 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
@@ -24,15 +24,15 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
#########################################################################
#

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -22,19 +22,19 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
#########################################################################
#
# Adjust sys.path to include our library directory.
# Adjust sys.path to include our library directory
#
import sys
@@ -47,20 +47,6 @@ else:
"../../../lib")))
#########################################################################
#
# If admins want nicer processes, here's the place to get them.
#
#try:
# os.nice(20) # bump the nice level of this process
#except:
# pass
#########################################################################
#
# Go do the work.
#
import sapi
import viewvc
@@ -68,7 +54,4 @@ import query
server = sapi.CgiServer()
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.cgi"
query.main(server, cfg, viewvc_base_url)
query.main(server, cfg, "viewvc.cgi")

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -22,19 +22,19 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
#########################################################################
#
# Adjust sys.path to include our library directory.
# Adjust sys.path to include our library directory
#
import sys
@@ -47,21 +47,12 @@ else:
"../../../lib")))
#########################################################################
#
# If admins want nicer processes, here's the place to get them.
#
#try:
# os.nice(20) # bump the nice level of this process
#except:
# pass
### add code for checking the load average
#########################################################################
#
# Go do the work.
#
# go do the work
import sapi
import viewvc

View File

@@ -1,216 +0,0 @@
#!/bin/sh
# ViewVC installation script for CustIS
if [ -f custis-install-config ]; then
. custis-install-config
else
cat >custis-install-config <<EOF
CVS_ASYNC=
SVN_ASYNC=
CVS_USER=www-data
CVS_GROUP=cvs.users
CVSROOTS=
SVNROOT=
SVN_FORBIDDENRE=
DB_HOST=
DB_PORT=
DB_SOCKET=/var/run/mysqld/mysqld.sock
DB=
DB_USER=
DB_PASSWD=
VIEWVC_DIR=
VIEWVC_URI=/
VIEWVC_URI_SLASH=/
VIEWVC_STATIC_URI=/static
http_proxy=
no_proxy=
EOF
echo Empty 'custis-install-config' initialized, please edit it before installation.
exit
fi
if [ ! "$DB" -o ! "$VIEWVC_DIR" ]; then
echo Please set up 'custis-install-config' before installation.
exit
fi
################################################################################
export http_proxy
export no_proxy
DEPS="python libapache2-mod-python rcs diff cvsnt subversion subversion-tools python-setuptools python-subversion python-mysqldb cvsgraph"
echo "*** Installing dependency packages: $DEPS"
apt-get install $DEPS
echo "*** Installing Pygments for Python"
wget http://pypi.python.org/packages/source/P/Pygments/Pygments-0.11.1.tar.gz
tar -zxf Pygments-0.11.1.tar.gz
cd Pygments-0.11.1
python setup.py install
cd ..
echo "*** Installing ViewVC into $VIEWVC_DIR"
if [ ! -e $VIEWVC_DIR/viewvc.conf ]; then
../viewvc-install <<EOF
$VIEWVC_DIR
EOF
fi
echo "*** Writing ViewVC configuration into $VIEWVC_DIR/viewvc.conf"
for CVSROOT in $CVSROOTS; do
if [ "$vccvsroots" ]; then
vccvsroots="$vccvsroots, "
fi
vcrootname=`basename $CVSROOT`
vccvsroots="$vccvsroots$vcrootname: $CVSROOT"
done;
cat >"$VIEWVC_DIR/viewvc.conf" <<EOF
[general]
cvs_roots = $vccvsroots
root_parents = $SVNROOT : svn
cvsnt_exe_path = /usr/bin/cvsnt
mime_types_file = /etc/mime.types
address = Admin address: stas [gav-gav] custis [ru]
kv_files =
languages = en-us, ru-ru
[utilities]
rcs_dir = /usr/bin
cvsnt = /usr/bin/cvsnt
svn = /usr/bin/svn
diff = /usr/bin/diff
cvsgraph = /usr/bin/cvsgraph
[options]
allowed_views = markup, annotate, roots
authorizer = forbiddenre
checkout_magic = 0
cross_copies = 1
cvsgraph_conf = $VIEWVC_DIR/cvsgraph.conf
default_file_view = log
diff_format = h
docroot = $VIEWVC_STATIC_URI
enable_syntax_coloration = 1
generate_etags = 1
hide_attic = 1
hide_cvsroot = 1
hide_errorful_entries = 0
hr_breakable = 1
hr_funout = 0
hr_ignore_keyword_subst = 1
hr_ignore_white = 1
hr_intraline = 0
http_expiration_time = 600
limit_changes = 100
log_sort = date
mangle_email_addresses = 1
root_as_url_component = 1
short_log_len = 80
show_log_in_markup = 1
show_logs = 1
show_subdir_lastmod = 0
sort_by = file
sort_group_dirs = 1
svn_config_dir =
template_dir = templates
use_cvsgraph = 1
use_localtime = 1
use_pagesize = 0
use_rcsparse = 0
use_re_search = 0
[templates]
[cvsdb]
enabled = 1
host = $DB_HOST
socket = $DB_SOCKET
database_name = $DB
user = $DB_USER
passwd = $DB_PASSWD
readonly_user = $DB_USER
readonly_passwd = $DB_PASSWD
[vhosts]
[authz-forbidden]
forbidden =
[authz-forbiddenre]
forbiddenre = $SVN_FORBIDDENRE
[authz-svnauthz]
authzfile =
EOF
echo "*** Initializing database: $DB using $DB_USER@$DB_HOST"
$VIEWVC_DIR/bin/make-database <<EOF
$DB_HOST
$DB_USER
$DB_PASSWD
$DB
EOF
echo "*** Configuring Apache"
cat >/etc/apache2/sites-available/viewvc <<EOF
<VirtualHost *:80>
ServerName viewvc.office.custis.ru
ServerAlias viewvc
ServerAdmin sysadmins@custis.ru
ServerSignature Off
ErrorLog /var/log/apache2/viewvc-error.log
TransferLog /var/log/apache2/viewvc-access.log
LogLevel warn
# ViewVC at $VIEWVC_URI installed at $VIEWVC_DIR
DocumentRoot $VIEWVC_DIR/bin/mod_python
Alias $VIEWVC_STATIC_URI $VIEWVC_DIR/templates/docroot
Alias $VIEWVC_URI $VIEWVC_DIR/bin/mod_python/
<Location ~ ^${VIEWVC_URI_SLASH}*(\?.*)?$>
Options -Indexes
RewriteEngine On
RewriteRule .* ${VIEWVC_URI_SLASH}viewvc.py%{REQUEST_URI} [R,L]
</Location>
<Directory $VIEWVC_DIR/bin/mod_python>
Options +ExecCGI
AddHandler python-program .py
PythonHandler handler
PythonDebug Off
</Directory>
</VirtualHost>
EOF
a2enmod python
a2enmod mod_python
a2enmod rewrite
a2ensite viewvc
echo "*** Restarting Apache"
apache2ctl stop
sleep 1
apache2ctl start
echo "*** Building commit database for CVS"
for CVSROOT in $CVSROOTS; do
$VIEWVC_DIR/bin/cvsdbadmin rebuild $CVSROOT
done;
echo "*** Building commit database for Subversion repositories"
for i in `ls $SVNROOT`; do
if [ -d "$SVNROOT/$i" ]; then
$VIEWVC_DIR/bin/svndbadmin -v rebuild "$SVNROOT/$i"
fi
done;
# setup hooks for CVS
./setup-cvs-hooks "$CVSROOTS" "$VIEWVC_DIR" "$CVS_USER" "$CVS_GROUP" "$CVS_ASYNC"
# setup hooks for Subversion
./setup-svn-hooks "$SVNROOT" "$VIEWVC_DIR" "$SVN_ASYNC"

View File

@@ -1,7 +1,7 @@
#!/usr/bin/python
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -18,15 +18,15 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
# Adjust sys.path to include our library directory
import sys
@@ -40,52 +40,37 @@ else:
#########################################################################
import os
import string
import cvsdb
import viewvc
import vclib.ccvs
from stat import *
import vclib.bincvs
def UpdateFile(db, repository, path, update, latest_checkin, quiet_level, encoding = None):
def UpdateFile(db, repository, path, update):
try:
if update:
mtime = os.stat(repository.rcsfile(path, 1))[ST_MTIME]
if mtime < latest_checkin:
return
commit_list = cvsdb.GetUnrecordedCommitList(repository, path, db)
else:
commit_list = cvsdb.GetCommitListFromRCSFile(repository, path)
except cvsdb.error, e:
print '[ERROR] %s' % (e)
return
except vclib.ItemNotFound, e:
return
file = '/'.join(path)
printing = 0
file = string.join(path, "/")
if update:
if quiet_level < 1 or (quiet_level < 2 and len(commit_list)):
printing = 1
print '[%s [%d new commits]]' % (file, len(commit_list)),
print '[%s [%d new commits]]' % (file, len(commit_list)),
else:
if quiet_level < 2:
printing = 1
print '[%s [%d commits]]' % (file, len(commit_list)),
print '[%s [%d commits]]' % (file, len(commit_list)),
## add the commits into the database
for commit in commit_list:
if encoding:
commit.SetFile(commit.GetFile().decode(encoding).encode('utf-8'))
db.AddCommit(commit)
if printing:
sys.stdout.write('.')
sys.stdout.write('.')
sys.stdout.flush()
if printing:
print
print
def RecurseUpdate(db, repository, directory, update, latest_checkin,
quiet_level, encoding = None):
def RecurseUpdate(db, repository, directory, update):
for entry in repository.listdir(directory, None, {}):
path = directory + [entry.name]
@@ -93,16 +78,13 @@ def RecurseUpdate(db, repository, directory, update, latest_checkin,
continue
if entry.kind is vclib.DIR:
RecurseUpdate(db, repository, path, update, latest_checkin,
quiet_level, encoding)
RecurseUpdate(db, repository, path, update)
continue
if entry.kind is vclib.FILE:
UpdateFile(db, repository, path, update, latest_checkin,
quiet_level, encoding)
UpdateFile(db, repository, path, update)
def RootPath(path, quiet_level):
def RootPath(path):
"""Break os path into cvs root path and other parts"""
root = os.path.abspath(path)
path_parts = []
@@ -111,16 +93,14 @@ def RootPath(path, quiet_level):
while 1:
if os.path.exists(os.path.join(p, 'CVSROOT')):
root = p
if quiet_level < 2:
print "Using repository root `%s'" % root
print "Using repository root `%s'" % root
break
p, pdir = os.path.split(p)
if not pdir:
del path_parts[:]
if quiet_level < 1:
print "Using repository root `%s'" % root
print "Warning: CVSROOT directory not found."
print "Using repository root `%s'" % root
print "Warning: CVSROOT directory not found."
break
path_parts.append(pdir)
@@ -130,82 +110,48 @@ def RootPath(path, quiet_level):
return root, path_parts
def usage():
cmd = os.path.basename(sys.argv[0])
sys.stderr.write(
"""Administer the ViewVC checkins database data for the CVS repository
located at REPOS-PATH.
Usage: 1. %s [[-q] -q] rebuild REPOS-PATH
2. %s [[-q] -q] update REPOS-PATH
3. %s [[-q] -q] purge REPOS-PATH
1. Rebuild the commit database information for the repository located
at REPOS-PATH, after first purging information specific to that
repository (if any).
2. Update the commit database information for all unrecorded commits
in the repository located at REPOS-PATH.
3. Purge information specific to the repository located at REPOS-PATH
from the database.
Use the -q flag to cause this script to be less verbose; use it twice to
invoke a peaceful state of noiselessness.
""" % (cmd, cmd, cmd))
print 'Usage: %s <command> [arguments]' % (sys.argv[0])
print 'Performs administrative functions for the CVSdb database'
print 'Commands:'
print ' rebuild <repository> rebuilds the CVSdb database'
print ' for all files in the repository'
print ' update <repository> updates the CVSdb database for'
print ' all unrecorded commits'
print
sys.exit(1)
## main
if __name__ == '__main__':
args = sys.argv
# check the quietness level (0 = verbose, 1 = new commits, 2 = silent)
quiet_level = 0
while 1:
try:
index = args.index('-q')
quiet_level = quiet_level + 1
del args[index]
except ValueError:
break
# validate the command
if len(args) <= 2:
usage()
command = args[1].lower()
if command not in ('rebuild', 'update', 'purge'):
sys.stderr.write('ERROR: unknown command %s\n' % command)
## check that a command was given
if len(sys.argv) <= 2:
usage()
# get repository and path, and do the work
root, path_parts = RootPath(args[2], quiet_level)
rootpath = vclib.ccvs.canonicalize_rootpath(root)
## set the handler function for the command
command = sys.argv[1]
if string.lower(command) == 'rebuild':
update = 0
elif string.lower(command) == 'update':
update = 1
else:
print 'ERROR: unknown command %s' % (command)
usage()
# get repository path
root, path_parts = RootPath(sys.argv[2])
## run command
try:
cfg = viewvc.load_config(CONF_PATHNAME)
db = cvsdb.ConnectDatabase(cfg)
## connect to the database we are updating
cfg = viewvc.load_config(CONF_PATHNAME)
db = cvsdb.ConnectDatabase(cfg)
if command in ('rebuild', 'purge'):
if quiet_level < 2:
print "Purging existing data for repository root `%s'" % root
try:
db.PurgeRepository(root, len(path_parts) and '/'.join(path_parts) or '')
except cvsdb.UnknownRepositoryError, e:
if command == 'purge':
sys.stderr.write("ERROR: " + str(e) + "\n")
sys.exit(1)
repository = vclib.bincvs.BinCVSRepository(None, root, cfg.general)
RecurseUpdate(db, repository, path_parts, update)
if command in ('rebuild', 'update'):
repository = vclib.ccvs.CVSRepository(None, rootpath, None,
cfg.utilities, 0, cfg.guesser())
latest_checkin = db.GetLatestCheckinTime(repository)
if latest_checkin is None:
command = 'rebuild'
RecurseUpdate(db, repository, path_parts,
command == 'update', latest_checkin, quiet_level,
cfg.options.cvs_ondisk_charset)
except KeyboardInterrupt:
print
print '** break **'
sys.exit(0)

View File

@@ -1,57 +0,0 @@
#!/usr/bin/perl
use strict;
my ($type, $path, $branch, $user, $perm);
my $R = ''; # -R for recursive
my $lvl = {
r => 1,
t => 2,
w => 3,
c => 4,
a => 4,
p => 5,
};
while(<>)
{
chomp;
next if /^\s*#/so;
($type, $path, $branch, $user, $perm) = split /:/, $_;
($user, $perm) = split /!/, $user;
next unless $perm;
$perm = [ sort { $lvl->{$b} cmp $lvl->{$a} } split //, $perm ];
$perm = $perm->[0];
if ($perm eq 't')
{
$perm = 'read,tag,nowrite,nocreate,nocontrol';
}
elsif ($perm eq 'r')
{
$perm = 'read,notag,nowrite,nocreate,nocontrol';
}
elsif ($perm eq 'w')
{
$perm = 'read,tag,write,nocreate,nocontrol';
}
elsif ($perm eq 'c' || $perm eq 'a')
{
$perm = 'read,tag,write,create,nocontrol';
}
elsif ($perm eq 'p')
{
$perm = 'read,tag,write,create,control';
}
print "cvs rchacl$R -a $perm";
print " -u '$user'" if $user ne 'ALL';
print " -r '$branch'" if $branch ne 'ALL';
if ($path ne 'ALL')
{
print " '$path'";
}
else
{
print ' `ls $CVSROOT | grep -v '."'#cvs'`";
}
print "\n";
}

View File

@@ -1,21 +0,0 @@
#!/usr/bin/perl
# Very simple inetd/xinetd "cvsnt rcsfile" service
# Useful for ViewVC as there is an unpleasant non-stable bug, probably somewhere
# inside mod_python or Apache, which SOMETIMES causes cvsnt subprocesses forked
# from mod_python to die. This gives empty diff outputs or different errors like
# "Error: Rlog output ended early. Expected RCS file ..."
# This script removes forking from mod_python code and solves the issue.
# Additional profit of this script is that you can probably browse REMOTE cvs
# repositories, if you expose this service to ViewVC, although it is not tested.
# USAGE: (local) create an inetd service with this script as server listening
# on some port of 127.0.0.1 and put "rcsfile_socket = 127.0.0.1:port"
# into "utilities" section of viewvc.conf
$args = <STDIN>;
$args =~ s/\s+$//so;
@args = $args =~ /\'([^\']+)\'/giso;
# We don't execute shell, so this is mostly safe, not a backdoor :)
exec('/usr/bin/cvsnt', 'rcsfile', @args);

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -17,44 +17,41 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
# Adjust sys.path to include our library directory
import sys
import os
if LIBRARY_DIR:
sys.path.insert(0, LIBRARY_DIR)
sys.path.insert(0, LIBRARY_DIR)
else:
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
#########################################################################
import os
import string
import getopt
import re
import cvsdb
import viewvc
import vclib.ccvs
import vclib.bincvs
DEBUG_FLAG = 0
## output functions
def debug(text):
def debug(text):
if DEBUG_FLAG:
if type(text) != (type([])):
text = [text]
for line in text:
line = line.rstrip('\n\r')
print 'DEBUG(viewvc-loginfo):', line
print 'DEBUG(viewvc-loginfo):', text
def warning(text):
print 'WARNING(viewvc-loginfo):', text
@@ -64,169 +61,147 @@ def error(text):
sys.exit(1)
_re_revisions = re.compile(
r",(?P<old>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision
r",(?P<new>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision
r"(?:$| )" # space or end of string
r",(?P<old>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision number
r",(?P<new>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision number
r"(?:$| )" # space or end of string
)
def Cvs1Dot12ArgParse(args):
"""CVS 1.12 introduced a new loginfo format while provides the various
pieces of interesting version information to the handler script as
individual arguments instead of as a single string."""
if args[1] == '- New directory':
return None, None
elif args[1] == '- Imported sources':
return None, None
else:
directory = args.pop(0)
files = []
while len(args) >= 3:
files.append(args[0:3])
args = args[3:]
return directory, files
def HeuristicArgParse(s, repository):
"""Older versions of CVS (except for CVSNT) do not escape spaces in file
and directory names that are passed to the loginfo handler. Since the input
to loginfo is a space separated string, this can lead to ambiguities. This
function attempts to guess intelligently which spaces are separators and
which are part of file or directory names. It disambiguates spaces in
filenames from the separator spaces between files by assuming that every
space which is preceded by two well-formed revision numbers is in fact a
separator. It disambiguates the first separator space from spaces in the
directory name by choosing the longest possible directory name that
actually exists in the repository"""
"""Current versions of CVS (except for CVSNT) do not escape spaces in file
and directory names that are passed to the loginfo handler. Since the input
to loginfo is a space separated string, this can lead to ambiguities. This
function attempts to guess intelligently which spaces are separators and
which are part of file or directory names. It disambiguates spaces in
filenames from the separator spaces between files by assuming that every
space which is preceded by two well-formed revision numbers is in fact a
separator. It disambiguates the first separator space from spaces in the
directory name by choosing the longest possible directory name that actually
exists in the repository"""
if (s[-16:] == ' - New directory'
or s[:26] == ' - New directory,NONE,NONE'):
return None, None
if (s[-19:] == ' - Imported sources'
or s[-29:] == ' - Imported sources,NONE,NONE'):
return None, None
file_data_list = []
start = 0
while 1:
m = _re_revisions.search(s, start)
if start == 0:
if m is None:
error('Argument "%s" does not contain any revision numbers' \
% s)
directory, filename = FindLongestDirectory(s[:m.start()],
repository)
if directory is None:
error('Argument "%s" does not start with a valid directory' \
% s)
debug('Directory name is "%s"' % directory)
else:
if m is None:
warning('Failed to interpret past position %i in the loginfo '
'argument, leftover string is "%s"' \
% start, pos[start:])
filename = s[start:m.start()]
old_version, new_version = m.group('old', 'new')
file_data_list.append((filename, old_version, new_version))
debug('File "%s", old revision %s, new revision %s'
% (filename, old_version, new_version))
start = m.end()
if start == len(s): break
return directory, file_data_list
def FindLongestDirectory(s, repository):
"""Splits the first part of the argument string into a directory name
and a file name, either of which may contain spaces. Returns the longest
possible directory name that actually exists"""
parts = s.split()
for i in range(len(parts)-1, 0, -1):
directory = ' '.join(parts[:i])
filename = ' '.join(parts[i:])
if os.path.isdir(os.path.join(repository, directory)):
return directory, filename
if s[-16:] == ' - New directory':
return None, None
if s[-19:] == r' - Imported sources':
return None, None
file_data_list = []
start = 0
while 1:
m = _re_revisions.search(s, start)
if start == 0:
if m is None:
error('Argument "%s" does not contain any revision numbers' % s)
directory, filename = FindLongestDirectory(s[:m.start()], repository)
if directory is None:
error('Argument "%s" does not start with a valid directory' % s)
debug('Directory name is "%s"' % directory)
else:
if m is None:
warning('Failed to interpret past position %i in the loginfo argument, '
'leftover string is "%s"' % start, pos[start:])
filename = s[start:m.start()]
old_version, new_version = m.group('old', 'new')
file_data_list.append((filename, old_version, new_version))
debug('File "%s", old revision %s, new revision %s'
% (filename, old_version, new_version))
start = m.end()
if start == len(s): break
return directory, file_data_list
def FindLongestDirectory(s, repository):
"""Splits the first part of the argument string into a directory name
and a file name, either of which may contain spaces. Returns the longest
possible directory name that actually exists"""
parts = string.split(s, " ")
for i in range(len(parts)-1, 0, -1):
directory = string.join(parts[:i])
filename = string.join(parts[i:])
if os.path.isdir(os.path.join(repository, directory)):
return directory, filename
return None, None
_re_cvsnt_revisions = re.compile(
r"(?P<filename>.*)" # comma and first revision
r",(?P<old>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision
r",(?P<new>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision
r"$" # end of string
r"(?P<filename>.*)" # comma and first revision number
r",(?P<old>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision number
r",(?P<new>(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision number
r"$" # end of string
)
def CvsNtArgParse(s, repository):
"""CVSNT escapes all spaces in filenames and directory names with
backslashes"""
"""CVSNT escapes all spaces in filenames and directory names with
backslashes"""
if s[-18:] == r' -\ New\ directory':
return None, None
if s[-18:] == r' -\ New\ directory':
return None, None
if s[-21:] == r' -\ Imported\ sources':
return None, None
if s[-21:] == r' -\ Imported\ sources':
return None, None
file_data_list = []
directory, pos = NextFile(s)
debug('Directory name is "%s"' % directory)
file_data_list = []
while 1:
fileinfo, pos = NextFile(s, pos)
if fileinfo is None:
break
directory, pos = NextFile(s)
debug('Directory name is "%s"' % directory)
m = _re_cvsnt_revisions.match(fileinfo)
if m is None:
warning('Can\'t parse file information in "%s"' % fileinfo)
continue
while 1:
fileinfo, pos = NextFile(s, pos)
if fileinfo is None:
break
file_data = m.group('filename', 'old', 'new')
file_data_list.append(file_data)
m = _re_cvsnt_revisions.match(fileinfo)
if m is None:
warning('Can\'t parse file information in "%s"' % fileinfo)
continue
debug('File "%s", old revision %s, new revision %s' % file_data)
file_data = m.group('filename', 'old', 'new')
return directory, file_data_list
debug('File "%s", old revision %s, new revision %s' % file_data)
file_data_list.append(file_data)
return directory, file_data_list
def NextFile(s, pos = 0):
escaped = 0
ret = ''
i = pos
while i < len(s):
c = s[i]
if escaped:
ret += c
escaped = 0
elif c == '\\':
escaped = 1
elif c == ' ':
return ret, i + 1
else:
ret += c
i += 1
escaped = 0
ret = ''
i = pos
while i < len(s):
c = s[i]
if escaped:
ret += c
escaped = 0
elif c == '\\':
escaped = 1
elif c == ' ':
return ret, i + 1
else:
ret += c
i += 1
return ret or None, i
return ret or None, i
def ProcessLoginfo(rootpath, directory, files):
cfg = viewvc.load_config(CONF_PATHNAME)
db = cvsdb.ConnectDatabase(cfg)
repository = vclib.ccvs.CVSRepository(None, rootpath, None,
cfg.utilities, 0)
repository = vclib.bincvs.BinCVSRepository(None, rootpath, cfg.general)
# split up the directory components
dirpath = filter(None, os.path.normpath(directory).split(os.sep))
dirpath = filter(None, string.split(os.path.normpath(directory), os.sep))
## build a list of Commit objects
commit_list = []
@@ -260,58 +235,41 @@ if __name__ == '__main__':
debug('Repository name is "%s"' % repository)
## parse arguments
argc = len(sys.argv)
debug('Got %d arguments:' % (argc))
debug(map(lambda x: ' ' + x, sys.argv))
# if we have more than 3 arguments, we are likely using the
# newer loginfo format introduced in CVS 1.12:
#
# ALL <path>/bin/loginfo-handler %p %{sVv}
if argc > 3:
directory, files = Cvs1Dot12ArgParse(sys.argv[1:])
if len(sys.argv) > 1:
# the first argument should contain file version information
arg = sys.argv[1]
else:
if len(sys.argv) > 1:
# the first argument should contain file version information
arg = sys.argv[1]
# if there are no arguments, read version information from first line
# of input like old versions of viewcvs
arg = string.rstrip(sys.stdin.readline())
if len(sys.argv) > 2:
# if there is a second argument it indicates which parser should be
# used to interpret the version information
if sys.argv[2] == 'cvs':
fun = HeuristicArgParse
elif sys.argv[2] == 'cvsnt':
fun = CvsNtArgParse
else:
# if there are no arguments, read version information from
# first line of input like old versions of ViewCVS did
arg = sys.stdin.readline().rstrip()
if len(sys.argv) > 2:
# if there is a second argument it indicates which parser
# should be used to interpret the version information
if sys.argv[2] == 'cvs':
fun = HeuristicArgParse
elif sys.argv[2] == 'cvsnt':
fun = CvsNtArgParse
else:
error('Bad arguments')
error('Bad arguments')
else:
# if there is no second argument, guess which parser to use based
# on the operating system. Since CVSNT now runs on Windows and
# Linux, the guess isn't neccessarily correct
if sys.platform == "win32":
fun = CvsNtArgParse
else:
# if there is no second argument, guess which parser to use based
# on the operating system. Since CVSNT now runs on Windows and
# Linux, the guess isn't necessarily correct
if sys.platform == "win32":
fun = CvsNtArgParse
else:
fun = HeuristicArgParse
directory, files = fun(arg, repository)
fun = HeuristicArgParse
debug('Discarded from stdin:')
debug(map(lambda x: ' ' + x, sys.stdin.readlines())) # consume stdin
if len(sys.argv) > 3:
error('Bad arguments')
repository = cvsdb.CleanRepository(repository)
debug('Repository: %s' % (repository))
debug('Directory: %s' % (directory))
debug('Files: %s' % (str(files)))
directory, files = fun(arg, repository)
if files is None:
debug('Not a checkin, nothing to do')
debug('Not a checkin, nothing to do')
else:
ProcessLoginfo(repository, directory, files)
ProcessLoginfo(repository, directory, files)
sys.exit(0)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2009 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -18,33 +18,31 @@
import os, sys, string
import popen2
import getopt
## ------------------------------------------------------------------------
## Stuff common to all schemas
##
DATABASE_SCRIPT_COMMON="""\
INTRO_TEXT = """\
This script creates the database and tables in MySQL used by the ViewVC
checkin database. You will be prompted for: database user, database user
password, and database name. This script will use mysql to create the
database for you. You will then need to set the appropriate parameters
in your viewvc.conf file under the [cvsdb] section.
"""
DATABASE_SCRIPT="""\
DROP DATABASE IF EXISTS <dbname>;
CREATE DATABASE <dbname>;
USE <dbname>;
"""
## ------------------------------------------------------------------------
## Version 0: The original, Bonsai-compatible schema.
##
DATABASE_SCRIPT_VERSION_0="""\
DROP TABLE IF EXISTS branches;
CREATE TABLE branches (
id mediumint(9) NOT NULL auto_increment,
branch varchar(64) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE branch (branch)
) TYPE=InnoDB;
);
DROP TABLE IF EXISTS checkins;
CREATE TABLE checkins (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
type enum('Change','Add','Remove'),
ci_when datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
whoid mediumint(9) DEFAULT '0' NOT NULL,
@@ -58,14 +56,13 @@ CREATE TABLE checkins (
removedlines int(11) DEFAULT '0' NOT NULL,
descid mediumint(9),
UNIQUE repositoryid (repositoryid,dirid,fileid,revision),
KEY repositoryid_when (repositoryid,ci_when),
KEY ci_when (ci_when),
KEY whoid (whoid,ci_when),
KEY whoid (whoid),
KEY repositoryid_2 (repositoryid),
KEY dirid (dirid),
KEY fileid (fileid),
KEY branchid (branchid),
KEY descid (descid)
) TYPE=InnoDB;
KEY branchid (branchid)
);
DROP TABLE IF EXISTS descs;
CREATE TABLE descs (
@@ -73,9 +70,8 @@ CREATE TABLE descs (
description text,
hash bigint(20) DEFAULT '0' NOT NULL,
PRIMARY KEY (id),
KEY hash (hash),
FULLTEXT KEY description (description)
) TYPE=MyISAM;
KEY hash (hash)
);
DROP TABLE IF EXISTS dirs;
CREATE TABLE dirs (
@@ -83,7 +79,7 @@ CREATE TABLE dirs (
dir varchar(255) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE dir (dir)
) TYPE=InnoDB;
);
DROP TABLE IF EXISTS files;
CREATE TABLE files (
@@ -91,15 +87,15 @@ CREATE TABLE files (
file varchar(255) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE file (file)
) TYPE=InnoDB;
);
DROP TABLE IF EXISTS people;
CREATE TABLE people (
id mediumint(9) NOT NULL auto_increment,
who varchar(128) binary DEFAULT '' NOT NULL,
who varchar(32) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE who (who)
) TYPE=InnoDB;
);
DROP TABLE IF EXISTS repositories;
CREATE TABLE repositories (
@@ -107,7 +103,7 @@ CREATE TABLE repositories (
repository varchar(64) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE repository (repository)
) TYPE=InnoDB;
);
DROP TABLE IF EXISTS tags;
CREATE TABLE tags (
@@ -121,230 +117,28 @@ CREATE TABLE tags (
KEY dirid (dirid),
KEY fileid (fileid),
KEY branchid (branchid)
) TYPE=InnoDB;
DROP TABLE IF EXISTS contents;
CREATE TABLE contents (
id int NOT NULL PRIMARY KEY,
content MEDIUMTEXT NOT NULL DEFAULT ''
) TYPE=MyISAM;
);
"""
## ------------------------------------------------------------------------
## Version 1: Adds the 'metadata' table. Adds 'descid' index to
## 'checkins' table, and renames that table to 'commits'.
##
DATABASE_SCRIPT_VERSION_1="""\
DROP TABLE IF EXISTS branches;
CREATE TABLE branches (
id mediumint(9) NOT NULL auto_increment,
branch varchar(64) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE branch (branch)
) TYPE=InnoDB;
DROP TABLE IF EXISTS commits;
CREATE TABLE commits (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
type enum('Change','Add','Remove'),
ci_when datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
whoid mediumint(9) DEFAULT '0' NOT NULL,
repositoryid mediumint(9) DEFAULT '0' NOT NULL,
dirid mediumint(9) DEFAULT '0' NOT NULL,
fileid mediumint(9) DEFAULT '0' NOT NULL,
revision varchar(32) binary DEFAULT '' NOT NULL,
stickytag varchar(255) binary DEFAULT '' NOT NULL,
branchid mediumint(9) DEFAULT '0' NOT NULL,
addedlines int(11) DEFAULT '0' NOT NULL,
removedlines int(11) DEFAULT '0' NOT NULL,
descid mediumint(9),
UNIQUE repositoryid (repositoryid,dirid,fileid,revision),
KEY repositoryid_when (repositoryid,ci_when),
KEY ci_when (ci_when),
KEY whoid (whoid,ci_when),
KEY dirid (dirid),
KEY fileid (fileid),
KEY branchid (branchid),
KEY descid (descid)
) TYPE=InnoDB;
DROP TABLE IF EXISTS descs;
CREATE TABLE descs (
id mediumint(9) NOT NULL auto_increment,
description text,
hash bigint(20) DEFAULT '0' NOT NULL,
PRIMARY KEY (id),
KEY hash (hash),
FULLTEXT KEY description (description)
) TYPE=MyISAM;
DROP TABLE IF EXISTS dirs;
CREATE TABLE dirs (
id mediumint(9) NOT NULL auto_increment,
dir varchar(255) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE dir (dir)
) TYPE=InnoDB;
DROP TABLE IF EXISTS files;
CREATE TABLE files (
id mediumint(9) NOT NULL auto_increment,
file varchar(255) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE file (file)
) TYPE=InnoDB;
DROP TABLE IF EXISTS people;
CREATE TABLE people (
id mediumint(9) NOT NULL auto_increment,
who varchar(128) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE who (who)
) TYPE=InnoDB;
DROP TABLE IF EXISTS repositories;
CREATE TABLE repositories (
id mediumint(9) NOT NULL auto_increment,
repository varchar(64) binary DEFAULT '' NOT NULL,
PRIMARY KEY (id),
UNIQUE repository (repository)
) TYPE=InnoDB;
DROP TABLE IF EXISTS tags;
CREATE TABLE tags (
repositoryid mediumint(9) DEFAULT '0' NOT NULL,
branchid mediumint(9) DEFAULT '0' NOT NULL,
dirid mediumint(9) DEFAULT '0' NOT NULL,
fileid mediumint(9) DEFAULT '0' NOT NULL,
revision varchar(32) binary DEFAULT '' NOT NULL,
UNIQUE repositoryid (repositoryid,dirid,fileid,branchid,revision),
KEY repositoryid_2 (repositoryid),
KEY dirid (dirid),
KEY fileid (fileid),
KEY branchid (branchid)
) TYPE=InnoDB;
DROP TABLE IF EXISTS metadata;
CREATE TABLE metadata (
name varchar(255) binary DEFAULT '' NOT NULL,
value text,
PRIMARY KEY (name),
UNIQUE name (name)
) TYPE=InnoDB;
INSERT INTO metadata (name, value) VALUES ('version', '1');
DROP TABLE IF EXISTS contents;
CREATE TABLE contents (
id int NOT NULL PRIMARY KEY,
content MEDIUMTEXT NOT NULL DEFAULT ''
) TYPE=MyISAM;
"""
BONSAI_COMPAT="""
WARNING: Creating Bonsai-compatible legacy database version. Some ViewVC
features may not be available, or may not perform especially well.
"""
## ------------------------------------------------------------------------
def usage_and_exit(errmsg=None):
stream = errmsg is None and sys.stdout or sys.stderr
stream.write("""\
Usage: %s [OPTIONS]
This script creates the database and tables in MySQL used by the
ViewVC checkin database. In order to operate correctly, it needs to
know the following: your database server hostname, database user,
database user password, and database name. (You will be prompted for
any of this information that you do not provide via command-line
options.) This script will use the 'mysql' program to create the
database for you. You will then need to set the appropriate
parameters in the [cvsdb] section of your viewvc.conf file.
Options:
--dbname=ARG Use ARG as the ViewVC database name to create.
[Default: ViewVC]
--help Show this usage message.
--hostname=ARG Use ARG as the hostname for the MySQL connection.
[Default: localhost]
--password=ARG Use ARG as the password for the MySQL connection.
--username=ARG Use ARG as the username for the MySQL connection.
--version=ARG Create the database using the schema employed by
version ARG of ViewVC. Valid values are:
[ "1.0" ]
""" % (os.path.basename(sys.argv[0])))
if errmsg is not None:
stream.write("[ERROR] %s.\n" % (errmsg))
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
try:
# Parse the command-line options, if any.
dbname = version = hostname = username = password = None
opts, args = getopt.getopt(sys.argv[1:], '', [ 'dbname=',
'help',
'hostname=',
'password=',
'username=',
'version=',
])
if len(args) > 0:
usage_and_exit("Unexpected command-line parameters")
for name, value in opts:
if name == '--help':
usage_and_exit(0)
elif name == '--dbname':
dbname = value
elif name == '--hostname':
hostname = value
elif name == '--username':
username = value
elif name == '--password':
password = value
elif name == '--version':
if value in ["1.0"]:
version = value
else:
usage_and_exit("Invalid version specified")
print INTRO_TEXT
user = raw_input("MySQL User: ")
passwd = raw_input("MySQL Password: ")
dbase = raw_input("ViewVC Database Name [default: ViewVC]: ")
if not dbase:
dbase = "ViewVC"
# Prompt for information not provided via command-line options.
if hostname is None:
hostname = raw_input("MySQL Hostname [default: localhost]: ") or ""
if username is None:
username = raw_input("MySQL User: ")
if password is None:
password = raw_input("MySQL Password: ")
if dbname is None:
dbname = raw_input("ViewVC Database Name [default: ViewVC]: ") or "ViewVC"
dscript = string.replace(DATABASE_SCRIPT, "<dbname>", dbase)
# Create the database
dscript = string.replace(DATABASE_SCRIPT_COMMON, "<dbname>", dbname)
if version == "1.0":
print BONSAI_COMPAT
dscript = dscript + DATABASE_SCRIPT_VERSION_0
else:
dscript = dscript + DATABASE_SCRIPT_VERSION_1
host_option = hostname and "--host=%s" % (hostname) or ""
if sys.platform == "win32":
cmd = "mysql --user=%s --password=%s %s "\
% (username, password, host_option)
mysql = os.popen(cmd, "w") # popen2.Popen3 is not provided on windows
# popen2.Popen3 is not provided on windows
cmd = "mysql --user=%s --password=%s" % (user, passwd)
mysql = os.popen(cmd, "w")
mysql.write(dscript)
status = mysql.close()
else:
cmd = "{ mysql --user=%s --password=%s %s ; } 2>&1" \
% (username, password, host_option)
cmd = "{ mysql --user=%s --password=%s ; } 2>&1" % (user, passwd)
pipes = popen2.Popen3(cmd)
pipes.tochild.write(dscript)
pipes.tochild.close()
@@ -352,12 +146,9 @@ if __name__ == "__main__":
status = pipes.wait()
if status:
print "[ERROR] The database did not create sucessfully."
sys.exit(1)
print "[ERROR] the database did not create sucessfully."
sys.exit(1)
print "Database created successfully. Don't forget to configure the "
print "[cvsdb] section of your viewvc.conf file."
except KeyboardInterrupt:
pass
sys.exit(0)
print "Database created successfully."
sys.exit(0)

View File

@@ -1,3 +1,3 @@
AddHandler python-program .py
PythonHandler handler
PythonDebug Off
PythonDebug On

8
bin/mod_python/handler.py Executable file → Normal file
View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 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
@@ -20,8 +20,10 @@ import os.path
def handler(req):
path, module_name = os.path.split(req.filename)
module_name, module_ext = os.path.splitext(module_name)
# Let it be 500 Internal Server Error in case of import error
module = apache.import_module(module_name, path=[path])
try:
module = apache.import_module(module_name, path=[path])
except ImportError:
raise apache.SERVER_RETURN, apache.HTTP_NOT_FOUND
req.add_common_vars()
module.index(req)

36
bin/mod_python/query.py Executable file → Normal file
View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -22,15 +21,15 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
#########################################################################
#
@@ -43,33 +42,16 @@ if LIBRARY_DIR:
sys.path.insert(0, LIBRARY_DIR)
import sapi
import imp
# Import real ViewVC module
fp, pathname, description = imp.find_module('viewvc', [LIBRARY_DIR])
try:
viewvc = imp.load_module('viewvc', fp, pathname, description)
finally:
if fp:
fp.close()
# Import real ViewVC Query modules
fp, pathname, description = imp.find_module('query', [LIBRARY_DIR])
try:
query = imp.load_module('query', fp, pathname, description)
finally:
if fp:
fp.close()
import viewvc
import query
reload(query) # need reload because initial import loads this stub file
cfg = viewvc.load_config(CONF_PATHNAME)
def index(req):
server = sapi.ModPythonServer(req)
try:
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)
query.main(server, cfg, "viewvc.py")
finally:
server.close()

28
bin/mod_python/viewvc.py Executable file → Normal file
View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -22,15 +21,15 @@
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
#########################################################################
#
@@ -43,22 +42,9 @@ if LIBRARY_DIR:
sys.path.insert(0, LIBRARY_DIR)
import sapi
import imp
import signal
import viewvc
reload(viewvc) # need reload because initial import loads this stub file
# Totally ignore SIGPIPE - needed for overcoming popen() errors
def sigpipe(signum, frame):
pass
signal.signal(signal.SIGPIPE, sigpipe)
# Import real ViewVC module
fp, pathname, description = imp.find_module('viewvc', [LIBRARY_DIR])
try:
viewvc = imp.load_module('viewvc', fp, pathname, description)
finally:
if fp:
fp.close()
def index(req):
server = sapi.ModPythonServer(req)

View File

@@ -1,57 +0,0 @@
#!/bin/sh
CVSROOTS=$1
VIEWVC_DIR=$2
CVS_USER=$3
CVS_GROUP=$4
CVS_ASYNC=$5
if [ ! "$CVSROOTS" -o ! "$VIEWVC_DIR" -o ! "$CVS_USER" -o ! "$CVS_GROUP" ]; then
echo "USAGE: $0 <CVS_ROOTS> <VIEWVC_DIR> <CVS_USER> <CVS_GROUP> [CVS_ASYNC]"
exit
fi
echo "*** Setting up commit hooks for CVS repositories (job=$CVS_ASYNC)"
chgrp $CVS_GROUP $VIEWVC_DIR
chmod 775 $VIEWVC_DIR
chmod 1777 .
for CVSROOT in $CVSROOTS; do
vcrootname=`basename $CVSROOT`
if [ "$CVS_ASYNC" ]; then
VHOOK="touch $VIEWVC_DIR/.cvs-updated-$vcrootname"
else
VHOOK="$VIEWVC_DIR/bin/cvsdbadmin update $CVSROOT >/dev/null"
fi;
su $CVS_USER -s /bin/sh <<EOSH
CVSROOT=$CVSROOT cvs co CVSROOT
if [ $? -eq 0 ]; then
cd CVSROOT
mv loginfo loginfo.bak
grep -v '/.cvs-updated' <loginfo.bak | grep -v '/bin/cvsdbadmin update' >loginfo
cat >>loginfo <<EOF
ALL $VHOOK
EOF
cvs ci -m 'CVS commit hook for ViewVC' loginfo
cd ..
rm -Rf CVSROOT
fi
EOSH
if [ "$CVS_ASYNC" ]; then
cat >$VIEWVC_DIR/cvshook-$vcrootname <<EOF
#!/bin/sh
if [ -e "$VIEWVC_DIR/.cvs-updated-$vcrootname" ]; then
rm "$VIEWVC_DIR/.cvs-updated-$vcrootname"
$VIEWVC_DIR/bin/cvsdbadmin update $CVSROOT >/dev/null
fi
EOF
chmod 755 $VIEWVC_DIR/cvshook-$vcrootname
cat >/etc/cron.d/viewvc-cvs-$vcrootname <<EOF
*/5 * * * * root $VIEWVC_DIR/cvshook-$vcrootname
EOF
fi
done;
if [ "$CVS_ASYNC" ]; then
echo "*** Reloading cron daemon"
/etc/init.d/cron reload
fi

View File

@@ -1,36 +0,0 @@
#!/bin/sh
SVNROOT=$1
VIEWVC_DIR=$2
SVN_ASYNC=$3
if [ ! "$SVNROOT" -o ! "$VIEWVC_DIR" ]; then
echo "USAGE: $0 <SVNROOT> <VIEWVC_DIR>"
exit
fi
echo "*** Setting up commit hooks for Subversion"
for i in `ls $SVNROOT`; do
if [ -d "$SVNROOT/$i" ]; then
if [ "$SVN_ASYNC" ]; then
cat >"$SVNROOT/$i/hooks/post-commit.tmp" <<EOF
#!/bin/sh
echo "\$1 \$2" >> $VIEWVC_DIR/.svn-updated
EOF
else
cat >"$SVNROOT/$i/hooks/post-commit.tmp" <<EOF
#!/bin/sh
$VIEWVC_DIR/bin/svndbadmin update \$1 \$2
EOF
fi
chmod 755 "$SVNROOT/$i/hooks/post-commit.tmp"
mv "$SVNROOT/$i/hooks/post-commit.tmp" "$SVNROOT/$i/hooks/post-commit"
fi
done;
if [ "$SVN_ASYNC" ]; then
cat >/etc/cron.d/viewvc-svn <<EOF
*/10 * * * * root $VIEWVC_DIR/bin/svnupdate-async.sh "$VIEWVC_DIR"
EOF
/etc/init.d/cron reload
fi

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 2004-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 2004-2007 James Henstridge
# Copyright (C) 2004 James Henstridge
#
# 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
@@ -28,41 +27,37 @@
#
# If you allow changes to revision properties in your repository, you
# might also want to set up something similar in the
# post-revprop-change hook using "update" with the --force option to
# keep the checkin database consistent with the repository.
# post-revprop-change hook using "rebuild" instead of "update" to keep
# the checkin database consistent with the repository.
#
# -----------------------------------------------------------------------
#
#########################################################################
#
# INSTALL-TIME CONFIGURATION
#
# These values will be set during the installation process. During
# development, there will be no 'viewvcinstallpath.py'
# development, they will remain None.
#
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
LIBRARY_DIR = None
CONF_PATHNAME = None
# Adjust sys.path to include our library directory
import sys
import os
if LIBRARY_DIR:
sys.path.insert(0, LIBRARY_DIR)
sys.path.insert(0, LIBRARY_DIR)
else:
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib")))
#########################################################################
import os
import string
import socket
import select
import re
import mimetypes
import time
import svn.core
import svn.repos
@@ -71,61 +66,27 @@ import svn.delta
import cvsdb
import viewvc
import vclib
from viewvcmagic import ContentMagic
class SvnRepo:
"""Class used to manage a connection to a SVN repository."""
def __init__(self, path, index_content = None, tika_client = None, guesser = None,
svn_ignore_mimetype = False, verbose = False):
def __init__(self, path, pool):
self.scratch_pool = svn.core.svn_pool_create(pool)
self.path = path
self.repo = svn.repos.svn_repos_open(path)
self.repo = svn.repos.svn_repos_open(path, pool)
self.fs = svn.repos.svn_repos_fs(self.repo)
self.rev_max = svn.fs.youngest_rev(self.fs)
self.index_content = index_content
self.tika_client = tika_client
self.guesser = guesser
self.verbose = verbose
self.svn_ignore_mimetype = svn_ignore_mimetype
# youngest revision of base of file system is highest revision number
self.rev_max = svn.fs.youngest_rev(self.fs, pool)
def __getitem__(self, rev):
if rev is None:
rev = self.rev_max
elif rev < 0:
rev = rev + self.rev_max + 1
assert 0 <= rev <= self.rev_max
rev = SvnRev(self, rev)
rev = SvnRev(self, rev, self.scratch_pool)
svn.core.svn_pool_clear(self.scratch_pool)
return rev
_re_diff_change_command = re.compile('^(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?')
class StupidBufferedReader:
def __init__(self, fp, buffer = 262144):
self.fp = fp
self.bufsize = buffer
self.buffer = ''
self.eof = False
def __iter__(self):
return self
def next(self):
if self.eof:
raise StopIteration
return self.readline()
def readline(self):
if self.eof:
return ''
p = self.buffer.find('\n')
while p < 0:
b = self.fp.read(self.bufsize)
if not len(b):
r = self.buffer
self.buffer = ''
self.eof = True
return r
self.buffer = self.buffer + b
p = self.buffer.find('\n')
r = self.buffer[0:p+1]
self.buffer = self.buffer[p+1:]
return r
_re_diff_change_command = re.compile('(\d+)(?:,(\d+))?([acd])(\d+)(?:,(\d+))?')
def _get_diff_counts(diff_fp):
"""Calculate the plus/minus counts by parsing the output of a
@@ -138,7 +99,8 @@ def _get_diff_counts(diff_fp):
GNU diff manual."""
plus, minus = 0, 0
for line in diff_fp:
line = diff_fp.readline()
while line:
match = re.match(_re_diff_change_command, line)
if match:
# size of first range
@@ -164,203 +126,78 @@ def _get_diff_counts(diff_fp):
# RdL - remove range R of file1, which would have been
# at line L of file2
minus = minus + count1
line = diff_fp.readline()
return plus, minus
class TikaClient:
# Create tika client
def __init__(self, tika_server, mime_types, verbose):
self.tika_server = tika_server
self.mime_types = mime_types
self.verbose = verbose
self.addr = tika_server.split(':')
# Split address
if len(self.addr) != 2:
raise Exception('tika_server value is incorrect: \''+tika_server+'\', please use \'host:port\' format')
self.addr = (self.addr[0], int(self.addr[1]))
# Build regexp for MIME types
m = re.split('\s+', mime_types.strip())
self.mime_regexp = re.compile('|'.join('^'+re.escape(i).replace('\\*', '.*')+'$' for i in m))
# Extract text content from file using Tika which runs in server mode
def get_text(self, filename, mime_type, log_filename):
if not self.mime_regexp.match(mime_type):
# Tika can't handle this mime type, return nothing
return ''
fd = None
s = None
text = ''
fsize = 0
try:
# Read original file
fd = open(filename, 'rb')
data = fd.read()
fsize = len(data)
if not fsize:
return ''
# Connect to Tika
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(self.addr)
s.setblocking(0)
sockfd = s.fileno()
# Tika is somewhat delicate about network IO, so:
# Read and write using poll(2) system call
p = select.poll()
p.register(sockfd)
while 1:
fds = p.poll()
if not fds:
break
(pollfd, event) = fds[0]
if event & select.POLLIN:
# Exception or empty data means EOF...
try: part = os.read(sockfd, 65536)
except: break
if not part: break
text += part
if event & select.POLLOUT:
if not len(data):
# Shutdown output and forget about POLLOUT
s.shutdown(socket.SHUT_WR)
p.modify(sockfd, select.POLLIN)
else:
# Write and consume some data
l = os.write(sockfd, data)
data = data[l:]
if len(text) == 0:
raise Exception('Empty response from Tika server')
if self.verbose:
print "Extracted %d bytes from %s (%s) of size %d" % (len(text), log_filename, mime_type, fsize)
except Exception, e:
if self.verbose:
print "Error extracting text from %s (%s) of size %d: %s" % (log_filename, mime_type, fsize, str(e))
finally:
if fd: fd.close()
if s: s.close()
return text
class SvnRev:
"""Class used to hold information about a particular revision of
the repository."""
def __init__(self, repo, rev):
def __init__(self, repo, rev, pool):
self.repo = repo
self.rev = rev
self.pool = pool
self.rev_roots = {} # cache of revision roots
subpool = svn.core.svn_pool_create(pool)
# revision properties ...
revprops = svn.fs.revision_proplist(repo.fs, rev)
self.author = str(revprops.get(svn.core.SVN_PROP_REVISION_AUTHOR,''))
self.date = str(revprops.get(svn.core.SVN_PROP_REVISION_DATE, ''))
self.log = str(revprops.get(svn.core.SVN_PROP_REVISION_LOG, ''))
properties = svn.fs.revision_proplist(repo.fs, rev, pool)
self.author = str(properties.get(svn.core.SVN_PROP_REVISION_AUTHOR,''))
self.date = str(properties.get(svn.core.SVN_PROP_REVISION_DATE, ''))
self.log = str(properties.get(svn.core.SVN_PROP_REVISION_LOG, ''))
# convert the date string to seconds since epoch ...
try:
self.date = svn.core.svn_time_from_cstring(self.date) / 1000000
except:
self.date = None
self.date = svn.core.secs_from_timestr(self.date, pool)
# get a root for the current revisions
fsroot = self._get_root_for_rev(rev)
# find changes in the revision
editor = svn.repos.ChangeCollector(repo.fs, fsroot)
e_ptr, e_baton = svn.delta.make_editor(editor)
svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton)
editor = svn.repos.RevisionChangeCollector(repo.fs, rev, pool)
e_ptr, e_baton = svn.delta.make_editor(editor, pool)
svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton, pool)
self.changes = []
changes_hash = {}
for path, change in editor.changes.items():
# clear the iteration subpool
svn.core.svn_pool_clear(subpool)
# skip non-file changes
if change.item_kind != svn.core.svn_node_file:
continue
if change.item_kind != svn.core.svn_node_file: continue
# deal with the change types we handle
action = None
base_root = None
base_path = change.base_path
if change.base_path:
base_root = self._get_root_for_rev(change.base_rev)
# figure out what kind of change this is, and get a diff
# object for it. note that prior to 1.4 Subversion's
# bindings didn't give us change.action, but that's okay
# because back then deleted paths always had a change.path
# of None.
if hasattr(change, 'action') \
and change.action == svn.repos.CHANGE_ACTION_DELETE:
action = 'remove'
elif not change.path:
if not change.path:
action = 'remove'
elif change.added:
action = 'add'
else:
action = 'change'
if action == 'remove':
diffobj = svn.fs.FileDiff(base_root, change.base_path, None, None, None, ['-b', '-B'])
else:
diffobj = svn.fs.FileDiff(base_root, change.base_path, fsroot, change.path, None, ['-b', '-B'])
diffobj = svn.fs.FileDiff(base_root and base_root or None,
base_root and change.base_path or None,
change.path and fsroot or None,
change.path and change.path or None,
subpool, [])
diff_fp = diffobj.get_pipe()
diff_fp = StupidBufferedReader(diff_fp)
plus, minus = _get_diff_counts(diff_fp)
# CustIS Bug 50473: a workaround for svnlib behaviour in file movements (FILE1 -> FILE2 + FILE1 -> null)
if change.base_path:
if not change.path and change.base_path in changes_hash:
minus = 0
elif change.path:
changes_hash[change.base_path] = change.path
content = ''
mime = ''
# need to check if binary file's content changed when copying,
# if not, don't extract it, just get it from previous revision later
if repo.index_content and action != 'remove' and change.path and (not change.base_path
or svn.fs.contents_changed(
base_root and base_root or None,
base_root and change.base_path or None,
fsroot, change.path
)):
props = svn.fs.node_proplist(fsroot, change.path)
if not repo.svn_ignore_mimetype:
mime = props.get('svn:mime-type', None)
else:
mime = None
mime = repo.guesser.guess_mime(
mime,
os.path.basename(change.path),
diffobj.tempfile2
)
# Read and guess charset by ourselves for text files
if mime and mime.startswith('text/') or (mime.startswith('application/') and mime.endswith('xml')):
try:
fd = open(diffobj.tempfile2, 'rb')
content = fd.read()
fd.close()
except: pass
# Guess charset
if content:
content, charset = repo.guesser.guess_charset(content)
if charset:
content = content.encode('utf-8')
if repo.verbose:
print 'Guessed %s for %s' % (charset, change.path)
elif repo.verbose:
print 'Failed to guess charset for %s, not indexing' % (change.path, )
# Try to extract content using Tika from binary documents
elif repo.tika_client:
content = repo.tika_client.get_text(diffobj.tempfile2, mime, change.path)
self.changes.append((path, action, plus, minus, content, mime))
self.changes.append((path, action, plus, minus))
def _get_root_for_rev(self, rev):
"""Fetch a revision root from a cache of such, or a fresh root
(which is then cached for later use."""
if not self.rev_roots.has_key(rev):
self.rev_roots[rev] = svn.fs.revision_root(self.repo.fs, rev)
self.rev_roots[rev] = svn.fs.revision_root(self.repo.fs, rev,
self.pool)
return self.rev_roots[rev]
def handle_revision(db, command, repo, rev, verbose, force=0):
def handle_revision(db, command, repo, rev, verbose):
"""Adds a particular revision of the repository to the checkin database."""
revision = repo[rev]
committed = 0
@@ -371,7 +208,7 @@ def handle_revision(db, command, repo, rev, verbose, force=0):
if verbose: print "skipped (no changes)."
return
for (path, action, plus, minus, content, mime) in revision.changes:
for (path, action, plus, minus) in revision.changes:
directory, file = os.path.split(path)
commit = cvsdb.CreateCommit()
commit.SetRepository(repo.path)
@@ -384,8 +221,6 @@ def handle_revision(db, command, repo, rev, verbose, force=0):
commit.SetPlusCount(plus)
commit.SetMinusCount(minus)
commit.SetBranch(None)
commit.SetContent(content)
commit.SetMimeType(mime)
if action == 'add':
commit.SetTypeAdd()
@@ -396,7 +231,7 @@ def handle_revision(db, command, repo, rev, verbose, force=0):
if command == 'update':
result = db.CheckCommit(commit)
if result and not force:
if result:
continue # already recorded
# commit to database
@@ -409,89 +244,38 @@ def handle_revision(db, command, repo, rev, verbose, force=0):
else:
print "skipped (already recorded)."
def main(command, repository, revs=[], verbose=0, force=0):
def main(pool, command, repository, rev=None, verbose=0):
cfg = viewvc.load_config(CONF_PATHNAME)
db = cvsdb.ConnectDatabase(cfg)
repository = os.path.realpath(repository)
# Purge what must be purged.
if command in ('rebuild', 'purge'):
if verbose:
print "Purging commit info for repository root `%s'" % repository
try:
db.PurgeRepository(repository)
except cvsdb.UnknownRepositoryError, e:
if command == 'purge':
sys.stderr.write("ERROR: " + str(e) + "\n")
sys.exit(1)
tika_client = None
if cfg.utilities.tika_server:
tika_client = TikaClient(cfg.utilities.tika_server, cfg.utilities.tika_mime_types, verbose)
repo = SvnRepo(
path = repository,
index_content = cfg.cvsdb.index_content,
tika_client = tika_client,
guesser = cfg.guesser(),
svn_ignore_mimetype = cfg.options.svn_ignore_mimetype,
verbose = verbose,
)
# Record what must be recorded.
if command == 'rebuild' or (command == 'update' and not revs):
for rev in range(repo.rev_max+1):
handle_revision(db, command, repo, rev, verbose, force)
elif command == 'update':
if revs[0] is None:
revs[0] = repo.rev_max
if revs[1] is None:
revs[1] = repo.rev_max
revs.sort()
for rev in range(revs[0], revs[1]+1):
handle_revision(db, command, repo, rev, verbose, force)
def _rev2int(r):
if r == 'HEAD':
r = None
repo = SvnRepo(repository, pool)
if rev:
handle_revision(db, command, repo, rev, verbose)
else:
r = int(r)
if r < 0:
raise ValueError, "invalid revision '%d'" % (r)
return r
for rev in range(repo.rev_max+1):
handle_revision(db, command, repo, rev, verbose)
def usage():
cmd = os.path.basename(sys.argv[0])
sys.stderr.write(
"""Administer the ViewVC checkins database data for the Subversion repository
located at REPOS-PATH.
sys.stderr.write("""
Usage: 1. %s [-v] rebuild REPOSITORY [REVISION]
2. %s [-v] update REPOSITORY [REVISION]
Usage: 1. %s [-v] rebuild REPOS-PATH
2. %s [-v] update REPOS-PATH [REV[:REV2]] [--force]
3. %s [-v] purge REPOS-PATH
1. Rebuild the commit database information for REPOSITORY across all revisions
or, optionally, only for the specified REVISION.
1. Rebuild the commit database information for the repository located
at REPOS-PATH across all revisions, after first purging
information specific to that repository (if any).
2. Update the commit database information for the repository located
at REPOS-PATH across all revisions or, optionally, only for the
specified revision REV (or revision range REV:REV2). This is just
like rebuilding, except that, unless --force is specified, no
commit information will be stored for commits already present in
the database. If a range is specified, the revisions will be
processed in ascending order, and you may specify "HEAD" to
indicate "the youngest revision currently in the repository".
3. Purge information specific to the repository located at REPOS-PATH
from the database.
2. Update the commit database information for REPOSITORY across all revisions
or, optionally, only for the specified REVISION. This is just like
rebuilding, except that no commit information will be stored for
commits already present in the database.
Use the -v flag to cause this script to give progress information as it works.
""" % (cmd, cmd, cmd))
""" % (cmd, cmd))
sys.exit(1)
if __name__ == '__main__':
verbose = 0
force = 0
args = sys.argv
try:
index = args.index('-v')
@@ -499,47 +283,29 @@ if __name__ == '__main__':
del args[index]
except ValueError:
pass
try:
index = args.index('--force')
force = 1
del args[index]
except ValueError:
pass
if len(args) < 3:
usage()
command = args[1].lower()
if command not in ('rebuild', 'update', 'purge'):
command = string.lower(args[1])
if command not in ('rebuild', 'update'):
sys.stderr.write('ERROR: unknown command %s\n' % command)
usage()
revs = []
repository = args[2]
if not os.path.exists(repository):
sys.stderr.write('ERROR: could not find repository %s\n' % repository)
usage()
if len(sys.argv) > 3:
if command == 'rebuild':
sys.stderr.write('ERROR: rebuild no longer accepts a revision '
'number argument. Usage update --force.')
usage()
elif command != 'update':
usage()
rev = sys.argv[3]
try:
revs = map(lambda x: _rev2int(x), sys.argv[3].split(':'))
if len(revs) > 2:
raise ValueError, "too many revisions in range"
if len(revs) == 1:
revs.append(revs[0])
rev = int(rev)
except ValueError:
sys.stderr.write('ERROR: invalid revision specification "%s"\n' \
% sys.argv[3])
sys.stderr.write('ERROR: revision "%s" is not numeric\n' % rev)
usage()
else:
rev = None
try:
repository = vclib.svn.canonicalize_rootpath(args[2])
repository = cvsdb.CleanRepository(os.path.abspath(repository))
main(command, repository, revs, verbose, force)
except KeyboardInterrupt:
print
print '** break **'
sys.exit(0)
repository = cvsdb.CleanRepository(os.path.abspath(repository))
svn.core.run_app(main, command, repository, rev, verbose)

View File

@@ -1,50 +0,0 @@
#!/usr/bin/perl
# Скрипт для обновления SVN репозиториев svndbadmin-ом
# Берёт номера ревизий и имена репозиториев из перечисленных файлов или STDIN,
# группирует их по номерам и выводит список команд, необходимых для обновления
use strict;
# первый аргумент - путь к svndbadmin
my $svndbadmin = shift @ARGV
|| die "USAGE: $0 <path_to_svndbadmin> FILES...";
# считываем названия репозиториев и номера ревизий из файла
my $tou = {};
my ($repos, $rev);
while (<>)
{
s/^\s+//so;
s/\s+$//so;
($repos, $rev) = split /\s+/, $_;
$tou->{$repos}->{$rev} = 1;
}
# превращаем номера ревизий в диапазоны ревизий
my ($i, $j, $r, $nr);
foreach $repos (keys %$tou)
{
$rev = [ sort keys %{$tou->{$repos}} ];
$nr = [];
$j = 0;
for $i (1..@$rev)
{
if ($i > $#$rev || $rev->[$i]-$rev->[$j] > $i-$j)
{
$r = $rev->[$j];
$r .= ":".$rev->[$i-1] if $i-$j > 1;
push @$nr, $r;
$j = $i;
}
}
$tou->{$repos} = $nr;
}
# выводим список команд для выполнения
foreach $repos (keys %$tou)
{
foreach (@{$tou->{$repos}})
{
print "$svndbadmin update $repos $_\n";
}
}

View File

@@ -1,8 +0,0 @@
#!/bin/sh
VIEWVC_DIR=$1
test -f "$VIEWVC_DIR/.svn-updating" -o ! -f "$VIEWVC_DIR/.svn-updated" && exit 0
mv "$VIEWVC_DIR/.svn-updated" "$VIEWVC_DIR/.svn-updating"
"$VIEWVC_DIR/bin/svnupdate-async" "$VIEWVC_DIR/bin/svndbadmin" "$VIEWVC_DIR/.svn-updating" | sh &> /tmp/svnupdate-async.log
rm "$VIEWVC_DIR/.svn-updating"

View File

@@ -1,54 +0,0 @@
#!/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()

View File

@@ -1,45 +0,0 @@
# -*-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 []

View File

@@ -1,50 +0,0 @@
#!/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()

View File

@@ -1,41 +0,0 @@
# -*-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 []

View File

@@ -1,41 +0,0 @@
# -*-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
import viewvcinstallpath
LIBRARY_DIR = viewvcinstallpath.LIBRARY_DIR
CONF_PATHNAME = viewvcinstallpath.CONF_PATHNAME
if LIBRARY_DIR:
sys.path.insert(0, LIBRARY_DIR)
else:
sys.path.insert(0, os.path.abspath(os.path.join(
os.path.dirname(__file__), "../../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 []

View File

@@ -1,32 +0,0 @@
#---------------------------------------------------------------------------
#
# MIME type mapping file for ViewVC
#
# Information on ViewVC is located at the following web site:
# http://viewvc.org/
#
#---------------------------------------------------------------------------
# THE FORMAT OF THIS FILE
#
# This file contains records -- one per line -- of the following format:
#
# MIME_TYPE [EXTENSION [EXTENSION ...]]
#
# where whitespace separates the MIME_TYPE from the EXTENSION(s),
# and the EXTENSIONs from each other.
#
# For example:
#
# text/x-csh csh
# text/x-csrc c
# text/x-diff diff patch
# image/png png
# image/jpeg jpeg jpg jpe
#
# By default, this file is left empty, allowing ViewVC to continue
# consulting it first without overriding the MIME type mappings
# found in more standard mapping files (such as those provided as
# part of the operating system or web server software).
#
#

File diff suppressed because it is too large Load Diff

View File

@@ -1,213 +0,0 @@
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html>
<head>
<title>ViewVC: 1.1.0 Release Notes</title>
<style>
.h2, .h3 {
padding: 0.25em 0em;
background: white;
}
.warning {
font-style: italic;
}
</style>
</head>
<body>
<h1>ViewVC 1.1.0 Release Notes</h1>
<div class="h2">
<h2 id="introduction">Introduction</h2>
<p>ViewVC 1.1.0 is the superset of all previous ViewVC releases.</p>
</div> <!-- h2 -->
<div class="h2">
<h2 id="compatibility">Compatibility</h2>
<p>Each ViewVC release strives to maintain URL stability with previous
releases, and 1.1.0 is no exception. All URLs considered valid for
previous ViewVC releases should continue to work correctly in this
release, though possibly only via the use of HTTP redirects
(generated by ViewVC itself).</p>
<p>The commits database functionality has changed in ViewVC 1.1.0 in
way that breaks compatibility with prior ViewVC releases, but only
for new database instantiations. ViewVC 1.1.0 will continue to
understand (for both read and write operations) the previous
schema, so you are not required to rebuild your commits database
for ViewVC 1.1.0 compatibility. By default, new commits databases
created using the 1.1.0 version of the <code>make-database</code>
script will use a new database schema that is unreadable by
previous ViewVC versions. However, if you need a database which
can co-exist with a previous ViewVC version, you can use
the <code>--version=1.0</code> option
to <code>make-database</code>.</p>
<p>The ViewVC configuration files and template language have changed
dramatically. See the file <code>docs/upgrading-howto.html</code>
in the release for information on porting existing versions of
those items for use with ViewVC 1.1.0.</p>
</div> <!-- h2 -->
<div class="h2">
<h2 id="compatibility">Features and Fixes</h2>
<div class="h3">
<h3 id="">Extensible path-based authorization w/ Subversion authz support</h3>
<p>In a nutshell, ViewVC is now able to do path-based authorization.
ViewVC 1.0 has a configuration option for naming 'forbidden'
modules, but it is really limited &mdash; it basically just makes a
universal decision about which top-level directories in every
hosted repository should be hidden by ViewVC. People want
more, and specifically requested that ViewVC learn how to honor
Subversion's authz files and semantics. So, along with some other
types of authorization approaches, that's what ViewVC 1.1 can now
do. If you are using mod_authz_svn with Apache today, or
svnserve's built-in authorization support, then you can now point
ViewVC to the same authz configuration file and have it honor the
access rules you've defined for your repositories.</p>
<p>Note that ViewVC does <strong>not</strong> handle authentication,
though. You'll need to configure your web server to demand login
credentials from users, which the web server itself can then hand
off to ViewVC for application against the authorization rules
you've defined.</p>
<p class="warning">WARNING: The root listing view does not consult the
authorization subsystem when deciding what roots to display to a
given user. If you need to protect your root names, consider
disabling it by removing <code>roots</code> from the set of views
listed in the <code>allowed_views</code> configuration option.
<strong>UPDATE: This was fixed in ViewVC 1.1.3.</strong></p>
<p class="warning">WARNING: Support for path-based authorization is
incomplete in the experimental version control backend modules,
including the one that permits display of remote Subversion
repositories. <strong>UPDATE: This was fixed in ViewVC
1.1.15.</strong></p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Subversion versioned properties display</h3>
<p>ViewVC 1.1 displays the properties that Subversion lets you store
on files and directories
(<code>svn:mime-type</code>, <code>svn:mergeinfo</code>,
<code>svn:ignore</code>, etc.). Directory properties are shown by
default at the bottom of that directory's entry listing. File
properties are displayed at the bottom of that file's
markup/annotate view.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Unified markup and annotation views</h3>
<p>The "markup" and "annotate" views in ViewVC now have a unified look
and feel (driven by a single EZT template). Both views support
syntax highlighting and Subversion file property display.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Unified, hassle-free Pygments-based syntax highlighting</h3>
<p>ViewVC 1.0 does syntax highlighting by working with GNU enscript, or
highlight, or php, or py2html &mdash; all these external tools just
to accomplish a single task. But they all do things in slightly
different ways. And if you configure them wrongly, you get strange
errors. <a href="http://www.pygments.org/">Pygments</a> (which is
also used by <a href="http://trac.edgewall.org/">Trac</a> for
syntax highlighting) is a Python package that requires no
configuration, is easier to use inside ViewVC, and so on. So
ViewVC 1.1 drops support for all those various old integrations,
and just uses Pygments for everything now. This change was about
developer and administrator sanity. There will be complaints, to
be sure, about how various color schemes differ and what file types
now are and aren't understood by the syntax highlighting engine,
but this change should vastly simplify the discussions of such
things.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Better MIME detection and handling</h3>
<p>ViewVC typically consults a MIME types file to determine what kind
of file a given document is, based on its filename extension
(<code>.jpg</code> = <code>image/jpeg</code>, &hellip;). But
Subversion lets you dictate a file's MIME type using
the <code>svn:mime-type</code> property. ViewVC now recognizes and
honors that property as the preferred source of a file's MIME type.
This can be disabled in the configuration, though, which might be
desirable if many of your Subversion-versioned files carry the
generic <code>application/octet-stream</code> MIME type that
Subversion uses by default for non-human-readable files.</p>
<p>Also, ViewVC now allows you to specify multiple MIME type mapping
files that you'd like it to consult when determine the MIME type of
files based on their extensions. This allows administrators to
easily define their own custom mappings for ViewVC's benefit
without potentially affecting the mappings used by other site
services.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Support for full content diffs</h3>
<p>ViewVC 1.1 expands the previously existing options of "colored
diff" and "long colored diff" with a new "full colored diff", which
shows the full contents of the changed file (instead of only the 3
or 15 lines of context shown via the older diff display types).</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Support for per-root configuration overrides</h3>
<p>In ViewVC 1.1, you can setup configuration option overrides on a
per-root (per-repository) basis (if you need/care to do so). See
the comments in the <code>viewvc.conf.dist</code> file for more on
how to do this.</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Optional email address obfuscation/mangling</h3>
<p>ViewVC can, when displaying revision metadata, munge strings that
look like email addresses to protect them from screen-scraping
spammers. For example, a log message that says, "Patch by:
cmpilato@red-bean.com" can optionally be displayed by ViewVC using
HTML entity encoding for the characters (a trick that causes no
visible change to the output, but that might confuse
unsophisticated spam bot crawlers) or as "Patch by: cmpilato@..."
(which isn't a complete email address at all, but might be enough
information for the human reading the log message to know who to
blame for the patch).</p>
</div> <!-- h3 -->
<div class="h3">
<h3 id="">Pagination improvements</h3>
<p>The way that ViewVC splits directory and log views across pages has
been reworked. The old way was "Fetch all the information you can
find, then display only one page's worth." The new way is "Fetch
only what you need to display the page requested, plus a little bit
of border information." This provides a large performance
enhancement for the default sort orderings.</p>
</div> <!-- h3 -->
</div> <!-- h2 -->
</body>
</html>

View File

@@ -1,49 +0,0 @@
<!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>

8
elemx/java/.cvsignore Normal file
View File

@@ -0,0 +1,8 @@
elx-java
j_keywords.c
j_keywords.h
j_scan.c
j_scan.h
java.c
java.h
*.output

6
elemx/python/.cvsignore Normal file
View File

@@ -0,0 +1,6 @@
elx-python
py_keywords.c
py_keywords.h
python.c
python.h
*.output

View File

@@ -51,12 +51,16 @@ keywordsList = ["and", "assert", "break", "class", "continue", "def",
"return", "try", "while",
]
# First a little helper, since I don't like to repeat things. (Tismer speaking)
def replace(where, what, with):
return string.join(string.split(where, what), with)
# A regexp for matching Python comments.
commentPat = "#.*"
# A regexp for matching simple quoted strings.
pat = "q[^q\\n]*(\\[\000-\377][^q\\n]*)*q"
quotePat = string.replace(pat, "q", "'") + "|" + string.replace(pat, 'q', '"')
quotePat = replace(pat, "q", "'") + "|" + replace(pat, 'q', '"')
# A regexp for matching multi-line tripled-quoted strings. (Way to go, Tim!)
pat = """
@@ -78,8 +82,7 @@ pat = """
qqq
"""
pat = string.join(string.split(pat), '') # get rid of whitespace
tripleQuotePat = string.replace(pat, "q", "'") + "|" \
+ string.replace(pat, 'q', '"')
tripleQuotePat = replace(pat, "q", "'") + "|" + replace(pat, 'q', '"')
# A regexp which matches all and only Python keywords. This will let
# us skip the uninteresting identifier references.

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -15,6 +15,7 @@
# -----------------------------------------------------------------------
import re
import string
def language(hdr):
@@ -38,8 +39,8 @@ def _parse(hdr, result):
while pos < len(hdr):
name = _re_token.match(hdr, pos)
if not name:
raise AcceptLanguageParseError()
a = result.item_class(name.group(1).lower())
raise AcceptParseError()
a = result.item_class(string.lower(name.group(1)))
pos = name.end()
while 1:
# are we looking at a parameter?
@@ -55,7 +56,7 @@ def _parse(hdr, result):
# the "=" was probably missing
continue
pname = match.group(1).lower()
pname = string.lower(match.group(1))
if pname == 'q' or pname == 'qs':
try:
a.quality = float(match.group(2))
@@ -69,7 +70,7 @@ def _parse(hdr, result):
# bad float literal
pass
elif pname == 'charset':
a.charset = match.group(2).lower()
a.charset = string.lower(match.group(2))
result.append(a)
if hdr[pos:pos+1] == ',':
@@ -209,7 +210,7 @@ class _LanguageSelector:
def append(self, item):
self.requested.append(item)
class AcceptLanguageParseError(Exception):
class AcceptParseError(Exception):
pass
def _test():

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
#
# By using this file, you agree to the terms and conditions set forth in
@@ -26,15 +26,15 @@
#
# -----------------------------------------------------------------------
import sys
import string
import os
import re
import time
import math
from common import _item
import cgi
import vclib
import sapi
import vclib.ccvs.blame
re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"')
@@ -43,7 +43,7 @@ def link_includes(text, repos, path_parts, include_url):
if match:
incfile = match.group(3)
include_path_parts = path_parts[:-1]
for part in filter(None, incfile.split('/')):
for part in filter(None, string.split(incfile, '/')):
if part == "..":
if not include_path_parts:
# nothing left to pop; don't bother marking up this include.
@@ -55,14 +55,14 @@ def link_includes(text, repos, path_parts, include_url):
include_path = None
try:
if repos.itemtype(include_path_parts, None) == vclib.FILE:
include_path = '/'.join(include_path_parts)
include_path = string.join(include_path_parts, '/')
except vclib.ItemNotFound:
pass
if include_path:
return '#%sinclude%s<a href="%s">"%s"</a>' % \
(match.group(1), match.group(2),
include_url.replace('/WHERE/', include_path), incfile)
string.replace(include_url, '/WHERE/', include_path), incfile)
return text
@@ -75,15 +75,14 @@ class HTMLBlameSource:
self.path_parts = path_parts
self.diff_url = diff_url
self.include_url = include_url
self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev,
True)
self.annotation, self.revision = self.repos.annotate(path_parts, opt_rev)
def __getitem__(self, idx):
item = self.annotation.__getitem__(idx)
diff_url = None
if item.prev_rev:
diff_url = '%sr1=%s&amp;r2=%s' % (self.diff_url, item.prev_rev, item.rev)
thisline = link_includes(sapi.escape(item.text), self.repos,
thisline = link_includes(cgi.escape(item.text), self.repos,
self.path_parts, self.include_url)
return _item(text=thisline, line_number=item.line_number,
rev=item.rev, prev_rev=item.prev_rev,
@@ -95,45 +94,79 @@ def blame(repos, path_parts, diff_url, include_url, opt_rev=None):
return source, source.revision
class _item:
def __init__(self, **kw):
vars(self).update(kw)
def make_html(root, rcs_path):
import vclib.ccvs.blame
bs = vclib.ccvs.blame.BlameSource(os.path.join(root, rcs_path))
count = bs.num_lines
if count == 0:
count = 1
line_num_width = int(math.log10(count)) + 1
revision_width = 3
author_width = 5
line = 0
old_revision = 0
row_color = 'ffffff'
row_color = ''
inMark = 0
rev_count = 0
align = ' style="text-align: %s;"'
open_table_tag = '<table cellpadding="0" cellspacing="0">'
startOfRow = '<tr><td colspan="3"%s><pre>'
endOfRow = '</td></tr>'
print open_table_tag + (startOfRow % '')
sys.stdout.write('<table cellpadding="2" cellspacing="2" style="font-family: monospace; whitespace: pre;">\n')
for line_data in bs:
revision = line_data.rev
thisline = line_data.text
line = line_data.line_number
author = line_data.author
prev_rev = line_data.prev_rev
output = ''
if old_revision != revision and line != 1:
if row_color == 'ffffff':
row_color = 'e7e7e7'
if row_color == '':
row_color = ' style="background-color:#e7e7e7"'
else:
row_color = 'ffffff'
row_color = ''
sys.stdout.write('<tr id="l%d" style="background-color: #%s; vertical-align: center;">' % (line, row_color))
sys.stdout.write('<td%s>%d</td>' % (align % 'right', line))
if not inMark:
output = output + endOfRow + (startOfRow % row_color)
output = output + '<a name="%d">%*d</a>' % (line, line_num_width, line)
if old_revision != revision or rev_count > 20:
sys.stdout.write('<td%s>%s</td>' % (align % 'right', author or '&nbsp;'))
sys.stdout.write('<td%s>%s</td>' % (align % 'left', revision))
revision_width = max(revision_width, len(revision))
output = output + ' '
author_width = max(author_width, len(author))
output = output + ('%-*s ' % (author_width, author))
output = output + revision
if prev_rev:
output = output + '</a>'
output = output + (' ' * (revision_width - len(revision) + 1))
old_revision = revision
rev_count = 0
else:
sys.stdout.write('<td>&nbsp;</td><td>&nbsp;</td>')
output = output + ' ' + (' ' * (author_width + revision_width))
rev_count = rev_count + 1
sys.stdout.write('<td%s>%s</td></tr>\n' % (align % 'left', thisline.rstrip() or '&nbsp;'))
sys.stdout.write('</table>\n')
output = output + thisline
# Close the highlighted section
#if (defined $mark_cmd and mark_cmd != 'begin'):
# chop($output)
# output = output + endOfRow + (startOfRow % row_color)
# inMark = 0
print output
print endOfRow + '</table>'
def main():

View File

@@ -1,60 +0,0 @@
# -*-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)

166
lib/compat.py Normal file
View File

@@ -0,0 +1,166 @@
# -*-python-*-
#
# Copyright (C) 1999-2006 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():
for i in range(10):
dir = tempfile.mktemp()
try:
os.mkdir(dir, 0700)
return dir
except OSError, e:
if e.errno == errno.EEXIST:
continue # try again
raise
raise IOError, (errno.EEXIST, "No usable temporary directory name found")
#
# the following stuff is *ONLY* needed for standalone.py.
# For that reason I've encapsulated it into a function.
#
def for_standalone():
import SocketServer
if not hasattr(SocketServer.TCPServer, "close_request"):
#
# method close_request() was missing until Python 2.1
#
class TCPServer(SocketServer.TCPServer):
def process_request(self, request, client_address):
"""Call finish_request.
Overridden by ForkingMixIn and ThreadingMixIn.
"""
self.finish_request(request, client_address)
self.close_request(request)
def close_request(self, request):
"""Called to clean up an individual request."""
request.close()
SocketServer.TCPServer = TCPServer

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -16,181 +16,64 @@
import sys
import os
import string
import ConfigParser
import fnmatch
import vclib
import vclib.ccvs
import vclib.svn
import vclib.git
import cvsdb
import viewvc
import copy
from viewvcmagic import ContentMagic
#########################################################################
#
# CONFIGURATION
# -------------
#
# There are three forms of configuration:
#
# 1. edit the viewvc.conf created by the viewvc-install(er)
# 2. as (1), but delete all unchanged entries from viewvc.conf
# 3. do not use viewvc.conf and just edit the defaults in this file
# 1) edit the viewvc.conf created by the viewvc-install(er)
# 2) as (1), but delete all unchanged entries from viewvc.conf
# 3) do not use viewvc.conf and just edit the defaults in this file
#
# Most users will want to use (1), but there are slight speed advantages
# to the other two options. Note that viewvc.conf values are a bit easier
# to work with since it is raw text, rather than python literal values.
#
#
# A WORD ABOUT OPTION LAYERING/OVERRIDES
# --------------------------------------
#
# ViewVC has three "layers" of configuration options:
#
# 1. base configuration options - very basic configuration bits
# found in sections like 'general', 'options', etc.
# 2. vhost overrides - these options overlay/override the base
# configuration on a per-vhost basis.
# 3. root overrides - these options overlay/override the base
# configuration and vhost overrides on a per-root basis.
#
# Here's a diagram of the valid overlays/overrides:
#
# PER-ROOT PER-VHOST BASE
#
# ,-----------. ,-----------.
# | vhost-*/ | | |
# | general | --> | general |
# | | | |
# `-----------' `-----------'
# ,-----------. ,-----------. ,-----------.
# | root-*/ | | vhost-*/ | | |
# | options | --> | options | --> | options |
# | | | | | |
# `-----------' `-----------' `-----------'
# ,-----------. ,-----------. ,-----------.
# | root-*/ | | vhost-*/ | | |
# | templates | --> | templates | --> | templates |
# | | | | | |
# `-----------' `-----------' `-----------'
# ,-----------. ,-----------. ,-----------.
# | root-*/ | | vhost-*/ | | |
# | utilities | --> | utilities | --> | utilities |
# | | | | | |
# `-----------' `-----------' `-----------'
# ,-----------. ,-----------.
# | vhost-*/ | | |
# | cvsdb | --> | cvsdb |
# | | | |
# `-----------' `-----------'
# ,-----------. ,-----------. ,-----------.
# | root-*/ | | vhost-*/ | | |
# | authz-* | --> | authz-* | --> | authz-* |
# | | | | | |
# `-----------' `-----------' `-----------'
# ,-----------.
# | |
# | vhosts |
# | |
# `-----------'
# ,-----------.
# | |
# | query |
# | |
# `-----------'
#
# ### TODO: Figure out what this all means for the 'kv' stuff.
#
#########################################################################
class Config:
_base_sections = (
# Base configuration sections.
'authz-*',
'cvsdb',
'general',
'options',
'query',
'templates',
'utilities',
)
_force_multi_value = (
# Configuration values with multiple, comma-separated values.
'allowed_views',
'binary_mime_types',
'custom_log_formatting',
'cvs_roots',
'git_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',
)
}
_sections = ('general', 'options', 'cvsdb', 'templates')
_force_multi_value = ('cvs_roots', 'forbidden',
'svn_roots', 'languages', 'kv_files',
'root_parents')
def __init__(self):
self.__guesser = None
self.__root_configs = {}
self.__parent = None
self.root_options_overlayed = 0
for section in self._base_sections:
if section[-1] == '*':
continue
for section in self._sections:
setattr(self, section, _sub_config())
def load_config(self, pathname, vhost=None):
"""Load the configuration file at PATHNAME, applying configuration
settings there as overrides to the built-in default values. If
VHOST is provided, also process the configuration overrides
specific to that virtual host."""
self.conf_path = os.path.isfile(pathname) and pathname or None
self.base = os.path.dirname(pathname)
self.parser = ConfigParser.ConfigParser()
self.parser.optionxform = lambda x: x # don't case-normalize option names.
self.parser.read(self.conf_path or [])
for section in self.parser.sections():
if self._is_allowed_section(section, self._base_sections):
self._process_section(self.parser, section, section)
if vhost and self.parser.has_section('vhosts'):
self._process_vhost(self.parser, vhost)
parser = ConfigParser.ConfigParser()
parser.read(pathname)
for section in self._sections:
if parser.has_section(section):
self._process_section(parser, section, section)
if vhost and parser.has_section('vhosts'):
self._process_vhost(parser, vhost)
def load_kv_files(self, language):
"""Process the key/value (kv) files specified in the
configuration, merging their values into the configuration as
dotted heirarchical items."""
kv = _sub_config()
for fname in self.general.kv_files:
if fname[0] == '[':
idx = fname.index(']')
parts = fname[1:idx].split('.')
fname = fname[idx+1:].strip()
idx = string.index(fname, ']')
parts = string.split(fname[1:idx], '.')
fname = string.strip(fname[idx+1:])
else:
parts = [ ]
fname = fname.replace('%lang%', language)
fname = string.replace(fname, '%lang%', language)
parser = ConfigParser.ConfigParser()
parser.optionxform = lambda x: x # don't case-normalize option names.
parser.read(os.path.join(self.base, fname))
for section in parser.sections():
for option in parser.options(section):
@@ -207,312 +90,164 @@ class Config:
return kv
def path(self, path):
"""Return PATH relative to the config file directory."""
return os.path.join(self.base, path)
def _process_section(self, parser, section, subcfg_name):
if not hasattr(self, subcfg_name):
setattr(self, subcfg_name, _sub_config())
sc = getattr(self, subcfg_name)
for opt in parser.options(section):
value = parser.get(section, opt)
if opt in self._force_multi_value:
value = map(lambda x: x.strip(), filter(None, value.split(',')))
value = map(string.strip, filter(None, string.split(value, ',')))
else:
try:
value = int(value)
except ValueError:
pass
### FIXME: This feels like unnecessary depth of knowledge for a
### semi-generic configuration object.
if opt == 'cvs_roots' or opt == 'svn_roots' or opt == 'git_roots':
if opt == 'cvs_roots' or opt == 'svn_roots':
value = _parse_roots(opt, value)
setattr(sc, opt, value)
def _is_allowed_section(self, section, allowed_sections):
"""Return 1 iff SECTION is an allowed section, defined as being
explicitly present in the ALLOWED_SECTIONS list or present in the
form 'someprefix-*' in that list."""
for allowed_section in allowed_sections:
if allowed_section[-1] == '*':
if _startswith(section, allowed_section[:-1]):
return 1
elif allowed_section == section:
return 1
return 0
def _is_allowed_override(self, sectype, secspec, section):
"""Test if SECTION is an allowed override section for sections of
type SECTYPE ('vhosts' or 'root', currently) and type-specifier
SECSPEC (a rootname or vhostname, currently). If it is, return
the overridden base section name. If it's not an override section
at all, return None. And if it's an override section but not an
allowed one, raise IllegalOverrideSection."""
cv = '%s-%s/' % (sectype, secspec)
lcv = len(cv)
if section[:lcv] != cv:
return None
base_section = section[lcv:]
if self._is_allowed_section(base_section,
self._allowed_overrides[sectype]):
return base_section
raise IllegalOverrideSection(sectype, section)
def _process_vhost(self, parser, vhost):
# Find a vhost name for this VHOST, if any (else, we've nothing to do).
canon_vhost = self._find_canon_vhost(parser, vhost)
if not canon_vhost:
# none of the vhost sections matched
return
# Overlay any option sections associated with this vhost name.
cv = canon_vhost + '-'
lcv = len(cv)
for section in parser.sections():
base_section = self._is_allowed_override('vhost', canon_vhost, section)
if base_section:
self._process_section(parser, section, base_section)
if section[:lcv] == cv:
self._process_section(parser, section, section[lcv:])
def _find_canon_vhost(self, parser, vhost):
vhost = vhost.lower().split(':')[0] # lower-case, no port
vhost = string.lower(vhost)
# Strip (ignore) port number:
vhost = string.split(vhost, ':')[0]
for canon_vhost in parser.options('vhosts'):
value = parser.get('vhosts', canon_vhost)
patterns = map(lambda x: x.lower().strip(),
filter(None, value.split(',')))
patterns = map(string.lower, map(string.strip,
filter(None, string.split(value, ','))))
for pat in patterns:
if fnmatch.fnmatchcase(vhost, pat):
return canon_vhost
return None
def get_root_config(self, rootname):
"""Get configuration object with per-root overrides for 'rootname'"""
if self.__parent:
return self.__parent.get_root_config(rootname)
elif rootname in self.__root_configs:
return self.__root_configs[rootname]
__guesser = self.__guesser
__root_configs = self.__root_configs
parser = self.parser
self.parser = None
self.__guesser = None
self.__root_configs = None
sub = copy.deepcopy(self)
sub.parser = parser
self.parser = parser
self.__guesser = __guesser
self.__root_configs = __root_configs
self.__root_configs[rootname] = sub
sub.__parent = self
sub.overlay_root_options(rootname)
return sub
def get_parent_config(self):
"""Get the parent non-overridden config."""
if self.__parent:
return self.__parent
return self
def overlay_root_options(self, rootname):
"""Overlay per-root options for ROOTNAME atop the existing option
set. This is a destructive change to the configuration."""
did_overlay = 0
if not self.conf_path:
return
for section in self.parser.sections():
base_section = self._is_allowed_override('root', rootname, section)
if base_section:
# We can currently only deal with root overlays happening
# once, so check that we've not yet done any overlaying of
# per-root options.
assert(self.root_options_overlayed == 0)
self._process_section(self.parser, section, base_section)
did_overlay = 1
# If we actually did any overlaying, remember this fact so we
# don't do it again later.
if did_overlay:
self.root_options_overlayed = 1
def _get_parser_items(self, parser, section):
"""Basically implement ConfigParser.items() for pre-Python-2.3 versions."""
try:
return self.parser.items(section)
except AttributeError:
d = {}
for option in parser.options(section):
d[option] = parser.get(section, option)
return d.items()
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)
params['__config'] = self
return params
def guesser(self):
if not self.__guesser:
self.__guesser = ContentMagic(self.options.encodings)
return self.__guesser
def set_defaults(self):
"Set some default values in the configuration."
self.general.cvs_roots = { }
self.general.svn_roots = { }
self.general.git_roots = { }
self.general.root_parents = []
self.general.default_root = ''
self.general.mime_types_files = ["mimetypes.conf"]
self.general.address = ''
self.general.rcs_path = ''
if sys.platform == "win32":
self.general.cvsnt_exe_path = 'cvs'
else:
self.general.cvsnt_exe_path = None
self.general.use_rcsparse = 0
self.general.svn_path = ''
self.general.mime_types_file = ''
self.general.address = '<a href="mailto:user@insert.your.domain.here">No admin address has been configured</a>'
self.general.forbidden = ()
self.general.kv_files = [ ]
self.general.languages = ['en-us']
self.utilities.rcs_dir = ''
if sys.platform == "win32":
self.utilities.cvsnt = 'cvs'
else:
self.utilities.cvsnt = None
self.utilities.rcsfile_socket = ''
self.utilities.svn = ''
self.utilities.diff = ''
self.utilities.cvsgraph = ''
self.utilities.tika_server = ''
self.utilities.tika_mime_types = ''
self.options.root_as_url_component = 1
self.options.checkout_magic = 0
self.options.allowed_views = ['annotate', 'diff', 'markup', 'roots']
self.options.authorizer = None
self.options.mangle_email_addresses = 0
self.options.custom_log_formatting = []
self.options.default_file_view = "log"
self.options.binary_mime_types = []
self.options.http_expiration_time = 600
self.options.generate_etags = 1
self.options.svn_ignore_mimetype = 0
self.options.svn_config_dir = None
self.options.max_filesize_kbytes = 512
self.options.use_rcsparse = 0
self.options.sort_by = 'file'
self.options.sort_group_dirs = 1
self.options.hide_attic = 1
self.options.hide_errorful_entries = 0
self.options.log_sort = 'date'
self.options.diff_format = 'h'
self.options.hide_cvsroot = 1
self.options.hr_breakable = 1
self.options.hr_funout = 1
self.options.hr_ignore_white = 0
self.options.hr_ignore_keyword_subst = 1
self.options.hr_intraline = 0
self.options.allow_compress = 0
self.options.template_dir = "templates/default"
self.options.docroot = None
self.options.show_subdir_lastmod = 0
self.options.show_roots_lastmod = 0
self.options.show_logs = 1
self.options.show_log_in_markup = 1
self.options.cross_copies = 1
self.options.use_localtime = 0
self.options.iso8601_timestamps = 0
self.options.short_log_len = 80
self.options.enable_syntax_coloration = 1
self.options.tabsize = 8
self.options.detect_encoding = 0
self.options.use_cvsgraph = 0
self.options.cvsgraph_conf = "cvsgraph.conf"
self.options.allowed_cvsgraph_useropts = []
self.options.use_re_search = 0
self.options.dir_pagesize = 0
self.options.log_pagesize = 0
self.options.log_pagesextra = 3
self.options.limit_changes = 100
self.options.cvs_ondisk_charset = 'cp1251'
self.options.encodings = 'cp1251:iso-8859-1'
self.templates.diff = None
self.templates.directory = None
self.templates.error = None
self.templates.file = None
self.templates.graph = None
self.templates.log = None
self.templates.query = None
self.templates.diff = None
self.templates.graph = None
self.templates.annotate = None
self.templates.markup = None
self.templates.error = None
self.templates.query_form = None
self.templates.query_results = None
self.templates.roots = None
self.cvsdb.enabled = 0
self.cvsdb.index_content = 0
self.cvsdb.enable_snippets = 1
self.cvsdb.content_max_size = 0
self.cvsdb.host = ''
self.cvsdb.port = 3306
self.cvsdb.socket = ''
self.cvsdb.database_name = ''
self.cvsdb.user = ''
self.cvsdb.passwd = ''
self.cvsdb.readonly_user = ''
self.cvsdb.readonly_passwd = ''
self.cvsdb.readonly_passwd = ''
self.cvsdb.row_limit = 1000
self.cvsdb.rss_row_limit = 100
self.cvsdb.check_database_for_root = 0
self.cvsdb.fulltext_min_relevance = 0.2
self.cvsdb.sphinx_host = ''
self.cvsdb.sphinx_port = 3307
self.cvsdb.sphinx_socket = ''
self.cvsdb.sphinx_index = ''
self.cvsdb.sphinx_preformatted_mime = 'text/(?!html|xml).*'
self.cvsdb.sphinx_snippet_options = \
'around: 15\n'\
'limit: 200\n'\
'before_match: <span style="color:red">\n'\
'after_match: </span>\n'\
'chunk_separator: ... \n\n'
self.options.root_as_url_component = 0
self.options.default_file_view = "log"
self.options.checkout_magic = 0
self.options.sort_by = 'file'
self.options.sort_group_dirs = 1
self.options.hide_attic = 1
self.options.log_sort = 'date'
self.options.diff_format = 'h'
self.options.hide_cvsroot = 1
self.options.hr_breakable = 1
self.options.hr_funout = 1
self.options.hr_ignore_white = 1
self.options.hr_ignore_keyword_subst = 1
self.options.hr_intraline = 0
self.options.allow_annotate = 1
self.options.allow_markup = 1
self.options.allow_compress = 1
self.options.template_dir = "templates"
self.options.docroot = None
self.options.show_subdir_lastmod = 0
self.options.show_logs = 1
self.options.show_log_in_markup = 1
self.options.cross_copies = 0
self.options.py2html_path = '.'
self.options.short_log_len = 80
self.options.use_enscript = 0
self.options.enscript_path = ''
self.options.use_highlight = 0
self.options.highlight_path = ''
self.options.highlight_line_numbers = 1
self.options.highlight_convert_tabs = 2
self.options.use_php = 0
self.options.php_exe_path = 'php'
self.options.allow_tar = 0
self.options.use_cvsgraph = 0
self.options.cvsgraph_path = ''
self.options.cvsgraph_conf = "cvsgraph.conf"
self.options.use_re_search = 0
self.options.use_pagesize = 0
self.options.limit_changes = 100
self.options.use_localtime = 0
self.options.http_expiration_time = 600
self.options.generate_etags = 1
def is_forbidden(self, module):
if not module:
return 0
default = 0
for pat in self.general.forbidden:
if pat[0] == '!':
default = 1
if fnmatch.fnmatchcase(module, pat[1:]):
return 0
elif fnmatch.fnmatchcase(module, pat):
return 1
return default
self.query.viewvc_base_url = None
def _startswith(somestr, substr):
return somestr[:len(substr)] == substr
def _parse_roots(config_name, config_value):
roots = { }
for root in config_value:
try:
name, path = root.split(':', 1)
except:
pos = string.find(root, ':')
if pos < 0:
raise MalformedRoot(config_name, root)
roots[name.strip()] = path.strip()
name, path = map(string.strip, (root[:pos], root[pos+1:]))
roots[name] = path
return roots
class ViewVCConfigurationError(Exception):
pass
class IllegalOverrideSection(ViewVCConfigurationError):
def __init__(self, override_type, section_name):
self.section_name = section_name
self.override_type = override_type
def __str__(self):
return "malformed configuration: illegal %s override section: %s" \
% (self.override_type, self.section_name)
class MalformedRoot(ViewVCConfigurationError):
class MalformedRoot(Exception):
def __init__(self, config_name, value_given):
Exception.__init__(self, config_name, value_given)
self.config_name = config_name
@@ -524,3 +259,10 @@ class MalformedRoot(ViewVCConfigurationError):
class _sub_config:
pass
if not hasattr(sys, 'hexversion'):
# Python 1.5 or 1.5.1. fix the syntax for ConfigParser options.
import regex
ConfigParser.option_cre = regex.compile('^\([-A-Za-z0-9._]+\)\(:\|['
+ string.whitespace
+ ']*=\)\(.*\)$')

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -14,7 +14,7 @@ import sys
import time
import types
import re
import calendar
import compat
import MySQLdb
# set to 1 to store commit times in UTC, or 0 to use the ViewVC machine's
@@ -55,17 +55,9 @@ def TicksFromDateTime(datetime):
t = datetime.tuple()
if utc_time:
return calendar.timegm(t)
return compat.timegm(t)
else:
return time.mktime(t[:8] + (-1,))
def connect(host, port, socket, user, passwd, db, charset = 'utf8'):
return MySQLdb.connect(
host = host,
port = port,
unix_socket = socket,
user = user,
passwd = passwd,
db = db,
charset = charset,
use_unicode = charset == 'utf8')
def connect(host, port, user, passwd, db):
return MySQLdb.connect(host=host, port=port, user=user, passwd=passwd, db=db)

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -17,20 +17,10 @@
import sys
# Set to non-zero to track and print processing times
SHOW_TIMES = 0
# Set to non-zero to display child process info
SHOW_CHILD_PROCESSES = 0
# Set to a server-side path to force the tarball view to generate the
# tarball as a file on the server, instead of transmitting the data
# back to the browser. This enables easy display of error
# considitions in the browser, as well as tarball inspection on the
# server. NOTE: The file will be a TAR archive, *not* gzip-compressed.
TARFILE_PATH = ''
if SHOW_TIMES:
import time
@@ -48,17 +38,13 @@ if SHOW_TIMES:
else:
_times[which] = t
def t_dump(out):
out.write('<div>')
names = _times.keys()
names.sort()
for name in names:
out.write('%s: %.6fs<br/>\n' % (name, _times[name]))
out.write('</div>')
def dump():
for name, value in _times.items():
print '%s: %.6f<br />' % (name, value)
else:
t_start = t_end = t_dump = lambda *args: None
t_start = t_end = dump = lambda *args: None
class ViewVCException:

View File

@@ -189,10 +189,9 @@ Directives
templates are escaped before they are put into the output stream. It
has no effect on the literal text of the templates, only the output
from [QUAL_NAME ...] directives. STRING can be one of "raw" "html"
"xml" or "uri". The "raw" mode leaves the output unaltered; the "html"
and "xml" modes escape special characters using entity escapes (like
&quot; and &gt;); the "uri" mode escapes characters using hexadecimal
escape sequences (like %20 and %7e).
or "xml". The "raw" mode leaves the output unaltered. The "html" and
"xml" modes escape special characters using entity escapes (like
&quot; and &gt;)
[format CALLBACK]
@@ -201,7 +200,7 @@ Directives
equivalent to "[CALLBACK QUAL_NAME]"
"""
#
# Copyright (C) 2001-2007 Greg Stein. All Rights Reserved.
# Copyright (C) 2001-2005 Greg Stein. All Rights Reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
@@ -236,7 +235,6 @@ import re
from types import StringType, IntType, FloatType, LongType, TupleType
import os
import cgi
import urllib
try:
import cStringIO
except ImportError:
@@ -249,7 +247,6 @@ except ImportError:
FORMAT_RAW = 'raw'
FORMAT_HTML = 'html'
FORMAT_XML = 'xml'
FORMAT_URI = 'uri'
#
# This regular expression matches three alternatives:
@@ -347,7 +344,7 @@ class Template:
for_names = [ ]
if base_format:
program.append((self._cmd_format, _formatters[base_format]))
program.append((self._cmd_format, _printers[base_format]))
for i in range(len(parts)):
piece = parts[i]
@@ -405,13 +402,13 @@ class Template:
elif cmd == 'format':
if args[1][0]:
# argument is a variable reference
formatter = args[1]
printer = args[1]
else:
# argument is a string constant referring to built-in formatter
formatter = _formatters.get(args[1][1])
if not formatter:
# argument is a string constant referring to built-in printer
printer = _printers.get(args[1][1])
if not printer:
raise UnknownFormatConstantError(str(args[1:]))
program.append((self._cmd_format, formatter))
program.append((self._cmd_format, printer))
# remember the cmd, current pos, args, and a section placeholder
stack.append([cmd, len(program), args[1:], None])
@@ -460,18 +457,15 @@ class Template:
def _cmd_print(self, valrefs, ctx):
value = _get_value(valrefs[0], ctx)
args = map(lambda valref, ctx=ctx: _get_value(valref, ctx), valrefs[1:])
try:
_write_value(value, args, ctx)
except TypeError:
raise Exception("Unprintable value type for '%s'" % (str(valrefs[0][0])))
_write_value(value, args, ctx)
def _cmd_format(self, formatter, ctx):
if type(formatter) is TupleType:
formatter = _get_value(formatter, ctx)
ctx.formatters.append(formatter)
def _cmd_format(self, printer, ctx):
if type(printer) is TupleType:
printer = _get_value(printer, ctx)
ctx.printers.append(printer)
def _cmd_end_format(self, valref, ctx):
ctx.formatters.pop()
ctx.printers.pop()
def _cmd_include(self, (valref, reader), ctx):
fname = _get_value(valref, ctx)
@@ -525,8 +519,7 @@ class Template:
((valref,), unused, section) = args
list = _get_value(valref, ctx)
if isinstance(list, StringType):
raise NeedSequenceError("The value of '%s' is not a sequence"
% (valref[0]))
raise NeedSequenceError()
refname = valref[0]
ctx.for_iterators[refname] = iterator = _iter(list)
for unused in iterator:
@@ -637,23 +630,14 @@ def _get_value((refname, start, rest), ctx):
# string or a sequence
return ob
def _print_formatted(formatters, ctx, chunk):
# print chunk to ctx.fp after running it sequentially through formatters
for formatter in formatters:
chunk = formatter(chunk)
ctx.fp.write(chunk)
def _write_value(value, args, ctx):
# value is a callback function, generates its own output
if callable(value):
apply(value, [ctx] + list(args))
return
# squirrel away formatters in case one of them recursively calls
# _write_value() -- we'll use them (in reverse order) to format our
# output.
formatters = ctx.formatters[:]
formatters.reverse()
# pop printer in case it recursively calls _write_value
printer = ctx.printers.pop()
try:
# if the value has a 'read' attribute, then it is a stream: copy it
@@ -662,7 +646,7 @@ def _write_value(value, args, ctx):
chunk = value.read(16384)
if not chunk:
break
_print_formatted(formatters, ctx, chunk)
printer(ctx, chunk)
# value is a substitution pattern
elif args:
@@ -675,23 +659,21 @@ def _write_value(value, args, ctx):
piece = args[idx]
else:
piece = '<undef>'
_print_formatted(formatters, ctx, piece)
printer(ctx, piece)
# plain old value, write to output
else:
_print_formatted(formatters, ctx, value)
printer(ctx, value)
finally:
# restore our formatters
formatters.reverse()
ctx.formatters = formatters
ctx.printers.append(printer)
class Context:
"""A container for the execution context"""
def __init__(self, fp):
self.fp = fp
self.formatters = []
self.printers = []
def write(self, value, args=()):
_write_value(value, args, self)
@@ -804,34 +786,16 @@ class BaseUnavailableError(EZTException):
class UnknownFormatConstantError(EZTException):
"""The format specifier is an unknown value."""
def _raw_formatter(s):
try: s = s.encode('utf-8')
except: pass
return s
def _raw_printer(ctx, s):
ctx.fp.write(s)
def _html_printer(ctx, s):
ctx.fp.write(cgi.escape(s))
def _html_formatter(s):
try: s = s.encode('utf-8')
except: pass
return cgi.escape(s)
def _xml_formatter(s):
try: s = s.encode('utf-8')
except: pass
s = s.replace('&', '&#x26;')
s = s.replace('<', '&#x3C;')
s = s.replace('>', '&#x3E;')
return s
def _uri_formatter(s):
try: s = s.encode('utf-8')
except: pass
return urllib.quote(s)
_formatters = {
FORMAT_RAW : _raw_formatter,
FORMAT_HTML : _html_formatter,
FORMAT_XML : _xml_formatter,
FORMAT_URI : _uri_formatter,
_printers = {
FORMAT_RAW : _raw_printer,
FORMAT_HTML : _html_printer,
FORMAT_XML : _html_printer,
}
# --- standard test environment ---
@@ -849,7 +813,7 @@ def test_parse():
['', '["a \\"b[foo]" c.d f]', None, '']
def _test(argv):
import doctest, ezt
import doctest, ezt
verbose = "-v" in argv
return doctest.testmod(ezt, verbose=verbose)

View File

@@ -1,283 +0,0 @@
# -*-python-*-
# -----------------------------------------------------------------------
# Simple Global Authentication client for ViewVC
# License: GPLv2+
# Author: Vitaliy Filippov
# -----------------------------------------------------------------------
#
# USAGE:
#
# import globalauth
# c = globalauth.GlobalAuthClient()
# c.auth(server)
# user_name = c.user_name
# user_url = c.user_url
#
# auth() will call sys.exit() when it needs to stop request processing
#
# -----------------------------------------------------------------------
import os
import re
import sys
import struct
import cgi
import binascii
import time
import datetime
import urllib
import urllib2
import anyjson
import random
import Cookie
import ga_config
class FileCache:
def __init__(self, dir):
self.dir = dir
if not os.path.isdir(dir):
os.mkdir(dir)
def fn(self, key):
key = re.sub('([^a-zA-Z0-9_\-]+)', lambda x: binascii.hexlify(x.group(1)), key)
return self.dir+'/'+key
def clean(self):
t = time.time()
for fn in os.listdir(self.dir):
if t > os.stat(self.dir+'/'+fn).st_mtime:
os.unlink(self.dir+'/'+fn)
def set(self, key, value, expire = 86400):
fn = self.fn(key)
try:
f = open(fn,'w')
if not expire:
expire = 86400
expire = time.time()+expire
f.write(value)
f.close()
os.chmod(fn, 0600)
os.utime(fn, (expire, expire))
except:
raise
return 1
def get(self, key):
fn = self.fn(key)
try:
f = open(fn,'r')
value = f.read()
f.close()
if time.time() > os.stat(fn).st_mtime:
os.unlink(fn)
return ''
return value
except:
pass
return ''
def delete(self, key):
fn = self.fn(key)
try:
os.unlink(fn)
except:
pass
class GlobalAuthClient:
wd = { 0 : 'Mon', 1 : 'Tue', 2 : 'Wed', 3 : 'Thu', 4 : 'Fri', 5 : 'Sat', 6 : 'Sun' }
ms = { 1 : 'Jan', 2 : 'Feb', 3 : 'Mar', 4 : 'Apr', 5 : 'May', 6 : 'Jun', 7 : 'Jul', 8 : 'Aug', 9 : 'Sep', 10 : 'Oct', 11 : 'Nov', 12 : 'Dec' }
def __init__(self, server):
self.server = server
self.v = {}
for name, values in server.params().items():
self.v[name] = values[0]
fs = server.FieldStorage()
for name in fs:
self.v[name] = fs[name].value
self.cookies = Cookie.SimpleCookie()
# '' default value is needed here - else we die here under WSGI without any exception O_o
self.cookies.load(self.server.getenv('HTTP_COOKIE', ''))
self.user_name = ''
self.user_url = ''
if not ga_config.gac.get('globalauth_server', '') and not ga_config.gac.get('fof_sudo_server', ''):
raise Exception('ga_config.gac must contain at least globalauth_server="URL" or fof_sudo_server="URL"')
self.gac = {
'cookie_name' : 'simple_global_auth',
'cookie_expire' : 86400*7,
'cookie_path' : '/',
'cookie_domain' : '',
'globalauth_server' : '',
'cache_dir' : os.path.abspath(os.path.dirname(__file__))+'/cache',
'cut_email_at' : 0,
'ga_always_require' : 0,
'fof_sudo_server' : '',
'fof_sudo_cookie' : 'fof_sudo_id',
'gc_probability' : 20,
}
for i in self.gac:
if ga_config.gac.get(i, None) is not None:
self.gac[i] = ga_config.gac[i]
self.cache = FileCache(self.gac['cache_dir'])
def auth(self):
if self.gac['fof_sudo_server']:
self.auth_fof_sudo()
if not self.user_name and self.gac['globalauth_server']:
self.auth_ga()
def auth_ga(self):
i = random.randint(1, self.gac['gc_probability'])
if i == 1:
self.cache.clean()
r_id = self.cookies.get(self.gac['cookie_name'], '')
if r_id:
r_id = r_id.value
ga_id = self.v.get('ga_id', '')
if self.v.get('ga_client', ''):
self.ga_client(r_id, ga_id)
return
r_data = ''
if r_id == 'nologin':
r_data = 'nologin'
elif r_id != '':
r_data = self.cache.get('D'+r_id)
if r_data != 'nologin':
try: r_data = anyjson.deserialize(r_data)
except: r_data = ''
is_browser = re.match('opera|firefox|chrome|safari', self.server.getenv('HTTP_USER_AGENT'), re.I)
if not r_data and (is_browser or self.gac['ga_always_require']) or self.v.get('ga_require', None):
self.ga_begin()
elif r_data and r_data != 'nologin':
self.set_user(r_data)
def ga_client(self, r_id, ga_id):
ga_key = self.v.get('ga_key', '')
if ga_key and ga_key == self.cache.get('K'+ga_id):
# Server-to-server request
self.cache.delete('K'+ga_id)
data = ''
if self.v.get('ga_nologin','') != '':
data = 'nologin'
else:
try: data = anyjson.deserialize(self.v.get('ga_data',''))
except: raise
if data != '':
if data != 'nologin':
data = anyjson.serialize(data)
self.cache.set('D'+ga_id, data)
self.server.header('text/plain')
self.server.write('1')
sys.exit()
elif ga_key == '' and r_id != ga_id:
# User redirect with different key
d = self.cache.get('D'+ga_id)
if d != 'nologin' and d != '':
try: d = anyjson.deserialize(d)
except: d = ''
if d != '':
self.setcookie(ga_id)
self.redirect(self.clean_uri())
self.server.header('text/plain', status=404)
self.server.write('GlobalAuth key doesn\'t match')
sys.exit()
def ga_begin(self):
ga_id = binascii.hexlify(os.urandom(16))
ga_key = binascii.hexlify(os.urandom(16))
url = self.add_param(self.gac['globalauth_server'], '')
try:
resp = urllib2.urlopen(url+'ga_id='+urllib2.quote(ga_id)+'&ga_key='+urllib2.quote(ga_key))
resp.read()
if resp.code != 200:
raise Exception(resp)
except:
self.setcookie('nologin')
self.redirect(self.clean_uri())
return_uri = 'http://'+self.server.getenv('HTTP_HOST')+self.server.getenv('REQUEST_URI')
return_uri = self.add_param(return_uri, 'ga_client=1')
self.cache.set('K'+ga_id, ga_key)
url = url+'ga_id='+urllib2.quote(ga_id)+'&ga_url='+urllib2.quote(return_uri)
if self.v.get('ga_require', '') == '' and not self.gac['ga_always_require']:
url = url+'&ga_check=1'
self.redirect(url)
def add_param(self, url, param):
if url.find('?') != -1:
url = url+'&'
else:
url = url+'?'
return url+param
def auth_fof_sudo(self):
sudo_id = self.cookies.get(self.gac['fof_sudo_cookie'], '')
if sudo_id:
sudo_id = sudo_id.value
if sudo_id != '':
url = self.gac['fof_sudo_server']
if url.find('?') != -1:
url = url+'&'
else:
url = url+'?'
try:
resp = urllib2.urlopen(url+'id='+urllib2.quote(sudo_id))
d = resp.read()
if resp.code != 200:
raise Exception(resp)
d = anyjson.deserialize(d)
self.set_user(d)
except:
pass
def log(self, s):
sys.stderr.write(s+"\n")
sys.stderr.flush()
def redirect(self, url):
self.server.addheader('Location', url)
self.server.header(status='302 Moved Temporarily')
self.server.write('This document is located <a href="%s">here</a>.' % url)
sys.exit()
def setcookie(self, value):
dom = self.gac['cookie_domain']
if not dom:
dom = self.server.getenv('HTTP_HOST')
exp = ''
if self.gac['cookie_expire'] > 0:
tm = int(time.time()+self.gac['cookie_expire'])
tm = datetime.datetime.utcfromtimestamp(tm)
tm = "%s, %02d-%s-%04d %02d:%02d:%02d GMT" % (self.wd[tm.weekday()], tm.day, self.ms[tm.month], tm.year, tm.hour, tm.minute, tm.second)
exp = '; expires='+tm
self.server.addheader('Set-Cookie', "%s=%s; path=%s; domain=%s%s" % (self.gac['cookie_name'], value, self.gac['cookie_path'], dom, exp))
def clean_uri(self):
uriargs = self.v.copy()
for i in [ 'ga_id', 'ga_res', 'ga_key', 'ga_client', 'ga_nologin', 'ga_require' ]:
uriargs.pop(i, None)
uri = self.server.getenv('REQUEST_URI')
p = uri.find('?')
if p != -1:
uri = uri[0:p]
uri = 'http://'+self.server.getenv('HTTP_HOST')+uri+'?'+urllib.urlencode(uriargs)
return uri
def set_user(self, r_data):
r_email = r_data.get('user_email', '').encode('utf-8')
r_url = r_data.get('user_url', '').encode('utf-8')
if self.gac['cut_email_at']:
p = r_email.find('@')
if p != -1:
r_email = r_email[0:p]
self.user_name = r_email
self.user_url = r_url

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -19,10 +19,8 @@ from __future__ import generators
import difflib
import sys
import re
from common import _item, _RCSDIFF_NO_CHANGES
import ezt
import sapi
import cgi
def sidebyside(fromlines, tolines, context):
"""Generate side by side diff"""
@@ -31,7 +29,6 @@ def sidebyside(fromlines, tolines, context):
line_strip = lambda line: line.rstrip("\n")
fromlines = map(line_strip, fromlines)
tolines = map(line_strip, tolines)
had_changes = 0
gap = False
for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context):
@@ -40,11 +37,8 @@ def sidebyside(fromlines, tolines, context):
else:
from_item = _mdiff_split(flag, fromdata)
to_item = _mdiff_split(flag, todata)
had_changes = 1
yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item), type="intraline")
yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item))
gap = False
if not had_changes:
yield _item(type=_RCSDIFF_NO_CHANGES)
_re_mdiff = re.compile("\0([+-^])(.*?)\1")
@@ -55,18 +49,18 @@ def _mdiff_split(flag, (line_number, text)):
while True:
m = _re_mdiff.search(text, pos)
if not m:
segments.append(_item(text=sapi.escape(text[pos:]), type=None))
segments.append(_item(text=cgi.escape(text[pos:]), type=None))
break
if m.start() > pos:
segments.append(_item(text=sapi.escape(text[pos:m.start()]), type=None))
segments.append(_item(text=cgi.escape(text[pos:m.start()]), type=None))
if m.group(1) == "+":
segments.append(_item(text=sapi.escape(m.group(2)), type="add"))
segments.append(_item(text=cgi.escape(m.group(2)), type="add"))
elif m.group(1) == "-":
segments.append(_item(text=sapi.escape(m.group(2)), type="remove"))
segments.append(_item(text=cgi.escape(m.group(2)), type="remove"))
elif m.group(1) == "^":
segments.append(_item(text=sapi.escape(m.group(2)), type="change"))
segments.append(_item(text=cgi.escape(m.group(2)), type="change"))
pos = m.end()
@@ -77,26 +71,19 @@ def unified(fromlines, tolines, context):
diff = difflib.Differ().compare(fromlines, tolines)
lastrow = None
had_changes = 0
for row in _trim_context(diff, context):
if row[0].startswith("? "):
had_changes = 1
yield _differ_split(lastrow, row[0])
lastrow = None
else:
if lastrow:
had_changes = 1
yield _differ_split(lastrow, None)
lastrow = row
if lastrow:
had_changes = 1
yield _differ_split(lastrow, None)
if not had_changes:
yield _item(type=_RCSDIFF_NO_CHANGES)
def _trim_context(lines, context_size):
"""Trim context lines that don't surround changes from Differ results
@@ -179,16 +166,20 @@ def _differ_split(row, guide):
for m in _re_differ.finditer(guide, pos):
if m.start() > pos:
segments.append(_item(text=sapi.escape(line[pos:m.start()]), type=None))
segments.append(_item(text=sapi.escape(line[m.start():m.end()]),
segments.append(_item(text=cgi.escape(line[pos:m.start()]), type=None))
segments.append(_item(text=cgi.escape(line[m.start():m.end()]),
type="change"))
pos = m.end()
segments.append(_item(text=sapi.escape(line[pos:]), type=None))
segments.append(_item(text=cgi.escape(line[pos:]), type=None))
return _item(gap=ezt.boolean(gap), type=type, segments=segments,
left_number=left_number, right_number=right_number)
class _item:
def __init__(self, **kw):
vars(self).update(kw)
try:
### Using difflib._mdiff function here was the easiest way of obtaining
### intraline diffs for use in ViewVC, but it doesn't exist prior to

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -23,6 +23,7 @@ import os
import sys
import sapi
import threading
import string
if sys.platform == "win32":
import win32popen
@@ -35,7 +36,7 @@ def popen(cmd, args, mode, capture_err=1):
if sys.platform == "win32":
command = win32popen.CommandLine(cmd, args)
if mode.find('r') >= 0:
if string.find(mode, 'r') >= 0:
hStdIn = None
if debug.SHOW_CHILD_PROCESSES:
@@ -84,7 +85,7 @@ def popen(cmd, args, mode, capture_err=1):
# in the parent
# close the descriptor that we don't need and return the other one.
if mode.find('r') >= 0:
if string.find(mode, 'r') >= 0:
os.close(w)
return _pipe(os.fdopen(r, mode), pid)
os.close(r)
@@ -95,7 +96,7 @@ def popen(cmd, args, mode, capture_err=1):
# we'll need /dev/null for the discarded I/O
null = os.open('/dev/null', os.O_RDWR)
if mode.find('r') >= 0:
if string.find(mode, 'r') >= 0:
# hook stdout/stderr to the "write" channel
os.dup2(w, 1)
# "close" stdin; the child shouldn't use it
@@ -124,12 +125,200 @@ def popen(cmd, args, mode, capture_err=1):
os.execvp(cmd, (cmd,) + tuple(args))
except:
# aid debugging, if the os.execvp above fails for some reason:
print "<h2>exec failed:</h2><pre>", cmd, ' '.join(args), "</pre>"
print "<h2>exec failed:</h2><pre>", cmd, string.join(args), "</pre>"
raise
# crap. shouldn't be here.
sys.exit(127)
def pipe_cmds(cmds, out=None):
"""Executes a sequence of commands. The output of each command is directed to
the input of the next command. A _pipe object is returned for writing to the
first command's input. The output of the last command is directed to the
"out" file object or the standard output if "out" is None. If "out" is not an
OS file descriptor, a separate thread will be spawned to send data to its
write() method."""
if out is None:
out = sys.stdout
if sys.platform == "win32":
### FIXME: windows implementation ignores "out" argument, always
### writing last command's output to standard out
if debug.SHOW_CHILD_PROCESSES:
dbgIn = StringIO.StringIO()
hStdIn, handle = win32popen.MakeSpyPipe(1, 0, (dbgIn,))
i = 0
for cmd in cmds:
i = i + 1
dbgOut, dbgErr = StringIO.StringIO(), StringIO.StringIO()
if i < len(cmds):
nextStdIn, hStdOut = win32popen.MakeSpyPipe(1, 1, (dbgOut,))
x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,))
else:
ehandle = win32event.CreateEvent(None, 1, 0, None)
nextStdIn, hStdOut = win32popen.MakeSpyPipe(None, 1, (dbgOut, sapi.server.file()), ehandle)
x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,))
command = win32popen.CommandLine(cmd[0], cmd[1:])
phandle, pid, thandle, tid = win32popen.CreateProcess(command, hStdIn, hStdOut, hStdErr)
if debug.SHOW_CHILD_PROCESSES:
debug.Process(command, dbgIn, dbgOut, dbgErr)
dbgIn = dbgOut
hStdIn = nextStdIn
else:
hStdIn, handle = win32popen.CreatePipe(1, 0)
spool = None
i = 0
for cmd in cmds:
i = i + 1
if i < len(cmds):
nextStdIn, hStdOut = win32popen.CreatePipe(1, 1)
else:
# very last process
nextStdIn = None
if sapi.server.inheritableOut:
# send child output to standard out
hStdOut = win32popen.MakeInheritedHandle(win32popen.FileObject2File(sys.stdout),0)
ehandle = None
else:
ehandle = win32event.CreateEvent(None, 1, 0, None)
x, hStdOut = win32popen.MakeSpyPipe(None, 1, (sapi.server.file(),), ehandle)
command = win32popen.CommandLine(cmd[0], cmd[1:])
phandle, pid, thandle, tid = win32popen.CreateProcess(command, hStdIn, hStdOut, None)
hStdIn = nextStdIn
return _pipe(win32popen.File2FileObject(handle, 'wb'), phandle, ehandle)
# flush the stdio buffers since we are about to change the FD under them
sys.stdout.flush()
sys.stderr.flush()
prev_r, parent_w = os.pipe()
null = os.open('/dev/null', os.O_RDWR)
child_pids = []
for cmd in cmds[:-1]:
r, w = os.pipe()
pid = os.fork()
if not pid:
# in the child
# hook up stdin to the "read" channel
os.dup2(prev_r, 0)
# hook up stdout to the output channel
os.dup2(w, 1)
# toss errors
os.dup2(null, 2)
# close these extra descriptors
os.close(prev_r)
os.close(parent_w)
os.close(null)
os.close(r)
os.close(w)
# time to run the command
try:
os.execvp(cmd[0], cmd)
except:
pass
sys.exit(127)
# in the parent
child_pids.append(pid)
# we don't need these any more
os.close(prev_r)
os.close(w)
# the read channel of this pipe will feed into to the next command
prev_r = r
# no longer needed
os.close(null)
# done with most of the commands. set up the last command to write to "out"
if not hasattr(out, 'fileno'):
r, w = os.pipe()
pid = os.fork()
if not pid:
# in the child (the last command)
# hook up stdin to the "read" channel
os.dup2(prev_r, 0)
# hook up stdout to "out"
if hasattr(out, 'fileno'):
if out.fileno() != 1:
os.dup2(out.fileno(), 1)
out.close()
else:
# "out" can't be hooked up directly, so use a pipe and a thread
os.dup2(w, 1)
os.close(r)
os.close(w)
# close these extra descriptors
os.close(prev_r)
os.close(parent_w)
# run the last command
try:
os.execvp(cmds[-1][0], cmds[-1])
except:
pass
sys.exit(127)
child_pids.append(pid)
# not needed any more
os.close(prev_r)
if not hasattr(out, 'fileno'):
os.close(w)
thread = _copy(r, out)
thread.start()
else:
thread = None
# write into the first pipe, wait on the final process
return _pipe(os.fdopen(parent_w, 'w'), child_pids, thread=thread)
class _copy(threading.Thread):
def __init__(self, srcfd, destfile):
self.srcfd = srcfd
self.destfile = destfile
threading.Thread.__init__(self)
def run(self):
try:
while 1:
s = os.read(self.srcfd, 1024)
if not s:
break
self.destfile.write(s)
finally:
os.close(self.srcfd)
class _pipe:
"Wrapper for a file which can wait() on a child process at close time."
@@ -175,12 +364,12 @@ class _pipe:
else:
if self.thread:
self.thread.join()
if type(self.child_pid) == type([]):
if type(self.child_pid) == type([]):
for pid in self.child_pid:
exit = os.waitpid(pid, 0)[1]
return exit
else:
return os.waitpid(self.child_pid, 0)[1]
else:
return os.waitpid(self.child_pid, 0)[1]
return None
def __getattr__(self, name):

View File

@@ -1,154 +0,0 @@
# -*- coding: utf-8 -*-
"""
pygments.lexers.sp4
~~~~~~~~~~~~~~~~~~~~~
Lexer for CustIS m4-preprocessed PL/SQL.
:copyright: 2009+ by Vitaliy Filippov <vitalif@mail.ru>.
:license: BSD, see LICENSE for more details.
,
'Sp4Lexer': ('pygments.lexers.sp4', 'SP4', ('sp4',), ('*.sp4'), ('text/x-sp4',))
"""
import re
from pygments.lexer import Lexer, RegexLexer, include, bygroups, using, this, \
do_insertions
from pygments.token import Error, Punctuation, \
Text, Comment, Operator, Keyword, Name, String, Number, Generic
from pygments.util import shebang_matches
__all__ = ['Sp4Lexer']
line_re = re.compile('.*?\n')
class Sp4Lexer(RegexLexer):
name = 'SP4'
aliases = ['sp4']
filenames = ['*.sp4']
mimetypes = ['text/x-sp4']
flags = re.IGNORECASE
tokens = {
'root': [
(r'\s+', Text),
(r'--.*?\n', Comment.Single),
(r'/\*', Comment.Multiline, 'multiline-comments'),
(r'(ABORT|ABS|ABSOLUTE|ACCESS|ADA|ADD|ADMIN|AFTER|AGGREGATE|'
r'ALIAS|ALL|ALLOCATE|ALTER|ANALYSE|ANALYZE|AND|ANY|ARE|AS|'
r'ASC|ASENSITIVE|ASSERTION|ASSIGNMENT|ASYMMETRIC|AT|ATOMIC|'
r'AUTHORIZATION|AVG|BACKWARD|BEFORE|BEGIN|BETWEEN|BITVAR|'
r'BIT_LENGTH|BOTH|BREADTH|BY|C|CACHE|CALL|CALLED|CARDINALITY|'
r'CASCADE|CASCADED|CASE|CAST|CATALOG|CATALOG_NAME|CHAIN|'
r'CHARACTERISTICS|CHARACTER_LENGTH|CHARACTER_SET_CATALOG|'
r'CHARACTER_SET_NAME|CHARACTER_SET_SCHEMA|CHAR_LENGTH|CHECK|'
r'CHECKED|CHECKPOINT|CLASS|CLASS_ORIGIN|CLOB|CLOSE|CLUSTER|'
r'COALSECE|COBOL|COLLATE|COLLATION|COLLATION_CATALOG|'
r'COLLATION_NAME|COLLATION_SCHEMA|COLUMN|COLUMN_NAME|'
r'COMMAND_FUNCTION|COMMAND_FUNCTION_CODE|COMMENT|COMMIT|'
r'COMMITTED|COMPLETION|CONDITION_NUMBER|CONNECT|CONNECTION|'
r'CONNECTION_NAME|CONSTRAINT|CONSTRAINTS|CONSTRAINT_CATALOG|'
r'CONSTRAINT_NAME|CONSTRAINT_SCHEMA|CONSTRUCTOR|CONTAINS|'
r'CONTINUE|CONVERSION|CONVERT|COPY|CORRESPONTING|COUNT|'
r'CREATE|CREATEDB|CREATEUSER|CROSS|CUBE|CURRENT|CURRENT_DATE|'
r'CURRENT_PATH|CURRENT_ROLE|CURRENT_TIME|CURRENT_TIMESTAMP|'
r'CURRENT_USER|CURSOR|CURSOR_NAME|CYCLE|DATA|DATABASE|'
r'DATETIME_INTERVAL_CODE|DATETIME_INTERVAL_PRECISION|DAY|'
r'DEALLOCATE|DECLARE|DEFAULT|DEFAULTS|DEFERRABLE|DEFERRED|'
r'DEFINED|DEFINER|DELETE|DELIMITER|DELIMITERS|DEREF|DESC|'
r'DESCRIBE|DESCRIPTOR|DESTROY|DESTRUCTOR|DETERMINISTIC|'
r'DIAGNOSTICS|DICTIONARY|DISCONNECT|DISPATCH|DISTINCT|DO|'
r'DOMAIN|DROP|DYNAMIC|DYNAMIC_FUNCTION|DYNAMIC_FUNCTION_CODE|'
r'EACH|ELSE|ENCODING|ENCRYPTED|END|END-EXEC|EQUALS|ESCAPE|EVERY|'
r'EXCEPT|ESCEPTION|EXCLUDING|EXCLUSIVE|EXEC|EXECUTE|EXISTING|'
r'EXISTS|EXPLAIN|EXTERNAL|EXTRACT|FALSE|FETCH|FINAL|FIRST|FOR|'
r'FORCE|FOREIGN|FORTRAN|FORWARD|FOUND|FREE|FREEZE|FROM|FULL|'
r'FUNCTION|G|GENERAL|GENERATED|GET|GLOBAL|GO|GOTO|GRANT|GRANTED|'
r'GROUP|GROUPING|HANDLER|HAVING|HIERARCHY|HOLD|HOST|IDENTITY|'
r'IGNORE|ILIKE|IMMEDIATE|IMMUTABLE|IMPLEMENTATION|IMPLICIT|IN|'
r'INCLUDING|INCREMENT|INDEX|INDITCATOR|INFIX|INHERITS|INITIALIZE|'
r'INITIALLY|INNER|INOUT|INPUT|INSENSITIVE|INSERT|INSTANTIABLE|'
r'INSTEAD|INTERSECT|INTO|INVOKER|IS|ISNULL|ISOLATION|ITERATE|JOIN|'
r'K|KEY|KEY_MEMBER|KEY_TYPE|LANCOMPILER|LANGUAGE|LARGE|LAST|'
r'LATERAL|LEADING|LEFT|LENGTH|LESS|LEVEL|LIKE|LILMIT|LISTEN|LOAD|'
r'LOCAL|LOCALTIME|LOCALTIMESTAMP|LOCATION|LOCATOR|LOCK|LOWER|M|'
r'MAP|MATCH|MAX|MAXVALUE|MESSAGE_LENGTH|MESSAGE_OCTET_LENGTH|'
r'MESSAGE_TEXT|METHOD|MIN|MINUTE|MINVALUE|MOD|MODE|MODIFIES|'
r'MODIFY|MONTH|MORE|MOVE|MUMPS|NAMES|NATIONAL|NATURAL|NCHAR|'
r'NCLOB|NEW|NEXT|NO|NOCREATEDB|NOCREATEUSER|NONE|NOT|NOTHING|'
r'NOTIFY|NOTNULL|NULL|NULLABLE|NULLIF|OBJECT|OCTET_LENGTH|OF|OFF|'
r'OFFSET|OIDS|OLD|ON|ONLY|OPEN|OPERATION|OPERATOR|OPTION|OPTIONS|'
r'OR|ORDER|ORDINALITY|OUT|OUTER|OUTPUT|OVERLAPS|OVERLAY|OVERRIDING|'
r'OWNER|PAD|PARAMETER|PARAMETERS|PARAMETER_MODE|PARAMATER_NAME|'
r'PARAMATER_ORDINAL_POSITION|PARAMETER_SPECIFIC_CATALOG|'
r'PARAMETER_SPECIFIC_NAME|PARAMATER_SPECIFIC_SCHEMA|PARTIAL|'
r'PASCAL|PENDANT|PLACING|PLI|POSITION|POSTFIX|PRECISION|PREFIX|'
r'PREORDER|PREPARE|PRESERVE|PRIMARY|PRIOR|PRIVILEGES|PROCEDURAL|'
r'PROCEDURE|PUBLIC|READ|READS|RECHECK|RECURSIVE|REF|REFERENCES|'
r'REFERENCING|REINDEX|RELATIVE|RENAME|REPEATABLE|REPLACE|RESET|'
r'RESTART|RESTRICT|RESULT|RETURN|RETURNED_LENGTH|'
r'RETURNED_OCTET_LENGTH|RETURNED_SQLSTATE|RETURNS|REVOKE|RIGHT|'
r'ROLE|ROLLBACK|ROLLUP|ROUTINE|ROUTINE_CATALOG|ROUTINE_NAME|'
r'ROUTINE_SCHEMA|ROW|ROWS|ROW_COUNT|RULE|SAVE_POINT|SCALE|SCHEMA|'
r'SCHEMA_NAME|SCOPE|SCROLL|SEARCH|SECOND|SECURITY|SELECT|SELF|'
r'SENSITIVE|SERIALIZABLE|SERVER_NAME|SESSION|SESSION_USER|SET|'
r'SETOF|SETS|SHARE|SHOW|SIMILAR|SIMPLE|SIZE|SOME|SOURCE|SPACE|'
r'SPECIFIC|SPECIFICTYPE|SPECIFIC_NAME|SQL|SQLCODE|SQLERROR|'
r'SQLEXCEPTION|SQLSTATE|SQLWARNINIG|STABLE|START|STATE|STATEMENT|'
r'STATIC|STATISTICS|STDIN|STDOUT|STORAGE|STRICT|STRUCTURE|STYPE|'
r'SUBCLASS_ORIGIN|SUBLIST|SUBSTRING|SUM|SYMMETRIC|SYSID|SYSTEM|'
r'SYSTEM_USER|TABLE|TABLE_NAME| TEMP|TEMPLATE|TEMPORARY|TERMINATE|'
r'THAN|THEN|TIMESTAMP|TIMEZONE_HOUR|TIMEZONE_MINUTE|TO|TOAST|'
r'TRAILING|TRANSATION|TRANSACTIONS_COMMITTED|'
r'TRANSACTIONS_ROLLED_BACK|TRANSATION_ACTIVE|TRANSFORM|'
r'TRANSFORMS|TRANSLATE|TRANSLATION|TREAT|TRIGGER|TRIGGER_CATALOG|'
r'TRIGGER_NAME|TRIGGER_SCHEMA|TRIM|TRUE|TRUNCATE|TRUSTED|TYPE|'
r'UNCOMMITTED|UNDER|UNENCRYPTED|UNION|UNIQUE|UNKNOWN|UNLISTEN|'
r'UNNAMED|UNNEST|UNTIL|UPDATE|UPPER|USAGE|USER|'
r'USER_DEFINED_TYPE_CATALOG|USER_DEFINED_TYPE_NAME|'
r'USER_DEFINED_TYPE_SCHEMA|USING|VACUUM|VALID|VALIDATOR|VALUES|'
r'VARIABLE|VERBOSE|VERSION|VIEW|VOLATILE|WHEN|WHENEVER|WHERE|'
r'WITH|WITHOUT|WORK|WRITE|YEAR|ZONE|BINARY_INTEGER|BODY|BREAK|'
r'CONSTANT|DEFINITION|DELETING|DLOB|DUMMY|ELSIF|ERRLVL|ERROR|'
r'EXCEPTION|EXIT|IF|INSERTING|ISOPEN|LOOP|NO_DATA_FOUND|NOTFOUND|'
r'OTHERS|PACKAGE|PRAGMA|RAISE|RECORD|RESTRICT_REFERENCES|RETURNING|'
r'RNDS|RNPS|ROWCOUNT|ROWNUM|ROWTYPE|SAVEPOINT|SQLERRM|TOO_MANY_ROWS|'
r'UPDATING|WHILE|WNDS|WNPS|BULK|CHR|COLLECT|GREATEST|INSTR|LEAST|'
r'LENGTHB|NOCOPY|NOWAIT|NVL|RPAD|SUBST|SUBSTB|SUBSTR|SYSDATE|'
r'TABLESPACE|TO_CHAR|TO_DATE|TO_NUMBER|TRUNC)\b', Keyword),
(r'(ARRAY|BIGINT|BINARY|BIT|BLOB|BOOLEAN|CHAR|CHARACTER|DATE|'
r'DEC|DECIMAL|FLOAT|INT|INTEGER|INTERVAL|NUMBER|NUMERIC|REAL|'
r'SERIAL|SMALLINT|VARCHAR|VARCHAR2|VARYING|INT8|SERIAL8|TEXT)\b',
Name.Builtin),
(r'(_5date_args|_actual_area|_ASSERT|_doc_code|_doc_var|_doc_var_list|'
r'_enforce|_ent|_enum|_enums_define|_enums_member_define|_field|'
r'_fields|_first|_FMT|_in_list|_index|_index_unique|_indexes'
r'_MAX_STRLEN|_MID_STRLEN|_param|_package|_pk|_plsql_param|_plsql_return|'
r'_private_function|_private_procedure|_private_type|_private_var|'
r'_protected_function|_protected_procedure|_protected_type|_protected_var|'
r'_public_function|_public_procedure|_public_type|_public_var|_quote|_RAISE|'
r'_RAISEW|_reference|_references|_request_arg|_request_define|'
r'_request_res|_STR_CONST|_substr|_table|_tail|builtin|cdr|changecom|'
r'changequote|changeword|debugfile|decr|define|divert|dummy_list|dumpdef|'
r'errprint|eval|foreach|forloop|GetNth|ifelse|include|incr|index|indir|'
r'len|lowcase|maketemp|Nth|patsubst|popdef|pushdef|regexp|'
r'remove_prefix|shift|sinclude|Size|syscmd|TRACE|traceoff|traceon|'
r'translit|undefine|undivert|upcase)\b',
Name.Function),
(r'[+*/<>=~!@#%^&|`?^-]', Operator),
(r'[0-9]+', Number.Integer),
(r"'(''|[^'\\]|\\\'|\\\\)*'", String.Single),
(r'"(""|[^"\\]|\\\"|\\\\)*"', String.Symbol),
(r'[a-zA-Z_][a-zA-Z0-9_]*', Name),
(r'[;:()\[\],\.]', Punctuation)
],
'multiline-comments': [
(r'/\*', Comment.Multiline, 'multiline-comments'),
(r'\*/', Comment.Multiline, '#pop'),
(r'[^/\*]+', Comment.Multiline),
(r'[/*]', Comment.Multiline)
]
}

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -23,13 +23,11 @@ import sys
import string
import time
from common import _item, TemplateData
import cvsdb
import viewvc
import ezt
import debug
import urllib
import fnmatch
class FormData:
def __init__(self, form):
@@ -41,7 +39,6 @@ class FormData:
self.file = ""
self.who = ""
self.sortby = ""
self.textquery = ""
self.date = ""
self.hours = 0
@@ -49,7 +46,7 @@ class FormData:
def decode_thyself(self, form):
try:
self.textquery = string.strip(form["textquery"].value)
self.repository = string.strip(form["repository"].value)
except KeyError:
pass
except TypeError:
@@ -58,16 +55,7 @@ class FormData:
self.valid = 1
try:
self.repository = form["repository"].value.strip()
except KeyError:
pass
except TypeError:
pass
else:
self.valid = 1
try:
self.branch = form["branch"].value.strip()
self.branch = string.strip(form["branch"].value)
except KeyError:
pass
except TypeError:
@@ -76,7 +64,7 @@ class FormData:
self.valid = 1
try:
self.directory = form["directory"].value.strip()
self.directory = string.strip(form["directory"].value)
except KeyError:
pass
except TypeError:
@@ -85,7 +73,7 @@ class FormData:
self.valid = 1
try:
self.file = form["file"].value.strip()
self.file = string.strip(form["file"].value)
except KeyError:
pass
except TypeError:
@@ -94,7 +82,7 @@ class FormData:
self.valid = 1
try:
self.who = form["who"].value.strip()
self.who = string.strip(form["who"].value)
except KeyError:
pass
except TypeError:
@@ -103,14 +91,14 @@ class FormData:
self.valid = 1
try:
self.sortby = form["sortby"].value.strip()
self.sortby = string.strip(form["sortby"].value)
except KeyError:
pass
except TypeError:
pass
try:
self.date = form["date"].value.strip()
self.date = string.strip(form["date"].value)
except KeyError:
pass
except TypeError:
@@ -169,7 +157,7 @@ def listparse_string(str):
## command; add the command and start over
elif c == ",":
## strip ending whitespace on un-quoted data
temp = temp.rstrip()
temp = string.rstrip(temp)
return_list.append( ("", temp) )
temp = ""
state = "eat leading whitespace"
@@ -228,9 +216,8 @@ def decode_command(cmd):
else:
return "exact"
def form_to_cvsdb_query(cfg, form_data):
def form_to_cvsdb_query(form_data):
query = cvsdb.CreateCheckinQuery()
query.SetLimit(cfg.cvsdb.row_limit)
if form_data.repository:
for cmd, str in listparse_string(form_data.repository):
@@ -257,15 +244,10 @@ def form_to_cvsdb_query(cfg, form_data):
cmd = decode_command(cmd)
query.SetAuthor(str, cmd)
if form_data.textquery:
query.SetTextQuery(form_data.textquery)
if form_data.sortby == "author":
query.SetSortMethod("author")
elif form_data.sortby == "file":
query.SetSortMethod("file")
elif form_data.sortby == "relevance" and form_data.textquery:
query.SetSortMethod("relevance")
else:
query.SetSortMethod("date")
@@ -283,67 +265,27 @@ def form_to_cvsdb_query(cfg, form_data):
def prev_rev(rev):
'''Returns a string representing the previous revision of the argument.'''
r = rev.split('.')
r = string.split(rev, '.')
# decrement final revision component
r[-1] = str(int(r[-1]) - 1)
# prune if we pass the beginning of the branch
if len(r) > 2 and r[-1] == '0':
r = r[:-2]
return '.'.join(r)
return string.join(r, '.')
def is_forbidden(cfg, cvsroot_name, module):
'''Return 1 if MODULE in CVSROOT_NAME is forbidden; return 0 otherwise.'''
# CVSROOT_NAME might be None here if the data comes from an
# unconfigured root. This interfaces doesn't care that the root
# isn't configured, but if that's the case, it will consult only
# the base and per-vhost configuration for authorizer and
# authorizer parameters.
if cvsroot_name:
cfg = cfg.get_root_config(cvsroot_name)
authorizer = cfg.options.authorizer
params = cfg.get_authorizer_params()
# If CVSROOT_NAME isn't configured to use an authorizer, nothing
# is forbidden. If it's configured to use something other than
# the 'forbidden' authorizer, complain. Otherwise, check for
# forbiddenness per the PARAMS as expected.
if not authorizer:
return 0
if authorizer != 'forbidden':
raise Exception("The 'forbidden' authorizer is the only one supported "
"by this interface. The '%s' root is configured to "
"use a different one." % (cvsroot_name))
forbidden = params.get('forbidden', '')
forbidden = map(lambda x: x.strip(), filter(None, forbidden.split(',')))
default = 0
for pat in forbidden:
if pat[0] == '!':
default = 1
if fnmatch.fnmatchcase(module, pat[1:]):
return 0
elif fnmatch.fnmatchcase(module, pat):
return 1
return default
def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
ob = _item(num_files=len(files), files=[])
ob.log = desc and server.escape(desc).replace('\n', '<br />') or '&nbsp;'
if desc:
ob.log = string.replace(server.escape(desc), '\n', '<br />')
else:
ob.log = '&nbsp;'
for commit in files:
repository = commit.GetRepository()
directory = commit.GetDirectory()
cvsroot_name = cvsroots.get(repository)
## find the module name (if any)
try:
module = filter(None, directory.split('/'))[0]
except IndexError:
module = None
## skip commits we aren't supposed to show
if module and ((module == 'CVSROOT' and cfg.options.hide_cvsroot) \
or is_forbidden(cfg, cvsroot_name, module)):
dir_parts = filter(None, string.split(commit.GetDirectory(), '/'))
if dir_parts \
and ((dir_parts[0] == 'CVSROOT' and cfg.options.hide_cvsroot) \
or cfg.is_forbidden(dir_parts[0])):
continue
ctime = commit.GetTime()
@@ -354,18 +296,17 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
ctime = time.strftime("%y/%m/%d %H:%M %Z", time.localtime(ctime))
else:
ctime = time.strftime("%y/%m/%d %H:%M", time.gmtime(ctime)) \
+ ' UTC'
+ ' UTC'
## make the file link
try:
file = (directory and directory + "/") + commit.GetFile()
except:
raise Exception, str([directory, commit.GetFile()])
repository = commit.GetRepository()
directory = commit.GetDirectory()
file = (directory and directory + "/") + commit.GetFile()
cvsroot_name = cvsroots.get(repository)
## If we couldn't find the cvsroot path configured in the
## viewvc.conf file, or we don't have a VIEWVC_LINK, then
## don't make the link.
if cvsroot_name and viewvc_link:
## if we couldn't find the cvsroot path configured in the
## viewvc.conf file, then don't make the link
if cvsroot_name:
flink = '[%s] <a href="%s/%s?root=%s">%s</a>' % (
cvsroot_name, viewvc_link, urllib.quote(file),
cvsroot_name, file)
@@ -379,9 +320,6 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
flink = '[%s] %s' % (repository, file)
dlink = None
ob.relevance = commit.GetRelevance()
ob.plus += int(commit.GetPlusCount())
ob.minus += int(commit.GetMinusCount())
ob.files.append(_item(date=ctime,
author=commit.GetAuthor(),
link=flink,
@@ -395,27 +333,24 @@ def build_commit(server, cfg, desc, files, cvsroots, viewvc_link):
return ob
def run_query(server, cfg, db, form_data, viewvc_link):
query = form_to_cvsdb_query(cfg, form_data)
def run_query(server, cfg, form_data, viewvc_link):
query = form_to_cvsdb_query(form_data)
db = cvsdb.ConnectDatabaseReadOnly(cfg)
db.RunQuery(query)
commit_list = query.GetCommitList()
if not commit_list:
return [ ], 0
row_limit_reached = query.GetLimitReached()
if not query.commit_list:
return [ ]
commits = [ ]
files = [ ]
cvsroots = {}
viewvc.expand_root_parents(cfg)
rootitems = cfg.general.svn_roots.items() + cfg.general.cvs_roots.items()
rootitems = cfg.general.cvs_roots.items() + cfg.general.svn_roots.items()
for key, value in rootitems:
cvsroots[cvsdb.CleanRepository(value)] = key
current_desc = commit_list[0].GetDescription()
for commit in commit_list:
current_desc = query.commit_list[0].GetDescription()
for commit in query.commit_list:
desc = commit.GetDescription()
if current_desc == desc:
files.append(commit)
@@ -431,14 +366,7 @@ def run_query(server, cfg, db, form_data, viewvc_link):
commits.append(build_commit(server, cfg, current_desc, files,
cvsroots, viewvc_link))
# Strip out commits that don't have any files attached to them. The
# files probably aren't present because they've been blocked via
# forbiddenness.
def _only_with_files(commit):
return len(commit.files) > 0
commits = filter(_only_with_files, commits)
return commits, row_limit_reached
return commits
def main(server, cfg, viewvc_link):
try:
@@ -446,45 +374,48 @@ def main(server, cfg, viewvc_link):
form = server.FieldStorage()
form_data = FormData(form)
db = cvsdb.ConnectDatabaseReadOnly(cfg, None)
if form_data.valid:
commits, row_limit_reached = run_query(server, cfg, db, form_data, viewvc_link)
commits = run_query(server, cfg, form_data, viewvc_link)
query = None
else:
commits = [ ]
row_limit_reached = 0
query = 'skipped'
docroot = cfg.options.docroot
if docroot is None and viewvc_link:
docroot = viewvc_link + '/' + viewvc.docroot_magic_path
data = TemplateData({
script_name = server.getenv('SCRIPT_NAME', '')
data = {
'cfg' : cfg,
'address' : cfg.general.address,
'vsn' : viewvc.__version__,
'textquery' : server.escape(form_data.textquery),
'repository' : server.escape(form_data.repository),
'branch' : server.escape(form_data.branch),
'directory' : server.escape(form_data.directory),
'file' : server.escape(form_data.file),
'who' : server.escape(form_data.who),
'docroot' : docroot,
'repository' : server.escape(form_data.repository, 1),
'branch' : server.escape(form_data.branch, 1),
'directory' : server.escape(form_data.directory, 1),
'file' : server.escape(form_data.file, 1),
'who' : server.escape(form_data.who, 1),
'docroot' : cfg.options.docroot is None \
and viewvc_link + '/' + viewvc.docroot_magic_path \
or cfg.options.docroot,
'sortby' : form_data.sortby,
'date' : form_data.date,
'query' : query,
'row_limit_reached' : ezt.boolean(row_limit_reached),
'commits' : commits,
'num_commits' : len(commits),
'rss_href' : None,
'repositories' : db.GetRepositoryList(),
'hours' : form_data.hours and form_data.hours or 2,
})
}
if form_data.hours:
data['hours'] = form_data.hours
else:
data['hours'] = 2
server.header()
# generate the page
server.header()
template = viewvc.get_view_template(cfg, "query")
template.generate(server.file(), data)
template.generate(sys.stdout, data)
except SystemExit, e:
pass
@@ -492,3 +423,7 @@ def main(server, cfg, viewvc_link):
exc_info = debug.GetExceptionData()
server.header(status=exc_info['status'])
debug.PrintException(server, exc_info)
class _item:
def __init__(self, **kw):
vars(self).update(kw)

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -16,32 +16,17 @@
# -----------------------------------------------------------------------
import types
import string
import os
import sys
import re
import cgi
# global server object. It will be either a CgiServer, a WsgiServer,
# or a proxy to an AspServer or ModPythonServer object.
# global server object. It will be either a CgiServer or a proxy to
# an AspServer or ModPythonServer object.
server = None
# Simple HTML string escaping. Note that we always escape the
# double-quote character -- ViewVC shouldn't ever need to preserve
# that character as-is, and sometimes needs to embed escaped values
# into HTML attributes.
def escape(s):
try: s = s.encode('utf8')
except: pass
s = str(s)
s = s.replace('&', '&amp;')
s = s.replace('>', '&gt;')
s = s.replace('<', '&lt;')
s = s.replace('"', "&quot;")
return s
class Server:
def __init__(self):
self.pageGlobals = {}
@@ -49,9 +34,6 @@ class Server:
def self(self):
return self
def escape(self, s):
return escape(s)
def close(self):
pass
@@ -147,10 +129,13 @@ class CgiServer(Server):
global server
server = self
global cgi
import cgi
def addheader(self, name, value):
self.headers.append((name, value))
def header(self, content_type='text/html; charset=UTF-8', status=None):
def header(self, content_type='text/html', status=None):
if not self.headerSent:
self.headerSent = 1
@@ -173,7 +158,11 @@ class CgiServer(Server):
if self.iis: url = fix_iis_url(self, url)
self.addheader('Location', url)
self.header(status='301 Moved')
sys.stdout.write('This document is located <a href="%s">here</a>.\n' % url)
print 'This document is located <a href="%s">here</a>.' % url
sys.exit(0)
def escape(self, s, quote = None):
return cgi.escape(s, quote)
def getenv(self, name, value=None):
ret = os.environ.get(name, value)
@@ -187,7 +176,7 @@ class CgiServer(Server):
def FieldStorage(fp=None, headers=None, outerboundary="",
environ=os.environ, keep_blank_values=0, strict_parsing=0):
return cgi.FieldStorage(fp, headers, outerboundary, environ,
keep_blank_values, strict_parsing)
keep_blank_values, strict_parsing)
def write(self, s):
sys.stdout.write(s)
@@ -199,60 +188,6 @@ class CgiServer(Server):
return sys.stdout
class WsgiServer(Server):
def __init__(self, environ, start_response):
Server.__init__(self)
self._environ = environ
self._start_response = start_response;
self._headers = []
self._wsgi_write = None
self.headerSent = False
global server
server = self
def addheader(self, name, value):
self._headers.append((name, value))
def header(self, content_type='text/html; charset=UTF-8', status=None):
if not status:
status = "200 OK"
if not self.headerSent:
self.headerSent = True
self._headers.insert(0, ("Content-Type", content_type),)
self._wsgi_write = self._start_response("%s" % status, self._headers)
def redirect(self, url):
"""Redirect client to url. This discards any data that has been queued
to be sent to the user. But there should never by any anyway.
"""
self.addheader('Location', url)
self.header(status='301 Moved')
self._wsgi_write('This document is located <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):
def __init__(self, Server, Request, Response, Application):
ThreadedServer.__init__(self)
@@ -274,16 +209,17 @@ class AspServer(ThreadedServer):
if not self.headerSent:
try:
self.headerSent = 1
if content_type is None:
self.response.ContentType = 'text/html; charset=UTF-8'
else:
self.response.ContentType = content_type
if content_type is not None: self.response.ContentType = content_type
if status is not None: self.response.Status = status
except AttributeError:
pass
def redirect(self, url):
self.response.Redirect(url)
sys.exit()
def escape(self, s, quote = None):
return self.server.HTMLEncode(str(s))
def getenv(self, name, value = None):
ret = self.request.ServerVariables(name)()
@@ -346,12 +282,15 @@ class ModPythonServer(ThreadedServer):
self.request = request
self.headerSent = 0
global cgi
import cgi
def addheader(self, name, value):
self.request.headers_out.add(name, value)
def header(self, content_type=None, status=None):
if content_type is None:
self.request.content_type = 'text/html; charset=UTF-8'
self.request.content_type = 'text/html'
else:
self.request.content_type = content_type
self.headerSent = 1
@@ -366,7 +305,11 @@ class ModPythonServer(ThreadedServer):
self.request.headers_out['Location'] = url
self.request.status = mod_python.apache.HTTP_MOVED_TEMPORARILY
self.request.write("You are being redirected to <a href=\"%s\">%s</a>"
% (url, url))
% (url, url))
sys.exit()
def escape(self, s, quote = None):
return cgi.escape(s, quote)
def getenv(self, name, value = None):
try:

View File

@@ -1,56 +0,0 @@
# -*-python-*-
#
# Copyright (C) 2006-2013 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
"""Generic API for implementing authorization checks employed by ViewVC."""
class GenericViewVCAuthorizer:
"""Abstract class encapsulating version control authorization routines."""
def __init__(self, username=None, params={}):
"""Create a GenericViewVCAuthorizer object which will be used to
validate that USERNAME has the permissions needed to view version
control repositories (in whole or in part). PARAMS is a
dictionary of custom parameters for the authorizer."""
pass
def check_root_access(self, rootname):
"""Return 1 iff the associated username is permitted to read ROOTNAME."""
pass
def check_universal_access(self, rootname):
"""Return 1 if the associated username is permitted to read every
path in the repository at every revision, 0 if the associated
username is prohibited from reading any path in the repository, or
None if no such determination can be made (perhaps because the
cost of making it is too great)."""
pass
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
"""Return 1 iff the associated username is permitted to read
revision REV of the path PATH_PARTS (of type PATHTYPE) in
repository ROOTNAME."""
pass
##############################################################################
class ViewVCAuthorizer(GenericViewVCAuthorizer):
"""The uber-permissive authorizer."""
def check_root_access(self, rootname):
return 1
def check_universal_access(self, rootname):
return 1
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
return 1

View File

@@ -1,92 +0,0 @@
# -*-python-*-
#
# Copyright (C) 2009 Vitaliy Filippov.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
import vcauth
import vclib
import string
from xml.dom.minidom import parse
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
"""An authorizer which uses CVSnt access control lists (which are in form
of XML files in CVS/ subdirectories in the repository)."""
def __init__(self, username, params={}):
self.username = username
self.params = params
self.cfg = params['__config']
self.default = params.get('default', 0)
self.cached = {}
self.xmlcache = {}
def dom_rights(self, doc, rightname, filename, username):
result = None
if filename:
for node in doc.getElementsByTagName('file'):
if node.getAttribute('name') == filename:
for acl in node.getElementsByTagName('acl'):
if not acl.getAttribute('branch'):
u = acl.getAttribute('user')
if result is None and (not u or u == 'anonymous') or u == username:
for r in acl.getElementsByTag(rightname):
result = not r.getAttribute('deny')
break
if result is None:
for node in doc.getElementsByTagName('directory'):
for acl in node.getElementsByTagName('acl'):
if not acl.getAttribute('branch'):
u = acl.getAttribute('user')
if result is None and (not u or u == 'anonymous') or u == username:
for r in acl.getElementsByTag(rightname):
result = not r.getAttribute('deny')
return result
def check(self, rootname, path_parts, filename):
d = self.cfg.general.cvs_roots.get(rootname,None)
if not d:
return self.default
i = len(path_parts)
r = None
while i >= 0:
try:
xml = d
if len(path_parts):
xml = xml + '/' + string.join(path_parts, '/')
xml = xml + '/CVS/fileattr.xml'
if self.cached.get(xml, None) is not None:
return self.cached.get(xml, None)
doc = self.xmlcache.get(xml, None)
if doc is None:
doc = parse(xml)
self.xmlcache[xml] = doc
r = self.dom_rights(doc, 'read', filename, self.username)
if r is not None:
self.cached[xml] = r
return r
raise Exception(None)
except:
if len(path_parts) > 0:
path_parts = path_parts[:-1]
filename = ''
i = i-1
return self.default
def check_root_access(self, rootname):
return self.check(rootname, [], '')
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
if not path_parts:
return self.check(rootname, [], '')
if pathtype == vclib.DIR:
return self.check(rootname, path_parts, '')
f = path_parts[-1]
path_parts = path_parts[:-1]
return self.check(rootname, path_parts, f)

View File

@@ -1,52 +0,0 @@
# -*-python-*-
#
# Copyright (C) 2006-2013 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
import vcauth
import vclib
import fnmatch
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
"""A simple top-level module authorizer."""
def __init__(self, username, params={}):
forbidden = params.get('forbidden', '')
self.forbidden = map(lambda x: x.strip(),
filter(None, forbidden.split(',')))
def check_root_access(self, rootname):
return 1
def check_universal_access(self, rootname):
# If there aren't any forbidden paths, we can grant universal read
# access. Otherwise, we make no claim.
if not self.forbidden:
return 1
return None
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
# No path? No problem.
if not path_parts:
return 1
# Not a directory? We aren't interested.
if pathtype != vclib.DIR:
return 1
# At this point we're looking at a directory path.
module = path_parts[0]
default = 1
for pat in self.forbidden:
if pat[0] == '!':
default = 0
if fnmatch.fnmatchcase(module, pat[1:]):
return 1
elif fnmatch.fnmatchcase(module, pat):
return 0
return default

View File

@@ -1,64 +0,0 @@
# -*-python-*-
#
# Copyright (C) 2008-2013 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
import vcauth
import vclib
import fnmatch
import re
def _split_regexp(restr):
"""Return a 2-tuple consisting of a compiled regular expression
object and a boolean flag indicating if that object should be
interpreted inversely."""
if restr[0] == '!':
return re.compile(restr[1:]), 1
return re.compile(restr), 0
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
"""A simple regular-expression-based authorizer."""
def __init__(self, username, params={}):
forbidden = params.get('forbiddenre', '')
self.forbidden = map(lambda x: _split_regexp(x.strip()),
filter(None, forbidden.split(',')))
def _check_root_path_access(self, root_path):
default = 1
for forbidden, negated in self.forbidden:
if negated:
default = 0
if forbidden.search(root_path):
return 1
elif forbidden.search(root_path):
return 0
return default
def check_root_access(self, rootname):
return self._check_root_path_access(rootname)
def check_universal_access(self, rootname):
# If there aren't any forbidden regexps, we can grant universal
# read access. Otherwise, we make no claim.
if not self.forbidden:
return 1
return None
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
root_path = rootname
if path_parts:
root_path = root_path + '/' + '/'.join(path_parts)
if pathtype == vclib.DIR:
root_path = root_path + '/'
else:
root_path = root_path + '/'
return self._check_root_path_access(root_path)

View File

@@ -1,62 +0,0 @@
# -*-python-*-
#
# Copyright (C) 2009 Vitaliy Filippov.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
import vcauth
import vclib
import string
import grp
import re
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
"""A simple authorizer checking system group membership for every
repository and every user."""
def __init__(self, username, params={}):
self.username = username
self.params = params
self.cfg = params['__config']
self.cached = {}
self.grp = {}
self.byroot = {}
self.fmt = map(lambda l: l.strip(), params.get('group_name_format', 'svn.%s|svn.%s.ro').split('|'))
byr = params.get('by_root', '')
for i in byr.split(','):
if i.find(':') < 0:
continue
(root, auth) = i.split(':', 2)
self.byroot[root.strip()] = map(lambda l: l.strip(), auth.split('|'))
def check_root_access(self, rootname):
r = self.cached.get(rootname, None)
if r is not None:
return r
grent = self.grp.get(rootname, None)
if grent is None:
grent = map(lambda grn: self.getgrnam(grn, rootname), self.byroot.get(rootname, self.fmt))
self.grp[rootname] = grent
r = 0
for i in grent:
if i and i.gr_mem and len(i.gr_mem) and self.username in i.gr_mem:
r = 1
break
self.cached[rootname] = r
return r
def getgrnam(self, grn, rootname):
try:
r = grp.getgrnam(grn.replace('%s', re.sub('[^\w\.\-]+', '', rootname)))
except KeyError:
r = None
return r
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
return self.check_root_access(rootname)

View File

@@ -1,269 +0,0 @@
# -*-python-*-
#
# Copyright (C) 2006-2013 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
# distribution or at http://viewvc.org/license-1.html.
#
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
# (c) 2006 Sergey Lapin <slapin@dataart.com>
import vcauth
import os.path
import debug
from ConfigParser import ConfigParser
class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer):
"""Subversion authz authorizer module"""
def __init__(self, username, params={}):
self.rootpaths = { } # {root -> { paths -> access boolean for USERNAME }}
# Get the authz file location from a passed-in parameter.
self.authz_file = params.get('authzfile')
if not self.authz_file:
raise debug.ViewVCException("No authzfile configured")
if not os.path.exists(self.authz_file):
raise debug.ViewVCException("Configured authzfile file not found")
# See if the admin wants us to do case normalization of usernames.
self.force_username_case = params.get('force_username_case')
if self.force_username_case == "upper":
self.username = username and username.upper() or username
elif self.force_username_case == "lower":
self.username = username and username.lower() or username
elif not self.force_username_case:
self.username = username
else:
raise debug.ViewVCException("Invalid value for force_username_case "
"option")
def _get_paths_for_root(self, rootname):
if self.rootpaths.has_key(rootname):
return self.rootpaths[rootname]
paths_for_root = { }
# Parse the authz file, replacing ConfigParser's optionxform()
# method with something that won't futz with the case of the
# option names.
cp = ConfigParser()
cp.optionxform = lambda x: x
try:
cp.read(self.authz_file)
except:
raise debug.ViewVCException("Unable to parse configured authzfile file")
# Figure out if there are any aliases for the current username
aliases = []
if cp.has_section('aliases'):
for alias in cp.options('aliases'):
entry = cp.get('aliases', alias)
if entry == self.username:
aliases.append(alias)
# Figure out which groups USERNAME has a part of.
groups = []
if cp.has_section('groups'):
all_groups = []
def _process_group(groupname):
"""Inline function to handle groups within groups.
For a group to be within another group in SVN, the group
definitions must be in the correct order in the config file.
ie. If group A is a member of group B then group A must be
defined before group B in the [groups] section.
Unfortunately, the ConfigParser class provides no way of
finding the order in which groups were defined so, for reasons
of practicality, this function lets you get away with them
being defined in the wrong order. Recursion is guarded
against though."""
# If we already know the user is part of this already-
# processed group, return that fact.
if groupname in groups:
return 1
# Otherwise, ensure we don't process a group twice.
if groupname in all_groups:
return 0
# Store the group name in a global list so it won't be processed again
all_groups.append(groupname)
group_member = 0
groupname = groupname.strip()
entries = cp.get('groups', groupname).split(',')
for entry in entries:
entry = entry.strip()
if entry == self.username:
group_member = 1
break
elif entry[0:1] == "@" and _process_group(entry[1:]):
group_member = 1
break
elif entry[0:1] == "&" and entry[1:] in aliases:
group_member = 1
break
if group_member:
groups.append(groupname)
return group_member
# Process the groups
for group in cp.options('groups'):
_process_group(group)
def _userspec_matches_user(userspec):
# If there is an inversion character, recurse and return the
# opposite result.
if userspec[0:1] == '~':
return not _userspec_matches_user(userspec[1:])
# See if the userspec applies to our current user.
return userspec == '*' \
or userspec == self.username \
or (self.username is not None and userspec == "$authenticated") \
or (self.username is None and userspec == "$anonymous") \
or (userspec[0:1] == "@" and userspec[1:] in groups) \
or (userspec[0:1] == "&" and userspec[1:] in aliases)
def _process_access_section(section):
"""Inline function for determining user access in a single
config secction. Return a two-tuple (ALLOW, DENY) containing
the access determination for USERNAME in a given authz file
SECTION (if any)."""
# Figure if this path is explicitly allowed or denied to USERNAME.
allow = deny = 0
for user in cp.options(section):
user = user.strip()
if _userspec_matches_user(user):
# See if the 'r' permission is among the ones granted to
# USER. If so, we can stop looking. (Entry order is not
# relevant -- we'll use the most permissive entry, meaning
# one 'allow' is all we need.)
allow = cp.get(section, user).find('r') != -1
deny = not allow
if allow:
break
return allow, deny
# Read the other (non-"groups") sections, and figure out in which
# repositories USERNAME or his groups have read rights. We'll
# first check groups that have no specific repository designation,
# then superimpose those that have a repository designation which
# matches the one we're asking about.
root_sections = []
for section in cp.sections():
# Skip the "groups" section -- we handled that already.
if section == 'groups':
continue
if section == 'aliases':
continue
# Process root-agnostic access sections; skip (but remember)
# root-specific ones that match our root; ignore altogether
# root-specific ones that don't match our root. While we're at
# it, go ahead and figure out the repository path we're talking
# about.
if section.find(':') == -1:
path = section
else:
name, path = section.split(':', 1)
if name == rootname:
root_sections.append(section)
continue
# Check for a specific access determination.
allow, deny = _process_access_section(section)
# If we got an explicit access determination for this path and this
# USERNAME, record it.
if allow or deny:
if path != '/':
path = '/' + '/'.join(filter(None, path.split('/')))
paths_for_root[path] = allow
# Okay. Superimpose those root-specific values now.
for section in root_sections:
# Get the path again.
name, path = section.split(':', 1)
# Check for a specific access determination.
allow, deny = _process_access_section(section)
# If we got an explicit access determination for this path and this
# USERNAME, record it.
if allow or deny:
if path != '/':
path = '/' + '/'.join(filter(None, path.split('/')))
paths_for_root[path] = allow
# If the root isn't readable, there's no point in caring about all
# the specific paths the user can't see. Just point the rootname
# to a None paths dictionary.
root_is_readable = 0
for path in paths_for_root.keys():
if paths_for_root[path]:
root_is_readable = 1
break
if not root_is_readable:
paths_for_root = None
self.rootpaths[rootname] = paths_for_root
return paths_for_root
def check_root_access(self, rootname):
paths = self._get_paths_for_root(rootname)
return (paths is not None) and 1 or 0
def check_universal_access(self, rootname):
paths = self._get_paths_for_root(rootname)
if not paths: # None or empty.
return 0
# Search the access determinations. If there's a mix, we can't
# claim a universal access determination.
found_allow = 0
found_deny = 0
for access in paths.values():
if access:
found_allow = 1
else:
found_deny = 1
if found_allow and found_deny:
return None
# We didn't find both allowances and denials, so we must have
# found one or the other. Denials only is a universal denial.
if found_deny:
return 0
# ... but allowances only is only a universal allowance if read
# access is granted to the root directory.
if found_allow and paths.has_key('/'):
return 1
# Anything else is indeterminable.
return None
def check_path_access(self, rootname, path_parts, pathtype, rev=None):
# Crawl upward from the path represented by PATH_PARTS toward to
# the root of the repository, looking for an explicitly grant or
# denial of access.
paths = self._get_paths_for_root(rootname)
if paths is None:
return 0
parts = path_parts[:]
while parts:
path = '/' + '/'.join(parts)
if paths.has_key(path):
return paths[path]
del parts[-1]
return paths.get('/', 0)

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -14,6 +14,7 @@
such as CVS.
"""
import string
import types
@@ -26,44 +27,11 @@ UNIFIED = 1
CONTEXT = 2
SIDE_BY_SIDE = 3
# root types returned by Repository.roottype().
CVS = 'cvs'
SVN = 'svn'
GIT = 'git'
# action kinds found in ChangedPath.action
ADDED = 'added'
DELETED = 'deleted'
REPLACED = 'replaced'
MODIFIED = 'modified'
# log sort keys
SORTBY_DEFAULT = 0 # default/no sorting
SORTBY_DATE = 1 # sorted by date, youngest first
SORTBY_REV = 2 # sorted by revision, youngest first
# ======================================================================
#
class Repository:
"""Abstract class representing a repository."""
def rootname(self):
"""Return the name of this repository."""
def roottype(self):
"""Return the type of this repository (vclib.CVS, vclib.SVN, ...)."""
def rootpath(self):
"""Return the location of this repository."""
def authorizer(self):
"""Return the vcauth.Authorizer object associated with this
repository, or None if no such association has been made."""
def open(self):
"""Open a connection to the repository."""
def itemtype(self, path_parts, rev):
"""Return the type of the item (file or dir) at the given path and revision
@@ -76,7 +44,7 @@ class Repository:
"""
pass
def openfile(self, path_parts, rev, options):
def openfile(self, path_parts, rev):
"""Open a file object to read file contents at a given path and revision.
The return value is a 2-tuple of containg the file object and revision
@@ -86,8 +54,6 @@ class Repository:
of the repository. e.g. ["subdir1", "subdir2", "filename"]
rev is the revision of the file to check out
options is a dictionary of implementation specific options
"""
def listdir(self, path_parts, rev, options):
@@ -109,7 +75,7 @@ class Repository:
New properties will be set on all of the DirEntry objects in the entries
list. At the very least, a "rev" property will be set to a revision
number or None if the entry doesn't have a number. Other properties that
may be set include "date", "author", "log", "size", and "lockinfo".
may be set include "date", "author", and "log".
The path is specified as a list of components, relative to the root
of the repository. e.g. ["subdir1", "subdir2", "filename"]
@@ -123,7 +89,7 @@ class Repository:
options is a dictionary of implementation specific options
"""
def itemlog(self, path_parts, rev, sortby, first, limit, options):
def itemlog(self, path_parts, rev, options):
"""Retrieve an item's log information
The result is a list of Revision objects
@@ -133,28 +99,9 @@ class Repository:
rev is the revision of the item to return information about
sortby indicates the way in which the returned list should be
sorted (SORTBY_DEFAULT, SORTBY_DATE, SORTBY_REV)
first is the 0-based index of the first Revision returned (after
sorting, if any, has occured)
limit is the maximum number of returned Revisions, or 0 to return
all available data
options is a dictionary of implementation specific options
"""
def itemprops(self, path_parts, rev):
"""Return a dictionary mapping property names to property values
for properties stored on an item.
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.
"""
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
"""Return a diff (in GNU diff format) of two file revisions
@@ -170,70 +117,29 @@ class Repository:
Return value is a python file object
"""
def annotate(self, path_parts, rev, include_text=False):
"""Return a list of Annotation object, sorted by their
"line_number" components, which describe the lines of given
version of a file.
def annotate(self, path_parts, rev):
"""Return a list of annotate file content lines and a revision.
The file path is specified as a list of components, relative to
the root of the repository. e.g. ["subdir1", "subdir2", "filename"]
The annotated lines are an collection of objects with the
following addressable members:
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."""
text - raw text of a line of file contents
line_number - line number on which the line is found
rev - revision in which the line was last modified
prev_rev - revision prior to 'rev'
author - author who last modified the line
date - date on which the line was last modified, in seconds
since the epoch, GMT
def revinfo(self, rev):
"""Return information about a global revision
rev is the revision of the item to return information about
Return value is a 5-tuple containing: the date, author, log
message, a list of ChangedPath items representing paths changed,
and a dictionary mapping property names to property values for
properties stored on an item.
Raise vclib.UnsupportedFeature if the version control system
doesn't support a global revision concept.
These object are sort by their line_number components.
"""
def isexecutable(self, path_parts, rev):
"""Return true iff a given revision of a versioned file is to be
considered an executable program or script.
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
"""
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:
"""Instances represent items in a directory listing"""
"Instances represent items in a directory listing"
def __init__(self, name, kind, errors=[]):
"""Create a new DirEntry() item:
NAME: The name of the directory entry
KIND: The path kind of the entry (vclib.DIR, vclib.FILE)
ERRORS: A list of error strings representing problems encountered
while determining the other info about this entry
"""
self.name = name
self.kind = kind
self.errors = errors
@@ -241,17 +147,16 @@ class DirEntry:
class Revision:
"""Instances holds information about revisions of versioned resources"""
def __init__(self, number, string, date, author, changed, log, size, lockinfo):
"""Create a new Revision() item:
NUMBER: Revision in an integer-based, sortable format
STRING: Revision as a string
DATE: Seconds since Epoch (GMT) that this revision was created
AUTHOR: Author of the revision
CHANGED: Lines-changed (contextual diff) information
LOG: Log message associated with the creation of this revision
SIZE: Size (in bytes) of this revision's fulltext (files only)
LOCKINFO: Information about locks held on this revision
"""
"""Create a new Revision() item:
NUMBER: Revision in an integer-based, sortable format
STRING: Revision as a string
DATE: Seconds since Epoch (GMT) that this revision was created
AUTHOR: Author of the revision
CHANGED: Lines-changed (contextual diff) information
LOG: Log message associated with the creation of this revision
SIZE: Size (in bytes) of this revision's fulltext (files only)
"""
def __init__(self, number, string, date, author, changed, log, size):
self.number = number
self.string = string
self.date = date
@@ -259,77 +164,23 @@ class Revision:
self.changed = changed
self.log = log
self.size = size
self.lockinfo = lockinfo
def __cmp__(self, other):
return cmp(self.number, other.number)
class Annotation:
"""Instances represent per-line file annotation information"""
def __init__(self, text, line_number, rev, prev_rev, author, date):
"""Create a new Annotation() item:
TEXT: Raw text of a line of file contents
LINE_NUMBER: Line number on which the line is found
REV: Revision in which the line was last modified
PREV_REV: Revision prior to 'rev'
AUTHOR: Author who last modified the line
DATE: Date on which the line was last modified, in seconds since
the epoch, GMT
"""
self.text = text
self.line_number = line_number
self.rev = rev
self.prev_rev = prev_rev
self.author = author
self.date = date
class ChangedPath:
"""Instances represent changes to paths"""
def __init__(self, path_parts, rev, pathtype, base_path_parts,
base_rev, action, copied, text_changed, props_changed):
"""Create a new ChangedPath() item:
PATH_PARTS: Path that was changed
REV: Revision represented by this change
PATHTYPE: Type of this path (vclib.DIR, vclib.FILE, ...)
BASE_PATH_PARTS: Previous path for this changed item
BASE_REV: Previous revision for this changed item
ACTION: Kind of change (vclib.ADDED, vclib.DELETED, ...)
COPIED: Boolean -- was this path copied from elsewhere?
TEXT_CHANGED: Boolean -- did the file's text change?
PROPS_CHANGED: Boolean -- did the item's metadata change?
"""
self.path_parts = path_parts
self.rev = rev
self.pathtype = pathtype
self.base_path_parts = base_path_parts
self.base_rev = base_rev
self.action = action
self.copied = copied
self.text_changed = text_changed
self.props_changed = props_changed
# ======================================================================
class Error(Exception):
pass
class ReposNotFound(Error):
pass
class UnsupportedFeature(Error):
pass
class ItemNotFound(Error):
def __init__(self, path):
# use '/' rather than os.sep because this is for user consumption, and
# it was defined using URL separators
if type(path) in (types.TupleType, types.ListType):
path = '/'.join(path)
path = string.join(path, '/')
Error.__init__(self, path)
class InvalidRevision(Error):
def __init__(self, revision=None):
if revision is None:
@@ -337,13 +188,10 @@ class InvalidRevision(Error):
else:
Error.__init__(self, "Invalid revision " + str(revision))
class NonTextualFileContents(Error):
pass
# ======================================================================
# Implementation code used by multiple vclib modules
import tempfile
import popen
import os
import time
@@ -352,18 +200,12 @@ def _diff_args(type, options):
args = []
if type == CONTEXT:
if options.has_key('context'):
if options['context'] is None:
args.append('--context=%i' % 0x01ffffff)
else:
args.append('--context=%i' % options['context'])
args.append('--context=%i' % options['context'])
else:
args.append('-c')
elif type == UNIFIED:
if options.has_key('context'):
if options['context'] is None:
args.append('--unified=%i' % 0x01ffffff)
else:
args.append('--unified=%i' % options['context'])
args.append('--unified=%i' % options['context'])
else:
args.append('-u')
elif type == SIDE_BY_SIDE:
@@ -384,18 +226,14 @@ class _diff_fp:
"""File object reading a diff between temporary files, cleaning up
on close"""
def __init__(self, temp1, temp2, info1=None, info2=None, diff_cmd='diff', diff_opts=[]):
def __init__(self, temp1, temp2, info1=None, info2=None, diff_opts=[]):
self.temp1 = temp1
self.temp2 = temp2
self.temp3 = tempfile.mktemp()
args = diff_opts[:]
if info1 and info2:
args.extend(["-L", self._label(info1), "-L", self._label(info2)])
args.extend([temp1, temp2])
args.insert(0, diff_cmd)
os.system("'"+"' '".join(args)+"' > '"+self.temp3+"' 2> '"+self.temp3+"'")
self.fp = open(self.temp3, 'rb')
self.fp.seek(0)
self.fp = popen.popen("diff", args, "r")
def read(self, bytes):
return self.fp.read(bytes)
@@ -403,9 +241,6 @@ class _diff_fp:
def readline(self):
return self.fp.readline()
def readlines(self):
return self.fp.readlines()
def close(self):
try:
if self.fp:
@@ -413,17 +248,13 @@ class _diff_fp:
self.fp = None
finally:
try:
if self.temp1 and self.temp1 != '/dev/null':
if self.temp1:
os.remove(self.temp1)
self.temp1 = None
self.temp1 = None
finally:
if self.temp2 and self.temp2 != '/dev/null':
if self.temp2:
os.remove(self.temp2)
self.temp2 = None
try:
os.remove(self.temp3)
finally:
self.temp3 = None
self.temp2 = None
def __del__(self):
self.close()
@@ -431,28 +262,3 @@ class _diff_fp:
def _label(self, (path, date, rev)):
date = date and time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date))
return "%s\t%s\t%s" % (path, date, rev)
def check_root_access(repos):
"""Return 1 iff the associated username is permitted to read REPOS,
as determined by consulting REPOS's Authorizer object (if any)."""
auth = repos.authorizer()
if not auth:
return 1
return auth.check_root_access(repos.rootname())
def check_path_access(repos, path_parts, pathtype=None, rev=None):
"""Return 1 iff the associated username is permitted to read
revision REV of the path PATH_PARTS (of type PATHTYPE) in repository
REPOS, as determined by consulting REPOS's Authorizer object (if any)."""
auth = repos.authorizer()
if not auth:
return 1
if not pathtype:
pathtype = repos.itemtype(path_parts, rev)
if not path_parts:
return auth.check_root_access(repos.rootname())
return auth.check_path_access(repos.rootname(), path_parts, pathtype, rev)

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 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
@@ -13,118 +13,65 @@
"Version Control lib driver for locally accessible cvs-repositories."
import vclib
import vcauth
import os
import os.path
import sys
import stat
import string
import re
import time
import cvsdb
import socket
import calendar
# ViewVC libs
import compat
import popen
import vclib.ccvs
def _path_join(path_parts):
return '/'.join(path_parts)
class BaseCVSRepository(vclib.Repository):
def __init__(self, name, rootpath, authorizer, utilities, charset_guesser = None):
class CVSRepository(vclib.Repository):
def __init__(self, name, rootpath):
if not os.path.isdir(rootpath):
raise vclib.ReposNotFound(name)
self.name = name
self.rootpath = rootpath
self.auth = authorizer
self.utilities = utilities
self.guesser = charset_guesser
# See if this repository is even viewable, authz-wise.
if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name)
def open(self):
# See if a universal read access determination can be made.
if self.auth and self.auth.check_universal_access(self.name) == 1:
self.auth = None
def rootname(self):
return self.name
def rootpath(self):
return self.rootpath
def roottype(self):
return vclib.CVS
def authorizer(self):
return self.auth
def itemtype(self, path_parts, rev):
basepath = self._getpath(path_parts)
kind = None
if os.path.isdir(basepath):
kind = vclib.DIR
elif os.path.isfile(basepath + ',v'):
kind = vclib.FILE
else:
atticpath = self._getpath(self._atticpath(path_parts))
if os.path.isfile(atticpath + ',v'):
kind = vclib.FILE
if not kind:
raise vclib.ItemNotFound(path_parts)
if not vclib.check_path_access(self, path_parts, kind, rev):
raise vclib.ItemNotFound(path_parts)
return kind
def itemprops(self, path_parts, rev):
self.itemtype(path_parts, rev) # does auth-check
return {} # CVS doesn't support properties
return vclib.DIR
if os.path.isfile(basepath + ',v'):
return vclib.FILE
atticpath = self._getpath(self._atticpath(path_parts))
if os.path.isfile(atticpath + ',v'):
return vclib.FILE
raise vclib.ItemNotFound(path_parts)
def listdir(self, path_parts, rev, options):
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory."
% (_path_join(path_parts)))
# Only RCS files (*,v) and subdirs are returned.
data = [ ]
full_name = self._getpath(path_parts)
for file in os.listdir(full_name):
name = None
kind, errors = _check_path(os.path.join(full_name, file))
if kind == vclib.FILE:
if file[-2:] == ',v':
name = file[:-2]
data.append(CVSDirEntry(file[:-2], kind, errors, 0))
elif kind == vclib.DIR:
if file != 'Attic' and file != 'CVS': # CVS directory is for fileattr
name = file
data.append(CVSDirEntry(file, kind, errors, 0))
else:
name = file
if not name:
continue
if vclib.check_path_access(self, path_parts + [name], kind, rev):
data.append(CVSDirEntry(name, kind, errors, 0))
data.append(CVSDirEntry(file, kind, errors, 0))
full_name = os.path.join(full_name, 'Attic')
if os.path.isdir(full_name):
for file in os.listdir(full_name):
name = None
kind, errors = _check_path(os.path.join(full_name, file))
if kind == vclib.FILE:
if file[-2:] == ',v':
name = file[:-2]
data.append(CVSDirEntry(file[:-2], kind, errors, 1))
elif kind != vclib.DIR:
name = file
if not name:
continue
if vclib.check_path_access(self, path_parts + [name], kind, rev):
data.append(CVSDirEntry(name, kind, errors, 1))
data.append(CVSDirEntry(file, kind, errors, 1))
return data
def _getpath(self, path_parts):
return apply(os.path.join, (self.rootpath,) + tuple(path_parts))
@@ -141,64 +88,48 @@ class BaseCVSRepository(vclib.Repository):
ret_file = self._getpath(ret_parts)
if not os.path.isfile(ret_file + ',v'):
raise vclib.ItemNotFound(path_parts)
if root:
ret = ret_file
else:
ret = _path_join(ret_parts)
ret = string.join(ret_parts, "/")
if v:
ret = ret + ",v"
return ret
def isexecutable(self, path_parts, rev):
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
rcsfile = self.rcsfile(path_parts, 1)
return os.access(rcsfile, os.X_OK)
def filesize(self, path_parts, rev):
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
return -1
class BinCVSRepository(CVSRepository):
def __init__(self, name, rootpath, rcs_paths):
CVSRepository.__init__(self, name, rootpath)
self.rcs_paths = rcs_paths
class BinCVSRepository(BaseCVSRepository):
def _get_tip_revision(self, rcs_file, rev=None):
"""Get the (basically) youngest revision (filtered by REV)."""
args = rcs_file,
fp = self.rcs_popen('rlog', args, 'rt', 0)
filename, default_branch, tags, lockinfo, msg, eof = _parse_log_header(fp)
filename, default_branch, tags, msg, eof = _parse_log_header(fp)
revs = []
while not eof:
revision, eof = _parse_log_entry(fp, self.guesser)
revision, eof = _parse_log_entry(fp)
if revision:
revs.append(revision)
revs = _file_log(revs, tags, lockinfo, default_branch, rev)
revs = _file_log(revs, tags, default_branch, rev)
if revs:
return revs[-1]
return None
def openfile(self, path_parts, rev, options):
"""see vclib.Repository.openfile docstring
Option values recognized by this implementation:
cvs_oldkeywords
boolean. true to use the original keyword substitution values.
"""
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
def openfile(self, path_parts, rev):
if not rev or rev == 'HEAD' or rev == 'MAIN':
rev_flag = '-p'
else:
rev_flag = '-p' + rev
if options.get('cvs_oldkeywords', 0):
kv_flag = '-ko'
else:
kv_flag = '-kkv'
full_name = self.rcsfile(path_parts, root=1, v=0)
used_rlog = 0
tip_rev = None # used only if we have to fallback to using rlog
fp = self.rcs_popen('co', (kv_flag, rev_flag, full_name), 'rb')
fp = self.rcs_popen('co', (rev_flag, full_name), 'rb')
try:
filename, revision = _parse_co_header(fp)
except COMissingRevision:
@@ -212,14 +143,14 @@ class BinCVSRepository(BaseCVSRepository):
used_rlog = 1
if not tip_rev:
raise vclib.Error("Unable to find valid revision")
fp = self.rcs_popen('co', ('-p' + tip_rev.string, full_name), 'rb')
fp = self.rcs_popen('co', ('-p' + tip_rev.string, full_name), 'rb')
filename, revision = _parse_co_header(fp)
if filename is None:
# CVSNT's co exits without any output if a dead revision is requested.
# Bug at http://www.cvsnt.org/cgi-bin/bugzilla/show_bug.cgi?id=190
# As a workaround, we invoke rlog to find the first non-dead revision
# that precedes it and check out that revision instead. Of course,
# that precedes it and check out that revision instead. Of course,
# if we've already invoked rlog above, we just reuse its output.
if not used_rlog:
tip_rev = self._get_tip_revision(full_name + ',v', rev)
@@ -228,7 +159,7 @@ class BinCVSRepository(BaseCVSRepository):
raise vclib.Error(
'Could not find non-dead revision preceding "%s"' % rev)
fp = self.rcs_popen('co', ('-p' + tip_rev.undead.string,
full_name), 'rb')
full_name), 'rb')
filename, revision = _parse_co_header(fp)
if filename is None:
@@ -258,16 +189,11 @@ class BinCVSRepository(BaseCVSRepository):
cvs_tags, cvs_branches
lists of tag and branch names encountered in the directory
"""
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory."
% (_path_join(path_parts)))
subdirs = options.get('cvs_subdirs', 0)
entries_to_fetch = []
for entry in entries:
if vclib.check_path_access(self, path_parts + [entry.name], None, rev):
entries_to_fetch.append(entry)
alltags = _get_logs(self, path_parts, entries_to_fetch, rev, subdirs, self.guesser)
dirpath = self._getpath(path_parts)
alltags = _get_logs(self, dirpath, entries, rev, subdirs)
branches = options['cvs_branches'] = []
tags = options['cvs_tags'] = []
for name, rev in alltags.items():
@@ -276,7 +202,7 @@ class BinCVSRepository(BaseCVSRepository):
else:
tags.append(name)
def itemlog(self, path_parts, rev, sortby, first, limit, options):
def itemlog(self, path_parts, rev, options):
"""see vclib.Repository.itemlog docstring
rev parameter can be a revision number, a branch number, a tag name,
@@ -296,9 +222,6 @@ class BinCVSRepository(BaseCVSRepository):
dictionary of Tag objects for all tags encountered
"""
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
# Invoke rlog
rcsfile = self.rcsfile(path_parts, 1)
if rev and options.get('cvs_pass_rev', 0):
@@ -307,60 +230,35 @@ class BinCVSRepository(BaseCVSRepository):
args = rcsfile,
fp = self.rcs_popen('rlog', args, 'rt', 0)
filename, default_branch, tags, lockinfo, msg, eof = _parse_log_header(fp)
filename, default_branch, tags, msg, eof = _parse_log_header(fp)
# Retrieve revision objects
revs = []
while not eof:
revision, eof = _parse_log_entry(fp, self.guesser)
revision, eof = _parse_log_entry(fp)
if revision:
revs.append(revision)
filtered_revs = _file_log(revs, tags, lockinfo, default_branch, rev)
filtered_revs = _file_log(revs, tags, default_branch, rev)
options['cvs_tags'] = tags
if sortby == vclib.SORTBY_DATE:
filtered_revs.sort(_logsort_date_cmp)
elif sortby == vclib.SORTBY_REV:
filtered_revs.sort(_logsort_rev_cmp)
if len(filtered_revs) < first:
return []
if limit:
return filtered_revs[first:first+limit]
return filtered_revs
def rcs_popen(self, rcs_cmd, rcs_args, mode, capture_err=1):
a = []
if self.utilities.rcsfile_socket:
a = self.utilities.rcsfile_socket.split(':')
if len(a) == 2:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((a[0], int(a[1])))
s = s.makefile()
s.write('\''+rcs_cmd+'\' \''+'\' \''.join(rcs_args)+"\'\x0d\x0a")
s.flush()
return s
elif self.utilities.cvsnt:
cmd = self.utilities.cvsnt
if self.rcs_paths.cvsnt_exe_path:
cmd = self.rcs_paths.cvsnt_exe_path
args = ['rcsfile', rcs_cmd]
args.extend(list(rcs_args))
else:
cmd = os.path.join(self.utilities.rcs_dir, rcs_cmd)
cmd = os.path.join(self.rcs_paths.rcs_path, rcs_cmd)
args = rcs_args
return popen.popen(cmd, args, mode, capture_err)
def annotate(self, path_parts, rev=None, include_text=False):
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
def annotate(self, path_parts, rev=None):
from vclib.ccvs import blame
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser, include_text)
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev)
return source, source.revision
def revinfo(self, rev):
raise vclib.UnsupportedFeature
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
"""see vclib.Repository.rawdiff docstring
@@ -368,17 +266,6 @@ class BinCVSRepository(BaseCVSRepository):
ignore_keyword_subst - boolean, ignore keyword substitution
"""
if not path_parts1:
path_parts1 = path_parts2
rev1 = '1.0'
if not path_parts2:
path_parts2 = path_parts1
rev2 = '1.0'
if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1)))
if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2)))
args = vclib._diff_args(type, options)
if options.get('ignore_keyword_subst', 0):
args.append('-kk')
@@ -386,7 +273,8 @@ class BinCVSRepository(BaseCVSRepository):
rcsfile = self.rcsfile(path_parts1, 1)
if path_parts1 != path_parts2:
raise NotImplementedError, "cannot diff across paths in cvs"
args.extend(['-N', '-r' + rev1, '-r' + rev2, rcsfile])
args.extend(['-r' + rev1, '-r' + rev2, rcsfile])
fp = self.rcs_popen('rcsdiff', args, 'rt')
# Eat up the non-GNU-diff-y headers.
@@ -395,18 +283,18 @@ class BinCVSRepository(BaseCVSRepository):
if not line or line[0:5] == 'diff ':
break
return fp
class CVSDirEntry(vclib.DirEntry):
def __init__(self, name, kind, errors, in_attic, absent=0):
def __init__(self, name, kind, errors, in_attic):
vclib.DirEntry.__init__(self, name, kind, errors)
self.in_attic = in_attic
self.absent = absent # meaning, no revisions found on requested tag
class Revision(vclib.Revision):
def __init__(self, revstr, date=None, author=None, dead=None,
changed=None, log=None):
vclib.Revision.__init__(self, _revision_tuple(revstr), revstr,
date, author, changed, log, None, None)
date, author, changed, log, None)
self.dead = dead
class Tag:
@@ -419,14 +307,6 @@ class Tag:
# ======================================================================
# Functions for dealing with Revision and Tag objects
def _logsort_date_cmp(rev1, rev2):
# sort on date; secondary on revision number
return -cmp(rev1.date, rev2.date) or -cmp(rev1.number, rev2.number)
def _logsort_rev_cmp(rev1, rev2):
# sort highest revision first
return -cmp(rev1.number, rev2.number)
def _match_revs_tags(revlist, taglist):
"""Match up a list of Revision objects with a list of Tag objects
@@ -456,9 +336,9 @@ def _match_revs_tags(revlist, taglist):
example: if revision is 1.2.3.4, parent is 1.2
"undead"
If the revision is dead, then this is a reference to the first
If the revision is dead, then this is a reference to the first
previous revision which isn't dead, otherwise it's a reference
to itself. If all the previous revisions are dead it's None.
to itself. If all the previous revisions are dead it's None.
"branch_number"
tuple representing branch number or empty tuple if on trunk
@@ -583,7 +463,7 @@ def _remove_tag(tag):
def _revision_tuple(revision_string):
"""convert a revision number into a tuple of integers"""
t = tuple(map(int, revision_string.split('.')))
t = tuple(map(int, string.split(revision_string, '.')))
if len(t) % 2 == 0:
return t
raise ValueError
@@ -591,7 +471,7 @@ def _revision_tuple(revision_string):
def _tag_tuple(revision_string):
"""convert a revision number or branch number into a tuple of integers"""
if revision_string:
t = map(int, revision_string.split('.'))
t = map(int, string.split(revision_string, '.'))
l = len(t)
if l == 1:
return ()
@@ -654,25 +534,31 @@ def _parse_co_header(fp):
raise COMalformedOutput, "Unable to find filename in co output stream"
filename = match.group(1)
# look through subsequent lines for a revision. we might encounter
# some ignorable or problematic lines along the way.
while 1:
line = fp.readline()
if not line:
break
# look for a revision.
match = _re_co_revision.match(line)
if match:
return filename, match.group(1)
elif _re_co_missing_rev.match(line) or _re_co_side_branches.match(line):
raise COMissingRevision, "Got missing revision error from co output stream"
elif _re_co_warning.match(line):
pass
else:
break
# look for a revision in the second line.
line = fp.readline()
if not line:
raise COMalformedOutput, "Missing second line from co output stream"
match = _re_co_revision.match(line)
if match:
return filename, match.group(1)
elif _re_co_missing_rev.match(line) or _re_co_side_branches.match(line):
raise COMissingRevision, "Got missing revision error from co output stream"
elif _re_co_warning.match(line):
pass
else:
raise COMalformedOutput, "Unable to find revision in co output stream"
# if we get here, the second line wasn't a revision, but it was a
# warning we can ignore. look for a revision in the third line.
line = fp.readline()
if not line:
raise COMalformedOutput, "Missing third line from co output stream"
match = _re_co_revision.match(line)
if match:
return filename, match.group(1)
raise COMalformedOutput, "Unable to find revision in co output stream"
# if your rlog doesn't use 77 '=' characters, then this must change
LOG_END_MARKER = '=' * 77 + '\n'
ENTRY_END_MARKER = '-' * 28 + '\n'
@@ -691,7 +577,7 @@ _EOF_ERROR = 'error message found' # rlog issued an error
# ^rlog\: (.*)(?:\:\d+)?\: (.*)$
#
# But for some reason the windows version of rlog omits the "rlog: " prefix
# for the first error message when the standard error stream has been
# for the first error message when the standard error stream has been
# redirected to a file or pipe. (the prefix is present in subsequent errors
# and when rlog is run from the console). So the expression below is more
# complicated
@@ -717,14 +603,13 @@ def _parse_log_header(fp):
If there is no revision information (e.g. the "-h" switch was passed to
rlog), then fp will consumed the file separator line on exit.
Returns: filename, default branch, tag dictionary, lock dictionary,
rlog error message, and eof flag
Returns: filename, default branch, tag dictionary, rlog error message,
and eof flag
"""
filename = head = branch = msg = ""
taginfo = { } # tag name => number
lockinfo = { } # revision => locker
state = 0 # 0 = base, 1 = parsing symbols, 2 = parsing locks
taginfo = { } # tag name => number
parsing_tags = 0
eof = None
while 1:
@@ -734,35 +619,24 @@ def _parse_log_header(fp):
eof = _EOF_LOG
break
if state == 1:
if parsing_tags:
if line[0] == '\t':
[ tag, rev ] = map(lambda x: x.strip(), line.split(':'))
[ tag, rev ] = map(string.strip, string.split(line, ':'))
taginfo[tag] = rev
else:
# oops. this line isn't tag info. stop parsing tags.
state = 0
parsing_tags = 0
if state == 2:
if line[0] == '\t':
[ locker, rev ] = map(lambda x: x.strip(), line.split(':'))
lockinfo[rev] = locker
else:
# oops. this line isn't lock info. stop parsing tags.
state = 0
if state == 0:
if not parsing_tags:
if line[:9] == 'RCS file:':
filename = line[10:-1]
elif line[:5] == 'head:':
head = line[6:-1]
elif line[:7] == 'branch:':
branch = line[8:-1]
elif line[:6] == 'locks:':
# start parsing the lock information
state = 2
elif line[:14] == 'symbolic names':
# start parsing the tag information
state = 1
parsing_tags = 1
elif line == ENTRY_END_MARKER:
# end of the headers
break
@@ -791,7 +665,7 @@ def _parse_log_header(fp):
eof = _EOF_ERROR
break
return filename, branch, taginfo, lockinfo, msg, eof
return filename, branch, taginfo, msg, eof
_re_log_info = re.compile(r'^date:\s+([^;]+);'
r'\s+author:\s+([^;]+);'
@@ -800,7 +674,7 @@ _re_log_info = re.compile(r'^date:\s+([^;]+);'
r'(\s+commitid:\s+([a-zA-Z0-9]+))?\n$')
### _re_rev should be updated to extract the "locked" flag
_re_rev = re.compile(r'^revision\s+([0-9.]+).*')
def _parse_log_entry(fp, guesser):
def _parse_log_entry(fp):
"""Parse a single log entry.
On entry, fp should point to the first line of the entry (the "revision"
@@ -853,7 +727,7 @@ def _parse_log_entry(fp, guesser):
return None, eof
# parse out a time tuple for the local time
tm = vclib.ccvs.cvs_strptime(match.group(1))
tm = compat.cvs_strptime(match.group(1))
# rlog seems to assume that two-digit years are 1900-based (so, "04"
# comes out as "1904", not "2004").
@@ -864,10 +738,7 @@ def _parse_log_entry(fp, guesser):
tm[0] = tm[0] + 100
if tm[0] < EPOCH:
raise ValueError, 'invalid year'
date = calendar.timegm(tm)
if guesser:
log = guesser.utf8(log)
date = compat.timegm(tm)
return Revision(rev, date,
# author, state, lines changed
@@ -894,7 +765,7 @@ def _paths_eq(path1, path2):
# ======================================================================
# Functions for interpreting and manipulating log information
def _file_log(revs, taginfo, lockinfo, cur_branch, filter):
def _file_log(revs, taginfo, cur_branch, filter):
"""Augment list of Revisions and a dictionary of Tags"""
# Add artificial ViewVC tag MAIN. If the file has a default branch, then
@@ -920,21 +791,17 @@ def _file_log(revs, taginfo, lockinfo, cur_branch, filter):
except ValueError:
view_tag = None
else:
tags.append(view_tag)
tags.append(view_tag)
# Match up tags and revisions
_match_revs_tags(revs, tags)
# Match up lockinfo and revision
for rev in revs:
rev.lockinfo = lockinfo.get(rev.string)
# Add artificial ViewVC tag HEAD, which acts like a non-branch tag pointing
# at the latest revision on the MAIN branch. The HEAD revision doesn't have
# anything to do with the "head" revision number specified in the RCS file
# and in rlog output. HEAD refers to the revision that the CVS and RCS co
# commands will check out by default, whereas the "head" field just refers
# to the highest revision on the trunk.
# to the highest revision on the trunk.
taginfo['HEAD'] = _add_tag('HEAD', taginfo['MAIN'].co_rev)
# Determine what revisions to return
@@ -972,10 +839,10 @@ def _file_log(revs, taginfo, lockinfo, cur_branch, filter):
_remove_tag(view_tag)
else:
filtered_revs = revs
return filtered_revs
def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs, guesser):
def _get_logs(repos, dirpath, entries, view_tag, get_dirs):
alltags = { # all the tags seen in the files of this dir
'MAIN' : '',
'HEAD' : '1.1'
@@ -990,15 +857,14 @@ def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs, guesser):
while len(chunk) < max_args and entries_idx < entries_len:
entry = entries[entries_idx]
path = _log_path(entry, repos._getpath(dir_path_parts), get_dirs)
path = _log_path(entry, dirpath, get_dirs)
if path:
entry.path = path
entry.idx = entries_idx
chunk.append(entry)
# set properties even if we don't retrieve logs
entry.rev = entry.date = entry.author = None
entry.dead = entry.log = entry.lockinfo = None
entry.rev = entry.date = entry.author = entry.dead = entry.log = None
entries_idx = entries_idx + 1
@@ -1018,11 +884,10 @@ def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs, guesser):
chunk_idx = 0
while chunk_idx < len(chunk):
file = chunk[chunk_idx]
filename, default_branch, taginfo, lockinfo, msg, eof \
= _parse_log_header(rlog)
filename, default_branch, taginfo, msg, eof = _parse_log_header(rlog)
if eof == _EOF_LOG:
# the rlog output ended early. this can happen on errors that rlog
# the rlog output ended early. this can happen on errors that rlog
# thinks are so serious that it stops parsing the current file and
# refuses to parse any of the files that come after it. one of the
# errors that triggers this obnoxious behavior looks like:
@@ -1058,20 +923,20 @@ def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs, guesser):
file.errors.append("rlog error: %s" % msg)
continue
tag = None
if view_tag == 'MAIN' or view_tag == 'HEAD':
tag = Tag(None, default_branch)
elif taginfo.has_key(view_tag):
tag = Tag(None, taginfo[view_tag])
elif view_tag and (eof != _EOF_FILE):
# the tag wasn't found, so skip this file (unless we already
# know there's nothing left of it to read)
elif view_tag:
# the tag wasn't found, so skip this file
_skip_file(rlog)
eof = _EOF_FILE
eof = 1
else:
tag = None
# 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
# merge the set of keys and keep values that allow us to make the
# the values point to branches or revisions. this the fastest way to
# merge the set of keys and keep values that allow us to make the
# distinction between branch tags and normal tags
alltags.update(taginfo)
@@ -1080,7 +945,7 @@ def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs, guesser):
while not eof:
# fetch one of the log entries
entry, eof = _parse_log_entry(rlog, guesser)
entry, eof = _parse_log_entry(rlog)
if not entry:
# parsing error
@@ -1107,15 +972,12 @@ def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs, guesser):
file.date = wanted_entry.date
file.author = wanted_entry.author
file.dead = file.kind == vclib.FILE and wanted_entry.dead
file.absent = 0
file.log = wanted_entry.log
file.lockinfo = lockinfo.get(file.rev)
# suppress rlog errors if we find a usable revision in the end
del file.errors[:]
elif file.kind == vclib.FILE:
file.dead = 0
#file.errors.append("No revisions exist on %s" % (view_tag or "MAIN"))
file.absent = 1
file.dead = 1
file.errors.append("No revisions exist on %s" % (view_tag or "MAIN"))
# done with this file now, skip the rest of this file's revisions
if not eof:
@@ -1228,8 +1090,6 @@ def _newest_file(dirpath):
newest_file = None
newest_time = 0
### FIXME: This sucker is leaking unauthorized paths! ###
for subfile in os.listdir(dirpath):
### filter CVS locks? stale NFS handles?
if subfile[-2:] != ',v':

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 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
@@ -9,59 +9,343 @@
# For more information, visit http://viewvc.org/
#
# -----------------------------------------------------------------------
"""
This is a Version Control library driver for locally accessible cvs-repositories.
"""
import os
import os.path
import time
import string
import re
import cStringIO
import tempfile
import vclib
import rcsparse
import blame
### The functionality shared with bincvs should probably be moved to a
### separate module
from vclib.bincvs import CVSRepository, Revision, Tag, \
_file_log, _log_path
class CCVSRepository(CVSRepository):
def dirlogs(self, path_parts, rev, entries, options):
"""see vclib.Repository.dirlogs docstring
rev can be a tag name or None. if set only information from revisions
matching the tag will be retrieved
Option values recognized by this implementation:
cvs_subdirs
boolean. true to fetch logs of the most recently modified file in each
subdirectory
Option values returned by this implementation:
cvs_tags, cvs_branches
lists of tag and branch names encountered in the directory
"""
subdirs = options.get('cvs_subdirs', 0)
dirpath = self._getpath(path_parts)
alltags = { # all the tags seen in the files of this dir
'MAIN' : '',
'HEAD' : '1.1'
}
for entry in entries:
entry.rev = entry.date = entry.author = entry.dead = entry.log = None
path = _log_path(entry, dirpath, subdirs)
if path:
entry.path = path
try:
rcsparse.Parser().parse(open(path, 'rb'), InfoSink(entry, rev, alltags))
except IOError, e:
entry.errors.append("rcsparse error: %s" % e)
except RuntimeError, e:
entry.errors.append("rcsparse error: %s" % e)
except rcsparse.RCSStopParser:
pass
branches = options['cvs_branches'] = []
tags = options['cvs_tags'] = []
for name, rev in alltags.items():
if Tag(None, rev).is_branch:
branches.append(name)
else:
tags.append(name)
def itemlog(self, path_parts, rev, options):
"""see vclib.Repository.itemlog docstring
rev parameter can be a revision number, a branch number, a tag name,
or None. If None, will return information about all revisions, otherwise,
will only return information about the specified revision or branch.
Option values returned by this implementation:
cvs_tags
dictionary of Tag objects for all tags encountered
"""
path = self.rcsfile(path_parts, 1)
sink = TreeSink()
rcsparse.Parser().parse(open(path, 'rb'), sink)
filtered_revs = _file_log(sink.revs.values(), sink.tags,
sink.default_branch, rev)
for rev in filtered_revs:
if rev.prev and len(rev.number) == 2:
rev.changed = rev.prev.next_changed
options['cvs_tags'] = sink.tags
return filtered_revs
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
temp1 = tempfile.mktemp()
open(temp1, 'wb').write(self.openfile(path_parts1, rev1)[0].getvalue())
temp2 = tempfile.mktemp()
open(temp2, 'wb').write(self.openfile(path_parts2, rev2)[0].getvalue())
r1 = self.itemlog(path_parts1, rev1, {})[-1]
r2 = self.itemlog(path_parts2, rev2, {})[-1]
info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string)
info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string)
diff_args = vclib._diff_args(type, options)
return vclib._diff_fp(temp1, temp2, info1, info2, diff_args)
def annotate(self, path_parts, rev=None):
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev)
return source, source.revision
def openfile(self, path_parts, rev=None):
path = self.rcsfile(path_parts, 1)
sink = COSink(rev)
rcsparse.Parser().parse(open(path, 'rb'), sink)
revision = sink.last and sink.last.string
return cStringIO.StringIO(string.join(sink.sstext.text, "\n")), revision
class MatchingSink(rcsparse.Sink):
"""Superclass for sinks that search for revisions based on tag or number"""
def __init__(self, find):
"""Initialize with tag name or revision number string to match against"""
if not find or find == 'MAIN' or find == 'HEAD':
self.find = None
else:
self.find = find
self.find_tag = None
def set_principal_branch(self, branch_number):
if self.find is None:
self.find_tag = Tag(None, branch_number)
def define_tag(self, name, revision):
if name == self.find:
self.find_tag = Tag(None, revision)
def admin_completed(self):
if self.find_tag is None:
if self.find is None:
self.find_tag = Tag(None, '')
else:
try:
self.find_tag = Tag(None, self.find)
except ValueError:
pass
class InfoSink(MatchingSink):
def __init__(self, entry, tag, alltags):
MatchingSink.__init__(self, tag)
self.entry = entry
self.alltags = alltags
self.matching_rev = None
self.perfect_match = 0
def define_tag(self, name, revision):
MatchingSink.define_tag(self, name, revision)
self.alltags[name] = revision
def admin_completed(self):
MatchingSink.admin_completed(self)
if self.find_tag is None:
# tag we're looking for doesn't exist
raise rcsparse.RCSStopParser
def define_revision(self, revision, date, author, state, branches, next):
if self.perfect_match:
return
tag = self.find_tag
rev = Revision(revision, date, author, state == "dead")
# perfect match if revision number matches tag number or if revision is on
# trunk and tag points to trunk. imperfect match if tag refers to a branch
# and this revision is the highest revision so far found on that branch
perfect = ((rev.number == tag.number) or
(not tag.number and len(rev.number) == 2))
if perfect or (tag.is_branch and tag.number == rev.number[:-1] and
(not self.matching_rev or
rev.number > self.matching_rev.number)):
self.matching_rev = rev
self.perfect_match = perfect
def set_revision_info(self, revision, log, text):
if self.matching_rev:
if revision == self.matching_rev.string:
self.entry.rev = self.matching_rev.string
self.entry.date = self.matching_rev.date
self.entry.author = self.matching_rev.author
self.entry.dead = self.matching_rev.dead
self.entry.log = log
raise rcsparse.RCSStopParser
else:
raise rcsparse.RCSStopParser
class TreeSink(rcsparse.Sink):
d_command = re.compile('^d(\d+)\\s(\\d+)')
a_command = re.compile('^a(\d+)\\s(\\d+)')
def __init__(self):
self.revs = { }
self.tags = { }
self.head = None
self.default_branch = None
def set_head_revision(self, revision):
self.head = revision
def set_principal_branch(self, branch_number):
self.default_branch = branch_number
def define_tag(self, name, revision):
# check !tags.has_key(tag_name)
self.tags[name] = revision
def define_revision(self, revision, date, author, state, branches, next):
# check !revs.has_key(revision)
self.revs[revision] = Revision(revision, date, author, state == "dead")
def set_revision_info(self, revision, log, text):
# check revs.has_key(revision)
rev = self.revs[revision]
rev.log = log
changed = None
added = 0
deled = 0
if self.head != revision:
changed = 1
lines = string.split(text, '\n')
idx = 0
while idx < len(lines):
command = lines[idx]
dmatch = self.d_command.match(command)
idx = idx + 1
if dmatch:
deled = deled + string.atoi(dmatch.group(2))
else:
amatch = self.a_command.match(command)
if amatch:
count = string.atoi(amatch.group(2))
added = added + count
idx = idx + count
elif command:
raise "error while parsing deltatext: %s" % command
if len(rev.number) == 2:
rev.next_changed = changed and "+%i -%i" % (deled, added)
else:
rev.changed = changed and "+%i -%i" % (added, deled)
class StreamText:
d_command = re.compile('^d(\d+)\\s(\\d+)')
a_command = re.compile('^a(\d+)\\s(\\d+)')
def __init__(self, text):
self.text = string.split(text, "\n")
def command(self, cmd):
adjust = 0
add_lines_remaining = 0
diffs = string.split(cmd, "\n")
if diffs[-1] == "":
del diffs[-1]
if len(diffs) == 0:
return
if diffs[0] == "":
del diffs[0]
for command in diffs:
if add_lines_remaining > 0:
# Insertion lines from a prior "a" command
self.text.insert(start_line + adjust, command)
add_lines_remaining = add_lines_remaining - 1
adjust = adjust + 1
continue
dmatch = self.d_command.match(command)
amatch = self.a_command.match(command)
if dmatch:
# "d" - Delete command
start_line = string.atoi(dmatch.group(1))
count = string.atoi(dmatch.group(2))
begin = start_line + adjust - 1
del self.text[begin:begin + count]
adjust = adjust - count
elif amatch:
# "a" - Add command
start_line = string.atoi(amatch.group(1))
count = string.atoi(amatch.group(2))
add_lines_remaining = count
else:
raise RuntimeError, 'Error parsing diff commands'
def secondnextdot(s, start):
# find the position the second dot after the start index.
return string.find(s, '.', string.find(s, '.', start) + 1)
def cvs_strptime(timestr):
return time.strptime(timestr, '%Y/%m/%d %H:%M:%S')[:-1] + (0,)
class COSink(MatchingSink):
def __init__(self, rev):
MatchingSink.__init__(self, rev)
def set_head_revision(self, revision):
self.head = Revision(revision)
self.last = None
self.sstext = None
def canonicalize_rootpath(rootpath):
assert os.path.isabs(rootpath)
return os.path.normpath(rootpath)
def admin_completed(self):
MatchingSink.admin_completed(self)
if self.find_tag is None:
raise vclib.InvalidRevision(self.find)
def set_revision_info(self, revision, log, text):
tag = self.find_tag
rev = Revision(revision)
def _is_cvsroot(path):
return os.path.exists(os.path.join(path, "CVSROOT", "config"))
if rev.number == tag.number:
self.log = log
depth = len(rev.number)
def expand_root_parent(parent_path):
# Each subdirectory of PARENT_PATH that contains a child
# "CVSROOT/config" is added the set of returned roots. Or, if the
# PARENT_PATH itself contains a child "CVSROOT/config", then all its
# subdirectories are returned as roots.
assert os.path.isabs(parent_path)
roots = {}
subpaths = os.listdir(parent_path)
for rootname in subpaths:
rootpath = os.path.join(parent_path, rootname)
if _is_cvsroot(parent_path) or _is_cvsroot(rootpath):
roots[rootname] = canonicalize_rootpath(rootpath)
return roots
if rev.number == self.head.number:
assert self.sstext is None
self.sstext = StreamText(text)
elif (depth == 2 and tag.number and rev.number >= tag.number[:depth]):
assert len(self.last.number) == 2
assert rev.number < self.last.number
self.sstext.command(text)
elif (depth > 2 and rev.number[:depth-1] == tag.number[:depth-1] and
(rev.number <= tag.number or len(tag.number) == depth-1)):
assert len(rev.number) - len(self.last.number) in (0, 2)
assert rev.number > self.last.number
self.sstext.command(text)
else:
rev = None
def find_root_in_parent(parent_path, rootname):
"""Search PARENT_PATH for a root named ROOTNAME, returning the
canonicalized ROOTPATH of the root if found; return None if no such
root is found."""
# Is PARENT_PATH itself a CVS repository? If so, we allow ROOTNAME
# to be any subdir within it. Otherwise, we expect
# PARENT_PATH/ROOTNAME to be a CVS repository.
assert os.path.isabs(parent_path)
rootpath = os.path.join(parent_path, rootname)
if (_is_cvsroot(parent_path) and os.path.exists(rootpath)) \
or _is_cvsroot(rootpath):
return canonicalize_rootpath(rootpath)
return None
def CVSRepository(name, rootpath, authorizer, utilities, use_rcsparse, charset_guesser = None):
rootpath = canonicalize_rootpath(rootpath)
if use_rcsparse:
import ccvs
return ccvs.CCVSRepository(name, rootpath, authorizer, utilities, charset_guesser)
else:
import bincvs
return bincvs.BinCVSRepository(name, rootpath, authorizer, utilities, charset_guesser)
if rev:
#print "tag =", tag.number, "rev =", rev.number, "<br>"
self.last = rev

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 2000 Curt Hagenlocher <curt@hagenlocher.org>
#
# By using this file, you agree to the terms and conditions set forth in
@@ -26,12 +26,11 @@
#
# -----------------------------------------------------------------------
import string
import re
import time
import math
import rcsparse
import vclib
import cvsdb
class CVSParser(rcsparse.Sink):
# Precompiled regular expressions
@@ -100,7 +99,7 @@ class CVSParser(rcsparse.Sink):
# Split deltatext specified by rev to each line.
def deltatext_split(self, rev):
lines = self.revision_deltatext[rev].split('\n')
lines = string.split(self.revision_deltatext[rev], '\n')
if lines[-1] == '':
del lines[-1]
return lines
@@ -139,16 +138,16 @@ class CVSParser(rcsparse.Sink):
adjust = adjust + 1
elif dmatch:
# "d" - Delete command
start_line = int(dmatch.group(1))
count = int(dmatch.group(2))
start_line = string.atoi(dmatch.group(1))
count = string.atoi(dmatch.group(2))
begin = start_line + adjust - 1
del text[begin:begin + count]
adjust = adjust - count
lines_removed_now = lines_removed_now + count
elif amatch:
# "a" - Add command
start_line = int(amatch.group(1))
count = int(amatch.group(2))
start_line = string.atoi(amatch.group(1))
count = string.atoi(amatch.group(2))
add_lines_remaining = count
lines_added_now = lines_added_now + count
else:
@@ -268,7 +267,7 @@ class CVSParser(rcsparse.Sink):
raise RuntimeError, ('error: %s appeared to be under CVS control, ' +
'but the RCS file is inaccessible.') % rcs_pathname
rcsparse.parse(rcsfile, self)
rcsparse.Parser().parse(rcsfile, self)
rcsfile.close()
if opt_rev in [None, '', 'HEAD']:
@@ -311,13 +310,13 @@ class CVSParser(rcsparse.Sink):
skip = skip - 1
elif dmatch:
# "d" - Delete command
start_line = int(dmatch.group(1))
count = int(dmatch.group(2))
start_line = string.atoi(dmatch.group(1))
count = string.atoi(dmatch.group(2))
line_count = line_count - count
elif amatch:
# "a" - Add command
start_line = int(amatch.group(1))
count = int(amatch.group(2))
start_line = string.atoi(amatch.group(1))
count = string.atoi(amatch.group(2))
skip = count
line_count = line_count + count
else:
@@ -347,7 +346,7 @@ class CVSParser(rcsparse.Sink):
is_trunk_revision = self.trunk_rev.match(revision) is not None
if is_trunk_revision:
diffs = self.deltatext_split(last_revision)
diffs = self.deltatext_split(last_revision)
# Revisions on the trunk specify deltas that transform a
# revision into an earlier revision, so invert the translation
@@ -359,8 +358,8 @@ class CVSParser(rcsparse.Sink):
dmatch = self.d_command.match(command)
amatch = self.a_command.match(command)
if dmatch:
start_line = int(dmatch.group(1))
count = int(dmatch.group(2))
start_line = string.atoi(dmatch.group(1))
count = string.atoi(dmatch.group(2))
temp = []
while count > 0:
temp.append(revision)
@@ -368,8 +367,8 @@ class CVSParser(rcsparse.Sink):
self.revision_map = (self.revision_map[:start_line - 1] +
temp + self.revision_map[start_line - 1:])
elif amatch:
start_line = int(amatch.group(1))
count = int(amatch.group(2))
start_line = string.atoi(amatch.group(1))
count = string.atoi(amatch.group(2))
del self.revision_map[start_line:start_line + count]
skip = count
else:
@@ -380,7 +379,7 @@ class CVSParser(rcsparse.Sink):
# the trunk. They specify deltas that transform a revision
# into a later revision.
adjust = 0
diffs = self.deltatext_split(revision)
diffs = self.deltatext_split(revision)
for command in diffs:
if skip > 0:
skip = skip - 1
@@ -388,15 +387,15 @@ class CVSParser(rcsparse.Sink):
dmatch = self.d_command.match(command)
amatch = self.a_command.match(command)
if dmatch:
start_line = int(dmatch.group(1))
count = int(dmatch.group(2))
start_line = string.atoi(dmatch.group(1))
count = string.atoi(dmatch.group(2))
adj_begin = start_line + adjust - 1
adj_end = start_line + adjust - 1 + count
del self.revision_map[adj_begin:adj_end]
adjust = adjust - count
elif amatch:
start_line = int(amatch.group(1))
count = int(amatch.group(2))
start_line = string.atoi(amatch.group(1))
count = string.atoi(amatch.group(2))
skip = count
temp = []
while count > 0:
@@ -414,7 +413,7 @@ class CVSParser(rcsparse.Sink):
class BlameSource:
def __init__(self, rcs_file, opt_rev=None, charset_guesser=None, include_text=False):
def __init__(self, rcs_file, opt_rev=None):
# Parse the CVS file
parser = CVSParser()
revision = parser.parse_cvs_file(rcs_file, opt_rev)
@@ -428,8 +427,6 @@ class BlameSource:
self.lines = lines
self.num_lines = count
self.parser = parser
self.guesser = charset_guesser
self.include_text = include_text
# keep track of where we are during an iteration
self.idx = -1
@@ -448,16 +445,10 @@ class BlameSource:
prev_rev = self.parser.prev_revision.get(rev)
line_number = idx + 1
author = self.parser.revision_author[rev]
if not self.include_text:
thisline = None
elif self.guesser:
thisline = self.guesser.utf8(self.lines[idx])
else:
thisline = self.lines[idx]
thisline = self.lines[idx]
### TODO: Put a real date in here.
item = vclib.Annotation(thisline, line_number, rev, prev_rev, author, None)
item = _item(text=thisline, line_number=line_number, rev=rev,
prev_rev=prev_rev, author=author, date=None)
self.last = item
self.idx = idx
return item
@@ -465,3 +456,9 @@ class BlameSource:
class BlameSequencingError(Exception):
pass
class _item:
def __init__(self, **kw):
vars(self).update(kw)

View File

@@ -1,417 +0,0 @@
# -*-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/
#
# -----------------------------------------------------------------------
import os
import re
import cStringIO
import tempfile
import vclib
import rcsparse
import blame
import cvsdb
### The functionality shared with bincvs should probably be moved to a
### separate module
from bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp, _path_join
class CCVSRepository(BaseCVSRepository):
def dirlogs(self, path_parts, rev, entries, options):
"""see vclib.Repository.dirlogs docstring
rev can be a tag name or None. if set only information from revisions
matching the tag will be retrieved
Option values recognized by this implementation:
cvs_subdirs
boolean. true to fetch logs of the most recently modified file in each
subdirectory
Option values returned by this implementation:
cvs_tags, cvs_branches
lists of tag and branch names encountered in the directory
"""
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory."
% (part2path(path_parts)))
entries_to_fetch = []
for entry in entries:
if vclib.check_path_access(self, path_parts + [entry.name], None, rev):
entries_to_fetch.append(entry)
subdirs = options.get('cvs_subdirs', 0)
dirpath = self._getpath(path_parts)
alltags = { # all the tags seen in the files of this dir
'MAIN' : '',
'HEAD' : '1.1'
}
for entry in entries_to_fetch:
entry.rev = entry.date = entry.author = None
entry.dead = entry.absent = entry.log = entry.lockinfo = None
path = _log_path(entry, dirpath, subdirs)
if path:
entry.path = path
try:
rcsparse.parse(open(path, 'rb'), InfoSink(entry, rev, alltags))
if self.guesser:
entry.log = self.guesser.utf8(entry.log)
except IOError, e:
entry.errors.append("rcsparse error: %s" % e)
except RuntimeError, e:
entry.errors.append("rcsparse error: %s" % e)
except rcsparse.RCSStopParser:
pass
branches = options['cvs_branches'] = []
tags = options['cvs_tags'] = []
for name, rev in alltags.items():
if Tag(None, rev).is_branch:
branches.append(name)
else:
tags.append(name)
def itemlog(self, path_parts, rev, sortby, first, limit, options):
"""see vclib.Repository.itemlog docstring
rev parameter can be a revision number, a branch number, a tag name,
or None. If None, will return information about all revisions, otherwise,
will only return information about the specified revision or branch.
Option values returned by this implementation:
cvs_tags
dictionary of Tag objects for all tags encountered
"""
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
path = self.rcsfile(path_parts, 1)
sink = TreeSink()
rcsparse.parse(open(path, 'rb'), sink)
filtered_revs = _file_log(sink.revs.values(), sink.tags, sink.lockinfo,
sink.default_branch, rev)
for rev in filtered_revs:
if rev.prev and len(rev.number) == 2:
rev.changed = rev.prev.next_changed
options['cvs_tags'] = sink.tags
if sortby == vclib.SORTBY_DATE:
filtered_revs.sort(_logsort_date_cmp)
elif sortby == vclib.SORTBY_REV:
filtered_revs.sort(_logsort_rev_cmp)
if len(filtered_revs) < first:
return []
if limit:
return filtered_revs[first:first+limit]
return filtered_revs
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
if path_parts1 and self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts1)))
if path_parts2 and self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts2)))
if not path_parts1 and not path_parts2:
raise vclib.Error("Nothing to diff.")
if path_parts1:
temp1 = tempfile.mktemp()
open(temp1, 'wb').write(self.openfile(path_parts1, rev1, {})[0].getvalue())
r1 = self.itemlog(path_parts1, rev1, vclib.SORTBY_DEFAULT, 0, 0, {})[-1]
info1 = (self.rcsfile(path_parts1, root=1, v=0), r1.date, r1.string)
else:
temp1 = '/dev/null'
info1 = ('/dev/null', '', '')
if path_parts2:
temp2 = tempfile.mktemp()
open(temp2, 'wb').write(self.openfile(path_parts2, rev2, {})[0].getvalue())
r2 = self.itemlog(path_parts2, rev2, vclib.SORTBY_DEFAULT, 0, 0, {})[-1]
info2 = (self.rcsfile(path_parts2, root=1, v=0), r2.date, r2.string)
else:
temp2 = '/dev/null'
info2 = ('/dev/null', '', '')
diff_args = vclib._diff_args(type, options)
return vclib._diff_fp(temp1, temp2, info1, info2,
self.utilities.diff or 'diff', diff_args)
def annotate(self, path_parts, rev=None, include_text=False):
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
source = blame.BlameSource(self.rcsfile(path_parts, 1), rev, self.guesser, include_text)
return source, source.revision
def revinfo(self, rev):
raise vclib.UnsupportedFeature
def openfile(self, path_parts, rev, options):
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % (_path_join(path_parts)))
path = self.rcsfile(path_parts, 1)
sink = COSink(rev)
rcsparse.parse(open(path, 'rb'), sink)
revision = sink.last and sink.last.string
return cStringIO.StringIO('\n'.join(sink.sstext.text)), revision
class MatchingSink(rcsparse.Sink):
"""Superclass for sinks that search for revisions based on tag or number"""
def __init__(self, find):
"""Initialize with tag name or revision number string to match against"""
if not find or find == 'MAIN' or find == 'HEAD':
self.find = None
else:
self.find = find
self.find_tag = None
def set_principal_branch(self, branch_number):
if self.find is None:
self.find_tag = Tag(None, branch_number)
def define_tag(self, name, revision):
if name == self.find:
self.find_tag = Tag(None, revision)
def admin_completed(self):
if self.find_tag is None:
if self.find is None:
self.find_tag = Tag(None, '')
else:
try:
self.find_tag = Tag(None, self.find)
except ValueError:
pass
class InfoSink(MatchingSink):
def __init__(self, entry, tag, alltags):
MatchingSink.__init__(self, tag)
self.entry = entry
self.alltags = alltags
self.matching_rev = None
self.perfect_match = 0
self.lockinfo = { }
self.saw_revision = False
def define_tag(self, name, revision):
MatchingSink.define_tag(self, name, revision)
self.alltags[name] = revision
def admin_completed(self):
MatchingSink.admin_completed(self)
if self.find_tag is None:
# tag we're looking for doesn't exist
if self.entry.kind == vclib.FILE:
self.entry.absent = 1
raise rcsparse.RCSStopParser
def parse_completed(self):
if not self.saw_revision:
#self.entry.errors.append("No revisions exist on %s" % (view_tag or "MAIN"))
self.entry.absent = 1
def set_locker(self, rev, locker):
self.lockinfo[rev] = locker
def define_revision(self, revision, date, author, state, branches, next):
self.saw_revision = True
if self.perfect_match:
return
tag = self.find_tag
rev = Revision(revision, date, author, state == "dead")
rev.lockinfo = self.lockinfo.get(revision)
# perfect match if revision number matches tag number or if
# revision is on trunk and tag points to trunk. imperfect match
# if tag refers to a branch and either a) this revision is the
# highest revision so far found on that branch, or b) this
# revision is the branchpoint.
perfect = ((rev.number == tag.number) or
(not tag.number and len(rev.number) == 2))
if perfect or (tag.is_branch and \
((tag.number == rev.number[:-1] and
(not self.matching_rev or
rev.number > self.matching_rev.number)) or
(rev.number == tag.number[:-1]))):
self.matching_rev = rev
self.perfect_match = perfect
def set_revision_info(self, revision, log, text):
if self.matching_rev:
if revision == self.matching_rev.string:
self.entry.rev = self.matching_rev.string
self.entry.date = self.matching_rev.date
self.entry.author = self.matching_rev.author
self.entry.dead = self.matching_rev.dead
self.entry.lockinfo = self.matching_rev.lockinfo
self.entry.absent = 0
self.entry.log = log
raise rcsparse.RCSStopParser
else:
raise rcsparse.RCSStopParser
class TreeSink(rcsparse.Sink):
d_command = re.compile('^d(\d+)\\s(\\d+)')
a_command = re.compile('^a(\d+)\\s(\\d+)')
def __init__(self):
self.revs = { }
self.tags = { }
self.head = None
self.default_branch = None
self.lockinfo = { }
def set_head_revision(self, revision):
self.head = revision
def set_principal_branch(self, branch_number):
self.default_branch = branch_number
def set_locker(self, rev, locker):
self.lockinfo[rev] = locker
def define_tag(self, name, revision):
# check !tags.has_key(tag_name)
self.tags[name] = revision
def define_revision(self, revision, date, author, state, branches, next):
# check !revs.has_key(revision)
self.revs[revision] = Revision(revision, date, author, state == "dead")
def set_revision_info(self, revision, log, text):
# check revs.has_key(revision)
rev = self.revs[revision]
rev.log = log
changed = None
added = 0
deled = 0
if self.head != revision:
changed = 1
lines = text.split('\n')
idx = 0
while idx < len(lines):
command = lines[idx]
dmatch = self.d_command.match(command)
idx = idx + 1
if dmatch:
deled = deled + int(dmatch.group(2))
else:
amatch = self.a_command.match(command)
if amatch:
count = int(amatch.group(2))
added = added + count
idx = idx + count
elif command:
raise "error while parsing deltatext: %s" % command
if len(rev.number) == 2:
rev.next_changed = changed and "+%i -%i" % (deled, added)
else:
rev.changed = changed and "+%i -%i" % (added, deled)
class StreamText:
d_command = re.compile('^d(\d+)\\s(\\d+)')
a_command = re.compile('^a(\d+)\\s(\\d+)')
def __init__(self, text):
self.text = text.split('\n')
def command(self, cmd):
adjust = 0
add_lines_remaining = 0
diffs = cmd.split('\n')
if diffs[-1] == "":
del diffs[-1]
if len(diffs) == 0:
return
if diffs[0] == "":
del diffs[0]
for command in diffs:
if add_lines_remaining > 0:
# Insertion lines from a prior "a" command
self.text.insert(start_line + adjust, command)
add_lines_remaining = add_lines_remaining - 1
adjust = adjust + 1
continue
dmatch = self.d_command.match(command)
amatch = self.a_command.match(command)
if dmatch:
# "d" - Delete command
start_line = int(dmatch.group(1))
count = int(dmatch.group(2))
begin = start_line + adjust - 1
del self.text[begin:begin + count]
adjust = adjust - count
elif amatch:
# "a" - Add command
start_line = int(amatch.group(1))
count = int(amatch.group(2))
add_lines_remaining = count
else:
raise RuntimeError, 'Error parsing diff commands'
def secondnextdot(s, start):
# find the position the second dot after the start index.
return s.find('.', s.find('.', start) + 1)
class COSink(MatchingSink):
def __init__(self, rev):
MatchingSink.__init__(self, rev)
def set_head_revision(self, revision):
self.head = Revision(revision)
self.last = None
self.sstext = None
def admin_completed(self):
MatchingSink.admin_completed(self)
if self.find_tag is None:
raise vclib.InvalidRevision(self.find)
def set_revision_info(self, revision, log, text):
tag = self.find_tag
rev = Revision(revision)
if rev.number == tag.number:
self.log = log
depth = len(rev.number)
if rev.number == self.head.number:
assert self.sstext is None
self.sstext = StreamText(text)
elif (depth == 2 and tag.number and rev.number >= tag.number[:depth]):
assert len(self.last.number) == 2
assert rev.number < self.last.number
self.sstext.command(text)
elif (depth > 2 and rev.number[:depth-1] == tag.number[:depth-1] and
(rev.number <= tag.number or len(tag.number) == depth-1)):
assert len(rev.number) - len(self.last.number) in (0, 2)
assert rev.number > self.last.number
self.sstext.command(text)
else:
rev = None
if rev:
#print "tag =", tag.number, "rev =", rev.number, "<br>"
self.last = rev

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -10,17 +10,8 @@
#
# -----------------------------------------------------------------------
"""This package provides parsing tools for RCS files.
"""This package provides parsing tools for RCS files."""
To use this package, first create a subclass of Sink. This should
declare all the callback methods you care about. Create an instance
of your class, and open() the RCS file you want to read. Then call
parse() to parse the file.
"""
# Make the "Sink" class and the various exception classes visible in this
# scope. That way, applications never need to import any of the
# sub-packages.
from common import *
try:
@@ -32,13 +23,4 @@ except ImportError:
from default import Parser
def parse(file, sink):
"""Parse an RCS file.
Parameters: FILE is the file object to parse. (I.e. an object of the
built-in Python type "file", usually created using Python's built-in
"open()" function). It should be opened in binary mode.
SINK is an instance of (some subclass of) Sink. It's methods will be
called as the file is parsed; see the definition of Sink for the
details.
"""
return Parser().parse(file, sink)

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -12,203 +12,46 @@
"""common.py: common classes and functions for the RCS parsing tools."""
import calendar
import time
import string
### compat isn't in vclib right now. need to work up a solution
import compat
class Sink:
"""Interface to be implemented by clients. The RCS parser calls this as
it parses the RCS file.
All these methods have stub implementations that do nothing, so you only
have to override the callbacks that you care about.
"""
def set_head_revision(self, revision):
"""Reports the head revision for this RCS file.
This is the value of the 'head' header in the admin section of the RCS
file. This function can only be called before admin_completed().
Parameter: REVISION is a string containing a revision number. This is
an actual revision number, not a branch number.
"""
pass
def set_principal_branch(self, branch_name):
"""Reports the principal branch for this RCS file. This is only called
if the principal branch is not trunk.
This is the value of the 'branch' header in the admin section of the RCS
file. This function can only be called before admin_completed().
Parameter: BRANCH_NAME is a string containing a branch number. If this
function is called, the parameter is typically "1.1.1", indicating the
vendor branch.
"""
pass
def set_access(self, accessors):
"""Reports the access control list for this RCS file. This function is
only called if the ACL is set. If this function is not called then
there is no ACL and all users are allowed access.
This is the value of the 'access' header in the admin section of the RCS
file. This function can only be called before admin_completed().
Parameter: ACCESSORS is a list of strings. Each string is a username.
The user is allowed access if and only if their username is in the list,
OR the user owns the RCS file on disk, OR the user is root.
Note that CVS typically doesn't use this field.
"""
pass
def define_tag(self, name, revision):
"""Reports a tag or branch definition. This function will be called
once for each tag or branch.
This is taken from the 'symbols' header in the admin section of the RCS
file. This function can only be called before admin_completed().
Parameters: NAME is a string containing the tag or branch name.
REVISION is a string containing a revision number. This may be
an actual revision number (for a tag) or a branch number.
The revision number consists of a number of decimal components separated
by dots. There are three common forms. If there are an odd number of
components, it's a branch. Otherwise, if the next-to-last component is
zero, it's a branch (and the next-to-last component is an artifact of
CVS and should not be shown to the user). Otherwise, it's a tag.
This function is called in the order that the tags appear in the RCS
file header. For CVS, this appears to be in reverse chronological
order of tag/branch creation.
"""
pass
def set_locker(self, revision, locker):
"""Reports a lock on this RCS file. This function will be called once
for each lock.
This is taken from the 'locks' header in the admin section of the RCS
file. This function can only be called before admin_completed().
Parameters: REVISION is a string containing a revision number. This is
an actual revision number, not a branch number.
LOCKER is a string containing a username.
"""
def set_access(self, accessors):
pass
def set_locking(self, mode):
"""Signals strict locking mode. This function will be called if and
only if the RCS file is in strict locking mode.
This is taken from the 'strict' header in the admin section of the RCS
file. This function can only be called before admin_completed().
Parameters: MODE is always the string 'strict'.
"""
pass
def set_comment(self, comment):
"""Reports the comment for this RCS file.
This is the value of the 'comment' header in the admin section of the
RCS file. This function can only be called before admin_completed().
Parameter: COMMENT is a string containing the comment. This may be
multi-line.
This field does not seem to be used by CVS.
"""
pass
def set_expansion(self, mode):
"""Reports the keyword expansion mode for this RCS file.
This is the value of the 'expand' header in the admin section of the
RCS file. This function can only be called before admin_completed().
Parameter: MODE is a string containing the keyword expansion mode.
Possible values include 'o' and 'b', amongst others.
"""
pass
def set_locking(self, mode):
"""Used to signal locking mode.
def admin_completed(self):
"""Reports that the initial RCS header has been parsed. This function is
called exactly once.
"""
Called with mode argument 'strict' if strict locking
Not called when no locking used."""
pass
def set_locker(self, revision, locker):
pass
def set_comment(self, comment):
pass
def set_description(self, description):
pass
def define_revision(self, revision, timestamp, author, state,
branches, next):
"""Reports metadata about a single revision.
This function is called for each revision. It is called later than
admin_completed() and earlier than tree_completed().
Parameter: REVISION is a revision number, as a string. This is an
actual revision number, not a branch number.
TIMESTAMP is the date and time that the revision was created, as an
integer number of seconds since the epoch. (I.e. "UNIX time" format).
AUTHOR is the author name, as a string.
STATE is the state of the revision, as a string. Common values are
"Exp" and "dead".
BRANCHES is a list of strings, with each string being an actual
revision number (not a branch number). For each branch which is based
on this revision and has commits, the revision number of the first
branch commit is listed here.
NEXT is either None or a string representing an actual revision number
(not a branch number).
When on trunk, NEXT points to what humans might consider to be the
'previous' revision number. For example, 1.3's NEXT is 1.2.
However, on a branch, NEXT really does point to what humans would
consider to be the 'next' revision number. For example, 1.1.2.1's
NEXT would be 1.1.2.2.
In other words, NEXT always means "where to find the next deltatext
that you need this revision to retrieve".
"""
pass
def tree_completed(self):
"""Reports that the RCS revision tree has been parsed. This function is
called exactly once. This function will be called later than
admin_completed().
"""
pass
def set_description(self, description):
"""Reports the description from the RCS file. This is set using the
"-m" flag to "cvs add". However, many CVS users don't use that option,
so this is often empty.
This function is called once, after tree_completed().
Parameter: DESCRIPTION is a string containing the description. This may
be multi-line.
"""
pass
def set_revision_info(self, revision, log, text):
"""Reports the log message and contents of a CVS revision.
This function is called for each revision. It is called later than
set_description().
Parameters: REVISION is a string containing the actual revision number.
LOG is a string containing the log message. This may be multi-line.
TEXT is the contents of the file in this revision, either as full-text or
as a diff. This is usually multi-line, and often quite large and/or
binary.
"""
pass
def admin_completed(self):
pass
def tree_completed(self):
pass
def parse_completed(self):
"""Reports that parsing an RCS file is complete.
This function is called once. After it is called, no more calls will be
made via this interface.
"""
pass
@@ -219,205 +62,97 @@ class Sink:
class RCSParseError(Exception):
pass
class RCSIllegalCharacter(RCSParseError):
pass
### need more work on this one
class RCSExpected(RCSParseError):
def __init__(self, got, wanted):
RCSParseError.__init__(
self,
'Unexpected parsing error in RCS file.\n'
'Expected token: %s, but saw: %s'
% (wanted, got)
)
RCSParseError.__init__(self, got, wanted)
class RCSStopParser(Exception):
pass
# --------------------------------------------------------------------------
#
# STANDARD TOKEN STREAM-BASED PARSER
#
class _Parser:
stream_class = None # subclasses need to define this
def _read_until_semicolon(self):
"""Read all tokens up to and including the next semicolon token.
Return the tokens (not including the semicolon) as a list."""
tokens = []
while 1:
token = self.ts.get()
if token == ';':
break
tokens.append(token)
return tokens
def _parse_admin_head(self, token):
rev = self.ts.get()
if rev == ';':
# The head revision is not specified. Just drop the semicolon
# on the floor.
pass
else:
self.sink.set_head_revision(rev)
self.ts.match(';')
def _parse_admin_branch(self, token):
branch = self.ts.get()
if branch != ';':
self.sink.set_principal_branch(branch)
self.ts.match(';')
def _parse_admin_access(self, token):
accessors = self._read_until_semicolon()
if accessors:
self.sink.set_access(accessors)
def _parse_admin_symbols(self, token):
while 1:
tag_name = self.ts.get()
if tag_name == ';':
break
self.ts.match(':')
tag_rev = self.ts.get()
self.sink.define_tag(tag_name, tag_rev)
def _parse_admin_locks(self, token):
while 1:
locker = self.ts.get()
if locker == ';':
break
self.ts.match(':')
rev = self.ts.get()
self.sink.set_locker(rev, locker)
def _parse_admin_strict(self, token):
self.sink.set_locking("strict")
self.ts.match(';')
def _parse_admin_comment(self, token):
self.sink.set_comment(self.ts.get())
self.ts.match(';')
def _parse_admin_expand(self, token):
expand_mode = self.ts.get()
self.sink.set_expansion(expand_mode)
self.ts.match(';')
admin_token_map = {
'head' : _parse_admin_head,
'branch' : _parse_admin_branch,
'access' : _parse_admin_access,
'symbols' : _parse_admin_symbols,
'locks' : _parse_admin_locks,
'strict' : _parse_admin_strict,
'comment' : _parse_admin_comment,
'expand' : _parse_admin_expand,
'desc' : None,
}
stream_class = None # subclasses need to define this
def parse_rcs_admin(self):
while 1:
# Read initial token at beginning of line
token = self.ts.get()
try:
f = self.admin_token_map[token]
except KeyError:
# We're done once we reach the description of the RCS tree
if token[0] in string.digits:
self.ts.unget(token)
return
else:
# Chew up "newphrase"
# warn("Unexpected RCS token: $token\n")
while self.ts.get() != ';':
pass
else:
if f is None:
self.ts.unget(token)
return
else:
f(self, token)
def _parse_rcs_tree_entry(self, revision):
# Parse date
self.ts.match('date')
date = self.ts.get()
self.ts.match(';')
# Convert date into standard UNIX time format (seconds since epoch)
date_fields = string.split(date, '.')
# According to rcsfile(5): the year "contains just the last two
# digits of the year for years from 1900 through 1999, and all the
# digits of years thereafter".
if len(date_fields[0]) == 2:
date_fields[0] = '19' + date_fields[0]
date_fields = map(string.atoi, date_fields)
EPOCH = 1970
if date_fields[0] < EPOCH:
raise ValueError, 'invalid year for revision %s' % (revision,)
try:
timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,))
except ValueError, e:
raise ValueError, 'invalid date for revision %s: %s' % (revision, e,)
# Parse author
### NOTE: authors containing whitespace are violations of the
### RCS specification. We are making an allowance here because
### CVSNT is known to produce these sorts of authors.
self.ts.match('author')
author = ' '.join(self._read_until_semicolon())
# Parse state
self.ts.match('state')
state = ''
while 1:
token = self.ts.get()
if token == ';':
break
state = state + token + ' '
state = state[:-1] # toss the trailing space
# Parse branches
self.ts.match('branches')
branches = self._read_until_semicolon()
# Parse revision of next delta in chain
self.ts.match('next')
next = self.ts.get()
if next == ';':
next = None
else:
self.ts.match(';')
# there are some files with extra tags in them. for example:
# owner 640;
# group 15;
# permissions 644;
# hardlinks @configure.in@;
# commitid mLiHw3bulRjnTDGr;
# this is "newphrase" in RCSFILE(5). we just want to skip over these.
while 1:
token = self.ts.get()
if token == 'desc' or token[0] in string.digits:
# We're done once we reach the description of the RCS tree
if token[0] in string.digits:
self.ts.unget(token)
break
# consume everything up to the semicolon
self._read_until_semicolon()
return
self.sink.define_revision(revision, timestamp, author, state, branches,
next)
if token == "head":
semi, rev = self.ts.mget(2)
self.sink.set_head_revision(rev)
if semi != ';':
raise RCSExpected(semi, ';')
elif token == "branch":
semi, branch = self.ts.mget(2)
if semi == ';':
self.sink.set_principal_branch(branch)
else:
if branch == ';':
self.ts.unget(semi);
else:
raise RCSExpected(semi, ';')
elif token == "symbols":
while 1:
tag = self.ts.get()
if tag == ';':
break
self.ts.match(':')
tag_name = tag
tag_rev = self.ts.get()
self.sink.define_tag(tag_name, tag_rev)
elif token == "comment":
semi, comment = self.ts.mget(2)
self.sink.set_comment(comment)
if semi != ';':
raise RCSExpected(semi, ';')
elif token == "expand":
semi, expand_mode = self.ts.mget(2)
self.sink.set_expansion(expand_mode)
if semi != ';':
raise RCSExpected(semi, ';')
elif token == "locks":
while 1:
tag = self.ts.get()
if tag == ';':
break
(locker, rev) = string.split(tag,':')
self.sink.set_locker(rev, locker)
tag = self.ts.get()
if tag == "strict":
self.sink.set_locking("strict")
self.ts.match(';')
else:
self.ts.unget(tag)
elif token == "access":
accessors = []
while 1:
tag = self.ts.get()
if tag == ';':
if accessors != []:
self.sink.set_access(accessors)
break
accessors = accessors + [ tag ]
# Chew up "newphrase"
else:
pass
# warn("Unexpected RCS token: $token\n")
raise RuntimeError, "Unexpected EOF"
def parse_rcs_tree(self):
while 1:
@@ -428,7 +163,86 @@ class _Parser:
self.ts.unget(revision)
return
self._parse_rcs_tree_entry(revision)
# Parse date
semi, date, sym = self.ts.mget(3)
if sym != 'date':
raise RCSExpected(sym, 'date')
if semi != ';':
raise RCSExpected(semi, ';')
# Convert date into timestamp
date_fields = string.split(date, '.') + ['0', '0', '0']
date_fields = map(string.atoi, date_fields)
# need to make the date four digits for timegm
EPOCH = 1970
if date_fields[0] < EPOCH:
if date_fields[0] < 70:
date_fields[0] = date_fields[0] + 2000
else:
date_fields[0] = date_fields[0] + 1900
if date_fields[0] < EPOCH:
raise ValueError, 'invalid year'
timestamp = compat.timegm(tuple(date_fields))
# Parse author
### NOTE: authors containing whitespace are violations of the
### RCS specification. We are making an allowance here because
### CVSNT is known to produce these sorts of authors.
self.ts.match('author')
author = ''
while 1:
token = self.ts.get()
if token == ';':
break
author = author + token + ' '
author = author[:-1] # toss the trailing space
# Parse state
self.ts.match('state')
state = ''
while 1:
token = self.ts.get()
if token == ';':
break
state = state + token + ' '
state = state[:-1] # toss the trailing space
# Parse branches
self.ts.match('branches')
branches = [ ]
while 1:
token = self.ts.get()
if token == ';':
break
branches.append(token)
# Parse revision of next delta in chain
next, sym = self.ts.mget(2)
if sym != 'next':
raise RCSExpected(sym, 'next')
if next == ';':
next = None
else:
self.ts.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:
token = self.ts.get()
if token == 'desc' or token[0] in string.digits:
self.ts.unget(token)
break
# consume everything up to the semicolon
while self.ts.get() != ';':
pass
self.sink.define_revision(revision, timestamp, author, state, branches,
next)
def parse_rcs_description(self):
self.ts.match('desc')
@@ -450,15 +264,6 @@ class _Parser:
self.sink.set_revision_info(revision, log, text)
def parse(self, file, sink):
"""Parse an RCS file.
Parameters: FILE is the file object to parse. (I.e. an object of the
built-in Python type "file", usually created using Python's built-in
"open()" function).
SINK is an instance of (some subclass of) Sink. It's methods will be
called as the file is parsed; see the definition of Sink for the
details.
"""
self.ts = self.stream_class(file)
self.sink = sink

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 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

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -19,18 +19,14 @@ import string
import common
class _TokenStream:
token_term = string.whitespace + ";:"
try:
token_term = frozenset(token_term)
except NameError:
pass
token_term = string.whitespace + ';'
# the algorithm is about the same speed for any CHUNK_SIZE chosen.
# grab a good-sized chunk, but not too large to overwhelm memory.
# note: we use a multiple of a standard block size
CHUNK_SIZE = 192 * 512 # about 100k
# CHUNK_SIZE = 5 # for debugging, make the function grind...
# CHUNK_SIZE = 5 # for debugging, make the function grind...
def __init__(self, file):
self.rcsfile = file
@@ -48,17 +44,15 @@ class _TokenStream:
# out more complex solutions.
buf = self.buf
lbuf = len(buf)
idx = self.idx
while 1:
if idx == lbuf:
if idx == len(buf):
buf = self.rcsfile.read(self.CHUNK_SIZE)
if buf == '':
# signal EOF by returning None as the token
del self.buf # so we fail if get() is called again
del self.buf # so we fail if get() is called again
return None
lbuf = len(buf)
idx = 0
if buf[idx] not in string.whitespace:
@@ -66,28 +60,27 @@ class _TokenStream:
idx = idx + 1
if buf[idx] in ';:':
if buf[idx] == ';':
self.buf = buf
self.idx = idx + 1
return buf[idx]
return ';'
if buf[idx] != '@':
end = idx + 1
token = ''
while 1:
# find token characters in the current buffer
while end < lbuf and buf[end] not in self.token_term:
while end < len(buf) and buf[end] not in self.token_term:
end = end + 1
token = token + buf[idx:end]
if end < lbuf:
if end < len(buf):
# we stopped before the end, so we have a full token
idx = end
break
# we stopped at the end of the buffer, so we may have a partial token
buf = self.rcsfile.read(self.CHUNK_SIZE)
lbuf = len(buf)
idx = end = 0
self.buf = buf
@@ -101,24 +94,22 @@ class _TokenStream:
chunks = [ ]
while 1:
if idx == lbuf:
if idx == len(buf):
idx = 0
buf = self.rcsfile.read(self.CHUNK_SIZE)
if buf == '':
raise RuntimeError, 'EOF'
lbuf = len(buf)
i = string.find(buf, '@', idx)
if i == -1:
chunks.append(buf[idx:])
idx = lbuf
idx = len(buf)
continue
if i == lbuf - 1:
if i == len(buf) - 1:
chunks.append(buf[idx:i])
idx = 0
buf = '@' + self.rcsfile.read(self.CHUNK_SIZE)
if buf == '@':
raise RuntimeError, 'EOF'
lbuf = len(buf)
continue
if buf[i + 1] == '@':
chunks.append(buf[idx:i+1])
@@ -143,7 +134,8 @@ class _TokenStream:
token = self.get()
if token != match:
raise common.RCSExpected(token, match)
raise RuntimeError, ('Unexpected parsing error in RCS file.\n' +
'Expected token: %s, but saw: %s' % (match, token))
def unget(self, token):
"Put this token back, for the next get() to return."
@@ -174,3 +166,75 @@ class _TokenStream:
class Parser(common._Parser):
stream_class = _TokenStream
def parse_rcs_admin(self):
while 1:
# Read initial token at beginning of line
token = self.ts.get()
# We're done once we reach the description of the RCS tree
if token[0] in string.digits:
self.ts.unget(token)
return
if token == "head":
semi, rev = self.ts.mget(2)
self.sink.set_head_revision(rev)
if semi != ';':
raise common.RCSExpected(semi, ';')
elif token == "branch":
semi, branch = self.ts.mget(2)
if semi == ';':
self.sink.set_principal_branch(branch)
else:
if branch == ';':
self.ts.unget(semi);
else:
raise common.RCSExpected(semi, ';')
elif token == "symbols":
while 1:
tag = self.ts.get()
if tag == ';':
break
(tag_name, tag_rev) = string.split(tag, ':')
self.sink.define_tag(tag_name, tag_rev)
elif token == "comment":
semi, comment = self.ts.mget(2)
self.sink.set_comment(comment)
if semi != ';':
raise common.RCSExpected(semi, ';')
elif token == "expand":
semi, expand_mode = self.ts.mget(2)
self.sink.set_expansion(expand_mode)
if semi != ';':
raise RCSExpected(semi, ';')
elif token == "locks":
while 1:
tag = self.ts.get()
if tag == ';':
break
(locker, rev) = string.split(tag,':')
self.sink.set_locker(rev, locker)
tag = self.ts.get()
if tag == "strict":
self.sink.set_locking("strict")
self.ts.match(';')
else:
self.ts.unget(tag)
elif token == "access":
accessors = []
while 1:
tag = self.ts.get()
if tag == ';':
if accessors != []:
self.sink.set_access(accessors)
break
accessors = accessors + [ tag ]
# Chew up "newphrase".
else:
pass
# warn("Unexpected RCS token: $token\n")
raise RuntimeError, "Unexpected EOF"

View File

@@ -1,73 +0,0 @@
#! /usr/bin/python
# (Be in -*- python -*- mode.)
#
# ====================================================================
# Copyright (c) 2006-2007 CollabNet. All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals. For exact contribution history, see the revision
# history and logs, available at http://cvs2svn.tigris.org/.
# ====================================================================
"""Parse an RCS file, showing the rcsparse callbacks that are called.
This program is useful to see whether an RCS file has a problem (in
the sense of not being parseable by rcsparse) and also to illuminate
the correspondence between RCS file contents and rcsparse callbacks.
The output of this program can also be considered to be a kind of
'canonical' format for RCS files, at least in so far as rcsparse
returns all relevant information in the file and provided that the
order of callbacks is always the same."""
import sys
import os
class Logger:
def __init__(self, f, name):
self.f = f
self.name = name
def __call__(self, *args):
self.f.write(
'%s(%s)\n' % (self.name, ', '.join(['%r' % arg for arg in args]),)
)
class LoggingSink:
def __init__(self, f):
self.f = f
def __getattr__(self, name):
return Logger(self.f, name)
if __name__ == '__main__':
# Since there is nontrivial logic in __init__.py, we have to import
# parse() via that file. First make sure that the directory
# containing this script is in the path:
sys.path.insert(0, os.path.dirname(sys.argv[0]))
from __init__ import parse
if sys.argv[1:]:
for path in sys.argv[1:]:
if os.path.isfile(path) and path.endswith(',v'):
parse(
open(path, 'rb'), LoggingSink(sys.stdout)
)
else:
sys.stderr.write('%r is being ignored.\n' % path)
else:
parse(sys.stdin, LoggingSink(sys.stdout))

View File

@@ -1,73 +0,0 @@
#! /usr/bin/python
# (Be in -*- python -*- mode.)
#
# ====================================================================
# Copyright (c) 2007 CollabNet. All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals. For exact contribution history, see the revision
# history and logs, available at http://viewvc.tigris.org/.
# ====================================================================
"""Run tests of rcsparse code."""
import sys
import os
import glob
from cStringIO import StringIO
from difflib import Differ
# Since there is nontrivial logic in __init__.py, we have to import
# parse() via that file. First make sure that the directory
# containing this script is in the path:
script_dir = os.path.dirname(sys.argv[0])
sys.path.insert(0, script_dir)
from __init__ import parse
from parse_rcs_file import LoggingSink
test_dir = os.path.join(script_dir, 'test-data')
filelist = glob.glob(os.path.join(test_dir, '*,v'))
filelist.sort()
all_tests_ok = 1
for filename in filelist:
sys.stderr.write('%s: ' % (filename,))
f = StringIO()
try:
parse(open(filename, 'rb'), LoggingSink(f))
except Exception, e:
sys.stderr.write('Error parsing file: %s!\n' % (e,))
all_tests_ok = 0
else:
output = f.getvalue()
expected_output_filename = filename[:-2] + '.out'
expected_output = open(expected_output_filename, 'rb').read()
if output == expected_output:
sys.stderr.write('OK\n')
else:
sys.stderr.write('Output does not match expected output!\n')
differ = Differ()
for diffline in differ.compare(
expected_output.splitlines(1), output.splitlines(1)
):
sys.stderr.write(diffline)
all_tests_ok = 0
if all_tests_ok:
sys.exit(0)
else:
sys.exit(1)

View File

@@ -1,102 +0,0 @@
head 1.2;
access;
symbols
B_SPLIT:1.2.0.4
B_MIXED:1.2.0.2
T_MIXED:1.2
B_FROM_INITIALS_BUT_ONE:1.1.1.1.0.4
B_FROM_INITIALS:1.1.1.1.0.2
T_ALL_INITIAL_FILES_BUT_ONE:1.1.1.1
T_ALL_INITIAL_FILES:1.1.1.1
vendortag:1.1.1.1
vendorbranch:1.1.1;
locks; strict;
comment @# @;
1.2
date 2003.05.23.00.17.53; author jrandom; state Exp;
branches
1.2.2.1
1.2.4.1;
next 1.1;
1.1
date 98.05.22.23.20.19; author jrandom; state Exp;
branches
1.1.1.1;
next ;
1.1.1.1
date 98.05.22.23.20.19; author jrandom; state Exp;
branches;
next ;
1.2.2.1
date 2003.05.23.00.31.36; author jrandom; state Exp;
branches;
next ;
1.2.4.1
date 2003.06.03.03.20.31; author jrandom; state Exp;
branches;
next ;
desc
@@
1.2
log
@Second commit to proj, affecting all 7 files.
@
text
@This is the file `default' in the top level of the project.
Every directory in the `proj' project has a file named `default'.
This line was added in the second commit (affecting all 7 files).
@
1.2.4.1
log
@First change on branch B_SPLIT.
This change excludes sub3/default, because it was not part of this
commit, and sub1/subsubB/default, which is not even on the branch yet.
@
text
@a5 2
First change on branch B_SPLIT.
@
1.2.2.1
log
@Modify three files, on branch B_MIXED.
@
text
@a5 2
This line was added on branch B_MIXED only (affecting 3 files).
@
1.1
log
@Initial revision
@
text
@d4 2
@
1.1.1.1
log
@Initial import.
@
text
@@

View File

@@ -1,26 +0,0 @@
set_head_revision('1.2')
define_tag('B_SPLIT', '1.2.0.4')
define_tag('B_MIXED', '1.2.0.2')
define_tag('T_MIXED', '1.2')
define_tag('B_FROM_INITIALS_BUT_ONE', '1.1.1.1.0.4')
define_tag('B_FROM_INITIALS', '1.1.1.1.0.2')
define_tag('T_ALL_INITIAL_FILES_BUT_ONE', '1.1.1.1')
define_tag('T_ALL_INITIAL_FILES', '1.1.1.1')
define_tag('vendortag', '1.1.1.1')
define_tag('vendorbranch', '1.1.1')
set_locking('strict')
set_comment('# ')
admin_completed()
define_revision('1.2', 1053649073, 'jrandom', 'Exp', ['1.2.2.1', '1.2.4.1'], '1.1')
define_revision('1.1', 895879219, 'jrandom', 'Exp', ['1.1.1.1'], None)
define_revision('1.1.1.1', 895879219, 'jrandom', 'Exp', [], None)
define_revision('1.2.2.1', 1053649896, 'jrandom', 'Exp', [], None)
define_revision('1.2.4.1', 1054610431, 'jrandom', 'Exp', [], None)
tree_completed()
set_description('')
set_revision_info('1.2', 'Second commit to proj, affecting all 7 files.\n', "This is the file `default' in the top level of the project.\n\nEvery directory in the `proj' project has a file named `default'.\n\nThis line was added in the second commit (affecting all 7 files).\n")
set_revision_info('1.2.4.1', 'First change on branch B_SPLIT.\n\nThis change excludes sub3/default, because it was not part of this\ncommit, and sub1/subsubB/default, which is not even on the branch yet.\n', 'a5 2\n\nFirst change on branch B_SPLIT.\n')
set_revision_info('1.2.2.1', 'Modify three files, on branch B_MIXED.\n', 'a5 2\n\nThis line was added on branch B_MIXED only (affecting 3 files).\n')
set_revision_info('1.1', 'Initial revision\n', 'd4 2\n')
set_revision_info('1.1.1.1', 'Initial import.\n', '')
parse_completed()

View File

@@ -1,10 +0,0 @@
head ;
access;
symbols;
locks; strict;
comment @# @;
desc
@@

View File

@@ -1,6 +0,0 @@
set_locking('strict')
set_comment('# ')
admin_completed()
tree_completed()
set_description('')
parse_completed()

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 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
@@ -25,7 +25,7 @@ _tt = TextTools
_idchar_list = map(chr, range(33, 127)) + map(chr, range(160, 256))
_idchar_list.remove('$')
_idchar_list.remove(',')
#_idchar_list.remove('.') # leave as part of 'num' symbol
#_idchar_list.remove('.') leave as part of 'num' symbol
_idchar_list.remove(':')
_idchar_list.remove(';')
_idchar_list.remove('@')
@@ -41,10 +41,10 @@ _T_STRING_START = 40
_T_STRING_SPAN = 60
_T_STRING_END = 70
_E_COMPLETE = 100 # ended on a complete token
_E_TOKEN = 110 # ended mid-token
_E_STRING_SPAN = 130 # ended within a string
_E_STRING_END = 140 # ended with string-end ('@') (could be mid-@@)
_E_COMPLETE = 100 # ended on a complete token
_E_TOKEN = 110 # ended mid-token
_E_STRING_SPAN = 130 # ended within a string
_E_STRING_END = 140 # ended with string-end ('@') (could be mid-@@)
_SUCCESS = +100
@@ -65,7 +65,7 @@ class _mxTokenStream:
# note: we use a multiple of a standard block size
CHUNK_SIZE = 192 * 512 # about 100k
# CHUNK_SIZE = 5 # for debugging, make the function grind...
# CHUNK_SIZE = 5 # for debugging, make the function grind...
def __init__(self, file):
self.rcsfile = file
@@ -83,84 +83,40 @@ class _mxTokenStream:
# construct a tag table which refers to the buffer we need to parse.
table = (
#1: ignore whitespace. with or without whitespace, move to the next rule.
# ignore whitespace. with or without whitespace, move to the next rule.
(None, _tt.AllInSet, _tt.whitespace_set, +1),
#2
(_E_COMPLETE, _tt.EOF + _tt.AppendTagobj, _tt.Here, +1, _SUCCESS),
#3: accumulate token text and exit, or move to the next rule.
# accumulate token text and exit, or move to the next rule.
(_UNUSED, _tt.AllInSet + _tt.AppendMatch, _idchar_set, +2),
#4
(_E_TOKEN, _tt.EOF + _tt.AppendTagobj, _tt.Here, -3, _SUCCESS),
#5: single character tokens exit immediately, or move to the next rule
# single character tokens exit immediately, or move to the next rule
(_UNUSED, _tt.IsInSet + _tt.AppendMatch, _onechar_token_set, +2),
#6
(_E_COMPLETE, _tt.EOF + _tt.AppendTagobj, _tt.Here, -5, _SUCCESS),
#7: if this isn't an '@' symbol, then we have a syntax error (go to a
# if this isn't an '@' symbol, then we have a syntax error (go to a
# negative index to indicate that condition). otherwise, suck it up
# and move to the next rule.
(_T_STRING_START, _tt.Is + _tt.AppendTagobj, '@'),
#8
(None, _tt.Is, '@', +4, +1),
#9
(buf, _tt.Is, '@', +1, -1),
#10
(_T_STRING_END, _tt.Skip + _tt.AppendTagobj, 0, 0, +1),
#11
(_E_STRING_END, _tt.EOF + _tt.AppendTagobj, _tt.Here, -10, _SUCCESS),
#12
(_E_STRING_SPAN, _tt.EOF + _tt.AppendTagobj, _tt.Here, +1, _SUCCESS),
#13: suck up everything that isn't an AT. go to next rule to look for EOF
# suck up everything that isn't an AT. go to next rule to look for EOF
(buf, _tt.AllInSet, _not_at_set, 0, +1),
#14: go back to look for double AT if we aren't at the end of the string
# go back to look for double AT if we aren't at the end of the string
(_E_STRING_SPAN, _tt.EOF + _tt.AppendTagobj, _tt.Here, -6, _SUCCESS),
)
# Fast, texttools may be, but it's somewhat lacking in clarity.
# Here's an attempt to document the logic encoded in the table above:
#
# Flowchart:
# _____
# / /\
# 1 -> 2 -> 3 -> 5 -> 7 -> 8 -> 9 -> 10 -> 11
# | \/ \/ \/ /\ \/
# \ 4 6 12 14 /
# \_______/_____/ \ / /
# \ 13 /
# \__________________________________________/
#
# #1: Skip over any whitespace.
# #2: If now EOF, exit with code _E_COMPLETE.
# #3: If we have a series of characters in _idchar_set, then:
# #4: Output them as a token, and go back to #1.
# #5: If we have a character in _onechar_token_set, then:
# #6: Output it as a token, and go back to #1.
# #7: If we do not have an '@', then error.
# If we do, then log a _T_STRING_START and continue.
# #8: If we have another '@', continue on to #9. Otherwise:
# #12: If now EOF, exit with code _E_STRING_SPAN.
# #13: Record the slice up to the next '@' (or EOF).
# #14: If now EOF, exit with code _E_STRING_SPAN.
# Otherwise, go back to #8.
# #9: If we have another '@', then we've just seen an escaped
# (by doubling) '@' within an @-string. Record a slice including
# just one '@' character, and jump back to #8.
# Otherwise, we've *either* seen the terminating '@' of an @-string,
# *or* we've seen one half of an escaped @@ sequence that just
# happened to be split over a chunk boundary - in either case,
# we continue on to #10.
# #10: Log a _T_STRING_END.
# #11: If now EOF, exit with _E_STRING_END. Otherwise, go back to #1.
success, taglist, idx = _tt.tag(buf, table, start)
if not success:
@@ -323,11 +279,16 @@ class _mxTokenStream:
def match(self, match):
if self.tokens:
token = self.tokens.pop()
if token != match:
raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
'Expected token: %s, but saw: %s'
% (match, token))
else:
token = self.get()
if token != match:
raise common.RCSExpected(token, match)
if token != match:
raise RuntimeError, ('Unexpected parsing error in RCS file.\n'
'Expected token: %s, but saw: %s'
% (match, token))
def unget(self, token):
self.tokens.append(token)

View File

@@ -1,320 +0,0 @@
# -*-python-*-
"Version Control lib driver for locally accessible Git repositories based on the GitPython library"
import vclib
import os
import os.path
import cStringIO
import time
import tempfile
import popen
import re
import urllib
from git import *
def _allow_all(root, path, pool):
"""Generic authz_read_func that permits access to all paths"""
return 1
def _path_parts(path):
return filter(None, path.split('/'))
def _cleanup_path(path):
return '/'.join(_path_parts(path))
def _compare_paths(path1, path2):
path1_len = len (path1);
path2_len = len (path2);
min_len = min(path1_len, path2_len)
i = 0
# Are the paths exactly the same?
if path1 == path2:
return 0
# Skip past common prefix
while (i < min_len) and (path1[i] == path2[i]):
i = i + 1
# Children of paths are greater than their parents, but less than
# greater siblings of their parents
char1 = '\0'
char2 = '\0'
if (i < path1_len):
char1 = path1[i]
if (i < path2_len):
char2 = path2[i]
if (char1 == '/') and (i == path2_len):
return 1
if (char2 == '/') and (i == path1_len):
return -1
if (i < path1_len) and (char1 == '/'):
return -1
if (i < path2_len) and (char2 == '/'):
return 1
# Common prefix was skipped above, next character is compared to
# determine order
return cmp(char1, char2)
def temp_checkout(blob):
temp = tempfile.mktemp()
fp = open(temp, 'wb')
blob.stream_data(fp)
fp.close()
return temp
class OStreamWrapper:
def __init__(self, fp):
self.fp = fp
def read(self, bytes):
return self.fp.read(bytes)
def readlines(self):
text = self.fp.read()
return text.rstrip().split('\n')
def close(self):
pass
def __del__(self):
self.close()
class LocalGitRepository(vclib.Repository):
def __init__(self, name, rootpath, authorizer, utilities):
if not (os.path.isdir(rootpath) and (
os.path.isdir(os.path.join(rootpath, '.git')) or os.path.isfile(os.path.join(rootpath, '.git', 'config')))):
raise vclib.ReposNotFound(name)
# Initialize some stuff.
self.rootpath = rootpath
self.name = name
self.auth = authorizer
self.diff_cmd = utilities.diff or 'diff'
# See if this repository is even viewable, authz-wise.
if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name)
def open(self):
# Open the repository and init some other variables.
self.repo = Repo(self.rootpath)
# See if a universal read access determination can be made.
if self.auth and self.auth.check_universal_access(self.name) == 1:
self.auth = None
def rootname(self):
return self.name
def rootpath(self):
return self.rootpath
def roottype(self):
return vclib.GIT
def authorizer(self):
return self.auth
def itemtype(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
return t
def openfile(self, path_parts, rev, options):
c, f, t = self._obj(path_parts, rev) # does authz-check
return OStreamWrapper(f.data_stream), c.hexsha
def listdir(self, path_parts, rev, options):
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.type != 'tree':
raise vclib.Error("Path '%s' is not a directory." % self._getpath(path))
entries = []
path = self._getpath(path_parts)
if path:
path = path+'/'
for i in f:
if i.type == 'tree':
kind = vclib.DIR
else:
kind = vclib.FILE
if vclib.check_path_access(self, path_parts + [ i.name ], kind, rev):
e = vclib.DirEntry(i.name, kind)
# <dirlogs>
h = self.repo.iter_commits(c.hexsha, path+i.name).next()
e.rev = h.binsha
e.date = h.authored_date
e.author = h.author.name + ' <' + h.author.email + '>'
e.log = h.message
e.lockinfo = None
# </dirlogs>
entries.append(e)
return entries
def dirlogs(self, path_parts, rev, entries, options):
pass
def itemlog(self, path_parts, rev, sortby, first, limit, options):
# FIXME sortby has no effect
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
c = self.repo.iter_commits(rev, path)
# FIXME (???) Include c.name_rev into data? (it's the symbolic commit name based on closest reference)
revs = []
i = 0
prev_rev = None
for c in self.repo.iter_commits(rev, path):
if i >= first:
if path_type == vclib.FILE:
f = c.tree
for p in path_parts:
f = f[p]
s = f.size
else:
s = 0
# FIXME we only take the first parent...
rev = vclib.Revision(c.authored_date, c.hexsha, c.authored_date,
c.author.name + ' <' + c.author.email + '>', c.authored_date, c.message, s, None)
if prev_rev:
prev_rev.prev = c.hexsha
prev_rev = rev
revs.append(rev)
i = i+1
if limit > 0 and i >= first+limit:
break
prev_rev.prev = None
return revs
def itemprops(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
return { 'filemode': "0%o" % f.mode }
def annotate(self, path_parts, rev, include_text=False):
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
if path_type != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % path)
blame = self.repo.blame(rev, path)
source = []
line_num = 1
youngest_rev = None
for (commit, lines) in blame:
if youngest_rev is None or youngest_rev.authored_date > commit.authored_date:
youngest_rev = commit
for line in lines:
prev_rev = None
source.append(vclib.Annotation(line, line_num, commit.hexsha, prev_rev, commit.author, commit.authored_date))
line_num = line_num+1
return source, youngest_rev.hexsha
def revinfo(self, rev):
commit = self.repo.commit(rev)
changes = []
# FIXME we only pick the first parent commit ;-(
base_rev = commit.parents[0].hexsha
for i in commit.stats.files:
p = _path_parts(i)
t = self.itemtype(p, rev) # does auth-check
# FIXME handle renames... 4th parameter is base path
# FIXME handle filemode change (last param is 'props changed?')
changes.append(ChangedPath(
p, commit.hexsha, t, p, base_rev, action, False, commit.stats.files[i].lines > 0, False
))
return (commit.authored_date, commit.author, commit.message, changes, {})
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
if path_parts1:
c1, f1, t = self._obj(path_parts1, rev1) # does authz-check
if t != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % self._getpath(path_parts1))
else:
f1 = None
if path_parts2:
c2, f2, t = self._obj(path_parts2, rev2) # does authz-check
if t != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % self._getpath(path_parts2))
else:
f2 = None
args = vclib._diff_args(type, options)
if f1:
temp1 = temp_checkout(f1)
info1 = self._getpath(path_parts1), c1.authored_date, rev1
else:
temp1 = '/dev/null'
info1 = '/dev/null', '', rev1
if f2:
temp2 = temp_checkout(f2)
info2 = self._getpath(path_parts2), c2.authored_date, rev2
else:
temp2 = '/dev/null'
info2 = '/dev/null', '', rev1
return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args)
def isexecutable(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.mode & 0777:
return True
return False
def filesize(self, path_parts, rev):
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.type != 'blob':
raise vclib.Error("Path '%s' is not a file." % path)
return f.size
##--- helpers ---##
# Returns (commit, object, type)
def _obj(self, path_parts, rev):
c = self.repo.commit(rev)
f = c.tree
if path_parts:
for i in path_parts[:-1]:
if not f or f.type != 'tree':
raise vclib.ItemNotFound(path_parts)
f = f[i]
f = f[path_parts[-1]]
if not f:
raise vclib.ItemNotFound(path_parts)
if f.type == 'blob':
t = vclib.FILE
else:
t = vclib.DIR
if not vclib.check_path_access(self, path_parts, t, rev):
raise vclib.ItemNotFound(t)
return c, f, t
def _getpath(self, path_parts):
return '/'.join(path_parts)
##--- custom ---##
def _getrev(self, rev):
rev = self.repo.commit(rev)
return rev.hexsha
def get_location(self, path, rev, old_rev):
old = self.repo.commit(old_rev)
diff = old.diff(rev)
old_path = None
for i in diff:
if i.b_blob and i.b_blob.path == path:
old_path = i.a_blob.path
elif i.rename_to == path:
old_path = i.rename_from
if old_path is None:
return path
return _cleanup_path(old_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."""
c, f, t = self._obj(path_parts, rev) # does authz-check
if f.type != 'blob':
raise vclib.Error("Path '%s' is not a file." % path)
if f.mode & 20000:
return f.data_stream.read()
return None

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -10,94 +10,703 @@
#
# -----------------------------------------------------------------------
"Version Control lib driver for Subversion repositories"
"Version Control lib driver for locally accessible Subversion repositories"
import vclib
import os
import os.path
import string
import cStringIO
import signal
import time
import tempfile
import popen
import re
import urllib
from svn import fs, repos, core, delta
_re_url = re.compile('^(http|https|file|svn|svn\+[^:]+)://')
def _canonicalize_path(path):
import svn.core
### Require Subversion 1.2.0 or better.
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 2, 0):
raise Exception, "Version requirement not met (needs 1.2.0 or better)"
def _allow_all(root, path, pool):
"""Generic authz_read_func that permits access to all paths"""
return 1
def _fs_path_join(base, relative):
# Subversion filesystem paths are '/'-delimited, regardless of OS.
joined_path = base + '/' + relative
parts = filter(None, string.split(joined_path, '/'))
return string.join(parts, '/')
def _cleanup_path(path):
"""Return a cleaned-up Subversion filesystem path"""
return string.join(filter(None, string.split(path, '/')), '/')
def _compare_paths(path1, path2):
path1_len = len (path1);
path2_len = len (path2);
min_len = min(path1_len, path2_len)
i = 0
# Are the paths exactly the same?
if path1 == path2:
return 0
# Skip past common prefix
while (i < min_len) and (path1[i] == path2[i]):
i = i + 1
# Children of paths are greater than their parents, but less than
# greater siblings of their parents
char1 = '\0'
char2 = '\0'
if (i < path1_len):
char1 = path1[i]
if (i < path2_len):
char2 = path2[i]
if (char1 == '/') and (i == path2_len):
return 1
if (char2 == '/') and (i == path1_len):
return -1
if (i < path1_len) and (char1 == '/'):
return -1
if (i < path2_len) and (char2 == '/'):
return 1
# Common prefix was skipped above, next character is compared to
# determine order
return cmp(char1, char2)
def _datestr_to_date(datestr, pool):
if datestr is None:
return None
return core.svn_time_from_cstring(datestr, pool) / 1000000
def _fs_rev_props(fsptr, rev, pool):
author = fs.revision_prop(fsptr, rev, core.SVN_PROP_REVISION_AUTHOR, pool)
msg = fs.revision_prop(fsptr, rev, core.SVN_PROP_REVISION_LOG, pool)
date = fs.revision_prop(fsptr, rev, core.SVN_PROP_REVISION_DATE, pool)
return date, author, msg
def date_from_rev(svnrepos, rev):
if (rev < 0) or (rev > svnrepos.youngest):
raise vclib.InvalidRevision(rev)
datestr = fs.revision_prop(svnrepos.fs_ptr, rev,
core.SVN_PROP_REVISION_DATE, svnrepos.pool)
return _datestr_to_date(datestr, svnrepos.pool)
def get_location(svnrepos, path, rev, old_rev):
try:
return svn.core.svn_path_canonicalize(path)
except AttributeError: # svn_path_canonicalize() appeared in 1.4.0 bindings
# There's so much more that we *could* do here, but if we're
# here at all its because there's a really old Subversion in
# place, and those older Subversion versions cared quite a bit
# less about the specifics of path canonicalization.
if re.search(_re_url, path):
return path.rstrip('/')
results = repos.svn_repos_trace_node_locations(svnrepos.fs_ptr, path,
rev, [old_rev],
_allow_all, svnrepos.pool)
except core.SubversionException, e:
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
raise vclib.ItemNotFound(path)
raise
try:
old_path = results[old_rev]
except KeyError:
raise vclib.ItemNotFound(path)
return _cleanup_path(old_path)
def last_rev(svnrepos, path, peg_revision, limit_revision=None):
"""Given PATH, known to exist in PEG_REVISION, find the youngest
revision older than, or equal to, LIMIT_REVISION in which path
exists. Return that revision, and the path at which PATH exists in
that revision."""
# Here's the plan, man. In the trivial case (where PEG_REVISION is
# the same as LIMIT_REVISION), this is a no-brainer. If
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
# history tracing code to find the right location. If, however,
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
# Subversion's lack of forward history searching. Our workaround,
# ugly as it may be, involves a binary search through the revisions
# between PEG_REVISION and LIMIT_REVISION to find our last live
# revision.
peg_revision = svnrepos._getrev(peg_revision)
limit_revision = svnrepos._getrev(limit_revision)
try:
if peg_revision == limit_revision:
return peg_revision, path
elif peg_revision > limit_revision:
fsroot = svnrepos._getroot(peg_revision)
history = fs.node_history(fsroot, path, svnrepos.scratch_pool)
while history:
path, peg_revision = fs.history_location(history,
svnrepos.scratch_pool);
if peg_revision <= limit_revision:
return max(peg_revision, limit_revision), _cleanup_path(path)
history = fs.history_prev(history, 1, svnrepos.scratch_pool)
return peg_revision, _cleanup_path(path)
else:
return os.path.normpath(path)
### Warning: this is *not* an example of good pool usage.
orig_id = fs.node_id(svnrepos._getroot(peg_revision), path,
svnrepos.scratch_pool)
while peg_revision != limit_revision:
mid = (peg_revision + 1 + limit_revision) / 2
try:
mid_id = fs.node_id(svnrepos._getroot(mid), path,
svnrepos.scratch_pool)
except core.SubversionException, e:
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
cmp = -1
else:
raise
else:
### Not quite right. Need a comparison function that only returns
### true when the two nodes are the same copy, not just related.
cmp = fs.compare_ids(orig_id, mid_id)
if cmp in (0, 1):
peg_revision = mid
else:
limit_revision = mid - 1
return peg_revision, path
finally:
svnrepos._scratch_clear()
def canonicalize_rootpath(rootpath):
# Try to canonicalize the rootpath using Subversion semantics.
rootpath = _canonicalize_path(rootpath)
def created_rev(svnrepos, full_name, rev):
fsroot = svnrepos._getroot(rev)
return fs.node_created_rev(fsroot, full_name, svnrepos.pool)
class Revision(vclib.Revision):
"Hold state for each revision's log entry."
def __init__(self, rev, date, author, msg, size,
filename, copy_path, copy_rev):
vclib.Revision.__init__(self, rev, str(rev), date, author, None, msg, size)
self.filename = filename
self.copy_path = copy_path
self.copy_rev = copy_rev
class NodeHistory:
def __init__(self, fs_ptr, show_all_logs):
self.histories = {}
self.fs_ptr = fs_ptr
self.show_all_logs = show_all_logs
def add_history(self, path, revision, pool):
# If filtering, only add the path and revision to the histories
# list if they were actually changed in this revision (where
# change means the path itself was changed, or one of its parents
# was copied). This is useful for omitting bubble-up directory
# changes.
if not self.show_all_logs:
rev_root = fs.revision_root(self.fs_ptr, revision, pool)
changed_paths = fs.paths_changed(rev_root, pool)
paths = changed_paths.keys()
if path not in paths:
# Look for a copied parent
test_path = path
found = 0
subpool = core.svn_pool_create(pool)
while 1:
core.svn_pool_clear(subpool)
off = string.rfind(test_path, '/')
if off < 0:
break
test_path = test_path[0:off]
if test_path in paths:
copyfrom_rev, copyfrom_path = \
fs.copied_from(rev_root, test_path, subpool)
if copyfrom_rev >= 0 and copyfrom_path:
found = 1
break
core.svn_pool_destroy(subpool)
if not found:
return
self.histories[revision] = _cleanup_path(path)
# 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:]))
def _get_history(svnrepos, full_name, rev, options={}):
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, full_name, svnrepos.pool)
if kind is core.svn_node_file:
show_all_logs = 1
# Instantiate a NodeHistory collector object.
history = NodeHistory(svnrepos.fs_ptr, show_all_logs)
# 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
# Do we want to cross copy history?
cross_copies = options.get('svn_cross_copies', 0)
# Get the history items for PATH.
repos.svn_repos_history(svnrepos.fs_ptr, full_name, history.add_history,
1, rev, cross_copies, svnrepos.pool)
return history.histories
def expand_root_parent(parent_path):
roots = {}
if re.search(_re_url, parent_path):
pass
else:
# Any subdirectories of PARENT_PATH which themselves have a child
# "format" are returned as roots.
assert os.path.isabs(parent_path)
subpaths = os.listdir(parent_path)
for rootname in subpaths:
rootpath = os.path.join(parent_path, rootname)
if os.path.exists(os.path.join(rootpath, "format")):
roots[rootname] = canonicalize_rootpath(rootpath)
return roots
class ChangedPath:
def __init__(self, filename, pathtype, prop_mods, text_mods,
base_path, base_rev, action, is_copy):
self.filename = filename
self.pathtype = pathtype
self.prop_mods = prop_mods
self.text_mods = text_mods
self.base_path = base_path
self.base_rev = base_rev
self.action = action
self.is_copy = is_copy
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."""
class ChangedPathSet:
def __init__(self):
self.changes = { }
def add_change(self, change):
if change.path:
change.path = _cleanup_path(change.path)
if change.base_path:
change.base_path = _cleanup_path(change.base_path)
path = change.path
action = 'modified'
is_copy = 0
if not change.path:
action = 'deleted'
path = change.base_path
elif change.added:
action = 'added'
replace_check_path = path
if change.base_path and change.base_rev:
is_copy = 1
replace_check_path = change.base_path
if self.changes.has_key(replace_check_path) \
and self.changes[replace_check_path].action == 'deleted':
action = 'replaced'
if change.item_kind == core.svn_node_dir:
pathtype = vclib.DIR
elif change.item_kind == core.svn_node_file:
pathtype = vclib.FILE
else:
pathtype = None
self.changes[path] = ChangedPath(path, pathtype, change.prop_changes,
change.text_changed, change.base_path,
change.base_rev, action, is_copy)
def get_changes(self):
changes = self.changes.values()
changes.sort(lambda a, b: _compare_paths(a.filename, b.filename))
return changes
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 get_revision_info(svnrepos, rev):
fsroot = svnrepos._getroot(rev)
# Get the changes for the revision
cps = ChangedPathSet()
editor = repos.ChangeCollector(svnrepos.fs_ptr, fsroot,
svnrepos.pool, cps.add_change)
e_ptr, e_baton = delta.make_editor(editor, svnrepos.pool)
repos.svn_repos_replay(fsroot, e_ptr, e_baton, svnrepos.pool)
# Now get the revision property info. Would use
# editor.get_root_props(), but something is broken there...
datestr, author, msg = _fs_rev_props(svnrepos.fs_ptr, rev, svnrepos.pool)
date = _datestr_to_date(datestr, svnrepos.pool)
return date, author, msg, cps.get_changes()
def SubversionRepository(name, rootpath, authorizer, utilities, config_dir):
rootpath = canonicalize_rootpath(rootpath)
if re.search(_re_url, rootpath):
import svn_ra
return svn_ra.RemoteSubversionRepository(name, rootpath, authorizer,
utilities, config_dir)
def _log_helper(svnrepos, rev, path, pool):
rev_root = fs.revision_root(svnrepos.fs_ptr, rev, pool)
# Was this path@rev the target of a copy?
copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path, pool)
# Assemble our LogEntry
datestr, author, msg = _fs_rev_props(svnrepos.fs_ptr, rev, pool)
date = _datestr_to_date(datestr, pool)
if fs.is_file(rev_root, path, pool):
size = fs.file_length(rev_root, path, pool)
else:
import svn_repos
return svn_repos.LocalSubversionRepository(name, rootpath, authorizer,
utilities, config_dir)
size = None
entry = Revision(rev, date, author, msg, size, path,
copyfrom_path and _cleanup_path(copyfrom_path),
copyfrom_rev)
return entry
def _fetch_log(svnrepos, full_name, which_rev, options, pool):
revs = []
if options.get('svn_latest_log', 0):
rev = _log_helper(svnrepos, which_rev, full_name, pool)
if rev:
revs.append(rev)
else:
history_set = _get_history(svnrepos, full_name, which_rev, options)
history_revs = history_set.keys()
history_revs.sort()
history_revs.reverse()
subpool = core.svn_pool_create(pool)
for history_rev in history_revs:
core.svn_pool_clear(subpool)
rev = _log_helper(svnrepos, history_rev, history_set[history_rev],
subpool)
if rev:
revs.append(rev)
core.svn_pool_destroy(subpool)
return revs
def _get_last_history_rev(fsroot, path, pool):
history = fs.node_history(fsroot, path, pool)
history = fs.history_prev(history, 0, pool)
history_path, history_rev = fs.history_location(history, pool);
return history_rev
def get_logs(svnrepos, full_name, rev, files):
fsroot = svnrepos._getroot(rev)
subpool = core.svn_pool_create(svnrepos.pool)
for file in files:
core.svn_pool_clear(subpool)
path = _fs_path_join(full_name, file.name)
rev = _get_last_history_rev(fsroot, path, subpool)
datestr, author, msg = _fs_rev_props(svnrepos.fs_ptr, rev, subpool)
date = _datestr_to_date(datestr, subpool)
file.rev = str(rev)
file.date = date
file.author = author
file.log = msg
if file.kind == vclib.FILE:
file.size = fs.file_length(fsroot, path, subpool)
core.svn_pool_destroy(subpool)
def get_youngest_revision(svnrepos):
return svnrepos.youngest
def temp_checkout(svnrepos, path, rev, pool):
"""Check out file revision to temporary file"""
temp = tempfile.mktemp()
fp = open(temp, 'wb')
try:
root = svnrepos._getroot(rev)
stream = fs.file_contents(root, path, pool)
try:
while 1:
chunk = core.svn_stream_read(stream, core.SVN_STREAM_CHUNK_SIZE)
if not chunk:
break
fp.write(chunk)
finally:
core.svn_stream_close(stream)
finally:
fp.close()
return temp
class FileContentsPipe:
def __init__(self, root, path, pool):
self._pool = core.svn_pool_create(pool)
self._stream = fs.file_contents(root, path, self._pool)
self._eof = 0
def __del__(self):
core.svn_pool_destroy(self._pool)
def read(self, len=None):
chunk = None
if not self._eof:
if len is None:
buffer = cStringIO.StringIO()
try:
while 1:
hunk = core.svn_stream_read(self._stream, 8192)
if not hunk:
break
buffer.write(hunk)
chunk = buffer.getvalue()
finally:
buffer.close()
else:
chunk = core.svn_stream_read(self._stream, len)
if not chunk:
self._eof = 1
return chunk
def readline(self):
chunk = None
if not self._eof:
chunk, self._eof = core.svn_stream_readline(self._stream, '\n',
self._pool)
if not self._eof:
chunk = chunk + '\n'
if not chunk:
self._eof = 1
return chunk
def readlines(self):
lines = []
while True:
line = self.readline()
if not line:
break
lines.append(line)
return lines
def close(self):
return core.svn_stream_close(self._stream)
def eof(self):
return self._eof
_re_blameinfo = re.compile(r"\s*(\d+)\s*(.*)")
class BlameSource:
def __init__(self, svn_client_path, rootpath, fs_path, rev, first_rev):
self.idx = -1
self.line_number = 1
self.last = None
self.first_rev = first_rev
# Do a little dance to get a URL that works in both Unix-y and
# Windows worlds.
rootpath = os.path.abspath(rootpath)
if rootpath and rootpath[0] != '/':
rootpath = '/' + rootpath
if os.sep != '/':
rootpath = string.replace(rootpath, os.sep, '/')
url = 'file://' + string.join([rootpath, fs_path], "/")
fp = popen.popen(svn_client_path,
('blame', "-r%d" % int(rev), "--non-interactive",
"%s@%d" % (url, int(rev))),
'rb', 1)
self.fp = fp
def __getitem__(self, idx):
if idx == self.idx:
return self.last
if idx != self.idx + 1:
raise BlameSequencingError()
line = self.fp.readline()
if not line:
raise IndexError("No more annotations")
m = _re_blameinfo.match(line[:17])
if not m:
raise vclib.Error("Could not parse blame output at line %i\n%s"
% (idx+1, line))
rev, author = m.groups()
text = line[18:]
rev = int(rev)
prev_rev = None
if rev > self.first_rev:
prev_rev = rev - 1
item = _item(text=text, line_number=idx+1, rev=rev,
prev_rev=prev_rev, author=author, date=None)
self.last = item
self.idx = idx
return item
class BlameSequencingError(Exception):
pass
class SubversionRepository(vclib.Repository):
def __init__(self, name, rootpath, svn_path):
if not os.path.isdir(rootpath):
raise vclib.ReposNotFound(name)
# Initialize some stuff.
self.pool = None
self.apr_init = 0
self.rootpath = rootpath
self.name = name
self.svn_client_path = os.path.normpath(os.path.join(svn_path, 'svn'))
# 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):
self._close()
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
# Initialize APR and get our top-level pool.
core.apr_initialize()
self.apr_init = 1
self.pool = core.svn_pool_create(None)
self.scratch_pool = core.svn_pool_create(self.pool)
# Open the repository and init some other variables.
self.repos = repos.svn_repos_open(rootpath, self.pool)
self.fs_ptr = repos.svn_repos_fs(self.repos)
self.youngest = fs.youngest_rev(self.fs_ptr, self.pool)
self._fsroots = {}
def __del__(self):
self._close()
def _close(self):
if self.pool:
core.svn_pool_destroy(self.pool)
self.pool = None
if self.apr_init:
core.apr_terminate()
self.apr_init = 0
def _scratch_clear(self):
core.svn_pool_clear(self.scratch_pool)
def itemtype(self, path_parts, rev):
rev = self._getrev(rev)
basepath = self._getpath(path_parts)
kind = fs.check_path(self._getroot(rev), basepath, self.scratch_pool)
self._scratch_clear()
if kind == core.svn_node_dir:
return vclib.DIR
if kind == core.svn_node_file:
return vclib.FILE
raise vclib.ItemNotFound(path_parts)
def openfile(self, path_parts, rev):
path = self._getpath(path_parts)
rev = self._getrev(rev)
fsroot = self._getroot(rev)
revision = str(_get_last_history_rev(fsroot, path, self.scratch_pool))
self._scratch_clear()
fp = FileContentsPipe(fsroot, path, self.pool)
return fp, revision
def listdir(self, path_parts, rev, options):
basepath = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR:
raise vclib.Error("Path '%s' is not a directory." % basepath)
rev = self._getrev(rev)
fsroot = self._getroot(rev)
dirents = fs.dir_entries(fsroot, basepath, self.scratch_pool)
entries = [ ]
for entry in dirents.values():
if entry.kind == core.svn_node_dir:
kind = vclib.DIR
elif entry.kind == core.svn_node_file:
kind = vclib.FILE
entries.append(vclib.DirEntry(entry.name, kind))
self._scratch_clear()
return entries
def dirlogs(self, path_parts, rev, entries, options):
get_logs(self, self._getpath(path_parts), self._getrev(rev), entries)
def itemlog(self, path_parts, rev, options):
"""see vclib.Repository.itemlog docstring
Option values recognized by this implementation
svn_show_all_dir_logs
boolean, default false. if set for a directory path, will include
revisions where files underneath the directory have changed
svn_cross_copies
boolean, default false. if set for a path created by a copy, will
include revisions from before the copy
svn_latest_log
boolean, default false. if set will return only newest single log
entry
"""
path = self._getpath(path_parts)
rev = self._getrev(rev)
revs = _fetch_log(self, path, rev, options, self.scratch_pool)
self._scratch_clear()
revs.sort()
prev = None
for rev in revs:
rev.prev = prev
prev = rev
return revs
def annotate(self, path_parts, rev):
path = self._getpath(path_parts)
rev = self._getrev(rev)
fsroot = self._getroot(rev)
history_set = _get_history(self, path, rev, {'svn_cross_copies': 1})
history_revs = history_set.keys()
history_revs.sort()
revision = history_revs[-1]
first_rev = history_revs[0]
source = BlameSource(self.svn_client_path, self.rootpath,
path, rev, first_rev)
return source, revision
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
p1 = self._getpath(path_parts1)
p2 = self._getpath(path_parts2)
r1 = self._getrev(rev1)
r2 = self._getrev(rev2)
args = vclib._diff_args(type, options)
try:
temp1 = temp_checkout(self, p1, r1, self.pool)
temp2 = temp_checkout(self, p2, r2, self.pool)
info1 = p1, date_from_rev(self, r1), r1
info2 = p2, date_from_rev(self, r2), r2
return vclib._diff_fp(temp1, temp2, info1, info2, args)
except vclib.svn.core.SubversionException, e:
if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND:
raise vclib.InvalidRevision
raise
def _getpath(self, path_parts):
return string.join(path_parts, '/')
def _getrev(self, rev):
if rev is None or rev == 'HEAD':
return self.youngest
try:
rev = int(rev)
except ValueError:
raise vclib.InvalidRevision(rev)
if (rev < 0) or (rev > self.youngest):
raise vclib.InvalidRevision(rev)
return rev
def _getroot(self, rev):
try:
return self._fsroots[rev]
except KeyError:
r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev, self.pool)
return r
class _item:
def __init__(self, **kw):
vars(self).update(kw)

View File

@@ -1,812 +0,0 @@
# -*-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/
#
# -----------------------------------------------------------------------
"Version Control lib driver for remotely accessible Subversion repositories."
import vclib
import sys
import os
import re
import tempfile
import time
import urllib
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
### Require Subversion 1.3.1 or better. (for svn_ra_get_locations support)
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)"
### BEGIN COMPATABILITY CODE ###
try:
SVN_INVALID_REVNUM = core.SVN_INVALID_REVNUM
except AttributeError: # The 1.4.x bindings are missing core.SVN_INVALID_REVNUM
SVN_INVALID_REVNUM = -1
def list_directory(url, peg_rev, rev, flag, ctx):
try:
dirents, locks = client.svn_client_ls3(url, peg_rev, rev, flag, ctx)
except TypeError: # 1.4.x bindings are goofed
dirents = client.svn_client_ls3(None, url, peg_rev, rev, flag, ctx)
locks = {}
return dirents, locks
def get_directory_props(ra_session, path, rev):
try:
dirents, fetched_rev, props = ra.svn_ra_get_dir(ra_session, path, rev)
except ValueError: # older bindings are goofed
props = ra.svn_ra_get_dir(ra_session, path, rev)
return props
def client_log(url, start_rev, end_rev, log_limit, include_changes,
cross_copies, cb_func, ctx):
include_changes = include_changes and 1 or 0
cross_copies = cross_copies and 1 or 0
try:
client.svn_client_log4([url], start_rev, start_rev, end_rev,
log_limit, include_changes, not cross_copies,
0, None, cb_func, ctx)
except AttributeError:
# Wrap old svn_log_message_receiver_t interface with a
# svn_log_entry_t one.
def cb_convert(paths, revision, author, date, message, pool):
class svn_log_entry_t:
pass
log_entry = svn_log_entry_t()
log_entry.changed_paths = paths
log_entry.revision = revision
log_entry.revprops = { core.SVN_PROP_REVISION_LOG : message,
core.SVN_PROP_REVISION_AUTHOR : author,
core.SVN_PROP_REVISION_DATE : date,
}
cb_func(log_entry, pool)
client.svn_client_log2([url], start_rev, end_rev, log_limit,
include_changes, not cross_copies, cb_convert, ctx)
def setup_client_ctx(config_dir):
# Ensure that the configuration directory exists.
core.svn_config_ensure(config_dir)
# Fetch the configuration (and 'config' bit thereof).
cfg = core.svn_config_get_config(config_dir)
config = cfg.get(core.SVN_CONFIG_CATEGORY_CONFIG)
# Here's the compat-sensitive part: try to use
# svn_cmdline_create_auth_baton(), and fall back to making our own
# if that fails.
try:
auth_baton = core.svn_cmdline_create_auth_baton(1, None, None, config_dir,
1, 1, config, None)
except AttributeError:
auth_baton = core.svn_auth_open([
client.svn_client_get_simple_provider(),
client.svn_client_get_username_provider(),
client.svn_client_get_ssl_server_trust_file_provider(),
client.svn_client_get_ssl_client_cert_file_provider(),
client.svn_client_get_ssl_client_cert_pw_file_provider(),
])
if config_dir is not None:
core.svn_auth_set_parameter(auth_baton,
core.SVN_AUTH_PARAM_CONFIG_DIR,
config_dir)
# Create, setup, and return the client context baton.
ctx = client.svn_client_create_context()
ctx.config = cfg
ctx.auth_baton = auth_baton
return ctx
### END COMPATABILITY CODE ###
class LogCollector:
def __init__(self, path, show_all_logs, lockinfo, access_check_func):
# This class uses leading slashes for paths internally
if not path:
self.path = '/'
else:
self.path = path[0] == '/' and path or '/' + path
self.logs = []
self.show_all_logs = show_all_logs
self.lockinfo = lockinfo
self.access_check_func = access_check_func
self.done = False
def add_log(self, log_entry, pool):
if self.done:
return
paths = log_entry.changed_paths
revision = log_entry.revision
msg, author, date, revprops = _split_revprops(log_entry.revprops)
# Changed paths have leading slashes
changed_paths = paths.keys()
changed_paths.sort(lambda a, b: _compare_paths(a, b))
this_path = None
if self.path in changed_paths:
this_path = self.path
change = paths[self.path]
if change.copyfrom_path:
this_path = change.copyfrom_path
for changed_path in changed_paths:
if changed_path != self.path:
# If a parent of our path was copied, our "next previous"
# (huh?) path will exist elsewhere (under the copy source).
if (self.path.rfind(changed_path) == 0) and \
self.path[len(changed_path)] == '/':
change = paths[changed_path]
if change.copyfrom_path:
this_path = change.copyfrom_path + self.path[len(changed_path):]
if self.show_all_logs or this_path:
if self.access_check_func is None \
or self.access_check_func(self.path[1:], revision):
entry = Revision(revision, date, author, msg, None, self.lockinfo,
self.path[1:], None, None)
self.logs.append(entry)
else:
self.done = True
if this_path:
self.path = this_path
def cat_to_tempfile(svnrepos, path, rev):
"""Check out file revision to temporary file"""
temp = tempfile.mktemp()
stream = core.svn_stream_from_aprfile(temp)
url = svnrepos._geturl(path)
client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev),
svnrepos.ctx)
core.svn_stream_close(stream)
return temp
class SelfCleanFP:
def __init__(self, path):
self._fp = open(path, 'r')
self._path = path
self._eof = 0
def read(self, len=None):
if len:
chunk = self._fp.read(len)
else:
chunk = self._fp.read()
if chunk == '':
self._eof = 1
return chunk
def readline(self):
chunk = self._fp.readline()
if chunk == '':
self._eof = 1
return chunk
def readlines(self):
lines = self._fp.readlines()
self._eof = 1
return lines
def close(self):
self._fp.close()
os.remove(self._path)
def __del__(self):
self.close()
def eof(self):
return self._eof
class RemoteSubversionRepository(vclib.Repository):
def __init__(self, name, rootpath, authorizer, utilities, config_dir):
self.name = name
self.rootpath = rootpath
self.auth = authorizer
self.diff_cmd = utilities.diff or 'diff'
self.config_dir = config_dir or None
# See if this repository is even viewable, authz-wise.
if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name)
def open(self):
# Setup the client context baton, complete with non-prompting authstuffs.
self.ctx = setup_client_ctx(self.config_dir)
ra_callbacks = ra.svn_ra_callbacks_t()
ra_callbacks.auth_baton = self.ctx.auth_baton
self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None,
self.ctx.config)
self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session)
self._dirent_cache = { }
self._revinfo_cache = { }
# See if a universal read access determination can be made.
if self.auth and self.auth.check_universal_access(self.name) == 1:
self.auth = None
def rootname(self):
return self.name
def rootpath(self):
return self.rootpath
def roottype(self):
return vclib.SVN
def authorizer(self):
return self.auth
def itemtype(self, path_parts, rev):
pathtype = None
if not len(path_parts):
pathtype = vclib.DIR
else:
path = self._getpath(path_parts)
rev = self._getrev(rev)
try:
kind = ra.svn_ra_check_path(self.ra_session, path, rev)
if kind == core.svn_node_file:
pathtype = vclib.FILE
elif kind == core.svn_node_dir:
pathtype = vclib.DIR
except:
pass
if pathtype is None:
raise vclib.ItemNotFound(path_parts)
if not vclib.check_path_access(self, path_parts, pathtype, rev):
raise vclib.ItemNotFound(path_parts)
return pathtype
def openfile(self, path_parts, rev, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev)
url = self._geturl(path)
### rev here should be the last history revision of the URL
fp = SelfCleanFP(cat_to_tempfile(self, path, rev))
lh_rev, c_rev = self._get_last_history_rev(path_parts, rev)
return fp, lh_rev
def listdir(self, path_parts, rev, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." % path)
rev = self._getrev(rev)
entries = []
dirents, locks = self._get_dirents(path, rev)
for name in dirents.keys():
entry = dirents[name]
if entry.kind == core.svn_node_dir:
kind = vclib.DIR
elif entry.kind == core.svn_node_file:
kind = vclib.FILE
else:
kind = None
entries.append(vclib.DirEntry(name, kind))
return entries
def dirlogs(self, path_parts, rev, entries, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." % path)
rev = self._getrev(rev)
dirents, locks = self._get_dirents(path, rev)
for entry in entries:
entry_path_parts = path_parts + [entry.name]
dirent = dirents.get(entry.name, None)
# dirents is authz-sanitized, so ensure the entry is found therein.
if dirent is None:
continue
# Get authz-sanitized revision metadata.
entry.date, entry.author, entry.log, revprops, changes = \
self._revinfo(dirent.created_rev)
entry.rev = str(dirent.created_rev)
entry.size = dirent.size
entry.lockinfo = None
if locks.has_key(entry.name):
entry.lockinfo = locks[entry.name].owner
def itemlog(self, path_parts, rev, sortby, first, limit, options):
assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV
path_type = self.itemtype(path_parts, rev) # does auth-check
path = self._getpath(path_parts)
rev = self._getrev(rev)
url = self._geturl(path)
# If this is a file, fetch the lock status and size (as of REV)
# for this item.
lockinfo = size_in_rev = None
if path_type == vclib.FILE:
basename = path_parts[-1]
list_url = self._geturl(self._getpath(path_parts[:-1]))
dirents, locks = list_directory(list_url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
if locks.has_key(basename):
lockinfo = locks[basename].owner
if dirents.has_key(basename):
size_in_rev = dirents[basename].size
# Special handling for the 'svn_latest_log' scenario.
### FIXME: Don't like this hack. We should just introduce
### something more direct in the vclib API.
if options.get('svn_latest_log', 0):
dir_lh_rev, dir_c_rev = self._get_last_history_rev(path_parts, rev)
date, author, log, revprops, changes = self._revinfo(dir_lh_rev)
return [vclib.Revision(dir_lh_rev, str(dir_lh_rev), date, author,
None, log, size_in_rev, lockinfo)]
def _access_checker(check_path, check_rev):
return vclib.check_path_access(self, _path_parts(check_path),
path_type, check_rev)
# It's okay if we're told to not show all logs on a file -- all
# the revisions should match correctly anyway.
lc = LogCollector(path, options.get('svn_show_all_dir_logs', 0),
lockinfo, _access_checker)
cross_copies = options.get('svn_cross_copies', 0)
log_limit = 0
if limit:
log_limit = first + limit
client_log(url, _rev2optrev(rev), _rev2optrev(1), log_limit, 1,
cross_copies, lc.add_log, self.ctx)
revs = lc.logs
revs.sort()
prev = None
for rev in revs:
# Swap out revision info with stuff from the cache (which is
# authz-sanitized).
rev.date, rev.author, rev.log, revprops, changes \
= self._revinfo(rev.number)
rev.prev = prev
prev = rev
revs.reverse()
if len(revs) < first:
return []
if limit:
return revs[first:first+limit]
return revs
def itemprops(self, path_parts, rev):
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
rev = self._getrev(rev)
url = self._geturl(path)
pairs = client.svn_client_proplist2(url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
return pairs and pairs[0][1] or {}
def annotate(self, path_parts, rev, include_text=False):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev)
url = self._geturl(path)
# Examine logs for the file to determine the oldest revision we are
# permitted to see.
log_options = {
'svn_cross_copies' : 1,
'svn_show_all_dir_logs' : 1,
}
revs = self.itemlog(path_parts, rev, vclib.SORTBY_REV, 0, 0, log_options)
oldest_rev = revs[-1].number
# Now calculate the annotation data. Note that we'll not
# inherently trust the provided author and date, because authz
# rules might necessitate that we strip that information out.
blame_data = []
def _blame_cb(line_no, revision, author, date,
line, pool, blame_data=blame_data):
prev_rev = None
if 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,
author, date))
client.blame2(url, _rev2optrev(rev), _rev2optrev(oldest_rev),
_rev2optrev(rev), _blame_cb, self.ctx)
return blame_data, rev
def revinfo(self, rev):
return self._revinfo(rev, 1)
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
if path_parts1 is not None:
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 is not None:
if not p1:
raise vclib.ItemNotFound(parh_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:
p2 = None
args = vclib._diff_args(type, options)
def _date_from_rev(rev):
date, author, msg, revprops, changes = self._revinfo(rev)
return date
try:
info1 = p1, _date_from_rev(r1), r1
info2 = p2, _date_from_rev(r2), r2
if p1:
temp1 = cat_to_tempfile(self, p1, r1)
else:
temp1 = '/dev/null'
if p2:
temp2 = cat_to_tempfile(self, p2, r2)
else:
temp2 = '/dev/null'
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
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)
rev = self._getrev(rev)
dirents, locks = self._get_dirents(self._getpath(path_parts[:-1]), rev)
dirent = dirents.get(path_parts[-1], None)
return dirent.size
def _getpath(self, path_parts):
return '/'.join(path_parts)
def _getrev(self, rev):
if rev is None or rev == 'HEAD':
return self.youngest
try:
if type(rev) == type(''):
while rev[0] == 'r':
rev = rev[1:]
rev = int(rev)
except:
raise vclib.InvalidRevision(rev)
if (rev < 0) or (rev > self.youngest):
raise vclib.InvalidRevision(rev)
return rev
def _geturl(self, path=None):
if not path:
return self.rootpath
path = self.rootpath + '/' + urllib.quote(path)
return _canonicalize_path(path)
def _get_dirents(self, path, rev):
"""Return a 2-type of dirents and locks, possibly reading/writing
from a local cache of that information. This functions performs
authz checks, stripping out unreadable dirents."""
dir_url = self._geturl(path)
path_parts = _path_parts(path)
if path:
key = str(rev) + '/' + path
else:
key = str(rev)
# Ensure that the cache gets filled...
dirents_locks = self._dirent_cache.get(key)
if not dirents_locks:
tmp_dirents, locks = list_directory(dir_url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
dirents = {}
for name, dirent in tmp_dirents.items():
dirent_parts = path_parts + [name]
kind = dirent.kind
if (kind == core.svn_node_dir or kind == core.svn_node_file) \
and vclib.check_path_access(self, dirent_parts,
kind == core.svn_node_dir \
and vclib.DIR or vclib.FILE, rev):
lh_rev, c_rev = self._get_last_history_rev(dirent_parts, rev)
dirent.created_rev = lh_rev
dirents[name] = dirent
dirents_locks = [dirents, locks]
self._dirent_cache[key] = dirents_locks
# ...then return the goodies from the cache.
return dirents_locks[0], dirents_locks[1]
def _get_last_history_rev(self, path_parts, rev):
"""Return the a 2-tuple which contains:
- the last interesting revision equal to or older than REV in
the history of PATH_PARTS.
- the created_rev of of PATH_PARTS as of REV."""
path = self._getpath(path_parts)
url = self._geturl(self._getpath(path_parts))
optrev = _rev2optrev(rev)
# Get the last-changed-rev.
revisions = []
def _info_cb(path, info, pool, retval=revisions):
revisions.append(info.last_changed_rev)
client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx)
last_changed_rev = revisions[0]
# Now, this object might not have been directly edited since the
# last-changed-rev, but it might have been the child of a copy.
# To determine this, we'll run a potentially no-op log between
# LAST_CHANGED_REV and REV.
lc = LogCollector(path, 1, None, None)
client_log(url, optrev, _rev2optrev(last_changed_rev), 1, 1, 0,
lc.add_log, self.ctx)
revs = lc.logs
if revs:
revs.sort()
return revs[0].number, last_changed_rev
else:
return last_changed_rev, last_changed_rev
def _revinfo_fetch(self, rev, include_changed_paths=0):
need_changes = include_changed_paths or self.auth
revs = []
def _log_cb(log_entry, pool, retval=revs):
# If Subversion happens to call us more than once, we choose not
# to care.
if retval:
return
revision = log_entry.revision
msg, author, date, revprops = _split_revprops(log_entry.revprops)
action_map = { 'D' : vclib.DELETED,
'A' : vclib.ADDED,
'R' : vclib.REPLACED,
'M' : vclib.MODIFIED,
}
# Easy out: if we won't use the changed-path info, just return a
# changes-less tuple.
if not need_changes:
return revs.append([date, author, msg, revprops, None])
# Subversion 1.5 and earlier didn't offer the 'changed_paths2'
# hash, and in Subversion 1.6, it's offered but broken.
try:
changed_paths = log_entry.changed_paths2
paths = (changed_paths or {}).keys()
except:
changed_paths = log_entry.changed_paths
paths = (changed_paths or {}).keys()
paths.sort(lambda a, b: _compare_paths(a, b))
# If we get this far, our caller needs changed-paths, or we need
# them for authz-related sanitization.
changes = []
found_readable = found_unreadable = 0
for path in paths:
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)
if change.copyfrom_path and change.copyfrom_rev:
is_copy = 1
base_path = change.copyfrom_path
base_rev = change.copyfrom_rev
elif action == vclib.ADDED or action == vclib.REPLACED:
is_copy = 0
base_path = base_rev = None
else:
is_copy = 0
base_path = path
base_rev = revision - 1
# Check authz rules (sadly, we have to lie about the path type)
parts = _path_parts(path)
if vclib.check_path_access(self, parts, vclib.FILE, revision):
if is_copy and base_path and (base_path != path):
parts = _path_parts(base_path)
if not vclib.check_path_access(self, parts, vclib.FILE, base_rev):
is_copy = 0
base_path = None
base_rev = None
found_unreadable = 1
changes.append(SVNChangedPath(path, revision, pathtype, base_path,
base_rev, action, is_copy,
text_modified, props_modified))
found_readable = 1
else:
found_unreadable = 1
# If our caller doesn't want changed-path stuff, and we have
# the info we need to make an authz determination already,
# quit this loop and get on with it.
if (not include_changed_paths) and found_unreadable and found_readable:
break
# Filter unreadable information.
if found_unreadable:
msg = None
if not found_readable:
author = None
date = None
# Drop unrequested changes.
if not include_changed_paths:
changes = None
# Add this revision information to the "return" array.
retval.append([date, author, msg, revprops, changes])
optrev = _rev2optrev(rev)
client_log(self.rootpath, optrev, optrev, 1, need_changes, 0,
_log_cb, self.ctx)
return tuple(revs[0])
def _revinfo(self, rev, include_changed_paths=0):
"""Internal-use, cache-friendly revision information harvester."""
# Consult the revinfo cache first. If we don't have cached info,
# or our caller wants changed paths and we don't have those for
# this revision, go do the real work.
rev = self._getrev(rev)
cached_info = self._revinfo_cache.get(rev)
if not cached_info \
or (include_changed_paths and cached_info[4] is None):
cached_info = self._revinfo_fetch(rev, include_changed_paths)
self._revinfo_cache[rev] = cached_info
return cached_info
##--- custom --##
def get_youngest_revision(self):
return self.youngest
def get_location(self, path, rev, old_rev):
try:
results = ra.get_locations(self.ra_session, path, rev, [old_rev])
except core.SubversionException, e:
_fix_subversion_exception(e)
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
raise vclib.ItemNotFound(path)
raise
try:
old_path = results[old_rev]
except KeyError:
raise vclib.ItemNotFound(path)
old_path = _cleanup_path(old_path)
old_path_parts = _path_parts(old_path)
# Check access (lying about path types)
if not vclib.check_path_access(self, old_path_parts, vclib.FILE, old_rev):
raise vclib.ItemNotFound(path)
return old_path
def created_rev(self, path, rev):
lh_rev, c_rev = self._get_last_history_rev(_path_parts(path), rev)
return lh_rev
def last_rev(self, path, peg_revision, limit_revision=None):
"""Given PATH, known to exist in PEG_REVISION, find the youngest
revision older than, or equal to, LIMIT_REVISION in which path
exists. Return that revision, and the path at which PATH exists in
that revision."""
# Here's the plan, man. In the trivial case (where PEG_REVISION is
# the same as LIMIT_REVISION), this is a no-brainer. If
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
# history tracing code to find the right location. If, however,
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
# Subversion's lack of forward history searching. Our workaround,
# ugly as it may be, involves a binary search through the revisions
# between PEG_REVISION and LIMIT_REVISION to find our last live
# revision.
peg_revision = self._getrev(peg_revision)
limit_revision = self._getrev(limit_revision)
if peg_revision == limit_revision:
return peg_revision, path
elif peg_revision > limit_revision:
path = self.get_location(path, peg_revision, limit_revision)
return limit_revision, path
else:
direction = 1
while peg_revision != limit_revision:
mid = (peg_revision + 1 + limit_revision) / 2
try:
path = self.get_location(path, peg_revision, mid)
except vclib.ItemNotFound:
limit_revision = mid - 1
else:
peg_revision = mid
return peg_revision, path
def get_symlink_target(self, path_parts, rev):
"""Return the target of the symbolic link versioned at PATH_PARTS
in REV, or None if that object is not a symlink."""
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
rev = self._getrev(rev)
url = self._geturl(path)
# Symlinks must be files with the svn:special property set on them
# and with file contents which read "link SOME_PATH".
if path_type != vclib.FILE:
return None
pairs = client.svn_client_proplist2(url, _rev2optrev(rev),
_rev2optrev(rev), 0, self.ctx)
props = pairs and pairs[0][1] or {}
if not props.has_key(core.SVN_PROP_SPECIAL):
return None
pathspec = ''
### FIXME: We're being a touch sloppy here, first by grabbing the
### whole file and then by checking only the first line
### of it.
fp = SelfCleanFP(cat_to_tempfile(self, path, rev))
pathspec = fp.readline()
fp.close()
if pathspec[:5] != 'link ':
return None
return pathspec[5:]

View File

@@ -1,979 +0,0 @@
# -*-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/
#
# -----------------------------------------------------------------------
"Version Control lib driver for locally accessible Subversion repositories"
import vclib
import os
import os.path
import cStringIO
import time
import tempfile
import popen
import re
import urllib
from svn import fs, repos, core, client, delta, diff
### Require Subversion 1.3.1 or better.
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 1):
raise Exception, "Version requirement not met (needs 1.3.1 or better)"
### Pre-1.5 Subversion doesn't have SVN_ERR_CEASE_INVOCATION
try:
_SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CEASE_INVOCATION
except:
_SVN_ERR_CEASE_INVOCATION = core.SVN_ERR_CANCELLED
### Pre-1.5 SubversionException's might not have the .msg and .apr_err members
def _fix_subversion_exception(e):
if not hasattr(e, 'apr_err'):
e.apr_err = e[1]
if not hasattr(e, 'message'):
e.message = e[0]
### Pre-1.4 Subversion doesn't have svn_path_canonicalize()
def _canonicalize_path(path):
try:
return core.svn_path_canonicalize(path)
except AttributeError:
return path
def _allow_all(root, path, pool):
"""Generic authz_read_func that permits access to all paths"""
return 1
def _path_parts(path):
return filter(None, path.split('/'))
def _cleanup_path(path):
"""Return a cleaned-up Subversion filesystem path"""
return '/'.join(_path_parts(path))
def _fs_path_join(base, relative):
return _cleanup_path(base + '/' + relative)
def _compare_paths(path1, path2):
path1_len = len (path1);
path2_len = len (path2);
min_len = min(path1_len, path2_len)
i = 0
# Are the paths exactly the same?
if path1 == path2:
return 0
# Skip past common prefix
while (i < min_len) and (path1[i] == path2[i]):
i = i + 1
# Children of paths are greater than their parents, but less than
# greater siblings of their parents
char1 = '\0'
char2 = '\0'
if (i < path1_len):
char1 = path1[i]
if (i < path2_len):
char2 = path2[i]
if (char1 == '/') and (i == path2_len):
return 1
if (char2 == '/') and (i == path1_len):
return -1
if (i < path1_len) and (char1 == '/'):
return -1
if (i < path2_len) and (char2 == '/'):
return 1
# Common prefix was skipped above, next character is compared to
# determine order
return cmp(char1, char2)
def _rev2optrev(rev):
assert type(rev) is int
rt = core.svn_opt_revision_t()
rt.kind = core.svn_opt_revision_number
rt.value.number = rev
return rt
def _rootpath2url(rootpath, path):
rootpath = os.path.abspath(rootpath)
drive, rootpath = os.path.splitdrive(rootpath)
if os.sep != '/':
rootpath = rootpath.replace(os.sep, '/')
rootpath = urllib.quote(rootpath)
path = urllib.quote(path)
if drive:
url = 'file:///' + drive + rootpath + '/' + path
else:
url = 'file://' + rootpath + '/' + path
return _canonicalize_path(url)
# Given a dictionary REVPROPS of revision properties, pull special
# ones out of them and return a 4-tuple containing the log message,
# the author, the date (converted from the date string property), and
# a dictionary of any/all other revprops.
def _split_revprops(revprops):
if not revprops:
return None, None, None, {}
special_props = []
for prop in core.SVN_PROP_REVISION_LOG, \
core.SVN_PROP_REVISION_AUTHOR, \
core.SVN_PROP_REVISION_DATE:
if revprops.has_key(prop):
special_props.append(revprops[prop])
del(revprops[prop])
else:
special_props.append(None)
msg, author, datestr = tuple(special_props)
date = _datestr_to_date(datestr)
return msg, author, date, revprops
def _datestr_to_date(datestr):
try:
return core.svn_time_from_cstring(datestr) / 1000000
except:
return None
class Revision(vclib.Revision):
"Hold state for each revision's log entry."
def __init__(self, rev, date, author, msg, size, lockinfo,
filename, copy_path, copy_rev):
vclib.Revision.__init__(self, rev, str(rev), date, author, None,
msg, size, lockinfo)
self.filename = filename
self.copy_path = copy_path
self.copy_rev = copy_rev
class NodeHistory:
"""An iterable object that returns 2-tuples of (revision, path)
locations along a node's change history, ordered from youngest to
oldest."""
def __init__(self, fs_ptr, show_all_logs, limit=0):
self.histories = []
self.fs_ptr = fs_ptr
self.show_all_logs = show_all_logs
self.oldest_rev = None
self.limit = limit
def add_history(self, path, revision, pool):
# If filtering, only add the path and revision to the histories
# list if they were actually changed in this revision (where
# change means the path itself was changed, or one of its parents
# was copied). This is useful for omitting bubble-up directory
# changes.
if not self.oldest_rev:
self.oldest_rev = revision
else:
assert(revision < self.oldest_rev)
if not self.show_all_logs:
rev_root = fs.revision_root(self.fs_ptr, revision)
changed_paths = fs.paths_changed(rev_root)
paths = changed_paths.keys()
if path not in paths:
# Look for a copied parent
test_path = path
found = 0
while 1:
off = test_path.rfind('/')
if off < 0:
break
test_path = test_path[0:off]
if test_path in paths:
copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, test_path)
if copyfrom_rev >= 0 and copyfrom_path:
found = 1
break
if not found:
return
self.histories.append([revision, _cleanup_path(path)])
if self.limit and len(self.histories) == self.limit:
raise core.SubversionException("", _SVN_ERR_CEASE_INVOCATION)
def __getitem__(self, idx):
return self.histories[idx]
def _get_last_history_rev(fsroot, path):
history = fs.node_history(fsroot, path)
history = fs.history_prev(history, 0)
history_path, history_rev = fs.history_location(history)
return history_rev
def temp_checkout(svnrepos, path, rev):
"""Check out file revision to temporary file"""
temp = tempfile.mktemp()
fp = open(temp, 'wb')
try:
root = svnrepos._getroot(rev)
stream = fs.file_contents(root, path)
try:
while 1:
chunk = core.svn_stream_read(stream, core.SVN_STREAM_CHUNK_SIZE)
if not chunk:
break
fp.write(chunk)
finally:
core.svn_stream_close(stream)
finally:
fp.close()
return temp
class FileContentsPipe:
def __init__(self, root, path):
self._stream = fs.file_contents(root, path)
self._eof = 0
def read(self, len=None):
chunk = None
if not self._eof:
if len is None:
buffer = cStringIO.StringIO()
try:
while 1:
hunk = core.svn_stream_read(self._stream, 8192)
if not hunk:
break
buffer.write(hunk)
chunk = buffer.getvalue()
finally:
buffer.close()
else:
chunk = core.svn_stream_read(self._stream, len)
if not chunk:
self._eof = 1
return chunk
def readline(self):
chunk = None
if not self._eof:
chunk, self._eof = core.svn_stream_readline(self._stream, '\n')
if not self._eof:
chunk = chunk + '\n'
if not chunk:
self._eof = 1
return chunk
def readlines(self):
lines = []
while True:
line = self.readline()
if not line:
break
lines.append(line)
return lines
def close(self):
return core.svn_stream_close(self._stream)
def eof(self):
return self._eof
class BlameSource:
def __init__(self, local_url, rev, first_rev, include_text, config_dir):
self.idx = -1
self.first_rev = first_rev
self.blame_data = []
self.include_text = include_text
ctx = client.svn_client_create_context()
core.svn_config_ensure(config_dir)
ctx.config = core.svn_config_get_config(config_dir)
ctx.auth_baton = core.svn_auth_open([])
try:
### TODO: Is this use of FIRST_REV always what we want? Should we
### pass 1 here instead and do filtering later?
client.blame3(local_url, _rev2optrev(rev), _rev2optrev(first_rev),
_rev2optrev(rev), diff.svn_diff_file_options_t(), True, self._blame_cb, ctx)
except core.SubversionException, e:
_fix_subversion_exception(e)
if e.apr_err == core.SVN_ERR_CLIENT_IS_BINARY_FILE:
raise vclib.NonTextualFileContents
raise
def _blame_cb(self, line_no, rev, author, date, text, pool):
prev_rev = None
if rev > self.first_rev:
prev_rev = rev - 1
if not self.include_text:
text = None
self.blame_data.append(vclib.Annotation(text, line_no + 1, rev,
prev_rev, author, None))
def __getitem__(self, idx):
if idx != self.idx + 1:
raise BlameSequencingError()
self.idx = idx
return self.blame_data[idx]
class BlameSequencingError(Exception):
pass
class SVNChangedPath(vclib.ChangedPath):
"""Wrapper around vclib.ChangedPath which handles path splitting."""
def __init__(self, path, rev, pathtype, base_path, base_rev,
action, copied, text_changed, props_changed):
path_parts = _path_parts(path or '')
base_path_parts = _path_parts(base_path or '')
vclib.ChangedPath.__init__(self, path_parts, rev, pathtype,
base_path_parts, base_rev, action,
copied, text_changed, props_changed)
class LocalSubversionRepository(vclib.Repository):
def __init__(self, name, rootpath, authorizer, utilities, config_dir):
if not (os.path.isdir(rootpath) \
and os.path.isfile(os.path.join(rootpath, 'format'))):
raise vclib.ReposNotFound(name)
# Initialize some stuff.
self.rootpath = rootpath
self.name = name
self.auth = authorizer
self.diff_cmd = utilities.diff or 'diff'
self.config_dir = config_dir or None
# See if this repository is even viewable, authz-wise.
if not vclib.check_root_access(self):
raise vclib.ReposNotFound(name)
def open(self):
# Open the repository and init some other variables.
self.repos = repos.svn_repos_open(self.rootpath)
self.fs_ptr = repos.svn_repos_fs(self.repos)
self.youngest = fs.youngest_rev(self.fs_ptr)
self._fsroots = {}
self._revinfo_cache = {}
# See if a universal read access determination can be made.
if self.auth and self.auth.check_universal_access(self.name) == 1:
self.auth = None
def rootname(self):
return self.name
def rootpath(self):
return self.rootpath
def roottype(self):
return vclib.SVN
def authorizer(self):
return self.auth
def itemtype(self, path_parts, rev):
rev = self._getrev(rev)
basepath = self._getpath(path_parts)
pathtype = self._gettype(basepath, rev)
if pathtype is None:
raise vclib.ItemNotFound(path_parts)
if not vclib.check_path_access(self, path_parts, pathtype, rev):
raise vclib.ItemNotFound(path_parts)
return pathtype
def openfile(self, path_parts, rev, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check
raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev)
fsroot = self._getroot(rev)
revision = str(_get_last_history_rev(fsroot, path))
fp = FileContentsPipe(fsroot, path)
return fp, revision
def listdir(self, path_parts, rev, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." % path)
rev = self._getrev(rev)
fsroot = self._getroot(rev)
dirents = fs.dir_entries(fsroot, path)
entries = [ ]
for entry in dirents.values():
if entry.kind == core.svn_node_dir:
kind = vclib.DIR
elif entry.kind == core.svn_node_file:
kind = vclib.FILE
if vclib.check_path_access(self, path_parts + [entry.name], kind, rev):
entries.append(vclib.DirEntry(entry.name, kind))
return entries
def dirlogs(self, path_parts, rev, entries, options):
path = self._getpath(path_parts)
if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check
raise vclib.Error("Path '%s' is not a directory." % path)
fsroot = self._getroot(self._getrev(rev))
rev = self._getrev(rev)
for entry in entries:
entry_path_parts = path_parts + [entry.name]
if not vclib.check_path_access(self, entry_path_parts, entry.kind, rev):
continue
path = self._getpath(entry_path_parts)
entry_rev = _get_last_history_rev(fsroot, path)
date, author, msg, revprops, changes = self._revinfo(entry_rev)
entry.rev = str(entry_rev)
entry.date = date
entry.author = author
entry.log = msg
if entry.kind == vclib.FILE:
entry.size = fs.file_length(fsroot, path)
lock = fs.get_lock(self.fs_ptr, path)
entry.lockinfo = lock and lock.owner or None
def itemlog(self, path_parts, rev, sortby, first, limit, options):
"""see vclib.Repository.itemlog docstring
Option values recognized by this implementation
svn_show_all_dir_logs
boolean, default false. if set for a directory path, will include
revisions where files underneath the directory have changed
svn_cross_copies
boolean, default false. if set for a path created by a copy, will
include revisions from before the copy
svn_latest_log
boolean, default false. if set will return only newest single log
entry
"""
assert sortby == vclib.SORTBY_DEFAULT or sortby == vclib.SORTBY_REV
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
rev = self._getrev(rev)
revs = []
lockinfo = None
# See if this path is locked.
try:
lock = fs.get_lock(self.fs_ptr, path)
if lock:
lockinfo = lock.owner
except NameError:
pass
# If our caller only wants the latest log, we'll invoke
# _log_helper for just the one revision. Otherwise, we go off
# into history-fetching mode. ### TODO: we could stand to have a
# 'limit' parameter here as numeric cut-off for the depth of our
# history search.
if options.get('svn_latest_log', 0):
revision = self._log_helper(path, rev, lockinfo)
if revision:
revision.prev = None
revs.append(revision)
else:
history = self._get_history(path, rev, path_type, first + limit, options)
if len(history) < first:
history = []
if limit:
history = history[first:first+limit]
for hist_rev, hist_path in history:
revision = self._log_helper(hist_path, hist_rev, lockinfo)
if revision:
# If we have unreadable copyfrom data, obscure it.
if revision.copy_path is not None:
cp_parts = _path_parts(revision.copy_path)
if not vclib.check_path_access(self, cp_parts, path_type,
revision.copy_rev):
revision.copy_path = revision.copy_rev = None
revision.prev = None
if len(revs):
revs[-1].prev = revision
revs.append(revision)
return revs
def itemprops(self, path_parts, rev):
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
rev = self._getrev(rev)
fsroot = self._getroot(rev)
return fs.node_proplist(fsroot, path)
def annotate(self, path_parts, rev, include_text=False):
path = self._getpath(path_parts)
path_type = self.itemtype(path_parts, rev) # does auth-check
if path_type != vclib.FILE:
raise vclib.Error("Path '%s' is not a file." % path)
rev = self._getrev(rev)
fsroot = self._getroot(rev)
history = self._get_history(path, rev, path_type, 0,
{'svn_cross_copies': 1})
youngest_rev, youngest_path = history[0]
oldest_rev, oldest_path = history[-1]
source = BlameSource(_rootpath2url(self.rootpath, path), youngest_rev,
oldest_rev, include_text, self.config_dir)
return source, youngest_rev
def revinfo(self, rev):
return self._revinfo(rev, 1)
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)
e_ptr, e_baton = delta.make_editor(editor)
repos.svn_repos_replay(fsroot, e_ptr, e_baton)
changedpaths = {}
changes = editor.get_changes()
# Copy the Subversion changes into a new hash, checking
# authorization and converting them into ChangedPath objects.
found_readable = found_unreadable = 0
for path in changes.keys():
change = changes[path]
if change.path:
change.path = _cleanup_path(change.path)
if change.base_path:
change.base_path = _cleanup_path(change.base_path)
is_copy = 0
if not hasattr(change, 'action'): # new to subversion 1.4.0
action = vclib.MODIFIED
if not change.path:
action = vclib.DELETED
elif change.added:
action = vclib.ADDED
replace_check_path = path
if change.base_path and change.base_rev:
replace_check_path = change.base_path
if changedpaths.has_key(replace_check_path) \
and changedpaths[replace_check_path].action == vclib.DELETED:
action = vclib.REPLACED
else:
if change.action == repos.CHANGE_ACTION_ADD:
action = vclib.ADDED
elif change.action == repos.CHANGE_ACTION_DELETE:
action = vclib.DELETED
elif change.action == repos.CHANGE_ACTION_REPLACE:
action = vclib.REPLACED
else:
action = vclib.MODIFIED
if (action == vclib.ADDED or action == vclib.REPLACED) \
and change.base_path \
and change.base_rev:
is_copy = 1
if change.item_kind == core.svn_node_dir:
pathtype = vclib.DIR
elif change.item_kind == core.svn_node_file:
pathtype = vclib.FILE
else:
pathtype = None
parts = _path_parts(path)
if vclib.check_path_access(self, parts, pathtype, rev):
if is_copy and change.base_path and (change.base_path != path):
parts = _path_parts(change.base_path)
if not vclib.check_path_access(self, parts, pathtype,
change.base_rev):
is_copy = 0
change.base_path = None
change.base_rev = None
found_unreadable = 1
changedpaths[path] = SVNChangedPath(path, rev, pathtype,
change.base_path,
change.base_rev, action,
is_copy, change.text_changed,
change.prop_changes)
found_readable = 1
else:
found_unreadable = 1
return found_readable, found_unreadable, changedpaths.values()
def _get_change_copyinfo(fsroot, path, change):
# If we know the copyfrom info, return it...
if hasattr(change, 'copyfrom_known') and change.copyfrom_known:
copyfrom_path = change.copyfrom_path
copyfrom_rev = change.copyfrom_rev
# ...otherwise, if this change could be a copy (that is, it
# contains an add action), query the copyfrom info ...
elif (change.change_kind == fs.path_change_add or
change.change_kind == fs.path_change_replace):
copyfrom_rev, copyfrom_path = fs.copied_from(fsroot, path)
# ...else, there's no copyfrom info.
else:
copyfrom_rev = core.SVN_INVALID_REVNUM
copyfrom_path = None
return copyfrom_path, copyfrom_rev
def _simple_auth_check(fsroot):
"""Return a 2-tuple: found_readable, found_unreadable."""
found_unreadable = found_readable = 0
if hasattr(fs, 'paths_changed2'):
changes = fs.paths_changed2(fsroot)
else:
changes = fs.paths_changed(fsroot)
paths = changes.keys()
for path in paths:
change = changes[path]
pathtype = None
if hasattr(change, 'node_kind'):
if change.node_kind == core.svn_node_file:
pathtype = vclib.FILE
elif change.node_kind == core.svn_node_dir:
pathtype = vclib.DIR
parts = _path_parts(path)
if pathtype is None:
# Figure out the pathtype so we can query the authz subsystem.
if change.change_kind == fs.path_change_delete:
# Deletions are annoying, because they might be underneath
# copies (make their previous location non-trivial).
prev_parts = parts
prev_rev = rev - 1
parent_parts = parts[:-1]
while parent_parts:
parent_path = '/' + self._getpath(parent_parts)
parent_change = changes.get(parent_path)
if not (parent_change and \
(parent_change.change_kind == fs.path_change_add or
parent_change.change_kind == fs.path_change_replace)):
del(parent_parts[-1])
continue
copyfrom_path, copyfrom_rev = \
_get_change_copyinfo(fsroot, parent_path, parent_change)
if copyfrom_path:
prev_rev = copyfrom_rev
prev_parts = _path_parts(copyfrom_path) + \
parts[len(parent_parts):]
break
del(parent_parts[-1])
pathtype = self._gettype(self._getpath(prev_parts), prev_rev)
else:
pathtype = self._gettype(self._getpath(parts), rev)
if vclib.check_path_access(self, parts, pathtype, rev):
found_readable = 1
copyfrom_path, copyfrom_rev = \
_get_change_copyinfo(fsroot, path, change)
if copyfrom_path and copyfrom_path != path:
parts = _path_parts(copyfrom_path)
if not vclib.check_path_access(self, parts, pathtype,
copyfrom_rev):
found_unreadable = 1
else:
found_unreadable = 1
if found_readable and found_unreadable:
break
return found_readable, found_unreadable
def _revinfo_helper(rev, include_changed_paths):
# Get the revision property info. (Would use
# editor.get_root_props(), but something is broken there...)
revprops = fs.revision_proplist(self.fs_ptr, rev)
msg, author, date, revprops = _split_revprops(revprops)
# Optimization: If our caller doesn't care about the changed
# paths, and we don't need them to do authz determinations, let's
# get outta here.
if self.auth is None and not include_changed_paths:
return date, author, msg, revprops, None
# If we get here, then we either need the changed paths because we
# were asked for them, or we need them to do authorization checks.
#
# If we only need them for authorization checks, though, we
# won't bother generating fully populated ChangedPath items (the
# cost is too great).
fsroot = self._getroot(rev)
if include_changed_paths:
found_readable, found_unreadable, changedpaths = \
_get_changed_paths(fsroot)
else:
changedpaths = None
found_readable, found_unreadable = _simple_auth_check(fsroot)
# Filter our metadata where necessary, and return the requested data.
if found_unreadable:
msg = None
if not found_readable:
author = None
date = None
return date, author, msg, revprops, changedpaths
# Consult the revinfo cache first. If we don't have cached info,
# or our caller wants changed paths and we don't have those for
# this revision, go do the real work.
rev = self._getrev(rev)
cached_info = self._revinfo_cache.get(rev)
if not cached_info \
or (include_changed_paths and cached_info[4] is None):
cached_info = _revinfo_helper(rev, include_changed_paths)
self._revinfo_cache[rev] = cached_info
return tuple(cached_info)
def _log_helper(self, path, rev, lockinfo):
rev_root = fs.revision_root(self.fs_ptr, rev)
copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path)
date, author, msg, revprops, changes = self._revinfo(rev)
if fs.is_file(rev_root, path):
size = fs.file_length(rev_root, path)
else:
size = None
return Revision(rev, date, author, msg, size, lockinfo, path,
copyfrom_path and _cleanup_path(copyfrom_path),
copyfrom_rev)
def _get_history(self, path, rev, path_type, limit=0, options={}):
if self.youngest == 0:
return []
rev_paths = []
fsroot = self._getroot(rev)
show_all_logs = options.get('svn_show_all_dir_logs', 0)
if not show_all_logs:
# See if the path is a file or directory.
kind = fs.check_path(fsroot, path)
if kind is core.svn_node_file:
show_all_logs = 1
# Instantiate a NodeHistory collector object, and use it to collect
# history items for PATH@REV.
history = NodeHistory(self.fs_ptr, show_all_logs, limit)
try:
repos.svn_repos_history(self.fs_ptr, path, history.add_history,
1, rev, options.get('svn_cross_copies', 0))
except core.SubversionException, e:
_fix_subversion_exception(e)
if e.apr_err != _SVN_ERR_CEASE_INVOCATION:
raise
# Now, iterate over those history items, checking for changes of
# location, pruning as necessitated by authz rules.
for hist_rev, hist_path in history:
path_parts = _path_parts(hist_path)
if not vclib.check_path_access(self, path_parts, path_type, hist_rev):
break
rev_paths.append([hist_rev, hist_path])
return rev_paths
def _getpath(self, path_parts):
return '/'.join(path_parts)
def _getrev(self, rev):
if rev is None or rev == 'HEAD':
return self.youngest
try:
if type(rev) == type(''):
while rev[0] == 'r':
rev = rev[1:]
rev = int(rev)
except:
raise vclib.InvalidRevision(rev)
if (rev < 0) or (rev > self.youngest):
raise vclib.InvalidRevision(rev)
return rev
def _getroot(self, rev):
try:
return self._fsroots[rev]
except KeyError:
r = self._fsroots[rev] = fs.revision_root(self.fs_ptr, rev)
return r
def _gettype(self, path, rev):
# Similar to itemtype(), but without the authz check. Returns
# None for missing paths.
try:
kind = fs.check_path(self._getroot(rev), path)
except:
return None
if kind == core.svn_node_dir:
return vclib.DIR
if kind == core.svn_node_file:
return vclib.FILE
return None
##--- custom ---##
def get_youngest_revision(self):
return self.youngest
def get_location(self, path, rev, old_rev):
try:
results = repos.svn_repos_trace_node_locations(self.fs_ptr, path,
rev, [old_rev], _allow_all)
except core.SubversionException, e:
_fix_subversion_exception(e)
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
raise vclib.ItemNotFound(path)
raise
try:
old_path = results[old_rev]
except KeyError:
raise vclib.ItemNotFound(path)
return _cleanup_path(old_path)
def created_rev(self, full_name, rev):
return fs.node_created_rev(self._getroot(rev), full_name)
def last_rev(self, path, peg_revision, limit_revision=None):
"""Given PATH, known to exist in PEG_REVISION, find the youngest
revision older than, or equal to, LIMIT_REVISION in which path
exists. Return that revision, and the path at which PATH exists in
that revision."""
# Here's the plan, man. In the trivial case (where PEG_REVISION is
# the same as LIMIT_REVISION), this is a no-brainer. If
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
# history tracing code to find the right location. If, however,
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
# Subversion's lack of forward history searching. Our workaround,
# ugly as it may be, involves a binary search through the revisions
# between PEG_REVISION and LIMIT_REVISION to find our last live
# revision.
peg_revision = self._getrev(peg_revision)
limit_revision = self._getrev(limit_revision)
try:
if peg_revision == limit_revision:
return peg_revision, path
elif peg_revision > limit_revision:
fsroot = self._getroot(peg_revision)
history = fs.node_history(fsroot, path)
while history:
path, peg_revision = fs.history_location(history)
if peg_revision <= limit_revision:
return peg_revision, _cleanup_path(path)
history = fs.history_prev(history, 1)
return peg_revision, _cleanup_path(path)
else:
orig_id = fs.node_id(self._getroot(peg_revision), path)
while peg_revision != limit_revision:
mid = (peg_revision + 1 + limit_revision) / 2
try:
mid_id = fs.node_id(self._getroot(mid), path)
except core.SubversionException, e:
_fix_subversion_exception(e)
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
cmp = -1
else:
raise
else:
### Not quite right. Need a comparison function that only returns
### true when the two nodes are the same copy, not just related.
cmp = fs.compare_ids(orig_id, mid_id)
if cmp in (0, 1):
peg_revision = mid
else:
limit_revision = mid - 1
return peg_revision, path
finally:
pass
def get_symlink_target(self, path_parts, rev):
"""Return the target of the symbolic link versioned at PATH_PARTS
in REV, or None if that object is not a symlink."""
path = self._getpath(path_parts)
rev = self._getrev(rev)
path_type = self.itemtype(path_parts, rev) # does auth-check
fsroot = self._getroot(rev)
# Symlinks must be files with the svn:special property set on them
# and with file contents which read "link SOME_PATH".
if path_type != vclib.FILE:
return None
props = fs.node_proplist(fsroot, path)
if not props.has_key(core.SVN_PROP_SPECIAL):
return None
pathspec = ''
### FIXME: We're being a touch sloppy here, only checking the first line
### of the file.
stream = fs.file_contents(fsroot, path)
try:
pathspec, eof = core.svn_stream_readline(stream, '\n')
finally:
core.svn_stream_close(stream)
if pathspec[:5] != 'link ':
return None
return pathspec[5:]

View File

@@ -0,0 +1,444 @@
# -*-python-*-
#
# Copyright (C) 1999-2006 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/
#
# -----------------------------------------------------------------------
"Version Control lib driver for remotely accessible Subversion repositories."
import vclib
import sys
import os
import string
import re
import tempfile
import popen2
import time
from vclib.svn import Revision, ChangedPath, _datestr_to_date, _compare_paths, _cleanup_path
from svn import core, delta, client, wc, ra
### Require Subversion 1.3.0 or better. (for svn_ra_get_locations support)
if (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_PATCH) < (1, 3, 0):
raise Exception, "Version requirement not met (needs 1.3.0 or better)"
def _rev2optrev(rev):
assert type(rev) is int
rt = core.svn_opt_revision_t()
rt.kind = core.svn_opt_revision_number
rt.value.number = rev
return rt
def date_from_rev(svnrepos, rev):
datestr = ra.svn_ra_rev_prop(svnrepos.ra_session, rev,
'svn:date', svnrepos.pool)
return _datestr_to_date(datestr, svnrepos.pool)
def get_location(svnrepos, path, rev, old_rev):
try:
results = ra.get_locations(svnrepos.ra_session, path, rev,
[old_rev], svnrepos.pool)
except core.SubversionException, e:
if e.apr_err == core.SVN_ERR_FS_NOT_FOUND:
raise vclib.ItemNotFound(path)
raise
try:
old_path = results[old_rev]
except KeyError:
raise vclib.ItemNotFound(path)
return _cleanup_path(old_path)
def last_rev(svnrepos, path, peg_revision, limit_revision=None):
"""Given PATH, known to exist in PEG_REVISION, find the youngest
revision older than, or equal to, LIMIT_REVISION in which path
exists. Return that revision, and the path at which PATH exists in
that revision."""
# Here's the plan, man. In the trivial case (where PEG_REVISION is
# the same as LIMIT_REVISION), this is a no-brainer. If
# LIMIT_REVISION is older than PEG_REVISION, we can use Subversion's
# history tracing code to find the right location. If, however,
# LIMIT_REVISION is younger than PEG_REVISION, we suffer from
# Subversion's lack of forward history searching. Our workaround,
# ugly as it may be, involves a binary search through the revisions
# between PEG_REVISION and LIMIT_REVISION to find our last live
# revision.
peg_revision = svnrepos._getrev(peg_revision)
limit_revision = svnrepos._getrev(limit_revision)
if peg_revision == limit_revision:
return peg_revision, path
elif peg_revision > limit_revision:
path = get_location(svnrepos, path, peg_revision, limit_revision)
return limit_revision, path
else:
### Warning: this is *not* an example of good pool usage.
direction = 1
while peg_revision != limit_revision:
mid = (peg_revision + 1 + limit_revision) / 2
try:
path = get_location(svnrepos, path, peg_revision, mid)
except vclib.ItemNotFound:
limit_revision = mid - 1
else:
peg_revision = mid
return peg_revision, path
def created_rev(svnrepos, full_name, rev):
kind = ra.svn_ra_check_path(svnrepos.ra_session, full_name, rev,
svnrepos.pool)
if kind == core.svn_node_dir:
props = ra.svn_ra_get_dir(svnrepos.ra_session, full_name,
rev, svnrepos.pool)
return int(props[core.SVN_PROP_ENTRY_COMMITTED_REV])
return core.SVN_INVALID_REVNUM
class LastHistoryCollector:
def __init__(self):
self.has_history = 0
def add_history(self, paths, revision, author, date, message, pool):
if not self.has_history:
self.has_history = 1
self.revision = revision
self.author = author
self.date = date
self.message = message
self.changes = []
if not paths:
return
changed_paths = paths.keys()
changed_paths.sort(lambda a, b: _compare_paths(a, b))
action_map = { 'D' : 'deleted',
'A' : 'added',
'R' : 'replaced',
'M' : 'modified',
}
for changed_path in changed_paths:
change = paths[changed_path]
action = action_map.get(change.action, '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:
self.changes.append(ChangedPath(changed_path[1:], None, 0, 0,
change.copyfrom_path,
change.copyfrom_rev, action, 1))
else:
self.changes.append(ChangedPath(changed_path[1:], None, 0, 0,
changed_path[1:], 0, action, 0))
def get_history(self):
if not self.has_history:
return None, None, None, None, None
return self.revision, self.author, self.date, self.message, self.changes
def _get_rev_details(svnrepos, rev, pool):
lhc = LastHistoryCollector()
client.svn_client_log([svnrepos.rootpath],
_rev2optrev(rev), _rev2optrev(rev),
1, 0, lhc.add_history, svnrepos.ctx, pool)
return lhc.get_history()
def get_revision_info(svnrepos, rev):
rev, author, date, log, changes = \
_get_rev_details(svnrepos, rev, svnrepos.pool)
return _datestr_to_date(date, svnrepos.pool), author, log, changes
class LogCollector:
def __init__(self, path, show_all_logs):
# This class uses leading slashes for paths internally
if not path:
self.path = '/'
else:
self.path = path[0] == '/' and path or '/' + path
self.logs = []
self.show_all_logs = show_all_logs
def add_log(self, paths, revision, author, date, message, pool):
# Changed paths have leading slashes
changed_paths = paths.keys()
changed_paths.sort(lambda a, b: _compare_paths(a, b))
this_path = None
if self.path in changed_paths:
this_path = self.path
change = paths[self.path]
if change.copyfrom_path:
this_path = change.copyfrom_path
for changed_path in changed_paths:
if changed_path != self.path:
# If a parent of our path was copied, our "next previous"
# (huh?) path will exist elsewhere (under the copy source).
if (string.rfind(self.path, changed_path) == 0) and \
self.path[len(changed_path)] == '/':
change = paths[changed_path]
if change.copyfrom_path:
this_path = change.copyfrom_path + self.path[len(changed_path):]
if self.show_all_logs or this_path:
date = _datestr_to_date(date, pool)
entry = Revision(revision, date, author, message, None,
self.path[1:], None, None)
self.logs.append(entry)
if this_path:
self.path = this_path
def get_logs(svnrepos, full_name, rev, files):
dirents = svnrepos._get_dirents(full_name, rev)
subpool = core.svn_pool_create(svnrepos.pool)
rev_info_cache = { }
for file in files:
core.svn_pool_clear(subpool)
entry = dirents[file.name]
if rev_info_cache.has_key(entry.created_rev):
rev, author, date, log = rev_info_cache[entry.created_rev]
else:
### i think this needs some get_last_history action to be accurate
rev, author, date, log, changes = \
_get_rev_details(svnrepos, entry.created_rev, subpool)
rev_info_cache[entry.created_rev] = rev, author, date, log
file.rev = rev
file.author = author
file.date = _datestr_to_date(date, subpool)
file.log = log
file.size = entry.size
core.svn_pool_destroy(subpool)
def get_youngest_revision(svnrepos):
return svnrepos.youngest
def temp_checkout(svnrepos, path, rev, pool):
"""Check out file revision to temporary file"""
temp = tempfile.mktemp()
stream = core.svn_stream_from_aprfile(temp, pool)
url = svnrepos.rootpath + (path and '/' + path)
client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev),
svnrepos.ctx, pool)
core.svn_stream_close(stream)
return temp
class SelfCleanFP:
def __init__(self, path):
self._fp = open(path, 'r')
self._path = path
self._eof = 0
def read(self, len):
if len:
chunk = self._fp.read(len)
else:
chunk = self._fp.read()
if chunk == '':
self._eof = 1
return chunk
def readline(self):
chunk = self._fp.readline()
if chunk == '':
self._eof = 1
return chunk
def close(self):
self._fp.close()
os.remove(self._path)
def __del__(self):
self.close()
def eof(self):
return self._eof
class SubversionRepository(vclib.Repository):
def __init__(self, name, rootpath):
# Init the client app
core.apr_initialize()
pool = core.svn_pool_create(None)
core.svn_config_ensure(None, pool)
# Start populating our members
self.pool = pool
self.name = name
self.rootpath = rootpath
# Setup the client context baton, complete with non-prompting authstuffs.
ctx = client.svn_client_ctx_t()
providers = []
providers.append(client.svn_client_get_simple_provider(pool))
providers.append(client.svn_client_get_username_provider(pool))
providers.append(client.svn_client_get_ssl_server_trust_file_provider(pool))
providers.append(client.svn_client_get_ssl_client_cert_file_provider(pool))
providers.append(client.svn_client_get_ssl_client_cert_pw_file_provider(pool))
ctx.auth_baton = core.svn_auth_open(providers, pool)
ctx.config = core.svn_config_get_config(None, pool)
self.ctx = ctx
ra_callbacks = ra.svn_ra_callbacks_t()
ra_callbacks.auth_baton = ctx.auth_baton
self.ra_session = ra.svn_ra_open(self.rootpath, ra_callbacks, None,
ctx.config, pool)
self.youngest = ra.svn_ra_get_latest_revnum(self.ra_session, pool)
self._dirent_cache = { }
def __del__(self):
core.svn_pool_destroy(self.pool)
core.apr_terminate()
def itemtype(self, path_parts, rev):
path = self._getpath(path_parts[:-1])
rev = self._getrev(rev)
if not len(path_parts):
return vclib.DIR
dirents = self._get_dirents(path, rev)
try:
entry = dirents[path_parts[-1]]
if entry.kind == core.svn_node_dir:
return vclib.DIR
if entry.kind == core.svn_node_file:
return vclib.FILE
except KeyError:
raise vclib.ItemNotFound(path_parts)
def openfile(self, path_parts, rev):
rev = self._getrev(rev)
url = self.rootpath
if len(path_parts):
url = self.rootpath + '/' + self._getpath(path_parts)
tmp_file = tempfile.mktemp()
stream = core.svn_stream_from_aprfile(tmp_file, self.pool)
### rev here should be the last history revision of the URL
client.svn_client_cat(core.Stream(stream), url,
_rev2optrev(rev), self.ctx, self.pool)
core.svn_stream_close(stream)
return SelfCleanFP(tmp_file), rev
def listdir(self, path_parts, rev, options):
path = self._getpath(path_parts)
rev = self._getrev(rev)
entries = [ ]
dirents = self._get_dirents(path, rev)
for name in dirents.keys():
entry = dirents[name]
if entry.kind == core.svn_node_dir:
kind = vclib.DIR
elif entry.kind == core.svn_node_file:
kind = vclib.FILE
entries.append(vclib.DirEntry(name, kind))
return entries
def dirlogs(self, path_parts, rev, entries, options):
get_logs(self, self._getpath(path_parts), self._getrev(rev), entries)
def itemlog(self, path_parts, rev, options):
full_name = self._getpath(path_parts)
rev = self._getrev(rev)
# It's okay if we're told to not show all logs on a file -- all
# the revisions should match correctly anyway.
lc = LogCollector(full_name, options.get('svn_show_all_dir_logs', 0))
dir_url = self.rootpath
if full_name:
dir_url = dir_url + '/' + full_name
cross_copies = options.get('svn_cross_copies', 0)
client.svn_client_log([dir_url], _rev2optrev(rev), _rev2optrev(1),
1, not cross_copies, lc.add_log,
self.ctx, self.pool)
revs = lc.logs
revs.sort()
prev = None
for rev in revs:
rev.prev = prev
prev = rev
return revs
def annotate(self, path_parts, rev):
path = self._getpath(path_parts)
rev = self._getrev(rev)
url = self.rootpath + (path and '/' + path)
blame_data = []
def _blame_cb(line_no, revision, author, date,
line, pool, blame_data=blame_data):
prev_rev = None
if revision > 1:
prev_rev = revision - 1
blame_data.append(_item(text=line, line_number=line_no+1,
rev=revision, prev_rev=prev_rev,
author=author, date=None))
client.svn_client_blame(url, _rev2optrev(1), _rev2optrev(rev),
_blame_cb, self.ctx, self.pool)
return blame_data, rev
def rawdiff(self, path_parts1, rev1, path_parts2, rev2, type, options={}):
p1 = self._getpath(path_parts1)
p2 = self._getpath(path_parts2)
r1 = self._getrev(rev1)
r2 = self._getrev(rev2)
args = vclib._diff_args(type, options)
try:
temp1 = temp_checkout(self, p1, r1, self.pool)
temp2 = temp_checkout(self, p2, r2, self.pool)
info1 = p1, date_from_rev(self, r1), r1
info2 = p2, date_from_rev(self, r2), r2
return vclib._diff_fp(temp1, temp2, info1, info2, args)
except vclib.svn.core.SubversionException, e:
if e.apr_err == vclib.svn.core.SVN_ERR_FS_NOT_FOUND:
raise vclib.InvalidRevision
raise
def _getpath(self, path_parts):
return string.join(path_parts, '/')
def _getrev(self, rev):
if rev is None or rev == 'HEAD':
return self.youngest
try:
rev = int(rev)
except ValueError:
raise vclib.InvalidRevision(rev)
if (rev < 0) or (rev > self.youngest):
raise vclib.InvalidRevision(rev)
return rev
def _get_dirents(self, path, rev):
if path:
key = str(rev) + '/' + path
dir_url = self.rootpath + '/' + path
else:
key = str(rev)
dir_url = self.rootpath
dirents = self._dirent_cache.get(key)
if dirents:
return dirents
dirents = client.svn_client_ls(dir_url, _rev2optrev(rev), 0,
self.ctx, self.pool)
self._dirent_cache[key] = dirents
return dirents
class _item:
def __init__(self, **kw):
vars(self).update(kw)

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +0,0 @@
#!/usr/bin/python
import mimetypes
import magic
have_chardet = 0
try:
import chardet
have_chardet = 1
except: pass
class ContentMagic:
def __init__(self, encodings):
self.encodings = encodings.split(':')
self.mime_magic = None
self.errors = []
# Try to load magic
self.mime_magic = magic.open(magic.MAGIC_MIME_TYPE)
self.mime_magic.load()
# returns MIME type
def guess_mime(self, mime, filename, tempfile):
if mime == 'application/octet-stream':
mime = ''
if not mime and filename:
mime = mimetypes.guess_type(filename)[0]
if not mime and tempfile and self.mime_magic:
if type(tempfile) == type(''):
mime = self.mime_magic.file(tempfile)
else:
c = tempfile.read(4096)
mime = self.mime_magic.buffer(c)
return mime
# returns (utf8_content, charset)
def guess_charset(self, content):
# Try UTF-8
charset = 'utf-8'
try: content = content.decode('utf-8')
except: charset = None
if charset is None and have_chardet and len(content) > 64:
# Try to guess with chardet
try:
# Only detect on first 256KB if content is longer
if len(content) > 256*1024:
charset = chardet.detect(content[0:256*1024])
else:
charset = chardet.detect(content)
if charset and charset['encoding']:
charset = charset['encoding']
if charset == 'MacCyrillic':
# Silly MacCyr, try cp1251
try:
content = content.decode('windows-1251')
charset = 'windows-1251'
except: content = content.decode(charset)
else:
content = content.decode(charset)
except: charset = None
# Then try to guess primitively
if charset is None:
for charset in self.encodings:
try:
content = content.decode(charset)
break
except: charset = None
return (content, charset)
# guess and encode return value into UTF-8
def utf8(self, content):
(uni, charset) = self.guess_charset(content)
if charset:
return uni.encode('utf-8')
return content

View File

@@ -1,6 +1,6 @@
# -*-python-*-
#
# Copyright (C) 1999-2013 The ViewCVS Group. All Rights Reserved.
# Copyright (C) 1999-2006 The ViewCVS Group. All Rights Reserved.
#
# By using this file, you agree to the terms and conditions set forth in
# the LICENSE.html file which can be found at the top level of the ViewVC
@@ -14,19 +14,9 @@
#
# -----------------------------------------------------------------------
import os, sys, traceback, thread
try:
import win32api
except ImportError, e:
raise ImportError, str(e) + """
Did you install the Python for Windows Extensions?
http://sourceforge.net/projects/pywin32/
"""
import os, sys, traceback, string, thread
import win32process, win32pipe, win32con
import win32event, win32file, winerror
import win32event, win32file, win32api, winerror
import pywintypes, msvcrt
# Buffer size for spooling
@@ -40,9 +30,9 @@ def CommandLine(command, args):
"""Convert an executable path and a sequence of arguments into a command
line that can be passed to CreateProcess"""
cmd = "\"" + command.replace("\"", "\"\"") + "\""
cmd = "\"" + string.replace(command, "\"", "\"\"") + "\""
for arg in args:
cmd = cmd + " \"" + arg.replace("\"", "\"\"") + "\""
cmd = cmd + " \"" + string.replace(arg, "\"", "\"\"") + "\""
return cmd
def CreateProcess(cmd, hStdInput, hStdOutput, hStdError):
@@ -109,13 +99,13 @@ def CreatePipe(readInheritable, writeInheritable):
def File2FileObject(pipe, mode):
"""Make a C stdio file object out of a win32 file handle"""
if mode.find('r') >= 0:
if string.find(mode, 'r') >= 0:
wmode = os.O_RDONLY
elif mode.find('w') >= 0:
elif string.find(mode, 'w') >= 0:
wmode = os.O_WRONLY
if mode.find('b') >= 0:
if string.find(mode, 'b') >= 0:
wmode = wmode | os.O_BINARY
if mode.find('t') >= 0:
if string.find(mode, 't') >= 0:
wmode = wmode | os.O_TEXT
return os.fdopen(msvcrt.open_osfhandle(pipe.Detach(),wmode),mode)

View File

@@ -1,82 +0,0 @@
Here lie TODO items for the pluggable authz system:
* Subversion uses path privelege to determine visibility of revision
metadata. That logic is pretty Subversion-specific, so it feels like it
belongs outside the vcauth library as just a helper function in viewvc.py
or something. The algorithm is something like this (culled from the
CollabNet implementation, and not expected to work as edited):
# Subversion revision access levels
REVISION_ACCESS_NONE = 0
REVISION_ACCESS_PARTIAL = 1
REVISION_ACCESS_FULL = 2
def check_svn_revision_access(request, rev):
# Check our revision access cache first.
if request.rev_access_cache.has_key(rev):
return request.rev_access_cache[rev]
# Check our cached answer to the question "Does the user have
# an all-access or a not-at-all-access pass?"
if request.full_access is not None:
return request.full_access \
and REVISION_ACCESS_FULL or REVISION_ACCESS_NONE
# Get a list of paths changed in REV.
### FIXME: There outta be a vclib-complaint way to do this,
### as this won't work for vclib.svn_ra.
import svn.fs
rev_root = svn.fs.revision_root(self.repos.fs_ptr, rev)
changes = svn.fs.paths_changed(rev_root)
# Loop over the list of changed paths, asking the access question
# for each one. We'll track whether we've found any readable paths
# as well as any un-readable (non-authorized) paths, and quit
# checking as soon as we know our revision access level.
found_readable = 0
found_unreadable = 0
for path in changes.keys():
parts = _path_parts(path)
kind = request.repos.itemtype(parts, rev)
if kind == vclib.DIR:
access = request.auth.check_dir_access(parts, rev)
elif:
access = request.auth.check_file_access(parts, rev)
if access:
found_readable = 1
else:
found_unreadable = 1
# Optimization: if we've found at least one readable, and one
# unreadable, we needn't ask about any more paths.
if found_readable and found_unreadable:
break
# If there are paths but we can't read any of them, no access is
# granted.
if len(changes) and not found_readable:
request.rev_access_cache[rev] = REVISION_ACCESS_NONE
# If we found at least one unreadable path, partial access is
# granted.
elif found_unreadable:
request.rev_access_cache[rev] = REVISION_ACCESS_PARTIAL
# Finally, if there were no paths at all, or none of the existing
# ones were unreadable, grant full access.
else:
request.rev_access_cache[rev] = REVISION_ACCESS_FULL
return request.rev_access_cache[rev]
The problems are: where does one hang the revision access cache
so that it doesn't survive a given request? On the request, as
shown in the edited code above?
Can we actually get a good interface into the vcauth layer for
asking the all-access / no-access question? Obviously each vcauth
provider can cache that value for itself and use it as it deems
necessary, but ideally the revision access level question will
want to know if said auth provider was able to answer the question
and, if so, what the answer was.
Another off-the-wall idea -- let's just teach Subversion to do
this calculation as part of a libsvn_repos API.

Binary file not shown.

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