diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..6a6a4f0b --- /dev/null +++ b/CHANGES @@ -0,0 +1,269 @@ +Version 1.1.0 (released ??-???-????) + + * 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 support for integration with GNU source-highlight (issue #285) + * add unified configury of allowed views + * add support for disabling the checkout view (now the default state) + * 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 + * add support for hiding errorful entries from the directory view (issue #105) + +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 + * fix Subversion annotate functionality on Windows (issue #18) + * fix annotate assertions on uncanonicalized #include paths (issue #208) + * make RSS URL method match the method used to generate it (issue #245) + * fix Subversion annotation to run non-interactively, preventing hangs + * fix bug in custom syntax highlighter fallback logic + * fix bug in PHP CGI hack to avoid force-cgi-redirect errors + +Version 1.0.1 (released 20-Jul-2006) + + * fix exception on log page when use_pagesize is enabled + * fix an XHTML validation bug in the footer template (issue #239) + * fix handling of single-component CVS revision numbers (issue #237) + * fix bug in download-as-text URL link generation (issue #241) + * fix query.cgi bug, missing 'rss_href' template data item (issue #249) + * no longer omit empty Subversion directories from tarballs (issue #250) + * use actual modification time for Subversion directories in tarballs + +Version 1.0 (released 01-May-2006) + + * add support for viewing Subversion repositories + * add support for running on MS Windows + * generate strict XHTML output + * add support for caching by sending "Last-Modified", "Expires", + "ETag", and "Cache-Control" headers + * add support for Mod_Python on Apache 2.x and ASP on IIS + * Several changes to standalone.py: + - -h commandline option to specify hostname for non local use. + - -r commandline option may be repeated to use more than repository + before actually installing ViewCVS. + - New GUI field to test paging. + * add new, better-integrated query interface + * add integrated RSS feeds + * add new "root_as_url_component" option to embed root names as + path components in ViewCVS URLs for a more natural URL scheme + in ViewCVS configurations with multiple repositories. + * add new "use_localtime" option to display local times instead of UTC times + * add new "root_parents" option to make it possible to add and + remove repositories without modifying the ViewCVS configuration + * add new "template_dir" option to facilitate switching between sets of + templates + * add new "sort_group_dirs" option to disable grouping of + directories in directory listings + * add new "port" option to connect to a MySQL database on a nonstandard port + * make "default_root" option optional. When no root is specified, + show a page listing all available repositories + * add "default_file_view" option to make it possible for relative + links and image paths in checked out HTML files to work without + the need for special /*checkout*/ prefixes in URLs. Deprecate + "checkout_magic" option and disable by default + * add "limit_changes" option to limit number of changed files shown per + commit by default in query results and in the Subversion revision view + * hide CVS "Attic" directories and add simple toggle for showing + dead files in directory listings + * show Unified, Context and Side-by-side diffs in HTML instead of + in bare text pages + * make View/Download links work the same for all file types + * add links to tip of selected branch on log page + * allow use of "Highlight" program for colorizing + * enable enscript colorizing for more file types + * add sorting arrows for directory views + * get rid of popup windows for checkout links + * obfuscate email addresses in html output by encoding @ symbol + with an HTML character reference + * add paging capability + * Improvements to templates + - add new template authoring guide + - increase coverage, use templates to produce HTML for diff pages, + markup pages, annotate pages, and error pages + - move more common page elements into includes + - add new template variables providing ViewCVS URLs for more + links between related pages and less URL generation inside + templates + * add new [define] EZT directive for assigning variables within templates + * add command line argument parsing to install script to allow + non-interactive installs + * add stricter parameter validation to lower likelihood of cross-site + scripting vulnerabilities + * add support for cvsweb's "mime_type=text/x-cvsweb-markup" URLs + * fix incompatibility with enscript 1.6.3 + * fix bug in parsing FreeBSD rlog output + * work around rlog assumption all two digit years in RCS files are + relative to the year 1900. + * change loginfo-handler to cope with spaces in filenames and + support a simpler command line invocation from CVS + * make cvsdbadmin work properly when invoked on CVS subdirectory + paths instead of top-level CVS root paths + * show diff error when comparing two binary files + * make regular expression search skip binary files + * make regular expression search skip nonversioned files in CVS + directories instead of choking on them + * fix tarball generator so it doesn't include forbidden modules + * output "404 Not Found" errors instead of "403 Forbidden" errors + to not reveal whether forbidden paths exist + * fix sorting bug in directory view + * reset log and directory page numbers when leaving those pages + * reset sort direction in directory listing when clicking new columns + * fix "Accept-Language" handling for Netscape 4.x browsers + * fix file descriptor leak in standalone server + * clean up zombie processes from running enscript + * fix mysql "Too many connections" error in cvsdbadmin + * get rid of mxDateTime dependency for query database + * store query database times in UTC instead of local time + * fix daylight saving time bugs in various parts of the code + +Version 0.9.4 (released 17-Aug-2005) + + * security fix: omit forbidden/hidden modules from query results. + +Version 0.9.3 (released 17-May-2005) + + * security fix: disallow bad "content-type" input [CAN-2004-1062] + * security fix: disallow bad "sortby" and "cvsroot" input [CAN-2002-0771] + * security fix: omit forbidden/hidden modules from tarballs [CAN-2004-0915] + +Version 0.9.2 (released 15-Jan-2002) + + * fix redirects to Attic for diffs + * fix diffs that have no changes (causing an infinite loop) + +Version 0.9.1 (released 26-Dec-2001) + + * fix a problem with some syntax in ndiff.py which isn't compatible + with Python 1.5.2 (causing problems at install time) + * remove a debug statement left in the code which continues to + append lines to /tmp/log + +Version 0.9 (released 23-Dec-2001) + + * create templates for the rest of the pages: markup pages, graphs, + annotation, and diff. + * add multiple language support and dynamic selection based on the + Accept-Language request header + * add support for key/value files to provide a way for user-defined + variables within templates + * add optional regex searching for file contents + * add new templates for the navigation header and the footer + * EZT changes: + - add formatting into print directives + - add parameters to [include] directives + - relax what can go in double quotes + - [include] directives are now relative to the current template + - throw an exception for unclosed blocks + * changes to standalone.py: add flag for regex search + * add more help pages + * change installer to optionally show diffs + * fix to log.ezt and log_table.ezt to select "Side by Side" properly + * 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 + * big update of cvs commit database + - updated MySQL support + - new CGI + - better database caching + - switch from old templates to new EZT templates (and integration + of look-and-feel) + * optional usage of CVSGraph is now builtin + * standalone server (for testing) is now provided + * shifted some options from viewcvs.conf to the templates + * the help at the top of the pages has been shifted to separate help + pages, so experienced users don't have to keep seeing it + * paths in viewcvs.conf don't require trailing slashes any more + * tweak the colorizing for Pascal and Fortran files + * fix file readability problem where the user had access via the + group, but the process' group did not match that group + * some Daylight Savings Time fixes in the CVS commit database + * fix tarball generation (the file name) for the root dir + * changed default human-readable-diff colors to "stoplight" metaphor + * web site and doc revamps + * fix the mime types on the download, view, etc links + * improved error response when the cvs root is missing + * don't try to process vhosts if the config section is not present + * various bug fixes and UI tweaks diff --git a/COMMITTERS b/COMMITTERS new file mode 100644 index 00000000..d3e729a3 --- /dev/null +++ b/COMMITTERS @@ -0,0 +1,28 @@ +The following people have commit access to the ViewVC sources. +Note that this is not a full list of ViewVC's authors, however -- +for that, you'd need to look over the log messages to see all the +patch contributors. + +If you have a question or comment, it's probably best to mail +dev@viewvc.tigris.org, rather than mailing any of these people +directly. + + gstein Greg Stein + jpaint Jay Painter + akr Tanaka Akira + timcera Tim Cera + pefu Peter Funk + lbruand Lucas Bruand + cmpilato C. Michael Pilato + rey4 Russell Yanofsky + mharig Mark Harig + northeye Takuo Kitame + jamesh James Henstridge + maxb Max Bowsher + eh Erik Hülsmann + mhagger Michael Haggerty + +## Local Variables: +## coding:utf-8 +## End: +## vim:encoding=utf8 diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..1d15d9a6 --- /dev/null +++ b/INSTALL @@ -0,0 +1,520 @@ +CONTENTS +-------- + TO THE IMPATIENT + SECURITY INFORMATION + INSTALLING VIEWVC + APACHE CONFIGURATION + UPGRADING VIEWVC + SQL CHECKIN DATABASE + ENABLING SYNTAX COLORATION + CVSGRAPH CONFIGURATION + IF YOU HAVE PROBLEMS... + + +TO THE IMPATIENT +---------------- +Congratulations on getting this far. :-) + + Required Software And Configuration Needed To Run ViewVC: + + 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 + (http://www.gnu.org/software/diffutils/diffutils.html) + * read-only, physical access to a CVS repository + (See http://www.cvshome.org/ for more information) + + For Subversion Support: + + * Python 2.0 or later + (http://www.python.org/) + * Subversion, Version Control System, 1.3.1 or later + (binary installation and Python bindings) + (http://subversion.tigris.org/) + + Optional: + + * a web server capable of running CGI programs + (for example, Apache at http://httpd.apache.org/) + * 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) + * CvsGraph 1.5.0 or later, graphical CVS revision tree generator + (http://www.akhphd.au.dk/~bertho/cvsgraph/) + + Quick sanity check: + + If you just want to see what your repository looks like when seen + through ViewVC, type: + + $ 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. + Next, configure your web server (in the way appropriate to that browser) + to run /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 +------------------ + +NOTE: Windows users can refer to windows/README for Windows-specific +installation instructions. + +1) To get viewvc.cgi to work, make sure that you have Python installed + and a webserver which is capable of executing CGI scripts (either + based on the .cgi extension, or by placing the script within a specific + directory). + + Note that to browse CVS repositories, the viewvc.cgi script needs to + have READ-ONLY, physical access to the repository (or a copy of it). + Therefore, rsh/ssh or pserver access to the repository will not work. + And you need to have the RCS utilities installed, specifically "rlog", + "rcsdiff", and "co". + +2) Installation is handled by the ./viewvc-install script. Run this + script and you will be prompted for a installation root path. + The default is /usr/local/viewvc-VERSION, where VERSION is + the version of this ViewVC release. The installer sets the install + path in some of the files, and ViewVC cannot be moved to a + different path after the install. + + Note: while 'root' is usually required to create /usr/local/viewvc, + ViewVC does not have to be installed as root, nor does it run as root. + It is just as valid to place ViewVC in a home directory, too. + + Note: viewvc-install will create directories if needed. It will + prompt before overwriting files that may have been modified (such + as viewvc.conf), thus making it safe to install over the top of + a previous installation. It will always overwrite program files, + however. + +3) Edit /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) + default_root + root_as_url_component + rcs_dir + mime_types_file + + There are some other options that are usually nice to change. See + viewvc.conf for more information. ViewVC provides a working, + default look. However, if you want to customize the look of ViewVC + then edit the files in /templates. + You need knowledge about HTML to edit the templates. + +4) The CGI programs are in /bin/cgi/. You can + symlink to this directory from somewhere in your published HTTP server + path if your webserver is configured to follow symbolic links. You can + also copy the installed /bin/cgi/*.cgi + 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. + + 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. + + +APACHE CONFIGURATION +-------------------- + +1) Find out where the web server configuration file is kept. Typical + locations are /etc/httpd/httpd.conf, /etc/httpd/conf/httpd.conf, + and /etc/apache/httpd.conf. Depending on how apache was installed, + you may also look under /usr/local/etc or /etc/local. Use the vendor + documentation or the find utility if in doubt. + +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 /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 + + continue with step 3). + +or alternatively METHOD B: +2) Copy the CGI scripts from + /bin/cgi/*.cgi + to the /cgi-bin/ directory configured in your httpd.conf file. + + continue with step 3). + +and then there's METHOD C: +2) Copy the CGI scripts from + /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: + + Options +ExecCGI + AddHandler cgi-script .cgi + + (Note: For this to work mod_cgi has to be loaded. And for the .htaccess file + to be effective, "AllowOverride All" or "AllowOverride Options FileInfo" + need to have been specified for the directory.) + + 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 + /bin/mod_python/ + 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. + + Note: If you are using Mod_Python under Apache 1.3 the tarball generation + feature may not work because it uses multithreading. This works fine + under Apache 2. + + continue with step 3). + +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" + +4) Optional: Add access control. + + In your httpd.conf you can control access to certain modules by adding + directives like this: + + /"> + AllowOverride None + AuthUserFile /path/to/passwd/file + AuthName "Client Access" + AuthType Basic + require valid-user + + + 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:///viewvc/*checkout*/ + http:///viewvc/~checkout~/ + http:///viewvc/.tar.gz?view=tar + +5) 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. + + +UPGRADING VIEWVC +----------------- + +Please read the file upgrading-howto.html in the docs/ subdirectory. + + +SQL CHECKIN DATABASE +-------------------- + +This feature is a clone of the Mozilla Project's Bonsai database. It +catalogs every commit in the CVS or Subversion repository into a SQL +database. In fact, the databases are 100% compatible. + +Various queries can be performed on the database. After installing ViewVC, +there are some additional steps required to get the database working. + +1) You need MySQL and MySQLdb (a Python DBAPI 2.0 module) installed. + +2) You need to create a MySQL user who has permission to create databases. + Optionally, you can create a second user with read-only access to the + database. + +3) Run the /bin/make-database script. It will + prompt you for your MySQL user, password, and the name of database you + want to create. The database name defaults to "ViewVC". This script + creates the database and sets up the empty tables. If you run this on a + existing ViewVC database, you will lose all your data! + +4) Edit your /viewvc.conf file. + There is a [cvsdb] section. You will need to set: + + 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 + passwd = # password for read/write database user + readonly_user = # read-only database user + readonly_passwd = # password for the read-only 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) 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 publish CVS commits into the database: + + 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. + + 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 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 /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 /bin/loginfo-handler %{sVv} + + 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 /bin/loginfo-handler %{sVv} cvsnt + + 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" + /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" + /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. + +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 +-------------------------- + +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). + + +CVSGRAPH CONFIGURATION +---------------------- + +CvsGraph is a program that can display a clickable, graphical tree +of files in a CVS repository. + +WARNING: Under certain circumstances (many revisions of a file +or many branches or both) CvsGraph can generate very huge images. +Especially on thin clients these images may crash the Web-Browser. +Currently there is no known way to avoid this behavior of CvsGraph. +So you have been warned! + +Nevertheless, CvsGraph can be quite helpful on repositories with +a reasonable number of revisions and branches. + +1) Install CvsGraph using your system's package manager or downloading + from the project home page. + +2) Set the 'use_cvsgraph' options in viewvc.conf to 1. + +3) You may also need to set the 'cvsgraph_path' option if the + CvsGraph executable is not located on the system PATH. + +4) There is a file /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. + + +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.) + +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: + + % 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' + >>> + +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.) + +Configuration of the Subversion repositories happens in much the same +way as with CVS repositories, only with the 'svn_roots' configuration +variable instead of the 'cvs_roots' one. + + +IF YOU HAVE PROBLEMS ... +------------------------ + +If nothing seems to 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 + + * 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) + + * 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'. + + * Does ViewVC find your RCS utilities? (edit rcs_dir) + +If something else happens or you can't get it to work: + + * Check the ViewVC home page: + + http://viewvc.org/ + + * 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 + + * Check the ViewVC issue database to see if the problem you are + seeing is the result of a known bug: + + 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. diff --git a/LICENSE.html b/LICENSE.html new file mode 100644 index 00000000..2dcb6d02 --- /dev/null +++ b/LICENSE.html @@ -0,0 +1,65 @@ + + + +ViewVC: License v1 + + + + +

The following text constitutes the license agreement for the ViewVC software (formerly known + as ViewCVS). It is an agreement between The ViewCVS + Group and the users of ViewVC.

+ +
+ +

Copyright © 1999-2008 The ViewCVS Group. All rights + reserved.

+ +

By using ViewVC, you agree to the terms and conditions set forth + below:

+ +

Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met:

+ +
    +
  1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following + disclaimer.
  2. +
  3. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution.
  4. +
+ +

THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE.

+ +
+ +
+ +

The following changes have occured to this license over time:

+
    +
  • May 12, 2001 — copyright years updated
  • +
  • September 5, 2002 — copyright years updated
  • +
  • March 17, 2006 — software renamed from "ViewCVS"
  • +
  • April 10, 2007 — copyright years updated
  • +
  • February 22, 2008 — copyright years updated
  • +
+ + + diff --git a/README b/README new file mode 100644 index 00000000..26ee5dea --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +ViewVC -- Viewing the content of CVS/SVN repositories with a Webbrowser. + +Please read the file INSTALL for more information. + +And see windows/README for more information on running ViewVC on +Microsoft Windows. diff --git a/bin/asp/query.asp b/bin/asp/query.asp new file mode 100644 index 00000000..17135795 --- /dev/null +++ b/bin/asp/query.asp @@ -0,0 +1,61 @@ +<%@ LANGUAGE = Python %> +<% + +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# query.asp: View CVS/SVN commit database by web browser +# +# ----------------------------------------------------------------------- +# +# This is a teeny stub to launch the main ViewVC app. It checks the load +# average, then loads the (precompiled) query.py file and runs it. +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +LIBRARY_DIR = None +CONF_PATHNAME = None + +######################################################################### +# +# Adjust sys.path to include our library directory +# + +import sys + +if LIBRARY_DIR: + if not LIBRARY_DIR in sys.path: + sys.path.insert(0, LIBRARY_DIR) + +######################################################################### + +import sapi +import viewvc +import query + +server = sapi.AspServer(Server, Request, Response, Application) +try: + cfg = viewvc.load_config(CONF_PATHNAME, server) + query.main(server, cfg, "viewvc.asp") +finally: + s.close() + +%> diff --git a/bin/asp/viewvc.asp b/bin/asp/viewvc.asp new file mode 100644 index 00000000..57fa1b64 --- /dev/null +++ b/bin/asp/viewvc.asp @@ -0,0 +1,65 @@ +<%@ LANGUAGE = Python %> +<% + +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# viewvc: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a teeny stub to launch the main ViewVC app. It checks the load +# average, then loads the (precompiled) viewvc.py file and runs it. +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +LIBRARY_DIR = None +CONF_PATHNAME = None + +######################################################################### +# +# Adjust sys.path to include our library directory +# + +import sys + +if LIBRARY_DIR: + if not LIBRARY_DIR in sys.path: + sys.path.insert(0, LIBRARY_DIR) + +######################################################################### + +### add code for checking the load average + +######################################################################### + +# go do the work +import sapi +import viewvc + +server = sapi.AspServer(Server, Request, Response, Application) +try: + cfg = viewvc.load_config(CONF_PATHNAME, server) + viewvc.main(server, cfg) +finally: + s.close() + +%> diff --git a/bin/cgi/query.cgi b/bin/cgi/query.cgi new file mode 100644 index 00000000..cbdd206c --- /dev/null +++ b/bin/cgi/query.cgi @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# query.cgi: View CVS/SVN commit database by web browser +# +# ----------------------------------------------------------------------- +# +# This is a teeny stub to launch the main ViewVC app. It checks the load +# average, then loads the (precompiled) viewvc.py file and runs it. +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +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) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], + "../../../lib"))) + +######################################################################### + +import sapi +import viewvc +import query + +server = sapi.CgiServer() +cfg = viewvc.load_config(CONF_PATHNAME, server) +query.main(server, cfg, "viewvc.cgi") diff --git a/bin/cgi/viewvc-strace.sh b/bin/cgi/viewvc-strace.sh new file mode 100644 index 00000000..f9606f89 --- /dev/null +++ b/bin/cgi/viewvc-strace.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# +# Set this script up with something like: +# +# ScriptAlias /viewvc-strace /home/gstein/src/viewvc/cgi/viewvc-strace.sh +# +thisdir="`dirname $0`" +exec strace -q -r -o /tmp/v-strace.log "${thisdir}/viewvc.cgi" diff --git a/bin/cgi/viewvc.cgi b/bin/cgi/viewvc.cgi new file mode 100644 index 00000000..b90e118d --- /dev/null +++ b/bin/cgi/viewvc.cgi @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# viewvc: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a teeny stub to launch the main ViewVC app. It checks the load +# average, then loads the (precompiled) viewvc.py file and runs it. +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +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) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], + "../../../lib"))) + +######################################################################### + +### add code for checking the load average + +######################################################################### + +# go do the work +import sapi +import viewvc + +server = sapi.CgiServer() +cfg = viewvc.load_config(CONF_PATHNAME, server) +viewvc.main(server, cfg) diff --git a/bin/cvsdbadmin b/bin/cvsdbadmin new file mode 100755 index 00000000..4f7010ad --- /dev/null +++ b/bin/cvsdbadmin @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 1999-2008 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/ +# +# ----------------------------------------------------------------------- +# +# administrative program for CVSdb; this is primarily +# used to add/rebuild CVS repositories to the database +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +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) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib"))) + +######################################################################### + +import os +import string +import cvsdb +import viewvc +import vclib.ccvs + + +def UpdateFile(db, repository, path, update, quiet_level): + try: + if update: + commit_list = cvsdb.GetUnrecordedCommitList(repository, path, db) + else: + commit_list = cvsdb.GetCommitListFromRCSFile(repository, path) + except cvsdb.error, e: + print '[ERROR] %s' % (e) + return + + file = string.join(path, "/") + printing = 0 + 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)), + else: + if quiet_level < 2: + printing = 1 + print '[%s [%d commits]]' % (file, len(commit_list)), + + ## add the commits into the database + for commit in commit_list: + db.AddCommit(commit) + if printing: + sys.stdout.write('.') + sys.stdout.flush() + if printing: + print + + +def RecurseUpdate(db, repository, directory, update, quiet_level): + for entry in repository.listdir(directory, None, {}): + path = directory + [entry.name] + + if entry.errors: + continue + + if entry.kind is vclib.DIR: + RecurseUpdate(db, repository, path, update, quiet_level) + continue + + if entry.kind is vclib.FILE: + UpdateFile(db, repository, path, update, quiet_level) + +def RootPath(path, quiet_level): + """Break os path into cvs root path and other parts""" + root = os.path.abspath(path) + path_parts = [] + + p = root + while 1: + if os.path.exists(os.path.join(p, 'CVSROOT')): + root = p + if quiet_level < 2: + 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." + break + + path_parts.append(pdir) + + root = cvsdb.CleanRepository(root) + path_parts.reverse() + 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)) + 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) + usage() + + # get repository and path, and do the work + root, path_parts = RootPath(args[2], quiet_level) + rootpath = vclib.ccvs.canonicalize_rootpath(root) + try: + 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 + db.PurgeRepository(root) + + if command in ('rebuild', 'update'): + repository = vclib.ccvs.CVSRepository(None, rootpath, None, + cfg.utilities, 0) + RecurseUpdate(db, repository, path_parts, + command == 'update', quiet_level) + except KeyboardInterrupt: + print + print '** break **' + + sys.exit(0) diff --git a/bin/loginfo-handler b/bin/loginfo-handler new file mode 100755 index 00000000..57ddccb9 --- /dev/null +++ b/bin/loginfo-handler @@ -0,0 +1,318 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 1999-2008 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/ +# +# ----------------------------------------------------------------------- +# +# updates SQL database with new commit records +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +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) +else: + 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 + +DEBUG_FLAG = 0 + +## output functions +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 + +def warning(text): + print 'WARNING(viewvc-loginfo):', text + +def error(text): + print 'ERROR(viewvc-loginfo):', text + sys.exit(1) + +_re_revisions = re.compile( + r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision + r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision + 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""" + + 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 = 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.*)" # comma and first revision + r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and first revision + r",(?P(?:\d+\.\d+)(?:\.\d+\.\d+)*|NONE)" # comma and second revision + r"$" # end of string +) + +def CvsNtArgParse(s, repository): + """CVSNT escapes all spaces in filenames and directory names with + backslashes""" + + if s[-18:] == r' -\ New\ directory': + 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) + + while 1: + fileinfo, pos = NextFile(s, pos) + if fileinfo is None: + break + + m = _re_cvsnt_revisions.match(fileinfo) + if m is None: + warning('Can\'t parse file information in "%s"' % fileinfo) + continue + + file_data = m.group('filename', 'old', 'new') + file_data_list.append(file_data) + + debug('File "%s", old revision %s, new revision %s' % 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 + + 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) + + # split up the directory components + dirpath = filter(None, string.split(os.path.normpath(directory), os.sep)) + + ## build a list of Commit objects + commit_list = [] + for filename, old_version, new_version in files: + filepath = dirpath + [filename] + + ## XXX: this is nasty: in the case of a removed file, we are not + ## given enough information to find it in the rlog output! + ## So instead, we rlog everything in the removed file, and + ## add any commits not already in the database + if new_version == 'NONE': + commits = cvsdb.GetUnrecordedCommitList(repository, filepath, db) + else: + commits = cvsdb.GetCommitListFromRCSFile(repository, filepath, + new_version) + + commit_list.extend(commits) + + ## add to the database + db.AddCommitList(commit_list) + + +## MAIN +if __name__ == '__main__': + ## get the repository from the environment + try: + repository = os.environ['CVSROOT'] + except KeyError: + error('CVSROOT not in environment') + + 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 /bin/loginfo-handler %p %{sVv} + if argc > 3: + directory, files = Cvs1Dot12ArgParse(sys.argv[1:]) + else: + if len(sys.argv) > 1: + # the first argument should contain file version information + arg = sys.argv[1] + else: + # if there are no arguments, read version information from + # first line of input like old versions of ViewCVS did + arg = string.rstrip(sys.stdin.readline()) + + 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') + 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) + + debug('Discarded from stdin:') + debug(map(lambda x: ' ' + x, sys.stdin.readlines())) # consume stdin + + repository = cvsdb.CleanRepository(repository) + + debug('Repository: %s' % (repository)) + debug('Directory: %s' % (directory)) + debug('Files: %s' % (str(files))) + + if files is None: + debug('Not a checkin, nothing to do') + else: + ProcessLoginfo(repository, directory, files) + + sys.exit(0) diff --git a/bin/make-database b/bin/make-database new file mode 100755 index 00000000..e27860ae --- /dev/null +++ b/bin/make-database @@ -0,0 +1,160 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- +# +# administrative program for CVSdb; creates a clean database in +# MySQL 3.22 or later +# +# ----------------------------------------------------------------------- + +import os, sys, string +import popen2 + +INTRO_TEXT = """\ +This script creates the database and tables in MySQL used by the +ViewVC checkin database. You will be prompted for: database server +hostname, database user, database user password, and database name. +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. +""" + +DATABASE_SCRIPT="""\ +DROP DATABASE IF EXISTS ; +CREATE DATABASE ; + +USE ; + +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=MyISAM; + +DROP TABLE IF EXISTS checkins; +CREATE TABLE checkins ( + 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 ci_when (ci_when), + KEY whoid (whoid), + KEY repositoryid_2 (repositoryid), + KEY dirid (dirid), + KEY fileid (fileid), + KEY branchid (branchid) +) TYPE=MyISAM; + +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) +) 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=MyISAM; + +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=MyISAM; + +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=MyISAM; + +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=MyISAM; + +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=MyISAM; +""" + +if __name__ == "__main__": + try: + print INTRO_TEXT + + # Prompt for necessary information + host = raw_input("MySQL Hostname [default: localhost]: ") or "" + user = raw_input("MySQL User: ") + passwd = raw_input("MySQL Password: ") + dbase = raw_input("ViewVC Database Name [default: ViewVC]: ") or "ViewVC" + + # Create the database + dscript = string.replace(DATABASE_SCRIPT, "", dbase) + host_option = host and "--host=%s" % (host) or "" + if sys.platform == "win32": + cmd = "mysql --user=%s --password=%s %s "\ + % (user, passwd, host_option) + mysql = os.popen(cmd, "w") # popen2.Popen3 is not provided on windows + mysql.write(dscript) + status = mysql.close() + else: + cmd = "{ mysql --user=%s --password=%s %s ; } 2>&1" \ + % (user, passwd, host_option) + pipes = popen2.Popen3(cmd) + pipes.tochild.write(dscript) + pipes.tochild.close() + print pipes.fromchild.read() + status = pipes.wait() + + if status: + print "[ERROR] the database did not create sucessfully." + sys.exit(1) + + print "Database created successfully." + except KeyboardInterrupt: + pass + sys.exit(0) + diff --git a/bin/mod_python/.htaccess b/bin/mod_python/.htaccess new file mode 100644 index 00000000..b30e4a3b --- /dev/null +++ b/bin/mod_python/.htaccess @@ -0,0 +1,3 @@ +AddHandler python-program .py +PythonHandler handler +PythonDebug On diff --git a/bin/mod_python/handler.py b/bin/mod_python/handler.py new file mode 100644 index 00000000..6ccabe9d --- /dev/null +++ b/bin/mod_python/handler.py @@ -0,0 +1,31 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# Mod_Python handler based on mod_python.publisher +# +# ----------------------------------------------------------------------- + +from mod_python import apache +import os.path + +def handler(req): + path, module_name = os.path.split(req.filename) + module_name, module_ext = os.path.splitext(module_name) + 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) + + return apache.OK diff --git a/bin/mod_python/query.py b/bin/mod_python/query.py new file mode 100644 index 00000000..d2258923 --- /dev/null +++ b/bin/mod_python/query.py @@ -0,0 +1,71 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# ViewVC: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a teeny stub to launch the main ViewVC app. It checks the load +# average, then loads the (precompiled) viewvc.py file and runs it. +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +LIBRARY_DIR = None +CONF_PATHNAME = None + +######################################################################### +# +# Adjust sys.path to include our library directory +# + +import sys + +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() + +cfg = viewvc.load_config(CONF_PATHNAME) + +def index(req): + server = sapi.ModPythonServer(req) + try: + query.main(server, cfg, "viewvc.py") + finally: + server.close() + diff --git a/bin/mod_python/viewvc.py b/bin/mod_python/viewvc.py new file mode 100644 index 00000000..842038e0 --- /dev/null +++ b/bin/mod_python/viewvc.py @@ -0,0 +1,61 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# viewvc: View CVS/SVN repositories via a web browser +# +# ----------------------------------------------------------------------- +# +# This is a teeny stub to launch the main ViewVC app. It checks the load +# average, then loads the (precompiled) viewvc.py file and runs it. +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +LIBRARY_DIR = None +CONF_PATHNAME = None + +######################################################################### +# +# Adjust sys.path to include our library directory +# + +import sys + +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() + +def index(req): + server = sapi.ModPythonServer(req) + cfg = viewvc.load_config(CONF_PATHNAME, server) + try: + viewvc.main(server, cfg) + finally: + server.close() diff --git a/bin/standalone.py b/bin/standalone.py new file mode 100755 index 00000000..657c2f36 --- /dev/null +++ b/bin/standalone.py @@ -0,0 +1,672 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- + +"""Run "standalone.py -p " to start an HTTP server on a given port +on the local machine to generate ViewVC web pages. +""" + +__author__ = "Peter Funk " +__date__ = "11 November 2001" +__version__ = "$Revision: 1962 $" +__credits__ = """Guido van Rossum, for an excellent programming language. +Greg Stein, for writing ViewCVS in the first place. +Ka-Ping Yee, for the GUI code and the framework stolen from pydoc.py. +""" + +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +LIBRARY_DIR = None +CONF_PATHNAME = None + +import sys +import os +import os.path +import stat +import string +import urllib +import rfc822 +import socket +import select +import BaseHTTPServer + +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 compat; compat.for_standalone() + + +class Options: + port = 49152 # default TCP/IP port used for the server + start_gui = 0 # No GUI unless requested. + daemon = 0 # stay in the foreground by default + repositories = {} # use default repositories specified in config + if sys.platform == 'mac': + host = '127.0.0.1' + else: + host = 'localhost' + script_alias = 'viewvc' + config_file = None + +# --- web browser interface: ---------------------------------------------- + +class StandaloneServer(sapi.CgiServer): + def __init__(self, handler): + sapi.CgiServer.__init__(self, inheritableOut = sys.platform != "win32") + self.handler = handler + + def header(self, content_type='text/html', status=None): + if not self.headerSent: + self.headerSent = 1 + if status is None: + statusCode = 200 + statusText = 'OK' + else: + p = string.find(status, ' ') + if p < 0: + statusCode = int(status) + statusText = '' + else: + statusCode = int(status[:p]) + statusText = status[p+1:] + self.handler.send_response(statusCode, statusText) + self.handler.send_header("Content-type", content_type) + for (name, value) in self.headers: + self.handler.send_header(name, value) + self.handler.end_headers() + + +def serve(host, port, callback=None): + """start a HTTP server on the given port. call 'callback' when the + server is ready to serve""" + + class ViewVC_Handler(BaseHTTPServer.BaseHTTPRequestHandler): + + def do_GET(self): + """Serve a GET request.""" + if not self.path or self.path == "/": + self.redirect() + elif self.is_viewvc(): + try: + self.run_viewvc() + except IOError: + # ignore IOError: [Errno 32] Broken pipe + pass + else: + self.send_error(404) + + def do_POST(self): + """Serve a POST request.""" + if self.is_viewvc(): + self.run_viewvc() + else: + self.send_error(501, "Can only POST to %s" + % (options.script_alias)) + + def is_viewvc(self): + """Check whether self.path is, or is a child of, the ScriptAlias""" + if self.path == '/' + options.script_alias: + return 1 + if self.path[:len(options.script_alias)+2] == \ + '/' + options.script_alias + '/': + return 1 + if self.path[:len(options.script_alias)+2] == \ + '/' + options.script_alias + '?': + return 1 + return 0 + + def redirect(self): + """redirect the browser to the viewvc URL""" + new_url = self.server.url + options.script_alias + '/' + self.send_response(301, "Moved (redirection follows)") + self.send_header("Content-type", "text/html") + self.send_header("Location", new_url) + self.end_headers() + self.wfile.write(""" + + + + +

Redirection to ViewVC

+Wait a second. You will be automatically redirected to ViewVC. +If this doesn't work, please click on the link above. + + +""" % tuple([new_url]*2)) + + def run_viewvc(self): + """This is a quick and dirty cut'n'rape from Python's + standard library module CGIHTTPServer.""" + scriptname = '/' + options.script_alias + assert string.find(self.path, scriptname) == 0 + viewvc_url = self.server.url[:-1] + scriptname + rest = self.path[len(scriptname):] + i = string.rfind(rest, '?') + if i >= 0: + rest, query = rest[:i], rest[i+1:] + else: + query = '' + # sys.stderr.write("Debug: '"+scriptname+"' '"+rest+"' '"+query+"'\n") + env = os.environ + # Since we're going to modify the env in the parent, provide empty + # values to override previously set values + for k in env.keys(): + if k[:5] == 'HTTP_': + del env[k] + for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', + 'HTTP_USER_AGENT', 'HTTP_COOKIE'): + if env.has_key(k): + env[k] = "" + # XXX Much of the following could be prepared ahead of time! + env['SERVER_SOFTWARE'] = self.version_string() + env['SERVER_NAME'] = self.server.server_name + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['SERVER_PROTOCOL'] = self.protocol_version + env['SERVER_PORT'] = str(self.server.server_port) + env['REQUEST_METHOD'] = self.command + uqrest = urllib.unquote(rest) + env['PATH_INFO'] = uqrest + env['SCRIPT_NAME'] = scriptname + if query: + env['QUERY_STRING'] = query + env['HTTP_HOST'] = self.server.address[0] + host = self.address_string() + if host != self.client_address[0]: + env['REMOTE_HOST'] = host + env['REMOTE_ADDR'] = self.client_address[0] + # AUTH_TYPE + # REMOTE_USER + # REMOTE_IDENT + if self.headers.typeheader is None: + env['CONTENT_TYPE'] = self.headers.type + else: + env['CONTENT_TYPE'] = self.headers.typeheader + length = self.headers.getheader('content-length') + if length: + env['CONTENT_LENGTH'] = length + accept = [] + for line in self.headers.getallmatchingheaders('accept'): + if line[:1] in string.whitespace: + accept.append(string.strip(line)) + else: + accept = accept + string.split(line[7:], ',') + env['HTTP_ACCEPT'] = string.joinfields(accept, ',') + ua = self.headers.getheader('user-agent') + if ua: + env['HTTP_USER_AGENT'] = ua + modified = self.headers.getheader('if-modified-since') + if modified: + env['HTTP_IF_MODIFIED_SINCE'] = modified + etag = self.headers.getheader('if-none-match') + if etag: + env['HTTP_IF_NONE_MATCH'] = etag + # XXX Other HTTP_* headers + decoded_query = string.replace(query, '+', ' ') + + # Preserve state, because we execute script in current process: + save_argv = sys.argv + save_stdin = sys.stdin + save_stdout = sys.stdout + save_stderr = sys.stderr + # For external tools like enscript we also need to redirect + # the real stdout file descriptor. (On windows, reassigning the + # sys.stdout variable is sufficient because pipe_cmds makes it + # the standard output for child processes.) + if sys.platform != "win32": save_realstdout = os.dup(1) + try: + try: + sys.stdout = self.wfile + if sys.platform != "win32": + os.dup2(self.wfile.fileno(), 1) + sys.stdin = self.rfile + viewvc.main(StandaloneServer(self), cfg) + finally: + sys.argv = save_argv + sys.stdin = save_stdin + sys.stdout.flush() + if sys.platform != "win32": + os.dup2(save_realstdout, 1) + os.close(save_realstdout) + sys.stdout = save_stdout + sys.stderr = save_stderr + except SystemExit, status: + self.log_error("ViewVC exit status %s", str(status)) + else: + self.log_error("ViewVC exited ok") + + class ViewVC_Server(BaseHTTPServer.HTTPServer): + def __init__(self, host, port, callback): + self.address = (host, port) + self.url = 'http://%s:%d/' % (host, port) + self.callback = callback + BaseHTTPServer.HTTPServer.__init__(self, self.address, + self.handler) + + def serve_until_quit(self): + self.quit = 0 + while not self.quit: + rd, wr, ex = select.select([self.socket.fileno()], [], [], 1) + if rd: + self.handle_request() + + def server_activate(self): + BaseHTTPServer.HTTPServer.server_activate(self) + if self.callback: + self.callback(self) + + def server_bind(self): + # set SO_REUSEADDR (if available on this platform) + if hasattr(socket, 'SOL_SOCKET') \ + and hasattr(socket, 'SO_REUSEADDR'): + self.socket.setsockopt(socket.SOL_SOCKET, + socket.SO_REUSEADDR, 1) + BaseHTTPServer.HTTPServer.server_bind(self) + + ViewVC_Server.handler = ViewVC_Handler + + try: + # XXX Move this code out of this function. + # Early loading of configuration here. Used to + # allow tinkering with some configuration settings: + handle_config(options.config_file) + if options.repositories: + cfg.general.default_root = "Development" + for repo_name in options.repositories.keys(): + repo_path = os.path.normpath(options.repositories[repo_name]) + if os.path.exists(os.path.join(repo_path, "CVSROOT", + "config")): + cfg.general.cvs_roots[repo_name] = repo_path + elif os.path.exists(os.path.join(repo_path, "format")): + cfg.general.svn_roots[repo_name] = repo_path + elif cfg.general.cvs_roots.has_key("Development") and \ + not os.path.isdir(cfg.general.cvs_roots["Development"]): + sys.stderr.write("*** No repository found. Please use the -r option.\n") + sys.stderr.write(" Use --help for more info.\n") + raise KeyboardInterrupt # Hack! + os.close(0) # To avoid problems with shell job control + + # always use default docroot location + cfg.options.docroot = None + + # if cvsnt isn't found, fall back to rcs + if (cfg.conf_path is None and cfg.utilities.cvsnt): + import popen + cvsnt_works = 0 + try: + fp = popen.popen(cfg.utilities.cvsnt, ['--version'], 'rt') + try: + while 1: + line = fp.readline() + if not line: break + if string.find(line, "Concurrent Versions System (CVSNT)")>=0: + cvsnt_works = 1 + while fp.read(4096): + pass + break + finally: + fp.close() + except: + pass + if not cvsnt_works: + cfg.utilities.cvsnt = None + + ViewVC_Server(host, port, callback).serve_until_quit() + except (KeyboardInterrupt, select.error): + pass + print 'server stopped' + +def handle_config(config_file): + global cfg + cfg = viewvc.load_config(config_file or CONF_PATHNAME) + +# --- graphical interface: -------------------------------------------------- + +def nogui(missing_module): + sys.stderr.write( + "Sorry! Your Python was compiled without the %s module"%missing_module+ + " enabled.\nI'm unable to run the GUI part. Please omit the '-g'\n"+ + "and '--gui' options or install another Python interpreter.\n") + raise SystemExit, 1 + +def gui(host, port): + """Graphical interface (starts web server and pops up a control window).""" + class GUI: + def __init__(self, window, host, port): + self.window = window + self.server = None + self.scanner = None + + try: + import Tkinter + except ImportError: + nogui("Tkinter") + + self.server_frm = Tkinter.Frame(window) + self.title_lbl = Tkinter.Label(self.server_frm, + text='Starting server...\n ') + self.open_btn = Tkinter.Button(self.server_frm, + text='open browser', command=self.open, state='disabled') + self.quit_btn = Tkinter.Button(self.server_frm, + text='quit serving', command=self.quit, state='disabled') + + + self.window.title('ViewVC standalone') + self.window.protocol('WM_DELETE_WINDOW', self.quit) + self.title_lbl.pack(side='top', fill='x') + self.open_btn.pack(side='left', fill='x', expand=1) + self.quit_btn.pack(side='right', fill='x', expand=1) + + # Early loading of configuration here. Used to + # allow tinkering with configuration settings through the gui: + handle_config() + if not LIBRARY_DIR: + cfg.options.cvsgraph_conf = "../cgi/cvsgraph.conf.dist" + + self.options_frm = Tkinter.Frame(window) + + # cvsgraph toggle: + self.cvsgraph_ivar = Tkinter.IntVar() + self.cvsgraph_ivar.set(cfg.options.use_cvsgraph) + self.cvsgraph_toggle = Tkinter.Checkbutton(self.options_frm, + text="enable cvsgraph (needs binary)", var=self.cvsgraph_ivar, + command=self.toggle_use_cvsgraph) + self.cvsgraph_toggle.pack(side='top', anchor='w') + + # enscript toggle: + self.enscript_ivar = Tkinter.IntVar() + self.enscript_ivar.set(cfg.options.use_enscript) + self.enscript_toggle = Tkinter.Checkbutton(self.options_frm, + text="enable enscript (needs binary)", var=self.enscript_ivar, + command=self.toggle_use_enscript) + self.enscript_toggle.pack(side='top', anchor='w') + + # show_subdir_lastmod toggle: + self.subdirmod_ivar = Tkinter.IntVar() + self.subdirmod_ivar.set(cfg.options.show_subdir_lastmod) + self.subdirmod_toggle = Tkinter.Checkbutton(self.options_frm, + text="show subdir last mod (dir view)", var=self.subdirmod_ivar, + command=self.toggle_subdirmod) + self.subdirmod_toggle.pack(side='top', anchor='w') + + # use_re_search toggle: + self.useresearch_ivar = Tkinter.IntVar() + self.useresearch_ivar.set(cfg.options.use_re_search) + self.useresearch_toggle = Tkinter.Checkbutton(self.options_frm, + text="allow regular expr search", var=self.useresearch_ivar, + command=self.toggle_useresearch) + self.useresearch_toggle.pack(side='top', anchor='w') + + # use_localtime toggle: + self.use_localtime_ivar = Tkinter.IntVar() + self.use_localtime_ivar.set(cfg.options.use_localtime) + self.use_localtime_toggle = Tkinter.Checkbutton(self.options_frm, + text="use localtime (instead of UTC)", + var=self.use_localtime_ivar, + command=self.toggle_use_localtime) + self.use_localtime_toggle.pack(side='top', anchor='w') + + # use_pagesize integer var: + self.usepagesize_lbl = Tkinter.Label(self.options_frm, + text='Paging (number of items per page, 0 disables):') + self.usepagesize_lbl.pack(side='top', anchor='w') + self.use_pagesize_ivar = Tkinter.IntVar() + self.use_pagesize_ivar.set(cfg.options.use_pagesize) + self.use_pagesize_entry = Tkinter.Entry(self.options_frm, + width=10, textvariable=self.use_pagesize_ivar) + self.use_pagesize_entry.bind('', self.set_use_pagesize) + self.use_pagesize_entry.pack(side='top', anchor='w') + + # directory view template: + self.dirtemplate_lbl = Tkinter.Label(self.options_frm, + text='Choose HTML Template for the Directory pages:') + self.dirtemplate_lbl.pack(side='top', anchor='w') + self.dirtemplate_svar = Tkinter.StringVar() + self.dirtemplate_svar.set(cfg.templates.directory) + self.dirtemplate_entry = Tkinter.Entry(self.options_frm, + width = 40, textvariable=self.dirtemplate_svar) + self.dirtemplate_entry.bind('', self.set_templates_directory) + self.dirtemplate_entry.pack(side='top', anchor='w') + self.templates_dir = Tkinter.Radiobutton(self.options_frm, + text="directory.ezt", value="templates/directory.ezt", + var=self.dirtemplate_svar, command=self.set_templates_directory) + self.templates_dir.pack(side='top', anchor='w') + self.templates_dir_alt = Tkinter.Radiobutton(self.options_frm, + text="dir_alternate.ezt", value="templates/dir_alternate.ezt", + var=self.dirtemplate_svar, command=self.set_templates_directory) + self.templates_dir_alt.pack(side='top', anchor='w') + + # log view template: + self.logtemplate_lbl = Tkinter.Label(self.options_frm, + text='Choose HTML Template for the Log pages:') + self.logtemplate_lbl.pack(side='top', anchor='w') + self.logtemplate_svar = Tkinter.StringVar() + self.logtemplate_svar.set(cfg.templates.log) + self.logtemplate_entry = Tkinter.Entry(self.options_frm, + width = 40, textvariable=self.logtemplate_svar) + self.logtemplate_entry.bind('', self.set_templates_log) + self.logtemplate_entry.pack(side='top', anchor='w') + self.templates_log = Tkinter.Radiobutton(self.options_frm, + text="log.ezt", value="templates/log.ezt", + var=self.logtemplate_svar, command=self.set_templates_log) + self.templates_log.pack(side='top', anchor='w') + self.templates_log_table = Tkinter.Radiobutton(self.options_frm, + text="log_table.ezt", value="templates/log_table.ezt", + var=self.logtemplate_svar, command=self.set_templates_log) + self.templates_log_table.pack(side='top', anchor='w') + + # query view template: + self.querytemplate_lbl = Tkinter.Label(self.options_frm, + text='Template for the database query page:') + self.querytemplate_lbl.pack(side='top', anchor='w') + self.querytemplate_svar = Tkinter.StringVar() + self.querytemplate_svar.set(cfg.templates.query) + self.querytemplate_entry = Tkinter.Entry(self.options_frm, + width = 40, textvariable=self.querytemplate_svar) + self.querytemplate_entry.bind('', self.set_templates_query) + self.querytemplate_entry.pack(side='top', anchor='w') + self.templates_query = Tkinter.Radiobutton(self.options_frm, + text="query.ezt", value="templates/query.ezt", + var=self.querytemplate_svar, command=self.set_templates_query) + self.templates_query.pack(side='top', anchor='w') + + # pack and set window manager hints: + self.server_frm.pack(side='top', fill='x') + self.options_frm.pack(side='top', fill='x') + + self.window.update() + self.minwidth = self.window.winfo_width() + self.minheight = self.window.winfo_height() + self.expanded = 0 + self.window.wm_geometry('%dx%d' % (self.minwidth, self.minheight)) + self.window.wm_minsize(self.minwidth, self.minheight) + + try: + import threading + except ImportError: + nogui("thread") + threading.Thread(target=serve, + args=(host, port, self.ready)).start() + + def toggle_use_cvsgraph(self, event=None): + cfg.options.use_cvsgraph = self.cvsgraph_ivar.get() + + def toggle_use_enscript(self, event=None): + cfg.options.use_enscript = self.enscript_ivar.get() + + def toggle_use_localtime(self, event=None): + cfg.options.use_localtime = self.use_localtime_ivar.get() + + def toggle_subdirmod(self, event=None): + cfg.options.show_subdir_lastmod = self.subdirmod_ivar.get() + + def toggle_useresearch(self, event=None): + cfg.options.use_re_search = self.useresearch_ivar.get() + + def set_use_pagesize(self, event=None): + cfg.options.use_pagesize = self.use_pagesize_ivar.get() + + def set_templates_log(self, event=None): + cfg.templates.log = self.logtemplate_svar.get() + + def set_templates_directory(self, event=None): + cfg.templates.directory = self.dirtemplate_svar.get() + + def set_templates_query(self, event=None): + cfg.templates.query = self.querytemplate_svar.get() + + def ready(self, server): + """used as callback parameter to the serve() function""" + self.server = server + self.title_lbl.config( + text='ViewVC standalone server at\n' + server.url) + self.open_btn.config(state='normal') + self.quit_btn.config(state='normal') + + def open(self, event=None, url=None): + """opens a browser window on the local machine""" + url = url or self.server.url + try: + import webbrowser + webbrowser.open(url) + except ImportError: # pre-webbrowser.py compatibility + if sys.platform == 'win32': + os.system('start "%s"' % url) + elif sys.platform == 'mac': + try: + import ic + ic.launchurl(url) + except ImportError: pass + else: + rc = os.system('netscape -remote "openURL(%s)" &' % url) + if rc: os.system('netscape "%s" &' % url) + + def quit(self, event=None): + if self.server: + self.server.quit = 1 + self.window.quit() + + import Tkinter + try: + gui = GUI(Tkinter.Tk(), host, port) + Tkinter.mainloop() + except KeyboardInterrupt: + pass + +# --- command-line interface: ---------------------------------------------- + +def cli(argv): + """Command-line interface (looks at argv to decide what to do).""" + import getopt + class BadUsage(Exception): pass + + try: + opts, args = getopt.getopt(argv[1:], 'gdc:p:r:h:s:', + ['gui', 'daemon', 'config-file=', 'host=', + 'port=', 'repository=', 'script-alias=']) + for opt, val in opts: + if opt in ('-g', '--gui'): + options.start_gui = 1 + elif opt in ('-r', '--repository'): + if options.repositories: # option may be used more than once: + num = len(options.repositories.keys())+1 + symbolic_name = "Repository"+str(num) + options.repositories[symbolic_name] = val + else: + options.repositories["Development"] = val + elif opt in ('-d', '--daemon'): + options.daemon = 1 + elif opt in ('-p', '--port'): + try: + options.port = int(val) + except ValueError: + raise BadUsage, "Port '%s' is not a valid port number" \ + % (val) + elif opt in ('-h', '--host'): + options.host = val + elif opt in ('-s', '--script-alias'): + options.script_alias = \ + string.join(filter(None, string.split(val, '/')), '/') + elif opt in ('-c', '--config-file'): + options.config_file = val + if options.start_gui and options.config_file: + raise BadUsage, "--config-file option is not valid in GUI mode." + if not options.start_gui and not options.port: + raise BadUsage, "You must supply a valid port, or run in GUI mode." + if options.daemon: + pid = os.fork() + if pid != 0: + sys.exit() + if options.start_gui: + gui(options.host, options.port, options.config_file) + return + elif options.port: + def ready(server): + print 'server ready at %s%s' % (server.url, + options.script_alias) + serve(options.host, options.port, ready) + return + except (getopt.error, BadUsage), err: + cmd = os.path.basename(sys.argv[0]) + port = options.port + host = options.host + script_alias = options.script_alias + if str(err): + sys.stderr.write("ERROR: %s\n\n" % (str(err))) + sys.stderr.write("""Usage: %(cmd)s [OPTIONS] + +Run a simple, standalone HTTP server configured to serve up ViewVC +requests. + +Options: + + --config-file=PATH (-c) Use the file at PATH as the ViewVC configuration + file. If not specified, ViewVC will try to use + the configuration file in its installation tree; + otherwise, built-in default values are used. + (Not valid in GUI mode.) + + --daemon (-d) Background the server process. + + --host=HOST (-h) Start the server listening on HOST. You need + to provide the hostname if you want to + access the standalone server from a remote + machine. [default: %(host)s] + + --port=PORT (-p) Start the server on the given PORT. + [default: %(port)d] + + --repository=PATH (-r) Serve up the Subversion or CVS repository located + at PATH. This option may be used more than once. + + --script-alias=PATH (-s) Specify the ScriptAlias, the artificial path + location that at which ViewVC appears to be + located. For example, if your ScriptAlias is + "cgi-bin/viewvc", then ViewVC will be accessible + at "http://%(host)s:%(port)s/cgi-bin/viewvc". + [default: %(script_alias)s] + + --gui (-g) Pop up a graphical interface for serving and + testing ViewVC. NOTE: this requires a valid + X11 display connection. +""" % locals()) + +if __name__ == '__main__': + options = Options() + cli(sys.argv) diff --git a/bin/svndbadmin b/bin/svndbadmin new file mode 100755 index 00000000..bbf6a381 --- /dev/null +++ b/bin/svndbadmin @@ -0,0 +1,360 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 2004-2007 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 +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- +# +# administrative program for loading Subversion revision information +# into the checkin database. It can be used to add a single revision +# to the database, or rebuild/update all revisions. +# +# To add all the checkins from a Subversion repository to the checkin +# database, run the following: +# /path/to/svndbadmin rebuild /path/to/repo +# +# This script can also be called from the Subversion post-commit hook, +# something like this: +# REPOS="$1" +# REV="$2" +# /path/to/svndbadmin update "$REPOS" "$REV" +# +# 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. +# +# ----------------------------------------------------------------------- +# + +######################################################################### +# +# INSTALL-TIME CONFIGURATION +# +# These values will be set during the installation process. During +# development, they will remain None. +# + +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) +else: + sys.path.insert(0, os.path.abspath(os.path.join(sys.argv[0], "../../lib"))) + +######################################################################### + +import os +import string +import re + +import svn.core +import svn.repos +import svn.fs +import svn.delta + +import cvsdb +import viewvc +import vclib + +class SvnRepo: + """Class used to manage a connection to a SVN repository.""" + def __init__(self, path): + self.path = path + self.repo = svn.repos.svn_repos_open(path) + self.fs = svn.repos.svn_repos_fs(self.repo) + self.rev_max = svn.fs.youngest_rev(self.fs) + 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) + return rev + +_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 + normal diff. The reasons for choosing Normal diff format are: + - the output is short, so should be quicker to parse. + - only the change commands need be parsed to calculate the counts. + - All file data is prefixed, so won't be mistaken for a change + command. + This code is based on the description of the format found in the + GNU diff manual.""" + + plus, minus = 0, 0 + line = diff_fp.readline() + while line: + match = re.match(_re_diff_change_command, line) + if match: + # size of first range + if match.group(2): + count1 = int(match.group(2)) - int(match.group(1)) + 1 + else: + count1 = 1 + cmd = match.group(3) + # size of second range + if match.group(5): + count2 = int(match.group(5)) - int(match.group(4)) + 1 + else: + count2 = 1 + + if cmd == 'a': + # LaR - insert after line L of file1 range R of file2 + plus = plus + count2 + elif cmd == 'c': + # FcT - replace range F of file1 with range T of file2 + minus = minus + count1 + plus = plus + count2 + elif cmd == 'd': + # 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 SvnRev: + """Class used to hold information about a particular revision of + the repository.""" + def __init__(self, repo, rev): + self.repo = repo + self.rev = rev + self.rev_roots = {} # cache of revision roots + + # 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, '')) + + # convert the date string to seconds since epoch ... + try: + self.date = svn.core.svn_time_from_cstring(self.date) / 1000000 + except: + self.date = None + + # get a root for the current revisions + fsroot = self._get_root_for_rev(rev) + + # find changes in the revision + editor = svn.repos.RevisionChangeCollector(repo.fs, rev) + e_ptr, e_baton = svn.delta.make_editor(editor) + svn.repos.svn_repos_replay(fsroot, e_ptr, e_baton) + + self.changes = [] + for path, change in editor.changes.items(): + # skip non-file changes + if change.item_kind != svn.core.svn_node_file: + continue + + # deal with the change types we handle + base_root = None + if change.base_path: + base_root = self._get_root_for_rev(change.base_rev) + + if not change.path: + action = 'remove' + elif change.added: + action = 'add' + else: + action = 'change' + + diffobj = svn.fs.FileDiff(base_root and base_root or None, + base_root and change.base_path or None, + change.path and fsroot or None, + change.path and change.path or None) + diff_fp = diffobj.get_pipe() + plus, minus = _get_diff_counts(diff_fp) + 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) + return self.rev_roots[rev] + + +def handle_revision(db, command, repo, rev, verbose, force=0): + """Adds a particular revision of the repository to the checkin database.""" + revision = repo[rev] + committed = 0 + + if verbose: print "Building commit info for revision %d..." % (rev), + + if not revision.changes: + if verbose: print "skipped (no changes)." + return + + for (path, action, plus, minus) in revision.changes: + directory, file = os.path.split(path) + commit = cvsdb.CreateCommit() + commit.SetRepository(repo.path) + commit.SetDirectory(directory) + commit.SetFile(file) + commit.SetRevision(str(rev)) + commit.SetAuthor(revision.author) + commit.SetDescription(revision.log) + commit.SetTime(revision.date) + commit.SetPlusCount(plus) + commit.SetMinusCount(minus) + commit.SetBranch(None) + + if action == 'add': + commit.SetTypeAdd() + elif action == 'remove': + commit.SetTypeRemove() + elif action == 'change': + commit.SetTypeChange() + + if command == 'update': + result = db.CheckCommit(commit) + if result and not force: + continue # already recorded + + # commit to database + db.AddCommit(commit) + committed = 1 + + if verbose: + if committed: + print "done." + else: + print "skipped (already recorded)." + +def main(command, repository, revs=[], verbose=0, force=0): + cfg = viewvc.load_config(CONF_PATHNAME) + db = cvsdb.ConnectDatabase(cfg) + + if command in ('rebuild', 'purge'): + if verbose: + print "Purging commit info for repository root `%s'" % repository + db.PurgeRepository(repository) + + repo = SvnRepo(repository) + if command == 'rebuild' or (command == 'update' and not revs): + for rev in range(repo.rev_max+1): + handle_revision(db, command, repo, rev, verbose) + 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 + else: + r = int(r) + if r < 0: + raise ValueError, "invalid revision '%d'" % (r) + return r + +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. + +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 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. + +Use the -v flag to cause this script to give progress information as it works. + +""" % (cmd, cmd, cmd)) + sys.exit(1) + +if __name__ == '__main__': + verbose = 0 + force = 0 + args = sys.argv + try: + index = args.index('-v') + verbose = 1 + 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 = string.lower(args[1]) + if command not in ('rebuild', 'update', 'purge'): + sys.stderr.write('ERROR: unknown command %s\n' % command) + usage() + + repository = args[2] + if not os.path.exists(repository): + sys.stderr.write('ERROR: could not find repository %s\n' % args[2]) + usage() + repository = vclib.svn.canonicalize_rootpath(repository) + + revs = [] + if len(sys.argv) > 3: + if command == 'rebuild': + sys.stderr.write('ERROR: rebuild no longer accepts a revision ' + 'number argument. Usage update --force.') + usage() + elif command != 'update': + usage() + 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]) + except ValueError: + sys.stderr.write('ERROR: invalid revision specification "%s"\n' \ + % sys.argv[3]) + usage() + else: + rev = None + + try: + repository = cvsdb.CleanRepository(os.path.abspath(repository)) + main(command, repository, revs, verbose, force) + except KeyboardInterrupt: + print + print '** break **' + sys.exit(0) diff --git a/cvsgraph.conf.dist b/cvsgraph.conf.dist new file mode 100644 index 00000000..dc970788 --- /dev/null +++ b/cvsgraph.conf.dist @@ -0,0 +1,394 @@ +# CvsGraph configuration +# +# - Empty lines and whitespace are ignored. +# +# - Comments start with '#' and everything until +# end of line is ignored. +# +# - Strings are C-style strings in which characters +# may be escaped with '\' and written in octal +# and hex escapes. Note that '\' must be escaped +# if it is to be entered as a character. +# +# - Some strings are expanded with printf like +# conversions which start with '%'. Not all +# are applicable at all times, in which case they +# will expand to nothing. +# %c = cvsroot (with trailing '/') +# %C = cvsroot (*without* trailing '/') +# %m = module (with trailing '/') +# %M = module (*without* trailing '/') +# %f = filename without path +# %F = filename without path and with ",v" stripped +# %p = path part of filename (with trailing '/') +# %r = number of revisions +# %b = number of branches +# %% = '%' +# %R = the revision number (e.g. '1.2.4.4') +# %P = previous revision number +# %B = the branch number (e.g. '1.2.4') +# %d = date of revision +# %a = author of revision +# %s = state of revision +# %t = current tag of branch or revision +# %0..%9 = command-line argument -0 .. -9 +# %l = HTMLized log entry of the revision +# NOTE: %l is obsolete. See %(%) and cvsgraph.conf(5) for +# more details. +# %L = log entry of revision +# The log entry expansion takes an optional argument to +# specify maximum length of the expansion like %L[25]. +# %(...%) = HTMLize the string within the parenthesis. +# ViewVC currently uses the following four command-line arguments to +# pass URL information to cvsgraph: +# -3 link to current file's log page +# -4 link to current file's checkout page minus "rev" parameter +# -5 link to current file's diff page minus "r1" and "r2" parameters +# -6 link to current directory page minus "pathrev" parameter +# +# - Numbers may be entered as octal, decimal or +# hex as in 0117, 79 and 0x4f respectively. +# +# - Fonts are numbered 0..4 (defined as in libgd) +# 0 = tiny +# 1 = small +# 2 = medium (bold) +# 3 = large +# 4 = giant +# +# - Colors are a string like HTML type colors in +# the form "#rrggbb" with parts written in hex +# rr = red (00..ff) +# gg = green (00-ff) +# bb = blue (00-ff) +# +# - There are several reserved words besides of the +# feature-keywords. These additional reserved words +# expand to numerical values: +# * false = 0 +# * true = 1 +# * not = -1 +# * left = 0 +# * center = 1 +# * right = 2 +# * gif = 0 +# * png = 1 +# * jpeg = 2 +# * tiny = 0 +# * small = 1 +# * medium = 2 +# * large = 3 +# * giant = 4 +# +# - Booleans have three possible arguments: true, false +# and not. `Not' means inverse of what it was (logical +# negation) and is represented by the value -1. +# For the configuration file that means that the default +# value is negated. +# + +# cvsroot +# The *absolute* base directory where the +# CVS/RCS repository can be found +# cvsmodule +# +cvsroot = "--unused--"; # unused with ViewVC, will be overridden +cvsmodule = ""; # unused with ViewVC -- please leave it blank + +# color_bg +# The background color of the image +# transparent_bg +# Make color_bg the transparent color (only useful with PNG) +color_bg = "#ffffff"; +transparent_bg = false; + +# date_format +# The strftime(3) format string for date and time +date_format = "%d-%b-%Y %H:%M:%S"; + +# box_shadow +# Add a shadow around the boxes +# upside_down +# Reverse the order of the revisions +# left_right +# Draw the image left to right instead of top down, +# or right to left is upside_down is set simultaneously. +# strip_untagged +# Remove all untagged revisions except the first, last and tagged ones +# strip_first_rev +# Also remove the first revision if untagged +# auto_stretch +# Try to reformat the tree to minimize image size +# use_ttf +# Use TrueType fonts for text +# anti_alias +# Enable pretty TrueType anti-alias drawing +# thick_lines +# Draw all connector lines thicker (range: 1..11) +box_shadow = true; +upside_down = false; +left_right = false; +strip_untagged = false; +strip_first_rev = false; +#auto_stretch = true; # not yet stable. +use_ttf = false; +anti_alias = true; +thick_lines = 1; + +# msg_color +# Sets the error/warning message color +# msg_font +# msg_ttfont +# msg_ttsize +# Sets the error/warning message font +msg_color = "#800000"; +msg_font = medium; +msg_ttfont = "/dos/windows/fonts/ariali.ttf"; +msg_ttsize = 11.0; + +# parse_logs +# Enable the parsing of the *entire* ,v file to read the +# log-entries between revisions. This is necessary for +# the %L expansion to work, but slows down parsing by +# a very large factor. You're warned. +parse_logs = false; + +# tag_font +# The font of the tag text +# tag_color +# The color of the tag text +# tag_ignore +# A extended regular expression to exclude certain tags from view. +# See regex(7) for details on the format. +# Note 1: tags matched in merge_from/merge_to are always displayed unless +# tag_ignore_merge is set to true. +# Note 2: normal string rules apply and special characters must be +# escaped. +# tag_ignore_merge +# If set to true, allows tag_ignore to also hide merge_from and merge_to +# tags. +# tag_nocase +# Ignore the case is tag_ignore expressions +# tag_negate +# Negate the matching criteria of tag_ignore. When true, only matching +# tags will be shown. +# Note: tags matched with merge_from/merge_to will still be displayed. +tag_font = medium; +#tag_ttfont = "/dos/windows/fonts/ariali.ttf"; +#tag_ttsize = 11.0; +tag_color = "#007000"; +#tag_ignore = "(test|alpha)_release"; +#tag_ignore_merge = false; +#tag_nocase = false; +#tag_negate = false; + +# rev_hidenumber +# If set to true no revision numbers will be printed in the graph. +#rev_hidenumber = false; +rev_font = giant; +#rev_ttfont = "/dos/windows/fonts/arial.ttf"; +#rev_ttsize = 12.0; +rev_color = "#000000"; +rev_bgcolor = "#f0f0f0"; +rev_separator = 1; +rev_minline = 15; +rev_maxline = 75; +rev_lspace = 5; +rev_rspace = 5; +rev_tspace = 3; +rev_bspace = 3; +rev_text = "%d"; # or "%d\n%a, %s" for author and state too +rev_text_font = tiny; +#rev_text_ttfont = "/dos/windows/fonts/times.ttf"; +#rev_text_ttsize = 9.0; +rev_text_color = "#500020"; +rev_maxtags = 25; + +# merge_color +# The color of the line connecting merges +# merge_front +# If true, draw the merge-lines on top if the image +# merge_nocase +# Ignore case in regular expressions +# merge_from +# A regex describing a tag that is used as the merge source +# merge_to +# A regex describing a tag that is the target of the merge +# merge_findall +# Try to match all merge_to targets possible. This can result in +# multiple lines originating from one tag. +# merge_arrows +# Use arrows to point to the merge destination. Default is true. +# merge_cvsnt +# Use CVSNT's mergepoint registration for merges +# merge_cvsnt_color +# The color of the line connecting merges from/to registered +# mergepoints. +# arrow_width +# arrow_length +# Specify the size of the arrows. Default is 3 wide and 12 long. +# +# NOTE: +# - The merge_from is an extended regular expression as described in +# regex(7) and POSIX 1003.2 (see also Single Unix Specification at +# http://www.opengroup.com). +# - The merge_to is an extended regular expression with a twist. All +# subexpressions from the merge_from are expanded into merge_to +# using %[1-9] (in contrast to \[1-9] for backreferences). Care is +# taken to escape the constructed expression. +# - A '$' at the end of the merge_to expression can be important to +# prevent 'near match' references. Normally, you want the destination +# to be a good representation of the source. However, this depends +# on how well you defined the tags in the first place. +# +# Example: +# merge_from = "^f_(.*)"; +# merge_to = "^t_%1$"; +# tags: f_foo, f_bar, f_foobar, t_foo, t_bar +# result: +# f_foo -> "^t_foo$" -> t_foo +# f_bar -> "^t_bar$" -> t_bar +# f_foobar-> "^t_foobar$" -> +# +merge_color = "#a000a0"; +merge_front = false; +merge_nocase = false; +merge_from = "^f_(.*)"; +merge_to = "^t_%1$"; +merge_findall = false; + +#merge_arrows = true; +#arrow_width = 3; +#arrow_length = 12; + +merge_cvsnt = true; +merge_cvsnt_color = "#606000"; + +# branch_font +# The font of the number and tags +# branch_color +# All branch element's color +# branch_[lrtb]space +# Interior spacing (margin) +# branch_margin +# Exterior spacing +# branch_connect +# Length of the vertical connector +# branch_dupbox +# Add the branch-tag also at the bottom/top of the trunk +# branch_fold +# Fold empty branches in one box to save space +# branch_foldall +# Put all empty branches in one box, even if they +# were interspaced with branches with revisions. +# branch_resort +# Resort the branches by the number of revisions to save space +# branch_subtree +# Only show the branch denoted or all branches that sprout +# from the denoted revision. The argument may be a symbolic +# tag. This option you would normally want to set from the +# command line with the -O option. +branch_font = medium; +#branch_ttfont = "/dos/windows/fonts/arialbd.ttf"; +#branch_ttsize = 18.0; +branch_tag_color= "#000080"; +branch_tag_font = medium; +#branch_tag_ttfont = "/dos/windows/fonts/arialbi.ttf"; +#branch_tag_ttsize = 14.0; +branch_color = "#0000c0"; +branch_bgcolor = "#ffffc0"; +branch_lspace = 5; +branch_rspace = 5; +branch_tspace = 3; +branch_bspace = 3; +branch_margin = 15; +branch_connect = 8; +branch_dupbox = false; +branch_fold = true; +branch_foldall = false; +branch_resort = false; +#branch_subtree = "1.2.4"; + +# title +# The title string is expanded (see above for details) +# title_[xy] +# Position of title +# title_font +# The font +# title_align +# 0 = left +# 1 = center +# 2 = right +# title_color +title = "%c%p%f\nRevisions: %r, Branches: %b"; +title_x = 10; +title_y = 5; +title_font = small; +#title_ttfont = "/dos/windows/fonts/times.ttf"; +#title_ttsize = 10.0; +title_align = left; +title_color = "#800000"; + +# Margins of the image +# Note: the title is outside the margin +margin_top = 35; +margin_bottom = 10; +margin_left = 10; +margin_right = 10; + +# Image format(s) +# image_type +# gif (0) = Create gif image +# png (1) = Create png image +# jpeg (2) = Create jpeg image +# Image types are available if they can be found in +# the gd library. Newer versions of gd do not have +# gif anymore. CvsGraph will automatically generate +# png images instead. +# image_quality +# The quality of a jpeg image (1..100) +# image_compress +# Set the compression of a PNG image (gd version >= 2.0.12). +# Values range from -1 to 9 where: +# - -1 default compression (usually 3) +# - 0 no compression +# - 1 lowest level compression +# - ... ... +# - 9 highest level of compression +# image_interlace +# Write interlaces PNG/JPEG images for progressive loading. +image_type = png; +image_quality = 75; +image_compress = 3; +image_interlace = true; + +# HTML image map generation +# map_name +# The name= attribute in ... +# map_branch_href +# map_branch_alt +# map_rev_href +# map_rev_alt +# map_diff_href +# map_diff_alt +# map_merge_href +# map_merge_alt +# These are the href= and alt= attributes in the +# tags of HTML. The strings are expanded (see above). +map_name = "MyMapName\" name=\"MyMapName"; +map_branch_href = "href=\"%6pathrev=%(%t%)\""; +map_branch_alt = "alt=\"%0 %(%t%) (%B)\""; +# You might want to experiment with the following setting: +# 1. The default setting will take you to a ViewVC generated page displaying +# that revision of the file, if you click into a revision box: +map_rev_href = "href=\"%4rev=%R\""; +# 2. This alternative setting will take you to the anchor representing this +# revision on a ViewVC generated Log page for that file: +# map_rev_href = "href=\"%3#rev%R\""; +# +map_rev_alt = "alt=\"%1 %(%t%) (%R)\""; +map_diff_href = "href=\"%5r1=%P&r2=%R\""; +map_diff_alt = "alt=\"%2 %P <-> %R\""; +map_merge_href = "href=\"%5r1=%P&r2=%R\""; +map_merge_alt = "alt=\"%2 %P <-> %R\""; + diff --git a/docs/template-authoring-guide.html b/docs/template-authoring-guide.html new file mode 100644 index 00000000..efdcf247 --- /dev/null +++ b/docs/template-authoring-guide.html @@ -0,0 +1,2144 @@ + + +ViewVC 1.0 Template Authoring Guide + + + + +

ViewVC 1.0 Template Authoring Guide

+ +
+

Introduction

+ +

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

+ +
+ + + +
+

Using EZT

+ +

### TODO ###

+ +
+ +
+

Variables Available to ViewVC Templates

+ +
+

Common Template Variable Set (COMMON)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
annotate_hrefStringURL of the ViewVC annotation view for the current resource. + Valid only when pathtype is file.
cfgObjectRepresentation of the object used by ViewVC for runtime + configuration parameters such as those parsed from + viewvc.conf. Dot-qualified children of this object + map to configuration sections and option keys. For example, + cfg.options.show_logs contains the value of the + show_logs variable in the options section + of the configuration file.
docrootStringURL of the static documents directory, generally used for + referencing stylesheets and images stored in and under that + directory (which is typically relative to the template + location).
download_hrefStringViewVC file contents download URL for the current resource. + Valid only when pathtype is file.
download_text_hrefStringViewVC file contents as-text download URL for the current resource. + Valid only when pathtype is file.
graph_hrefStringURL of the ViewVC revision graph view for the current resource. + Valid only when pathtype is file.
kvObjectRepresentation of the object used by ViewVC for user-defined + key/value mappings. Dot-qualified children of this object map + to named key/value files and the sections and keys within + them. For example, kv.l10n.labels.directory + maps to the value of the directory key under the + labels section of the key/value file whose configured + abstract name is l10n.
lockinfoStringInformation about the lock status of the current resource.
log_hrefStringURL of the ViewVC revision log view for the current + resource. Valid only when pathtype is file + or (for Subversion roots) dir.
log_rev_hrefStringRevision number of the file-revision currently being viewed, or + None.
nav_pathListOrdered list of path components from the repository root to the + current resource.
nav_path.hrefStringURL of the default ViewVC view for the path component.
nav_path.nameStringName of the path component.
pathtypeStringPath kind of the current resource. Valid values: file + (file), dir (directory); may be empty.
prefer_markupBooleanIndicates whether to make the default file link a link to the markup + page instead of the checkout page. Valid only when + pathtype is file.
queryform_hrefStringURL for a query form returning results from this directory. + Valid only when pathtype is dir.
revStringRevision of the current resource.
revision_hrefStringURL of the Subversion revision view for the current revision.
rootnameStringName of the current repository (root).
roots_hrefStringURL of ViewVC root listing view. Valid only when ViewVC is + configured in roots-as-url-components mode.
rootpathStringServer-local location of the current repository. WARNING: Revealing + information to untrusted guests about the details of your server + configuration can have negative security implications. Use this + token at your own risk.
roottypeStringVersion control type of the current repository (root). + Valid values: cvs (CVS), svn (Subversion).
rss_hrefStringURL of RSS feed for current location.
tarball_hrefStringURL to download tarball of the current directory.
up_hrefStringLink to the current object's parent directory view.
usernameStringAuthenticated username of the requesting user.
viewStringName of the current view. Valid values: annotate + (annotation view), diff (file difference view), + roots (root listing view), dir (directory + listing view), graph (revision graph view), log + (revision log view), markup (file contents view), + query (revision history query results view), + queryform (revision history query form view), + rev (revision/changeset view).
view_hrefStringURL of the ViewVC file contents view for the current resource. + Valid only when pathtype is file or + dir.
vsnStringViewVC version identifier.
whereStringPath (relative to the current repository root) of the current + resource.
+
+ +
+

Path Revision Form Variable Set (PATHREV)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
lastrevStringIf the current path is deleted in a future revision, last + revision where the path is available. (Subversion only)
pathrevStringCurrent sticky revision (Subversion) or sticky tag (CVS)
pathrev_actionStringForm action URL for the sticky revision/tag selection form.
pathrev_hidden_valuesListHidden value name/value pairs for the revision/tag selection form.
pathrev_clear_actionStringForm action URL for the path revision clear button.
pathrev_clear_hidden_valuesListHidden value name/value pairs for the path revision clear button.
+
+ +
+

Paging Form Variable Set (PAGING)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
picklistListList of pages that make up the current directory or log view.
picklist.countStringNumber of the first item on the page (indexed from 0)
picklist.endStringName of last item on the page
picklist.moreBooleanIf set, indicates that this picklist item is a placeholder for + an unspecified number of additional pages. In this case, + picklist.end is undefined.
picklist.pageStringPage number (indexed from 1)
picklist.startStringName of first item on the page
picklist_lenStringNumber of pages in picklist
+
+ +
+

Property Listing Variable Set (PROPERTIES)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
propertiesListList of item properties set on the current directory, minus + those with undisplayable names.
properties.nameStringName of an item property.
properties.undisplayableBooleanIndicates whether or not the value of this property is + undisplayable (by virtue of not being UTF-8 text).
properties.valueStringValue of this property.
+
+ +
+

File Contents View (file.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON and + PROPERTIES variable sets
agoStringText description of the time elapsed since date.
annotationStringIf set, indicates that annotations were requested. Valid values + are "annotated" (annotation was successful), "binary" (file contents + are not line-based and human-readable), and "error" (something went + wrong during annotation).
authorStringAuthor of the revision being viewed.
branch_pointsStringList of branch tag names which branch off of the revision being + viewed (CVS only).
branchesListIf revision currently being viewed is on a branch, list of names + for the branch.
changedStringNumbers of lines added and removed since the previous revision.
dateStringDate (in UTC if not otherwise configured) of the revision currently + being viewed.
image_src_hrefStringURL used to display the current revision of the file as an + embedded image. (Set only if the file is not a web-viewable + image.)
linesListSet of objects containing information about the most recent + modification of a single line of file content in the current + resource, naturally ordered by the line numbers they represent. + Every line in the resource is represented in the set.
lines.authorStringUsername of the most recent modifier of the line.
lines.dateStringDate (in UTC if not otherwise configured) of the modification of + the line.
lines.diff_hrefStringURL of the ViewVC file difference view which displays the + modification of the line.
lines.line_numberStringLine number (1-based) of the line.
lines.prev_revStringYoungest revision of the resource prior to the line's + modification.
lines.revStringRevision in which the modification of the line occured.
lines.textStringTextual contents of the line.
logStringLog message of the revision currently being viewed.
mime_typeStringMIME type of the current file.
orig_pathStringWhen viewing an old file revision through a copy of the file, + this is the old file revision's original path.
orig_hrefStringURL of a ViewVC log view for orig_path.
prevStringPrevious revision number.
sizeStringSize of the file revision, in bytes. Subversion only.
stateStringState of the file revision. Possible values: dead, and + the empty string.
tagsListNames of tags that have been applied to the current file + revision.
vendor_branchBooleanIndicates whether or not the current file revision is on a vendor + branch.
+
+ +
+

Revision Graph View (graph.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON variable set
imagemapStringHTML markup containing the image map associated with the + revision graph.
imagesrcStringURL of the ViewVC revision graph image for the current + resource.
+
+ +
+

File Difference View (diff.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON variable set
changesListSet of objects which contain information about a single line of + file difference data. Valid only when diff_format is + h or l.
changes.have_leftBooleanSpecifies whether the left file has a line of content relevant + to the difference data line. Valid only when + changes.type is change.
changes.have_rightBooleanSpecifies whether the right file has a line of content relevant + to the difference data line. Valid only when + changes.type is change.
changes.leftStringTextual contents of the relevant line in the left file. Valid + only when changes.type is change, + context, or remove. When + changes.type is change, valid only when + changes.have_left is set (in order to delineate + between missing lines and empty lines, which EZT does not + support).
changes.rightStringTextual contents of the relevant line in the right file. Valid + only when changes.type is add, change, + or context. When + changes.type is change, valid only when + changes.have_left is set (in order to delineate + between missing lines and empty lines, which EZT does not + support).
changes.line_info_extraStringAdditional line information for the current difference hunk. + Valid only when changes.type is header.
changes.line_info_leftStringFirst line number represented by the current hunk in the left + file. Valid only when changes.type is header.
changes.line_info_rightStringFirst line number represented by the current hunk in the right + file. Valid only when changes.type is header.
changes.line_numberStringLine number (1-based) of the line.
changes.typeStringThe type of change. Value values: add, + change, context, header, + no-changes, remove.
diff_formatStringDifference dislay format: Valid values are c + (context), f (full human-readable), + h (human-readable, or colored), l (long + human-readable), s (side-by-side), u + (unified).
diff_format_actionStringForm action URL for the diff format selection form.
diff_format_hidden_valuesListHidden value name/value pairs for the diff format selection form.
leftContainerContainer object for grouping information about the left file.
left.annotate_hrefStringURL of the ViewVC annotation view for the left file. + Valid only when entries.pathtype is file.
left.dateStringDate (in UTC if not otherwise configured) in which the left file + revision was created.
left.download_hrefStringURL to download the HEAD revision of the left file.
left.download_text_hrefStringURL to download the HEAD revision of the left file as + text/plain.
left.pathStringPath of the left file.
left.prefer_markupBooleanIndicates whether to make the default file link a link to the markup + page instead of the checkout page.
left.revStringRevision of the left file.
left.revision_hrefStringURL of the Subversion revision view for the left file's + current revision. Valid only when roottype is + svn.
left.tagStringTag of the left file.
left.view_hrefStringThis is a URL for the markup view of the left file.
raw_diffStringRaw difference text. Valid only when diff_format is + c, s, or u.
rightContainerContainer object for grouping information about the right file.
right.annotate_hrefStringURL of the ViewVC annotation view for the right file. + Valid only when entries.pathtype is file.
right.dateStringDate (in UTC if not otherwise configured) in which the right file + revision was created.
right.download_hrefStringURL to download the HEAD revision of the right file.
right.download_text_hrefStringURL to download the HEAD revision of the right file as + text/plain.
right.pathStringPath of the right file.
right.prefer_markupBooleanIndicates whether to make the default file link a link to the markup + page instead of the checkout page.
right.revStringRevision of the right file.
right.revision_hrefStringURL of the Subversion revision view for the right file's + current revision. Valid only when roottype is + svn.
right.tagStringTag of the right file.
right.view_hrefStringThis is a URL for the markup view of the right file.
+
+ +
+

Directory Listing View (directory.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON, + PATHREV, + PAGING, and + PROPERTIES variable sets
attic_showingBooleanIndicates whether or not the directory list include "dead" files + (files not available in, perhaps deleted from, the current + tag). CVS only.
branch_tagsListSet of branch tag names in use by files in the current directory. + CVS only.
dir_pagestartStringItem number (zero-based) of the first directory entry requested + to be shown on the page. Corresponds to the + dir_pagestart CGI parameter.
dir_paging_actionStringForm action URL for the page selection form.
dir_paging_hidden_valuesListHidden value name/value pairs for the page selection form.
entriesListSet of objects which represent the entries of this directory.
entries.agoStringTextual description of the time since entries.date.
entries.annotate_hrefStringURL of the ViewVC annotation view for the directory entry. + Valid only when entries.pathtype is file.
entries.authorStringUsername of the last modifier of the directory entry.
entries.dateStringDate (in UTC if not otherwise configured) of the last + modification of the directory entry.
entries.download_hrefStringURL to download the HEAD revision of the directory entry.
entries.download_text_hrefStringURL to download the HEAD revision of the directory entry as + text/plain.
entries.errorsListList of strings containing error messages encountered by the + version control backend as it attempted to harvest information about + this directory entry. At this time the strings are somewhat + freeform; in the future it would be nice to expose these as + testable error code or somesuch.
entries.graph_hrefStringURL of the ViewVC revision graph view for the directory + entry.
entries.lockinfoStringInformation about the lock status of the directory entry.
entries.logStringLog message of last modification to the directory entry.
entries.log_fileStringViewVC optionally calculates the log message of a CVS directory + as the log message associated with the most recently modified + file in that directory. When that occurs, this is the name of + that file. Valid only when entries.pathtype is + dir. See also entries.log_rev.
entries.log_hrefStringURL of the ViewVC revision log view for the directory + entry.
entries.log_revStringViewVC optionally calculates the log message of a CVS directory + as the log message associated with the most recently modified + file in that directory. When that occurs, this is the revision of + that file. Valid only when entries.pathtype is + dir. See also entries.log_file.
entries.mime_typeStringMIME type of the directory entry.
entries.nameStringName of the directory entry.
entries.pathtypeStringPath kind of the directory entry. Valid values: file + (file), dir (directory); may be empty.
entries.prefer_markupBooleanIndicates whether to make the default file link a link to the markup + page instead of the checkout page. Valid only when + entries.pathtype is file.
entries.revStringRevision of the directory entry. For CVS repositories, this is + a revision at the tip of the selected tag or branch; for + Subversion, this is the youngest revision as of the revision of + the directory being viewed.
entries.revision_hrefStringURL of the Subversion revision view for the directory entry's + current revision. Valid only when roottype is + svn.
entries.short_logStringLog message of last modification to the directory entry, + truncated to contain no more than the number of characters + specified by the short_log_len configuration option.
entries.sizeStringSize (in bytes) of the directory entry. Valid only when + roottype is svn and + entries.pathtype is file.
entries.stateStringState of the directory entry. If the state is uninteresting + (a typical, versioned object), this field is empty. Valid, + non-empty states include: dead (the object is not + available on, or possible removed from, this branch; CVS only).
entries.view_hrefStringThis is a URL for the markup view if the entry is a file, and a + URL for a directory listing if the entry is a directory.
files_shownStringNumber of files displayed.
hide_attic_hrefStringURL for the current view, but with "dead" files hidden. + CVS only.
num_deadStringNumber of dead files in the current directory.
plain_tagsListList of tag names in use by files in the current directory. + CVS only.
search_reStringCurrent search expression, if any.
search_re_formBooleanIndicates whether or not to display the regular expression search + form. Value depends on the whether searching is enabled in the + configuration and whether or not the current directory is + empty.
search_re_actionStringForm action URL for the regular expression search form.
search_re_hidden_valuesListHidden value name/value pairs for the regular expression search form.
show_attic_hrefStringURL for the current view, but with "dead" files shown. + CVS only.
sortbyStringCurrent sorting mode. Valid values: file, rev, + date, author, and log.
sortby_author_hrefStringURL for the current view, but sorted by author.
sortby_date_hrefStringURL for the current view, but sorted by date.
sortby_file_hrefStringURL for the current view, but sorted by filename.
sortby_log_hrefStringURL for the current view, but sorted by log message.
sortby_rev_hrefStringURL for the current view, but sorted by revision number.
sortdirStringCurrent sorting mode. Valid values: up (ascending) and + down (descending)
tree_revStringLast revision number where the current directory (or any path + underneath it) was modified. Subversion only.
tree_rev_hrefStringURL for revision view showing information about the + tree_rev revision. Subversion only.
youngest_revStringLast revision number in the repository. Subversion only
youngest_rev_hrefStringURL for revision view showing information about the + youngest_rev revision. Subversion only.
+
+ +
+

Error View (error.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
msgStringMessage describing the current error.
stacktraceStringPython stack trace showing where the error occurred.
statusStringHTTP status code like "404 Not Found" that was sent to the + browser with this error message.
+
+ +
+

Revision Log View (log.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON, + PATHREV, and + PAGING variable sets
branch_tagsListNames of branch tags in the file. CVS only.
default_branchListDefault branch names (CVS only)
diff_formatStringCurrently selected diff format in the diff selection form. Valid + values are c (context), h (human-readable, + or colored), l (long human-readable), s + (side-by-side), u (unified).
diff_select_actionStringForm action URL for the diff selection form.
diff_select_hidden_valuesListHidden value name/value pairs for the diff selection form.
entriesListList of revisions where the file or directory was modified.
entries.agoStringText description of the time elapsed since + entries.date.
entries.annotate_hrefStringURL for the annotate view of the file revision.
entries.authorStringAuthor of the revision.
entries.branch_namesListIf this last revision on a branch, a list of names for that + branch.
entries.branch_pointStringIf the revision is on a branch, this is the revision number + the branch branched off from. CVS only.
entries.branch_pointsStringList of branch tags which branch off of this revision. + CVS only.
entries.branch_points.nameStringName of the branch tag.
entries.branch_points.hrefStringURL for the current view, but with this tag set as the sticky + tag.
entries.branchesStringList of branch tags that include this file revision. + CVS only.
entries.branches.nameStringName of the branch tag.
entries.branches.hrefStringURL for the current view, but with this tag set as the sticky + tag.
entries.changedStringNumbers of lines added and removed since the previous revision. + CVS only.
entries.copy_hrefStringURL for log view of entries.copy_path.
entries.copy_pathStringIf the file revision was copied from somewhere, this is the path it + was copied from. Subversion only.
entries.copy_revStringIf the file revision was copied from somewhere, this is the revision + number of the path it was copied from. Subversion only.
entries.dateStringDate (in UTC if not otherwise configured) of the revision.
entries.diff_to_branch_hrefStringURL for a diff view of this file revision showing the changes + since the branch was created at (entries.branch_point). + CVS only.
entries.diff_to_main_hrefStringIf this revision is at the tip of a branch, URL for a diff view of + this file revision showing the differences between it and the latest + revision on the parent branch + (entries.next_main). CVS only.
entries.diff_to_prev_hrefStringURL for a diff view of this file revision showing the changes + since the previous revision (entries.prev).
entries.diff_to_sel_hrefStringURL for a diff view of this file revision and the currently + selected revision (rev_selected).
entries.download_hrefStringURL to download the file revision.
entries.download_text_hrefStringURL to download the file revision as text/plain.
entries.lockinfoStringInformation about the lock status of this revision.
entries.logStringRevision log message.
entries.next_mainStringIf this revision is on the tip of the branch, this is the latest + revision of the parent branch, a likely merge candidate.
entries.orig_hrefStringURL for log view of entries.orig_path
entries.orig_pathStringIf this file revision is located at a different path than the + newest file revision (because it precedes a copy or move), this + is the path it was originally located at. Subversion only.
entries.prefer_markupBooleanIndicates whether to make the default file link a link to the markup + page instead of the checkout page.
entries.prevStringPrevious revision number.
entries.revStringRevision number.
entries.revision_hrefStringURL for revision view showing more information about the + revision.
entries.sel_for_diff_hrefStringURL for current view, but with this revision selected for + diffs.
entries.sizeStringSize of the file revision, in bytes. Subversion only.
entries.stateStringState of the file revision. Possible values: dead, and + the empty string.
entries.tag_namesListList of tag names which refer to the revision.
entries.tagsListList of tags which refer to the revision
entries.tags.nameStringName of the tag.
entries.tags.hrefStringURL for the current page, but with this tag set as the sticky + tag.
entries.vendor_branchBooleanIndicates if this revision is on a vendor branch.
entries.view_hrefStringURL for markup view for a file revision, or directory listing view + for a directory revision.
head_annotate_hrefStringURL for annotate view of the HEAD revision of the file.
head_download_hrefStringURL to download the HEAD revision of the file.
head_download_text_hrefStringURL to download the HEAD revision of the file as + text/plain.
head_prefer_markupBooleanIndicates whether to make the default HEAD file link a link to the markup + page instead of the checkout page.
head_view_hrefStringURL for markup view of the HEAD revision of the file.
human_readableBooleanIndicates whether or not currently selected diff format + (diff_format) is human readable (colored).
log_pagestartStringItem number (zero based) of the first directory entry requested + to be shown on the page. Corresponds to the + log_pagestart query parameter.
log_paging_actionStringForm action URL for the page selection form.
log_paging_hidden_valuesListHidden value name/value pairs for the page selection form.
logsortStringCurrent sorting mode. Possible values: date and + rev.
logsort_actionStringForm action URL for log sort drop down box.
logsort_hidden_valuesListHidden value name/value pairs for the log sort drop down box
mime_typeStringMIME type of current file.
plain_tagsListNames of non-branch in the file. CVS only.
rev_selectedStringRevision number currently selected for diffs.
tag_annotate_hrefStringURL for annotate view of the file at currently selected sticky + tag.
tag_download_hrefStringURL to download the file at currently selected sticky tag. + CVS only.
tag_download_text_hrefStringURL to download the file as text/plain at the currently + selected sticky tag. CVS only
tag_prefer_markupBooleanIndicates whether to make the default sticky tag file link a + link to the markup page instead of the checkout page.
tag_view_hrefStringURL for markup view of the file at the currently selected sticky + tag. CVS only.
tagsStringList of tags that in the current file. CVS only.
tags.revStringRevision number for a non-branch tag, or the latest revision + number on the branch for a branch tag.
tags.nameStringTag name
+
+ +
+

Revision History Query Results View +(query_results.ezt, rss.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON variable set
backout_hrefStringURL for a page that shows a list of commands that can be applied + to a working copy to revert all the changes returned by the + query.
commitsListList of commits to files under the current directory that meet + the query criteria.
commits.authorStringAuthor of the commit.
commits.filesListList of files under the current directory affected by the + commit.
commits.files.authorStringAuthor of the commit.
commits.files.dateStringDate (in UTC if not otherwise configured) the change to the file + was committed.
commits.files.dirStringPath of the directory containing the file.
commits.files.dir_hrefStringURL for directory listing of commits.files.dir.
commits.files.fileStringFile name.
commits.files.revStringRevision number of the file.
commits.files.branchFileBranch the commit occurred on.
commits.files.diff_hrefStringURL to diff page showing changes since previous file revision.
commits.files.log_hrefStringURL for file's log page.
commits.files.minusStringNumber of lines removed from the file by the commit.
commits.files.plusStringNumber of lines added to the file by the commit.
commits.files.typeStringType of change made to the file by the commit. Possible values: + Change, Add, Remove
commits.logStringCommit log message.
commits.limited_filesBooleanTrue if files list was cut short due to limit_changes.
commits.minusStringTotal number of lines removed from files in this commit.
commits.num_filesStringTotal number of files in the commits.files list.
commits.plusStringNumber of lines added to files in this commit.
commits.revStringCommit revision number. Subversion only.
commits.rss_dateStringDate of the commit formatted for RSS.
commits.rss_urlStringAbsolute URL of the revision page for the commit. Subversion only.
commits.short_logStringTruncated commit log message.
english_queryStringText description of the current query criteria.
limit_changesStringCurrent limit_changes value, maximum number of changed files + to show per commit.
limit_changes_hrefStringURL for the current view but with limit_changes disabled.
minus_countStringTotal number of lines removed from all files across all returned + commits.
plus_countStringTotal number of lines added to all files across all returned + commits.
querysortStringIndicates how query results are being sorted. Possible values: + date, author, and file.
show_branchBooleanIndicates whether or not to list branch names in the results. True + when query results can include more than a single branch.
sqlStringSQL string used to query database. Included for debugging + purposes.
+
+ +
+

Revision History Query Form View (query_form.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON variable set
branchStringQuery string for filtering results by branch.
branch_matchStringType of match to perform with branch query string. + Valid values: exact, like, glob, + regex, notregex.
commentStringQuery string for filtering results by log message.
comment_matchStringType of match to perform with comment query string. Possible + values: exact, like, glob, + regex, notregex.
dateStringOption for filtering results by date. Possible values: + hours, day, week, month, + all, explicit.
dirStringQuery string for filtering results by subdirectory.
dir_hrefStringURL for directory list of current directory.
fileStringQuery string for filtering results by file name.
file_matchStringType of match to perform with file query string. + Valid values: exact, like, glob, + regex, notregex.
hoursStringIf date is hours, number of hours back to + include results from.
limit_changesStringCurrent limit_changes value, maximum number of changed files + to show per commit.
maxdateStringIf date is explicit, latest date to + include results from.
mindateStringIf date is explicit, earliest date to + include results from.
query_actionStringForm action URL for query form.
query_hidden_valuesListHidden value name/value pairs for query form.
querysortStringOption for sorting query results. Possible values: date, + author, and file.
whoStringQuery string for filtering results by author.
who_matchStringType of match to perform with who query string. Possible + values: exact, like, glob, + regex, notregex.
+
+ +
+

Revision/ChangeSet View (revision.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON variable set
agoStringText description of the time elapsed since date.
authorStringAuthor of the revision.
changesListList of paths changed in this revision.
changes.actionStringIndicates what happened to the path in this revision. Valid + values are: added, modified, replaced, + and deleted.
changes.copy_pathStringOriginal path if path was copied from somewhere in this + revision.
changes.copy_revStringRevision number of original path if path was copied from somewhere + in this revision
changes.diff_hrefStringURL for diff of changed path against previous revision.
changes.is_copyBooleanIndicates whether path was copied from another path in this + revision.
changes.log_hrefStringURL for log view of changed path.
changes.pathStringChanged path.
changes.pathtypeStringType of changed path. Valid values: file or + dir
changes.prop_modsBooleanIndicates whether the path's properties changed in this revision
changes.text_modsBooleanIndicates whether the path's file contents changed in this + revision.
changes.view_hrefStringURL for markup view of changed path.
dateStringRevision date (in UTC if not otherwise configured).
first_changesStringConfigured value for limit_changes.
first_changes_hrefStringURL for the current view but with limit_changes set to the + configured value.
jump_rev_actionStringForm action URL for revision jump form.
jump_rev_hidden_valuesListHidden value name/value pairs for revision jump form.
limit_changesStringCurrent limit_changes value, maximum number of changed files + to show per commit.
logStringRevision log message.
more_changesStringNumber of changes not being shown due to limit_changes.
more_changes_hrefStringURL for the current view but with limit_changes disabled.
next_hrefStringURL for revision page of the next revision.
prev_hrefStringURL for revision page of the previous revision.
revStringRevision number.
+
+ +
+

Root Listing View (roots.ezt)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableTypeDescription
Includes all variables from the + COMMON variable set
rootsListSet of configured viewable repositories.
roots.nameStringName of a configured repository.
roots.pathStringServer-local location of a configured repository. WARNING: Revealing + information to untrusted guests about the details of your server + configuration can have negative security implications. Use this + token at your own risk.
roots.typeStringVersion control type of a configured repository. Valid + values: cvs, svn.
roots.hrefStringURL of root directory view for a configured repository.
+ +
+ + diff --git a/docs/upgrading-howto.html b/docs/upgrading-howto.html new file mode 100644 index 00000000..0d81e1ca --- /dev/null +++ b/docs/upgrading-howto.html @@ -0,0 +1,1730 @@ + + + +ViewVC: Upgrading + + + + + +

Upgrading ViewVC

+ +
+

Introduction

+ +

This document describes some of the things that you will need to + consider, change, or handle when upgrading an existing ViewVC + or ViewCVS installation to a newer version.

+ +

Upgrading from an ancient version to the latest version + isn't necessarily a multi step process. The instructions are only + organized that way. You can certainly upgrade in a single step.

+ +

It is always recommended to install the new version in a fresh + directory and to carefully compare the configuration files. A + possible approach is to name the directories + /usr/local/viewvc-1.0, + /usr/local/viewvc-1.1 and so on and than create a + symbolic link viewvc pointing to the production + version. This way you can easily test several versions and switch + back if your users start to complain.

+ +
+ + + +
+

Upgrading From ViewVC 1.0

+ +

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

+ +
+

root_as_url_component Now Enabled by Default

+ +

In ViewVC 1.1, the root_as_url_component configuration + option is now enabled by default. This option causes ViewVC URLs + to be of the form + …/root-name/path-in-root[?…] instead of + …/path-in-root/?root=root-name[&…], + and makes for a much more intuitive user experience, + navigation-wise, when ViewVC is serving up multiple version control + repositories. When in this mode, ViewVC will automatically perform + the obvious redirection for URLs which have a root= + CGI parameter.

+ +

Unfortunately, there's a catch. Older URLs for the default root + (specified by the default_root configuration option) + were optimized to not include the root= CGI + parameter. This means they look unfortunately similar to the newer + root-in-the-path URL format, and ViewVC will not attempt to + redirect them. But ViewVC won't be able to handle them, either. + So, old-style default-root URLs, when aimed at a ViewVC for which + the root_as_url_component option has been subsequently + enabled, will result in an error. If you need to preserve the + functionality of those old URLs, you'll need to either disable + root_as_url_component, or use some other mechanism + (like server URL rewriting) to morph them into compliance with the + new URL format.

+ +
+ +
+

Path-Based Authorization / Forbidden Modules

+ +

ViewVC 1.1 introduces a new pluggable authorization (authz) + subsystem which gives administrators greater control over the + accessibility of the information ViewVC displays in its output. + ViewVC provides a number of working authz modules and a framework for + configuring them. But of specific interest to folks upgrading from + ViewVC 1.0 is that one of these new modules has replaced the + handling of forbidden modules. As such, the forbidden + configuration option now lives under the configuration section + specific to that authz module.

+ +

Migrating your existing configuration of forbidden modules should + be fairly straightforward:

+ +
    + +
  1. In the new "authz-forbidden" section of viewvc.conf, set the + forbidden option to the same value as the + forbidden option in your ViewVC 1.0.x + configuration's "general" section.
  2. + +
  3. In the new "authz-forbiddenre" section of viewvc.conf, set the + forbiddenre option to the same value as the + forbiddenre option in your ViewVC 1.0.x + configuration's "general" section.
  4. + +
  5. Finally, ensure that that the new authorizer + option is set to either "forbidden" (which is the default) or + "forbiddenre", depending on which of those you were using in + ViewVC 1.0.x.
  6. + +
+ +

Of course, you might wish to take advantage of another of the + provided authz modules. Or, you might wish to write a brand new + one for your purposes. The flexibility is yours.

+ +

Known Issues:

+ +
    + +
  • ViewVC does not provide an authentication framework. + It does, however, inherit authenticated usernames as determined + by the HTTP server (Apache, e.g.) via the CGI environment. So, + any authorization module that assigns privileges based on + usernames will work only if ViewVC is deployed in a way that + requires successful authentication (as opposed to allowing + anonymous access).
  • + +
  • Currently, the root listing view only honors the global or + vhost-specific configurations, not any root-specific + configuration. In the event that ViewVC is using root-specific + configuration for its authorization stuffs, this may cause + either the unintended leak of root names to users or the + inability to see roots at all. However, for root-specific + ViewVC views, all configuration — include root-specific + configuration — is honored. If you are concerned about + leaking root names in the root listing view, you might consider + disabling that view altogether by removing roots + from the list of views specified in the + allowed_views configuration option.
  • + +
  • The experimental module which allows ViewVC to serve up views + of remote Subversion repositories is not yet fully integrated + with the authorization subsystem, and almost certainly will + leak privileged data. Sorry. That's (one reason) why it's + experimental.
  • + +
+ +
+ +
+

Syntax Highlighting

+ +

ViewVC 1.0.x supports syntax highlighting provided by multiple + third-party highlighting engines, including GNU enscript, GNU + source-highlight, highlight, php, and py2html. Unfortunately, each + of those integrations worked differently than the others. Some + supported line numbers, some didn't; some were under active + development and recognized newer languages; some weren't; each had + its own set of CSS stylations that needed to be customized; + etc.

+ +

In ViewVC 1.1, we've dropped support for all of those integations + in favor of a single integration with Pygments, a + Python-module-based syntax highlighting engine. As such, the + configuration options for the various other engines (both those + that enabled the integration and those that specified the locations + of the third-party tools) have been removed from ViewVC, and have + been replaced by a single new configuration option: + enable_syntax_coloration.

+ +

The list of removed options is as follows:

+ +
    +
  • options/enscript_path
  • +
  • options/highlight_convert_tabs
  • +
  • options/highlight_path
  • +
  • options/markup_line_numbers
  • +
  • options/php_exe
  • +
  • options/py2html_path
  • +
  • options/use_enscript
  • +
  • options/use_highlight
  • +
  • options/use_php
  • +
  • options/use_py2html
  • +
  • options/use_pygments
  • +
  • options/use_source_highlight
  • +
+ +
+ +
+

Checkin Database

+ +

In ViewVC 1.1, the svndbadmin program's "rebuild" + subcommand has had its purpose become more defined. It no longer + accepts a revision argument, and therefore can now only be used to + completely rebuild the entirety of the checkin database information + for a Subversion repository (instead of being able to only update + the information related to single Subversion revision). For + per-revision updating, use svndbadmin update and + provide a revision (or revision range). And to get the previous + rebuild-a-revision effect, pass the new --force + option to svndbadmin update.

+ +

In other words, where you once did this:

+ +
$ svndbadmin rebuild /path/to/repository 1234
+
+
+ +

you now need to do this:

+ +
$ svndbadmin update /path/to/repository 1234 --force
+
+
+ +
+ +
+

Configuration Options

+ +

This section covers changes to configuration options not already + discussed in other sections pertaining to this upgrade.

+ +

In ViewVC 1.1, a new "utilities" section was added to the + viewvc.conf file. All the options used for configuring the + locations of various helper applications that ViewVC uses which + were previously scattered throughout the configuration file are now + all centralized in this one new section. To accomplish this, the + following options were added:

+ +
    +
  • utilities/cvsgraph
  • +
  • utilities/cvsnt
  • +
  • utilities/diff
  • +
  • utilities/rcs_dir
  • +
  • utilities/svn
  • +
+ +

…and these were removed:

+ +
    +
  • general/cvsnt_ext_path
  • +
  • general/rcs_path
  • +
  • general/svn_path
  • +
  • options/cvsgraph_path
  • +
+ +

All the options which governed which ViewVC views were enabled have + been consolidated into a single new option. This new option:

+ +
    +
  • options/allowed_views
  • +
+ +

…replaces these, which have been removed:

+ +
    +
  • options/allow_annotate
  • +
  • options/allow_markup
  • +
  • options/allow_tar
  • +
+ +

The use_rcsparse option was moved from the "general" + section to the "options" section.

+ +

The log_sort option's value "cvs" has been renamed to + "none" (for general application across all supported version + control systems).

+ +

Custom sections which define per-virtual-host configuration option + overrides must now have their names prefixed with "vhost-". Also, + instead of a hyphen (-) between the virtual host name and the base + configuration section being overridden, now there should be a + forward slash character (/). For example, the following + configuration which was valid in ViewVC 1.0 is no longer valid:

+ +
[vhosts]
+all = viewvc.*
+
+[all-options]
+allow_tar = 1
+
+
+ +

This now needs to be written like so:

+ +
[vhosts]
+all = viewvc.*
+
+[vhost-all/options]
+allow_tar = 1
+
+
+ +

The following is a grab-bag of additional new options:

+ +
    +
  • options/hide_errorful_entries
  • +
  • options/mangle_email_addresses
  • +
+ +
+ +
+

Templates

+ +

This section describes template variable changes introduced in this + release. See the Template + Authoring Guide for the current set of variables available to + each templates.

+ +

One notable change in ViewVC 1.1 is that the markup.ezt and + annotate.ezt templates have been combined into a single file.ezt + template.

+ +

Also, the configuration options under the [templates] + section are now paths relative to the configured template directory + (either the value of the options/template_dir + option, or the default "templates" directory), instead of being + relative to the configuration location.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableLocationChanges
rootpathall templatesadded
change_root_actionall templatesremoved
change_root_hidden_valuesall templatesremoved
rootsall templates except roots.eztremoved
roots.pathroots.eztadded
queryform_hrefdiff.ezt, file.ezt, graph.ezt, log.ezt, log_table.ezt, + query_form.ezt, revision.ezt, roots.eztadded
tarball_hrefdiff.ezt, file.ezt, graph.ezt, log.ezt, log_table.ezt, + query_form.ezt, query_results.ezt, revision.ezt, roots.eztadded
propertiesdirectory.ezt, file.eztadded
properties.namedirectory.ezt, file.eztadded
properties.undisplayabledirectory.ezt, file.eztadded
properties.valuedirectory.ezt, file.eztadded
diff_format_hidden_valuesdiff.eztnow is an iterable list of objects with .name and .value attributes
diff_typediff.eztnew value: f
date_leftdiff.eztrenamed to left.date
path_leftdiff.eztrenamed to left.path
rev_leftdiff.eztrenamed to left.rev
tag_leftdiff.eztrenamed to left.tag
date_rightdiff.eztrenamed to right.date
path_rightdiff.eztrenamed to right.path
rev_rightdiff.eztrenamed to right.rev
tag_rightdiff.eztrenamed to right.tag
annotate_hrefdiff.eztremoved, use right.annotate_href instead
left.annotate_hrefdiff.eztadded
left.download_hrefdiff.eztadded
left.download_text_hrefdiff.eztadded
left.prefer_markupdiff.eztadded
left.revision_hrefdiff.eztadded
left.view_hrefdiff.eztadded
right.annotate_hrefdiff.eztadded
right.download_hrefdiff.eztadded
right.download_text_hrefdiff.eztadded
right.prefer_markupdiff.eztadded
right.revision_hrefdiff.eztadded
right.view_hrefdiff.eztadded
right.view_hrefdiff.eztadded
dir_paging_hidden_valuesdirectory.ezt, , dir_alternate.eztnow is an iterable list of objects with .name and .value attributes
entries.logdirectory.ezt, dir_alternate.eztnow always contains untruncated log message
entries.short_logdirectory.ezt, dir_alternate.eztadded
search_re_hidden_valuesdirectory.ezt, dir_alternate.eztnow is an iterable list of objects with .name and .value attributes
search_tag_hidden_valuesdirectory.ezt, dir_alternate.eztnow is an iterable list of objects with .name and .value attributes
pathrev_clear_hidden_valueslog.ezt, log_table.ezt, directory.ezt, dir_alternate.eztnow is an iterable list of objects with .name and .value attributes
pathrev_hidden_valueslog.ezt, log_table.ezt, directory.ezt, dir_alternate.eztnow is an iterable list of objects with .name and .value attributes
annotate_hreflog.ezt, log_table.eztrenamed to head_annotate_href
diff_form_hidden_valueslog.ezt, log_table.eztnow is an iterable list of objects with .name and .value attributes
download_hreflog.ezt, log_table.eztrenamed to head_download_href
download_text_hreflog.ezt, log_table.eztrenamed to head_download_text_href
log_paging_hidden_valueslog.ezt, log_table.eztnow is an iterable list of objects with .name and .value attributes
logsort_hidden_valueslog.ezt, log_table.eztnow is an iterable list of objects with .name and .value attributes
prefer_markuplog.ezt, log_table.eztrenamed to head_prefer_markup
view_hreflog.ezt, log_table.eztrenamed to head_view_href
rss_link_hrefquery.ezt, rss.eztadded
query_hidden_valuesquery_form.eztnow is an iterable list of objects with .name and .value attributes
jump_rev_hidden_valuesrevision.eztnow is an iterable list of objects with .name and .value attributes
+ +
+
+ +
+

Upgrading From ViewCVS 0.9

+ +

This section discusses how to upgrade ViewCVS 0.9 to ViewVC 1.0.

+ +
+

CGI Stubs

+ +

The CGI stub scripts haved been moved from + <VIEWVC_INSTALLATION_DIRECTORY>/cgi/ to + <VIEWVC_INSTALLATION_DIRECTORY>/bin/cgi/, so + you will need update any ScriptAlias directives pointing to them in + your apache configuration. Also, the contents of these scripts have + changed, so you may need to replace copies of the old scripts you + put in other directories.

+ +
+ +
+

Checkin Database

+ +

ViewVC 1.0 reads and writes commit times in the MySQL database in + UTC time rather than local time. This can cause times displayed on + the query page to be a few hours off if an old database is being + used with a new version of ViewVC. The best way to fix this is to + rebuild the database with the new version of cvsdbadmin, but it + is also possible to enable a backwards compatibility mode by + setting utc_time = 0 at the top of lib/dbi.py

+ +
+ +
+

"checkout_magic" Option

+ +

In ViewVC 1.0, the checkout_magic option has been + disabled by default to provide a simpler URL scheme that works + safely with URL authorization. Most users will not notice any + difference in behavior, but users who had been using ViewCVS to + browse the contents of static HTML pages stored in a repository + may notice that links and images in those pages targetted at other + files in the repository no longer display correctly. The new + default_file_view option can be used to solve this + problem and, if neccessary, checkout_magic can also + be re-enabled. The viewcvs.conf file describes these + options in detail.

+ +
+ +
+

Configuration Options

+ +

The following options have been added:

+ +
    +
  • general/svn_roots
  • +
  • general/root_parents
  • +
  • general/use_rcsparse
  • +
  • general/cvsnt_exe_path
  • +
  • options/template_dir
  • +
  • options/docroot
  • +
  • options/http_expiration_time
  • +
  • options/generate_etags
  • +
  • options/root_as_url_component
  • +
  • options/default_file_view
  • +
  • options/sort_group_dirs
  • +
  • options/use_pagesize
  • +
  • options/limit_changes
  • +
  • options/use_localtime
  • +
  • options/cross_copies
  • +
  • options/use_highlight
  • +
  • options/highlight_path
  • +
  • options/highlight_line_numbers
  • +
  • options/highlight_convert_tabs
  • +
  • options/use_php
  • +
  • options/php_path
  • +
  • options/svn_path
  • +
  • templates/error
  • +
  • templates/query_form
  • +
  • templates/query_results
  • +
  • cvsdb/port
  • +
  • cvsdb/rss_row_limit
  • +
+ +

The following options have been removed:

+ +
    +
  • general/main_title
  • +
  • options/diff_font_face
  • +
  • options/diff_font_size
  • +
  • options/disable_enscript_lang
  • +
  • templates/footer
  • +
+ +
+ +
+

Templates

+ +

The templates have changed drastically in this version of ViewVC. + If you are using customized templates from 0.9 or earlier, you will want + to port your old customizations to the new template files instead of + trying to get the old template files to work with the new ViewVC.

+ +

There is a new Template + Authoring Guide for ViewVC 1.0 templates that can help you + with your customizations. And the chart below lists all 0.9 + template variables and shows what's become of them in 1.0.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableLocationChanges
agomarkup.eztunchanged
authormarkup.eztunchanged
back_urllog.ezt, log_table.eztreplaced by up_href, which doesn't include the current #file anchor
branchlog.ezt, log_table.eztreplaced by default_branch, which is a list instead of a string
branchquery.eztunchanged
branch_nameslog.ezt, log_table.eztrenamed to branch_tags
branch_pointsmarkup.eztunchanged
branch_tagsdir_alternate.ezt, directory.eztunchanged
branchesmarkup.eztunchanged
cfg.general.addressfooter.eztunchanged
cfg.general.main_titledir_alternate.ezt, directory.ezt, query.eztremoved, used to be a string from the configuration file that was shown in the title of the root directory page.
cfg.options.use_cvsgraphdir_alternate.ezt, directory.eztunchanged
cfg.options.use_re_searchdir_alternate.ezt, directory.eztunchanged
changedmarkup.eztunchanged
changesdiff.eztattributes changed, see below
changes.extradiff.eztrenamed to changes.line_info_extra
changes.have_leftdiff.eztunchanged
changes.have_rightdiff.eztunchanged
changes.leftdiff.eztunchanged
changes.line1diff.eztrenamed to changes.line_info_left
changes.line2diff.eztrenamed to changes.line_info_right
changes.rightdiff.eztunchanged
changes.typediff.eztnew values binary_diff and error
commitsquery.eztunchanged
commits.descquery.eztunchanged
commits.filesquery.eztunchanged
commits.files.authorquery.eztunchanged
commits.files.branchquery.eztunchanged
commits.files.datequery.eztunchanged
commits.files.linkquery.eztunchanged
commits.files.minusquery.eztunchanged
commits.files.plusquery.eztunchanged
commits.files.revquery.eztunchanged
current_rootdir_alternate.ezt, directory.eztrenamed to rootname
datequery.eztunchanged
date1diff.eztrenamed to date_left
date2diff.eztrenamed to date_right
diff_formatdiff.ezt, log.ezt, log_table.eztunchanged
directoryquery.eztunchanged
entrieslog.ezt, log_table.eztattributes changed, see below
entries.agolog.ezt, log_table.eztunchanged
entries.authorlog.ezt, log_table.eztunchanged
entries.branch_nameslog.eztunchanged
entries.branch_pointlog.ezt, log_table.eztunchanged
entries.branch_pointslog.ezt, log_table.eztunchanged
entries.branch_points.hreflog.ezt, log_table.eztunchanged
entries.branch_points.namelog.ezt, log_table.eztunchanged
entries.brancheslog.ezt, log_table.eztunchanged
entries.branches.hreflog.ezt, log_table.eztunchanged
entries.branches.namelog.ezt, log_table.eztunchanged
entries.changedlog.eztunchanged
entries.hreflog.ezt, log_table.eztrenamed to entries.download_href
entries.html_loglog.ezt, log_table.eztrenamed to entries.log
entries.next_mainlog.ezt, log_table.eztunchanged
entries.prevlog.ezt, log_table.eztunchanged
entries.revlog.ezt, log_table.eztunchanged
entries.statelog.ezt, log_table.eztunchanged
entries.tag_nameslog.eztunchanged
entries.tagslog.ezt, log_table.eztunchanged
entries.tags.hreflog.eztunchanged
entries.tags.namelog.ezt, log_table.eztunchanged
entries.text_hreflog.ezt, log_table.eztrenamed to entries.download_text_href
entries.to_selectedlog.ezt, log_table.eztcombined into diff_to_sel_href variable
entries.utc_datelog.ezt, log_table.eztrenamed to entries.date
entries.vendor_branchlog.ezt, log_table.eztunchanged
entries.view_hreflog.ezt, log_table.eztunchanged
filequery.eztunchanged
file_urlheader.eztrenamed to log_href
filenameheader.eztremoved, used to be set to the name of the file being shown
files_showndir_alternate.ezt, directory.eztunchanged
graph_hreflog.ezt, log_table.eztunchanged
has_tagsdir_alternate.ezt, directory.eztremoved, used to be a boolean that was true when either tag information was available from the current directory or a tag was set. Determined whether or not to show a tag selector box on the bottom of the directory page.
have_logsdir_alternate.ezt, directory.eztremoved, used to be a boolean that was true whenever any logs were being shown in a directory listing. When it was false the template code would omit the log column from the directory table.
head_abs_hreflog.ezt, log_table.eztrenamed to download_href
head_hreflog.ezt, log_table.eztreplaced by view_href and download_href
hidden_valuesdiff.ezt, log.ezt, log_table.eztcombined into *_hidden_values variables
hide_attic_hrefdir_alternate.ezt, directory.eztunchanged
hoursquery.eztunchanged
hreflog.ezt, log_table.eztcombined into *_href variables
hrefmarkup.eztrenamed to download_href
human_readablelog.ezt, log_table.eztunchanged
imagemapgraph.eztunchanged
logmarkup.eztunchanged
logsortlog.ezt, log_table.eztunchanged
mime_typelog.ezt, log_table.ezt, markup.eztunchanged
nav_filemarkup.eztreplaced with nav_path
nav_pathdir_alternate.ezt, directory.ezt, header.ezt, log.ezt, log_table.eztchanged from a block of HTML to a list of path components
no_matchdir_alternate.ezt, directory.eztremoved, used to be a boolean that was true when a directory contained files, but none of them could be displayed due to regular expression or view tag filtering. Would trigger an error message.
num_commitsquery.eztunchanged
num_filesdir_alternate.ezt, directory.eztremoved, used to be a count of files in a directory, including dead and filtered files. This number was only shown in the no_match error message.
paramsdir_alternate.ezt, directory.eztreplaced by search_re_hidden_values
pathheader.eztremoved, used to be set to the directory path of the file being shown
plain_tagsdir_alternate.ezt, directory.eztunchanged
prevmarkup.eztunchanged
qqueryheader.eztcombined into log_href variable
qquerylog.ezt, log_table.eztreplaced by diff_select_hidden_values
querylog.ezt, log_table.eztcombined into *_href variables
queryquery.eztunchanged
repositoryquery.eztunchanged
request.amp_querygraph.eztcombined into imagesrc variable
request.script_namedir_alternate.ezt, directory.ezt, log.ezt, log_table.eztcombined into *_href variables
request.urldiff.eztcombined into diff_format_action variable
request.urlgraph.eztcombined into imagesrc variable
request.wheregraph.eztrenamed to just where
revgraph.eztremoved, used to be set to the value of the "graph" parameter in CvsGraph page urls. The value was passed on through the rev parameter to CvsGraph image URLs, where, oddly enough, it was ignored. It'd be set to a file revision number in directory page graph links, and just "1" in log page graph links.
revgraph.ezt, header.ezt, markup.eztunchanged
rev1diff.eztrenamed to rev_left
rev2diff.eztrenamed to rev_right
rev_selectedlog.ezt, log_table.eztunchanged
rootsdir_alternate.ezt, directory.eztchanged to a list of objects instead of a list of strings
rowsdir_alternate.ezt, directory.eztreplaced by entries
rows.anchordir_alternate.ezt, directory.eztrenamed to entries.anchor
rows.authordir_alternate.ezt, directory.eztrenamed to entries.author
rows.cvsdir_alternate.ezt, directory.eztreplaced by entries.errors
rows.graph_hrefdir_alternate.ezt, directory.eztrenamed to entries.graph_href
rows.hrefdir_alternate.ezt, directory.eztrenamed to entries.log_href
rows.logdir_alternate.ezt, directory.eztrenamed to entries.short_log
rows.log_filedir_alternate.ezt, directory.eztrenamed to entries.log_file
rows.log_revdir_alternate.ezt, directory.eztrenamed to entries.log_rev
rows.namedir_alternate.ezt, directory.eztrenamed to entries.name
rows.revdir_alternate.ezt, directory.eztrenamed to entries.rev
rows.rev_hrefdir_alternate.ezt, directory.eztreplaced by entries.view_href and entries.download_href
rows.show_logdir_alternate.ezt, directory.eztremoved, used to be a boolean that was true whenever a log message was present for the directory entry.
rows.statedir_alternate.ezt, directory.eztrenamed to entries.state
rows.timedir_alternate.ezt, directory.eztrenamed to entries.ago
rows.typedir_alternate.ezt, directory.eztrenamed to entries.pathtype
search_redir_alternate.ezt, directory.eztunchanged
selection_formdir_alternate.ezt, directory.eztrenamed to search_re_form
show_attic_hrefdir_alternate.ezt, directory.eztunchanged
sortbydir_alternate.ezt, directory.ezt, query.eztunchanged
sortby_author_hrefdir_alternate.ezt, directory.eztunchanged
sortby_date_hrefdir_alternate.ezt, directory.eztunchanged
sortby_file_hrefdir_alternate.ezt, directory.eztunchanged
sortby_log_hrefdir_alternate.ezt, directory.eztunchanged
sortby_rev_hrefdir_alternate.ezt, directory.eztunchanged
statemarkup.eztunchanged
tagmarkup.eztrenamed to pathrev
tag1diff.eztrenamed to tag_left
tag2diff.eztrenamed to tag_right
tagslog.ezt, log_table.eztunchanged
tagsmarkup.eztunchanged
tags.namelog.ezt, log_table.eztunchanged
tags.revlog.ezt, log_table.eztunchanged
tagsmarkup.eztunchanged
tarball_hrefdir_alternate.ezt, directory.eztunchanged
text_hrefmarkup.eztrenamed to download_text_href
tr1log.ezt, log_table.eztremoved, used to be a default value for the first text field in the diff selector form. In 1.0, the default value is computed in the templates.
tr2log.ezt, log_table.eztremoved, used to be a default value for the second text field in the diff selector form. In 1.0, the default value is computed in the templates.
unreadabledir_alternate.ezt, directory.eztremoved, used to be a boolean that was true whenever any of the files in the directory listing were 'unreadable.' It would trigger a generic error message at the bottom of the page.
utc_datemarkup.eztrenamed to date
vendor_branchmarkup.eztunchanged
view_tagdir_alternate.ezt, directory.ezt, log.ezt, log_table.eztrenamed to pathrev
viewablelog.ezt, log_table.eztrenamed to prefer_markup
vsnfooter.eztunchanged
wherediff.ezt, dir_alternate.ezt, directory.ezt, log.ezt, log_table.eztunchanged
whoquery.eztunchanged
+ +
+ +
+

Template Arrangement

+ +

The default templates have been rearranged a bit in ViewVC 1.0. + Specifically, "header.ezt" and "footer.ezt" have moved into the + "templates/include/" subdirectory. Also, "directory.ezt" and + "dir_alternate.ezt" now reference new template files + "dir_header.ezt" and "dir_footer.ezt", also found in the + "templates/include/" subdirectory.

+ +

Notably, the "markup.ezt" and "annotate.ezt" templates are now + fully self-contained. That is, the markup and annotation data + generated by ViewVC is now accessible in those templates just like + other template variables. As a result, ViewVC now has no more + internal need for the templates.footer configuration + variable, so that variable has been removed from the default + configuration file.

+ +
+
+ +
+

Upgrading From ViewCVS 0.8

+ +

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

+ +

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

+ +
+

Configuration Options

+ +

More templates were introduced in version 0.8 of the software, + which made many of the configuration options obsolete. This section + covers which options were removed. If you made any changes to these + options, then you will need to make corresponding changes in the + templates.

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

+
+ +
markup_log
+
+ This option has been incorporated into the + markup.ezt template. + +

+
+ +
Colors: nav_header + and alt_background
+
+ These options have been incorporated into the + header.ezt template. + +

+
+ +
+ Images: + back_icon, + dir_icon, + and file_icon +
+
+ These options have been incorporated into the + directory.ezt, header.ezt, + log.ezt, log_table.ezt, and + query.ezt templates. + +

+
+ +
use_java_script + and open_extern_window
+
+ The templates now use JavaScript in all applicable places, + and open external windows for most downloading and viewing + of files. If you wish to not use JavaScript and/or external + windows, then remove the feature(s) from the templates. + +

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

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

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

+
+ +
+ +
+ +
+

Template Variables

+ +

Some template variables that were available in 0.8 have been + removed in 0.9. If you have custom templates that refer to these + variables, then you will need to modify your templates.

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

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

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

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

+
+
+ +
+
+ + + diff --git a/docs/url-reference.html b/docs/url-reference.html new file mode 100644 index 00000000..ebea1630 --- /dev/null +++ b/docs/url-reference.html @@ -0,0 +1,1489 @@ + + +ViewVC 1.1 URL Reference + + + + +

ViewVC 1.1 URL Reference

+ +
+

Introduction

+ +

This document describes the format of URLs accepted by ViewVC 1.1 + and is intended to be useful to users and outside software + developers who want to create links to ViewVC pages. It should also + be useful to ViewVC developers as a way of tracking and documenting + the many quirks of ViewVC's URL handling logic.

+ +

Future releases of ViewVC will support the URL formats described in + this document. Certain URLs currently accepted by ViewVC, such as + redirecting URLs that result from the submission of form elements + on ViewVC pages, are not documented here and therefore may not be + supported by future releases of ViewVC.

+ +
+ + + +
+ +
+

URL Components

+ +

A ViewVC URL consists of 3 components: a Script Location, a Repository Path, and some optional Query Parameters. Some examples:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ViewVC Script LocationRepository PathQuery Parameters
http://example.org/viewvc.cgi/some/file?view=log&revision=1.34
http://example.org/viewvc.cgi/*checkout*/some/other/file?revision=1.10.2.5
http://example.org/viewvc.cgi/some/dir/?pathrev=BRANCH_2_3
http://example.org/viewvc.cgi/some/dir.tar.gz?view=tar&pathrev=BRANCH_2_3
http://example.org/viewvc.cgi/yet/another/file?view=diff?r1=1.12&r2=1.14
+ +

ViewVC Script Location

+ +

The script location is a common base for all ViewVC URL's + pertaining to a particular installation. It's whatever location the + web server is configured to serve ViewVC pages from.

+ +

Repository Path

+ +

Since ViewVC is essentially a file system browser for repositories, + repository paths referring to the files and directories being + browsed get their own section in ViewVC URLs immediately following + the script location. Repository paths are always case sensitive and + separated by forward slashes, regardless of the underlying + filesystem.

+ +

Repository paths can be given certain "magic" prefixes and + suffixes. For example, a "/*checkout*" prefix can be added to file + views to force a checkout even without an explicit "view=co" query + parameter. And a ".tar.gz" suffix is added to download tarball URLs + so downloaded tarballs will be saved with sensible default + names.

+ +

If the root_as_url_component configuration option is + enabled, the first directory name in a repository path (after any + magic prefix) is taken to be the name (from the ViewVC + configuration) of the repository to browse rather than the name of + an actual directory. The repository name can also be specified in a + "root=" query parameter instead (see root parameter below).

+ +

Paths beginning with "/*docroot*/" are an exception the rules + above. These paths are used to provide access to files in the + ViewVC template directory: image files, static HTML pages, and the + ViewVC stylesheet. For more information, see the Docroot View syntax below.

+ +

Query Parameters

+ +

Following the ViewVC and repository locations in URLs is an + optional query string, a list of parameter/value pairs written like + "?param1=value1&param2=value2..." Some parameters, like + "revision", "pathrev", and "root", augment repository path + information, specifying revisions and repositories. Other + parameters like "logsort" and "diff_format" control ViewVC display + options. Some parameters are also "sticky" and get embedded into + links inside the generated pages, sticking around for future page + views.

+ +

ViewVC provides a number of different views of repository data + including a directory listing view, a log view, a file diff view, + and others. (The views are listed and described in the + help_rootview.html ViewVC help page). URLs for each of + these views accept specific parameters described in the URL Syntax section, but some parameters are used + by multiple views and they are described below:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterDescription
viewThe name of the view to display, does not need to be + explicitly specified in many URLs. Possible values are shown + in the URL Syntax section + below.
revisionThe revision or tag to display information about, used by + several different views.
pathrevThe current sticky revision (Subversion) or sticky tag (CVS), + as described in the help_rootview.html ViewVC + help page. In Subversion, because path information is revision + controlled, this value is also used to look up paths in the + repository, providing a means of accessing paths that no + longer exist in HEAD.
rootThe name of the repository to browse. When the + default_root configuration option is set or the + root_as_url_component option is enabled, it is + not neccessary to to specify this parameter. When the + root_as_url_component option is enabled, ViewVC + URLs with root parameters redirect to locations + with the root values embedded in the repository paths.
+
+ +
+

URL Syntax

+ +

This section lists URL syntax for each ViewVC view. Parts of URLs + which may vary shown as variables in UPPERCASE.

+ +

Annotate View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to annotate
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=annotatedependsview parameter. Not + required when an annotate parameter is + present
annotate=REVISIONoptionalrevision or tag to annotate
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+ + +

Checkout View

+ + + + + + + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/*checkout*optionalmagic prefix. If specified when the + checkout_magic configuration option is disabled, + ViewVC will redirect to a URL without the prefix.
/PATHrequiredfile path to check out
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=codependsview parameter, not + needed if the default_file_view configuration + variable is set to co, since that makes the + checkout view the default view for file paths. Also not needed + if the /*checkout* magic prefix or the + revision parameter is present. +
content-type=TYPEoptionalMIME type to send with checked out file, default is a guess + based on file extension
revision=REVISIONoptionalrevision parameter
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+ +

Diff View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to display diff of
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=diffoptionalview parameter
r1=R1requiredstarting revision or tag or the string "text" to indicate that + TR1 value (below) should override this + one
r2=R2requiredending revision or tag or the string "text" to indicate that + TR2 value (below) should override this + one
tr1=TR1dependsstarting revision or tag, used if r1 parameter is + present and set to "text"
tr2=TR2dependsending revision or tag, used if r2 parameter is + present and set to "text"
p1=P1optionalstarting file path that can override the + PATH value to allow files at two different + paths to be compared
p2=P2optionalending file path that can override the + PATH value to allow files at two different + paths to be compared
diff_format=DIFF_FORMAToptionalvalue specifying the type of diff to display. Can be "u" for + unified diff, "c" for context diff, "s" for side by side diff, "h" + for human readable diff, "l" for long human readable diff, and "f" + for a full human readable diff. If no value is specified the + default depends on the diff_format configuration + option.
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+ +

Directory Listing View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATH/requireddirectory path to view. If the trailing slash is omitted, + ViewVC will redirect to a URL that has a trailing slash.
+
+ + + + + + + + + + + + + + + + view parameter + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=diroptional
hideattic=HIDEATTICoptional"0" to show dead files in CVS directory listings or "1" to + hide dead files. Default depends on the hide_attic + configuration value.
search=SEARCHoptionalregular expression to search files in the directory with if + use_re_search configuration option is enabled
sortby=SORTBYoptional"file" "rev" "date" "author" or "log" to indicate how the + directory listing should be sorted. Default depends on + sortby configuration option.
sortdir=SORTBYoptional"up" to sort directory in ascending order or "down" for + descending order. Default is "up".
dir_pagestart=PAGEoptionalitem number to start listing at if paging is enabled
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+ +

Docroot View

+ + + + + + + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/*docroot*requiredmagic prefix
/PATHrequiredfile path to retrieve. ViewVC will return the contents of the + file located at PATH, relative to the + docroot subdirectory of the directory specified in + the template_dir configuration option.
+
+ + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
+ +

Graph View

+ + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to generate CvsGraph page for
+
+ + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=graphrequiredview parameter
root=ROOTdependsroot parameter
+ +

Graph Image View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to generate CvsGraph image for
+
+ + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=graphimgrequiredview parameter
root=ROOTdependsroot parameter
+ +

Log View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile or directory path to generate log for
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=logdependsview parameter, does + not need to be specified for file paths when the + default_file_view configuration option is set to + log. However it is recommended that the parameter be + passed anyway for consistency with directory log URLs and + compatibility with ViewVC installations that set + default_file_view to co.
logsort=SORToptional"rev" to sort log entries by revision number or "date" to sort + by date. Default depends on the log_sort + configuration value.
log_pagestart=PAGEoptionalitem number to start listing at if paging is enabled
r1=R1optionalcurrent revision selected for diffs
diff_format=DIFF_FORMAToptionalcurrently selected diff format
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+ +

Markup View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to mark up
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=markuprequiredview parameter
revision=REVISIONoptionalrevision parameter
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+ +

Patch View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to display patch of
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=patchrequiredview parameter
r1=R1requiredstarting revision or tag or the string "text" to indicate that + TR1 value (below) should override this + one
r2=R2requiredending revision or tag or the string "text" to indicate that + TR2 value (below) should override this + one
tr1=TR1dependsstarting revision or tag, only used if r1 parameter + is present and set to "text"
tr2=TR2dependsending revision or tag, only used if r2 parameter + is present and set to "text"
p1=P1optionalstarting file path that can override the + PATH value to allow files at two different + paths to be compared
p2=P2optionalending file path that can override the + PATH value to allow files at two different + paths to be compared
diff_format=DIFF_FORMAToptionalvalue specifying the type of patch to display. Can be "u" for + unified diff or "c" for context diff. If no value is specified the + default depends on the diff_format configuration + option. +
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+ +

Query Form View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to display query results from
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=queryformrequiredview parameter
branch=BRANCHoptionalbranch query string
branch_match=BRANCH_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of branch match
dir=DIRoptionaldirectory query string
file=FILEoptionalfile query string
file_match=FILE_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of file match
who=WHOoptionalauthor query string
who_match=WHO_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of author match
comment=COMMENToptionallog message query string
comment_match=COMMENT_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of log message match
querysort=SORToptional"date" "author" or "file" determining order of query results
date=DATEoptional"hours" "day" "week" "month" "all" or "explicit" to filter + query results by date
hours=HOURSoptionalnumber of hours back to include results from when + DATE is "hours"
mindate=MINDATEoptionalearliest date to include results from when + DATE is "explicit"
maxdate=MAXDATEoptionallatest date to include results from when + DATE is "explicit"
limit_changes=LIMIT_CHANGESoptionalmaximum number of files to list per commit in query + results. Default is value of limit_changes + configuration option
root=ROOTdependsroot parameter
+ + +

Query View

+ + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequiredfile path to display query results from
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=queryrequiredview parameter
branch=BRANCHoptionalbranch query string
branch_match=BRANCH_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of branch match
dir=DIRoptionaldirectory query string
file=FILEoptionalfile query string
file_match=FILE_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of file match
who=WHOoptionalauthor query string
who_match=WHO_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of author match
comment=COMMENToptionallog message query string
comment_match=COMMENT_MATCHoptional"exact" "like" "glob" "regex" or "notregex" determining type + of log message match
querysort=SORToptional"date" "author" or "file" determining order of query results
date=DATEoptional"hours" "day" "week" "month" "all" or "explicit" to filter + query results by date
hours=HOURSoptionalnumber of hours back to include results from when + DATE is "hours"
mindate=MINDATEoptionalearliest date to include results from when + DATE is "explicit"
maxdate=MAXDATEoptionallatest date to include results from when + DATE is "explicit"
format=FORMAToptional"rss" or "backout" values to generate an rss feed or list of + commands to back out changes instead showing a normal query result + page
limit=LIMIToptionalmaximum number of file-revisions to process during a + query. Default is value of row_limit configuration + option
limit_changes=LIMIT_CHANGESoptionalmaximum number of files to list per commit in query + results. Default is value of limit_changes + configuration option
root=ROOTdependsroot parameter
+ +

Revision View

+ + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=revisionrequiredview parameter
revision=REVISIONoptionalrevision parameter
limit_changes=LIMIT_CHANGESoptionalmaximum number of files to list per commit. Default is value + of limit_changes configuration option
root=ROOTdependsroot parameter
+ +

Repository Listing

+ + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
+
+ + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=rootsdependsview parameter. Not + required if the root_as_url_component configuration + is enabled or the default_root option is not + set.
+ +

Tarball Download

+ + + + + + + + + + + + + + + + + + + + + + +
+ Path Components (in order of appearance in the URL) +
ComponentOpt/ReqDescription
/PATHrequireddirectory path to download
.tar.gzdependsmagic suffix. Only required when the name of the directory + being downloaded ends in ".tar.gz" and the parent + parameter not is present. But it is recommended to add the magic + suffix to all tarball URLs to avoid this special case and give the + downloaded files sensible default names.
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Query Parameters
ParameterOpt/ReqDescription
view=tarrequiredview parameter
parent=1optionalIf the parent parameter is specified, the last + component of the PATH is discarded before + it is ever looked up in the repository. This feature is used when + the root_as_url_component configuration option is + disabled to allow root tarball URLs to be saved with names like + "ROOT-root.tar.gz".
pathrev=PATHREVoptionalpathrev parameter
root=ROOTdependsroot parameter
+
+ +
+

Backwards Compatibility

+ +

ViewVC's URL format has changed a lot over time, but ViewVC goes + out of its way to support URLs using older formats so there aren't + broken links when an installation of ViewVC is upgraded. The + support is implemented as a set of URL transformations that + recognize elements of old-style URLs and convert them to newer + equivalents. If any transformations are applied (with some + exceptions, mentioned below), ViewVC will issue a single redirect + to the transformed URL. Descriptions of the transformations + follow.

+ +

'view=rev' Parameter ⇒ 'view=revision'

+ +

URLs with a view=rev parameter will automatically be + redirected to URLs with a view=revision parameter + instead.

+ +

'cvsroot' Parameter ⇒ 'root'

+ +

URLs with a cvsroot parameter will automatically be + redirected to URLs with a root parameter instead.

+ +

'only_with_tag' Parameter ⇒ 'pathrev'

+ +

URLs with an only_with_tag parameter will + automatically be redirected to URLs with a pathrev + parameter instead.

+ +

'~checkout~' Magic Path Prefix ⇒ '*checkout*'

+ +

URLs with a ~checkout~ path prefix get interpreted + just like URLs with a '*checkout*' prefix. There is currently no + redirect, but there could be in the future.

+ +

'*checkout*' Magic Path Prefix ⇒ 'view=co'

+ +

When the checkout_magic configuration option is + disabled, URLs with a *checkout* magic prefix will + redirect to an equivalent URL that does not use the prefix.

+ +

'root' Parameter ⇒ Root Path Component

+ +

When the root_as_url_component configuration option is + enabled, URLs with a root parameter will redirect to + an equivalent URL with the root name embedded in the path.

+ +

'rev' Parameter ⇒ 'revision' and 'pathrev'

+ +

CVS URLs with a rev parameter will redirect to URLs + with a revision parameter instead. Subversion URLs + with a rev parameter will redirect to URLs with a + pathrev parameter, in order to account for the how the + Subversion backend used to look up paths before + pathrev was introduced.

+ +

'.diff' Suffix ⇒ Diff View

+ +

When ViewVC encounters a invalid repository path that ends in + .diff, and stripping that ending yields a valid file + path, it will redirect to a diff view of the file.

+ +

'.tar.gz' Suffix ⇒ 'view=tar'

+ +

When ViewVC encounters a invalid repository path that ends in + .tar.gz, /root.tar.gz, or + /REPOS-root.tar.gz, and stripping the ending yields a + valid directory path, it will redirect to a URL to download a + tarball of the directory.

+ +

'tarball=1' Parameter ⇒ 'view=tar'

+ +

A tarball=1 parameter is treated pretty much like a + view=tar parameter. There is no redirect when it is + encountered, but there could be in the future.

+ +

'graph=1' Parameter ⇒ 'view=graph'

+ +

A graph=1 parameter is treated like a + view=graph parameter. There is currently no redirect + when it is encountered, but there could be one in the future.

+ +

'graph=1&makeimage=1' Parameters ⇒ 'view=graphimg'

+ +

A graph=1&makeimage=1 parameter is treated like a + view=graph parameter. There is currently no redirect + when it is encountered, but there could be one in the future.

+ +

'content_type=text/vnd.viewcvs-markup' and 'content_type=text/x-cvsweb-markup' Parameters⇒ 'view=markup'

+ +

content-type=text/vnd.viewcvs-markup and + content-type=text/x-cvsweb-markup parameters are + treated like a view=markup parameter. There is + currently no redirect when it is encountered, but there could be + one in the future. Other values of the content-type + parameter, which were used to dictate the MIME type of files + displayed in the checkout/download view prior to ViewVC 1.0.6, are + ignored.

+ +

'Attic/FILE' Paths ⇒ 'FILE'

+ +

When ViewVC encounters an invalid repository path whose last or + second-to-last component is named Attic, and stripping + the component yields a valid path, it will redirect to a URL with + that path.

+ +
+ + + diff --git a/lib/PyFontify.py b/lib/PyFontify.py new file mode 100644 index 00000000..e4967c72 --- /dev/null +++ b/lib/PyFontify.py @@ -0,0 +1,170 @@ +"""Module to analyze Python source code; for syntax coloring tools. + +Interface: + + tags = fontify(pytext, searchfrom, searchto) + +The PYTEXT argument is a string containing Python source code. The +(optional) arguments SEARCHFROM and SEARCHTO may contain a slice in +PYTEXT. + +The returned value is a list of tuples, formatted like this: + + [('keyword', 0, 6, None), + ('keyword', 11, 17, None), + ('comment', 23, 53, None), + ... + ] + +The tuple contents are always like this: + + (tag, startindex, endindex, sublist) + +TAG is one of 'keyword', 'string', 'comment' or 'identifier' +SUBLIST is not used, hence always None. +""" + +# Based on FontText.py by Mitchell S. Chapman, +# which was modified by Zachary Roadhouse, +# then un-Tk'd by Just van Rossum. +# Many thanks for regular expression debugging & authoring are due to: +# Tim (the-incredib-ly y'rs) Peters and Cristian Tismer +# So, who owns the copyright? ;-) How about this: +# Copyright 1996-1997: +# Mitchell S. Chapman, +# Zachary Roadhouse, +# Tim Peters, +# Just van Rossum + +__version__ = "0.3.1" + +import string, re + + +# This list of keywords is taken from ref/node13.html of the +# Python 1.3 HTML documentation. ("access" is intentionally omitted.) + +keywordsList = ["and", "assert", "break", "class", "continue", "def", + "del", "elif", "else", "except", "exec", "finally", + "for", "from", "global", "if", "import", "in", "is", + "lambda", "not", "or", "pass", "print", "raise", + "return", "try", "while", + ] + +# 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', '"') + +# A regexp for matching multi-line tripled-quoted strings. (Way to go, Tim!) +pat = """ + qqq + [^q]* + ( + ( \\[\000-\377] + | q + ( \\[\000-\377] + | [^q] + | q + ( \\[\000-\377] + | [^q] + ) + ) + ) + [^q]* + )* + qqq +""" +pat = string.join(string.split(pat), '') # get rid of whitespace +tripleQuotePat = string.replace(pat, "q", "'") + "|" \ + + string.replace(pat, 'q', '"') + +# A regexp which matches all and only Python keywords. This will let +# us skip the uninteresting identifier references. +nonKeyPat = "(^|[^a-zA-Z0-9_.\"'])" # legal keyword-preceding characters +keyPat = nonKeyPat + "(" + string.join(keywordsList, "|") + ")" + nonKeyPat + +# Our final syntax-matching regexp is the concatation of the regexp's we +# constructed above. +syntaxPat = keyPat + \ + "|" + commentPat + \ + "|" + tripleQuotePat + \ + "|" + quotePat +syntaxRE = re.compile(syntaxPat) + +# Finally, we construct a regexp for matching indentifiers (with +# optional leading whitespace). +idKeyPat = "[ \t]*[A-Za-z_][A-Za-z_0-9.]*" +idRE = re.compile(idKeyPat) + + +def fontify(pytext, searchfrom=0, searchto=None): + if searchto is None: + searchto = len(pytext) + tags = [] + commentTag = 'comment' + stringTag = 'string' + keywordTag = 'keyword' + identifierTag = 'identifier' + + start = 0 + end = searchfrom + while 1: + # Look for some syntax token we're interested in. If find + # nothing, we're done. + matchobj = syntaxRE.search(pytext, end) + if not matchobj: + break + + # If we found something outside our search area, it doesn't + # count (and we're done). + start = matchobj.start() + if start >= searchto: + break + + match = matchobj.group(0) + end = start + len(match) + c = match[0] + if c == '#': + # We matched a comment. + tags.append((commentTag, start, end, None)) + elif c == '"' or c == '\'': + # We matched a string. + tags.append((stringTag, start, end, None)) + else: + # We matched a keyword. + if start != searchfrom: + # there's still a redundant char before and after it, strip! + match = match[1:-1] + start = start + 1 + else: + # This is the first keyword in the text. + # Only a space at the end. + match = match[:-1] + end = end - 1 + tags.append((keywordTag, start, end, None)) + # If this was a defining keyword, look ahead to the + # following identifier. + if match in ["def", "class"]: + matchobj = idRE.search(pytext, end) + if matchobj: + start = matchobj.start() + if start == end and start < searchto: + end = start + len(matchobj.group(0)) + tags.append((identifierTag, start, end, None)) + return tags + + +def test(path): + f = open(path) + text = f.read() + f.close() + tags = fontify(text) + for tag, start, end, sublist in tags: + print tag, `text[start:end]` + +if __name__ == "__main__": + import sys + test(sys.argv[0]) diff --git a/lib/accept.py b/lib/accept.py new file mode 100644 index 00000000..0980287c --- /dev/null +++ b/lib/accept.py @@ -0,0 +1,236 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# accept.py: parse/handle the various Accept headers from the client +# +# ----------------------------------------------------------------------- + +import re +import string + + +def language(hdr): + "Parse an Accept-Language header." + + # parse the header, storing results in a _LanguageSelector object + return _parse(hdr, _LanguageSelector()) + +# ----------------------------------------------------------------------- + +_re_token = re.compile(r'\s*([^\s;,"]+|"[^"]*")+\s*') +_re_param = re.compile(r';\s*([^;,"]+|"[^"]*")+\s*') +_re_split_param = re.compile(r'([^\s=])\s*=\s*(.*)') + +def _parse(hdr, result): + # quick exit for empty or not-supplied header + if not hdr: + return result + + pos = 0 + while pos < len(hdr): + name = _re_token.match(hdr, pos) + if not name: + raise AcceptParseError() + a = result.item_class(string.lower(name.group(1))) + pos = name.end() + while 1: + # are we looking at a parameter? + match = _re_param.match(hdr, pos) + if not match: + break + param = match.group(1) + pos = match.end() + + # split up the pieces of the parameter + match = _re_split_param.match(param) + if not match: + # the "=" was probably missing + continue + + pname = string.lower(match.group(1)) + if pname == 'q' or pname == 'qs': + try: + a.quality = float(match.group(2)) + except ValueError: + # bad float literal + pass + elif pname == 'level': + try: + a.level = float(match.group(2)) + except ValueError: + # bad float literal + pass + elif pname == 'charset': + a.charset = string.lower(match.group(2)) + + result.append(a) + if hdr[pos:pos+1] == ',': + pos = pos + 1 + + return result + +class _AcceptItem: + def __init__(self, name): + self.name = name + self.quality = 1.0 + self.level = 0.0 + self.charset = '' + + def __str__(self): + s = self.name + if self.quality != 1.0: + s = '%s;q=%.3f' % (s, self.quality) + if self.level != 0.0: + s = '%s;level=%.3f' % (s, self.level) + if self.charset: + s = '%s;charset=%s' % (s, self.charset) + return s + +class _LanguageRange(_AcceptItem): + def matches(self, tag): + "Match the tag against self. Returns the qvalue, or None if non-matching." + if tag == self.name: + return self.quality + + # are we a prefix of the available language-tag + name = self.name + '-' + if tag[:len(name)] == name: + return self.quality + return None + +class _LanguageSelector: + """Instances select an available language based on the user's request. + + Languages found in the user's request are added to this object with the + append() method (they should be instances of _LanguageRange). After the + languages have been added, then the caller can use select_from() to + determine which user-request language(s) best matches the set of + available languages. + + Strictly speaking, this class is pretty close for more than just + language matching. It has been implemented to enable q-value based + matching between requests and availability. Some minor tweaks may be + necessary, but simply using a new 'item_class' should be sufficient + to allow the _parse() function to construct a selector which holds + the appropriate item implementations (e.g. _LanguageRange is the + concrete _AcceptItem class that handles matching of language tags). + """ + + item_class = _LanguageRange + + def __init__(self): + self.requested = [ ] + + def select_from(self, avail): + """Select one of the available choices based on the request. + + Note: if there isn't a match, then the first available choice is + considered the default. Also, if a number of matches are equally + relevant, then the first-requested will be used. + + avail is a list of language-tag strings of available languages + """ + + # tuples of (qvalue, language-tag) + matches = [ ] + + # try matching all pairs of desired vs available, recording the + # resulting qvalues. we also need to record the longest language-range + # that matches since the most specific range "wins" + for tag in avail: + longest = 0 + final = 0.0 + + # check this tag against the requests from the user + for want in self.requested: + qvalue = want.matches(tag) + #print 'have %s. want %s. qvalue=%s' % (tag, want.name, qvalue) + if qvalue is not None and len(want.name) > longest: + # we have a match and it is longer than any we may have had. + # the final qvalue should be from this tag. + final = qvalue + longest = len(want.name) + + # a non-zero qvalue is a potential match + if final: + matches.append((final, tag)) + + # if there are no matches, then return the default language tag + if not matches: + return avail[0] + + # get the highest qvalue and its corresponding tag + matches.sort() + qvalue, tag = matches[-1] + + # if the qvalue is zero, then we have no valid matches. return the + # default language tag. + if not qvalue: + return avail[0] + + # if there are two or more matches, and the second-highest has a + # qvalue equal to the best, then we have multiple "best" options. + # select the one that occurs first in self.requested + if len(matches) >= 2 and matches[-2][0] == qvalue: + # remove non-best matches + while matches[0][0] != qvalue: + del matches[0] + #print "non-deterministic choice", matches + + # sequence through self.requested, in order + for want in self.requested: + # try to find this one in our best matches + for qvalue, tag in matches: + if want.matches(tag): + # this requested item is one of the "best" options + ### note: this request item could match *other* "best" options, + ### so returning *this* one is rather non-deterministic. + ### theoretically, we could go further here, and do another + ### search based on the ordering in 'avail'. however, note + ### that this generally means that we are picking from multiple + ### *SUB* languages, so I'm all right with the non-determinism + ### at this point. stupid client should send a qvalue if they + ### want to refine. + return tag + + # NOTREACHED + + # return the best match + return tag + + def append(self, item): + self.requested.append(item) + +class AcceptParseError(Exception): + pass + +def _test(): + s = language('en') + assert s.select_from(['en']) == 'en' + assert s.select_from(['en', 'de']) == 'en' + assert s.select_from(['de', 'en']) == 'en' + + # Netscape 4.x and early version of Mozilla may not send a q value + s = language('en, ja') + assert s.select_from(['en', 'ja']) == 'en' + + s = language('fr, de;q=0.9, en-gb;q=0.7, en;q=0.6, en-gb-foo;q=0.8') + assert s.select_from(['en']) == 'en' + assert s.select_from(['en-gb-foo']) == 'en-gb-foo' + assert s.select_from(['de', 'fr']) == 'fr' + assert s.select_from(['de', 'en-gb']) == 'de' + assert s.select_from(['en-gb', 'en-gb-foo']) == 'en-gb-foo' + assert s.select_from(['en-bar']) == 'en-bar' + assert s.select_from(['en-gb-bar', 'en-gb-foo']) == 'en-gb-foo' + + # non-deterministic. en-gb;q=0.7 matches both avail tags. + #assert s.select_from(['en-gb-bar', 'en-gb']) == 'en-gb' diff --git a/lib/blame.py b/lib/blame.py new file mode 100644 index 00000000..ad9b5038 --- /dev/null +++ b/lib/blame.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 2000 Curt Hagenlocher +# +# By using this file, you agree to the terms and conditions set forth in +# 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/ +# +# ----------------------------------------------------------------------- +# +# blame.py: Annotate each line of a CVS file with its author, +# revision #, date, etc. +# +# ----------------------------------------------------------------------- +# +# This file is based on the cvsblame.pl portion of the Bonsai CVS tool, +# developed by Steve Lamm for Netscape Communications Corporation. More +# information about Bonsai can be found at +# http://www.mozilla.org/bonsai.html +# +# cvsblame.pl, in turn, was based on Scott Furman's cvsblame script +# +# ----------------------------------------------------------------------- + +import sys +import string +import os +import re +import time +import math +import cgi +import vclib + + +re_includes = re.compile('\\#(\\s*)include(\\s*)"(.*?)"') + +def link_includes(text, repos, path_parts, include_url): + match = re_includes.match(text) + if match: + incfile = match.group(3) + include_path_parts = path_parts[:-1] + 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. + return text + include_path_parts.pop() + elif part and part != ".": + include_path_parts.append(part) + + include_path = None + try: + if repos.itemtype(include_path_parts, None) == vclib.FILE: + include_path = string.join(include_path_parts, '/') + except vclib.ItemNotFound: + pass + + if include_path: + return '#%sinclude%s"%s"' % \ + (match.group(1), match.group(2), + string.replace(include_url, '/WHERE/', include_path), incfile) + + return text + + +class HTMLBlameSource: + """Wrapper around a the object by the vclib.annotate() which does + HTML escaping, diff URL generation, and #include linking.""" + def __init__(self, repos, path_parts, diff_url, include_url, opt_rev=None): + self.repos = repos + 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) + + def __getitem__(self, idx): + item = self.annotation.__getitem__(idx) + diff_url = None + if item.prev_rev: + diff_url = '%sr1=%s&r2=%s' % (self.diff_url, item.prev_rev, item.rev) + thisline = link_includes(cgi.escape(item.text), self.repos, + self.path_parts, self.include_url) + return _item(text=thisline, line_number=item.line_number, + rev=item.rev, prev_rev=item.prev_rev, + diff_url=diff_url, date=item.date, author=item.author) + + +def blame(repos, path_parts, diff_url, include_url, opt_rev=None): + source = HTMLBlameSource(repos, path_parts, diff_url, include_url, opt_rev) + 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)) + + line = 0 + old_revision = 0 + row_color = 'ffffff' + rev_count = 0 + + align = ' style="text-align: %s;"' + + sys.stdout.write('\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 + + if old_revision != revision and line != 1: + if row_color == 'ffffff': + row_color = 'e7e7e7' + else: + row_color = 'ffffff' + + sys.stdout.write('' % (line, row_color)) + sys.stdout.write('%d' % (align % 'right', line)) + + if old_revision != revision or rev_count > 20: + sys.stdout.write('%s' % (align % 'right', author or ' ')) + sys.stdout.write('%s' % (align % 'left', revision)) + old_revision = revision + rev_count = 0 + else: + sys.stdout.write('') + rev_count = rev_count + 1 + + sys.stdout.write('%s\n' % (align % 'left', string.rstrip(thisline) or ' ')) + sys.stdout.write('
  
\n') + + +def main(): + import sys + if len(sys.argv) != 3: + print 'USAGE: %s cvsroot rcs-file' % sys.argv[0] + sys.exit(1) + make_html(sys.argv[1], sys.argv[2]) + +if __name__ == '__main__': + main() diff --git a/lib/compat.py b/lib/compat.py new file mode 100644 index 00000000..3b220f8d --- /dev/null +++ b/lib/compat.py @@ -0,0 +1,180 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- +# +# compat.py: compatibility functions for operation across Python 1.5.x to 2.2.x +# +# ----------------------------------------------------------------------- + +import urllib +import string +import time +import calendar +import re +import os +import rfc822 +import tempfile +import errno + +# +# urllib.urlencode() is new to Python 1.5.2 +# +try: + urlencode = urllib.urlencode +except AttributeError: + def urlencode(dict): + "Encode a dictionary as application/x-url-form-encoded." + if not dict: + return '' + quote = urllib.quote_plus + keyvalue = [ ] + for key, value in dict.items(): + keyvalue.append(quote(key) + '=' + quote(str(value))) + return string.join(keyvalue, '&') + +# +# time.strptime() is new to Python 1.5.2 +# +if hasattr(time, 'strptime'): + def cvs_strptime(timestr): + 'Parse a CVS-style date/time value.' + return time.strptime(timestr, '%Y/%m/%d %H:%M:%S')[:-1] + (0,) +else: + _re_rev_date = re.compile('([0-9]{4})/([0-9][0-9])/([0-9][0-9]) ' + '([0-9][0-9]):([0-9][0-9]):([0-9][0-9])') + def cvs_strptime(timestr): + 'Parse a CVS-style date/time value.' + match = _re_rev_date.match(timestr) + if match: + return tuple(map(int, match.groups())) + (0, 1, 0) + else: + raise ValueError('date is not in cvs format') + +# +# os.makedirs() is new to Python 1.5.2 +# +try: + makedirs = os.makedirs +except AttributeError: + def makedirs(path, mode=0777): + head, tail = os.path.split(path) + if head and tail and not os.path.exists(head): + makedirs(head, mode) + os.mkdir(path, mode) + +# +# rfc822.formatdate() is new to Python 1.6 +# +try: + formatdate = rfc822.formatdate +except AttributeError: + def formatdate(timeval): + if timeval is None: + timeval = time.time() + timeval = time.gmtime(timeval) + return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( + ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][timeval[6]], + timeval[2], + ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][timeval[1]-1], + timeval[0], timeval[3], timeval[4], timeval[5]) + +# +# calendar.timegm() is new to Python 2.x and +# calendar.leapdays() was wrong in Python 1.5.2 +# +try: + timegm = calendar.timegm +except AttributeError: + def leapdays(year1, year2): + """Return number of leap years in range [year1, year2). + Assume year1 <= year2.""" + year1 = year1 - 1 + year2 = year2 - 1 + return (year2/4 - year1/4) - (year2/100 - + year1/100) + (year2/400 - year1/400) + + EPOCH = 1970 + def timegm(tuple): + """Unrelated but handy function to calculate Unix timestamp from GMT.""" + year, month, day, hour, minute, second = tuple[:6] + # assert year >= EPOCH + # assert 1 <= month <= 12 + days = 365*(year-EPOCH) + leapdays(EPOCH, year) + for i in range(1, month): + days = days + calendar.mdays[i] + if month > 2 and calendar.isleap(year): + days = days + 1 + days = days + day - 1 + hours = days*24 + hour + minutes = hours*60 + minute + seconds = minutes*60 + second + return seconds + +# +# tempfile.mkdtemp() is new to Python 2.3 +# +try: + mkdtemp = tempfile.mkdtemp +except AttributeError: + def mkdtemp(suffix="", prefix="tmp", dir=None): + # mktemp() only took a single suffix argument until Python 2.3. + # We'll do the best we can. + oldtmpdir = os.environ.get('TMPDIR') + try: + for i in range(10): + if dir: + os.environ['TMPDIR'] = dir + dir = tempfile.mktemp(suffix) + if prefix: + parent, base = os.path.split(dir) + dir = os.path.join(parent, prefix + base) + try: + os.mkdir(dir, 0700) + return dir + except OSError, e: + if e.errno == errno.EEXIST: + continue # try again + raise + finally: + if oldtmpdir: + os.environ['TMPDIR'] = oldtmpdir + elif os.environ.has_key('TMPDIR'): + del(os.environ['TMPDIR']) + + raise IOError, (errno.EEXIST, "No usable temporary directory name found") + +# +# the following stuff is *ONLY* needed for standalone.py. +# For that reason I've encapsulated it into a function. +# + +def for_standalone(): + import SocketServer + if not hasattr(SocketServer.TCPServer, "close_request"): + # + # method close_request() was missing until Python 2.1 + # + class TCPServer(SocketServer.TCPServer): + def process_request(self, request, client_address): + """Call finish_request. + + Overridden by ForkingMixIn and ThreadingMixIn. + + """ + self.finish_request(request, client_address) + self.close_request(request) + + def close_request(self, request): + """Called to clean up an individual request.""" + request.close() + + SocketServer.TCPServer = TCPServer diff --git a/lib/compat_difflib.py b/lib/compat_difflib.py new file mode 100755 index 00000000..9dfecf40 --- /dev/null +++ b/lib/compat_difflib.py @@ -0,0 +1,786 @@ +#! /usr/bin/env python +# Backported to Python 1.5.2 for the ViewCVS project by pf@artcom-gmbh.de +# 24-Dec-2001, original version "stolen" from Python-2.1.1 +""" +Module difflib -- helpers for computing deltas between objects. + +Function get_close_matches(word, possibilities, n=3, cutoff=0.6): + + Use SequenceMatcher to return list of the best "good enough" matches. + + word is a sequence for which close matches are desired (typically a + string). + + possibilities is a list of sequences against which to match word + (typically a list of strings). + + Optional arg n (default 3) is the maximum number of close matches to + return. n must be > 0. + + Optional arg cutoff (default 0.6) is a float in [0, 1]. Possibilities + that don't score at least that similar to word are ignored. + + The best (no more than n) matches among the possibilities are returned + in a list, sorted by similarity score, most similar first. + + >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"]) + ['apple', 'ape'] + >>> import keyword + >>> get_close_matches("wheel", keyword.kwlist) + ['while'] + >>> get_close_matches("apple", keyword.kwlist) + [] + >>> get_close_matches("accept", keyword.kwlist) + ['except'] + +Class SequenceMatcher + +SequenceMatcher is a flexible class for comparing pairs of sequences of any +type, so long as the sequence elements are hashable. The basic algorithm +predates, and is a little fancier than, an algorithm published in the late +1980's by Ratcliff and Obershelp under the hyperbolic name "gestalt pattern +matching". The basic idea is to find the longest contiguous matching +subsequence that contains no "junk" elements (R-O doesn't address junk). +The same idea is then applied recursively to the pieces of the sequences to +the left and to the right of the matching subsequence. This does not yield +minimal edit sequences, but does tend to yield matches that "look right" +to people. + +Example, comparing two strings, and considering blanks to be "junk": + +>>> s = SequenceMatcher(lambda x: x == " ", +... "private Thread currentThread;", +... "private volatile Thread currentThread;") +>>> + +.ratio() returns a float in [0, 1], measuring the "similarity" of the +sequences. As a rule of thumb, a .ratio() value over 0.6 means the +sequences are close matches: + +>>> print round(s.ratio(), 3) +0.866 +>>> + +If you're only interested in where the sequences match, +.get_matching_blocks() is handy: + +>>> for block in s.get_matching_blocks(): +... print "a[%d] and b[%d] match for %d elements" % block +a[0] and b[0] match for 8 elements +a[8] and b[17] match for 6 elements +a[14] and b[23] match for 15 elements +a[29] and b[38] match for 0 elements + +Note that the last tuple returned by .get_matching_blocks() is always a +dummy, (len(a), len(b), 0), and this is the only case in which the last +tuple element (number of elements matched) is 0. + +If you want to know how to change the first sequence into the second, use +.get_opcodes(): + +>>> for opcode in s.get_opcodes(): +... print "%6s a[%d:%d] b[%d:%d]" % opcode + equal a[0:8] b[0:8] +insert a[8:8] b[8:17] + equal a[8:14] b[17:23] + equal a[14:29] b[23:38] + +See Tools/scripts/ndiff.py for a fancy human-friendly file differencer, +which uses SequenceMatcher both to view files as sequences of lines, and +lines as sequences of characters. + +See also function get_close_matches() in this module, which shows how +simple code building on SequenceMatcher can be used to do useful work. + +Timing: Basic R-O is cubic time worst case and quadratic time expected +case. SequenceMatcher is quadratic time for the worst case and has +expected-case behavior dependent in a complicated way on how many +elements the sequences have in common; best case time is linear. + +SequenceMatcher methods: + +__init__(isjunk=None, a='', b='') + Construct a SequenceMatcher. + + Optional arg isjunk is None (the default), or a one-argument function + that takes a sequence element and returns true iff the element is junk. + None is equivalent to passing "lambda x: 0", i.e. no elements are + considered to be junk. For example, pass + lambda x: x in " \\t" + if you're comparing lines as sequences of characters, and don't want to + synch up on blanks or hard tabs. + + Optional arg a is the first of two sequences to be compared. By + default, an empty string. The elements of a must be hashable. + + Optional arg b is the second of two sequences to be compared. By + default, an empty string. The elements of b must be hashable. + +set_seqs(a, b) + Set the two sequences to be compared. + + >>> s = SequenceMatcher() + >>> s.set_seqs("abcd", "bcde") + >>> s.ratio() + 0.75 + +set_seq1(a) + Set the first sequence to be compared. + + The second sequence to be compared is not changed. + + >>> s = SequenceMatcher(None, "abcd", "bcde") + >>> s.ratio() + 0.75 + >>> s.set_seq1("bcde") + >>> s.ratio() + 1.0 + >>> + + SequenceMatcher computes and caches detailed information about the + second sequence, so if you want to compare one sequence S against many + sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for + each of the other sequences. + + See also set_seqs() and set_seq2(). + +set_seq2(b) + Set the second sequence to be compared. + + The first sequence to be compared is not changed. + + >>> s = SequenceMatcher(None, "abcd", "bcde") + >>> s.ratio() + 0.75 + >>> s.set_seq2("abcd") + >>> s.ratio() + 1.0 + >>> + + SequenceMatcher computes and caches detailed information about the + second sequence, so if you want to compare one sequence S against many + sequences, use .set_seq2(S) once and call .set_seq1(x) repeatedly for + each of the other sequences. + + See also set_seqs() and set_seq1(). + +find_longest_match(alo, ahi, blo, bhi) + Find longest matching block in a[alo:ahi] and b[blo:bhi]. + + If isjunk is not defined: + + Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where + alo <= i <= i+k <= ahi + blo <= j <= j+k <= bhi + and for all (i',j',k') meeting those conditions, + k >= k' + i <= i' + and if i == i', j <= j' + + In other words, of all maximal matching blocks, return one that starts + earliest in a, and of all those maximal matching blocks that start + earliest in a, return the one that starts earliest in b. + + >>> s = SequenceMatcher(None, " abcd", "abcd abcd") + >>> s.find_longest_match(0, 5, 0, 9) + (0, 4, 5) + + If isjunk is defined, first the longest matching block is determined as + above, but with the additional restriction that no junk element appears + in the block. Then that block is extended as far as possible by + matching (only) junk elements on both sides. So the resulting block + never matches on junk except as identical junk happens to be adjacent + to an "interesting" match. + + Here's the same example as before, but considering blanks to be junk. + That prevents " abcd" from matching the " abcd" at the tail end of the + second sequence directly. Instead only the "abcd" can match, and + matches the leftmost "abcd" in the second sequence: + + >>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd") + >>> s.find_longest_match(0, 5, 0, 9) + (1, 0, 4) + + If no blocks match, return (alo, blo, 0). + + >>> s = SequenceMatcher(None, "ab", "c") + >>> s.find_longest_match(0, 2, 0, 1) + (0, 0, 0) + +get_matching_blocks() + Return list of triples describing matching subsequences. + + Each triple is of the form (i, j, n), and means that + a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in i + and in j. + + The last triple is a dummy, (len(a), len(b), 0), and is the only triple + with n==0. + + >>> s = SequenceMatcher(None, "abxcd", "abcd") + >>> s.get_matching_blocks() + [(0, 0, 2), (3, 2, 2), (5, 4, 0)] + +get_opcodes() + Return list of 5-tuples describing how to turn a into b. + + Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple has + i1 == j1 == 0, and remaining tuples have i1 == the i2 from the tuple + preceding it, and likewise for j1 == the previous j2. + + The tags are strings, with these meanings: + + 'replace': a[i1:i2] should be replaced by b[j1:j2] + 'delete': a[i1:i2] should be deleted. + Note that j1==j2 in this case. + 'insert': b[j1:j2] should be inserted at a[i1:i1]. + Note that i1==i2 in this case. + 'equal': a[i1:i2] == b[j1:j2] + + >>> a = "qabxcd" + >>> b = "abycdf" + >>> s = SequenceMatcher(None, a, b) + >>> for tag, i1, i2, j1, j2 in s.get_opcodes(): + ... print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % + ... (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2])) + delete a[0:1] (q) b[0:0] () + equal a[1:3] (ab) b[0:2] (ab) + replace a[3:4] (x) b[2:3] (y) + equal a[4:6] (cd) b[3:5] (cd) + insert a[6:6] () b[5:6] (f) + +ratio() + Return a measure of the sequences' similarity (float in [0,1]). + + Where T is the total number of elements in both sequences, and M is the + number of matches, this is 2,0*M / T. Note that this is 1 if the + sequences are identical, and 0 if they have nothing in common. + + .ratio() is expensive to compute if you haven't already computed + .get_matching_blocks() or .get_opcodes(), in which case you may want to + try .quick_ratio() or .real_quick_ratio() first to get an upper bound. + + >>> s = SequenceMatcher(None, "abcd", "bcde") + >>> s.ratio() + 0.75 + >>> s.quick_ratio() + 0.75 + >>> s.real_quick_ratio() + 1.0 + +quick_ratio() + Return an upper bound on .ratio() relatively quickly. + + This isn't defined beyond that it is an upper bound on .ratio(), and + is faster to compute. + +real_quick_ratio(): + Return an upper bound on ratio() very quickly. + + This isn't defined beyond that it is an upper bound on .ratio(), and + is faster to compute than either .ratio() or .quick_ratio(). +""" + +TRACE = 0 + +class SequenceMatcher: + def __init__(self, isjunk=None, a='', b=''): + """Construct a SequenceMatcher. + + Optional arg isjunk is None (the default), or a one-argument + function that takes a sequence element and returns true iff the + element is junk. None is equivalent to passing "lambda x: 0", i.e. + no elements are considered to be junk. For example, pass + lambda x: x in " \\t" + if you're comparing lines as sequences of characters, and don't + want to synch up on blanks or hard tabs. + + Optional arg a is the first of two sequences to be compared. By + default, an empty string. The elements of a must be hashable. See + also .set_seqs() and .set_seq1(). + + Optional arg b is the second of two sequences to be compared. By + default, an empty string. The elements of b must be hashable. See + also .set_seqs() and .set_seq2(). + """ + + # Members: + # a + # first sequence + # b + # second sequence; differences are computed as "what do + # we need to do to 'a' to change it into 'b'?" + # b2j + # for x in b, b2j[x] is a list of the indices (into b) + # at which x appears; junk elements do not appear + # b2jhas + # b2j.has_key + # fullbcount + # for x in b, fullbcount[x] == the number of times x + # appears in b; only materialized if really needed (used + # only for computing quick_ratio()) + # matching_blocks + # a list of (i, j, k) triples, where a[i:i+k] == b[j:j+k]; + # ascending & non-overlapping in i and in j; terminated by + # a dummy (len(a), len(b), 0) sentinel + # opcodes + # a list of (tag, i1, i2, j1, j2) tuples, where tag is + # one of + # 'replace' a[i1:i2] should be replaced by b[j1:j2] + # 'delete' a[i1:i2] should be deleted + # 'insert' b[j1:j2] should be inserted + # 'equal' a[i1:i2] == b[j1:j2] + # isjunk + # a user-supplied function taking a sequence element and + # returning true iff the element is "junk" -- this has + # subtle but helpful effects on the algorithm, which I'll + # get around to writing up someday <0.9 wink>. + # DON'T USE! Only __chain_b uses this. Use isbjunk. + # isbjunk + # for x in b, isbjunk(x) == isjunk(x) but much faster; + # it's really the has_key method of a hidden dict. + # DOES NOT WORK for x in a! + + self.isjunk = isjunk + self.a = self.b = None + self.set_seqs(a, b) + + def set_seqs(self, a, b): + """Set the two sequences to be compared. + + >>> s = SequenceMatcher() + >>> s.set_seqs("abcd", "bcde") + >>> s.ratio() + 0.75 + """ + + self.set_seq1(a) + self.set_seq2(b) + + def set_seq1(self, a): + """Set the first sequence to be compared. + + The second sequence to be compared is not changed. + + >>> s = SequenceMatcher(None, "abcd", "bcde") + >>> s.ratio() + 0.75 + >>> s.set_seq1("bcde") + >>> s.ratio() + 1.0 + >>> + + SequenceMatcher computes and caches detailed information about the + second sequence, so if you want to compare one sequence S against + many sequences, use .set_seq2(S) once and call .set_seq1(x) + repeatedly for each of the other sequences. + + See also set_seqs() and set_seq2(). + """ + + if a is self.a: + return + self.a = a + self.matching_blocks = self.opcodes = None + + def set_seq2(self, b): + """Set the second sequence to be compared. + + The first sequence to be compared is not changed. + + >>> s = SequenceMatcher(None, "abcd", "bcde") + >>> s.ratio() + 0.75 + >>> s.set_seq2("abcd") + >>> s.ratio() + 1.0 + >>> + + SequenceMatcher computes and caches detailed information about the + second sequence, so if you want to compare one sequence S against + many sequences, use .set_seq2(S) once and call .set_seq1(x) + repeatedly for each of the other sequences. + + See also set_seqs() and set_seq1(). + """ + + if b is self.b: + return + self.b = b + self.matching_blocks = self.opcodes = None + self.fullbcount = None + self.__chain_b() + + # For each element x in b, set b2j[x] to a list of the indices in + # b where x appears; the indices are in increasing order; note that + # the number of times x appears in b is len(b2j[x]) ... + # when self.isjunk is defined, junk elements don't show up in this + # map at all, which stops the central find_longest_match method + # from starting any matching block at a junk element ... + # also creates the fast isbjunk function ... + # note that this is only called when b changes; so for cross-product + # kinds of matches, it's best to call set_seq2 once, then set_seq1 + # repeatedly + + def __chain_b(self): + # Because isjunk is a user-defined (not C) function, and we test + # for junk a LOT, it's important to minimize the number of calls. + # Before the tricks described here, __chain_b was by far the most + # time-consuming routine in the whole module! If anyone sees + # Jim Roskind, thank him again for profile.py -- I never would + # have guessed that. + # The first trick is to build b2j ignoring the possibility + # of junk. I.e., we don't call isjunk at all yet. Throwing + # out the junk later is much cheaper than building b2j "right" + # from the start. + b = self.b + self.b2j = b2j = {} + self.b2jhas = b2jhas = b2j.has_key + for i in xrange(len(b)): + elt = b[i] + if b2jhas(elt): + b2j[elt].append(i) + else: + b2j[elt] = [i] + + # Now b2j.keys() contains elements uniquely, and especially when + # the sequence is a string, that's usually a good deal smaller + # than len(string). The difference is the number of isjunk calls + # saved. + isjunk, junkdict = self.isjunk, {} + if isjunk: + for elt in b2j.keys(): + if isjunk(elt): + junkdict[elt] = 1 # value irrelevant; it's a set + del b2j[elt] + + # Now for x in b, isjunk(x) == junkdict.has_key(x), but the + # latter is much faster. Note too that while there may be a + # lot of junk in the sequence, the number of *unique* junk + # elements is probably small. So the memory burden of keeping + # this dict alive is likely trivial compared to the size of b2j. + self.isbjunk = junkdict.has_key + + def find_longest_match(self, alo, ahi, blo, bhi): + """Find longest matching block in a[alo:ahi] and b[blo:bhi]. + + If isjunk is not defined: + + Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where + alo <= i <= i+k <= ahi + blo <= j <= j+k <= bhi + and for all (i',j',k') meeting those conditions, + k >= k' + i <= i' + and if i == i', j <= j' + + In other words, of all maximal matching blocks, return one that + starts earliest in a, and of all those maximal matching blocks that + start earliest in a, return the one that starts earliest in b. + + >>> s = SequenceMatcher(None, " abcd", "abcd abcd") + >>> s.find_longest_match(0, 5, 0, 9) + (0, 4, 5) + + If isjunk is defined, first the longest matching block is + determined as above, but with the additional restriction that no + junk element appears in the block. Then that block is extended as + far as possible by matching (only) junk elements on both sides. So + the resulting block never matches on junk except as identical junk + happens to be adjacent to an "interesting" match. + + Here's the same example as before, but considering blanks to be + junk. That prevents " abcd" from matching the " abcd" at the tail + end of the second sequence directly. Instead only the "abcd" can + match, and matches the leftmost "abcd" in the second sequence: + + >>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd") + >>> s.find_longest_match(0, 5, 0, 9) + (1, 0, 4) + + If no blocks match, return (alo, blo, 0). + + >>> s = SequenceMatcher(None, "ab", "c") + >>> s.find_longest_match(0, 2, 0, 1) + (0, 0, 0) + """ + + # CAUTION: stripping common prefix or suffix would be incorrect. + # E.g., + # ab + # acab + # Longest matching block is "ab", but if common prefix is + # stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + # strip, so ends up claiming that ab is changed to acab by + # inserting "ca" in the middle. That's minimal but unintuitive: + # "it's obvious" that someone inserted "ac" at the front. + # Windiff ends up at the same place as diff, but by pairing up + # the unique 'b's and then matching the first two 'a's. + + a, b, b2j, isbjunk = self.a, self.b, self.b2j, self.isbjunk + besti, bestj, bestsize = alo, blo, 0 + # find longest junk-free match + # during an iteration of the loop, j2len[j] = length of longest + # junk-free match ending with a[i-1] and b[j] + j2len = {} + nothing = [] + for i in xrange(alo, ahi): + # look at all instances of a[i] in b; note that because + # b2j has no junk keys, the loop is skipped if a[i] is junk + j2lenget = j2len.get + newj2len = {} + for j in b2j.get(a[i], nothing): + # a[i] matches b[j] + if j < blo: + continue + if j >= bhi: + break + k = newj2len[j] = j2lenget(j-1, 0) + 1 + if k > bestsize: + besti, bestj, bestsize = i-k+1, j-k+1, k + j2len = newj2len + + # Now that we have a wholly interesting match (albeit possibly + # empty!), we may as well suck up the matching junk on each + # side of it too. Can't think of a good reason not to, and it + # saves post-processing the (possibly considerable) expense of + # figuring out what to do with it. In the case of an empty + # interesting match, this is clearly the right thing to do, + # because no other kind of match is possible in the regions. + while besti > alo and bestj > blo and \ + isbjunk(b[bestj-1]) and \ + a[besti-1] == b[bestj-1]: + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + while besti+bestsize < ahi and bestj+bestsize < bhi and \ + isbjunk(b[bestj+bestsize]) and \ + a[besti+bestsize] == b[bestj+bestsize]: + bestsize = bestsize + 1 + + if TRACE: + print "get_matching_blocks", alo, ahi, blo, bhi + print " returns", besti, bestj, bestsize + return besti, bestj, bestsize + + def get_matching_blocks(self): + """Return list of triples describing matching subsequences. + + Each triple is of the form (i, j, n), and means that + a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in + i and in j. + + The last triple is a dummy, (len(a), len(b), 0), and is the only + triple with n==0. + + >>> s = SequenceMatcher(None, "abxcd", "abcd") + >>> s.get_matching_blocks() + [(0, 0, 2), (3, 2, 2), (5, 4, 0)] + """ + + if self.matching_blocks is not None: + return self.matching_blocks + self.matching_blocks = [] + la, lb = len(self.a), len(self.b) + self.__helper(0, la, 0, lb, self.matching_blocks) + self.matching_blocks.append( (la, lb, 0) ) + if TRACE: + print '*** matching blocks', self.matching_blocks + return self.matching_blocks + + # builds list of matching blocks covering a[alo:ahi] and + # b[blo:bhi], appending them in increasing order to answer + + def __helper(self, alo, ahi, blo, bhi, answer): + i, j, k = x = self.find_longest_match(alo, ahi, blo, bhi) + # a[alo:i] vs b[blo:j] unknown + # a[i:i+k] same as b[j:j+k] + # a[i+k:ahi] vs b[j+k:bhi] unknown + if k: + if alo < i and blo < j: + self.__helper(alo, i, blo, j, answer) + answer.append(x) + if i+k < ahi and j+k < bhi: + self.__helper(i+k, ahi, j+k, bhi, answer) + + def get_opcodes(self): + """Return list of 5-tuples describing how to turn a into b. + + Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple + has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the + tuple preceding it, and likewise for j1 == the previous j2. + + The tags are strings, with these meanings: + + 'replace': a[i1:i2] should be replaced by b[j1:j2] + 'delete': a[i1:i2] should be deleted. + Note that j1==j2 in this case. + 'insert': b[j1:j2] should be inserted at a[i1:i1]. + Note that i1==i2 in this case. + 'equal': a[i1:i2] == b[j1:j2] + + >>> a = "qabxcd" + >>> b = "abycdf" + >>> s = SequenceMatcher(None, a, b) + >>> for tag, i1, i2, j1, j2 in s.get_opcodes(): + ... print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" % + ... (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2])) + delete a[0:1] (q) b[0:0] () + equal a[1:3] (ab) b[0:2] (ab) + replace a[3:4] (x) b[2:3] (y) + equal a[4:6] (cd) b[3:5] (cd) + insert a[6:6] () b[5:6] (f) + """ + + if self.opcodes is not None: + return self.opcodes + i = j = 0 + self.opcodes = answer = [] + for ai, bj, size in self.get_matching_blocks(): + # invariant: we've pumped out correct diffs to change + # a[:i] into b[:j], and the next matching block is + # a[ai:ai+size] == b[bj:bj+size]. So we need to pump + # out a diff to change a[i:ai] into b[j:bj], pump out + # the matching block, and move (i,j) beyond the match + tag = '' + if i < ai and j < bj: + tag = 'replace' + elif i < ai: + tag = 'delete' + elif j < bj: + tag = 'insert' + if tag: + answer.append( (tag, i, ai, j, bj) ) + i, j = ai+size, bj+size + # the list of matching blocks is terminated by a + # sentinel with size 0 + if size: + answer.append( ('equal', ai, i, bj, j) ) + return answer + + def ratio(self): + """Return a measure of the sequences' similarity (float in [0,1]). + + Where T is the total number of elements in both sequences, and + M is the number of matches, this is 2,0*M / T. + Note that this is 1 if the sequences are identical, and 0 if + they have nothing in common. + + .ratio() is expensive to compute if you haven't already computed + .get_matching_blocks() or .get_opcodes(), in which case you may + want to try .quick_ratio() or .real_quick_ratio() first to get an + upper bound. + + >>> s = SequenceMatcher(None, "abcd", "bcde") + >>> s.ratio() + 0.75 + >>> s.quick_ratio() + 0.75 + >>> s.real_quick_ratio() + 1.0 + """ + + matches = reduce(lambda sum, triple: sum + triple[-1], + self.get_matching_blocks(), 0) + return 2.0 * matches / (len(self.a) + len(self.b)) + + def quick_ratio(self): + """Return an upper bound on ratio() relatively quickly. + + This isn't defined beyond that it is an upper bound on .ratio(), and + is faster to compute. + """ + + # viewing a and b as multisets, set matches to the cardinality + # of their intersection; this counts the number of matches + # without regard to order, so is clearly an upper bound + if self.fullbcount is None: + self.fullbcount = fullbcount = {} + for elt in self.b: + fullbcount[elt] = fullbcount.get(elt, 0) + 1 + fullbcount = self.fullbcount + # avail[x] is the number of times x appears in 'b' less the + # number of times we've seen it in 'a' so far ... kinda + avail = {} + availhas, matches = avail.has_key, 0 + for elt in self.a: + if availhas(elt): + numb = avail[elt] + else: + numb = fullbcount.get(elt, 0) + avail[elt] = numb - 1 + if numb > 0: + matches = matches + 1 + return 2.0 * matches / (len(self.a) + len(self.b)) + + def real_quick_ratio(self): + """Return an upper bound on ratio() very quickly. + + This isn't defined beyond that it is an upper bound on .ratio(), and + is faster to compute than either .ratio() or .quick_ratio(). + """ + + la, lb = len(self.a), len(self.b) + # can't have more matches than the number of elements in the + # shorter sequence + return 2.0 * min(la, lb) / (la + lb) + +def get_close_matches(word, possibilities, n=3, cutoff=0.6): + """Use SequenceMatcher to return list of the best "good enough" matches. + + word is a sequence for which close matches are desired (typically a + string). + + possibilities is a list of sequences against which to match word + (typically a list of strings). + + Optional arg n (default 3) is the maximum number of close matches to + return. n must be > 0. + + Optional arg cutoff (default 0.6) is a float in [0, 1]. Possibilities + that don't score at least that similar to word are ignored. + + The best (no more than n) matches among the possibilities are returned + in a list, sorted by similarity score, most similar first. + + >>> get_close_matches("appel", ["ape", "apple", "peach", "puppy"]) + ['apple', 'ape'] + >>> import keyword + >>> get_close_matches("wheel", keyword.kwlist) + ['while'] + >>> get_close_matches("apple", keyword.kwlist) + [] + >>> get_close_matches("accept", keyword.kwlist) + ['except'] + """ + + if not n > 0: + raise ValueError("n must be > 0: " + `n`) + if not 0.0 <= cutoff <= 1.0: + raise ValueError("cutoff must be in [0.0, 1.0]: " + `cutoff`) + result = [] + s = SequenceMatcher() + s.set_seq2(word) + for x in possibilities: + s.set_seq1(x) + if s.real_quick_ratio() >= cutoff and \ + s.quick_ratio() >= cutoff and \ + s.ratio() >= cutoff: + result.append((s.ratio(), x)) + # Sort by score. + result.sort() + # Retain only the best n. + result = result[-n:] + # Move best-scorer to head of list. + result.reverse() + # Strip scores. + # Python 2.x list comprehensions: return [x for score, x in result] + return_result = [] + for score, x in result: + return_result.append(x) + return return_result + +def _test(): + import doctest, difflib + return doctest.testmod(difflib) + +if __name__ == "__main__": + _test() diff --git a/lib/compat_ndiff.py b/lib/compat_ndiff.py new file mode 100644 index 00000000..543354ec --- /dev/null +++ b/lib/compat_ndiff.py @@ -0,0 +1,346 @@ +#! /usr/bin/env python + +# Module ndiff version 1.6.0 +# Released to the public domain 08-Dec-2000, +# by Tim Peters (tim.one@home.com). + +# Backported to Python 1.5.2 for ViewCVS by pf@artcom-gmbh.de, 24-Dec-2001 + +# Provided as-is; use at your own risk; no warranty; no promises; enjoy! + +"""ndiff [-q] file1 file2 + or +ndiff (-r1 | -r2) < ndiff_output > file1_or_file2 + +Print a human-friendly file difference report to stdout. Both inter- +and intra-line differences are noted. In the second form, recreate file1 +(-r1) or file2 (-r2) on stdout, from an ndiff report on stdin. + +In the first form, if -q ("quiet") is not specified, the first two lines +of output are + +-: file1 ++: file2 + +Each remaining line begins with a two-letter code: + + "- " line unique to file1 + "+ " line unique to file2 + " " line common to both files + "? " line not present in either input file + +Lines beginning with "? " attempt to guide the eye to intraline +differences, and were not present in either input file. These lines can be +confusing if the source files contain tab characters. + +The first file can be recovered by retaining only lines that begin with +" " or "- ", and deleting those 2-character prefixes; use ndiff with -r1. + +The second file can be recovered similarly, but by retaining only " " and +"+ " lines; use ndiff with -r2; or, on Unix, the second file can be +recovered by piping the output through + + sed -n '/^[+ ] /s/^..//p' + +See module comments for details and programmatic interface. +""" + +__version__ = 1, 6, 1 + +# SequenceMatcher tries to compute a "human-friendly diff" between +# two sequences (chiefly picturing a file as a sequence of lines, +# and a line as a sequence of characters, here). Unlike e.g. UNIX(tm) +# diff, the fundamental notion is the longest *contiguous* & junk-free +# matching subsequence. That's what catches peoples' eyes. The +# Windows(tm) windiff has another interesting notion, pairing up elements +# that appear uniquely in each sequence. That, and the method here, +# appear to yield more intuitive difference reports than does diff. This +# method appears to be the least vulnerable to synching up on blocks +# of "junk lines", though (like blank lines in ordinary text files, +# or maybe "

" lines in HTML files). That may be because this is +# the only method of the 3 that has a *concept* of "junk" . +# +# Note that ndiff makes no claim to produce a *minimal* diff. To the +# contrary, minimal diffs are often counter-intuitive, because they +# synch up anywhere possible, sometimes accidental matches 100 pages +# apart. Restricting synch points to contiguous matches preserves some +# notion of locality, at the occasional cost of producing a longer diff. +# +# With respect to junk, an earlier version of ndiff simply refused to +# *start* a match with a junk element. The result was cases like this: +# before: private Thread currentThread; +# after: private volatile Thread currentThread; +# If you consider whitespace to be junk, the longest contiguous match +# not starting with junk is "e Thread currentThread". So ndiff reported +# that "e volatil" was inserted between the 't' and the 'e' in "private". +# While an accurate view, to people that's absurd. The current version +# looks for matching blocks that are entirely junk-free, then extends the +# longest one of those as far as possible but only with matching junk. +# So now "currentThread" is matched, then extended to suck up the +# preceding blank; then "private" is matched, and extended to suck up the +# following blank; then "Thread" is matched; and finally ndiff reports +# that "volatile " was inserted before "Thread". The only quibble +# remaining is that perhaps it was really the case that " volatile" +# was inserted after "private". I can live with that . +# +# NOTE on junk: the module-level names +# IS_LINE_JUNK +# IS_CHARACTER_JUNK +# can be set to any functions you like. The first one should accept +# a single string argument, and return true iff the string is junk. +# The default is whether the regexp r"\s*#?\s*$" matches (i.e., a +# line without visible characters, except for at most one splat). +# The second should accept a string of length 1 etc. The default is +# whether the character is a blank or tab (note: bad idea to include +# newline in this!). +# +# After setting those, you can call fcompare(f1name, f2name) with the +# names of the files you want to compare. The difference report +# is sent to stdout. Or you can call main(args), passing what would +# have been in sys.argv[1:] had the cmd-line form been used. + +from compat_difflib import SequenceMatcher + +TRACE = 0 + +# define what "junk" means +import re + +def IS_LINE_JUNK(line, pat=re.compile(r"\s*#?\s*$").match): + return pat(line) is not None + +def IS_CHARACTER_JUNK(ch, ws=" \t"): + return ch in ws + +del re + +# meant for dumping lines +def dump(tag, x, lo, hi): + for i in xrange(lo, hi): + print tag, x[i], + +def plain_replace(a, alo, ahi, b, blo, bhi): + assert alo < ahi and blo < bhi + # dump the shorter block first -- reduces the burden on short-term + # memory if the blocks are of very different sizes + if bhi - blo < ahi - alo: + dump('+', b, blo, bhi) + dump('-', a, alo, ahi) + else: + dump('-', a, alo, ahi) + dump('+', b, blo, bhi) + +# When replacing one block of lines with another, this guy searches +# the blocks for *similar* lines; the best-matching pair (if any) is +# used as a synch point, and intraline difference marking is done on +# the similar pair. Lots of work, but often worth it. + +def fancy_replace(a, alo, ahi, b, blo, bhi): + if TRACE: + print '*** fancy_replace', alo, ahi, blo, bhi + dump('>', a, alo, ahi) + dump('<', b, blo, bhi) + + # don't synch up unless the lines have a similarity score of at + # least cutoff; best_ratio tracks the best score seen so far + best_ratio, cutoff = 0.74, 0.75 + cruncher = SequenceMatcher(IS_CHARACTER_JUNK) + eqi, eqj = None, None # 1st indices of equal lines (if any) + + # search for the pair that matches best without being identical + # (identical lines must be junk lines, & we don't want to synch up + # on junk -- unless we have to) + for j in xrange(blo, bhi): + bj = b[j] + cruncher.set_seq2(bj) + for i in xrange(alo, ahi): + ai = a[i] + if ai == bj: + if eqi is None: + eqi, eqj = i, j + continue + cruncher.set_seq1(ai) + # computing similarity is expensive, so use the quick + # upper bounds first -- have seen this speed up messy + # compares by a factor of 3. + # note that ratio() is only expensive to compute the first + # time it's called on a sequence pair; the expensive part + # of the computation is cached by cruncher + if cruncher.real_quick_ratio() > best_ratio and \ + cruncher.quick_ratio() > best_ratio and \ + cruncher.ratio() > best_ratio: + best_ratio, best_i, best_j = cruncher.ratio(), i, j + if best_ratio < cutoff: + # no non-identical "pretty close" pair + if eqi is None: + # no identical pair either -- treat it as a straight replace + plain_replace(a, alo, ahi, b, blo, bhi) + return + # no close pair, but an identical pair -- synch up on that + best_i, best_j, best_ratio = eqi, eqj, 1.0 + else: + # there's a close pair, so forget the identical pair (if any) + eqi = None + + # a[best_i] very similar to b[best_j]; eqi is None iff they're not + # identical + if TRACE: + print '*** best_ratio', best_ratio, best_i, best_j + dump('>', a, best_i, best_i+1) + dump('<', b, best_j, best_j+1) + + # pump out diffs from before the synch point + fancy_helper(a, alo, best_i, b, blo, best_j) + + # do intraline marking on the synch pair + aelt, belt = a[best_i], b[best_j] + if eqi is None: + # pump out a '-', '?', '+', '?' quad for the synched lines + atags = btags = "" + cruncher.set_seqs(aelt, belt) + for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes(): + la, lb = ai2 - ai1, bj2 - bj1 + if tag == 'replace': + atags = atags + '^' * la + btags = btags + '^' * lb + elif tag == 'delete': + atags = atags + '-' * la + elif tag == 'insert': + btags = btags + '+' * lb + elif tag == 'equal': + atags = atags + ' ' * la + btags = btags + ' ' * lb + else: + raise ValueError, 'unknown tag ' + `tag` + printq(aelt, belt, atags, btags) + else: + # the synch pair is identical + print ' ', aelt, + + # pump out diffs from after the synch point + fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi) + +def fancy_helper(a, alo, ahi, b, blo, bhi): + if alo < ahi: + if blo < bhi: + fancy_replace(a, alo, ahi, b, blo, bhi) + else: + dump('-', a, alo, ahi) + elif blo < bhi: + dump('+', b, blo, bhi) + +# Crap to deal with leading tabs in "?" output. Can hurt, but will +# probably help most of the time. + +def printq(aline, bline, atags, btags): + common = min(count_leading(aline, "\t"), + count_leading(bline, "\t")) + common = min(common, count_leading(atags[:common], " ")) + print "-", aline, + if count_leading(atags, " ") < len(atags): + print "?", "\t" * common + atags[common:] + print "+", bline, + if count_leading(btags, " ") < len(btags): + print "?", "\t" * common + btags[common:] + +def count_leading(line, ch): + i, n = 0, len(line) + while i < n and line[i] == ch: + i = i+1 + return i + +def fail(msg): + import sys + out = sys.stderr.write + out(msg + "\n\n") + out(__doc__) + return 0 + +# open a file & return the file object; gripe and return 0 if it +# couldn't be opened +def fopen(fname): + try: + return open(fname, 'r') + except IOError, detail: + return fail("couldn't open " + fname + ": " + str(detail)) + +# open two files & spray the diff to stdout; return false iff a problem +def fcompare(f1name, f2name): + f1 = fopen(f1name) + f2 = fopen(f2name) + if not f1 or not f2: + return 0 + + a = f1.readlines(); f1.close() + b = f2.readlines(); f2.close() + + cruncher = SequenceMatcher(IS_LINE_JUNK, a, b) + for tag, alo, ahi, blo, bhi in cruncher.get_opcodes(): + if tag == 'replace': + fancy_replace(a, alo, ahi, b, blo, bhi) + elif tag == 'delete': + dump('-', a, alo, ahi) + elif tag == 'insert': + dump('+', b, blo, bhi) + elif tag == 'equal': + dump(' ', a, alo, ahi) + else: + raise ValueError, 'unknown tag ' + `tag` + + return 1 + +# crack args (sys.argv[1:] is normal) & compare; +# return false iff a problem + +def main(args): + import getopt + try: + opts, args = getopt.getopt(args, "qr:") + except getopt.error, detail: + return fail(str(detail)) + noisy = 1 + qseen = rseen = 0 + for opt, val in opts: + if opt == "-q": + qseen = 1 + noisy = 0 + elif opt == "-r": + rseen = 1 + whichfile = val + if qseen and rseen: + return fail("can't specify both -q and -r") + if rseen: + if args: + return fail("no args allowed with -r option") + if whichfile in "12": + restore(whichfile) + return 1 + return fail("-r value must be 1 or 2") + if len(args) != 2: + return fail("need 2 filename args") + f1name, f2name = args + if noisy: + print '-:', f1name + print '+:', f2name + return fcompare(f1name, f2name) + +def restore(which): + import sys + tag = {"1": "- ", "2": "+ "}[which] + prefixes = (" ", tag) + for line in sys.stdin.readlines(): + if line[:2] in prefixes: + print line[2:], + +if __name__ == '__main__': + import sys + args = sys.argv[1:] + if "-profile" in args: + import profile, pstats + args.remove("-profile") + statf = "ndiff.pro" + profile.run("main(args)", statf) + stats = pstats.Stats(statf) + stats.strip_dirs().sort_stats('time').print_stats() + else: + main(args) diff --git a/lib/config.py b/lib/config.py new file mode 100644 index 00000000..3e163453 --- /dev/null +++ b/lib/config.py @@ -0,0 +1,317 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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/ +# +# ----------------------------------------------------------------------- +# +# config.py: configuration utilities +# +# ----------------------------------------------------------------------- + +import sys +import os +import string +import ConfigParser +import fnmatch + + +######################################################################### +# +# 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 +# +# 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. +# +######################################################################### + +class Config: + _sections = ('general', 'utilities', 'options', 'cvsdb', 'templates') + _force_multi_value = ('cvs_roots', 'svn_roots', 'languages', 'kv_files', + 'root_parents', 'allowed_views') + + def __init__(self): + for section in self._sections: + setattr(self, section, _sub_config()) + + def load_config(self, pathname, vhost=None, rootname=None): + self.conf_path = os.path.isfile(pathname) and pathname or None + self.base = os.path.dirname(pathname) + self.parser = ConfigParser.ConfigParser() + self.parser.read(self.conf_path or []) + + for section in self._sections: + if self.parser.has_section(section): + self._process_section(self.parser, section, section) + + if vhost and self.parser.has_section('vhosts'): + self._process_vhost(self.parser, vhost) + + if rootname: + self._process_root_options(self.parser, rootname) + + def load_kv_files(self, language): + kv = _sub_config() + + for fname in self.general.kv_files: + if fname[0] == '[': + idx = string.index(fname, ']') + parts = string.split(fname[1:idx], '.') + fname = string.strip(fname[idx+1:]) + else: + parts = [ ] + fname = string.replace(fname, '%lang%', language) + + parser = ConfigParser.ConfigParser() + parser.read(os.path.join(self.base, fname)) + for section in parser.sections(): + for option in parser.options(section): + full_name = parts + [section] + ob = kv + for name in full_name: + try: + ob = getattr(ob, name) + except AttributeError: + c = _sub_config() + setattr(ob, name, c) + ob = c + setattr(ob, option, parser.get(section, option)) + + 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): + sc = getattr(self, subcfg_name) + + for opt in parser.options(section): + value = parser.get(section, opt) + if opt in self._force_multi_value: + value = map(string.strip, filter(None, string.split(value, ','))) + else: + try: + value = int(value) + except ValueError: + pass + + if opt == 'cvs_roots' or opt == 'svn_roots': + value = _parse_roots(opt, value) + + setattr(sc, opt, value) + + def _process_vhost(self, parser, vhost): + # find a vhost name for this vhost, if any (if not, we've nothing to do) + canon_vhost = self._find_canon_vhost(parser, vhost) + if not canon_vhost: + return + + # overlay any option sections associated with this vhost name + cv = 'vhost-%s/' % (canon_vhost) + lcv = len(cv) + for section in parser.sections(): + if section[:lcv] == cv: + base_section = section[lcv:] + if base_section not in self._sections: + raise IllegalOverrideSection('vhost', section) + self._process_section(parser, section, base_section) + + def _find_canon_vhost(self, parser, vhost): + vhost = string.split(string.lower(vhost), ':')[0] # lower-case, no port + for canon_vhost in parser.options('vhosts'): + value = parser.get('vhosts', canon_vhost) + patterns = map(string.lower, map(string.strip, + filter(None, string.split(value, ',')))) + for pat in patterns: + if fnmatch.fnmatchcase(vhost, pat): + return canon_vhost + + return None + + def _process_root_options(self, parser, rootname): + rn = 'root-%s/' % (rootname) + lrn = len(rn) + for section in parser.sections(): + if section[:lrn] == rn: + base_section = section[lrn:] + if base_section in self._sections: + if base_section == 'general': + raise IllegalOverrideSection('root', section) + self._process_section(parser, section, base_section) + elif _startswith(base_section, 'authz-'): + pass + else: + raise IllegalOverrideSection('root', section) + + def overlay_root_options(self, rootname): + "Overly per-root options atop the existing option set." + if not self.conf_path: + return + self._process_root_options(self.parser, rootname) + + 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, rootname=None): + if not self.conf_path: + return {} + + params = {} + authz_section = 'authz-%s' % (authorizer) + for section in self.parser.sections(): + if section == authz_section: + for key, value in self._get_parser_items(self.parser, section): + params[key] = value + if rootname: + root_authz_section = 'root-%s/authz-%s' % (rootname, authorizer) + for section in self.parser.sections(): + if section == root_authz_section: + for key, value in self._get_parser_items(self.parser, section): + params[key] = value + return params + + def set_defaults(self): + "Set some default values in the configuration." + + self.general.cvs_roots = { } + self.general.svn_roots = { } + self.general.root_parents = [] + self.general.default_root = '' + self.general.mime_types_file = '' + self.general.address = '' + 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.svn = '' + self.utilities.diff = '' + self.utilities.cvsgraph = '' + + self.options.root_as_url_component = 1 + self.options.checkout_magic = 0 + self.options.allowed_views = ['markup', 'annotate', 'roots'] + self.options.authorizer = 'forbidden' + self.options.mangle_email_addresses = 0 + self.options.default_file_view = "log" + self.options.http_expiration_time = 600 + self.options.generate_etags = 1 + self.options.svn_config_dir = None + 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" + 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.use_localtime = 0 + self.options.short_log_len = 80 + self.options.enable_syntax_coloration = 1 + self.options.use_cvsgraph = 0 + self.options.cvsgraph_conf = "cvsgraph.conf" + self.options.use_re_search = 0 + self.options.use_pagesize = 0 + self.options.limit_changes = 100 + + 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.query_form = None + self.templates.query_results = None + self.templates.roots = None + + self.cvsdb.enabled = 0 + self.cvsdb.host = '' + self.cvsdb.port = 3306 + self.cvsdb.database_name = '' + self.cvsdb.user = '' + self.cvsdb.passwd = '' + self.cvsdb.readonly_user = '' + self.cvsdb.readonly_passwd = '' + self.cvsdb.row_limit = 1000 + self.cvsdb.rss_row_limit = 100 + self.cvsdb.check_database_for_root = 0 + +def _startswith(somestr, substr): + return somestr[:len(substr)] == substr + +def _parse_roots(config_name, config_value): + roots = { } + for root in config_value: + pos = string.find(root, ':') + if pos < 0: + raise MalformedRoot(config_name, root) + 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): + def __init__(self, config_name, value_given): + Exception.__init__(self, config_name, value_given) + self.config_name = config_name + self.value_given = value_given + def __str__(self): + return "malformed configuration: '%s' uses invalid syntax: %s" \ + % (self.config_name, self.value_given) + + +class _sub_config: + pass + +if not hasattr(sys, 'hexversion'): + # Python 1.5 or 1.5.1. fix the syntax for ConfigParser options. + import regex + ConfigParser.option_cre = regex.compile('^\([-A-Za-z0-9._]+\)\(:\|[' + + string.whitespace + + ']*=\)\(.*\)$') diff --git a/lib/cvsdb.py b/lib/cvsdb.py new file mode 100644 index 00000000..dd1ec221 --- /dev/null +++ b/lib/cvsdb.py @@ -0,0 +1,839 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- + +import os +import sys +import string +import time +import fnmatch +import re + +import vclib +import dbi + + +## error +error = "cvsdb error" + +## CheckinDatabase provides all interfaces needed to the SQL database +## back-end; it needs to be subclassed, and have its "Connect" method +## defined to actually be complete; it should run well off of any DBI 2.0 +## complient database interface + +class CheckinDatabase: + def __init__(self, host, port, user, passwd, database, row_limit): + self._host = host + self._port = port + self._user = user + self._passwd = passwd + self._database = database + self._row_limit = row_limit + + ## database lookup caches + self._get_cache = {} + self._get_id_cache = {} + self._desc_id_cache = {} + + def Connect(self): + self.db = dbi.connect( + self._host, self._port, self._user, self._passwd, self._database) + cursor = self.db.cursor() + cursor.execute("SET AUTOCOMMIT=1") + + def sql_get_id(self, table, column, value, auto_set): + sql = "SELECT id FROM %s WHERE %s=%%s" % (table, column) + sql_args = (value, ) + + cursor = self.db.cursor() + cursor.execute(sql, sql_args) + try: + (id, ) = cursor.fetchone() + except TypeError: + if not auto_set: + return None + else: + return str(int(id)) + + ## insert the new identifier + sql = "INSERT INTO %s(%s) VALUES(%%s)" % (table, column) + sql_args = (value, ) + cursor.execute(sql, sql_args) + + return self.sql_get_id(table, column, value, 0) + + def get_id(self, table, column, value, auto_set): + ## attempt to retrieve from cache + try: + return self._get_id_cache[table][column][value] + except KeyError: + pass + + id = self.sql_get_id(table, column, value, auto_set) + if id == None: + return None + + ## add to cache + try: + temp = self._get_id_cache[table] + except KeyError: + temp = self._get_id_cache[table] = {} + + try: + temp2 = temp[column] + except KeyError: + temp2 = temp[column] = {} + + temp2[value] = id + return id + + def sql_get(self, table, column, id): + sql = "SELECT %s FROM %s WHERE id=%%s" % (column, table) + sql_args = (id, ) + + cursor = self.db.cursor() + cursor.execute(sql, sql_args) + try: + (value, ) = cursor.fetchone() + except TypeError: + return None + + return value + + def get(self, table, column, id): + ## attempt to retrieve from cache + try: + return self._get_cache[table][column][id] + except KeyError: + pass + + value = self.sql_get(table, column, id) + if value == None: + return None + + ## add to cache + try: + temp = self._get_cache[table] + except KeyError: + temp = self._get_cache[table] = {} + + try: + temp2 = temp[column] + except KeyError: + temp2 = temp[column] = {} + + temp2[id] = value + return value + + def get_list(self, table, field_index): + sql = "SELECT * FROM %s" % (table) + cursor = self.db.cursor() + cursor.execute(sql) + + list = [] + while 1: + row = cursor.fetchone() + if row == None: + break + list.append(row[field_index]) + + return list + + def GetBranchID(self, branch, auto_set = 1): + return self.get_id("branches", "branch", branch, auto_set) + + def GetBranch(self, id): + return self.get("branches", "branch", id) + + def GetDirectoryID(self, dir, auto_set = 1): + return self.get_id("dirs", "dir", dir, auto_set) + + def GetDirectory(self, id): + return self.get("dirs", "dir", id) + + def GetFileID(self, file, auto_set = 1): + return self.get_id("files", "file", file, auto_set) + + def GetFile(self, id): + return self.get("files", "file", id) + + def GetAuthorID(self, author, auto_set = 1): + return self.get_id("people", "who", author, auto_set) + + def GetAuthor(self, id): + return self.get("people", "who", id) + + def GetRepositoryID(self, repository, auto_set = 1): + return self.get_id("repositories", "repository", repository, auto_set) + + def GetRepository(self, id): + return self.get("repositories", "repository", id) + + def SQLGetDescriptionID(self, description, auto_set = 1): + ## lame string hash, blame Netscape -JMP + hash = len(description) + + sql = "SELECT id FROM descs WHERE hash=%s AND description=%s" + sql_args = (hash, description) + + cursor = self.db.cursor() + cursor.execute(sql, sql_args) + try: + (id, ) = cursor.fetchone() + except TypeError: + if not auto_set: + return None + else: + return str(int(id)) + + sql = "INSERT INTO descs (hash,description) values (%s,%s)" + sql_args = (hash, description) + cursor.execute(sql, sql_args) + + return self.GetDescriptionID(description, 0) + + def GetDescriptionID(self, description, auto_set = 1): + ## attempt to retrieve from cache + hash = len(description) + try: + return self._desc_id_cache[hash][description] + except KeyError: + pass + + id = self.SQLGetDescriptionID(description, auto_set) + if id == None: + return None + + ## add to cache + try: + temp = self._desc_id_cache[hash] + except KeyError: + temp = self._desc_id_cache[hash] = {} + + temp[description] = id + return id + + def GetDescription(self, id): + return self.get("descs", "description", id) + + def GetRepositoryList(self): + return self.get_list("repositories", 1) + + def GetBranchList(self): + return self.get_list("branches", 1) + + def GetAuthorList(self): + return self.get_list("people", 1) + + def AddCommitList(self, commit_list): + for commit in commit_list: + self.AddCommit(commit) + + def AddCommit(self, commit): + ci_when = dbi.DateTimeFromTicks(commit.GetTime() or 0.0) + ci_type = commit.GetTypeString() + who_id = self.GetAuthorID(commit.GetAuthor()) + repository_id = self.GetRepositoryID(commit.GetRepository()) + directory_id = self.GetDirectoryID(commit.GetDirectory()) + file_id = self.GetFileID(commit.GetFile()) + revision = commit.GetRevision() + sticky_tag = "NULL" + branch_id = self.GetBranchID(commit.GetBranch()) + plus_count = commit.GetPlusCount() or '0' + minus_count = commit.GetMinusCount() or '0' + description_id = self.GetDescriptionID(commit.GetDescription()) + + sql = "REPLACE INTO checkins"\ + " (type,ci_when,whoid,repositoryid,dirid,fileid,revision,"\ + " stickytag,branchid,addedlines,removedlines,descid)"\ + "VALUES(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)" + sql_args = (ci_type, ci_when, who_id, repository_id, + directory_id, file_id, revision, sticky_tag, branch_id, + plus_count, minus_count, description_id) + + cursor = self.db.cursor() + try: + cursor.execute(sql, sql_args) + except Exception, e: + raise Exception("Error adding commit: '%s'\n" + "Values were:\n" + "\ttype = %s\n" + "\tci_when = %s\n" + "\twhoid = %s\n" + "\trepositoryid = %s\n" + "\tdirid = %s\n" + "\tfileid = %s\n" + "\trevision = %s\n" + "\tstickytag = %s\n" + "\tbranchid = %s\n" + "\taddedlines = %s\n" + "\tremovedlines = %s\n" + "\tdescid = %s\n" + % ((str(e), ) + sql_args)) + + def SQLQueryListString(self, field, query_entry_list): + sqlList = [] + + for query_entry in query_entry_list: + data = query_entry.data + ## figure out the correct match type + if query_entry.match == "exact": + match = "=" + elif query_entry.match == "like": + match = " LIKE " + elif query_entry.match == "glob": + match = " REGEXP " + # use fnmatch to translate the glob into a regexp + data = fnmatch.translate(data) + if data[0] != '^': data = '^' + data + elif query_entry.match == "regex": + match = " REGEXP " + elif query_entry.match == "notregex": + match = " NOT REGEXP " + + sqlList.append("%s%s%s" % (field, match, self.db.literal(data))) + + return "(%s)" % (string.join(sqlList, " OR ")) + + def CreateSQLQueryString(self, query): + tableList = [("checkins", None)] + condList = [] + + if len(query.repository_list): + tableList.append(("repositories", + "(checkins.repositoryid=repositories.id)")) + temp = self.SQLQueryListString("repositories.repository", + query.repository_list) + condList.append(temp) + + if len(query.branch_list): + tableList.append(("branches", "(checkins.branchid=branches.id)")) + temp = self.SQLQueryListString("branches.branch", + query.branch_list) + condList.append(temp) + + if len(query.directory_list): + tableList.append(("dirs", "(checkins.dirid=dirs.id)")) + temp = self.SQLQueryListString("dirs.dir", query.directory_list) + condList.append(temp) + + if len(query.file_list): + tableList.append(("files", "(checkins.fileid=files.id)")) + temp = self.SQLQueryListString("files.file", query.file_list) + condList.append(temp) + + if len(query.author_list): + tableList.append(("people", "(checkins.whoid=people.id)")) + temp = self.SQLQueryListString("people.who", query.author_list) + condList.append(temp) + + if len(query.comment_list): + tableList.append(("descs", "(checkins.descid=descs.id)")) + temp = self.SQLQueryListString("descs.description", + query.comment_list) + condList.append(temp) + + if query.from_date: + temp = "(checkins.ci_when>=\"%s\")" % (str(query.from_date)) + condList.append(temp) + + if query.to_date: + temp = "(checkins.ci_when<=\"%s\")" % (str(query.to_date)) + condList.append(temp) + + if query.sort == "date": + order_by = "ORDER BY checkins.ci_when DESC,descid" + elif query.sort == "author": + tableList.append(("people", "(checkins.whoid=people.id)")) + order_by = "ORDER BY people.who,descid" + elif query.sort == "file": + tableList.append(("files", "(checkins.fileid=files.id)")) + order_by = "ORDER BY files.file,descid" + + ## exclude duplicates from the table list, and split out join + ## conditions from table names. In future, the join conditions + ## might be handled by INNER JOIN statements instead of WHERE + ## clauses, but MySQL 3.22 apparently doesn't support them well. + tables = [] + joinConds = [] + for (table, cond) in tableList: + if table not in tables: + tables.append(table) + if cond is not None: joinConds.append(cond) + + tables = string.join(tables, ",") + conditions = string.join(joinConds + condList, " AND ") + conditions = conditions and "WHERE %s" % conditions + + ## limit the number of rows requested or we could really slam + ## a server with a large database + limit = "" + if query.limit: + limit = "LIMIT %s" % (str(query.limit)) + elif self._row_limit: + limit = "LIMIT %s" % (str(self._row_limit)) + + sql = "SELECT checkins.* FROM %s %s %s %s" % ( + tables, conditions, order_by, limit) + + return sql + + def RunQuery(self, query): + sql = self.CreateSQLQueryString(query) + cursor = self.db.cursor() + cursor.execute(sql) + + while 1: + row = cursor.fetchone() + if not row: + break + + (dbType, dbCI_When, dbAuthorID, dbRepositoryID, dbDirID, + dbFileID, dbRevision, dbStickyTag, dbBranchID, dbAddedLines, + dbRemovedLines, dbDescID) = row + + commit = LazyCommit(self) + if dbType == 'Add': + commit.SetTypeAdd() + elif dbType == 'Remove': + commit.SetTypeRemove() + else: + commit.SetTypeChange() + commit.SetTime(dbi.TicksFromDateTime(dbCI_When)) + commit.SetFileID(dbFileID) + commit.SetDirectoryID(dbDirID) + commit.SetRevision(dbRevision) + commit.SetRepositoryID(dbRepositoryID) + commit.SetAuthorID(dbAuthorID) + commit.SetBranchID(dbBranchID) + commit.SetPlusCount(dbAddedLines) + commit.SetMinusCount(dbRemovedLines) + commit.SetDescriptionID(dbDescID) + + query.AddCommit(commit) + + def CheckCommit(self, commit): + repository_id = self.GetRepositoryID(commit.GetRepository(), 0) + if repository_id == None: + return None + + dir_id = self.GetDirectoryID(commit.GetDirectory(), 0) + if dir_id == None: + return None + + file_id = self.GetFileID(commit.GetFile(), 0) + if file_id == None: + return None + + sql = "SELECT * FROM checkins WHERE "\ + " repositoryid=%s AND dirid=%s AND fileid=%s AND revision=%s" + sql_args = (repository_id, dir_id, file_id, commit.GetRevision()) + + cursor = self.db.cursor() + cursor.execute(sql, sql_args) + try: + (ci_type, ci_when, who_id, repository_id, + dir_id, file_id, revision, sticky_tag, branch_id, + plus_count, minus_count, description_id) = cursor.fetchone() + except TypeError: + return None + + return commit + + def sql_delete(self, table, key, value, keep_fkey = None): + sql = "DELETE FROM %s WHERE %s=%%s" % (table, key) + sql_args = (value, ) + if keep_fkey: + sql += " AND %s NOT IN (SELECT %s FROM checkins WHERE %s = %%s)" \ + % (key, keep_fkey, keep_fkey) + sql_args = (value, value) + cursor = self.db.cursor() + cursor.execute(sql, sql_args) + + def PurgeRepository(self, repository): + rep_id = self.GetRepositoryID(repository) + if not rep_id: + raise Exception, "Unknown repository '%s'" % (repository) + + sql = "SELECT * FROM checkins WHERE repositoryid=%s" + sql_args = (rep_id, ) + cursor = self.db.cursor() + cursor.execute(sql, sql_args) + checkins = [] + while 1: + try: + (ci_type, ci_when, who_id, repository_id, + dir_id, file_id, revision, sticky_tag, branch_id, + plus_count, minus_count, description_id) = cursor.fetchone() + except TypeError: + break + checkins.append([file_id, dir_id, branch_id, description_id, who_id]) + + #self.sql_delete('repositories', 'id', rep_id) + self.sql_delete('checkins', 'repositoryid', rep_id) + for checkin in checkins: + self.sql_delete('files', 'id', checkin[0], 'fileid') + self.sql_delete('dirs', 'id', checkin[1], 'dirid') + self.sql_delete('branches', 'id', checkin[2], 'branchid') + self.sql_delete('descs', 'id', checkin[3], 'descid') + self.sql_delete('people', 'id', checkin[4], 'whoid') + +## the Commit class holds data on one commit, the representation is as +## close as possible to how it should be committed and retrieved to the +## database engine +class Commit: + ## static constants for type of commit + CHANGE = 0 + ADD = 1 + REMOVE = 2 + + def __init__(self): + self.__directory = '' + self.__file = '' + self.__repository = '' + self.__revision = '' + self.__author = '' + self.__branch = '' + self.__pluscount = '' + self.__minuscount = '' + self.__description = '' + self.__gmt_time = 0.0 + self.__type = Commit.CHANGE + + def SetRepository(self, repository): + self.__repository = repository + + def GetRepository(self): + return self.__repository + + def SetDirectory(self, dir): + self.__directory = dir + + def GetDirectory(self): + return self.__directory + + def SetFile(self, file): + self.__file = file + + def GetFile(self): + return self.__file + + def SetRevision(self, revision): + self.__revision = revision + + def GetRevision(self): + return self.__revision + + def SetTime(self, gmt_time): + if gmt_time is None: + ### We're just going to assume that a datestamp of The Epoch + ### ain't real. + self.__gmt_time = 0.0 + else: + self.__gmt_time = float(gmt_time) + + def GetTime(self): + return self.__gmt_time and self.__gmt_time or None + + def SetAuthor(self, author): + self.__author = author + + def GetAuthor(self): + return self.__author + + def SetBranch(self, branch): + self.__branch = branch or '' + + def GetBranch(self): + return self.__branch + + def SetPlusCount(self, pluscount): + self.__pluscount = pluscount + + def GetPlusCount(self): + return self.__pluscount + + def SetMinusCount(self, minuscount): + self.__minuscount = minuscount + + def GetMinusCount(self): + return self.__minuscount + + def SetDescription(self, description): + self.__description = description + + def GetDescription(self): + return self.__description + + def SetTypeChange(self): + self.__type = Commit.CHANGE + + def SetTypeAdd(self): + self.__type = Commit.ADD + + def SetTypeRemove(self): + self.__type = Commit.REMOVE + + def GetType(self): + return self.__type + + def GetTypeString(self): + if self.__type == Commit.CHANGE: + return 'Change' + elif self.__type == Commit.ADD: + return 'Add' + elif self.__type == Commit.REMOVE: + return 'Remove' + +## LazyCommit overrides a few methods of Commit to only retrieve +## it's properties as they are needed +class LazyCommit(Commit): + def __init__(self, db): + Commit.__init__(self) + self.__db = db + + def SetFileID(self, dbFileID): + self.__dbFileID = dbFileID + + def GetFileID(self): + return self.__dbFileID + + def GetFile(self): + return self.__db.GetFile(self.__dbFileID) + + def SetDirectoryID(self, dbDirID): + self.__dbDirID = dbDirID + + def GetDirectoryID(self): + return self.__dbDirID + + def GetDirectory(self): + return self.__db.GetDirectory(self.__dbDirID) + + def SetRepositoryID(self, dbRepositoryID): + self.__dbRepositoryID = dbRepositoryID + + def GetRepositoryID(self): + return self.__dbRepositoryID + + def GetRepository(self): + return self.__db.GetRepository(self.__dbRepositoryID) + + def SetAuthorID(self, dbAuthorID): + self.__dbAuthorID = dbAuthorID + + def GetAuthorID(self): + return self.__dbAuthorID + + def GetAuthor(self): + return self.__db.GetAuthor(self.__dbAuthorID) + + def SetBranchID(self, dbBranchID): + self.__dbBranchID = dbBranchID + + def GetBranchID(self): + return self.__dbBranchID + + def GetBranch(self): + return self.__db.GetBranch(self.__dbBranchID) + + def SetDescriptionID(self, dbDescID): + self.__dbDescID = dbDescID + + def GetDescriptionID(self): + return self.__dbDescID + + def GetDescription(self): + return self.__db.GetDescription(self.__dbDescID) + +## QueryEntry holds data on one match-type in the SQL database +## match is: "exact", "like", or "regex" +class QueryEntry: + def __init__(self, data, match): + self.data = data + self.match = match + +## CheckinDatabaseQueryData is a object which contains the search parameters +## for a query to the CheckinDatabase +class CheckinDatabaseQuery: + def __init__(self): + ## sorting + self.sort = "date" + + ## repository to query + self.repository_list = [] + self.branch_list = [] + self.directory_list = [] + self.file_list = [] + self.author_list = [] + self.comment_list = [] + + ## date range in DBI 2.0 timedate objects + self.from_date = None + self.to_date = None + + ## limit on number of rows to return + self.limit = None + + ## list of commits -- filled in by CVS query + self.commit_list = [] + + ## commit_cb provides a callback for commits as they + ## are added + self.commit_cb = None + + def SetRepository(self, repository, match = "exact"): + self.repository_list.append(QueryEntry(repository, match)) + + def SetBranch(self, branch, match = "exact"): + self.branch_list.append(QueryEntry(branch, match)) + + def SetDirectory(self, directory, match = "exact"): + self.directory_list.append(QueryEntry(directory, match)) + + def SetFile(self, file, match = "exact"): + self.file_list.append(QueryEntry(file, match)) + + def SetAuthor(self, author, match = "exact"): + self.author_list.append(QueryEntry(author, match)) + + def SetComment(self, comment, match = "exact"): + self.comment_list.append(QueryEntry(comment, match)) + + def SetSortMethod(self, sort): + self.sort = sort + + def SetFromDateObject(self, ticks): + self.from_date = dbi.DateTimeFromTicks(ticks) + + def SetToDateObject(self, ticks): + self.to_date = dbi.DateTimeFromTicks(ticks) + + def SetFromDateHoursAgo(self, hours_ago): + ticks = time.time() - (3600 * hours_ago) + self.from_date = dbi.DateTimeFromTicks(ticks) + + def SetFromDateDaysAgo(self, days_ago): + ticks = time.time() - (86400 * days_ago) + self.from_date = dbi.DateTimeFromTicks(ticks) + + def SetToDateDaysAgo(self, days_ago): + ticks = time.time() - (86400 * days_ago) + self.to_date = dbi.DateTimeFromTicks(ticks) + + def SetLimit(self, limit): + self.limit = limit; + + def AddCommit(self, commit): + self.commit_list.append(commit) + + +## +## entrypoints +## +def CreateCommit(): + return Commit() + +def CreateCheckinQuery(): + return CheckinDatabaseQuery() + +def ConnectDatabase(cfg, readonly=0): + if readonly: + user = cfg.cvsdb.readonly_user + passwd = cfg.cvsdb.readonly_passwd + else: + user = cfg.cvsdb.user + passwd = cfg.cvsdb.passwd + db = CheckinDatabase(cfg.cvsdb.host, cfg.cvsdb.port, user, passwd, + cfg.cvsdb.database_name, cfg.cvsdb.row_limit) + db.Connect() + return db + +def ConnectDatabaseReadOnly(cfg): + return ConnectDatabase(cfg, 1) + +def GetCommitListFromRCSFile(repository, path_parts, revision=None): + commit_list = [] + + directory = string.join(path_parts[:-1], "/") + file = path_parts[-1] + + revs = repository.itemlog(path_parts, revision, vclib.SORTBY_DEFAULT, + 0, 0, {"cvs_pass_rev": 1}) + for rev in revs: + commit = CreateCommit() + commit.SetRepository(repository.rootpath) + commit.SetDirectory(directory) + commit.SetFile(file) + commit.SetRevision(rev.string) + commit.SetAuthor(rev.author) + commit.SetDescription(rev.log) + commit.SetTime(rev.date) + + if rev.changed: + # extract the plus/minus and drop the sign + plus, minus = string.split(rev.changed) + commit.SetPlusCount(plus[1:]) + commit.SetMinusCount(minus[1:]) + + if rev.dead: + commit.SetTypeRemove() + else: + commit.SetTypeChange() + else: + commit.SetTypeAdd() + + commit_list.append(commit) + + # if revision is on a branch which has at least one tag + if len(rev.number) > 2 and rev.branches: + commit.SetBranch(rev.branches[0].name) + + return commit_list + +def GetUnrecordedCommitList(repository, path_parts, db): + commit_list = GetCommitListFromRCSFile(repository, path_parts) + + unrecorded_commit_list = [] + for commit in commit_list: + result = db.CheckCommit(commit) + if not result: + unrecorded_commit_list.append(commit) + + return unrecorded_commit_list + +_re_likechars = re.compile(r"([_%\\])") + +def EscapeLike(literal): + """Escape literal string for use in a MySQL LIKE pattern""" + return re.sub(_re_likechars, r"\\\1", literal) + +def FindRepository(db, path): + """Find repository path in database given path to subdirectory + Returns normalized repository path and relative directory path""" + path = os.path.normpath(path) + dirs = [] + while path: + rep = os.path.normcase(path) + if db.GetRepositoryID(rep, 0) is None: + path, pdir = os.path.split(path) + if not pdir: + return None, None + dirs.append(pdir) + else: + break + dirs.reverse() + return rep, dirs + +def CleanRepository(path): + """Return normalized top-level repository path""" + return os.path.normcase(os.path.normpath(path)) + diff --git a/lib/dbi.py b/lib/dbi.py new file mode 100644 index 00000000..1f28dce8 --- /dev/null +++ b/lib/dbi.py @@ -0,0 +1,63 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- + +import sys +import time +import types +import re +import compat +import MySQLdb + +# set to 1 to store commit times in UTC, or 0 to use the ViewVC machine's +# local timezone. Using UTC is recommended because it ensures that the +# database will remain valid even if it is moved to another machine or the host +# computer's time zone is changed. UTC also avoids the ambiguity associated +# with daylight saving time (for example if a computer in New York recorded the +# local time 2002/10/27 1:30 am, there would be no way to tell whether the +# actual time was recorded before or after clocks were rolled back). Use local +# times for compatibility with databases used by ViewCVS 0.92 and earlier +# versions. +utc_time = 1 + +def DateTimeFromTicks(ticks): + """Return a MySQL DATETIME value from a unix timestamp""" + + if utc_time: + t = time.gmtime(ticks) + else: + t = time.localtime(ticks) + return "%04d-%02d-%02d %02d:%02d:%02d" % t[:6] + +_re_datetime = 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 TicksFromDateTime(datetime): + """Return a unix timestamp from a MySQL DATETIME value""" + + if type(datetime) == types.StringType: + # datetime is a MySQL DATETIME string + matches = _re_datetime.match(datetime).groups() + t = tuple(map(int, matches)) + (0, 0, 0) + elif hasattr(datetime, "timetuple"): + # datetime is a Python >=2.3 datetime.DateTime object + t = datetime.timetuple() + else: + # datetime is an eGenix mx.DateTime object + t = datetime.tuple() + + if utc_time: + return compat.timegm(t) + else: + return time.mktime(t[:8] + (-1,)) + +def connect(host, port, user, passwd, db): + return MySQLdb.connect(host=host, port=port, user=user, passwd=passwd, db=db) diff --git a/lib/debug.py b/lib/debug.py new file mode 100644 index 00000000..fbaa9bd0 --- /dev/null +++ b/lib/debug.py @@ -0,0 +1,199 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- + +# +# Note: a t_start/t_end pair consumes about 0.00005 seconds on a P3/700. +# the lambda form (when debugging is disabled) should be even faster. +# + +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 + + _timers = { } + _times = { } + + def t_start(which): + _timers[which] = time.time() + + def t_end(which): + t = time.time() - _timers[which] + if _times.has_key(which): + _times[which] = _times[which] + t + else: + _times[which] = t + + def dump(): + for name, value in _times.items(): + print '%s: %.6f
' % (name, value) + +else: + + t_start = t_end = dump = lambda *args: None + + +class ViewVCException: + def __init__(self, msg, status=None): + self.msg = msg + self.status = status + + def __str__(self): + if self.status: + return '%s: %s' % (self.status, self.msg) + return "ViewVC Unrecoverable Error: %s" % self.msg + + +def PrintException(server, exc_data): + status = exc_data['status'] + msg = exc_data['msg'] + tb = exc_data['stacktrace'] + + server.header(status=status) + server.write("

An Exception Has Occurred

\n") + + s = '' + if msg: + s = '

%s

' % server.escape(msg) + if status: + s = s + ('

HTTP Response Status

\n

\n%s


\n' + % status) + server.write(s) + + server.write("

Python Traceback

\n

")
+  server.write(server.escape(tb))
+  server.write("

\n") + + +def GetExceptionData(): + # capture the exception before doing anything else + exc_type, exc, exc_tb = sys.exc_info() + + exc_dict = { + 'status' : None, + 'msg' : None, + 'stacktrace' : None, + } + + try: + import traceback, string + + if isinstance(exc, ViewVCException): + exc_dict['msg'] = exc.msg + exc_dict['status'] = exc.status + + tb = string.join(traceback.format_exception(exc_type, exc, exc_tb), '') + exc_dict['stacktrace'] = tb + + finally: + # prevent circular reference. sys.exc_info documentation warns + # "Assigning the traceback return value to a local variable in a function + # that is handling an exception will cause a circular reference..." + # This is all based on 'exc_tb', and we're now done with it. Toss it. + del exc_tb + + return exc_dict + + +if SHOW_CHILD_PROCESSES: + class Process: + def __init__(self, command, inStream, outStream, errStream): + self.command = command + self.debugIn = inStream + self.debugOut = outStream + self.debugErr = errStream + + import sapi + if not sapi.server is None: + if not sapi.server.pageGlobals.has_key('processes'): + sapi.server.pageGlobals['processes'] = [self] + else: + sapi.server.pageGlobals['processes'].append(self) + + def DumpChildren(server): + import os + + if not server.pageGlobals.has_key('processes'): + return + + server.header() + lastOut = None + i = 0 + + for k in server.pageGlobals['processes']: + i = i + 1 + server.write("\n") + server.write("" % i) + server.write("\n \n\n") + server.write("\n \n\n") + + if k.debugOut is k.debugErr: + server.write("\n \n\n") + + else: + server.write("\n \n\n") + server.write("\n \n\n") + + server.write("
Child Process%i
Command Line
")
+      server.write(server.escape(k.command))
+      server.write("
Standard In: ") + + if k.debugIn is lastOut and not lastOut is None: + server.write("Output from process %i" % (i - 1)) + elif k.debugIn: + server.write("
")
+        server.write(server.escape(k.debugIn.getvalue()))
+        server.write("
") + + server.write("
Standard Out & Error:
")
+        if k.debugOut:
+          server.write(server.escape(k.debugOut.getvalue()))
+        server.write("
Standard Out:
")
+        if k.debugOut:
+          server.write(server.escape(k.debugOut.getvalue()))
+        server.write("
Standard Error:
")
+        if k.debugErr:
+          server.write(server.escape(k.debugErr.getvalue()))
+        server.write("
\n") + server.flush() + lastOut = k.debugOut + + server.write("\n") + server.write("") + for k, v in os.environ.items(): + server.write("\n \n \n") + server.write("
Environment Variables
")
+      server.write(server.escape(k))
+      server.write("
")
+      server.write(server.escape(v))
+      server.write("
") + +else: + + def DumpChildren(server): + pass + diff --git a/lib/ezt.py b/lib/ezt.py new file mode 100644 index 00000000..3f391aa6 --- /dev/null +++ b/lib/ezt.py @@ -0,0 +1,830 @@ +#!/usr/bin/env python +"""ezt.py -- easy templating + +ezt templates are simply text files in whatever format you so desire +(such as XML, HTML, etc.) which contain directives sprinkled +throughout. With these directives it is possible to generate the +dynamic content from the ezt templates. + +These directives are enclosed in square brackets. If you are a +C-programmer, you might be familar with the #ifdef directives of the C +preprocessor 'cpp'. ezt provides a similar concept. Additionally EZT +has a 'for' directive, which allows it to iterate (repeat) certain +subsections of the template according to sequence of data items +provided by the application. + +The final rendering is performed by the method generate() of the Template +class. Building template instances can either be done using external +EZT files (convention: use the suffix .ezt for such files): + + >>> template = Template("../templates/log.ezt") + +or by calling the parse() method of a template instance directly with +a EZT template string: + + >>> template = Template() + >>> template.parse(''' + ... [title_string] + ...

[title_string]

+ ... [for a_sequence]

[a_sequence]

+ ... [end]
+ ... The [person] is [if-any state]in[else]out[end]. + ... + ... + ... ''') + +The application should build a dictionary 'data' and pass it together +with the output fileobject to the templates generate method: + + >>> data = {'title_string' : "A Dummy Page", + ... 'a_sequence' : ['list item 1', 'list item 2', 'another element'], + ... 'person': "doctor", + ... 'state' : None } + >>> import sys + >>> template.generate(sys.stdout, data) + + A Dummy Page +

A Dummy Page

+

list item 1

+

list item 2

+

another element

+
+ The doctor is out. + + + +Template syntax error reporting should be improved. Currently it is +very sparse (template line numbers would be nice): + + >>> Template().parse("[if-any where] foo [else] bar [end unexpected args]") + Traceback (innermost last): + File "", line 1, in ? + File "ezt.py", line 220, in parse + self.program = self._parse(text) + File "ezt.py", line 275, in _parse + raise ArgCountSyntaxError(str(args[1:])) + ArgCountSyntaxError: ['unexpected', 'args'] + >>> Template().parse("[if unmatched_end]foo[end]") + Traceback (innermost last): + File "", line 1, in ? + File "ezt.py", line 206, in parse + self.program = self._parse(text) + File "ezt.py", line 266, in _parse + raise UnmatchedEndError() + UnmatchedEndError + + +Directives +========== + + Several directives allow the use of dotted qualified names refering to objects + or attributes of objects contained in the data dictionary given to the + .generate() method. + + Qualified names + --------------- + + Qualified names have two basic forms: a variable reference, or a string + constant. References are a name from the data dictionary with optional + dotted attributes (where each intermediary is an object with attributes, + of course). + + Examples: + + [varname] + + [ob.attr] + + ["string"] + + Simple directives + ----------------- + + [QUAL_NAME] + + This directive is simply replaced by the value of the qualified name. + If the value is a number it's converted to a string before being + outputted. If it is None, nothing is outputted. If it is a python file + object (i.e. any object with a "read" method), it's contents are + outputted. If it is a callback function (any callable python object + is assumed to be a callback function), it is invoked and passed an EZT + Context object as an argument. + + [QUAL_NAME QUAL_NAME ...] + + If the first value is a callback function, it is invoked with an EZT + Context object as a first argument, and the rest of the values as + additional arguments. + + Otherwise, the first value defines a substitution format, specifying + constant text and indices of the additional arguments. The arguments + are substituted and the result is inserted into the output stream. + + Example: + ["abc %0 def %1 ghi %0" foo bar.baz] + + Note that the first value can be any type of qualified name -- a string + constant or a variable reference. Use %% to substitute a percent sign. + Argument indices are 0-based. + + [include "filename"] or [include QUAL_NAME] + + This directive is replaced by content of the named include file. Note + that a string constant is more efficient -- the target file is compiled + inline. In the variable form, the target file is compiled and executed + at runtime. + + Block directives + ---------------- + + [for QUAL_NAME] ... [end] + + The text within the [for ...] directive and the corresponding [end] + is repeated for each element in the sequence referred to by the + qualified name in the for directive. Within the for block this + identifiers now refers to the actual item indexed by this loop + iteration. + + [if-any QUAL_NAME [QUAL_NAME2 ...]] ... [else] ... [end] + + Test if any QUAL_NAME value is not None or an empty string or list. + The [else] clause is optional. CAUTION: Numeric values are + converted to string, so if QUAL_NAME refers to a numeric value 0, + the then-clause is substituted! + + [if-index INDEX_FROM_FOR odd] ... [else] ... [end] + [if-index INDEX_FROM_FOR even] ... [else] ... [end] + [if-index INDEX_FROM_FOR first] ... [else] ... [end] + [if-index INDEX_FROM_FOR last] ... [else] ... [end] + [if-index INDEX_FROM_FOR NUMBER] ... [else] ... [end] + + These five directives work similar to [if-any], but are only useful + within a [for ...]-block (see above). The odd/even directives are + for example useful to choose different background colors for + adjacent rows in a table. Similar the first/last directives might + be used to remove certain parts (for example "Diff to previous" + doesn't make sense, if there is no previous). + + [is QUAL_NAME STRING] ... [else] ... [end] + [is QUAL_NAME QUAL_NAME] ... [else] ... [end] + + The [is ...] directive is similar to the other conditional + directives above. But it allows to compare two value references or + a value reference with some constant string. + + [define VARIABLE] ... [end] + + The [define ...] directive allows you to create and modify template + variables from within the template itself. Essentially, any data + between inside the [define ...] and its matching [end] will be + expanded using the other template parsing and output generation + rules, and then stored as a string value assigned to the variable + VARIABLE. The new (or changed) variable is then available for use + with other mechanisms such as [is ...] or [if-any ...], as long as + they appear later in the template. + + [format STRING] ... [end] + + The format directive controls how the values substituted into + 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 + " and >); the "uri" mode escapes characters using hexadecimal + escape sequences (like %20 and %7e). + + [format CALLBACK] + + Python applications using EZT can provide custom formatters as callback + variables. "[format CALLBACK][QUAL_NAME][end]" is in most cases + equivalent to "[CALLBACK QUAL_NAME]" +""" +# +# Copyright (C) 2001-2007 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 +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# +# This software is maintained by Greg and is available at: +# http://svn.webdav.org/repos/projects/ezt/trunk/ +# + +import string +import re +from types import StringType, IntType, FloatType, LongType, TupleType +import os +import cgi +import urllib +try: + import cStringIO +except ImportError: + import StringIO + cStringIO = StringIO + +# +# Formatting types +# +FORMAT_RAW = 'raw' +FORMAT_HTML = 'html' +FORMAT_XML = 'xml' +FORMAT_URI = 'uri' + +# +# This regular expression matches three alternatives: +# expr: DIRECTIVE | BRACKET | COMMENT +# DIRECTIVE: '[' ITEM (whitespace ITEM)* '] +# ITEM: STRING | NAME +# STRING: '"' (not-slash-or-dquote | '\' anychar)* '"' +# NAME: (alphanum | '_' | '-' | '.')+ +# BRACKET: '[[]' +# COMMENT: '[#' not-rbracket* ']' +# +# When used with the split() method, the return value will be composed of +# non-matching text and the two paren groups (DIRECTIVE and BRACKET). Since +# the COMMENT matches are not placed into a group, they are considered a +# "splitting" value and simply dropped. +# +_item = r'(?:"(?:[^\\"]|\\.)*"|[-\w.]+)' +_re_parse = re.compile(r'\[(%s(?: +%s)*)\]|(\[\[\])|\[#[^\]]*\]' % (_item, _item)) + +_re_args = re.compile(r'"(?:[^\\"]|\\.)*"|[-\w.]+') + +# block commands and their argument counts +_block_cmd_specs = { 'if-index':2, 'for':1, 'is':2, 'define':1, 'format':1 } +_block_cmds = _block_cmd_specs.keys() + +# two regular expresssions for compressing whitespace. the first is used to +# compress any whitespace including a newline into a single newline. the +# second regex is used to compress runs of whitespace into a single space. +_re_newline = re.compile('[ \t\r\f\v]*\n\\s*') +_re_whitespace = re.compile(r'\s\s+') + +# this regex is used to substitute arguments into a value. we split the value, +# replace the relevant pieces, and then put it all back together. splitting +# will produce a list of: TEXT ( splitter TEXT )*. splitter will be '%' or +# an integer. +_re_subst = re.compile('%(%|[0-9]+)') + +class Template: + + def __init__(self, fname=None, compress_whitespace=1, + base_format=FORMAT_RAW): + self.compress_whitespace = compress_whitespace + if fname: + self.parse_file(fname, base_format) + + def parse_file(self, fname, base_format=FORMAT_RAW): + "fname -> a string object with pathname of file containg an EZT template." + + self.parse(_FileReader(fname), base_format) + + def parse(self, text_or_reader, base_format=FORMAT_RAW): + """Parse the template specified by text_or_reader. + + The argument should be a string containing the template, or it should + specify a subclass of ezt.Reader which can read templates. The base + format for printing values is given by base_format. + """ + if not isinstance(text_or_reader, Reader): + # assume the argument is a plain text string + text_or_reader = _TextReader(text_or_reader) + + self.program = self._parse(text_or_reader, base_format=base_format) + + def generate(self, fp, data): + if hasattr(data, '__getitem__') or callable(getattr(data, 'keys', None)): + # a dictionary-like object was passed. convert it to an + # attribute-based object. + class _data_ob: + def __init__(self, d): + vars(self).update(d) + data = _data_ob(data) + + ctx = Context(fp) + ctx.data = data + ctx.for_iterators = { } + ctx.defines = { } + self._execute(self.program, ctx) + + def _parse(self, reader, for_names=None, file_args=(), base_format=None): + """text -> string object containing the template. + + This is a private helper function doing the real work for method parse. + It returns the parsed template as a 'program'. This program is a sequence + made out of strings or (function, argument) 2-tuples. + + Note: comment directives [# ...] are automatically dropped by _re_parse. + """ + + # parse the template program into: (TEXT DIRECTIVE BRACKET)* TEXT + parts = _re_parse.split(reader.text) + + program = [ ] + stack = [ ] + if not for_names: + for_names = [ ] + + if base_format: + program.append((self._cmd_format, _printers[base_format])) + + for i in range(len(parts)): + piece = parts[i] + which = i % 3 # discriminate between: TEXT DIRECTIVE BRACKET + if which == 0: + # TEXT. append if non-empty. + if piece: + if self.compress_whitespace: + piece = _re_whitespace.sub(' ', _re_newline.sub('\n', piece)) + program.append(piece) + elif which == 2: + # BRACKET directive. append '[' if present. + if piece: + program.append('[') + elif piece: + # DIRECTIVE is present. + args = _re_args.findall(piece) + cmd = args[0] + if cmd == 'else': + if len(args) > 1: + raise ArgCountSyntaxError(str(args[1:])) + ### check: don't allow for 'for' cmd + idx = stack[-1][1] + true_section = program[idx:] + del program[idx:] + stack[-1][3] = true_section + elif cmd == 'end': + if len(args) > 1: + raise ArgCountSyntaxError(str(args[1:])) + # note: true-section may be None + try: + cmd, idx, args, true_section = stack.pop() + except IndexError: + raise UnmatchedEndError() + else_section = program[idx:] + if cmd == 'format': + program.append((self._cmd_end_format, None)) + else: + func = getattr(self, '_cmd_' + re.sub('-', '_', cmd)) + program[idx:] = [ (func, (args, true_section, else_section)) ] + if cmd == 'for': + for_names.pop() + elif cmd in _block_cmds: + if len(args) > _block_cmd_specs[cmd] + 1: + raise ArgCountSyntaxError(str(args[1:])) + ### this assumes arg1 is always a ref unless cmd is 'define' + if cmd != 'define': + args[1] = _prepare_ref(args[1], for_names, file_args) + + # handle arg2 for the 'is' command + if cmd == 'is': + args[2] = _prepare_ref(args[2], for_names, file_args) + elif cmd == 'for': + for_names.append(args[1][0]) # append the refname + elif cmd == 'format': + if args[1][0]: + # argument is a variable reference + printer = args[1] + else: + # 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, printer)) + + # remember the cmd, current pos, args, and a section placeholder + stack.append([cmd, len(program), args[1:], None]) + elif cmd == 'include': + if args[1][0] == '"': + include_filename = args[1][1:-1] + f_args = [ ] + for arg in args[2:]: + f_args.append(_prepare_ref(arg, for_names, file_args)) + program.extend(self._parse(reader.read_other(include_filename), + for_names, f_args)) + else: + if len(args) != 2: + raise ArgCountSyntaxError(str(args)) + program.append((self._cmd_include, + (_prepare_ref(args[1], for_names, file_args), + reader))) + elif cmd == 'if-any': + f_args = [ ] + for arg in args[1:]: + f_args.append(_prepare_ref(arg, for_names, file_args)) + stack.append(['if-any', len(program), f_args, None]) + else: + # implied PRINT command + f_args = [ ] + for arg in args: + f_args.append(_prepare_ref(arg, for_names, file_args)) + program.append((self._cmd_print, f_args)) + + if stack: + ### would be nice to say which blocks... + raise UnclosedBlocksError() + return program + + def _execute(self, program, ctx): + """This private helper function takes a 'program' sequence as created + by the method '_parse' and executes it step by step. strings are written + to the file object 'fp' and functions are called. + """ + for step in program: + if isinstance(step, StringType): + ctx.fp.write(step) + else: + step[0](step[1], ctx) + + def _cmd_print(self, valrefs, ctx): + value = _get_value(valrefs[0], ctx) + args = map(lambda valref, ctx=ctx: _get_value(valref, ctx), valrefs[1:]) + _write_value(value, args, ctx) + + 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.printers.pop() + + def _cmd_include(self, (valref, reader), ctx): + fname = _get_value(valref, ctx) + ### note: we don't have the set of for_names to pass into this parse. + ### I don't think there is anything to do but document it. + self._execute(self._parse(reader.read_other(fname)), ctx) + + def _cmd_if_any(self, args, ctx): + "If any value is a non-empty string or non-empty list, then T else F." + (valrefs, t_section, f_section) = args + value = 0 + for valref in valrefs: + if _get_value(valref, ctx): + value = 1 + break + self._do_if(value, t_section, f_section, ctx) + + def _cmd_if_index(self, args, ctx): + ((valref, value), t_section, f_section) = args + iterator = ctx.for_iterators[valref[0]] + if value == 'even': + value = iterator.index % 2 == 0 + elif value == 'odd': + value = iterator.index % 2 == 1 + elif value == 'first': + value = iterator.index == 0 + elif value == 'last': + value = iterator.is_last() + else: + value = iterator.index == int(value) + self._do_if(value, t_section, f_section, ctx) + + def _cmd_is(self, args, ctx): + ((left_ref, right_ref), t_section, f_section) = args + value = _get_value(right_ref, ctx) + value = string.lower(_get_value(left_ref, ctx)) == string.lower(value) + self._do_if(value, t_section, f_section, ctx) + + def _do_if(self, value, t_section, f_section, ctx): + if t_section is None: + t_section = f_section + f_section = None + if value: + section = t_section + else: + section = f_section + if section is not None: + self._execute(section, ctx) + + def _cmd_for(self, args, ctx): + ((valref,), unused, section) = args + list = _get_value(valref, ctx) + if isinstance(list, StringType): + raise NeedSequenceError() + refname = valref[0] + ctx.for_iterators[refname] = iterator = _iter(list) + for unused in iterator: + self._execute(section, ctx) + del ctx.for_iterators[refname] + + def _cmd_define(self, args, ctx): + ((name,), unused, section) = args + origfp = ctx.fp + ctx.fp = cStringIO.StringIO() + if section is not None: + self._execute(section, ctx) + ctx.defines[name] = ctx.fp.getvalue() + ctx.fp = origfp + +def boolean(value): + "Return a value suitable for [if-any bool_var] usage in a template." + if value: + return 'yes' + return None + + +def _prepare_ref(refname, for_names, file_args): + """refname -> a string containing a dotted identifier. example:"foo.bar.bang" + for_names -> a list of active for sequences. + + Returns a `value reference', a 3-tuple made out of (refname, start, rest), + for fast access later. + """ + # is the reference a string constant? + if refname[0] == '"': + return None, refname[1:-1], None + + parts = string.split(refname, '.') + start = parts[0] + rest = parts[1:] + + # if this is an include-argument, then just return the prepared ref + if start[:3] == 'arg': + try: + idx = int(start[3:]) + except ValueError: + pass + else: + if idx < len(file_args): + orig_refname, start, more_rest = file_args[idx] + if more_rest is None: + # the include-argument was a string constant + return None, start, None + + # prepend the argument's "rest" for our further processing + rest[:0] = more_rest + + # rewrite the refname to ensure that any potential 'for' processing + # has the correct name + ### this can make it hard for debugging include files since we lose + ### the 'argNNN' names + if not rest: + return start, start, [ ] + refname = start + '.' + string.join(rest, '.') + + if for_names: + # From last to first part, check if this reference is part of a for loop + for i in range(len(parts), 0, -1): + name = string.join(parts[:i], '.') + if name in for_names: + return refname, name, parts[i:] + + return refname, start, rest + +def _get_value((refname, start, rest), ctx): + """(refname, start, rest) -> a prepared `value reference' (see above). + ctx -> an execution context instance. + + Does a name space lookup within the template name space. Active + for blocks take precedence over data dictionary members with the + same name. + """ + if rest is None: + # it was a string constant + return start + + # get the starting object + if ctx.for_iterators.has_key(start): + ob = ctx.for_iterators[start].last_item + elif ctx.defines.has_key(start): + ob = ctx.defines[start] + elif hasattr(ctx.data, start): + ob = getattr(ctx.data, start) + else: + raise UnknownReference(refname) + + # walk the rest of the dotted reference + for attr in rest: + try: + ob = getattr(ob, attr) + except AttributeError: + raise UnknownReference(refname) + + # make sure we return a string instead of some various Python types + if isinstance(ob, IntType) \ + or isinstance(ob, LongType) \ + or isinstance(ob, FloatType): + return str(ob) + if ob is None: + return '' + + # string or a sequence + return ob + +def _write_value(value, args, ctx): + # value is a callback function, generates its own output + if callable(value): + apply(value, [ctx] + list(args)) + return + + # pop printer in case it recursively calls _write_value + printer = ctx.printers.pop() + + try: + # if the value has a 'read' attribute, then it is a stream: copy it + if hasattr(value, 'read'): + while 1: + chunk = value.read(16384) + if not chunk: + break + printer(ctx, chunk) + + # value is a substitution pattern + elif args: + parts = _re_subst.split(value) + for i in range(len(parts)): + piece = parts[i] + if i%2 == 1 and piece != '%': + idx = int(piece) + if idx < len(args): + piece = args[idx] + else: + piece = '' + printer(ctx, piece) + + # plain old value, write to output + else: + printer(ctx, value) + + finally: + ctx.printers.append(printer) + + +class Context: + """A container for the execution context""" + def __init__(self, fp): + self.fp = fp + self.printers = [] + def write(self, value, args=()): + _write_value(value, args, self) + +class Reader: + "Abstract class which allows EZT to detect Reader objects." + +class _FileReader(Reader): + """Reads templates from the filesystem.""" + def __init__(self, fname): + self.text = open(fname, 'rb').read() + self._dir = os.path.dirname(fname) + def read_other(self, relative): + return _FileReader(os.path.join(self._dir, relative)) + +class _TextReader(Reader): + """'Reads' a template from provided text.""" + def __init__(self, text): + self.text = text + def read_other(self, relative): + raise BaseUnavailableError() + +class _Iterator: + """Specialized iterator for EZT that counts items and can look ahead + + Implements standard iterator interface and provides an is_last() method + and two public members: + + index - integer index of the current item + last_item - last item returned by next()""" + + def __init__(self, sequence): + self._iter = iter(sequence) + + def next(self): + if hasattr(self, '_next_item'): + self.last_item = self._next_item + del self._next_item + else: + self.last_item = self._iter.next() # may raise StopIteration + + if hasattr(self, 'index'): + self.index = self.index + 1 + else: + self.index = 0 + + return self.last_item + + def is_last(self): + """Return true if the current item is the last in the sequence""" + # the only way we can tell if the current item is last is to call next() + # and store the return value so it doesn't get lost + if not hasattr(self, '_next_item'): + try: + self._next_item = self._iter.next() + except StopIteration: + return 1 + return 0 + + def __iter__(self): + return self + +class _OldIterator: + """Alternate implemention of _Iterator for old Pythons without iterators + + This class implements the sequence protocol, instead of the iterator + interface, so it's really not an iterator at all. But it can be used in + python "for" loops as a drop-in replacement for _Iterator. It also provides + the is_last() method and "last_item" and "index" members described in the + _Iterator docstring.""" + + def __init__(self, sequence): + self._seq = sequence + + def __getitem__(self, index): + self.last_item = self._seq[index] # may raise IndexError + self.index = index + return self.last_item + + def is_last(self): + return self.index + 1 >= len(self._seq) + +try: + iter +except NameError: + _iter = _OldIterator +else: + _iter = _Iterator + +class EZTException(Exception): + """Parent class of all EZT exceptions.""" + +class ArgCountSyntaxError(EZTException): + """A bracket directive got the wrong number of arguments.""" + +class UnknownReference(EZTException): + """The template references an object not contained in the data dictionary.""" + +class NeedSequenceError(EZTException): + """The object dereferenced by the template is no sequence (tuple or list).""" + +class UnclosedBlocksError(EZTException): + """This error may be simply a missing [end].""" + +class UnmatchedEndError(EZTException): + """This error may be caused by a misspelled if directive.""" + +class BaseUnavailableError(EZTException): + """Base location is unavailable, which disables includes.""" + +class UnknownFormatConstantError(EZTException): + """The format specifier is an unknown value.""" + +def _raw_printer(ctx, s): + ctx.fp.write(s) + +def _html_printer(ctx, s): + ctx.fp.write(cgi.escape(s)) + +def _uri_printer(ctx, s): + ctx.fp.write(urllib.quote(s)) + +_printers = { + FORMAT_RAW : _raw_printer, + FORMAT_HTML : _html_printer, + FORMAT_XML : _html_printer, + FORMAT_URI : _uri_printer, +} + +# --- standard test environment --- +def test_parse(): + assert _re_parse.split('[a]') == ['', '[a]', None, ''] + assert _re_parse.split('[a] [b]') == \ + ['', '[a]', None, ' ', '[b]', None, ''] + assert _re_parse.split('[a c] [b]') == \ + ['', '[a c]', None, ' ', '[b]', None, ''] + assert _re_parse.split('x [a] y [b] z') == \ + ['x ', '[a]', None, ' y ', '[b]', None, ' z'] + assert _re_parse.split('[a "b" c "d"]') == \ + ['', '[a "b" c "d"]', None, ''] + assert _re_parse.split(r'["a \"b[foo]" c.d f]') == \ + ['', '["a \\"b[foo]" c.d f]', None, ''] + +def _test(argv): + import doctest, ezt + verbose = "-v" in argv + return doctest.testmod(ezt, verbose=verbose) + +if __name__ == "__main__": + # invoke unit test for this module: + import sys + sys.exit(_test(sys.argv)[0]) diff --git a/lib/idiff.py b/lib/idiff.py new file mode 100644 index 00000000..b4cf36e6 --- /dev/null +++ b/lib/idiff.py @@ -0,0 +1,190 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# idiff: display differences between files highlighting intraline changes +# +# ----------------------------------------------------------------------- + +from __future__ import generators + +import difflib +import sys +import re +import ezt +import cgi + +def sidebyside(fromlines, tolines, context): + """Generate side by side diff""" + + ### for some reason mdiff chokes on \n's in input lines + line_strip = lambda line: line.rstrip("\n") + fromlines = map(line_strip, fromlines) + tolines = map(line_strip, tolines) + + gap = False + for fromdata, todata, flag in difflib._mdiff(fromlines, tolines, context): + if fromdata is None and todata is None and flag is None: + gap = True + else: + from_item = _mdiff_split(flag, fromdata) + to_item = _mdiff_split(flag, todata) + yield _item(gap=ezt.boolean(gap), columns=(from_item, to_item)) + gap = False + +_re_mdiff = re.compile("\0([+-^])(.*?)\1") + +def _mdiff_split(flag, (line_number, text)): + """Break up row from mdiff output into segments""" + segments = [] + pos = 0 + while True: + m = _re_mdiff.search(text, pos) + if not m: + segments.append(_item(text=cgi.escape(text[pos:]), type=None)) + break + + if m.start() > pos: + segments.append(_item(text=cgi.escape(text[pos:m.start()]), type=None)) + + if m.group(1) == "+": + segments.append(_item(text=cgi.escape(m.group(2)), type="add")) + elif m.group(1) == "-": + segments.append(_item(text=cgi.escape(m.group(2)), type="remove")) + elif m.group(1) == "^": + segments.append(_item(text=cgi.escape(m.group(2)), type="change")) + + pos = m.end() + + return _item(segments=segments, line_number=line_number) + +def unified(fromlines, tolines, context): + """Generate unified diff""" + + diff = difflib.Differ().compare(fromlines, tolines) + lastrow = None + + for row in _trim_context(diff, context): + if row[0].startswith("? "): + yield _differ_split(lastrow, row[0]) + lastrow = None + else: + if lastrow: + yield _differ_split(lastrow, None) + lastrow = row + + if lastrow: + yield _differ_split(lastrow, None) + +def _trim_context(lines, context_size): + """Trim context lines that don't surround changes from Differ results + + yields (line, leftnum, rightnum, gap) tuples""" + + # circular buffer to hold context lines + context_buffer = [None] * (context_size or 0) + context_start = context_len = 0 + + # number of context lines left to print after encountering a change + context_owed = 0 + + # current line numbers + leftnum = rightnum = 0 + + # whether context lines have been dropped + gap = False + + for line in lines: + row = save = None + + if line.startswith("- "): + leftnum = leftnum + 1 + row = line, leftnum, None + context_owed = context_size + + elif line.startswith("+ "): + rightnum = rightnum + 1 + row = line, None, rightnum + context_owed = context_size + + else: + if line.startswith(" "): + leftnum = leftnum = leftnum + 1 + rightnum = rightnum = rightnum + 1 + if context_owed > 0: + context_owed = context_owed - 1 + elif context_size is not None: + save = True + + row = line, leftnum, rightnum + + if save: + # don't yield row right away, store it in buffer + context_buffer[(context_start + context_len) % context_size] = row + if context_len == context_size: + context_start = (context_start + 1) % context_size + gap = True + else: + context_len = context_len + 1 + else: + # yield row, but first drain stuff in buffer + context_len == context_size + while context_len: + yield context_buffer[context_start] + (gap,) + gap = False + context_start = (context_start + 1) % context_size + context_len = context_len - 1 + yield row + (gap,) + gap = False + +_re_differ = re.compile(r"[+-^]+") + +def _differ_split(row, guide): + """Break row into segments using guide line""" + line, left_number, right_number, gap = row + + if left_number and right_number: + type = "" + elif left_number: + type = "remove" + elif right_number: + type = "add" + + segments = [] + pos = 2 + + if guide: + assert guide.startswith("? ") + + for m in _re_differ.finditer(guide, pos): + if m.start() > pos: + segments.append(_item(text=cgi.escape(line[pos:m.start()]), type=None)) + segments.append(_item(text=cgi.escape(line[m.start():m.end()]), + type="change")) + pos = m.end() + + 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 + ### Python 2.4 and is not part of the public difflib API, so for now + ### fall back if it doesn't exist. + difflib._mdiff +except AttributeError: + sidebyside = None diff --git a/lib/popen.py b/lib/popen.py new file mode 100644 index 00000000..608f6351 --- /dev/null +++ b/lib/popen.py @@ -0,0 +1,379 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- +# +# popen.py: a replacement for os.popen() +# +# This implementation of popen() provides a cmd + args calling sequence, +# rather than a system() type of convention. The shell facilities are not +# available, but that implies we can avoid worrying about shell hacks in +# the arguments. +# +# ----------------------------------------------------------------------- + +import os +import sys +import sapi +import threading +import string + +if sys.platform == "win32": + import win32popen + import win32event + import win32process + import debug + import StringIO + +def popen(cmd, args, mode, capture_err=1): + if sys.platform == "win32": + command = win32popen.CommandLine(cmd, args) + + if string.find(mode, 'r') >= 0: + hStdIn = None + + if debug.SHOW_CHILD_PROCESSES: + dbgIn, dbgOut = None, StringIO.StringIO() + + handle, hStdOut = win32popen.MakeSpyPipe(0, 1, (dbgOut,)) + + if capture_err: + hStdErr = hStdOut + dbgErr = dbgOut + else: + dbgErr = StringIO.StringIO() + x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,)) + else: + handle, hStdOut = win32popen.CreatePipe(0, 1) + if capture_err: + hStdErr = hStdOut + else: + hStdErr = win32popen.NullFile(1) + + else: + if debug.SHOW_CHILD_PROCESSES: + dbgIn, dbgOut, dbgErr = StringIO.StringIO(), StringIO.StringIO(), StringIO.StringIO() + hStdIn, handle = win32popen.MakeSpyPipe(1, 0, (dbgIn,)) + x, hStdOut = win32popen.MakeSpyPipe(None, 1, (dbgOut,)) + x, hStdErr = win32popen.MakeSpyPipe(None, 1, (dbgErr,)) + else: + hStdIn, handle = win32popen.CreatePipe(0, 1) + hStdOut = None + hStdErr = None + + phandle, pid, thandle, tid = win32popen.CreateProcess(command, hStdIn, hStdOut, hStdErr) + + if debug.SHOW_CHILD_PROCESSES: + debug.Process(command, dbgIn, dbgOut, dbgErr) + + return _pipe(win32popen.File2FileObject(handle, mode), phandle) + + # flush the stdio buffers since we are about to change the FD under them + sys.stdout.flush() + sys.stderr.flush() + + r, w = os.pipe() + pid = os.fork() + if pid: + # in the parent + + # close the descriptor that we don't need and return the other one. + if string.find(mode, 'r') >= 0: + os.close(w) + return _pipe(os.fdopen(r, mode), pid) + os.close(r) + return _pipe(os.fdopen(w, mode), pid) + + # in the child + + # we'll need /dev/null for the discarded I/O + null = os.open('/dev/null', os.O_RDWR) + + 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 + ### this isn't quite right... we may want the child to read from stdin + os.dup2(null, 0) + # what to do with errors? + if capture_err: + os.dup2(w, 2) + else: + os.dup2(null, 2) + else: + # hook stdin to the "read" channel + os.dup2(r, 0) + # "close" stdout/stderr; the child shouldn't use them + ### this isn't quite right... we may want the child to write to these + os.dup2(null, 1) + os.dup2(null, 2) + + # don't need these FDs any more + os.close(null) + os.close(r) + os.close(w) + + # the stdin/stdout/stderr are all set up. exec the target + try: + os.execvp(cmd, (cmd,) + tuple(args)) + except: + # aid debugging, if the os.execvp above fails for some reason: + print "

exec failed:

", cmd, string.join(args), "
" + 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." + + def __init__(self, file, child_pid, done_event = None, thread = None): + self.file = file + self.child_pid = child_pid + if sys.platform == "win32": + if done_event: + self.wait_for = (child_pid, done_event) + else: + self.wait_for = (child_pid,) + else: + self.thread = thread + + def eof(self): + ### should be calling file.eof() here instead of file.close(), there + ### may be data in the pipe or buffer after the process exits + if sys.platform == "win32": + r = win32event.WaitForMultipleObjects(self.wait_for, 1, 0) + if r == win32event.WAIT_OBJECT_0: + self.file.close() + self.file = None + return win32process.GetExitCodeProcess(self.child_pid) + return None + + if self.thread and self.thread.isAlive(): + return None + + pid, status = os.waitpid(self.child_pid, os.WNOHANG) + if pid: + self.file.close() + self.file = None + return status + return None + + def close(self): + if self.file: + self.file.close() + self.file = None + if sys.platform == "win32": + win32event.WaitForMultipleObjects(self.wait_for, 1, win32event.INFINITE) + return win32process.GetExitCodeProcess(self.child_pid) + else: + if self.thread: + self.thread.join() + 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] + return None + + def __getattr__(self, name): + return getattr(self.file, name) + + def __del__(self): + self.close() diff --git a/lib/py2html.py b/lib/py2html.py new file mode 100755 index 00000000..3c8c9912 --- /dev/null +++ b/lib/py2html.py @@ -0,0 +1,541 @@ +#!/usr/bin/python -u + +""" Python Highlighter Version: 0.8 + + py2html.py [options] files... + + options: + -h print help + - read from stdin, write to stdout + -stdout read from files, write to stdout + -files read from files, write to filename+'.html' (default) + -format: + html output XHTML page (default) + rawhtml output pure XHTML (without headers, titles, etc.) + -mode: + color output in color (default) + mono output b/w (for printing) + -title:Title use 'Title' as title of the generated page + -bgcolor:color use color as background-color for page + -header:file use contents of file as header + -footer:file use contents of file as footer + -URL replace all occurances of 'URL: link' with + 'link'; this is always enabled + in CGI mode + -v verbose + + Takes the input, assuming it is Python code and formats it into + colored XHTML. When called without parameters the script tries to + work in CGI mode. It looks for a field 'script=URL' and tries to + use that URL as input file. If it can't find this field, the path + info (the part of the URL following the CGI script name) is + tried. In case no host is given, the host where the CGI script + lives and HTTP are used. + + * Uses Just van Rossum's PyFontify version 0.3 to tag Python scripts. + You can get it via his homepage on starship: + URL: http://starship.python.net/crew/just +""" +__comments__ = """ + + The following snippet is a small shell script I use for viewing + Python scripts via less on Unix: + +pyless: +#!/bin/sh +# Browse pretty printed Python code using ANSI codes for highlighting +py2html -stdout -format:ansi -mode:color $* | less -r + + History: + + 0.8: Added patch by Patrick Lynch to have py2html.py use style + sheets for markup + 0.7: Added patch by Ville Skyttä to make py2html.py output + valid XHTML. + 0.6: Fixed a bug in .escape_html(); thanks to Vespe Savikko for + finding this one. + 0.5: Added a few suggestions by Kevin Ng to make the CGI version + a little more robust. + +""" +__copyright__ = """\ + Copyright (c) 1998-2000, Marc-Andre Lemburg; mailto:mal@lemburg.com + Copyright (c) 2000-2002, eGenix.com Software GmbH; mailto:info@egenix.com + Distributed under the terms and conditions of the eGenix.com Public + License. See http://www.egenix.com/files/python/mxLicense.html for + details, or contact the author. All Rights Reserved.\ +""" + +__version__ = '0.8' + +__cgifooter__ = ('\n
# code highlighted using py2html.py '
+                 'version %s
\n' % __version__) + +import sys,string,re + +# Adjust path so that PyFontify is found... +sys.path.append('.') + +### Constants + +# URL of the input form the user is redirected to in case no script=xxx +# form field is given. The URL *must* be absolute. Leave blank to +# have the script issue an error instead. +INPUT_FORM = 'http://www.lemburg.com/files/python/SoftwareDescriptions.html#py2html.py' + +# HTML DOCTYPE and XML namespace +HTML_DOCTYPE = '' +HTML_XMLNS = ' xmlns="http://www.w3.org/1999/xhtml"' + +### Helpers + +def fileio(file, mode='rb', data=None, close=0): + + if type(file) == type(''): + f = open(file,mode) + close = 1 + else: + f = file + if data: + f.write(data) + else: + data = f.read() + if close: f.close() + return data + +### Converter class + +class PrettyPrint: + + """ generic Pretty Printer class + + * supports tagging Python scripts in the following ways: + + # format/mode | color mono + # -------------------------- + # rawhtml | x x (HTML without headers, etc.) + # html | x x (a HTML page with HEAD&BODY:) + # ansi | x x (with Ansi-escape sequences) + + * interfaces: + + file_filter -- takes two files: input & output (may be stdin/stdout) + filter -- takes a string and returns the highlighted version + + * to create an instance use: + + c = PrettyPrint(tagfct,format,mode) + + where format and mode must be strings according to the + above table if you plan to use PyFontify.fontify as + tagfct + + * the tagfct has to take one argument, text, and return a taglist + (format: [(id,left,right,sublist),...], where id is the + "name" given to the slice left:right in text and sublist is a + taglist for tags inside the slice or None) + + """ + + # misc settings + title = '' + bgcolor = '#FFFFFF' + css = '' + header = '' + footer = '' + replace_URLs = 0 + # formats to be used + formats = {} + + def __init__(self,tagfct=None,format='html',mode='color'): + + self.tag = tagfct + self.set_mode = getattr(self,'set_mode_%s_%s' % (format, mode)) + self.filter = getattr(self,'filter_%s' % format) + + def file_filter(self,infile,outfile): + + self.set_mode() + text = fileio(infile,'r') + if type(infile) == type('') and self.title == '': + self.title = infile + fileio(outfile,'w',self.filter(text)) + + ### Set pre- and postfixes for formats & modes + # + # These methods must set self.formats to a dictionary having + # an entry for every tag returned by the tagging function. + # + # The format used is simple: + # tag:(prefix,postfix) + # where prefix and postfix are either strings or callable objects, + # that return a string (they are called with the matching tag text + # as only parameter). prefix is inserted in front of the tag, postfix + # is inserted right after the tag. + + def set_mode_html_color(self): + + self.css = """ + """ % self.bgcolor + + self.formats = { + 'all':('
','
'), + 'comment':('',''), + 'keyword':('',''), + 'parameter':('',''), + 'identifier':( lambda x,strip=string.strip: + '' % (strip(x)), + ''), + 'string':('','') + } + + set_mode_rawhtml_color = set_mode_html_color + + def set_mode_html_mono(self): + + self.css = """ + """ % self.bgcolor + + self.formats = { + 'all':('
','
'), + 'comment':('',''), + 'keyword':( '',''), + 'parameter':('',''), + 'identifier':( lambda x,strip=string.strip: + '' % (strip(x)), + ''), + 'string':('','') + } + + set_mode_rawhtml_mono = set_mode_html_mono + + def set_mode_ansi_mono(self): + + self.formats = { + 'all':('',''), + 'comment':('\033[2m','\033[m'), + 'keyword':('\033[4m','\033[m'), + 'parameter':('',''), + 'identifier':('\033[1m','\033[m'), + 'string':('','') + } + + def set_mode_ansi_color(self): + + self.formats = { + 'all':('',''), + 'comment':('\033[34;2m','\033[m'), + 'keyword':('\033[1;34m','\033[m'), + 'parameter':('',''), + 'identifier':('\033[1;31m','\033[m'), + 'string':('\033[32;2m','\033[m') + } + + ### Filters for Python scripts given as string + + def escape_html(self,text): + + t = (('&','&'),('<','<'),('>','>')) + for x,y in t: + text = string.join(string.split(text,x),y) + return text + + def filter_html(self,text): + + output = self.fontify(self.escape_html(text)) + if self.replace_URLs: + output = re.sub('URL:([ \t]+)([^ \n\r<]+)', + 'URL:\\1\\2',output) + html = """%s + + %s + + %s + + + + %s + + %s + + %s + \n"""%(HTML_DOCTYPE, + HTML_XMLNS, + self.title, + self.css, + self.header, + output, + self.footer) + return html + + def filter_rawhtml(self,text): + + output = self.fontify(self.escape_html(text)) + if self.replace_URLs: + output = re.sub('URL:([ \t]+)([^ \n\r<]+)', + 'URL:\\1\\2',output) + return self.header + output + self.footer + + def filter_ansi(self,text): + + output = self.fontify(text) + return self.header + output + self.footer + + ### Fontify engine + + def fontify(self,pytext): + + # parse + taglist = self.tag(pytext) + + # prepend special 'all' tag: + taglist[:0] = [('all',0,len(pytext),None)] + + # prepare splitting + splits = [] + addsplits(splits,pytext,self.formats,taglist) + + # do splitting & inserting + splits.sort() + l = [] + li = 0 + for ri,dummy,insert in splits: + if ri > li: l.append(pytext[li:ri]) + l.append(insert) + li = ri + if li < len(pytext): l.append(pytext[li:]) + + return string.join(l,'') + +def addsplits(splits,text,formats,taglist): + + """ Helper for .fontify() + """ + for id,left,right,sublist in taglist: + try: + pre,post = formats[id] + except KeyError: + # sys.stderr.write('Warning: no format for %s specified\n'%repr(id)) + pre,post = '','' + if type(pre) != type(''): + pre = pre(text[left:right]) + if type(post) != type(''): + post = post(text[left:right]) + # len(splits) is a dummy used to make sorting stable + splits.append((left,len(splits),pre)) + if sublist: + addsplits(splits,text,formats,sublist) + splits.append((right,len(splits),post)) + +def write_html_error(titel,text): + + print """\ +%s%s + +

%s

+%s + +""" % (HTML_DOCTYPE,HTML_XMLNS,titel,titel,text) + +def redirect_to(url): + + sys.stdout.write('Content-Type: text/html\r\n') + sys.stdout.write('Status: 302\r\n') + sys.stdout.write('Location: %s\r\n\r\n' % url) + print """ +%s +302 Moved Temporarily + +

302 Moved Temporarily

+The document has moved to %s.

+ +""" % (HTML_DOCTYPE,HTML_XMLNS,url,url) + +def main(cmdline): + + """ main(cmdline) -- process cmdline as if it were sys.argv + """ + # parse options/files + options = [] + optvalues = {} + for o in cmdline[1:]: + if o[0] == '-': + if ':' in o: + k,v = tuple(string.split(o,':')) + optvalues[k] = v + options.append(k) + else: + options.append(o) + else: + break + files = cmdline[len(options)+1:] + + ### create converting object + + # load fontifier + if '-marcs' in options: + # use mxTextTool's tagging engine as fontifier + from mx.TextTools import tag + from mx.TextTools.Examples.Python import python_script + tagfct = lambda text,tag=tag,pytable=python_script: \ + tag(text,pytable)[1] + print "Py2HTML: using Marc's tagging engine" + else: + # load Just's fontifier + try: + import PyFontify + if PyFontify.__version__ < '0.3': raise ValueError + tagfct = PyFontify.fontify + except: + print """ + Sorry, but this script needs the PyFontify.py module version 0.3; + You can download it from Just's homepage at + + URL: http://starship.python.net/crew/just +""" + sys.exit() + + + if '-format' in options: + format = optvalues['-format'] + else: + # use default + format = 'html' + + if '-mode' in options: + mode = optvalues['-mode'] + else: + # use default + mode = 'color' + + c = PrettyPrint(tagfct,format,mode) + convert = c.file_filter + + ### start working + + if '-title' in options: + c.title = optvalues['-title'] + + if '-bgcolor' in options: + c.bgcolor = optvalues['-bgcolor'] + + if '-header' in options: + try: + f = open(optvalues['-header']) + c.header = f.read() + f.close() + except IOError: + if verbose: print 'IOError: header file not found' + + if '-footer' in options: + try: + f = open(optvalues['-footer']) + c.footer = f.read() + f.close() + except IOError: + if verbose: print 'IOError: footer file not found' + + if '-URL' in options: + c.replace_URLs = 1 + + if '-' in options: + convert(sys.stdin,sys.stdout) + sys.exit() + + if '-h' in options: + print __doc__ + sys.exit() + + if len(files) == 0: + # Turn URL processing on + c.replace_URLs = 1 + # Try CGI processing... + import cgi,urllib,urlparse,os + form = cgi.FieldStorage() + if not form.has_key('script'): + # Ok, then try pathinfo + if not os.environ.has_key('PATH_INFO'): + if INPUT_FORM: + redirect_to(INPUT_FORM) + else: + sys.stdout.write('Content-Type: text/html\r\n\r\n') + write_html_error('Missing Parameter', + 'Missing script=URL field in request') + sys.exit(1) + url = os.environ['PATH_INFO'][1:] # skip the leading slash + else: + url = form['script'].value + sys.stdout.write('Content-Type: text/html\r\n\r\n') + scheme, host, path, params, query, frag = urlparse.urlparse(url) + if not host: + scheme = 'http' + if os.environ.has_key('HTTP_HOST'): + host = os.environ['HTTP_HOST'] + else: + host = 'localhost' + url = urlparse.urlunparse((scheme, host, path, params, query, frag)) + #print url; sys.exit() + network = urllib.URLopener() + try: + tempfile,headers = network.retrieve(url) + except IOError,reason: + write_html_error('Error opening "%s"' % url, + 'The given URL could not be opened. Reason: %s' %\ + str(reason)) + sys.exit(1) + f = open(tempfile,'rb') + c.title = url + c.footer = __cgifooter__ + convert(f,sys.stdout) + f.close() + network.close() + sys.exit() + + if '-stdout' in options: + filebreak = '-'*72 + for f in files: + try: + if len(files) > 1: + print filebreak + print 'File:',f + print filebreak + convert(f,sys.stdout) + except IOError: + pass + else: + verbose = ('-v' in options) + if verbose: + print 'Py2HTML: working on', + for f in files: + try: + if verbose: print f, + convert(f,f+'.html') + except IOError: + if verbose: print '(IOError!)', + if verbose: + print + print 'Done.' + +if __name__=='__main__': + main(sys.argv) + + diff --git a/lib/query.py b/lib/query.py new file mode 100644 index 00000000..523d33d8 --- /dev/null +++ b/lib/query.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 1999-2008 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/ +# +# ----------------------------------------------------------------------- +# +# CGI script to process and display queries to CVSdb +# +# This script is part of the ViewVC package. More information can be +# found at http://viewvc.org +# +# ----------------------------------------------------------------------- + +import os +import sys +import string +import time + +import cvsdb +import viewvc +import ezt +import debug +import urllib +import fnmatch + +class FormData: + def __init__(self, form): + self.valid = 0 + + self.repository = "" + self.branch = "" + self.directory = "" + self.file = "" + self.who = "" + self.sortby = "" + self.date = "" + self.hours = 0 + + self.decode_thyself(form) + + def decode_thyself(self, form): + try: + self.repository = string.strip(form["repository"].value) + except KeyError: + pass + except TypeError: + pass + else: + self.valid = 1 + + try: + self.branch = string.strip(form["branch"].value) + except KeyError: + pass + except TypeError: + pass + else: + self.valid = 1 + + try: + self.directory = string.strip(form["directory"].value) + except KeyError: + pass + except TypeError: + pass + else: + self.valid = 1 + + try: + self.file = string.strip(form["file"].value) + except KeyError: + pass + except TypeError: + pass + else: + self.valid = 1 + + try: + self.who = string.strip(form["who"].value) + except KeyError: + pass + except TypeError: + pass + else: + self.valid = 1 + + try: + self.sortby = string.strip(form["sortby"].value) + except KeyError: + pass + except TypeError: + pass + + try: + self.date = string.strip(form["date"].value) + except KeyError: + pass + except TypeError: + pass + + try: + self.hours = int(form["hours"].value) + except KeyError: + pass + except TypeError: + pass + except ValueError: + pass + else: + self.valid = 1 + +## returns a tuple-list (mod-str, string) +def listparse_string(str): + return_list = [] + + cmd = "" + temp = "" + escaped = 0 + state = "eat leading whitespace" + + for c in str: + ## handle escaped charactors + if not escaped and c == "\\": + escaped = 1 + continue + + ## strip leading white space + if state == "eat leading whitespace": + if c in string.whitespace: + continue + else: + state = "get command or data" + + ## parse to '"' or "," + if state == "get command or data": + + ## just add escaped charactors + if escaped: + escaped = 0 + temp = temp + c + continue + + ## the data is in quotes after the command + elif c == "\"": + cmd = temp + temp = "" + state = "get quoted data" + continue + + ## this tells us there was no quoted data, therefore no + ## command; add the command and start over + elif c == ",": + ## strip ending whitespace on un-quoted data + temp = string.rstrip(temp) + return_list.append( ("", temp) ) + temp = "" + state = "eat leading whitespace" + continue + + ## record the data + else: + temp = temp + c + continue + + ## parse until ending '"' + if state == "get quoted data": + + ## just add escaped charactors + if escaped: + escaped = 0 + temp = temp + c + continue + + ## look for ending '"' + elif c == "\"": + return_list.append( (cmd, temp) ) + cmd = "" + temp = "" + state = "eat comma after quotes" + continue + + ## record the data + else: + temp = temp + c + continue + + ## parse until "," + if state == "eat comma after quotes": + if c in string.whitespace: + continue + + elif c == ",": + state = "eat leading whitespace" + continue + + else: + print "format error" + sys.exit(1) + + if cmd or temp: + return_list.append((cmd, temp)) + + return return_list + +def decode_command(cmd): + if cmd == "r": + return "regex" + elif cmd == "l": + return "like" + else: + return "exact" + +def form_to_cvsdb_query(form_data): + query = cvsdb.CreateCheckinQuery() + + if form_data.repository: + for cmd, str in listparse_string(form_data.repository): + cmd = decode_command(cmd) + query.SetRepository(str, cmd) + + if form_data.branch: + for cmd, str in listparse_string(form_data.branch): + cmd = decode_command(cmd) + query.SetBranch(str, cmd) + + if form_data.directory: + for cmd, str in listparse_string(form_data.directory): + cmd = decode_command(cmd) + query.SetDirectory(str, cmd) + + if form_data.file: + for cmd, str in listparse_string(form_data.file): + cmd = decode_command(cmd) + query.SetFile(str, cmd) + + if form_data.who: + for cmd, str in listparse_string(form_data.who): + cmd = decode_command(cmd) + query.SetAuthor(str, cmd) + + if form_data.sortby == "author": + query.SetSortMethod("author") + elif form_data.sortby == "file": + query.SetSortMethod("file") + else: + query.SetSortMethod("date") + + if form_data.date: + if form_data.date == "hours" and form_data.hours: + query.SetFromDateHoursAgo(form_data.hours) + elif form_data.date == "day": + query.SetFromDateDaysAgo(1) + elif form_data.date == "week": + query.SetFromDateDaysAgo(7) + elif form_data.date == "month": + query.SetFromDateDaysAgo(31) + + return query + +def prev_rev(rev): + '''Returns a string representing the previous revision of the argument.''' + 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 string.join(r, '.') + +def is_forbidden(cfg, cvsroot_name, module): + auth_params = cfg.get_authorizer_params('forbidden', cvsroot_name) + forbidden = auth_params.get('forbidden', '') + forbidden = map(string.strip, filter(None, string.split(forbidden, ','))) + 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=[]) + + if desc: + ob.log = string.replace(server.escape(desc), '\n', '
') + else: + ob.log = ' ' + + 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, string.split(directory, '/'))[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)): + continue + + ctime = commit.GetTime() + if not ctime: + ctime = " " + else: + if (cfg.options.use_localtime): + 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' + + ## make the file link + try: + file = (directory and directory + "/") + commit.GetFile() + except: + raise Exception, str([directory, commit.GetFile()]) + + ## if we couldn't find the cvsroot path configured in the + ## viewvc.conf file, then don't make the link + if cvsroot_name: + flink = '[%s] %s' % ( + cvsroot_name, viewvc_link, urllib.quote(file), + cvsroot_name, file) + if commit.GetType() == commit.CHANGE: + dlink = '%s/%s?root=%s&view=diff&r1=%s&r2=%s' % ( + viewvc_link, urllib.quote(file), cvsroot_name, + prev_rev(commit.GetRevision()), commit.GetRevision()) + else: + dlink = None + else: + flink = '[%s] %s' % (repository, file) + dlink = None + + ob.files.append(_item(date=ctime, + author=commit.GetAuthor(), + link=flink, + rev=commit.GetRevision(), + branch=commit.GetBranch(), + plus=int(commit.GetPlusCount()), + minus=int(commit.GetMinusCount()), + type=commit.GetTypeString(), + difflink=dlink, + )) + + return ob + +def run_query(server, cfg, form_data, viewvc_link): + query = form_to_cvsdb_query(form_data) + db = cvsdb.ConnectDatabaseReadOnly(cfg) + db.RunQuery(query) + + if not query.commit_list: + return [ ] + + commits = [ ] + files = [ ] + + cvsroots = {} + rootitems = cfg.general.svn_roots.items() + cfg.general.cvs_roots.items() + for key, value in rootitems: + cvsroots[cvsdb.CleanRepository(value)] = key + + current_desc = query.commit_list[0].GetDescription() + for commit in query.commit_list: + desc = commit.GetDescription() + if current_desc == desc: + files.append(commit) + continue + + commits.append(build_commit(server, cfg, current_desc, files, + cvsroots, viewvc_link)) + + files = [ commit ] + current_desc = desc + + ## add the last file group to the commit list + 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 + +def main(server, cfg, viewvc_link): + try: + + form = server.FieldStorage() + form_data = FormData(form) + + if form_data.valid: + commits = run_query(server, cfg, form_data, viewvc_link) + query = None + else: + commits = [ ] + query = 'skipped' + + script_name = server.getenv('SCRIPT_NAME', '') + + data = { + 'cfg' : cfg, + 'address' : cfg.general.address, + 'vsn' : viewvc.__version__, + + '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, + 'commits' : commits, + 'num_commits' : len(commits), + 'rss_href' : None, + } + + if form_data.hours: + data['hours'] = form_data.hours + else: + data['hours'] = 2 + + server.header() + + # generate the page + template = viewvc.get_view_template(cfg, "query") + template.generate(server.file(), data) + + except SystemExit, e: + pass + except: + exc_info = debug.GetExceptionData() + server.header(status=exc_info['status']) + debug.PrintException(server, exc_info) + +class _item: + def __init__(self, **kw): + vars(self).update(kw) diff --git a/lib/sapi.py b/lib/sapi.py new file mode 100644 index 00000000..123f093e --- /dev/null +++ b/lib/sapi.py @@ -0,0 +1,391 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- +# +# generic server api - currently supports normal cgi, mod_python, and +# active server pages +# +# ----------------------------------------------------------------------- + +import types +import string +import os +import sys +import re + + +# global server object. It will be either a CgiServer or a proxy to +# an AspServer or ModPythonServer object. +server = None + + +class Server: + def __init__(self): + self.pageGlobals = {} + + def self(self): + return self + + def close(self): + pass + + +class ThreadedServer(Server): + def __init__(self): + Server.__init__(self) + + self.inheritableOut = 0 + + global server + if not isinstance(server, ThreadedServerProxy): + server = ThreadedServerProxy() + if not isinstance(sys.stdout, File): + sys.stdout = File(server) + server.registerThread(self) + + def file(self): + return File(self) + + def close(self): + server.unregisterThread() + + +class ThreadedServerProxy: + """In a multithreaded server environment, ThreadedServerProxy stores the + different server objects being used to display pages and transparently + forwards access to them based on the current thread id.""" + + def __init__(self): + self.__dict__['servers'] = { } + global thread + import thread + + def registerThread(self, server): + self.__dict__['servers'][thread.get_ident()] = server + + def unregisterThread(self): + del self.__dict__['servers'][thread.get_ident()] + + def self(self): + """This function bypasses the getattr and setattr trickery and returns + the actual server object.""" + return self.__dict__['servers'][thread.get_ident()] + + def __getattr__(self, key): + return getattr(self.self(), key) + + def __setattr__(self, key, value): + setattr(self.self(), key, value) + + def __delattr__(self, key): + delattr(self.self(), key) + + +class File: + def __init__(self, server): + self.closed = 0 + self.mode = 'w' + self.name = "" + self.softspace = 0 + self.server = server + + def write(self, s): + self.server.write(s) + + def writelines(self, list): + for s in list: + self.server.write(s) + + def flush(self): + self.server.flush() + + def truncate(self, size): + pass + + def close(self): + pass + + +class CgiServer(Server): + def __init__(self, inheritableOut = 1): + Server.__init__(self) + self.headerSent = 0 + self.headers = [] + self.inheritableOut = inheritableOut + self.iis = os.environ.get('SERVER_SOFTWARE', '')[:13] == 'Microsoft-IIS' + + if sys.platform == "win32" and inheritableOut: + import msvcrt + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + + 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): + if not self.headerSent: + self.headerSent = 1 + + extraheaders = '' + for (name, value) in self.headers: + extraheaders = extraheaders + '%s: %s\r\n' % (name, value) + + # The only way ViewVC pages and error messages are visible under + # IIS is if a 200 error code is returned. Otherwise IIS instead + # sends the static error page corresponding to the code number. + if status is None or (status[:3] != '304' and self.iis): + status = '' + else: + status = 'Status: %s\r\n' % status + + sys.stdout.write('%sContent-Type: %s\r\n%s\r\n' + % (status, content_type, extraheaders)) + + def redirect(self, url): + if self.iis: url = fix_iis_url(self, url) + self.addheader('Location', url) + self.header(status='301 Moved') + print 'This document is located here.' % 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) + if self.iis and name == 'PATH_INFO' and ret: + ret = fix_iis_path_info(self, ret) + return ret + + def params(self): + return cgi.parse() + + 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) + + def write(self, s): + sys.stdout.write(s) + + def flush(self): + sys.stdout.flush() + + def file(self): + return sys.stdout + + +class AspServer(ThreadedServer): + def __init__(self, Server, Request, Response, Application): + ThreadedServer.__init__(self) + self.headerSent = 0 + self.server = Server + self.request = Request + self.response = Response + self.application = Application + + def addheader(self, name, value): + self.response.AddHeader(name, value) + + def header(self, content_type=None, status=None): + # Normally, setting self.response.ContentType after headers have already + # been sent simply results in an AttributeError exception, but sometimes + # it leads to a fatal ASP error. For this reason I'm keeping the + # self.headerSent member and only checking for the exception as a + # secondary measure + 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 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)() + if not type(ret) is types.UnicodeType: + return value + ret = str(ret) + if name == 'PATH_INFO': + ret = fix_iis_path_info(self, ret) + return ret + + def params(self): + p = {} + for i in self.request.Form: + p[str(i)] = map(str, self.request.Form[i]) + for i in self.request.QueryString: + p[str(i)] = map(str, self.request.QueryString[i]) + return p + + def FieldStorage(self, fp=None, headers=None, outerboundary="", + environ=os.environ, keep_blank_values=0, strict_parsing=0): + + # Code based on a very helpful usenet post by "Max M" (maxm@mxm.dk) + # Subject "Re: Help! IIS and Python" + # http://groups.google.com/groups?selm=3C7C0AB6.2090307%40mxm.dk + + from StringIO import StringIO + from cgi import FieldStorage + + environ = {} + for i in self.request.ServerVariables: + environ[str(i)] = str(self.request.ServerVariables(i)()) + + # this would be bad for uploaded files, could use a lot of memory + binaryContent, size = self.request.BinaryRead(int(environ['CONTENT_LENGTH'])) + + fp = StringIO(str(binaryContent)) + fs = FieldStorage(fp, None, "", environ, keep_blank_values, strict_parsing) + fp.close() + return fs + + def write(self, s): + t = type(s) + if t is types.StringType: + s = buffer(s) + elif not t is types.BufferType: + s = buffer(str(s)) + + self.response.BinaryWrite(s) + + def flush(self): + self.response.Flush() + + +_re_status = re.compile("\\d+") + + +class ModPythonServer(ThreadedServer): + def __init__(self, request): + ThreadedServer.__init__(self) + 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' + else: + self.request.content_type = content_type + self.headerSent = 1 + + if status is not None: + m = _re_status.match(status) + if not m is None: + self.request.status = int(m.group()) + + def redirect(self, url): + import mod_python.apache + self.request.headers_out['Location'] = url + self.request.status = mod_python.apache.HTTP_MOVED_TEMPORARILY + self.request.write("You are being redirected to %s" + % (url, url)) + sys.exit() + + def escape(self, s, quote = None): + return cgi.escape(s, quote) + + def getenv(self, name, value = None): + try: + return self.request.subprocess_env[name] + except KeyError: + return value + + def params(self): + import mod_python.util + if self.request.args is None: + return {} + else: + return mod_python.util.parse_qs(self.request.args) + + def FieldStorage(self, fp=None, headers=None, outerboundary="", + environ=os.environ, keep_blank_values=0, strict_parsing=0): + import mod_python.util + return mod_python.util.FieldStorage(self.request, keep_blank_values, strict_parsing) + + def write(self, s): + self.request.write(s) + + def flush(self): + pass + + +def fix_iis_url(server, url): + """When a CGI application under IIS outputs a "Location" header with a url + beginning with a forward slash, IIS tries to optimise the redirect by not + returning any output from the original CGI script at all and instead just + returning the new page in its place. Because of this, the browser does + not know it is getting a different page than it requested. As a result, + The address bar that appears in the browser window shows the wrong location + and if the new page is in a different folder than the old one, any relative + links on it will be broken. + + This function can be used to circumvent the IIS "optimization" of local + redirects. If it is passed a location that begins with a forward slash it + will return a URL constructed with the information in CGI environment. + If it is passed a URL or any location that doens't begin with a forward slash + it will return just argument unaltered. + """ + if url[0] == '/': + if server.getenv('HTTPS') == 'on': + dport = "443" + prefix = "https://" + else: + dport = "80" + prefix = "http://" + prefix = prefix + server.getenv('HTTP_HOST') + if server.getenv('SERVER_PORT') != dport: + prefix = prefix + ":" + server.getenv('SERVER_PORT') + return prefix + url + return url + + +def fix_iis_path_info(server, path_info): + """Fix the PATH_INFO value in IIS""" + # If the viewvc cgi's are in the /viewvc/ folder on the web server and a + # request looks like + # + # /viewvc/viewvc.cgi/myproject/?someoption + # + # The CGI environment variables on IIS will look like this: + # + # SCRIPT_NAME = /viewvc/viewvc.cgi + # PATH_INFO = /viewvc/viewvc.cgi/myproject/ + # + # Whereas on Apache they look like: + # + # SCRIPT_NAME = /viewvc/viewvc.cgi + # PATH_INFO = /myproject/ + # + # This function converts the IIS PATH_INFO into the nonredundant form + # expected by ViewVC + return path_info[len(server.getenv('SCRIPT_NAME', '')):] diff --git a/lib/vcauth/__init__.py b/lib/vcauth/__init__.py new file mode 100644 index 00000000..5261415a --- /dev/null +++ b/lib/vcauth/__init__.py @@ -0,0 +1,49 @@ +# -*-python-*- +# +# Copyright (C) 2006-2008 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.""" + +import string +import vclib + + +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_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_path_access(self, rootname, path_parts, pathtype, rev=None): + return 1 diff --git a/lib/vcauth/forbidden/__init__.py b/lib/vcauth/forbidden/__init__.py new file mode 100644 index 00000000..30dfa45b --- /dev/null +++ b/lib/vcauth/forbidden/__init__.py @@ -0,0 +1,46 @@ +# -*-python-*- +# +# Copyright (C) 2006-2008 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 string + +class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): + """A simple top-level module authorizer.""" + def __init__(self, username, params={}): + forbidden = params.get('forbidden', '') + self.forbidden = map(string.strip, + filter(None, string.split(forbidden, ','))) + + def check_root_access(self, rootname): + return 1 + + 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 diff --git a/lib/vcauth/forbiddenre/__init__.py b/lib/vcauth/forbiddenre/__init__.py new file mode 100644 index 00000000..61d54e47 --- /dev/null +++ b/lib/vcauth/forbiddenre/__init__.py @@ -0,0 +1,58 @@ +# -*-python-*- +# +# Copyright (C) 2008 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 string +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(string.strip(x)), + filter(None, string.split(forbidden, ','))) + + 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_path_access(self, rootname, path_parts, pathtype, rev=None): + root_path = rootname + if path_parts: + root_path = root_path + '/' + string.join(path_parts, '/') + if pathtype == vclib.DIR: + root_path = root_path + '/' + else: + root_path = root_path + '/' + return self._check_root_path_access(root_path) + diff --git a/lib/vcauth/svnauthz/__init__.py b/lib/vcauth/svnauthz/__init__.py new file mode 100644 index 00000000..1137a48d --- /dev/null +++ b/lib/vcauth/svnauthz/__init__.py @@ -0,0 +1,223 @@ +# -*-python-*- +# +# Copyright (C) 2006-2008 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 + +import vcauth +import string +import os.path +import debug + +from ConfigParser import ConfigParser + +class ViewVCAuthorizer(vcauth.GenericViewVCAuthorizer): + """Subversion authz authorizer module""" + + def __init__(self, username, params={}): + self.username = username + 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") + + def _get_paths_for_root(self, rootname): + if self.rootpaths.has_key(rootname): + return self.rootpaths[rootname] + + paths_for_root = { } + + # Parse the authz file. + cp = ConfigParser() + cp.read(self.authz_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 = string.split(cp.get('groups', groupname), ',') + for entry in entries: + entry = string.strip(entry) + 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 = string.strip(user) + if _userspec_matches_user(user): + # See if the 'r' permission is among the ones granted to + # USER. If so, we can stop looking. (Entry order is not + # relevant -- we'll use the most permissive entry, meaning + # one 'allow' is all we need.) + allow = string.find(cp.get(section, user), 'r') != -1 + 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 = string.split(section, ':', 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 = '/' + string.join(filter(None, string.split(path, '/')), '/') + paths_for_root[path] = allow + + # Okay. Superimpose those root-specific values now. + for section in root_sections: + + # Get the path again. + name, path = string.split(section, ':', 1) + + # 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 = '/' + string.join(filter(None, string.split(path, '/')), '/') + 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_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 = '/' + string.join(parts, '/') + if paths.has_key(path): + return paths[path] + del parts[-1] + return paths.get('/', 0) diff --git a/lib/vclib/__init__.py b/lib/vclib/__init__.py new file mode 100644 index 00000000..d2874ed1 --- /dev/null +++ b/lib/vclib/__init__.py @@ -0,0 +1,420 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 is an abstract API to access versioning systems +such as CVS. +""" + +import string +import types + + +# item types returned by Repository.itemtype(). +FILE = 'FILE' +DIR = 'DIR' + +# diff types recognized by Repository.rawdiff(). +UNIFIED = 1 +CONTEXT = 2 +SIDE_BY_SIDE = 3 + +# root types returned by Repository.roottype(). +CVS = 'cvs' +SVN = 'svn' + +# 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 + + The result will be vclib.DIR or vclib.FILE + + 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 check + """ + pass + + 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 + number in canonical form. + + 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 file to check out + """ + + def listdir(self, path_parts, rev, options): + """Return list of files in a directory + + The result is a list of DirEntry objects + + 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 directory to list + + options is a dictionary of implementation specific options + """ + + def dirlogs(self, path_parts, rev, entries, options): + """Augment directory entries with log information + + 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". + + 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 directory listing and will effect which log + messages are returned + + entries is a list of DirEntry objects returned from a previous call to + the listdir() method + + options is a dictionary of implementation specific options + """ + + def itemlog(self, path_parts, rev, sortby, first, limit, options): + """Retrieve an item's log information + + The result is a list of Revision objects + + 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 + + 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 + + type is the requested diff type (UNIFIED, CONTEXT, etc) + + options is a dictionary that can contain the following options plus + implementation-specific options + + context - integer, number of context lines to include + funout - boolean, include C function names + ignore_white - boolean, ignore whitespace + + Return value is a python file object + """ + + def annotate(self, path_parts, rev): + """Return a list of annotate file content lines and a revision. + + The result is a list of Annotation objects, sorted by their + line_number components. + """ + + def revinfo(self, rev): + """Return information about a global revision + + rev is the revision of the item to return information about + + Return value is a 4-tuple containing the date, author, log + message, and a list of ChangedPath items representing paths changed + + Raise vclib.UnsupportedFeature if the version control system + doesn't support a global revision concept. + """ + + 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 + """ + + +# ====================================================================== +class DirEntry: + """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 + +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 + """ + self.number = number + self.string = string + self.date = date + self.author = author + 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 = string.join(path, '/') + Error.__init__(self, path) + +class InvalidRevision(Error): + def __init__(self, revision=None): + if revision is None: + Error.__init__(self, "Invalid revision") + else: + Error.__init__(self, "Invalid revision " + str(revision)) + +class NonTextualFileContents(Error): + pass + +# ====================================================================== +# Implementation code used by multiple vclib modules + +import popen +import os +import time + +def _diff_args(type, options): + """generate argument list to pass to diff or rcsdiff""" + args = [] + if type == CONTEXT: + if options.has_key('context'): + if options['context'] is None: + args.append('--context=-1') + else: + 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=-1') + else: + args.append('--unified=%i' % options['context']) + else: + args.append('-u') + elif type == SIDE_BY_SIDE: + args.append('--side-by-side') + args.append('--width=164') + else: + raise NotImplementedError + + if options.get('funout', 0): + args.append('-p') + + if options.get('ignore_white', 0): + args.append('-w') + + return args + +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=[]): + self.temp1 = temp1 + self.temp2 = temp2 + args = diff_opts[:] + if info1 and info2: + args.extend(["-L", self._label(info1), "-L", self._label(info2)]) + args.extend([temp1, temp2]) + self.fp = popen.popen(diff_cmd, args, "r") + + def read(self, bytes): + return self.fp.read(bytes) + + def readline(self): + return self.fp.readline() + + def close(self): + try: + if self.fp: + self.fp.close() + self.fp = None + finally: + try: + if self.temp1: + os.remove(self.temp1) + self.temp1 = None + finally: + if self.temp2: + os.remove(self.temp2) + self.temp2 = None + + def __del__(self): + self.close() + + 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) + return auth.check_path_access(repos.rootname(), path_parts, pathtype, rev) + diff --git a/lib/vclib/ccvs/__init__.py b/lib/vclib/ccvs/__init__.py new file mode 100644 index 00000000..374eb522 --- /dev/null +++ b/lib/vclib/ccvs/__init__.py @@ -0,0 +1,43 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 os.path + + +def canonicalize_rootpath(rootpath): + return os.path.normpath(rootpath) + + +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. + roots = {} + subpaths = os.listdir(parent_path) + cvsroot = os.path.exists(os.path.join(parent_path, "CVSROOT", "config")) + for rootname in subpaths: + rootpath = os.path.join(parent_path, rootname) + if cvsroot \ + or (os.path.exists(os.path.join(rootpath, "CVSROOT", "config"))): + roots[rootname] = canonicalize_rootpath(rootpath) + return roots + + +def CVSRepository(name, rootpath, authorizer, utilities, use_rcsparse): + rootpath = canonicalize_rootpath(rootpath) + if use_rcsparse: + import ccvs + return ccvs.CCVSRepository(name, rootpath, authorizer, utilities) + else: + import bincvs + return bincvs.BinCVSRepository(name, rootpath, authorizer, utilities) diff --git a/lib/vclib/ccvs/bincvs.py b/lib/vclib/ccvs/bincvs.py new file mode 100644 index 00000000..3b79a53e --- /dev/null +++ b/lib/vclib/ccvs/bincvs.py @@ -0,0 +1,1211 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 cvs-repositories." + +import vclib +import vcauth +import os +import os.path +import sys +import stat +import string +import re +import time + +# ViewVC libs +import compat +import popen + +class BaseCVSRepository(vclib.Repository): + def __init__(self, name, rootpath, authorizer, utilities): + if not os.path.isdir(rootpath): + raise vclib.ReposNotFound(name) + + self.name = name + self.rootpath = rootpath + self.auth = authorizer + self.utilities = utilities + + # See if this repository is even viewable, authz-wise. + if not vclib.check_root_access(self): + raise vclib.ReposNotFound(name) + + 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 + + def listdir(self, path_parts, rev, options): + if self.itemtype(path_parts, rev) != vclib.DIR: # does auth-check + raise vclib.Error("Path '%s' is not a directory." + % (string.join(path_parts, "/"))) + + # 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] + elif kind == vclib.DIR: + if file != 'Attic' and file != 'CVS': # CVS directory is for fileattr + name = file + 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)) + + 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] + 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)) + + return data + + def _getpath(self, path_parts): + return apply(os.path.join, (self.rootpath,) + tuple(path_parts)) + + def _atticpath(self, path_parts): + return path_parts[:-1] + ['Attic'] + path_parts[-1:] + + def rcsfile(self, path_parts, root=0, v=1): + "Return path to RCS file" + + ret_parts = path_parts + ret_file = self._getpath(ret_parts) + if not os.path.isfile(ret_file + ',v'): + ret_parts = self._atticpath(path_parts) + 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 = 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." + % (string.join(path_parts, "/"))) + rcsfile = self.rcsfile(path_parts, 1) + return os.access(rcsfile, os.X_OK) + + +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) + revs = [] + while not eof: + revision, eof = _parse_log_entry(fp) + if revision: + revs.append(revision) + revs = _file_log(revs, tags, lockinfo, default_branch, rev) + if revs: + return revs[-1] + return None + + def openfile(self, path_parts, rev): + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts, "/"))) + if not rev or rev == 'HEAD' or rev == 'MAIN': + rev_flag = '-p' + else: + rev_flag = '-p' + rev + full_name = self.rcsfile(path_parts, root=1, v=0) + + used_rlog = 0 + tip_rev = None # used only if we have to fallback to using rlog + + fp = self.rcs_popen('co', (rev_flag, full_name), 'rb') + try: + filename, revision = _parse_co_header(fp) + except COMissingRevision: + # We got a "revision X.Y.Z absent" error from co. This could be + # because we were asked to find a tip of a branch, which co + # doesn't seem to handle. So we do rlog-gy stuff to figure out + # which revision the tip of the branch currently maps to. + ### TODO: Only do this when 'rev' is a branch symbol name? + if not used_rlog: + tip_rev = self._get_tip_revision(full_name + ',v', rev) + 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') + 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, + # 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) + used_rlog = 1 + if not (tip_rev and tip_rev.undead): + 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') + filename, revision = _parse_co_header(fp) + + if filename is None: + raise vclib.Error('Missing output from co (filename = "%s")' % full_name) + + if not _paths_eq(filename, full_name): + raise vclib.Error( + 'The filename from co ("%s") did not match (expected "%s")' + % (filename, full_name)) + + return fp, revision + + 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." + % (string.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) + 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 recognized by this implementation: + + cvs_pass_rev + boolean, default false. set to true to pass rev parameter as -r + argument to rlog, this is more efficient but causes less + information to be returned + + 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." + % (string.join(path_parts, "/"))) + + # Invoke rlog + rcsfile = self.rcsfile(path_parts, 1) + if rev and options.get('cvs_pass_rev', 0): + args = '-r' + rev, rcsfile + else: + args = rcsfile, + + fp = self.rcs_popen('rlog', args, 'rt', 0) + filename, default_branch, tags, lockinfo, msg, eof = _parse_log_header(fp) + + # Retrieve revision objects + revs = [] + while not eof: + revision, eof = _parse_log_entry(fp) + if revision: + revs.append(revision) + + filtered_revs = _file_log(revs, tags, lockinfo, 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): + if self.utilities.cvsnt: + cmd = self.utilities.cvsnt + args = ['rcsfile', rcs_cmd] + args.extend(list(rcs_args)) + else: + cmd = os.path.join(self.utilities.rcs_dir, rcs_cmd) + args = rcs_args + return popen.popen(cmd, args, mode, capture_err) + + def annotate(self, path_parts, rev=None): + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts, "/"))) + + from vclib.ccvs import blame + 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 + + Option values recognized by this implementation: + + ignore_keyword_subst - boolean, ignore keyword substitution + """ + if self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts1, "/"))) + if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts2, "/"))) + + args = vclib._diff_args(type, options) + if options.get('ignore_keyword_subst', 0): + args.append('-kk') + + rcsfile = self.rcsfile(path_parts1, 1) + if path_parts1 != path_parts2: + raise NotImplementedError, "cannot diff across paths in cvs" + args.extend(['-r' + rev1, '-r' + rev2, rcsfile]) + + fp = self.rcs_popen('rcsdiff', args, 'rt') + + # Eat up the non-GNU-diff-y headers. + while 1: + line = fp.readline() + 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): + 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) + self.dead = dead + +class Tag: + def __init__(self, name, revstr): + self.name = name + self.number = _tag_tuple(revstr) + self.is_branch = len(self.number) % 2 == 1 or not self.number + + +# ====================================================================== +# 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 + + Sets the following properties on each Revision in revlist: + "tags" + list of non-branch tags which refer to this revision + example: if revision is 1.2.3.4, tags is a list of all 1.2.3.4 tags + + "branches" + list of branch tags which refer to this revision's branch + example: if revision is 1.2.3.4, branches is a list of all 1.2.3 tags + + "branch_points" + list of branch tags which branch off of this revision + example: if revision is 1.2, it's a list of tags like 1.2.3 and 1.2.4 + + "prev" + reference to the previous revision, possibly None + example: if revision is 1.2.3.4, prev is 1.2.3.3 + + "next" + reference to next revision, possibly None + example: if revision is 1.2.3.4, next is 1.2.3.5 + + "parent" + reference to revision this one branches off of, possibly None + 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 + previous revision which isn't dead, otherwise it's a reference + to itself. If all the previous revisions are dead it's None. + + "branch_number" + tuple representing branch number or empty tuple if on trunk + example: if revision is 1.2.3.4, branch_number is (1, 2, 3) + + Each tag in taglist gets these properties set: + "co_rev" + reference to revision that would be retrieved if tag were checked out + + "branch_rev" + reference to revision branched off of, only set for branch tags + example: if tag is 1.2.3, branch_rev points to 1.2 revision + + "aliases" + list of tags that have the same number + """ + + # map of branch numbers to lists of corresponding branch Tags + branch_dict = {} + + # map of revision numbers to lists of non-branch Tags + tag_dict = {} + + # map of revision numbers to lists of branch Tags + branch_point_dict = {} + + # toss tags into "branch_dict", "tag_dict", and "branch_point_dict" + # set "aliases" property and default "co_rev" and "branch_rev" values + for tag in taglist: + tag.co_rev = None + if tag.is_branch: + tag.branch_rev = None + _dict_list_add(branch_point_dict, tag.number[:-1], tag) + tag.aliases = _dict_list_add(branch_dict, tag.number, tag) + else: + tag.aliases = _dict_list_add(tag_dict, tag.number, tag) + + # sort the revisions so the loop below can work properly + revlist.sort() + + # array of the most recently encountered revision objects indexed by depth + history = [] + + # loop through revisions, setting properties and storing state in "history" + for rev in revlist: + depth = len(rev.number) / 2 - 1 + + # set "prev" and "next" properties + rev.prev = rev.next = None + if depth < len(history): + prev = history[depth] + if prev and (depth == 0 or rev.number[:-1] == prev.number[:-1]): + rev.prev = prev + prev.next = rev + + # set "parent" + rev.parent = None + if depth and depth <= len(history): + parent = history[depth-1] + if parent and parent.number == rev.number[:-2]: + rev.parent = history[depth-1] + + # set "undead" + if rev.dead: + prev = rev.prev or rev.parent + rev.undead = prev and prev.undead + else: + rev.undead = rev + + # set "tags" and "branch_points" + rev.tags = tag_dict.get(rev.number, []) + rev.branch_points = branch_point_dict.get(rev.number, []) + + # set "branches" and "branch_number" + if rev.prev: + rev.branches = rev.prev.branches + rev.branch_number = rev.prev.branch_number + else: + rev.branch_number = depth and rev.number[:-1] or () + try: + rev.branches = branch_dict[rev.branch_number] + except KeyError: + rev.branches = [] + + # set "co_rev" and "branch_rev" + for tag in rev.tags: + tag.co_rev = rev + + for tag in rev.branch_points: + tag.co_rev = rev + tag.branch_rev = rev + + # This loop only needs to be run for revisions at the heads of branches, + # but for the simplicity's sake, it actually runs for every revision on + # a branch. The later revisions overwrite values set by the earlier ones. + for branch in rev.branches: + branch.co_rev = rev + + # end of outer loop, store most recent revision in "history" array + while len(history) <= depth: + history.append(None) + history[depth] = rev + +def _add_tag(tag_name, revision): + """Create a new tag object and associate it with a revision""" + if revision: + tag = Tag(tag_name, revision.string) + tag.aliases = revision.tags + revision.tags.append(tag) + else: + tag = Tag(tag_name, None) + tag.aliases = [] + tag.co_rev = revision + tag.is_branch = 0 + return tag + +def _remove_tag(tag): + """Remove a tag's associations""" + tag.aliases.remove(tag) + if tag.is_branch and tag.branch_rev: + tag.branch_rev.branch_points.remove(tag) + +def _revision_tuple(revision_string): + """convert a revision number into a tuple of integers""" + t = tuple(map(int, string.split(revision_string, '.'))) + if len(t) % 2 == 0: + return t + raise ValueError + +def _tag_tuple(revision_string): + """convert a revision number or branch number into a tuple of integers""" + if revision_string: + t = map(int, string.split(revision_string, '.')) + l = len(t) + if l == 1: + return () + if l > 2 and t[-2] == 0 and l % 2 == 0: + del t[-2] + return tuple(t) + return () + +def _dict_list_add(dict, idx, elem): + try: + list = dict[idx] + except KeyError: + list = dict[idx] = [elem] + else: + list.append(elem) + return list + + +# ====================================================================== +# Functions for parsing output from RCS utilities + + +class COMalformedOutput(vclib.Error): + pass +class COMissingRevision(vclib.Error): + pass + +### suck up other warnings in _re_co_warning? +_re_co_filename = re.compile(r'^(.*),v\s+-->\s+(?:(?:standard output)|(?:stdout))\s*\n?$') +_re_co_warning = re.compile(r'^.*co: .*,v: warning: Unknown phrases like .*\n$') +_re_co_missing_rev = re.compile(r'^.*co: .*,v: revision.*absent\n$') +_re_co_side_branches = re.compile(r'^.*co: .*,v: no side branches present for [\d\.]+\n$') +_re_co_revision = re.compile(r'^revision\s+([\d\.]+)\s*\n$') + +def _parse_co_header(fp): + """Parse RCS co header. + + fp is a file (pipe) opened for reading the co standard error stream. + + Returns: (filename, revision) or (None, None) if output is empty + """ + + # header from co: + # + #/home/cvsroot/mod_dav/dav_shared_stub.c,v --> standard output + #revision 1.1 + # + # Sometimes, the following line might occur at line 2: + #co: INSTALL,v: warning: Unknown phrases like `permissions ...;' are present. + + # parse the output header + filename = None + + # look for a filename in the first line (if there is a first line). + line = fp.readline() + if not line: + return None, None + match = _re_co_filename.match(line) + if not match: + 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 + + 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' + +_EOF_FILE = 'end of file entries' # no more entries for this RCS file +_EOF_LOG = 'end of log' # hit the true EOF on the pipe +_EOF_ERROR = 'error message found' # rlog issued an error + +# rlog error messages look like +# +# rlog: filename/goes/here,v: error message +# rlog: filename/goes/here,v:123: error message +# +# so we should be able to match them with a regex like +# +# ^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 +# 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 +_re_log_error = re.compile(r'^(?:rlog\: )*(.*,v)(?:\:\d+)?\: (.*)$') + +# CVSNT error messages look like: +# cvs rcsfile: `C:/path/to/file,v' does not appear to be a valid rcs file +# cvs [rcsfile aborted]: C:/path/to/file,v: No such file or directory +# cvs [rcsfile aborted]: cannot open C:/path/to/file,v: Permission denied +_re_cvsnt_error = re.compile(r'^(?:cvs rcsfile\: |cvs \[rcsfile aborted\]: )' + r'(?:\`(.*,v)\' |cannot open (.*,v)\: |(.*,v)\: |)' + r'(.*)$') + +def _parse_log_header(fp): + """Parse and RCS/CVS log header. + + fp is a file (pipe) opened for reading the log information. + + On entry, fp should point to the start of a log entry. + On exit, fp will have consumed the separator line between the header and + the first revision log. + + 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 + """ + + filename = head = branch = msg = "" + taginfo = { } # tag name => number + lockinfo = { } # revision => locker + state = 0 # 0 = base, 1 = parsing symbols, 2 = parsing locks + eof = None + + while 1: + line = fp.readline() + if not line: + # the true end-of-file + eof = _EOF_LOG + break + + if state == 1: + if line[0] == '\t': + [ tag, rev ] = map(string.strip, string.split(line, ':')) + taginfo[tag] = rev + else: + # oops. this line isn't tag info. stop parsing tags. + state = 0 + + if state == 2: + if line[0] == '\t': + [ locker, rev ] = map(string.strip, string.split(line, ':')) + lockinfo[rev] = locker + else: + # oops. this line isn't lock info. stop parsing tags. + state = 0 + + if state == 0: + 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 + elif line == ENTRY_END_MARKER: + # end of the headers + break + elif line == LOG_END_MARKER: + # end of this file's log information + eof = _EOF_FILE + break + else: + error = _re_cvsnt_error.match(line) + if error: + p1, p2, p3, msg = error.groups() + filename = p1 or p2 or p3 + if not filename: + raise vclib.Error("Could not get filename from CVSNT error:\n%s" + % line) + eof = _EOF_ERROR + break + + error = _re_log_error.match(line) + if error: + filename, msg = error.groups() + if msg[:30] == 'warning: Unknown phrases like ': + # don't worry about this warning. it can happen with some RCS + # files that have unknown fields in them (e.g. "permissions 644;" + continue + eof = _EOF_ERROR + break + + return filename, branch, taginfo, lockinfo, msg, eof + +_re_log_info = re.compile(r'^date:\s+([^;]+);' + r'\s+author:\s+([^;]+);' + r'\s+state:\s+([^;]+);' + r'(\s+lines:\s+([0-9\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): + """Parse a single log entry. + + On entry, fp should point to the first line of the entry (the "revision" + line). + On exit, fp will have consumed the log separator line (dashes) or the + end-of-file marker (equals). + + Returns: Revision object and eof flag (see _EOF_*) + """ + rev = None + line = fp.readline() + if not line: + return None, _EOF_LOG + if line == LOG_END_MARKER: + # Needed because some versions of RCS precede LOG_END_MARKER + # with ENTRY_END_MARKER + return None, _EOF_FILE + if line[:8] == 'revision': + match = _re_rev.match(line) + if not match: + return None, _EOF_LOG + rev = match.group(1) + + line = fp.readline() + if not line: + return None, _EOF_LOG + match = _re_log_info.match(line) + + eof = None + log = '' + while 1: + line = fp.readline() + if not line: + # true end-of-file + eof = _EOF_LOG + break + if line[:9] == 'branches:': + continue + if line == ENTRY_END_MARKER: + break + if line == LOG_END_MARKER: + # end of this file's log information + eof = _EOF_FILE + break + + log = log + line + + if not rev or not match: + # there was a parsing error + return None, eof + + # parse out a time tuple for the local time + 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"). + EPOCH = 1970 + if tm[0] < EPOCH: + tm = list(tm) + if (tm[0] - 1900) < 70: + tm[0] = tm[0] + 100 + if tm[0] < EPOCH: + raise ValueError, 'invalid year' + date = compat.timegm(tm) + + return Revision(rev, date, + # author, state, lines changed + match.group(2), match.group(3) == "dead", match.group(5), + log), eof + +def _skip_file(fp): + "Skip the rest of a file's log information." + while 1: + line = fp.readline() + if not line: + break + if line == LOG_END_MARKER: + break + +def _paths_eq(path1, path2): + "See if two path strings are the same" + # This function is neccessary because CVSNT (since version 2.0.29) + # converts paths passed as arguments to use upper case drive + # letter and forward slashes + return os.path.normcase(path1) == os.path.normcase(path2) + + +# ====================================================================== +# Functions for interpreting and manipulating log information + +def _file_log(revs, taginfo, lockinfo, 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 + # MAIN acts like a branch tag pointing to that branch. Otherwise MAIN acts + # like a branch tag that points to the trunk. (Note: A default branch is + # just a branch number specified in an RCS file that tells CVS and RCS + # what branch to use for checkout and update operations by default, when + # there's no revision argument or sticky branch to override it. Default + # branches get set by "cvs import" to point to newly created vendor + # branches. Sometimes they are also set manually with "cvs admin -b") + taginfo['MAIN'] = cur_branch + + # Create tag objects + for name, num in taginfo.items(): + taginfo[name] = Tag(name, num) + tags = taginfo.values() + + # Set view_tag to a Tag object in order to filter results. We can filter by + # revision number or branch number + if filter: + try: + view_tag = Tag(None, filter) + except ValueError: + view_tag = None + else: + 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. + taginfo['HEAD'] = _add_tag('HEAD', taginfo['MAIN'].co_rev) + + # Determine what revisions to return + if filter: + # If view_tag isn't set, it means filter is not a valid revision or + # branch number. Check taginfo to see if filter is set to a valid tag + # name. If so, filter by that tag, otherwise raise an error. + if not view_tag: + try: + view_tag = taginfo[filter] + except KeyError: + raise vclib.Error('Invalid tag or revision number "%s"' % filter) + filtered_revs = [ ] + + # only include revisions on the tag branch or it's parent branches + if view_tag.is_branch: + branch = view_tag.number + elif len(view_tag.number) > 2: + branch = view_tag.number[:-1] + else: + branch = () + + # for a normal tag, include all tag revision and all preceding revisions. + # for a branch tag, include revisions on branch, branch point revision, + # and all preceding revisions + for rev in revs: + if (rev.number == view_tag.number + or rev.branch_number == view_tag.number + or (rev.number < view_tag.number + and rev.branch_number == branch[:len(rev.branch_number)])): + filtered_revs.append(rev) + + # get rid of the view_tag if it was only created for filtering + if view_tag.name is None: + _remove_tag(view_tag) + else: + filtered_revs = revs + + return filtered_revs + +def _get_logs(repos, dir_path_parts, entries, view_tag, get_dirs): + alltags = { # all the tags seen in the files of this dir + 'MAIN' : '', + 'HEAD' : '1.1' + } + + entries_idx = 0 + entries_len = len(entries) + max_args = 100 + + while 1: + chunk = [] + + 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) + 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 + + entries_idx = entries_idx + 1 + + if not chunk: + return alltags + + args = [] + if not view_tag: + # NOTE: can't pass tag on command line since a tag may contain "-" + # we'll search the output for the appropriate revision + # fetch the latest revision on the default branch + args.append('-r') + args.extend(map(lambda x: x.path, chunk)) + rlog = repos.rcs_popen('rlog', args, 'rt') + + # consume each file found in the resulting log + chunk_idx = 0 + while chunk_idx < len(chunk): + file = chunk[chunk_idx] + filename, default_branch, taginfo, lockinfo, msg, eof \ + = _parse_log_header(rlog) + + if eof == _EOF_LOG: + # 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: + # + # rlog: c:\cvsroot\dir\file,v:8: unknown expand mode u + # rlog aborted + + # if current file has errors, restart on the next one + if file.errors: + chunk_idx = chunk_idx + 1 + if chunk_idx < len(chunk): + entries_idx = chunk[chunk_idx].idx + break + + # otherwise just error out + raise vclib.Error('Rlog output ended early. Expected RCS file "%s"' + % file.path) + + # if rlog filename doesn't match current file and we already have an + # error message about this file, move on to the next file + while not (file and _paths_eq(file.path, filename)): + if file and file.errors: + chunk_idx = chunk_idx + 1 + file = chunk_idx < len(chunk) and chunk[chunk_idx] or None + continue + + raise vclib.Error('Error parsing rlog output. Expected RCS file %s' + ', found %s' % (file and file.path, filename)) + + # if we get an rlog error message, restart loop without advancing + # chunk_idx cause there might be more output about the same file + if eof == _EOF_ERROR: + file.errors.append("rlog error: %s" % msg) + continue + + if view_tag == 'MAIN' or view_tag == 'HEAD': + tag = Tag(None, default_branch) + elif taginfo.has_key(view_tag): + tag = Tag(None, taginfo[view_tag]) + elif view_tag: + # the tag wasn't found, so skip this file + _skip_file(rlog) + 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 + # distinction between branch tags and normal tags + alltags.update(taginfo) + + # read all of the log entries until we find the revision we want + wanted_entry = None + while not eof: + + # fetch one of the log entries + entry, eof = _parse_log_entry(rlog) + + if not entry: + # parsing error + break + + # A perfect match is a revision on the branch being viewed or + # a revision having the tag being viewed or any revision + # when nothing is being viewed. When there's a perfect match + # we set the wanted_entry value and break out of the loop. + # An imperfect match is a revision at the branch point of a + # branch being viewed. When there's an imperfect match we + # also set the wanted_entry value but keep looping in case + # something better comes along. + perfect = not tag or entry.number == tag.number or \ + (len(entry.number) == 2 and not tag.number) or \ + entry.number[:-1] == tag.number + if perfect or entry.number == tag.number[:-1]: + wanted_entry = entry + if perfect: + break + + if wanted_entry: + file.rev = wanted_entry.string + 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 + + # done with this file now, skip the rest of this file's revisions + if not eof: + _skip_file(rlog) + + # end of while loop, advance index + chunk_idx = chunk_idx + 1 + + rlog.close() + +def _log_path(entry, dirpath, getdirs): + path = name = None + if not entry.errors: + if entry.kind == vclib.FILE: + path = entry.in_attic and 'Attic' or '' + name = entry.name + elif entry.kind == vclib.DIR and getdirs: + entry.newest_file = _newest_file(os.path.join(dirpath, entry.name)) + if entry.newest_file: + path = entry.name + name = entry.newest_file + + if name: + return os.path.join(dirpath, path, name + ',v') + return None + + +# ====================================================================== +# Functions for dealing with the filesystem + +if sys.platform == "win32": + def _check_path(path): + kind = None + errors = [] + + if os.path.isfile(path): + kind = vclib.FILE + elif os.path.isdir(path): + kind = vclib.DIR + else: + errors.append("error: path is not a file or directory") + + if not os.access(path, os.R_OK): + errors.append("error: path is not accessible") + + return kind, errors + +else: + _uid = os.getuid() + _gid = os.getgid() + + def _check_path(pathname): + try: + info = os.stat(pathname) + except os.error, e: + return None, ["stat error: %s" % e] + + kind = None + errors = [] + + mode = info[stat.ST_MODE] + isdir = stat.S_ISDIR(mode) + isreg = stat.S_ISREG(mode) + if isreg or isdir: + # + # Quick version of access() where we use existing stat() data. + # + # This might not be perfect -- the OS may return slightly different + # results for some bizarre reason. However, we make a good show of + # "can I read this file/dir?" by checking the various perm bits. + # + # NOTE: if the UID matches, then we must match the user bits -- we + # cannot defer to group or other bits. Similarly, if the GID matches, + # then we must have read access in the group bits. + # + # If the UID or GID don't match, we need to check the + # results of an os.access() call, in case the web server process + # is in the group that owns the directory. + # + if isdir: + mask = stat.S_IROTH | stat.S_IXOTH + else: + mask = stat.S_IROTH + + if info[stat.ST_UID] == _uid: + if ((mode >> 6) & mask) != mask: + errors.append("error: path is not accessible to user %i" % _uid) + elif info[stat.ST_GID] == _gid: + if ((mode >> 3) & mask) != mask: + errors.append("error: path is not accessible to group %i" % _gid) + # If the process running the web server is a member of + # the group stat.ST_GID access may be granted. + # so the fall back to os.access is needed to figure this out. + elif (mode & mask) != mask: + if not os.access(pathname, isdir and (os.R_OK | os.X_OK) or os.R_OK): + errors.append("error: path is not accessible") + + if isdir: + kind = vclib.DIR + else: + kind = vclib.FILE + + else: + errors.append("error: path is not a file or directory") + + return kind, errors + +def _newest_file(dirpath): + """Find the last modified RCS file in a directory""" + 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': + continue + path = os.path.join(dirpath, subfile) + info = os.stat(path) + if not stat.S_ISREG(info[stat.ST_MODE]): + continue + if info[stat.ST_MTIME] > newest_time: + kind, verboten = _check_path(path) + if kind == vclib.FILE and not verboten: + newest_file = subfile[:-2] + newest_time = info[stat.ST_MTIME] + + return newest_file diff --git a/lib/vclib/ccvs/blame.py b/lib/vclib/ccvs/blame.py new file mode 100644 index 00000000..77345856 --- /dev/null +++ b/lib/vclib/ccvs/blame.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# Copyright (C) 2000 Curt Hagenlocher +# +# By using this file, you agree to the terms and conditions set forth in +# 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/ +# +# ----------------------------------------------------------------------- +# +# blame.py: Annotate each line of a CVS file with its author, +# revision #, date, etc. +# +# ----------------------------------------------------------------------- +# +# This file is based on the cvsblame.pl portion of the Bonsai CVS tool, +# developed by Steve Lamm for Netscape Communications Corporation. More +# information about Bonsai can be found at +# http://www.mozilla.org/bonsai.html +# +# cvsblame.pl, in turn, was based on Scott Furman's cvsblame script +# +# ----------------------------------------------------------------------- + +import string +import re +import time +import math +import rcsparse +import vclib + +class CVSParser(rcsparse.Sink): + # Precompiled regular expressions + trunk_rev = re.compile('^[0-9]+\\.[0-9]+$') + last_branch = re.compile('(.*)\\.[0-9]+') + is_branch = re.compile('^(.*)\\.0\\.([0-9]+)$') + d_command = re.compile('^d(\d+)\\s(\\d+)') + a_command = re.compile('^a(\d+)\\s(\\d+)') + + SECONDS_PER_DAY = 86400 + + def __init__(self): + self.Reset() + + def Reset(self): + self.last_revision = {} + self.prev_revision = {} + self.revision_date = {} + self.revision_author = {} + self.revision_branches = {} + self.next_delta = {} + self.prev_delta = {} + self.tag_revision = {} + self.timestamp = {} + self.revision_ctime = {} + self.revision_age = {} + self.revision_log = {} + self.revision_deltatext = {} + self.revision_map = [] # map line numbers to revisions + self.lines_added = {} + self.lines_removed = {} + + # Map a tag to a numerical revision number. The tag can be a symbolic + # branch tag, a symbolic revision tag, or an ordinary numerical + # revision number. + def map_tag_to_revision(self, tag_or_revision): + try: + revision = self.tag_revision[tag_or_revision] + match = self.is_branch.match(revision) + if match: + branch = match.group(1) + '.' + match.group(2) + if self.last_revision.get(branch): + return self.last_revision[branch] + else: + return match.group(1) + else: + return revision + except: + return '' + + # Construct an ordered list of ancestor revisions to the given + # revision, starting with the immediate ancestor and going back + # to the primordial revision (1.1). + # + # Note: The generated path does not traverse the tree the same way + # that the individual revision deltas do. In particular, + # the path traverses the tree "backwards" on branches. + def ancestor_revisions(self, revision): + ancestors = [] + revision = self.prev_revision.get(revision) + while revision: + ancestors.append(revision) + revision = self.prev_revision.get(revision) + + return ancestors + + # Split deltatext specified by rev to each line. + def deltatext_split(self, rev): + lines = string.split(self.revision_deltatext[rev], '\n') + if lines[-1] == '': + del lines[-1] + return lines + + # Extract the given revision from the digested RCS file. + # (Essentially the equivalent of cvs up -rXXX) + def extract_revision(self, revision): + path = [] + add_lines_remaining = 0 + start_line = 0 + count = 0 + while revision: + path.append(revision) + revision = self.prev_delta.get(revision) + path.reverse() + path = path[1:] # Get rid of head revision + + text = self.deltatext_split(self.head_revision) + + # Iterate, applying deltas to previous revision + for revision in path: + adjust = 0 + diffs = self.deltatext_split(revision) + self.lines_added[revision] = 0 + self.lines_removed[revision] = 0 + lines_added_now = 0 + lines_removed_now = 0 + + for command in diffs: + dmatch = self.d_command.match(command) + amatch = self.a_command.match(command) + if add_lines_remaining > 0: + # Insertion lines from a prior "a" command + text.insert(start_line + adjust, command) + add_lines_remaining = add_lines_remaining - 1 + adjust = adjust + 1 + elif dmatch: + # "d" - Delete command + 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 = string.atoi(amatch.group(1)) + count = string.atoi(amatch.group(2)) + add_lines_remaining = count + lines_added_now = lines_added_now + count + else: + raise RuntimeError, 'Error parsing diff commands' + + self.lines_added[revision] = self.lines_added[revision] + lines_added_now + self.lines_removed[revision] = self.lines_removed[revision] + lines_removed_now + return text + + def set_head_revision(self, revision): + self.head_revision = revision + + def set_principal_branch(self, branch_name): + self.principal_branch = branch_name + + def define_tag(self, name, revision): + # Create an associate array that maps from tag name to + # revision number and vice-versa. + self.tag_revision[name] = revision + + def set_comment(self, comment): + self.file_description = comment + + def set_description(self, description): + self.rcs_file_description = description + + # Construct dicts that represent the topology of the RCS tree + # and other arrays that contain info about individual revisions. + # + # The following dicts are created, keyed by revision number: + # self.revision_date -- e.g. "96.02.23.00.21.52" + # self.timestamp -- seconds since 12:00 AM, Jan 1, 1970 GMT + # self.revision_author -- e.g. "tom" + # self.revision_branches -- descendant branch revisions, separated by spaces, + # e.g. "1.21.4.1 1.21.2.6.1" + # self.prev_revision -- revision number of previous *ancestor* in RCS tree. + # Traversal of this array occurs in the direction + # of the primordial (1.1) revision. + # self.prev_delta -- revision number of previous revision which forms + # the basis for the edit commands in this revision. + # This causes the tree to be traversed towards the + # trunk when on a branch, and towards the latest trunk + # revision when on the trunk. + # self.next_delta -- revision number of next "delta". Inverts prev_delta. + # + # Also creates self.last_revision, keyed by a branch revision number, which + # indicates the latest revision on a given branch, + # e.g. self.last_revision{"1.2.8"} == 1.2.8.5 + def define_revision(self, revision, timestamp, author, state, + branches, next): + self.tag_revision[revision] = revision + branch = self.last_branch.match(revision).group(1) + self.last_revision[branch] = revision + + #self.revision_date[revision] = date + self.timestamp[revision] = timestamp + + # Pretty print the date string + ltime = time.localtime(self.timestamp[revision]) + formatted_date = time.strftime("%d %b %Y %H:%M", ltime) + self.revision_ctime[revision] = formatted_date + + # Save age + self.revision_age[revision] = ((time.time() - self.timestamp[revision]) + / self.SECONDS_PER_DAY) + + # save author + self.revision_author[revision] = author + + # ignore the state + + # process the branch information + branch_text = '' + for branch in branches: + self.prev_revision[branch] = revision + self.next_delta[revision] = branch + self.prev_delta[branch] = revision + branch_text = branch_text + branch + '' + self.revision_branches[revision] = branch_text + + # process the "next revision" information + if next: + self.next_delta[revision] = next + self.prev_delta[next] = revision + is_trunk_revision = self.trunk_rev.match(revision) is not None + if is_trunk_revision: + self.prev_revision[revision] = next + else: + self.prev_revision[next] = revision + + # Construct associative arrays containing info about individual revisions. + # + # The following associative arrays are created, keyed by revision number: + # revision_log -- log message + # revision_deltatext -- Either the complete text of the revision, + # in the case of the head revision, or the + # encoded delta between this revision and another. + # The delta is either with respect to the successor + # revision if this revision is on the trunk or + # relative to its immediate predecessor if this + # revision is on a branch. + def set_revision_info(self, revision, log, text): + self.revision_log[revision] = log + self.revision_deltatext[revision] = text + + def parse_cvs_file(self, rcs_pathname, opt_rev = None, opt_m_timestamp = None): + # Args in: opt_rev - requested revision + # opt_m - time since modified + # Args out: revision_map + # timestamp + # revision_deltatext + + # CheckHidden(rcs_pathname) + try: + rcsfile = open(rcs_pathname, 'rb') + except: + raise RuntimeError, ('error: %s appeared to be under CVS control, ' + + 'but the RCS file is inaccessible.') % rcs_pathname + + rcsparse.parse(rcsfile, self) + rcsfile.close() + + if opt_rev in [None, '', 'HEAD']: + # Explicitly specified topmost revision in tree + revision = self.head_revision + else: + # Symbolic tag or specific revision number specified. + revision = self.map_tag_to_revision(opt_rev) + if revision == '': + raise RuntimeError, 'error: -r: No such revision: ' + opt_rev + + # The primordial revision is not always 1.1! Go find it. + primordial = revision + while self.prev_revision.get(primordial): + primordial = self.prev_revision[primordial] + + # Don't display file at all, if -m option is specified and no + # changes have been made in the specified file. + if opt_m_timestamp and self.timestamp[revision] < opt_m_timestamp: + return '' + + # Figure out how many lines were in the primordial, i.e. version 1.1, + # check-in by moving backward in time from the head revision to the + # first revision. + line_count = 0 + if self.revision_deltatext.get(self.head_revision): + tmp_array = self.deltatext_split(self.head_revision) + line_count = len(tmp_array) + + skip = 0 + + rev = self.prev_revision.get(self.head_revision) + while rev: + diffs = self.deltatext_split(rev) + for command in diffs: + dmatch = self.d_command.match(command) + amatch = self.a_command.match(command) + if skip > 0: + # Skip insertion lines from a prior "a" command + skip = skip - 1 + elif dmatch: + # "d" - Delete command + 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 = string.atoi(amatch.group(1)) + count = string.atoi(amatch.group(2)) + skip = count + line_count = line_count + count + else: + raise RuntimeError, 'error: illegal RCS file' + + rev = self.prev_revision.get(rev) + + # Now, play the delta edit commands *backwards* from the primordial + # revision forward, but rather than applying the deltas to the text of + # each revision, apply the changes to an array of revision numbers. + # This creates a "revision map" -- an array where each element + # represents a line of text in the given revision but contains only + # the revision number in which the line was introduced rather than + # the line text itself. + # + # Note: These are backward deltas for revisions on the trunk and + # forward deltas for branch revisions. + + # Create initial revision map for primordial version. + self.revision_map = [primordial] * line_count + + ancestors = [revision, ] + self.ancestor_revisions(revision) + ancestors = ancestors[:-1] # Remove "1.1" + last_revision = primordial + ancestors.reverse() + for revision in ancestors: + is_trunk_revision = self.trunk_rev.match(revision) is not None + + if is_trunk_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 + # of the 'diff' commands. + for command in diffs: + if skip > 0: + skip = skip - 1 + else: + dmatch = self.d_command.match(command) + amatch = self.a_command.match(command) + if dmatch: + start_line = string.atoi(dmatch.group(1)) + count = string.atoi(dmatch.group(2)) + temp = [] + while count > 0: + temp.append(revision) + count = count - 1 + self.revision_map = (self.revision_map[:start_line - 1] + + temp + self.revision_map[start_line - 1:]) + elif amatch: + start_line = string.atoi(amatch.group(1)) + count = string.atoi(amatch.group(2)) + del self.revision_map[start_line:start_line + count] + skip = count + else: + raise RuntimeError, 'Error parsing diff commands' + + else: + # Revisions on a branch are arranged backwards from those on + # the trunk. They specify deltas that transform a revision + # into a later revision. + adjust = 0 + diffs = self.deltatext_split(revision) + for command in diffs: + if skip > 0: + skip = skip - 1 + else: + dmatch = self.d_command.match(command) + amatch = self.a_command.match(command) + if dmatch: + start_line = string.atoi(dmatch.group(1)) + count = string.atoi(dmatch.group(2)) + adj_begin = start_line + adjust - 1 + adj_end = start_line + adjust - 1 + count + del self.revision_map[adj_begin:adj_end] + adjust = adjust - count + elif amatch: + start_line = string.atoi(amatch.group(1)) + count = string.atoi(amatch.group(2)) + skip = count + temp = [] + while count > 0: + temp.append(revision) + count = count - 1 + self.revision_map = (self.revision_map[:start_line + adjust] + + temp + self.revision_map[start_line + adjust:]) + adjust = adjust + skip + else: + raise RuntimeError, 'Error parsing diff commands' + + last_revision = revision + + return revision + + +class BlameSource: + def __init__(self, rcs_file, opt_rev=None): + # Parse the CVS file + parser = CVSParser() + revision = parser.parse_cvs_file(rcs_file, opt_rev) + count = len(parser.revision_map) + lines = parser.extract_revision(revision) + if len(lines) != count: + raise RuntimeError, 'Internal consistency error' + + # set up some state variables + self.revision = revision + self.lines = lines + self.num_lines = count + self.parser = parser + + # keep track of where we are during an iteration + self.idx = -1 + self.last = None + + def __getitem__(self, idx): + if idx == self.idx: + return self.last + if idx >= self.num_lines: + raise IndexError("No more annotations") + if idx != self.idx + 1: + raise BlameSequencingError() + + # Get the line and metadata for it. + rev = self.parser.revision_map[idx] + prev_rev = self.parser.prev_revision.get(rev) + line_number = idx + 1 + author = self.parser.revision_author[rev] + thisline = self.lines[idx] + ### TODO: Put a real date in here. + item = vclib.Annotation(thisline, line_number, rev, prev_rev, author, None) + self.last = item + self.idx = idx + return item + + +class BlameSequencingError(Exception): + pass diff --git a/lib/vclib/ccvs/ccvs.py b/lib/vclib/ccvs/ccvs.py new file mode 100644 index 00000000..dc38ab77 --- /dev/null +++ b/lib/vclib/ccvs/ccvs.py @@ -0,0 +1,398 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 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 bincvs import BaseCVSRepository, Revision, Tag, _file_log, _log_path, _logsort_date_cmp, _logsort_rev_cmp + +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." + % (string.join(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)) + 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." + % (string.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 self.itemtype(path_parts1, rev1) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts1, "/"))) + if self.itemtype(path_parts2, rev2) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts2, "/"))) + + 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, vclib.SORTBY_DEFAULT, 0, 0, {})[-1] + r2 = self.itemlog(path_parts2, rev2, vclib.SORTBY_DEFAULT, 0, 0, {})[-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, + self.utilities.diff or 'diff', diff_args) + + def annotate(self, path_parts, rev=None): + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts, "/"))) + source = blame.BlameSource(self.rcsfile(path_parts, 1), rev) + return source, source.revision + + def revinfo(self, rev): + raise vclib.UnsupportedFeature + + def openfile(self, path_parts, rev=None): + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." + % (string.join(path_parts, "/"))) + path = self.rcsfile(path_parts, 1) + sink = COSink(rev) + rcsparse.parse(open(path, 'rb'), sink) + revision = sink.last and sink.last.string + return cStringIO.StringIO(string.join(sink.sstext.text, "\n")), revision + +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 = { } + + 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 set_locker(self, rev, locker): + self.lockinfo[rev] = locker + + 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") + rev.lockinfo = self.lockinfo.get(revision) + + # perfect match if revision number matches tag number or if revision is on + # trunk and tag points to trunk. imperfect match if tag refers to a branch + # and this revision is the highest revision so far found on that branch + perfect = ((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.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 = 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) + + +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, "
" + self.last = rev diff --git a/lib/vclib/ccvs/rcsparse/__init__.py b/lib/vclib/ccvs/rcsparse/__init__.py new file mode 100644 index 00000000..829c1170 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/__init__.py @@ -0,0 +1,26 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- + +"""This package provides parsing tools for RCS files.""" + +from common import * + +try: + from tparse import parse +except ImportError: + try: + from texttools import Parser + except ImportError: + from default import Parser + + def parse(file, sink): + return Parser().parse(file, sink) diff --git a/lib/vclib/ccvs/rcsparse/common.py b/lib/vclib/ccvs/rcsparse/common.py new file mode 100644 index 00000000..f3ca2541 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/common.py @@ -0,0 +1,324 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- + +"""common.py: common classes and functions for the RCS parsing tools.""" + +import calendar +import string + +class Sink: + def set_head_revision(self, revision): + pass + + def set_principal_branch(self, branch_name): + pass + + def set_access(self, accessors): + pass + + def define_tag(self, name, revision): + pass + + def set_locker(self, revision, locker): + pass + + def set_locking(self, mode): + """Used to signal locking mode. + + Called with mode argument 'strict' if strict locking + Not called when no locking used.""" + + pass + + def set_comment(self, comment): + pass + + def set_expansion(self, mode): + pass + + def admin_completed(self): + pass + + def define_revision(self, revision, timestamp, author, state, + branches, next): + pass + + def tree_completed(self): + pass + + def set_description(self, description): + pass + + def set_revision_info(self, revision, log, text): + pass + + def parse_completed(self): + pass + + +# -------------------------------------------------------------------------- +# +# EXCEPTIONS USED BY RCSPARSE +# + +class RCSParseError(Exception): + pass + + +class RCSIllegalCharacter(RCSParseError): + pass + + +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) + ) + + +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, + } + + 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") + 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 timestamp + 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' + timestamp = calendar.timegm(tuple(date_fields) + (0, 0, 0,)) + + # 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@; + # 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 + self._read_until_semicolon() + + self.sink.define_revision(revision, timestamp, author, state, branches, + next) + + def parse_rcs_tree(self): + while 1: + revision = self.ts.get() + + # End of RCS tree description ? + if revision == 'desc': + self.ts.unget(revision) + return + + self._parse_rcs_tree_entry(revision) + + def parse_rcs_description(self): + self.ts.match('desc') + self.sink.set_description(self.ts.get()) + + def parse_rcs_deltatext(self): + while 1: + revision = self.ts.get() + if revision is None: + # EOF + break + text, sym2, log, sym1 = self.ts.mget(4) + if sym1 != 'log': + print `text[:100], sym2[:100], log[:100], sym1[:100]` + raise RCSExpected(sym1, 'log') + if sym2 != 'text': + raise RCSExpected(sym2, 'text') + ### need to add code to chew up "newphrase" + self.sink.set_revision_info(revision, log, text) + + def parse(self, file, sink): + self.ts = self.stream_class(file) + self.sink = sink + + self.parse_rcs_admin() + + # let sink know when the admin section has been completed + self.sink.admin_completed() + + self.parse_rcs_tree() + + # many sinks want to know when the tree has been completed so they can + # do some work to prep for the arrival of the deltatext + self.sink.tree_completed() + + self.parse_rcs_description() + self.parse_rcs_deltatext() + + # easiest for us to tell the sink it is done, rather than worry about + # higher level software doing it. + self.sink.parse_completed() + + self.ts = self.sink = None + +# -------------------------------------------------------------------------- diff --git a/lib/vclib/ccvs/rcsparse/debug.py b/lib/vclib/ccvs/rcsparse/debug.py new file mode 100644 index 00000000..cfeaf2b6 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/debug.py @@ -0,0 +1,122 @@ +# -*-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/ +# +# ----------------------------------------------------------------------- + +"""debug.py: various debugging tools for the rcsparse package.""" + +import time + +from __init__ import parse +import common + + +class DebugSink(common.Sink): + def set_head_revision(self, revision): + print 'head:', revision + + def set_principal_branch(self, branch_name): + print 'branch:', branch_name + + def define_tag(self, name, revision): + print 'tag:', name, '=', revision + + def set_comment(self, comment): + print 'comment:', comment + + def set_description(self, description): + print 'description:', description + + def define_revision(self, revision, timestamp, author, state, + branches, next): + print 'revision:', revision + print ' timestamp:', timestamp + print ' author:', author + print ' state:', state + print ' branches:', branches + print ' next:', next + + def set_revision_info(self, revision, log, text): + print 'revision:', revision + print ' log:', log + print ' text:', text[:100], '...' + + +class DumpSink(common.Sink): + """Dump all the parse information directly to stdout. + + The output is relatively unformatted and untagged. It is intended as a + raw dump of the data in the RCS file. A copy can be saved, then changes + made to the parsing engine, then a comparison of the new output against + the old output. + """ + def __init__(self): + global sha + import sha + + def set_head_revision(self, revision): + print revision + + def set_principal_branch(self, branch_name): + print branch_name + + def define_tag(self, name, revision): + print name, revision + + def set_comment(self, comment): + print comment + + def set_description(self, description): + print description + + def define_revision(self, revision, timestamp, author, state, + branches, next): + print revision, timestamp, author, state, branches, next + + def set_revision_info(self, revision, log, text): + print revision, sha.new(log).hexdigest(), sha.new(text).hexdigest() + + def tree_completed(self): + print 'tree_completed' + + def parse_completed(self): + print 'parse_completed' + + +def dump_file(fname): + parse(open(fname, 'rb'), DumpSink()) + +def time_file(fname): + f = open(fname, 'rb') + s = common.Sink() + t = time.time() + parse(f, s) + t = time.time() - t + print t + +def _usage(): + print 'This is normally a module for importing, but it has a couple' + print 'features for testing as an executable script.' + print 'USAGE: %s COMMAND filename,v' % sys.argv[0] + print ' where COMMAND is one of:' + print ' dump: filename is "dumped" to stdout' + print ' time: filename is parsed with the time written to stdout' + sys.exit(1) + +if __name__ == '__main__': + import sys + if len(sys.argv) != 3: + _usage() + if sys.argv[1] == 'dump': + dump_file(sys.argv[2]) + elif sys.argv[1] == 'time': + time_file(sys.argv[2]) + else: + _usage() diff --git a/lib/vclib/ccvs/rcsparse/default.py b/lib/vclib/ccvs/rcsparse/default.py new file mode 100644 index 00000000..4473d199 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/default.py @@ -0,0 +1,167 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- +# +# This file was originally based on portions of the blame.py script by +# Curt Hagenlocher. +# +# ----------------------------------------------------------------------- + +import string +import common + +class _TokenStream: + 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... + + def __init__(self, file): + self.rcsfile = file + self.idx = 0 + self.buf = self.rcsfile.read(self.CHUNK_SIZE) + if self.buf == '': + raise RuntimeError, 'EOF' + + def get(self): + "Get the next token from the RCS file." + + # Note: we can afford to loop within Python, examining individual + # characters. For the whitespace and tokens, the number of iterations + # is typically quite small. Thus, a simple iterative loop will beat + # out more complex solutions. + + buf = self.buf + idx = self.idx + + while 1: + 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 + return None + idx = 0 + + if buf[idx] not in string.whitespace: + break + + idx = idx + 1 + + if buf[idx] == ';' or buf[idx] == ':': + self.buf = buf + self.idx = idx + 1 + return buf[idx] + + if buf[idx] != '@': + end = idx + 1 + token = '' + while 1: + # find token characters in the current buffer + while end < len(buf) and buf[end] not in self.token_term: + end = end + 1 + token = token + buf[idx:end] + + 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) + idx = end = 0 + + self.buf = buf + self.idx = idx + return token + + # a "string" which starts with the "@" character. we'll skip it when we + # search for content. + idx = idx + 1 + + chunks = [ ] + + while 1: + if idx == len(buf): + idx = 0 + buf = self.rcsfile.read(self.CHUNK_SIZE) + if buf == '': + raise RuntimeError, 'EOF' + i = string.find(buf, '@', idx) + if i == -1: + chunks.append(buf[idx:]) + idx = len(buf) + continue + if i == len(buf) - 1: + chunks.append(buf[idx:i]) + idx = 0 + buf = '@' + self.rcsfile.read(self.CHUNK_SIZE) + if buf == '@': + raise RuntimeError, 'EOF' + continue + if buf[i + 1] == '@': + chunks.append(buf[idx:i+1]) + idx = i + 2 + continue + + chunks.append(buf[idx:i]) + + self.buf = buf + self.idx = i + 1 + + return string.join(chunks, '') + +# _get = get +# def get(self): + token = self._get() + print 'T:', `token` + return token + + def match(self, match): + "Try to match the next token from the input buffer." + + token = self.get() + if token != match: + raise common.RCSExpected(token, match) + + def unget(self, token): + "Put this token back, for the next get() to return." + + # Override the class' .get method with a function which clears the + # overridden method then returns the pushed token. Since this function + # will not be looked up via the class mechanism, it should be a "normal" + # function, meaning it won't have "self" automatically inserted. + # Therefore, we need to pass both self and the token thru via defaults. + + # note: we don't put this into the input buffer because it may have been + # @-unescaped already. + + def give_it_back(self=self, token=token): + del self.get + return token + + self.get = give_it_back + + def mget(self, count): + "Return multiple tokens. 'next' is at the end." + result = [ ] + for i in range(count): + result.append(self.get()) + result.reverse() + return result + + +class Parser(common._Parser): + stream_class = _TokenStream diff --git a/lib/vclib/ccvs/rcsparse/parse_rcs_file.py b/lib/vclib/ccvs/rcsparse/parse_rcs_file.py new file mode 100755 index 00000000..05ff5b07 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/parse_rcs_file.py @@ -0,0 +1,73 @@ +#! /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)) + + diff --git a/lib/vclib/ccvs/rcsparse/run-tests.py b/lib/vclib/ccvs/rcsparse/run-tests.py new file mode 100755 index 00000000..caa0fdce --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/run-tests.py @@ -0,0 +1,73 @@ +#! /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) + diff --git a/lib/vclib/ccvs/rcsparse/test-data/default,v b/lib/vclib/ccvs/rcsparse/test-data/default,v new file mode 100644 index 00000000..c34d7dab --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/test-data/default,v @@ -0,0 +1,102 @@ +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 +@@ diff --git a/lib/vclib/ccvs/rcsparse/test-data/default.out b/lib/vclib/ccvs/rcsparse/test-data/default.out new file mode 100644 index 00000000..1eea48f5 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/test-data/default.out @@ -0,0 +1,26 @@ +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() diff --git a/lib/vclib/ccvs/rcsparse/test-data/empty-file,v b/lib/vclib/ccvs/rcsparse/test-data/empty-file,v new file mode 100644 index 00000000..fda0da01 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/test-data/empty-file,v @@ -0,0 +1,10 @@ +head ; +access; +symbols; +locks; strict; +comment @# @; + + + +desc +@@ diff --git a/lib/vclib/ccvs/rcsparse/test-data/empty-file.out b/lib/vclib/ccvs/rcsparse/test-data/empty-file.out new file mode 100644 index 00000000..56f10cfc --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/test-data/empty-file.out @@ -0,0 +1,6 @@ +set_locking('strict') +set_comment('# ') +admin_completed() +tree_completed() +set_description('') +parse_completed() diff --git a/lib/vclib/ccvs/rcsparse/texttools.py b/lib/vclib/ccvs/rcsparse/texttools.py new file mode 100644 index 00000000..99bfc6f8 --- /dev/null +++ b/lib/vclib/ccvs/rcsparse/texttools.py @@ -0,0 +1,348 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- + +import string + +# note: this will raise an ImportError if it isn't available. the rcsparse +# package will recognize this and switch over to the default parser. +from mx import TextTools + +import common + + +# for convenience +_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(':') +_idchar_list.remove(';') +_idchar_list.remove('@') +_idchar = string.join(_idchar_list, '') +_idchar_set = _tt.set(_idchar) + +_onechar_token_set = _tt.set(':;') + +_not_at_set = _tt.invset('@') + +_T_TOKEN = 30 +_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-@@) + +_SUCCESS = +100 + +_EOF = 'EOF' +_CONTINUE = 'CONTINUE' +_UNUSED = 'UNUSED' + + +# continuation of a token over a chunk boundary +_c_token_table = ( + (_T_TOKEN, _tt.AllInSet, _idchar_set), + ) + +class _mxTokenStream: + + # 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... + + def __init__(self, file): + self.rcsfile = file + self.tokens = [ ] + self.partial = None + + self.string_end = None + + def _parse_chunk(self, buf, start=0): + "Get the next token from the RCS file." + + buflen = len(buf) + + assert start < buflen + + # 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. + (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. + (_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 + (_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 + # 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 + (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 + (_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: + ### need a better way to report this error + raise common.RCSIllegalCharacter() + assert idx == buflen + + # pop off the last item + last_which = taglist.pop() + + i = 0 + tlen = len(taglist) + while i < tlen: + if taglist[i] == _T_STRING_START: + j = i + 1 + while j < tlen: + if taglist[j] == _T_STRING_END: + s = _tt.join(taglist, '', i+1, j) + del taglist[i:j] + tlen = len(taglist) + taglist[i] = s + break + j = j + 1 + else: + assert last_which == _E_STRING_SPAN + s = _tt.join(taglist, '', i+1) + del taglist[i:] + self.partial = (_T_STRING_SPAN, [ s ]) + break + i = i + 1 + + # figure out whether we have a partial last-token + if last_which == _E_TOKEN: + self.partial = (_T_TOKEN, [ taglist.pop() ]) + elif last_which == _E_COMPLETE: + pass + elif last_which == _E_STRING_SPAN: + assert self.partial + else: + assert last_which == _E_STRING_END + self.partial = (_T_STRING_END, [ taglist.pop() ]) + + taglist.reverse() + taglist.extend(self.tokens) + self.tokens = taglist + + def _set_end(self, taglist, text, l, r, subtags): + self.string_end = l + + def _handle_partial(self, buf): + which, chunks = self.partial + if which == _T_TOKEN: + success, taglist, idx = _tt.tag(buf, _c_token_table) + if not success: + # The start of this buffer was not a token. So the end of the + # prior buffer was a complete token. + self.tokens.insert(0, string.join(chunks, '')) + else: + assert len(taglist) == 1 and taglist[0][0] == _T_TOKEN \ + and taglist[0][1] == 0 and taglist[0][2] == idx + if idx == len(buf): + # + # The whole buffer was one huge token, so we may have a + # partial token again. + # + # Note: this modifies the list of chunks in self.partial + # + chunks.append(buf) + + # consumed the whole buffer + return len(buf) + + # got the rest of the token. + chunks.append(buf[:idx]) + self.tokens.insert(0, string.join(chunks, '')) + + # no more partial token + self.partial = None + + return idx + + if which == _T_STRING_END: + if buf[0] != '@': + self.tokens.insert(0, string.join(chunks, '')) + return 0 + chunks.append('@') + start = 1 + else: + start = 0 + + self.string_end = None + string_table = ( + (None, _tt.Is, '@', +3, +1), + (_UNUSED, _tt.Is + _tt.AppendMatch, '@', +1, -1), + (self._set_end, _tt.Skip + _tt.CallTag, 0, 0, _SUCCESS), + + (None, _tt.EOF, _tt.Here, +1, _SUCCESS), + + # suck up everything that isn't an AT. move to next rule to look + # for EOF + (_UNUSED, _tt.AllInSet + _tt.AppendMatch, _not_at_set, 0, +1), + + # go back to look for double AT if we aren't at the end of the string + (None, _tt.EOF, _tt.Here, -5, _SUCCESS), + ) + + success, unused, idx = _tt.tag(buf, string_table, + start, len(buf), chunks) + + # must have matched at least one item + assert success + + if self.string_end is None: + assert idx == len(buf) + self.partial = (_T_STRING_SPAN, chunks) + elif self.string_end < len(buf): + self.partial = None + self.tokens.insert(0, string.join(chunks, '')) + else: + self.partial = (_T_STRING_END, chunks) + + return idx + + def _parse_more(self): + buf = self.rcsfile.read(self.CHUNK_SIZE) + if not buf: + return _EOF + + if self.partial: + idx = self._handle_partial(buf) + if idx is None: + return _CONTINUE + if idx < len(buf): + self._parse_chunk(buf, idx) + else: + self._parse_chunk(buf) + + return _CONTINUE + + def get(self): + try: + return self.tokens.pop() + except IndexError: + pass + + while not self.tokens: + action = self._parse_more() + if action == _EOF: + return None + + return self.tokens.pop() + + +# _get = get +# def get(self): + token = self._get() + print 'T:', `token` + return token + + def match(self, match): + if self.tokens: + token = self.tokens.pop() + else: + token = self.get() + + if token != match: + raise common.RCSExpected(token, match) + + def unget(self, token): + self.tokens.append(token) + + def mget(self, count): + "Return multiple tokens. 'next' is at the end." + while len(self.tokens) < count: + action = self._parse_more() + if action == _EOF: + ### fix this + raise RuntimeError, 'EOF hit while expecting tokens' + result = self.tokens[-count:] + del self.tokens[-count:] + return result + + +class Parser(common._Parser): + stream_class = _mxTokenStream diff --git a/lib/vclib/svn/__init__.py b/lib/vclib/svn/__init__.py new file mode 100644 index 00000000..51ef3d4f --- /dev/null +++ b/lib/vclib/svn/__init__.py @@ -0,0 +1,55 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 Subversion repositories" + +import os +import os.path +import re + +_re_url = re.compile('^(http|https|file|svn|svn\+[^:]+)://') + +def canonicalize_rootpath(rootpath): + try: + import svn.core + return svn.core.svn_path_canonicalize(rootpath) + except: + if re.search(_re_url, rootpath): + return rootpath[-1] == '/' and rootpath[:-1] or rootpath + return os.path.normpath(rootpath) + + +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. + 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 + + +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) + else: + import svn_repos + return svn_repos.LocalSubversionRepository(name, rootpath, authorizer, + utilities, config_dir) diff --git a/lib/vclib/svn/svn_ra.py b/lib/vclib/svn/svn_ra.py new file mode 100644 index 00000000..6c341cd9 --- /dev/null +++ b/lib/vclib/svn/svn_ra.py @@ -0,0 +1,546 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 +import urllib +from svn_repos import Revision, SVNChangedPath, _datestr_to_date, _compare_paths, _path_parts, _cleanup_path, _rev2optrev +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 + +### END COMPATABILITY CODE ### + + +class LogCollector: + ### TODO: Make this thing authz-aware + + def __init__(self, path, show_all_logs, lockinfo): + # 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 + + 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: + entry = Revision(revision, _datestr_to_date(date), author, message, None, + self.lockinfo, self.path[1:], None, None) + self.logs.append(entry) + if this_path: + self.path = this_path + +def temp_checkout(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. + # TODO: svn_cmdline_setup_auth_baton() is mo' better (when available) + core.svn_config_ensure(self.config_dir) + self.ctx = client.svn_client_ctx_t() + self.ctx.auth_baton = core.svn_auth_open([ + client.svn_client_get_simple_provider(), + client.svn_client_get_username_provider(), + client.svn_client_get_ssl_server_trust_file_provider(), + client.svn_client_get_ssl_client_cert_file_provider(), + client.svn_client_get_ssl_client_cert_pw_file_provider(), + ]) + self.ctx.config = core.svn_config_get_config(self.config_dir) + if self.config_dir is not None: + core.svn_auth_set_parameter(self.ctx.auth_baton, + core.SVN_AUTH_PARAM_CONFIG_DIR, + self.config_dir) + 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 = { } + + 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): + path = self._getpath(path_parts) + if self.itemtype(path_parts, rev) != vclib.FILE: # does auth-check + raise vclib.Error("Path '%s' is not a file." % path) + rev = self._getrev(rev) + url = self._geturl(path) + tmp_file = tempfile.mktemp() + stream = core.svn_stream_from_aprfile(tmp_file) + ### rev here should be the last history revision of the URL + client.svn_client_cat(core.Stream(stream), url, _rev2optrev(rev), self.ctx) + core.svn_stream_close(stream) + return SelfCleanFP(tmp_file), self._get_last_history_rev(path_parts, rev) + + 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 + if vclib.check_path_access(self, path_parts + [name], kind, rev): + 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] + if not vclib.check_path_access(self, entry_path_parts, entry.kind, rev): + continue + dirent = dirents[entry.name] + entry.date, entry.author, entry.log, changes = \ + self.revinfo(dirent.created_rev) + entry.rev = 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) + + # Use ls3 to fetch the lock status for this item. + lockinfo = None + basename = path_parts and path_parts[-1] or "" + dirents, locks = list_directory(url, _rev2optrev(rev), + _rev2optrev(rev), 0, self.ctx) + if locks.has_key(basename): + lockinfo = locks[basename].owner + + # 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) + + cross_copies = options.get('svn_cross_copies', 0) + log_limit = 0 + if limit: + log_limit = first + limit + client.svn_client_log2([url], _rev2optrev(rev), _rev2optrev(1), + log_limit, 1, not cross_copies, + lc.add_log, self.ctx) + revs = lc.logs + revs.sort() + prev = None + for rev in revs: + 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): + 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) + + 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(vclib.Annotation(line, line_no+1, revision, prev_rev, + author, None)) + + client.svn_client_blame(url, _rev2optrev(1), _rev2optrev(rev), + _blame_cb, self.ctx) + + return blame_data, rev + + def revinfo(self, rev): + rev = self._getrev(rev) + cached_info = self._revinfo_cache.get(rev) + if not cached_info: + cached_info = self._revinfo_raw(rev) + self._revinfo_cache[rev] = cached_info + return cached_info[0], cached_info[1], cached_info[2], cached_info[3] + + 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) + if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1): + raise vclib.ItemNotFound(path_parts1) + if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2): + raise vclib.ItemNotFound(path_parts2) + + args = vclib._diff_args(type, options) + + def _date_from_rev(rev): + date, author, msg, changes = self.revinfo(rev) + return date + + try: + temp1 = temp_checkout(self, p1, r1) + temp2 = temp_checkout(self, p2, r2) + info1 = p1, _date_from_rev(r1), r1 + info2 = p2, _date_from_rev(r2), r2 + return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args) + except core.SubversionException, e: + if e.apr_err == vclib.svn.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 _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 _geturl(self, path=None): + if not path: + return self.rootpath + return self.rootpath + '/' + urllib.quote(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.""" + + dir_url = self._geturl(path) + if path: + key = str(rev) + '/' + path + else: + key = str(rev) + dirents_locks = self._dirent_cache.get(key) + if not dirents_locks: + dirents, locks = list_directory(dir_url, _rev2optrev(rev), + _rev2optrev(rev), 0, self.ctx) + dirents_locks = [dirents, locks] + self._dirent_cache[key] = dirents_locks + return dirents_locks[0], dirents_locks[1] + + def _get_last_history_rev(self, path_parts, rev): + url = self._geturl(self._getpath(path_parts)) + optrev = _rev2optrev(rev) + revisions = [] + def _info_cb(path, info, pool, retval=revisions): + revisions.append(info.last_changed_rev) + client.svn_client_info(url, optrev, optrev, _info_cb, 0, self.ctx) + return revisions[0] + + def _revinfo_raw(self, rev): + # return 5-tuple (date, author, message, changes) + optrev = _rev2optrev(rev) + revs = [] + + def _log_cb(changed_paths, revision, author, + datestr, message, pool, retval=revs): + date = _datestr_to_date(datestr) + action_map = { 'D' : vclib.DELETED, + 'A' : vclib.ADDED, + 'R' : vclib.REPLACED, + 'M' : vclib.MODIFIED, + } + paths = (changed_paths or {}).keys() + paths.sort(lambda a, b: _compare_paths(a, b)) + changes = [] + found_readable = found_unreadable = 0 + for path in paths: + pathtype = None + change = changed_paths[path] + action = action_map.get(change.action, vclib.MODIFIED) + ### Wrong, diddily wrong wrong wrong. Can you say, + ### "Manufacturing data left and right because it hurts to + ### figure out the right stuff?" + if change.copyfrom_path and change.copyfrom_rev: + is_copy = 1 + base_path = change.copyfrom_path + 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 (we lie about the path type) + parts = _path_parts(path) + if vclib.check_path_access(self, parts, vclib.FILE, revision): + if is_copy and base_path and (base_path != path): + parts = _path_parts(base_path) + if vclib.check_path_access(self, parts, vclib.FILE, base_rev): + is_copy = 0 + base_path = None + base_rev = None + changes.append(SVNChangedPath(path, revision, pathtype, base_path, + base_rev, action, is_copy, 0, 0)) + found_readable = 1 + else: + found_unreadable = 1 + + if found_unreadable: + message = None + if not found_readable: + author = None + date = None + revs.append([date, author, message, changes]) + + client.svn_client_log([self.rootpath], optrev, optrev, + 1, 0, _log_cb, self.ctx) + return revs[0][0], revs[0][1], revs[0][2], revs[0][3] + + ##--- 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: + 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, path, rev): + # NOTE: We can't use svn_client_propget here because the + # interfaces in that layer strip out the properties not meant for + # human consumption (such as svn:entry:committed-rev, which we are + # using here to get the created revision of PATH@REV). + kind = ra.svn_ra_check_path(self.ra_session, path, rev) + if kind == core.svn_node_none: + raise vclib.ItemNotFound(_path_parts(path)) + elif kind == core.svn_node_dir: + props = get_directory_props(self.ra_session, path, rev) + elif kind == core.svn_node_file: + fetched_rev, props = ra.svn_ra_get_file(self.ra_session, path, rev, None) + return int(props.get(core.SVN_PROP_ENTRY_COMMITTED_REV, + SVN_INVALID_REVNUM)) + + 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 diff --git a/lib/vclib/svn/svn_repos.py b/lib/vclib/svn/svn_repos.py new file mode 100644 index 00000000..6f98e79f --- /dev/null +++ b/lib/vclib/svn/svn_repos.py @@ -0,0 +1,778 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 stat +import string +import cStringIO +import signal +import shutil +import time +import tempfile +import popen +import re +from svn import fs, repos, core, client, delta + + +### 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)" + + +def _allow_all(root, path, pool): + """Generic authz_read_func that permits access to all paths""" + return 1 + + +def _path_parts(path): + return filter(None, string.split(path, '/')) + + +def _cleanup_path(path): + """Return a cleaned-up Subversion filesystem path""" + return string.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) + if rootpath and rootpath[0] != '/': + rootpath = '/' + rootpath + if os.sep != '/': + rootpath = string.replace(rootpath, os.sep, '/') + return 'file://' + string.join([rootpath, path], "/") + + +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 = 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) + 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("", core.SVN_ERR_CEASE_INVOCATION) + + def __getitem__(self, idx): + return self.histories[idx] + + +def _get_history(svnrepos, path, rev, path_type, limit=0, options={}): + rev_paths = [] + fsroot = svnrepos._getroot(rev) + show_all_logs = options.get('svn_show_all_dir_logs', 0) + if not show_all_logs: + # See if the path is a file or directory. + kind = fs.check_path(fsroot, path) + if kind is core.svn_node_file: + show_all_logs = 1 + + # Instantiate a NodeHistory collector object, and use it to collect + # history items for PATH@REV. + history = NodeHistory(svnrepos.fs_ptr, show_all_logs, limit) + try: + repos.svn_repos_history(svnrepos.fs_ptr, path, history.add_history, + 1, rev, options.get('svn_cross_copies', 0)) + except core.SubversionException, e: + if e.apr_err != core.SVN_ERR_CEASE_INVOCATION: + raise + + # Now, iterate over those history items, checking for changes of + # location, pruning as necessitated by authz rules. + for hist_rev, hist_path in history: + path_parts = _path_parts(hist_path) + if not vclib.check_path_access(svnrepos, path_parts, path_type, hist_rev): + break + rev_paths.append([hist_rev, hist_path]) + return rev_paths + + +def _log_helper(svnrepos, path, rev, lockinfo): + rev_root = fs.revision_root(svnrepos.fs_ptr, rev) + + # Was this path@rev the target of a copy? + copyfrom_rev, copyfrom_path = fs.copied_from(rev_root, path) + + # Assemble our LogEntry + date, author, msg, changes = svnrepos.revinfo(rev) + if fs.is_file(rev_root, path): + size = fs.file_length(rev_root, path) + else: + size = None + entry = Revision(rev, date, author, msg, size, lockinfo, path, + copyfrom_path and _cleanup_path(copyfrom_path), + copyfrom_rev) + return entry + + +def _get_last_history_rev(fsroot, path): + history = fs.node_history(fsroot, path) + history = fs.history_prev(history, 0) + 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): + self.idx = -1 + self.first_rev = first_rev + self.blame_data = [] + + ctx = client.ctx_t() + core.svn_config_ensure(None) + ctx.config = core.svn_config_get_config(None) + 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.blame2(local_url, _rev2optrev(rev), _rev2optrev(first_rev), + _rev2optrev(rev), self._blame_cb, ctx) + except core.SubversionException, 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 + 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.svn_client_path = utilities.svn or 'svn' + self.diff_cmd = utilities.diff or 'diff' + self.config_dir = config_dir + + # See if this repository is even viewable, authz-wise. + if not vclib.check_root_access(self): + raise vclib.ReposNotFound(name) + + def open(self): + # Register a handler for SIGTERM so we can have a chance to + # cleanup. If ViewVC takes too long to start generating CGI + # output, Apache will grow impatient and SIGTERM it. While we + # don't mind getting told to bail, we want to gracefully close the + # repository before we bail. + def _sigterm_handler(signum, frame, self=self): + sys.exit(-1) + try: + signal.signal(signal.SIGTERM, _sigterm_handler) + except ValueError: + # This is probably "ValueError: signal only works in main + # thread", which will get thrown by the likes of mod_python + # when trying to install a signal handler from a thread that + # isn't the main one. We'll just not care. + pass + + # Open the repository and init some other variables. + self.repos = repos.svn_repos_open(self.rootpath) + self.fs_ptr = repos.svn_repos_fs(self.repos) + self.youngest = fs.youngest_rev(self.fs_ptr) + self._fsroots = {} + self._revinfo_cache = {} + + 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) + kind = fs.check_path(self._getroot(rev), basepath) + pathtype = None + if kind == core.svn_node_dir: + pathtype = vclib.DIR + elif kind == core.svn_node_file: + pathtype = vclib.FILE + else: + 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): + 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, 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 = _log_helper(self, path, rev, lockinfo) + if revision: + revision.prev = None + revs.append(revision) + else: + history = _get_history(self, path, rev, path_type, + first + limit, options) + if len(history) < first: + history = [] + if limit: + history = history[first:first+limit] + + for hist_rev, hist_path in history: + revision = _log_helper(self, hist_path, hist_rev, lockinfo) + 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): + path = self._getpath(path_parts) + path_type = self.itemtype(path_parts, rev) # does auth-check + if path_type != vclib.FILE: + raise vclib.Error("Path '%s' is not a file." % path) + rev = self._getrev(rev) + fsroot = self._getroot(rev) + history = _get_history(self, path, rev, path_type, 0, + {'svn_cross_copies': 1}) + youngest_rev, youngest_path = history[0] + oldest_rev, oldest_path = history[-1] + source = BlameSource(_rootpath2url(self.rootpath, path), + youngest_rev, oldest_rev) + return source, youngest_rev + + def _revinfo_raw(self, rev): + fsroot = self._getroot(rev) + + # Get the changes for the revision + editor = repos.ChangeCollector(self.fs_ptr, fsroot) + e_ptr, e_baton = delta.make_editor(editor) + repos.svn_repos_replay(fsroot, e_ptr, e_baton) + changes = editor.get_changes() + changedpaths = {} + + # Now get the revision property info. Would use + # editor.get_root_props(), but something is broken there... + revprops = fs.revision_proplist(self.fs_ptr, rev) + msg = revprops.get(core.SVN_PROP_REVISION_LOG) + author = revprops.get(core.SVN_PROP_REVISION_AUTHOR) + datestr = revprops.get(core.SVN_PROP_REVISION_DATE) + + # Copy the Subversion changes into a new hash, converting them into + # ChangedPath objects. + found_readable = found_unreadable = 0 + for path in changes.keys(): + change = changes[path] + if change.path: + change.path = _cleanup_path(change.path) + if change.base_path: + change.base_path = _cleanup_path(change.base_path) + is_copy = 0 + if not hasattr(change, 'action'): # new to subversion 1.4.0 + action = vclib.MODIFIED + if not change.path: + action = vclib.DELETED + elif change.added: + action = vclib.ADDED + replace_check_path = path + if change.base_path and change.base_rev: + replace_check_path = change.base_path + if changedpaths.has_key(replace_check_path) \ + and changedpaths[replace_check_path].action == vclib.DELETED: + action = vclib.REPLACED + else: + if change.action == repos.CHANGE_ACTION_ADD: + action = vclib.ADDED + elif change.action == repos.CHANGE_ACTION_DELETE: + action = vclib.DELETED + elif change.action == repos.CHANGE_ACTION_REPLACE: + action = vclib.REPLACED + else: + action = vclib.MODIFIED + if (action == vclib.ADDED or action == vclib.REPLACED) \ + and change.base_path \ + and change.base_rev: + is_copy = 1 + if change.item_kind == core.svn_node_dir: + pathtype = vclib.DIR + elif change.item_kind == core.svn_node_file: + pathtype = vclib.FILE + else: + pathtype = None + + parts = _path_parts(path) + if vclib.check_path_access(self, parts, pathtype, rev): + if is_copy and change.base_path and (change.base_path != path): + parts = _path_parts(change.base_path) + if not vclib.check_path_access(self, parts, pathtype, change.base_rev): + is_copy = 0 + change.base_path = None + change.base_rev = None + changedpaths[path] = SVNChangedPath(path, rev, pathtype, + change.base_path, + change.base_rev, action, + is_copy, change.text_changed, + change.prop_changes) + found_readable = 1 + else: + found_unreadable = 1 + + # Return our tuple, auth-filtered: date, author, msg, changes + if found_unreadable: + msg = None + if not found_readable: + author = None + datestr = None + + date = _datestr_to_date(datestr) + return date, author, msg, changedpaths.values() + + def revinfo(self, rev): + rev = self._getrev(rev) + cached_info = self._revinfo_cache.get(rev) + if not cached_info: + cached_info = self._revinfo_raw(rev) + self._revinfo_cache[rev] = cached_info + return cached_info[0], cached_info[1], cached_info[2], cached_info[3] + + 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) + if not vclib.check_path_access(self, path_parts1, vclib.FILE, rev1): + raise vclib.ItemNotFound(path_parts1) + if not vclib.check_path_access(self, path_parts2, vclib.FILE, rev2): + raise vclib.ItemNotFound(path_parts2) + + args = vclib._diff_args(type, options) + + def _date_from_rev(rev): + date, author, msg, changes = self.revinfo(rev) + return date + + try: + temp1 = temp_checkout(self, p1, r1) + temp2 = temp_checkout(self, p2, r2) + info1 = p1, _date_from_rev(r1), r1 + info2 = p2, _date_from_rev(r2), r2 + return vclib._diff_fp(temp1, temp2, info1, info2, self.diff_cmd, args) + except core.SubversionException, 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 _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) + return r + + ##--- 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: + 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 max(peg_revision, limit_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: + 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 diff --git a/lib/viewvc.py b/lib/viewvc.py new file mode 100644 index 00000000..1df66bf4 --- /dev/null +++ b/lib/viewvc.py @@ -0,0 +1,4021 @@ +# -*-python-*- +# +# Copyright (C) 1999-2008 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 +# +# ----------------------------------------------------------------------- + +__version__ = '1.1.0-beta1' + +# this comes from our library; measure the startup time +import debug +debug.t_start('startup') +debug.t_start('imports') + +# standard modules that we know are in the path or builtin +import sys +import os +import cgi +import gzip +import mimetypes +import re +import rfc822 +import stat +import string +import struct +import tempfile +import time +import types +import urllib + +# These modules come from our library (the stub has set up the path) +import accept +import compat +import config +import ezt +import popen +import sapi +import vcauth +import vclib +import vclib.ccvs +import vclib.svn + +try: + import idiff +except (SyntaxError, ImportError): + idiff = None + +debug.t_end('imports') + +######################################################################### + +checkout_magic_path = '*checkout*' +# According to RFC 1738 the '~' character is unsafe in URLs. +# But for compatibility with URLs bookmarked with old releases of ViewCVS: +oldstyle_checkout_magic_path = '~checkout~' +docroot_magic_path = '*docroot*' +viewcvs_mime_type = 'text/vnd.viewcvs-markup' +alt_mime_type = 'text/x-cvsweb-markup' +view_roots_magic = '*viewroots*' + +# Put here the variables we need in order to hold our state - they +# will be added (with their current value) to (almost) any link/query +# string you construct. +_sticky_vars = [ + 'hideattic', + 'sortby', + 'sortdir', + 'logsort', + 'diff_format', + 'search', + 'limit_changes', + ] + +# number of extra pages of information on either side of the current +# page to fetch (see use_pagesize configuration option) +EXTRA_PAGES = 3 + +# for reading/writing between a couple descriptors +CHUNK_SIZE = 8192 + +# for rcsdiff processing of header +_RCSDIFF_IS_BINARY = 'binary-diff' +_RCSDIFF_ERROR = 'error' + +# special characters that don't need to be URL encoded +_URL_SAFE_CHARS = "/*~" + + +class Request: + def __init__(self, server, cfg): + self.server = server + self.cfg = cfg + + self.script_name = _normalize_path(server.getenv('SCRIPT_NAME', '')) + self.browser = server.getenv('HTTP_USER_AGENT', 'unknown') + + # process the Accept-Language: header, and load the key/value + # files, given the selected language + hal = server.getenv('HTTP_ACCEPT_LANGUAGE','') + self.lang_selector = accept.language(hal) + self.language = self.lang_selector.select_from(cfg.general.languages) + self.kv = cfg.load_kv_files(self.language) + + # check for an authenticated username + self.username = server.getenv('REMOTE_USER') + + # if we allow compressed output, see if the client does too + self.gzip_compress_level = 0 + if cfg.options.allow_compress: + http_accept_encoding = os.environ.get("HTTP_ACCEPT_ENCODING", "") + if "gzip" in filter(None, + map(lambda x: string.strip(x), + string.split(http_accept_encoding, ","))): + self.gzip_compress_level = 9 # make this configurable? + + def run_viewvc(self): + + cfg = self.cfg + + # This function first parses the query string and sets the following + # variables. Then it executes the request. + self.view_func = None # function to call to process the request + self.repos = None # object representing current repository + self.rootname = None # name of current root (as used in viewvc.conf) + self.roottype = None # current root type ('svn' or 'cvs') + self.rootpath = None # physical path to current root + self.pathtype = None # type of path, either vclib.FILE or vclib.DIR + self.where = None # path to file or directory in current root + self.query_dict = {} # validated and cleaned up query options + self.path_parts = None # for convenience, equals where.split('/') + self.pathrev = None # current path revision or tag + self.auth = None # authorizer module in use + + # redirect if we're loading from a valid but irregular URL + # These redirects aren't neccessary to make ViewVC work, it functions + # just fine without them, but they make it easier for server admins to + # implement access restrictions based on URL + needs_redirect = 0 + + # Process the query params + for name, values in self.server.params().items(): + # patch up old queries that use 'cvsroot' to look like they used 'root' + if name == 'cvsroot': + name = 'root' + needs_redirect = 1 + + # same for 'only_with_tag' and 'pathrev' + if name == 'only_with_tag': + name = 'pathrev' + needs_redirect = 1 + + # validate the parameter + _validate_param(name, values[0]) + + # Only allow the magic ViewVC MIME types (the ones used for + # requesting the markup as as-text views) to be declared via CGI + # params. Ignore disallowed values. + if (name == 'content-type') and \ + (not values[0] in (viewcvs_mime_type, + alt_mime_type, + 'text/plain')): + continue + + # if we're here, then the parameter is okay + self.query_dict[name] = values[0] + + # handle view parameter, redirecting old view=rev URLs to view=revision + if self.query_dict.get('view') == 'rev': + self.query_dict['view'] = 'revision' + needs_redirect = 1 + self.view_func = _views.get(self.query_dict.get('view', None), + self.view_func) + + # Process PATH_INFO component of query string + path_info = self.server.getenv('PATH_INFO', '') + + # clean it up. this removes duplicate '/' characters and any that may + # exist at the front or end of the path. + ### we might want to redirect to the cleaned up URL + path_parts = _path_parts(path_info) + + if path_parts: + # handle magic path prefixes + if path_parts[0] == docroot_magic_path: + # if this is just a simple hunk of doc, then serve it up + self.where = _path_join(path_parts[1:]) + return view_doc(self) + elif path_parts[0] in (checkout_magic_path, + oldstyle_checkout_magic_path): + path_parts.pop(0) + self.view_func = view_checkout + if not cfg.options.checkout_magic: + needs_redirect = 1 + + # handle tarball magic suffixes + if self.view_func is download_tarball: + if (self.query_dict.get('parent')): + del path_parts[-1] + elif path_parts[-1][-7:] == ".tar.gz": + path_parts[-1] = path_parts[-1][:-7] + + # Figure out root name + self.rootname = self.query_dict.get('root') + if self.rootname == view_roots_magic: + del self.query_dict['root'] + self.rootname = "" + needs_redirect = 1 + elif self.rootname is None: + if cfg.options.root_as_url_component: + if path_parts: + self.rootname = path_parts.pop(0) + else: + self.rootname = "" + elif self.view_func != view_roots: + self.rootname = cfg.general.default_root + elif cfg.options.root_as_url_component: + needs_redirect = 1 + + self.where = _path_join(path_parts) + self.path_parts = path_parts + + if self.rootname: + roottype, rootpath = locate_root(cfg, self.rootname) + if roottype: + # Overlay root-specific options. + cfg.overlay_root_options(self.rootname) + + # Setup an Authorizer for this rootname and username + self.auth = setup_authorizer(cfg, self.username, self.rootname) + + # Create the repository object + try: + if roottype == 'cvs': + self.rootpath = vclib.ccvs.canonicalize_rootpath(rootpath) + self.repos = vclib.ccvs.CVSRepository(self.rootname, + self.rootpath, + self.auth, + cfg.utilities, + cfg.options.use_rcsparse) + # required so that spawned rcs programs correctly expand + # $CVSHeader$ + os.environ['CVSROOT'] = self.rootpath + elif roottype == 'svn': + self.rootpath = vclib.svn.canonicalize_rootpath(rootpath) + self.repos = vclib.svn.SubversionRepository(self.rootname, + self.rootpath, + self.auth, + cfg.utilities, + cfg.options.svn_config_dir) + else: + raise vclib.ReposNotFound() + except vclib.ReposNotFound: + pass + if self.repos is None: + raise debug.ViewVCException( + 'The root "%s" is unknown. If you believe the value is ' + 'correct, then please double-check your configuration.' + % self.rootname, "404 Not Found") + + if self.repos: + self.repos.open() + type = self.repos.roottype() + if type == vclib.SVN: + self.roottype = 'svn' + elif type == vclib.CVS: + self.roottype = 'cvs' + else: + raise debug.ViewVCException( + 'The root "%s" has an unknown type (%s).' % (self.rootname, type), + "500 Internal Server Error") + + # If this is using an old-style 'rev' parameter, redirect to new hotness. + # Subversion URLs will now use 'pathrev'; CVS ones use 'revision'. + if self.repos and self.query_dict.has_key('rev'): + if self.roottype == 'svn' \ + and not self.query_dict.has_key('pathrev') \ + and not self.view_func == view_revision: + self.query_dict['pathrev'] = self.query_dict['rev'] + del self.query_dict['rev'] + else: # elif not self.query_dict.has_key('revision'): ? + self.query_dict['revision'] = self.query_dict['rev'] + del self.query_dict['rev'] + needs_redirect = 1 + + if self.repos and self.view_func is not redirect_pathrev: + # If this is an intended-to-be-hidden CVSROOT path, complain. + if cfg.options.hide_cvsroot \ + and is_cvsroot_path(self.roottype, path_parts): + raise debug.ViewVCException("Unknown location: /%s" % self.where, + "404 Not Found") + + # Make sure path exists + self.pathrev = pathrev = self.query_dict.get('pathrev') + self.pathtype = _repos_pathtype(self.repos, path_parts, pathrev) + + if self.pathtype is None: + # Path doesn't exist, see if it could be an old-style ViewVC URL + # with a fake suffix. + result = _strip_suffix('.diff', path_parts, pathrev, vclib.FILE, \ + self.repos, view_diff) or \ + _strip_suffix('.tar.gz', path_parts, pathrev, vclib.DIR, \ + self.repos, download_tarball) or \ + _strip_suffix('root.tar.gz', path_parts, pathrev, vclib.DIR,\ + self.repos, download_tarball) or \ + _strip_suffix(self.rootname + '-root.tar.gz', \ + path_parts, pathrev, vclib.DIR, \ + self.repos, download_tarball) or \ + _strip_suffix('root', \ + path_parts, pathrev, vclib.DIR, \ + self.repos, download_tarball) or \ + _strip_suffix(self.rootname + '-root', \ + path_parts, pathrev, vclib.DIR, \ + self.repos, download_tarball) + if result: + self.path_parts, self.pathtype, self.view_func = result + self.where = _path_join(self.path_parts) + needs_redirect = 1 + else: + raise debug.ViewVCException("Unknown location: /%s" % self.where, + "404 Not Found") + + # If we have an old ViewCVS Attic URL which is still valid, redirect + if self.roottype == 'cvs': + attic_parts = None + if (self.pathtype == vclib.FILE and len(self.path_parts) > 1 + and self.path_parts[-2] == 'Attic'): + attic_parts = self.path_parts[:-2] + self.path_parts[-1:] + elif (self.pathtype == vclib.DIR and len(self.path_parts) > 0 + and self.path_parts[-1] == 'Attic'): + attic_parts = self.path_parts[:-1] + if attic_parts: + self.path_parts = attic_parts + self.where = _path_join(attic_parts) + needs_redirect = 1 + + if self.view_func is None: + # view parameter is not set, try looking at pathtype and the + # other parameters + if not self.rootname: + self.view_func = view_roots + elif self.pathtype == vclib.DIR: + # ViewCVS 0.9.2 used to put ?tarball=1 at the end of tarball urls + if self.query_dict.has_key('tarball'): + self.view_func = download_tarball + else: + self.view_func = view_directory + elif self.pathtype == vclib.FILE: + if self.query_dict.has_key('r1') and self.query_dict.has_key('r2'): + self.view_func = view_diff + elif self.query_dict.has_key('annotate'): + self.view_func = view_annotate + elif self.query_dict.has_key('graph'): + if not self.query_dict.has_key('makeimage'): + self.view_func = view_cvsgraph + else: + self.view_func = view_cvsgraph_image + elif self.query_dict.has_key('revision') \ + or cfg.options.default_file_view != "log": + if cfg.options.default_file_view == "markup" \ + or self.query_dict.get('content-type', None) \ + in (viewcvs_mime_type, alt_mime_type): + self.view_func = view_markup + else: + self.view_func = view_checkout + else: + self.view_func = view_log + + # If we've chosen the roots or revision view, our effective + # location is not really "inside" the repository, so we have no + # path and therefore no path parts or type, either. + if self.view_func is view_revision or self.view_func is view_roots: + self.where = '' + self.path_parts = [] + self.pathtype = None + + # if we have a directory and the request didn't end in "/", then redirect + # so that it does. + if (self.pathtype == vclib.DIR and path_info[-1:] != '/' + and self.view_func is not download_tarball + and self.view_func is not redirect_pathrev): + needs_redirect = 1 + + # redirect now that we know the URL is valid + if needs_redirect: + self.server.redirect(self.get_url()) + + # startup is done now. + debug.t_end('startup') + + # Call the function for the selected view. + self.view_func(self) + + def get_url(self, escape=0, partial=0, prefix=0, **args): + """Constructs a link to another ViewVC page just like the get_link + function except that it returns a single URL instead of a URL + split into components. If PREFIX is set, include the protocol and + server name portions of the URL.""" + + url, params = apply(self.get_link, (), args) + qs = compat.urlencode(params) + if qs: + result = urllib.quote(url, _URL_SAFE_CHARS) + '?' + qs + else: + result = urllib.quote(url, _URL_SAFE_CHARS) + + if partial: + result = result + (qs and '&' or '?') + if escape: + result = self.server.escape(result) + if prefix: + result = '%s://%s%s' % \ + (self.server.getenv("HTTPS") == "on" and "https" or "http", + self.server.getenv("HTTP_HOST"), + result) + return result + + def get_form(self, **args): + """Constructs a link to another ViewVC page just like the get_link + function except that it returns a base URL suitable for use as an + HTML form action, and an iterable object with .name and .value + attributes representing stuff that should be in tags with the link parameters.""" + + url, params = apply(self.get_link, (), args) + action = self.server.escape(urllib.quote(url, _URL_SAFE_CHARS)) + hidden_values = [] + for name, value in params.items(): + hidden_values.append(_item(name=name, value=value)) + return action, hidden_values + + def get_link(self, view_func=None, where=None, pathtype=None, params=None): + """Constructs a link pointing to another ViewVC page. All arguments + correspond to members of the Request object. If they are set to + None they take values from the current page. Return value is a base + URL and a dictionary of parameters""" + + cfg = self.cfg + + if view_func is None: + view_func = self.view_func + + if params is None: + params = self.query_dict.copy() + else: + params = params.copy() + + # must specify both where and pathtype or neither + assert (where is None) == (pathtype is None) + + # if we are asking for the revision info view, we don't need any + # path information + if (view_func is view_revision or view_func is view_roots + or view_func is redirect_pathrev): + where = pathtype = None + elif where is None: + where = self.where + pathtype = self.pathtype + + # no need to add sticky variables for views with no links + sticky_vars = not (view_func is view_checkout + or view_func is download_tarball) + + # The logic used to construct the URL is an inverse of the + # logic used to interpret URLs in Request.run_viewvc + + url = self.script_name + + # add checkout magic if neccessary + if view_func is view_checkout and cfg.options.checkout_magic: + url = url + '/' + checkout_magic_path + + # add root to url + rootname = None + if view_func is not view_roots: + if cfg.options.root_as_url_component: + # remove root from parameter list if present + try: + rootname = params['root'] + except KeyError: + rootname = self.rootname + else: + del params['root'] + + # add root path component + if rootname is not None: + url = url + '/' + rootname + + else: + # add root to parameter list + try: + rootname = params['root'] + except KeyError: + rootname = params['root'] = self.rootname + + # no need to specify default root + if rootname == cfg.general.default_root: + del params['root'] + + # add 'pathrev' value to parameter list + if (self.pathrev is not None + and not params.has_key('pathrev') + and view_func is not view_revision + and rootname == self.rootname): + params['pathrev'] = self.pathrev + + # add path + if where: + url = url + '/' + where + + # add trailing slash for a directory + if pathtype == vclib.DIR: + url = url + '/' + + # normalize top level URLs for use in Location headers and A tags + elif not url: + url = '/' + + # no need to explicitly specify directory view for a directory + if view_func is view_directory and pathtype == vclib.DIR: + view_func = None + + # no need to explicitly specify roots view when in root_as_url + # mode or there's no default root + if view_func is view_roots and (cfg.options.root_as_url_component + or not cfg.general.default_root): + view_func = None + + # no need to explicitly specify annotate view when + # there's an annotate parameter + if view_func is view_annotate and params.get('annotate') is not None: + view_func = None + + # no need to explicitly specify diff view when + # there's r1 and r2 parameters + if (view_func is view_diff and params.get('r1') is not None + and params.get('r2') is not None): + view_func = None + + # no need to explicitly specify checkout view when it's the default + # view or when checkout_magic is enabled + if view_func is view_checkout: + if ((cfg.options.default_file_view == "co" and pathtype == vclib.FILE) + or cfg.options.checkout_magic): + view_func = None + + # no need to explicitly specify markup view when it's the default view + if view_func is view_markup: + if (cfg.options.default_file_view == "markup" \ + and pathtype == vclib.FILE): + view_func = None + + # set the view parameter + view_code = _view_codes.get(view_func) + if view_code and not (params.has_key('view') and params['view'] is None): + params['view'] = view_code + + # add sticky values to parameter list + if sticky_vars: + for name in _sticky_vars: + value = self.query_dict.get(name) + if value is not None and not params.has_key(name): + params[name] = value + + # remove null values from parameter list + for name, value in params.items(): + if value is None: + del params[name] + + return url, params + +def _path_parts(path): + """Split up a repository path into a list of path components""" + # clean it up. this removes duplicate '/' characters and any that may + # exist at the front or end of the path. + return filter(None, string.split(path, '/')) + +def _normalize_path(path): + """Collapse leading slashes in the script name + + You only get multiple slashes in the script name when users accidentally + type urls like http://abc.com//viewvc.cgi/, but we correct for it + because we output the script name in links and web browsers + interpret //viewvc.cgi/ as http://viewvc.cgi/ + """ + + i = 0 + for c in path: + if c != '/': + break + i = i + 1 + + if i: + return path[i-1:] + + return path + +def _validate_param(name, value): + """Validate whether the given value is acceptable for the param name. + + If the value is not allowed, then an error response is generated, and + this function throws an exception. Otherwise, it simply returns None. + """ + + try: + validator = _legal_params[name] + except KeyError: + raise debug.ViewVCException( + 'An illegal parameter name ("%s") was passed.' % name, + '400 Bad Request') + + if validator is None: + return + + # is the validator a regex? + if hasattr(validator, 'match'): + if not validator.match(value): + raise debug.ViewVCException( + 'An illegal value ("%s") was passed as a parameter.' % + value, '400 Bad Request') + return + + # the validator must be a function + validator(value) + +def _validate_regex(value): + # hmm. there isn't anything that we can do here. + + ### we need to watch the flow of these parameters through the system + ### to ensure they don't hit the page unescaped. otherwise, these + ### parameters could constitute a CSS attack. + pass + +# obvious things here. note that we don't need uppercase for alpha. +_re_validate_alpha = re.compile('^[a-z]+$') +_re_validate_number = re.compile('^[0-9]+$') + +# when comparing two revs, we sometimes construct REV:SYMBOL, so ':' is needed +_re_validate_revnum = re.compile('^[-_.a-zA-Z0-9:~\\[\\]/]*$') + +# it appears that RFC 2045 also says these chars are legal: !#$%&'*+^{|}~` +# but woah... I'll just leave them out for now +_re_validate_mimetype = re.compile('^[-_.a-zA-Z0-9/]+$') + +# date time values +_re_validate_datetime = re.compile(r'^(\d\d\d\d-\d\d-\d\d(\s+\d\d:\d\d' + '(:\d\d)?)?)?$') + +# the legal query parameters and their validation functions +_legal_params = { + 'root' : None, + 'view' : None, + 'search' : _validate_regex, + 'p1' : None, + 'p2' : None, + + 'hideattic' : _re_validate_number, + 'limit_changes' : _re_validate_number, + 'sortby' : _re_validate_alpha, + 'sortdir' : _re_validate_alpha, + 'logsort' : _re_validate_alpha, + 'diff_format' : _re_validate_alpha, + 'pathrev' : _re_validate_revnum, + 'dir_pagestart' : _re_validate_number, + 'log_pagestart' : _re_validate_number, + 'annotate' : _re_validate_revnum, + 'graph' : _re_validate_revnum, + 'makeimage' : _re_validate_number, + 'r1' : _re_validate_revnum, + 'tr1' : _re_validate_revnum, + 'r2' : _re_validate_revnum, + 'tr2' : _re_validate_revnum, + 'revision' : _re_validate_revnum, + 'content-type' : _re_validate_mimetype, + + # for query + 'branch' : _validate_regex, + 'branch_match' : _re_validate_alpha, + 'dir' : None, + 'file' : _validate_regex, + 'file_match' : _re_validate_alpha, + 'who' : _validate_regex, + 'who_match' : _re_validate_alpha, + 'comment' : _validate_regex, + 'comment_match' : _re_validate_alpha, + 'querysort' : _re_validate_alpha, + 'date' : _re_validate_alpha, + 'hours' : _re_validate_number, + 'mindate' : _re_validate_datetime, + 'maxdate' : _re_validate_datetime, + 'format' : _re_validate_alpha, + 'limit' : _re_validate_number, + + # for redirect_pathrev + 'orig_path' : None, + 'orig_pathtype' : None, + 'orig_pathrev' : None, + 'orig_view' : None, + + # deprecated + 'parent' : _re_validate_number, + 'rev' : _re_validate_revnum, + 'tarball' : _re_validate_number, + 'hidecvsroot' : _re_validate_number, + } + +def _path_join(path_parts): + return string.join(path_parts, '/') + +def _strip_suffix(suffix, path_parts, rev, pathtype, repos, view_func): + """strip the suffix from a repository path if the resulting path + is of the specified type, otherwise return None""" + if not path_parts: + return None + l = len(suffix) + if path_parts[-1][-l:] == suffix: + path_parts = path_parts[:] + if len(path_parts[-1]) == l: + del path_parts[-1] + else: + path_parts[-1] = path_parts[-1][:-l] + t = _repos_pathtype(repos, path_parts, rev) + if pathtype == t: + return path_parts, t, view_func + return None + +def _repos_pathtype(repos, path_parts, rev): + """Return the type of a repository path, or None if the path doesn't + exist""" + try: + return repos.itemtype(path_parts, rev) + except vclib.ItemNotFound: + return None + +def _orig_path(request, rev_param='revision', path_param=None): + "Get original path of requested file at old revision before copies or moves" + + # The 'pathrev' variable is interpreted by nearly all ViewVC views to + # provide a browsable snapshot of a repository at some point in its history. + # 'pathrev' is a tag name for CVS repositories and a revision number for + # Subversion repositories. It's automatically propagated between pages by + # logic in the Request.get_link() function which adds it to links like a + # sticky variable. When 'pathrev' is set, directory listings only include + # entries that exist in the specified revision or tag. Similarly, log pages + # will only show revisions preceding the point in history specified by + # 'pathrev.' Markup, checkout, and annotate pages show the 'pathrev' + # revision of files by default when no other revision is specified. + # + # In Subversion repositories, paths are always considered to refer to the + # pathrev revision. For example, if there is a "circle.jpg" in revision 3, + # which is renamed and modified as "square.jpg" in revision 4, the original + # circle image is visible at the following URLs: + # + # *checkout*/circle.jpg?pathrev=3 + # *checkout*/square.jpg?revision=3 + # *checkout*/square.jpg?revision=3&pathrev=4 + # + # Note that the following: + # + # *checkout*/circle.jpg?rev=3 + # + # now gets redirected to one of the following URLs: + # + # *checkout*/circle.jpg?pathrev=3 (for Subversion) + # *checkout*/circle.jpg?revision=3 (for CVS) + # + rev = request.query_dict.get(rev_param, request.pathrev) + path = request.query_dict.get(path_param, request.where) + + if rev is not None and hasattr(request.repos, '_getrev'): + try: + pathrev = request.repos._getrev(request.pathrev) + rev = request.repos._getrev(rev) + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid revision', '404 Not Found') + return _path_parts(request.repos.get_location(path, pathrev, rev)), rev + return _path_parts(path), rev + +def setup_authorizer(cfg, username, rootname): + import imp + + # No configured authorizer? No problem. + if not cfg.options.authorizer: + return None + + # First, try to load a module with the configured name. + fp = None + try: + try: + fp, path, desc = imp.find_module("%s" % (cfg.options.authorizer), + vcauth.__path__) + my_auth = imp.load_module('viewvc', fp, path, desc) + except ImportError: + raise debug.ViewVCException( + 'Invalid authorizer (%s) specified for root "%s"' \ + % (cfg.options.authorizer, rootname), + '500 Internal Server Error') + finally: + if fp: + fp.close() + + # Now we'll get custom parameters for our particular root. + params = cfg.get_authorizer_params(cfg.options.authorizer, rootname) + + # Finally, instantiate our Authorizer. + return my_auth.ViewVCAuthorizer(username, params) + +def check_freshness(request, mtime=None, etag=None, weak=0): + cfg = request.cfg + + # See if we are supposed to disable etags (for debugging, usually) + if not cfg.options.generate_etags: + return 0 + + request_etag = request_mtime = None + if etag is not None: + if weak: + etag = 'W/"%s"' % etag + else: + etag = '"%s"' % etag + request_etag = request.server.getenv('HTTP_IF_NONE_MATCH') + if mtime is not None: + try: + request_mtime = request.server.getenv('HTTP_IF_MODIFIED_SINCE') + request_mtime = rfc822.mktime_tz(rfc822.parsedate_tz(request_mtime)) + except: + request_mtime = None + + # if we have an etag, use that for freshness checking. + # if not available, then we use the last-modified time. + # if not available, then the document isn't fresh. + if etag is not None: + isfresh = (request_etag == etag) + elif mtime is not None: + isfresh = (request_mtime >= mtime) + else: + isfresh = 0 + + # require revalidation after the configured amount of time + if cfg and cfg.options.http_expiration_time >= 0: + expiration = compat.formatdate(time.time() + + cfg.options.http_expiration_time) + request.server.addheader('Expires', expiration) + request.server.addheader('Cache-Control', + 'max-age=%d' % cfg.options.http_expiration_time) + + if isfresh: + request.server.header(status='304 Not Modified') + else: + if etag is not None: + request.server.addheader('ETag', etag) + if mtime is not None: + request.server.addheader('Last-Modified', compat.formatdate(mtime)) + return isfresh + +def get_view_template(cfg, view_name, language="en"): + # See if the configuration specifies a template for this view. If + # not, use the default template path for this view. + tname = vars(cfg.templates).get(view_name) or view_name + ".ezt" + + # Template paths are relative to the configurated template_dir (if + # any, "templates" otherwise), so build the template path as such. + tname = os.path.join(cfg.options.template_dir or "templates", tname) + + # Allow per-language template selection. + tname = string.replace(tname, '%lang%', language) + + # Finally, construct the whole template path. + tname = cfg.path(tname) + + debug.t_start('ezt-parse') + template = ezt.Template(tname) + debug.t_end('ezt-parse') + + return template + +def get_writeready_server_file(request, content_type=None): + """Return a file handle to a response body stream, after outputting + any queued special headers (on REQUEST.server) and (optionally) a + 'Content-Type' header whose value is CONTENT_TYPE. After this is + called, it is too late to add new headers to the response.""" + if request.gzip_compress_level: + request.server.addheader('Content-Encoding', 'gzip') + if content_type: + request.server.header(content_type) + else: + request.server.header() + if request.gzip_compress_level: + fp = gzip.GzipFile('', 'wb', request.gzip_compress_level, + request.server.file()) + else: + fp = request.server.file() + return fp + +def generate_page(request, view_name, data, content_type=None): + server_fp = get_writeready_server_file(request) + template = get_view_template(request.cfg, view_name, request.language) + template.generate(server_fp, data) + +def nav_path(request): + """Return current path as list of items with "name" and "href" members + + The href members are view_directory links for directories and view_log + links for files, but are set to None when the link would point to + the current view""" + + if not request.repos: + return [] + + is_dir = request.pathtype == vclib.DIR + + # add root item + items = [] + root_item = _item(name=request.server.escape(request.repos.name), href=None) + if request.path_parts or request.view_func is not view_directory: + root_item.href = request.get_url(view_func=view_directory, + where='', pathtype=vclib.DIR, + params={}, escape=1) + items.append(root_item) + + # add path part items + path_parts = [] + for part in request.path_parts: + path_parts.append(part) + is_last = len(path_parts) == len(request.path_parts) + + item = _item(name=part, href=None) + + if not is_last or (is_dir and request.view_func is not view_directory): + item.href = request.get_url(view_func=view_directory, + where=_path_join(path_parts), + pathtype=vclib.DIR, + params={}, escape=1) + elif not is_dir and request.view_func is not view_log: + item.href = request.get_url(view_func=view_log, + where=_path_join(path_parts), + pathtype=vclib.FILE, + params={}, escape=1) + items.append(item) + + return items + +def prep_tags(request, tags): + url, params = request.get_link(params={'pathrev': None}) + params = compat.urlencode(params) + if params: + url = urllib.quote(url, _URL_SAFE_CHARS) + '?' + params + '&pathrev=' + else: + url = urllib.quote(url, _URL_SAFE_CHARS) + '?pathrev=' + url = request.server.escape(url) + + links = [ ] + for tag in tags: + links.append(_item(name=tag.name, href=url+tag.name)) + links.sort(lambda a, b: cmp(a.name, b.name)) + return links + +def guess_mime(filename): + return mimetypes.guess_type(filename)[0] + +def is_viewable_image(mime_type): + return mime_type and mime_type in ('image/gif', 'image/jpeg', 'image/png') + +def is_text(mime_type): + return not mime_type or mime_type[:5] == 'text/' + +def is_cvsroot_path(roottype, path_parts): + return roottype == 'cvs' and path_parts and path_parts[0] == 'CVSROOT' + +def is_plain_text(mime_type): + return not mime_type or mime_type == 'text/plain' + +def default_view(mime_type, cfg): + "Determine whether file should be viewed through markup page or sent raw" + # If the mime type is text/anything or a supported image format we view + # through the markup page. If the mime type is something else, we send + # it directly to the browser. That way users can see things like flash + # animations, pdfs, word documents, multimedia, etc, which wouldn't be + # very useful marked up. If the mime type is totally unknown (happens when + # we encounter an unrecognized file extension) we also view it through + # the markup page since that's better than sending it text/plain. + if ('markup' in cfg.options.allowed_views and + (is_viewable_image(mime_type) or is_text(mime_type))): + return view_markup + return view_checkout + +def get_file_view_info(request, where, rev=None, mime_type=None, pathrev=-1): + """Return common hrefs and a viewability flag used for various views + of FILENAME at revision REV whose MIME type is MIME_TYPE.""" + rev = rev and str(rev) or None + mime_type = mime_type or guess_mime(where) + if pathrev == -1: # cheesy default value, since we need to preserve None + pathrev = request.pathrev + + view_href = None + download_href = None + download_text_href = None + annotate_href = None + revision_href = None + + if 'markup' in request.cfg.options.allowed_views: + view_href = request.get_url(view_func=view_markup, + where=where, + pathtype=vclib.FILE, + params={'revision': rev, + 'pathrev': pathrev}, + escape=1) + if 'co' in request.cfg.options.allowed_views: + download_href = request.get_url(view_func=view_checkout, + where=where, + pathtype=vclib.FILE, + params={'revision': rev, + 'pathrev': pathrev}, + escape=1) + if not is_plain_text(mime_type): + download_text_href = request.get_url(view_func=view_checkout, + where=where, + pathtype=vclib.FILE, + params={'content-type': 'text/plain', + 'revision': rev, + 'pathrev': pathrev}, + escape=1) + if 'annotate' in request.cfg.options.allowed_views: + annotate_href = request.get_url(view_func=view_annotate, + where=where, + pathtype=vclib.FILE, + params={'annotate': rev, + 'pathrev': pathrev}, + escape=1) + if request.roottype == 'svn': + revision_href = request.get_url(view_func=view_revision, + params={'revision': rev}, + escape=1) + + prefer_markup = default_view(mime_type, request.cfg) == view_markup + + return view_href, download_href, download_text_href, \ + annotate_href, revision_href, ezt.boolean(prefer_markup) + + +# Regular expressions for location text that looks like URLs and email +# addresses. Note that the regexps assume the text is already HTML-encoded. +_re_rewrite_url = re.compile('((http|https|ftp|file|svn|svn\+ssh)' + '(://[-a-zA-Z0-9%.~:_/]+)((\?|\&)' + '([-a-zA-Z0-9%.~:_]+)=([-a-zA-Z0-9%.~:_])+)*' + '(#([-a-zA-Z0-9%.~:_]+)?)?)') +_re_rewrite_email = re.compile('([-a-zA-Z0-9_.\+]+)@' + '(([-a-zA-Z0-9]+\.)+[A-Za-z]{2,4})') + +def mangle_email_addresses(text, style=0): + # style=2: truncation mangling + if style == 2: + return re.sub(_re_rewrite_email, r'\1@…', text) + + # style=1: entity-encoding and at-wrapping + if style == 1: + def _match_replace(matchobj): + return string.join(map(lambda x: '&#%d;' % (ord(x)), + matchobj.group(1)), '') \ + + ' {at} ' + \ + string.join(map(lambda x: '&#%d;' % (ord(x)), + matchobj.group(2)), '') + return re.sub(_re_rewrite_email, _match_replace, text) + + # otherwise, no mangling + return text + +def htmlify(html, mangle_email_addrs=0): + if not html: + return html + html = cgi.escape(html) + html = re.sub(_re_rewrite_url, r'\1', html) + html = mangle_email_addresses(html, mangle_email_addrs) + return html + +def format_log(log, cfg, htmlize=1): + if not log: + return log + if htmlize: + s = htmlify(log[:cfg.options.short_log_len], + cfg.options.mangle_email_addresses) + else: + s = cgi.escape(log[:cfg.options.short_log_len]) + if cfg.options.mangle_email_addresses == 2: + s = re.sub(_re_rewrite_email, r'\1@...', s) + if len(log) > cfg.options.short_log_len: + s = s + '...' + return s + +_time_desc = { + 1 : 'second', + 60 : 'minute', + 3600 : 'hour', + 86400 : 'day', + 604800 : 'week', + 2628000 : 'month', + 31536000 : 'year', + } + +def get_time_text(request, interval, num): + "Get some time text, possibly internationalized." + ### some languages have even harder pluralization rules. we'll have to + ### deal with those on demand + if num == 0: + return '' + text = _time_desc[interval] + if num == 1: + attr = text + '_singular' + fmt = '%d ' + text + else: + attr = text + '_plural' + fmt = '%d ' + text + 's' + try: + fmt = getattr(request.kv.i18n.time, attr) + except AttributeError: + pass + return fmt % num + +def little_time(request): + try: + return request.kv.i18n.time.little_time + except AttributeError: + return 'very little time' + +def html_time(request, secs, extended=0): + secs = long(time.time()) - secs + if secs < 2: + return little_time(request) + breaks = _time_desc.keys() + breaks.sort() + i = 0 + while i < len(breaks): + if secs < 2 * breaks[i]: + break + i = i + 1 + value = breaks[i - 1] + s = get_time_text(request, value, secs / value) + + if extended and i > 1: + secs = secs % value + value = breaks[i - 2] + ext = get_time_text(request, value, secs / value) + if ext: + ### this is not i18n compatible. pass on it for now + s = s + ', ' + ext + return s + +def common_template_data(request, revision=None, mime_type=None): + cfg = request.cfg + data = { + 'cfg' : cfg, + 'vsn' : __version__, + 'kv' : request.kv, + 'docroot' : cfg.options.docroot is None \ + and request.script_name + '/' + docroot_magic_path \ + or cfg.options.docroot, + 'username' : request.username, + 'where' : request.server.escape(request.where), + 'roottype' : request.roottype, + 'rootname' : request.rootname \ + and request.server.escape(request.rootname) or None, + 'rootpath' : request.rootpath, + 'pathtype' : None, + 'nav_path' : nav_path(request), + 'view' : _view_codes[request.view_func], + 'rev' : None, + 'lockinfo' : None, + 'view_href' : None, + 'annotate_href' : None, + 'download_href' : None, + 'download_text_href' : None, + 'revision_href' : None, + 'queryform_href' : None, + 'tarball_href' : None, + 'up_href' : None, + 'log_href' : None, + 'log_href_rev': None, + 'graph_href': None, + 'rss_href' : None, + 'roots_href' : request.get_url(view_func=view_roots, escape=1, params={}), + 'prefer_markup' : ezt.boolean(0), + } + + rev = revision + if not rev: + rev = request.query_dict.get('annotate') + if not rev: + rev = request.query_dict.get('revision') + if not rev and request.roottype == 'svn': + rev = request.query_dict.get('pathrev') + try: + data['rev'] = hasattr(request.repos, '_getrev') \ + and request.repos._getrev(rev) or rev + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid revision', '404 Not Found') + + if request.pathtype == vclib.DIR: + data['pathtype'] = 'dir' + elif request.pathtype == vclib.FILE: + data['pathtype'] = 'file' + + if request.path_parts: + dir = _path_join(request.path_parts[:-1]) + data['up_href'] = request.get_url(view_func=view_directory, + where=dir, pathtype=vclib.DIR, + params={}, escape=1) + + if request.pathtype == vclib.FILE: + data['view_href'], data['download_href'], data['download_text_href'], \ + data['annotate_href'], data['revision_href'], data['prefer_markup'] \ + = get_file_view_info(request, request.where, data['rev'], mime_type) + data['log_href'] = request.get_url(view_func=view_log, + params={}, escape=1) + if request.roottype == 'cvs' and cfg.options.use_cvsgraph: + data['graph_href'] = request.get_url(view_func=view_cvsgraph, + params={}, escape=1) + file_data = request.repos.listdir(request.path_parts[:-1], + request.pathrev, {}) + def _only_this_file(item): + return item.name == request.path_parts[-1] + entries = filter(_only_this_file, file_data) + if len(entries) == 1: + request.repos.dirlogs(request.path_parts[:-1], request.pathrev, + entries, {}) + data['lockinfo'] = entries[0].lockinfo + elif request.pathtype == vclib.DIR: + data['view_href'] = request.get_url(view_func=view_directory, + params={}, escape=1) + if 'tar' in cfg.options.allowed_views: + data['tarball_href'] = request.get_url(view_func=download_tarball, + params={}, + escape=1) + if request.roottype == 'svn': + data['revision_href'] = request.get_url(view_func=view_revision, + params={'revision': data['rev']}, + escape=1) + + data['log_href'] = request.get_url(view_func=view_log, + params={}, escape=1) + + if is_querydb_nonempty_for_root(request): + if request.pathtype == vclib.DIR: + params = {} + if request.roottype == 'cvs' and request.pathrev: + params['branch'] = request.pathrev + data['queryform_href'] = request.get_url(view_func=view_queryform, + params=params, + escape=1) + data['rss_href'] = request.get_url(view_func=view_query, + params={'date': 'month', + 'format': 'rss'}, + escape=1) + elif request.pathtype == vclib.FILE: + parts = _path_parts(request.where) + where = _path_join(parts[:-1]) + data['rss_href'] = request.get_url(view_func=view_query, + where=where, + pathtype=request.pathtype, + params={'date': 'month', + 'format': 'rss', + 'file': parts[-1], + 'file_match': 'exact'}, + escape=1) + return data + +def retry_read(src, reqlen=CHUNK_SIZE): + while 1: + chunk = src.read(CHUNK_SIZE) + if not chunk: + # need to check for eof methods because the cStringIO file objects + # returned by ccvs don't provide them + if hasattr(src, 'eof') and src.eof() is None: + time.sleep(1) + continue + return chunk + +def copy_stream(src, dst, cfg, htmlize=0): + while 1: + chunk = retry_read(src) + if not chunk: + break + if htmlize: + chunk = htmlify(chunk, mangle_email_addrs=0) + dst.write(chunk) + +class MarkupPipeWrapper: + """An EZT callback that outputs a filepointer, plus some optional + pre- and post- text.""" + + def __init__(self, cfg, fp, pretext=None, posttext=None, htmlize=1): + self.fp = fp + self.cfg = cfg + self.pretext = pretext + self.posttext = posttext + self.htmlize = htmlize + + def __call__(self, ctx): + if self.pretext: + ctx.fp.write(self.pretext) + copy_stream(self.fp, ctx.fp, self.cfg, self.htmlize) + self.fp.close() + if self.posttext: + ctx.fp.write(self.posttext) + +def markup_stream_pygments(request, cfg, blame_data, fp, filename, mime_type): + # Determine if we should use Pygments to highlight our output. + # Reasons not to include a) being told not to by the configuration, + # b) not being able to import the Pygments modules, and c) Pygments + # not having a lexer for our file's format. + blame_source = [] + if blame_data: + for i in blame_data: + i.text = cgi.escape(i.text) + i.diff_href = None + if i.prev_rev: + i.diff_href = request.get_url(view_func=view_diff, + params={'r1': i.prev_rev, + 'r2': i.rev}, + escape=1, partial=1) + blame_source.append(i) + blame_data = blame_source + lexer = None + use_pygments = cfg.options.enable_syntax_coloration + try: + from pygments import highlight + from pygments.formatters import HtmlFormatter + from pygments.lexers import ClassNotFound, \ + get_lexer_by_name, \ + get_lexer_for_mimetype, \ + get_lexer_for_filename + try: + lexer = get_lexer_for_mimetype(mime_type) + except ClassNotFound: + try: + lexer = get_lexer_for_filename(filename) + except ClassNotFound: + use_pygments = 0 + except ImportError: + use_pygments = 0 + + # If we aren't going to be highlighting anything, just return the + # BLAME_SOURCE. If there's no blame_source, we'll generate a fake + # one from the file contents we fetch with PATH and REV. + if not use_pygments: + if blame_source: + return blame_source + else: + lines = [] + line_no = 0 + while 1: + line = fp.readline() + if not line: + break + line_no = line_no + 1 + item = vclib.Annotation(cgi.escape(line), line_no, + None, None, None, None) + item.diff_href = None + lines.append(item) + return lines + + # If we get here, we're highlighting something. + class PygmentsSink: + def __init__(self, blame_data): + if blame_data: + self.has_blame_data = 1 + self.blame_data = blame_data + else: + self.has_blame_data = 0 + self.blame_data = [] + self.line_no = 0 + def write(self, buf): + ### FIXME: Don't bank on write() being called once per line + if self.has_blame_data: + self.blame_data[self.line_no].text = buf + else: + item = vclib.Annotation(buf, self.line_no + 1, + None, None, None, None) + item.diff_href = None + self.blame_data.append(item) + self.line_no = self.line_no + 1 + ps = PygmentsSink(blame_source) + highlight(fp.read(), lexer, + HtmlFormatter(nowrap=True, + classprefix="pygments-", + encoding='utf-8'), ps) + return ps.blame_data + +def make_time_string(date, cfg): + """Returns formatted date string in either local time or UTC. + + The passed in 'date' variable is seconds since epoch. + + """ + if date is None: + return None + if cfg.options.use_localtime: + localtime = time.localtime(date) + return time.asctime(localtime) + ' ' + time.tzname[localtime[8]] + else: + return time.asctime(time.gmtime(date)) + ' UTC' + +def make_rss_time_string(date, cfg): + """Returns formatted date string in UTC, formatted for RSS. + + The passed in 'date' variable is seconds since epoch. + + """ + if date is None: + return None + return time.strftime("%a, %d %b %Y %H:%M:%S", time.gmtime(date)) + ' UTC' + +def get_itemprops(request, path_parts, rev): + itemprops = request.repos.itemprops(path_parts, rev) + propnames = itemprops.keys() + propnames.sort() + props = [] + has_binary_props = 0 + for name in propnames: + value = itemprops[name] + undisplayable = ezt.boolean(0) + # skip non-utf8 property names + try: + unicode(name, 'utf8') + except: + continue + # note non-utf8 property values + try: + unicode(value, 'utf8') + except: + value = None + undisplayable = ezt.boolean(1) + props.append(_item(name=name, value=value, undisplayable=undisplayable)) + return props + +def calculate_mime_type(request, path_parts, rev): + mime_type = None + if not path_parts: + return None + if request.roottype == 'svn': + try: + itemprops = request.repos.itemprops(path_parts, rev) + mime_type = itemprops.get('svn:mime-type') + if mime_type: + return mime_type + except: + pass + return guess_mime(path_parts[-1]) + +def markup_or_annotate(request, is_annotate): + cfg = request.cfg + path, rev = _orig_path(request, is_annotate and 'annotate' or 'revision') + lines = fp = image_src_href = None + annotation = None + revision = None + mime_type = calculate_mime_type(request, path, rev) + + # Is this a viewable image type? + if is_viewable_image(mime_type) \ + and 'co' in cfg.options.allowed_views: + fp, revision = request.repos.openfile(path, rev) + fp.close() + if check_freshness(request, None, revision, weak=1): + return + annotation = 'binary' + image_src_href = request.get_url(view_func=view_checkout, + params={'revision': rev}, escape=1) + + # Not a viewable image. + else: + blame_source = None + if is_annotate: + # Try to annotate this file, but don't croak if we fail. + try: + blame_source, revision = request.repos.annotate(path, rev) + annotation = 'annotated' + if check_freshness(request, None, revision, weak=1): + return + except vclib.NonTextualFileContents: + annotation = 'binary' + except: + annotation = 'error' + + fp, revision = request.repos.openfile(path, rev) + if check_freshness(request, None, revision, weak=1): + fp.close() + return + lines = markup_stream_pygments(request, cfg, blame_source, fp, + path[-1], mime_type) + fp.close() + + data = common_template_data(request, revision) + data.update({ + 'mime_type' : mime_type, + 'log' : None, + 'date' : None, + 'ago' : None, + 'author' : None, + 'branches' : None, + 'tags' : None, + 'branch_points' : None, + 'changed' : None, + 'size' : None, + 'state' : None, + 'vendor_branch' : None, + 'prev' : None, + 'orig_path' : None, + 'orig_href' : None, + 'image_src_href' : image_src_href, + 'lines' : lines, + 'properties' : get_itemprops(request, path, rev), + 'annotation' : annotation, + }) + + if cfg.options.show_log_in_markup: + options = {'svn_latest_log': 1} ### FIXME: No longer needed? + revs = request.repos.itemlog(path, revision, vclib.SORTBY_DEFAULT, + 0, 1, options) + entry = revs[-1] + data.update({ + 'date' : make_time_string(entry.date, cfg), + 'author' : entry.author, + 'changed' : entry.changed, + 'log' : htmlify(entry.log, cfg.options.mangle_email_addresses), + 'size' : entry.size, + }) + + if entry.date is not None: + data['ago'] = html_time(request, entry.date, 1) + + if request.roottype == 'cvs': + branch = entry.branch_number + prev = entry.prev or entry.parent + data.update({ + 'state' : entry.dead and 'dead', + 'prev' : prev and prev.string, + 'vendor_branch' : ezt.boolean(branch and branch[2] % 2 == 1), + 'branches' : string.join(map(lambda x: x.name, entry.branches), ', '), + 'tags' : string.join(map(lambda x: x.name, entry.tags), ', '), + 'branch_points': string.join(map(lambda x: x.name, + entry.branch_points), ', ') + }) + + if path != request.path_parts: + orig_path = _path_join(path) + data['orig_path'] = orig_path + data['orig_href'] = request.get_url(view_func=view_log, + where=orig_path, + pathtype=vclib.FILE, + params={'pathrev': revision}, + escape=1) + + generate_page(request, "file", data) + +def view_markup(request): + if 'markup' not in request.cfg.options.allowed_views: + raise debug.ViewVCException('Markup view is disabled', + '403 Forbidden') + markup_or_annotate(request, 0) + +def view_annotate(request): + if 'annotate' not in request.cfg.options.allowed_views: + raise debug.ViewVCException('Annotation view is disabled', + '403 Forbidden') + markup_or_annotate(request, 1) + +def revcmp(rev1, rev2): + rev1 = map(int, string.split(rev1, '.')) + rev2 = map(int, string.split(rev2, '.')) + return cmp(rev1, rev2) + +def sort_file_data(file_data, roottype, sortdir, sortby, group_dirs): + # convert sortdir into a sign bit + s = sortdir == "down" and -1 or 1 + + # in cvs, revision numbers can't be compared meaningfully between + # files, so try to do the right thing and compare dates instead + if roottype == "cvs" and sortby == "rev": + sortby = "date" + + def file_sort_cmp(file1, file2, sortby=sortby, group_dirs=group_dirs, s=s): + # if we're grouping directories together, sorting is pretty + # simple. a directory sorts "higher" than a non-directory, and + # two directories are sorted as normal. + if group_dirs: + if file1.kind == vclib.DIR: + if file2.kind == vclib.DIR: + # two directories, no special handling. + pass + else: + # file1 is a directory, it sorts first. + return -1 + elif file2.kind == vclib.DIR: + # file2 is a directory, it sorts first. + return 1 + + # we should have data on these. if not, then it is because we requested + # a specific tag and that tag is not present on the file. + if file1.rev is not None and file2.rev is not None: + # sort according to sortby + if sortby == 'rev': + return s * revcmp(file1.rev, file2.rev) + elif sortby == 'date': + return s * cmp(file2.date, file1.date) # latest date is first + elif sortby == 'log': + return s * cmp(file1.log, file2.log) + elif sortby == 'author': + return s * cmp(file1.author, file2.author) + elif file1.rev is not None: + return -1 + elif file2.rev is not None: + return 1 + + # sort by file name + return s * cmp(file1.name, file2.name) + + file_data.sort(file_sort_cmp) + +def icmp(x, y): + """case insensitive comparison""" + return cmp(string.lower(x), string.lower(y)) + +def view_roots(request): + if 'roots' not in request.cfg.options.allowed_views: + raise debug.ViewVCException('Root listing view is disabled', + '403 Forbidden') + + # add in the roots for the selection + roots = [] + expand_root_parents(request.cfg) + allroots = list_roots(request) + if len(allroots): + rootnames = allroots.keys() + rootnames.sort(icmp) + for rootname in rootnames: + href = request.get_url(view_func=view_directory, + where='', pathtype=vclib.DIR, + params={'root': rootname}, escape=1) + roots.append(_item(name=request.server.escape(rootname), + type=allroots[rootname][1], + path=allroots[rootname][0], + href=href)) + + data = common_template_data(request) + data['roots'] = roots + generate_page(request, "roots", data) + +def view_directory(request): + # For Subversion repositories, the revision acts as a weak validator for + # the directory listing (to take into account template changes or + # revision property changes). + if request.roottype == 'svn': + try: + rev = request.repos._getrev(request.pathrev) + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid revision', '404 Not Found') + tree_rev = request.repos.created_rev(request.where, rev) + if check_freshness(request, None, str(tree_rev), weak=1): + return + + # List current directory + cfg = request.cfg + options = {} + if request.roottype == 'cvs': + hideattic = int(request.query_dict.get('hideattic', + cfg.options.hide_attic)) + options["cvs_subdirs"] = (cfg.options.show_subdir_lastmod and + cfg.options.show_logs) + file_data = request.repos.listdir(request.path_parts, request.pathrev, + options) + + # sort with directories first, and using the "sortby" criteria + sortby = request.query_dict.get('sortby', cfg.options.sort_by) or 'file' + sortdir = request.query_dict.get('sortdir', 'up') + + # when paging and sorting by filename, we can greatly improve + # performance by "cheating" -- first, we sort (we already have the + # names), then we just fetch dirlogs for the needed entries. + # however, when sorting by other properties or not paging, we've no + # choice but to fetch dirlogs for everything. + debug.t_start("dirlogs") + if cfg.options.use_pagesize and sortby == 'file': + dirlogs_first = int(request.query_dict.get('dir_pagestart', 0)) + if dirlogs_first > len(file_data): + dirlogs_first = 0 + dirlogs_last = dirlogs_first + cfg.options.use_pagesize + for file in file_data: + file.rev = None + file.date = None + file.log = None + file.author = None + file.size = None + file.lockinfo = None + file.dead = None + sort_file_data(file_data, request.roottype, sortdir, sortby, + cfg.options.sort_group_dirs) + # request dirlogs only for the slice of files in "this page" + request.repos.dirlogs(request.path_parts, request.pathrev, + file_data[dirlogs_first:dirlogs_last], options) + else: + request.repos.dirlogs(request.path_parts, request.pathrev, + file_data, options) + sort_file_data(file_data, request.roottype, sortdir, sortby, + cfg.options.sort_group_dirs) + debug.t_end("dirlogs") + + # If a regex is specified, build a compiled form thereof for filtering + searchstr = None + search_re = request.query_dict.get('search', '') + if cfg.options.use_re_search and search_re: + searchstr = re.compile(search_re) + + # loop through entries creating rows and changing these values + rows = [ ] + num_displayed = 0 + num_dead = 0 + + # set some values to be used inside loop + where = request.where + where_prefix = where and where + '/' + + for file in file_data: + row = _item(author=None, log=None, short_log=None, state=None, size=None, + log_file=None, log_rev=None, graph_href=None, mime_type=None, + date=None, ago=None, view_href=None, log_href=None, + revision_href=None, annotate_href=None, download_href=None, + download_text_href=None, prefer_markup=ezt.boolean(0)) + if request.roottype == 'cvs' and file.absent: + continue + if cfg.options.hide_errorful_entries and file.errors: + continue + row.rev = file.rev + row.author = file.author + row.state = (request.roottype == 'cvs' and file.dead) and 'dead' or '' + if file.date is not None: + row.date = make_time_string(file.date, cfg) + row.ago = html_time(request, file.date) + if cfg.options.show_logs: + row.short_log = format_log(file.log, cfg) + row.log = htmlify(file.log, cfg.options.mangle_email_addresses) + row.lockinfo = file.lockinfo + row.anchor = request.server.escape(file.name) + row.name = request.server.escape(file.name) + row.pathtype = (file.kind == vclib.FILE and 'file') or \ + (file.kind == vclib.DIR and 'dir') + row.errors = file.errors + + if file.kind == vclib.DIR: + if cfg.options.hide_cvsroot \ + and is_cvsroot_path(request.roottype, + request.path_parts + [file.name]): + continue + + row.view_href = request.get_url(view_func=view_directory, + where=where_prefix+file.name, + pathtype=vclib.DIR, + params={}, + escape=1) + + if request.roottype == 'svn': + row.revision_href = request.get_url(view_func=view_revision, + params={'revision': file.rev}, + escape=1) + + if request.roottype == 'cvs' and file.rev is not None: + row.rev = None + if cfg.options.show_logs: + row.log_file = file.newest_file + row.log_rev = file.rev + + if request.roottype == 'svn': + row.log_href = request.get_url(view_func=view_log, + where=where_prefix + file.name, + pathtype=vclib.DIR, + params={}, + escape=1) + + elif file.kind == vclib.FILE: + if searchstr is not None: + if request.roottype == 'cvs' and (file.errors or file.dead): + continue + if not search_file(request.repos, request.path_parts + [file.name], + request.pathrev, searchstr): + continue + if request.roottype == 'cvs' and file.dead: + num_dead = num_dead + 1 + if hideattic: + continue + + num_displayed = num_displayed + 1 + + file_where = where_prefix + file.name + if request.roottype == 'svn': + row.size = file.size + + row.mime_type = calculate_mime_type(request, _path_parts(file_where), + file.rev) + row.view_href, row.download_href, row.download_text_href, \ + row.annotate_href, row.revision_href, \ + row.prefer_markup \ + = get_file_view_info(request, file_where, file.rev, row.mime_type) + row.log_href = request.get_url(view_func=view_log, + where=file_where, + pathtype=vclib.FILE, + params={}, + escape=1) + if cfg.options.use_cvsgraph and request.roottype == 'cvs': + row.graph_href = request.get_url(view_func=view_cvsgraph, + where=file_where, + pathtype=vclib.FILE, + params={}, + escape=1) + + rows.append(row) + + # prepare the data that will be passed to the template + data = common_template_data(request) + data.update({ + 'entries' : rows, + 'sortby' : sortby, + 'sortdir' : sortdir, + 'search_re' : search_re \ + and htmlify(search_re, cfg.options.mangle_email_addresses) \ + or None, + 'dir_pagestart' : None, + 'sortby_file_href' : request.get_url(params={'sortby': 'file', + 'sortdir': None}, + escape=1), + 'sortby_rev_href' : request.get_url(params={'sortby': 'rev', + 'sortdir': None}, + escape=1), + 'sortby_date_href' : request.get_url(params={'sortby': 'date', + 'sortdir': None}, + escape=1), + 'sortby_author_href' : request.get_url(params={'sortby': 'author', + 'sortdir': None}, + escape=1), + 'sortby_log_href' : request.get_url(params={'sortby': 'log', + 'sortdir': None}, + escape=1), + 'files_shown' : num_displayed, + 'num_dead' : num_dead, + 'youngest_rev' : None, + 'youngest_rev_href' : None, + 'selection_form' : None, + 'attic_showing' : None, + 'show_attic_href' : None, + 'hide_attic_href' : None, + 'branch_tags': None, + 'plain_tags': None, + 'properties': get_itemprops(request, request.path_parts, request.pathrev), + }) + + # clicking on sort column reverses sort order + if sortdir == 'down': + revsortdir = None # 'up' + else: + revsortdir = 'down' + if sortby in ['file', 'rev', 'date', 'log', 'author']: + data['sortby_%s_href' % sortby] = request.get_url(params={'sortdir': + revsortdir}, + escape=1) + + # set cvs-specific fields + if request.roottype == 'cvs': + plain_tags = options['cvs_tags'] + plain_tags.sort(icmp) + plain_tags.reverse() + + branch_tags = options['cvs_branches'] + branch_tags.sort(icmp) + branch_tags.reverse() + + data.update({ + 'attic_showing' : ezt.boolean(not hideattic), + 'show_attic_href' : request.get_url(params={'hideattic': 0}, escape=1), + 'hide_attic_href' : request.get_url(params={'hideattic': 1}, escape=1), + 'branch_tags': branch_tags, + 'plain_tags': plain_tags, + }) + + # set svn-specific fields + elif request.roottype == 'svn': + data['tree_rev'] = tree_rev + data['tree_rev_href'] = request.get_url(view_func=view_revision, + params={'revision': tree_rev}, + escape=1) + data['youngest_rev'] = request.repos.get_youngest_revision() + data['youngest_rev_href'] = request.get_url(view_func=view_revision, + params={}, + escape=1) + + if cfg.options.use_pagesize: + data['dir_paging_action'], data['dir_paging_hidden_values'] = \ + request.get_form(params={'dir_pagestart': None}) + + pathrev_form(request, data) + + ### one day, if EZT has "or" capability, we can lose this + data['search_re_form'] = ezt.boolean(cfg.options.use_re_search) + if data['search_re_form']: + data['search_re_action'], data['search_re_hidden_values'] = \ + request.get_form(params={'search': None}) + + if cfg.options.use_pagesize: + data['dir_pagestart'] = int(request.query_dict.get('dir_pagestart',0)) + data['entries'] = paging(data, 'entries', data['dir_pagestart'], 'name', + cfg.options.use_pagesize) + + generate_page(request, "directory", data) + +def paging(data, key, pagestart, local_name, pagesize): + # Implement paging + # Create the picklist + picklist = data['picklist'] = [] + for i in range(0, len(data[key]), pagesize): + pick = _item(start=None, end=None, count=None, more=ezt.boolean(0)) + pick.start = getattr(data[key][i], local_name) + pick.count = i + pick.page = (i / pagesize) + 1 + try: + pick.end = getattr(data[key][i+pagesize-1], local_name) + except IndexError: + pick.end = getattr(data[key][-1], local_name) + picklist.append(pick) + data['picklist_len'] = len(picklist) + # Need to fix + # pagestart can be greater than the length of data[key] if you + # select a tag or search while on a page other than the first. + # Should reset to the first page, this test won't do that every + # time that it is needed. + # Problem might go away if we don't hide non-matching files when + # selecting for tags or searching. + if pagestart > len(data[key]): + pagestart = 0 + pageend = pagestart + pagesize + # Slice + return data[key][pagestart:pageend] + +def paging_sws(data, key, pagestart, local_name, pagesize, offset): + """Implement sliding window-style paging.""" + # Create the picklist + last_requested = pagestart + (EXTRA_PAGES * pagesize) + picklist = data['picklist'] = [] + has_more = ezt.boolean(0) + for i in range(0, len(data[key]), pagesize): + pick = _item(start=None, end=None, count=None, more=ezt.boolean(0)) + pick.start = getattr(data[key][i], local_name) + pick.count = offset + i + pick.page = (pick.count / pagesize) + 1 + try: + pick.end = getattr(data[key][i+pagesize-1], local_name) + except IndexError: + pick.end = getattr(data[key][-1], local_name) + picklist.append(pick) + if pick.count >= last_requested: + pick.more = ezt.boolean(1) + break + data['picklist_len'] = len(picklist) + first = pagestart - offset + # FIXME: first can be greater than the length of data[key] if + # you select a tag or search while on a page other than the first. + # Should reset to the first page, but this test won't do that every + # time that it is needed. Problem might go away if we don't hide + # non-matching files when selecting for tags or searching. + if first > len(data[key]): + pagestart = 0 + pageend = first + pagesize + # Slice + return data[key][first:pageend] + +def pathrev_form(request, data): + lastrev = None + + if request.roottype == 'svn': + data['pathrev_action'], data['pathrev_hidden_values'] = \ + request.get_form(view_func=redirect_pathrev, + params={'pathrev': None, + 'orig_path': request.where, + 'orig_pathtype': request.pathtype, + 'orig_pathrev': request.pathrev, + 'orig_view': _view_codes.get(request.view_func)}) + + if request.pathrev: + youngest = request.repos.get_youngest_revision() + lastrev = request.repos.last_rev(request.where, request.pathrev, + youngest)[0] + + if lastrev == youngest: + lastrev = None + + data['pathrev'] = request.pathrev + data['lastrev'] = lastrev + + action, hidden_values = request.get_form(params={'pathrev': lastrev}) + if request.roottype != 'svn': + data['pathrev_action'] = action + data['pathrev_hidden_values'] = hidden_values + data['pathrev_clear_action'] = action + data['pathrev_clear_hidden_values'] = hidden_values + + return lastrev + +def redirect_pathrev(request): + assert request.roottype == 'svn' + new_pathrev = request.query_dict.get('pathrev') or None + path = request.query_dict.get('orig_path', '') + pathtype = request.query_dict.get('orig_pathtype') + pathrev = request.query_dict.get('orig_pathrev') + view = _views.get(request.query_dict.get('orig_view')) + + youngest = request.repos.get_youngest_revision() + + # go out of the way to allow revision numbers higher than youngest + try: + new_pathrev = int(new_pathrev) + except ValueError: + new_pathrev = youngest + except TypeError: + pass + else: + if new_pathrev > youngest: + new_pathrev = youngest + + if _repos_pathtype(request.repos, _path_parts(path), new_pathrev): + pathrev = new_pathrev + else: + pathrev, path = request.repos.last_rev(path, pathrev, new_pathrev) + # allow clearing sticky revision by submitting empty string + if new_pathrev is None and pathrev == youngest: + pathrev = None + + request.server.redirect(request.get_url(view_func=view, + where=path, + pathtype=pathtype, + params={'pathrev': pathrev})) + +def view_log(request): + cfg = request.cfg + diff_format = request.query_dict.get('diff_format', cfg.options.diff_format) + pathtype = request.pathtype + + if pathtype is vclib.DIR: + if request.roottype == 'cvs': + raise debug.ViewVCException('Unsupported feature: log view on CVS ' + 'directory', '400 Bad Request') + mime_type = None + else: + mime_type = calculate_mime_type(request, request.path_parts, request.pathrev) + + options = {} + options['svn_show_all_dir_logs'] = 1 ### someday make this optional? + options['svn_cross_copies'] = cfg.options.cross_copies + + logsort = request.query_dict.get('logsort', cfg.options.log_sort) + if request.roottype == "svn": + sortby = vclib.SORTBY_DEFAULT + logsort = None + else: + if logsort == 'date': + sortby = vclib.SORTBY_DATE + elif logsort == 'rev': + sortby = vclib.SORTBY_REV + else: + sortby = vclib.SORTBY_DEFAULT + + first = last = 0 + if cfg.options.use_pagesize: + log_pagestart = int(request.query_dict.get('log_pagestart', 0)) + first = log_pagestart - min(log_pagestart, + (EXTRA_PAGES * cfg.options.use_pagesize)) + last = log_pagestart + ((EXTRA_PAGES + 1) * cfg.options.use_pagesize) + 1 + show_revs = request.repos.itemlog(request.path_parts, request.pathrev, + sortby, first, last - first, options) + + # selected revision + selected_rev = request.query_dict.get('r1') + + entries = [ ] + name_printed = { } + cvs = request.roottype == 'cvs' + for rev in show_revs: + entry = _item() + entry.rev = rev.string + entry.state = (cvs and rev.dead and 'dead') + entry.author = rev.author + entry.changed = rev.changed + entry.date = make_time_string(rev.date, cfg) + entry.ago = None + if rev.date is not None: + entry.ago = html_time(request, rev.date, 1) + entry.log = htmlify(rev.log or "", cfg.options.mangle_email_addresses) + entry.size = rev.size + entry.lockinfo = rev.lockinfo + entry.branch_point = None + entry.next_main = None + entry.orig_path = None + entry.copy_path = None + + entry.view_href = None + entry.download_href = None + entry.download_text_href = None + entry.annotate_href = None + entry.revision_href = None + entry.sel_for_diff_href = None + entry.diff_to_sel_href = None + entry.diff_to_prev_href = None + entry.diff_to_branch_href = None + entry.diff_to_main_href = None + + if request.roottype == 'cvs': + prev = rev.prev or rev.parent + entry.prev = prev and prev.string + + branch = rev.branch_number + entry.vendor_branch = ezt.boolean(branch and branch[2] % 2 == 1) + + entry.branches = prep_tags(request, rev.branches) + entry.tags = prep_tags(request, rev.tags) + entry.branch_points = prep_tags(request, rev.branch_points) + + entry.tag_names = map(lambda x: x.name, rev.tags) + if branch and not name_printed.has_key(branch): + entry.branch_names = map(lambda x: x.name, rev.branches) + name_printed[branch] = 1 + else: + entry.branch_names = [ ] + + if rev.parent and rev.parent is not prev and not entry.vendor_branch: + entry.branch_point = rev.parent.string + + # if it's the last revision on a branch then diff against the + # last revision on the higher branch (e.g. change is committed and + # brought over to -stable) + if not rev.next and rev.parent and rev.parent.next: + r = rev.parent.next + while r.next: + r = r.next + entry.next_main = r.string + + elif request.roottype == 'svn': + entry.prev = rev.prev and rev.prev.string + entry.branches = entry.tags = entry.branch_points = [ ] + entry.tag_names = entry.branch_names = [ ] + entry.vendor_branch = None + if rev.filename != request.where: + entry.orig_path = rev.filename + entry.copy_path = rev.copy_path + entry.copy_rev = rev.copy_rev + + if entry.orig_path: + entry.orig_href = request.get_url(view_func=view_log, + where=entry.orig_path, + pathtype=vclib.FILE, + params={'pathrev': rev.string}, + escape=1) + + if rev.copy_path: + entry.copy_href = request.get_url(view_func=view_log, + where=rev.copy_path, + pathtype=vclib.FILE, + params={'pathrev': rev.copy_rev}, + escape=1) + + + # view/download links + if pathtype is vclib.FILE: + entry.view_href, entry.download_href, entry.download_text_href, \ + entry.annotate_href, entry.revision_href, entry.prefer_markup \ + = get_file_view_info(request, request.where, rev.string, mime_type) + else: + entry.revision_href = request.get_url(view_func=view_revision, + params={'revision': rev.string}, + escape=1) + entry.view_href = request.get_url(view_func=view_directory, + where=rev.filename, + pathtype=vclib.DIR, + params={'pathrev': rev.string}, + escape=1) + + # calculate diff links + if selected_rev != entry.rev: + entry.sel_for_diff_href = \ + request.get_url(view_func=view_log, + params={'r1': entry.rev}, + escape=1) + if entry.prev is not None: + entry.diff_to_prev_href = \ + request.get_url(view_func=view_diff, + params={'r1': entry.prev, + 'r2': entry.rev, + 'diff_format': None}, + escape=1) + if selected_rev and \ + selected_rev != str(entry.rev) and \ + selected_rev != str(entry.prev) and \ + selected_rev != str(entry.branch_point) and \ + selected_rev != str(entry.next_main): + entry.diff_to_sel_href = \ + request.get_url(view_func=view_diff, + params={'r1': selected_rev, + 'r2': entry.rev, + 'diff_format': None}, + escape=1) + + if entry.next_main: + entry.diff_to_main_href = \ + request.get_url(view_func=view_diff, + params={'r1': entry.next_main, + 'r2': entry.rev, + 'diff_format': None}, + escape=1) + if entry.branch_point: + entry.diff_to_branch_href = \ + request.get_url(view_func=view_diff, + params={'r1': entry.branch_point, + 'r2': entry.rev, + 'diff_format': None}, + escape=1) + + # Save our escaping until the end so stuff above works + if entry.orig_path: + entry.orig_path = request.server.escape(entry.orig_path) + if entry.copy_path: + entry.copy_path = request.server.escape(entry.copy_path) + entries.append(entry) + + data = common_template_data(request) + data.update({ + 'default_branch' : None, + 'mime_type' : mime_type, + 'rev_selected' : selected_rev, + 'diff_format' : diff_format, + 'logsort' : logsort, + 'human_readable' : ezt.boolean(diff_format in ('h', 'l')), + 'log_pagestart' : None, + 'entries': entries, + 'head_prefer_markup' : ezt.boolean(0), + 'head_view_href' : None, + 'head_download_href': None, + 'head_download_text_href': None, + 'head_annotate_href': None, + 'tag_prefer_markup' : ezt.boolean(0), + 'tag_view_href' : None, + 'tag_download_href': None, + 'tag_download_text_href': None, + 'tag_annotate_href': None, + }) + + lastrev = pathrev_form(request, data) + + data['diff_select_action'], data['diff_select_hidden_values'] = \ + request.get_form(view_func=view_diff, + params={'r1': None, 'r2': None, 'tr1': None, + 'tr2': None, 'diff_format': None}) + + data['logsort_action'], data['logsort_hidden_values'] = \ + request.get_form(params={'logsort': None}) + + if pathtype is vclib.FILE: + if not request.pathrev or lastrev is None: + view_href, download_href, download_text_href, \ + annotate_href, revision_href, prefer_markup \ + = get_file_view_info(request, request.where, None, mime_type, None) + data.update({ + 'head_view_href': view_href, + 'head_download_href': download_href, + 'head_download_text_href': download_text_href, + 'head_annotate_href': annotate_href, + 'head_prefer_markup': prefer_markup, + }) + + if request.pathrev and request.roottype == 'cvs': + view_href, download_href, download_text_href, \ + annotate_href, revision_href, prefer_markup \ + = get_file_view_info(request, request.where, None, mime_type) + data.update({ + 'tag_view_href': view_href, + 'tag_download_href': download_href, + 'tag_download_text_href': download_text_href, + 'tag_annotate_href': annotate_href, + 'tag_prefer_markup': prefer_markup, + }) + else: + data['head_view_href'] = request.get_url(view_func=view_directory, + params={}, escape=1) + + taginfo = options.get('cvs_tags', {}) + tagitems = taginfo.items() + tagitems.sort() + tagitems.reverse() + + main = taginfo.get('MAIN') + if main: + # Default branch may have multiple names so we list them + branches = [] + for branch in main.aliases: + # Don't list MAIN + if branch is not main: + branches.append(branch) + data['default_branch'] = prep_tags(request, branches) + + data['tags'] = tags = [ ] + data['branch_tags'] = branch_tags = [] + data['plain_tags'] = plain_tags = [] + for tag, rev in tagitems: + if rev.co_rev: + tags.append(_item(rev=rev.co_rev.string, name=tag)) + if rev.is_branch: + branch_tags.append(tag) + else: + plain_tags.append(tag) + + if cfg.options.use_pagesize: + data['log_paging_action'], data['log_paging_hidden_values'] = \ + request.get_form(params={'log_pagestart': None}) + data['log_pagestart'] = int(request.query_dict.get('log_pagestart',0)) + data['entries'] = paging_sws(data, 'entries', data['log_pagestart'], + 'rev', cfg.options.use_pagesize, first) + + generate_page(request, "log", data) + +def view_checkout(request): + + cfg = request.cfg + + if 'co' not in cfg.options.allowed_views: + raise debug.ViewVCException('Checkout view is disabled', + '403 Forbidden') + + path, rev = _orig_path(request) + fp, revision = request.repos.openfile(path, rev) + + # The revision number acts as a strong validator. + if not check_freshness(request, None, revision): + mime_type = request.query_dict.get('content-type') \ + or calculate_mime_type(request, path, rev) \ + or 'text/plain' + server_fp = get_writeready_server_file(request, mime_type) + copy_stream(fp, server_fp, cfg) + fp.close() + +def view_cvsgraph_image(request): + "output the image rendered by cvsgraph" + # this function is derived from cgi/cvsgraphmkimg.cgi + + cfg = request.cfg + + if not cfg.options.use_cvsgraph: + raise debug.ViewVCException('Graph view is disabled', '403 Forbidden') + + # If cvsgraph can't find its supporting libraries, uncomment and set + # accordingly. Do the same in view_cvsgraph(). + #os.environ['LD_LIBRARY_PATH'] = '/usr/lib:/usr/local/lib:/path/to/cvsgraph' + + rcsfile = request.repos.rcsfile(request.path_parts) + fp = popen.popen(cfg.utilities.cvsgraph or 'cvsgraph', + ("-c", cfg.path(cfg.options.cvsgraph_conf), + "-r", request.repos.rootpath, + rcsfile), 'rb', 0) + + copy_stream(fp, get_writeready_server_file(request, 'image/png'), cfg) + fp.close() + +def view_cvsgraph(request): + "output a page containing an image rendered by cvsgraph" + + cfg = request.cfg + + if not cfg.options.use_cvsgraph: + raise debug.ViewVCException('Graph view is disabled', '403 Forbidden') + + data = common_template_data(request) + + # If cvsgraph can't find its supporting libraries, uncomment and set + # accordingly. Do the same in view_cvsgraph_image(). + #os.environ['LD_LIBRARY_PATH'] = '/usr/lib:/usr/local/lib:/path/to/cvsgraph' + + imagesrc = request.get_url(view_func=view_cvsgraph_image, escape=1) + mime_type = guess_mime(request.where) + view = default_view(mime_type, cfg) + up_where = _path_join(request.path_parts[:-1]) + + # Create an image map + rcsfile = request.repos.rcsfile(request.path_parts) + fp = popen.popen(cfg.utilities.cvsgraph or 'cvsgraph', + ("-i", + "-c", cfg.path(cfg.options.cvsgraph_conf), + "-r", request.repos.rootpath, + "-x", "x", + "-3", request.get_url(view_func=view_log, params={}, + escape=1), + "-4", request.get_url(view_func=view, + params={'revision': None}, + escape=1, partial=1), + "-5", request.get_url(view_func=view_diff, + params={'r1': None, 'r2': None}, + escape=1, partial=1), + "-6", request.get_url(view_func=view_directory, + where=up_where, + pathtype=vclib.DIR, + params={'pathrev': None}, + escape=1, partial=1), + rcsfile), 'rb', 0) + + data.update({ + 'imagemap' : fp, + 'imagesrc' : imagesrc, + }) + + generate_page(request, "graph", data) + +def search_file(repos, path_parts, rev, search_re): + """Return 1 iff the contents of the file at PATH_PARTS in REPOS as + of revision REV matches regular expression SEARCH_RE.""" + + # Read in each line of a checked-out file, and then use re.search to + # search line. + fp = repos.openfile(path_parts, rev)[0] + matches = 0 + while 1: + line = fp.readline() + if not line: + break + if search_re.search(line): + matches = 1 + fp.close() + break + return matches + +def view_doc(request): + """Serve ViewVC static content locally. + + Using this avoids the need for modifying the setup of the web server. + """ + cfg = request.cfg + document = request.where + filename = cfg.path(os.path.join(cfg.options.template_dir, + "docroot", document)) + + # Stat the file to get content length and last-modified date. + try: + info = os.stat(filename) + except OSError, v: + raise debug.ViewVCException('Static file "%s" not available (%s)' + % (document, str(v)), '404 Not Found') + content_length = str(info[stat.ST_SIZE]) + last_modified = info[stat.ST_MTIME] + + # content_length + mtime makes a pretty good etag. + if check_freshness(request, last_modified, + "%s-%s" % (content_length, last_modified)): + return + + try: + fp = open(filename, "rb") + except IOError, v: + raise debug.ViewVCException('Static file "%s" not available (%s)' + % (document, str(v)), '404 Not Found') + + request.server.addheader('Content-Length', content_length) + if document[-3:] == 'png': + mime_type = 'image/png' + elif document[-3:] == 'jpg': + mime_type = 'image/jpeg' + elif document[-3:] == 'gif': + mime_type = 'image/gif' + elif document[-3:] == 'css': + mime_type = 'text/css' + else: # assume HTML: + mime_type = None + copy_stream(fp, get_writeready_server_file(request, mime_type), cfg) + fp.close() + +def rcsdiff_date_reformat(date_str, cfg): + if date_str is None: + return None + try: + date = compat.cvs_strptime(date_str) + except ValueError: + return date_str + return make_time_string(compat.timegm(date), cfg) + +_re_extract_rev = re.compile(r'^[-+*]{3} [^\t]+\t([^\t]+)\t((\d+\.)*\d+)$') +_re_extract_info = re.compile(r'@@ \-([0-9]+).*\+([0-9]+).*@@(.*)') + +class DiffSource: + def __init__(self, fp, cfg): + self.fp = fp + self.cfg = cfg + self.save_line = None + self.line_number = None + self.prev_line_number = None + + # keep track of where we are during an iteration + self.idx = -1 + self.last = None + + # these will be set once we start reading + self.state = 'no-changes' + self.left_col = [ ] + self.right_col = [ ] + + def __getitem__(self, idx): + if idx == self.idx: + return self.last + if idx != self.idx + 1: + raise DiffSequencingError() + + # keep calling _get_row until it gives us something. sometimes, it + # doesn't return a row immediately because it is accumulating changes. + # when it is out of data, _get_row will raise IndexError. + while 1: + item = self._get_row() + if item: + self.idx = idx + self.last = item + return item + + def _format_text(self, text): + text = string.expandtabs(string.rstrip(text)) + hr_breakable = self.cfg.options.hr_breakable + + # in the code below, "\x01" will be our stand-in for "&". We don't want + # to insert "&" because it would get escaped by htmlify(). Similarly, + # we use "\x02" as a stand-in for "
" + + if hr_breakable > 1 and len(text) > hr_breakable: + text = re.sub('(' + ('.' * hr_breakable) + ')', '\\1\x02', text) + if hr_breakable: + # make every other space "breakable" + text = string.replace(text, ' ', ' \x01nbsp;') + else: + text = string.replace(text, ' ', '\x01nbsp;') + text = htmlify(text, mangle_email_addrs=0) + text = string.replace(text, '\x01', '&') + text = string.replace(text, '\x02', + '\
') + return text + + def _get_row(self): + if self.state[:5] == 'flush': + item = self._flush_row() + if item: + return item + self.state = 'dump' + + if self.save_line: + line = self.save_line + self.save_line = None + else: + line = self.fp.readline() + + if not line: + if self.state == 'no-changes': + self.state = 'done' + return _item(type='no-changes') + + # see if there are lines to flush + if self.left_col or self.right_col: + # move into the flushing state + self.state = 'flush-' + self.state + return None + + # nothing more to return + raise IndexError + + if line[:2] == '@@': + self.state = 'dump' + self.left_col = [ ] + self.right_col = [ ] + + match = _re_extract_info.match(line) + self.line_number = int(match.group(2)) - 1 + self.prev_line_number = int(match.group(1)) - 1 + return _item(type='header', + line_info_left=match.group(1), + line_info_right=match.group(2), + line_info_extra=match.group(3)) + + if line[0] == '\\': + # \ No newline at end of file + + # move into the flushing state. note: it doesn't matter if we really + # have data to flush or not; that will be figured out later + self.state = 'flush-' + self.state + return None + + diff_code = line[0] + output = self._format_text(line[1:]) + + if diff_code == '+': + if self.state == 'dump': + self.line_number = self.line_number + 1 + return _item(type='add', right=output, line_number=self.line_number) + + self.state = 'pre-change-add' + self.right_col.append(output) + return None + + if diff_code == '-': + self.state = 'pre-change-remove' + self.left_col.append(output) + return None # early exit to avoid line in + + if self.left_col or self.right_col: + # save the line for processing again later, and move into the + # flushing state + self.save_line = line + self.state = 'flush-' + self.state + return None + + self.line_number = self.line_number + 1 + self.prev_line_number = self.prev_line_number + 1 + return _item(type='context', left=output, right=output, + line_number=self.line_number) + + def _flush_row(self): + if not self.left_col and not self.right_col: + # nothing more to flush + return None + + if self.state == 'flush-pre-change-remove': + self.prev_line_number = self.prev_line_number + 1 + return _item(type='remove', left=self.left_col.pop(0), + line_number=self.prev_line_number) + + # state == flush-pre-change-add + item = _item(type='change', + have_left=ezt.boolean(0), + have_right=ezt.boolean(0)) + if self.left_col: + self.prev_line_number = self.prev_line_number + 1 + item.have_left = ezt.boolean(1) + item.left = self.left_col.pop(0) + item.line_number = self.prev_line_number + if self.right_col: + self.line_number = self.line_number + 1 + item.have_right = ezt.boolean(1) + item.right = self.right_col.pop(0) + item.line_number = self.line_number + return item + +class DiffSequencingError(Exception): + pass + +def diff_parse_headers(fp, diff_type, rev1, rev2, sym1=None, sym2=None): + date1 = date2 = log_rev1 = log_rev2 = flag = None + header_lines = [] + + if diff_type == vclib.UNIFIED: + f1 = '--- ' + f2 = '+++ ' + elif diff_type == vclib.CONTEXT: + f1 = '*** ' + f2 = '--- ' + else: + f1 = f2 = None + + # If we're parsing headers, then parse and tweak the diff headers, + # collecting them in an array until we've read and handled them all. + if f1 and f2: + parsing = 1 + len_f1 = len(f1) + len_f2 = len(f2) + while parsing: + line = fp.readline() + if not line: + break + + if line[:len(f1)] == f1: + match = _re_extract_rev.match(line) + if match: + date1 = match.group(1) + log_rev1 = match.group(2) + if sym1: + line = line[:-1] + ' %s\n' % sym1 + elif line[:len(f2)] == f2: + match = _re_extract_rev.match(line) + if match: + date2 = match.group(1) + log_rev2 = match.group(2) + if sym2: + line = line[:-1] + ' %s\n' % sym2 + parsing = 0 + elif line[:3] == 'Bin': + flag = _RCSDIFF_IS_BINARY + parsing = 0 + elif (string.find(line, 'not found') != -1 or + string.find(line, 'illegal option') != -1): + flag = _RCSDIFF_ERROR + parsing = 0 + header_lines.append(line) + + if (log_rev1 and log_rev1 != rev1): + raise debug.ViewVCException('rcsdiff found revision %s, but expected ' + 'revision %s' % (log_rev1, rev1), + '500 Internal Server Error') + if (log_rev2 and log_rev2 != rev2): + raise debug.ViewVCException('rcsdiff found revision %s, but expected ' + 'revision %s' % (log_rev2, rev2), + '500 Internal Server Error') + + return date1, date2, flag, string.join(header_lines, '') + + +def _get_diff_path_parts(request, query_key, rev, base_rev): + repos = request.repos + if request.query_dict.has_key(query_key): + parts = _path_parts(request.query_dict[query_key]) + elif request.roottype == 'svn': + try: + parts = _path_parts(repos.get_location(request.where, + repos._getrev(base_rev), + repos._getrev(rev))) + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' + 'to diff', '400 Bad Request') + except vclib.ItemNotFound: + raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' + 'to diff', '400 Bad Request') + else: + parts = request.path_parts + return parts + + +def setup_diff(request): + query_dict = request.query_dict + + rev1 = r1 = query_dict['r1'] + rev2 = r2 = query_dict['r2'] + sym1 = sym2 = None + + # hack on the diff revisions + if r1 == 'text': + rev1 = query_dict.get('tr1', None) + if not rev1: + raise debug.ViewVCException('Missing revision from the diff ' + 'form text field', '400 Bad Request') + else: + idx = string.find(r1, ':') + if idx == -1: + rev1 = r1 + else: + rev1 = r1[:idx] + sym1 = r1[idx+1:] + + if r2 == 'text': + rev2 = query_dict.get('tr2', None) + if not rev2: + raise debug.ViewVCException('Missing revision from the diff ' + 'form text field', '400 Bad Request') + sym2 = '' + else: + idx = string.find(r2, ':') + if idx == -1: + rev2 = r2 + else: + rev2 = r2[:idx] + sym2 = r2[idx+1:] + + if request.roottype == 'svn': + try: + rev1 = str(request.repos._getrev(rev1)) + rev2 = str(request.repos._getrev(rev2)) + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid revision(s) passed to diff', + '400 Bad Request') + + p1 = _get_diff_path_parts(request, 'p1', rev1, request.pathrev) + p2 = _get_diff_path_parts(request, 'p2', rev2, request.pathrev) + + try: + if revcmp(rev1, rev2) > 0: + rev1, rev2 = rev2, rev1 + sym1, sym2 = sym2, sym1 + p1, p2 = p2, p1 + except ValueError: + raise debug.ViewVCException('Invalid revision(s) passed to diff', + '400 Bad Request') + return p1, p2, rev1, rev2, sym1, sym2 + + +def view_patch(request): + cfg = request.cfg + query_dict = request.query_dict + p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request) + + # In the absence of a format dictation in the CGI params, we'll let + # use the configured diff format, allowing 'c' to mean 'c' and + # anything else to mean 'u'. + format = query_dict.get('diff_format', + cfg.options.diff_format == 'c' and 'c' or 'u') + if format == 'c': + diff_type = vclib.CONTEXT + elif format == 'u': + diff_type = vclib.UNIFIED + else: + raise debug.ViewVCException('Diff format %s not understood' + % format, '400 Bad Request') + + try: + fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type) + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' + 'to diff', '400 Bad Request') + + date1, date2, flag, headers = diff_parse_headers(fp, diff_type, rev1, rev2, + sym1, sym2) + + server_fp = get_writeready_server_file(request, 'text/plain') + server_fp.write(headers) + copy_stream(fp, server_fp, cfg) + fp.close() + + +def view_diff(request): + cfg = request.cfg + query_dict = request.query_dict + p1, p2, rev1, rev2, sym1, sym2 = setup_diff(request) + + # since templates are in use and subversion allows changes to the dates, + # we can't provide a strong etag + if check_freshness(request, None, '%s-%s' % (rev1, rev2), weak=1): + return + + diff_type = None + diff_options = {} + human_readable = 0 + + format = query_dict.get('diff_format', cfg.options.diff_format) + if format == 'c': + diff_type = vclib.CONTEXT + elif format == 's': + diff_type = vclib.SIDE_BY_SIDE + elif format == 'l': + diff_type = vclib.UNIFIED + diff_options['context'] = 15 + human_readable = 1 + elif format == 'f': + diff_type = vclib.UNIFIED + diff_options['context'] = None + human_readable = 1 + elif format == 'h': + diff_type = vclib.UNIFIED + human_readable = 1 + elif format == 'u': + diff_type = vclib.UNIFIED + else: + raise debug.ViewVCException('Diff format %s not understood' + % format, '400 Bad Request') + + if human_readable: + diff_options['funout'] = cfg.options.hr_funout + diff_options['ignore_white'] = cfg.options.hr_ignore_white + diff_options['ignore_keyword_subst'] = cfg.options.hr_ignore_keyword_subst + try: + fp = sidebyside = unified = None + if (cfg.options.hr_intraline and idiff + and ((human_readable and idiff.sidebyside) + or (not human_readable and diff_type == vclib.UNIFIED))): + f1 = request.repos.openfile(p1, rev1)[0] + try: + lines_left = f1.readlines() + finally: + f1.close() + + f2 = request.repos.openfile(p2, rev2)[0] + try: + lines_right = f2.readlines() + finally: + f2.close() + + if human_readable: + sidebyside = idiff.sidebyside(lines_left, lines_right, + diff_options.get("context", 5)) + else: + unified = idiff.unified(lines_left, lines_right, + diff_options.get("context", 2)) + else: + fp = request.repos.rawdiff(p1, rev1, p2, rev2, diff_type, diff_options) + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid path(s) or revision(s) passed ' + 'to diff', '400 Bad Request') + path_left = _path_join(p1) + path_right = _path_join(p2) + if fp: + date1, date2, flag, headers = diff_parse_headers(fp, diff_type, + rev1, rev2, + sym1, sym2) + else: + date1 = date2 = flag = headers = None + + raw_diff_fp = changes = None + if fp: + if human_readable: + if flag is not None: + changes = [ _item(type=flag) ] + else: + changes = DiffSource(fp, cfg) + else: + raw_diff_fp = MarkupPipeWrapper(cfg, fp, + htmlify(headers, mangle_email_addrs=0), + None, 1) + + no_format_params = request.query_dict.copy() + no_format_params['diff_format'] = None + + left = _item(date=rcsdiff_date_reformat(date1, cfg), + path=path_left, rev=rev1, tag=sym1) + left.view_href, left.download_href, left.download_text_href, \ + left.annotate_href, left.revision_href, left.prefer_markup \ + = get_file_view_info(request, path_left, rev1) + + right = _item(date=rcsdiff_date_reformat(date2, cfg), + path=path_right, rev=rev2, tag=sym2) + right.view_href, right.download_href, right.download_text_href, \ + right.annotate_href, right.revision_href, right.prefer_markup \ + = get_file_view_info(request, path_right, rev2) + + data = common_template_data(request) + data.update({ + 'left' : left, + 'right' : right, + 'raw_diff' : raw_diff_fp, + 'changes' : changes, + 'sidebyside': sidebyside, + 'unified': unified, + 'diff_format' : request.query_dict.get('diff_format', + cfg.options.diff_format), + 'patch_href' : request.get_url(view_func=view_patch, + params=no_format_params, + escape=1), + }) + + data['diff_format_action'], data['diff_format_hidden_values'] = \ + request.get_form(params=no_format_params) + + generate_page(request, "diff", data) + + +def generate_tarball_header(out, name, size=0, mode=None, mtime=0, + uid=0, gid=0, typefrag=None, linkname='', + uname='viewvc', gname='viewvc', + devmajor=1, devminor=0, prefix=None, + magic='ustar', version='00', chksum=None): + if not mode: + if name[-1:] == '/': + mode = 0755 + else: + mode = 0644 + + if not typefrag: + if name[-1:] == '/': + typefrag = '5' # directory + else: + typefrag = '0' # regular file + + if not prefix: + prefix = '' + + # generate a GNU tar extension header for long names. + if len(name) >= 100: + generate_tarball_header(out, '././@LongLink', len(name), + 0644, 0, 0, 0, 'L') + out.write(name) + out.write('\0' * (511 - ((len(name) + 511) % 512))) + + block1 = struct.pack('100s 8s 8s 8s 12s 12s', + name, + '%07o' % mode, + '%07o' % uid, + '%07o' % gid, + '%011o' % size, + '%011o' % mtime) + + block2 = struct.pack('c 100s 6s 2s 32s 32s 8s 8s 155s', + typefrag, + linkname, + magic, + version, + uname, + gname, + '%07o' % devmajor, + '%07o' % devminor, + prefix) + + if not chksum: + dummy_chksum = ' ' + block = block1 + dummy_chksum + block2 + chksum = 0 + for i in range(len(block)): + chksum = chksum + ord(block[i]) + + block = block1 + struct.pack('8s', '%07o' % chksum) + block2 + block = block + '\0' * (512 - len(block)) + + out.write(block) + +def generate_tarball(out, request, reldir, stack, dir_mtime=None): + # get directory info from repository + rep_path = request.path_parts + reldir + entries = request.repos.listdir(rep_path, request.pathrev, {}) + request.repos.dirlogs(rep_path, request.pathrev, entries, {}) + entries.sort(lambda a, b: cmp(a.name, b.name)) + + # figure out corresponding path in tar file. everything gets put underneath + # a single top level directory named after the repository directory being + # tarred + if request.path_parts: + tar_dir = request.path_parts[-1] + '/' + else: + tar_dir = request.rootname + '/' + if reldir: + tar_dir = tar_dir + _path_join(reldir) + '/' + + cvs = request.roottype == 'cvs' + + # If our caller doesn't dictate a datestamp to use for the current + # directory, its datestamps will be the youngest of the datestamps + # of versioned items in that subdirectory. We'll be ignoring dead + # or busted items and, in CVS, subdirs. + if dir_mtime is None: + dir_mtime = 0 + for file in entries: + if cvs and (file.kind != vclib.FILE or file.rev is None or file.dead): + continue + if (file.date is not None) and (file.date > dir_mtime): + dir_mtime = file.date + + # Push current directory onto the stack. + stack.append(tar_dir) + + # If this is Subversion, we generate a header for this directory + # regardless of its contents. For CVS it will only get into the + # tarball if it has files underneath it, which we determine later. + if not cvs: + generate_tarball_header(out, tar_dir, mtime=dir_mtime) + + # Run through the files in this directory, skipping busted and + # unauthorized ones. + for file in entries: + if file.kind != vclib.FILE: + continue + if cvs and (file.rev is None or file.dead): + continue + + # If we get here, we've seen at least one valid file in the + # current directory. For CVS, we need to make sure there are + # directory parents to contain it, so we flush the stack. + if cvs: + for dir in stack: + generate_tarball_header(out, dir, mtime=dir_mtime) + del stack[:] + + # Calculate the mode for the file. Sure, we could look directly + # at the ,v file in CVS, but that's a layering violation we'd like + # to avoid as much as possible. + if request.repos.isexecutable(rep_path + [file.name], request.pathrev): + mode = 0755 + else: + mode = 0644 + + ### FIXME: Read the whole file into memory? Bad... better to do + ### 2 passes. + fp = request.repos.openfile(rep_path + [file.name], request.pathrev)[0] + contents = fp.read() + fp.close() + + generate_tarball_header(out, tar_dir + file.name, + len(contents), mode, + file.date is not None and file.date or 0) + out.write(contents) + out.write('\0' * (511 - ((len(contents) + 511) % 512))) + + # Recurse into subdirectories, skipping busted and unauthorized (or + # configured-to-be-hidden) ones. + for file in entries: + if file.errors or file.kind != vclib.DIR: + continue + if request.cfg.options.hide_cvsroot \ + and is_cvsroot_path(request.roottype, rep_path + [file.name]): + continue + + mtime = request.roottype == 'svn' and file.date or None + generate_tarball(out, request, reldir + [file.name], stack, mtime) + + # Pop the current directory from the stack. + del stack[-1:] + +def download_tarball(request): + cfg = request.cfg + + if 'tar' not in request.cfg.options.allowed_views: + raise debug.ViewVCException('Tarball generation is disabled', + '403 Forbidden') + + if debug.TARFILE_PATH: + fp = open(debug.TARFILE_PATH, 'w') + else: + tarfile = request.rootname + if request.path_parts: + tarfile = "%s-%s" % (tarfile, request.path_parts[-1]) + request.server.addheader('Content-Disposition', + 'attachment; filename="%s.tar.gz"' % (tarfile)) + server_fp = get_writeready_server_file(request, 'application/x-gzip') + request.server.flush() + + # Try to use the Python gzip module, if available; otherwise, + # we'll use the configured 'gzip' binary. + fp = gzip.GzipFile('', 'wb', 9, server_fp) + + ### FIXME: For Subversion repositories, we can get the real mtime of the + ### top-level directory here. + generate_tarball(fp, request, [], []) + + fp.write('\0' * 1024) + fp.close() + + if debug.TARFILE_PATH: + request.server.header('') + print """ + + +

Tarball '%s' successfully generated!

+ +""" % (debug.TARFILE_PATH) + + +def view_revision(request): + if request.roottype == "cvs": + raise ViewVCException("Revision view not supported for CVS repositories " + "at this time.", "400 Bad Request") + + cfg = request.cfg + data = common_template_data(request) + query_dict = request.query_dict + try: + rev = request.repos._getrev(query_dict.get('revision')) + except vclib.InvalidRevision: + raise debug.ViewVCException('Invalid revision', '404 Not Found') + youngest_rev = request.repos.get_youngest_revision() + + # The revision number acts as a weak validator (but we tell browsers + # not to cache the youngest revision). + if rev != youngest_rev and check_freshness(request, None, str(rev), weak=1): + return + + # Fetch the revision information. + date, author, msg, changes = request.repos.revinfo(rev) + date_str = make_time_string(date, cfg) + + # Sort the changes list by path. + def changes_sort_by_path(a, b): + return cmp(a.path_parts, b.path_parts) + changes.sort(changes_sort_by_path) + + # Handle limit_changes parameter + cfg_limit_changes = cfg.options.limit_changes + limit_changes = int(query_dict.get('limit_changes', cfg_limit_changes)) + more_changes = None + more_changes_href = None + first_changes = None + first_changes_href = None + if limit_changes and len(changes) > limit_changes: + more_changes = len(changes) - limit_changes + params = query_dict.copy() + params['limit_changes'] = 0 + more_changes_href = request.get_url(params=params, escape=1) + changes = changes[:limit_changes] + elif cfg_limit_changes and len(changes) > cfg_limit_changes: + first_changes = cfg_limit_changes + params = query_dict.copy() + params['limit_changes'] = None + first_changes_href = request.get_url(params=params, escape=1) + + # Add the hrefs, types, and prev info + for change in changes: + change.view_href = change.diff_href = change.type = change.log_href = None + + # If the path is newly added, don't claim text or property + # modifications. + if (change.action == vclib.ADDED or change.action == vclib.REPLACED) \ + and not change.copied: + change.text_changed = 0 + change.props_changed = 0 + + # Calculate the view link URLs (for which we must have a pathtype). + if change.pathtype: + view_func = None + if change.pathtype is vclib.FILE \ + and 'markup' in cfg.options.allowed_views: + view_func = view_markup + elif change.pathtype is vclib.DIR: + view_func = view_directory + + path = _path_join(change.path_parts) + base_path = _path_join(change.base_path_parts) + if change.action == vclib.DELETED: + link_rev = str(change.base_rev) + link_where = base_path + else: + link_rev = str(rev) + link_where = path + + change.view_href = request.get_url(view_func=view_func, + where=link_where, + pathtype=change.pathtype, + params={'pathrev' : link_rev}, + escape=1) + change.log_href = request.get_url(view_func=view_log, + where=link_where, + pathtype=change.pathtype, + params={'pathrev' : link_rev}, + escape=1) + + if change.pathtype is vclib.FILE and change.text_changed: + change.diff_href = request.get_url(view_func=view_diff, + where=path, + pathtype=change.pathtype, + params={'pathrev' : str(rev), + 'r1' : str(rev), + 'r2' : str(change.base_rev), + }, + escape=1) + + + # use same variable names as the log template + change.path = _path_join(change.path_parts) + change.copy_path = _path_join(change.base_path_parts) + change.copy_rev = change.base_rev + change.text_mods = ezt.boolean(change.text_changed) + change.prop_mods = ezt.boolean(change.props_changed) + change.is_copy = ezt.boolean(change.copied) + change.pathtype = (change.pathtype == vclib.FILE and 'file') \ + or (change.pathtype == vclib.DIR and 'dir') \ + or None + del change.path_parts + del change.base_path_parts + del change.base_rev + del change.text_changed + del change.props_changed + del change.copied + + prev_rev_href = next_rev_href = None + if rev > 0: + prev_rev_href = request.get_url(view_func=view_revision, + where=None, + pathtype=None, + params={'revision': str(rev - 1)}, + escape=1) + if rev < request.repos.get_youngest_revision(): + next_rev_href = request.get_url(view_func=view_revision, + where=None, + pathtype=None, + params={'revision': str(rev + 1)}, + escape=1) + data.update({ + 'rev' : str(rev), + 'author' : author, + 'date' : date_str, + 'log' : msg and htmlify(msg, cfg.options.mangle_email_addresses) or None, + 'ago' : None, + 'changes' : changes, + 'prev_href' : prev_rev_href, + 'next_href' : next_rev_href, + 'limit_changes': limit_changes, + 'more_changes': more_changes, + 'more_changes_href': more_changes_href, + 'first_changes': first_changes, + 'first_changes_href': first_changes_href, + }) + + if date is not None: + data['ago'] = html_time(request, date, 1) + + data['jump_rev_action'], data['jump_rev_hidden_values'] = \ + request.get_form(params={'revision': None}) + + if rev == youngest_rev: + request.server.addheader("Cache-control", "no-store") + generate_page(request, "revision", data) + +def is_query_supported(request): + """Returns true if querying is supported for the given path.""" + return request.cfg.cvsdb.enabled \ + and request.pathtype == vclib.DIR \ + and request.roottype in ['cvs', 'svn'] + +def is_querydb_nonempty_for_root(request): + """Return 1 iff commits database integration is supported *and* the + current root is found in that database. Only does this check if + check_database is set to 1.""" + if request.cfg.cvsdb.enabled and request.roottype in ['cvs', 'svn']: + if request.cfg.cvsdb.check_database_for_root: + global cvsdb + import cvsdb + db = cvsdb.ConnectDatabaseReadOnly(request.cfg) + repos_root, repos_dir = cvsdb.FindRepository(db, request.rootpath) + if repos_root: + return 1 + else: + return 1 + return 0 + +def view_queryform(request): + if not is_query_supported(request): + raise debug.ViewVCException('Can not query project root "%s" at "%s".' + % (request.rootname, request.where), + '403 Forbidden') + + data = common_template_data(request) + + data['query_action'], data['query_hidden_values'] = \ + request.get_form(view_func=view_query, params={'limit_changes': None}) + + # default values ... + data['branch'] = request.query_dict.get('branch', '') + data['branch_match'] = request.query_dict.get('branch_match', 'exact') + data['dir'] = request.query_dict.get('dir', '') + data['file'] = request.query_dict.get('file', '') + data['file_match'] = request.query_dict.get('file_match', 'exact') + data['who'] = request.query_dict.get('who', '') + data['who_match'] = request.query_dict.get('who_match', 'exact') + data['comment'] = request.query_dict.get('comment', '') + data['comment_match'] = request.query_dict.get('comment_match', 'exact') + data['querysort'] = request.query_dict.get('querysort', 'date') + data['date'] = request.query_dict.get('date', 'hours') + data['hours'] = request.query_dict.get('hours', '2') + data['mindate'] = request.query_dict.get('mindate', '') + data['maxdate'] = request.query_dict.get('maxdate', '') + data['limit_changes'] = int(request.query_dict.get('limit_changes', + request.cfg.options.limit_changes)) + + data['dir_href'] = request.get_url(view_func=view_directory, params={}, + escape=1) + + generate_page(request, "query_form", data) + +def parse_date(datestr): + """Parse a date string from the query form.""" + + match = re.match(r'^(\d\d\d\d)-(\d\d)-(\d\d)(?:\ +' + '(\d\d):(\d\d)(?::(\d\d))?)?$', datestr) + if match: + year = int(match.group(1)) + month = int(match.group(2)) + day = int(match.group(3)) + hour = match.group(4) + if hour is not None: + hour = int(hour) + else: + hour = 0 + minute = match.group(5) + if minute is not None: + minute = int(minute) + else: + minute = 0 + second = match.group(6) + if second is not None: + second = int(second) + else: + second = 0 + # return a "seconds since epoch" value assuming date given in UTC + tm = (year, month, day, hour, minute, second, 0, 0, 0) + return compat.timegm(tm) + else: + return None + +def english_query(request): + """Generate a sentance describing the query.""" + cfg = request.cfg + ret = [ 'Checkins ' ] + dir = request.query_dict.get('dir', '') + if dir: + ret.append('to ') + if ',' in dir: + ret.append('subdirectories') + else: + ret.append('subdirectory') + ret.append(' %s ' % request.server.escape(dir)) + file = request.query_dict.get('file', '') + if file: + if len(ret) != 1: + ret.append('and ') + ret.append('to file %s ' % request.server.escape(file)) + who = request.query_dict.get('who', '') + branch = request.query_dict.get('branch', '') + if branch: + ret.append('on branch %s ' % request.server.escape(branch)) + else: + ret.append('on all branches ') + comment = request.query_dict.get('comment', '') + if comment: + ret.append('with comment %s ' + % htmlify(comment, mangle_email_addrs=0)) + if who: + ret.append('by %s ' % request.server.escape(who)) + date = request.query_dict.get('date', 'hours') + if date == 'hours': + ret.append('in the last %s hours' \ + % request.server.escape(request.query_dict.get('hours', '2'))) + elif date == 'day': + ret.append('in the last day') + elif date == 'week': + ret.append('in the last week') + elif date == 'month': + ret.append('in the last month') + elif date == 'all': + ret.append('since the beginning of time') + elif date == 'explicit': + mindate = request.query_dict.get('mindate', '') + maxdate = request.query_dict.get('maxdate', '') + if mindate and maxdate: + w1, w2 = 'between', 'and' + else: + w1, w2 = 'since', 'before' + if mindate: + mindate = make_time_string(parse_date(mindate), cfg) + ret.append('%s %s ' % (w1, mindate)) + if maxdate: + maxdate = make_time_string(parse_date(maxdate), cfg) + ret.append('%s %s ' % (w2, maxdate)) + return string.join(ret, '') + +def prev_rev(rev): + """Returns a string representing the previous revision of the argument.""" + 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 string.join(r, '.') + +def build_commit(request, files, max_files, dir_strip, format): + """Return a commit object build from the information in FILES, or + None if no allowed files are present in the set. DIR_STRIP is the + path prefix to remove from the commit object's set of files. If + MAX_FILES is non-zero, it is used to limit the number of files + returned in the commit object. FORMAT is the requested output + format of the query request.""" + + cfg = request.cfg + author = files[0].GetAuthor() + date = files[0].GetTime() + desc = files[0].GetDescription() + commit_rev = files[0].GetRevision() + len_strip = len(dir_strip) + commit_files = [] + num_allowed = 0 + plus_count = 0 + minus_count = 0 + found_unreadable = 0 + + for f in files: + dirname = f.GetDirectory() + filename = f.GetFile() + if dir_strip: + assert dirname[:len_strip] == dir_strip + assert len(dirname) == len_strip or dirname[len(dir_strip)] == '/' + dirname = dirname[len_strip+1:] + where = dirname and ("%s/%s" % (dirname, filename)) or filename + rev = f.GetRevision() + rev_prev = prev_rev(rev) + commit_time = f.GetTime() + if commit_time: + commit_time = make_time_string(commit_time, cfg) + change_type = f.GetTypeString() + + # In CVS, we can actually look at deleted revisions; in Subversion + # we can't -- we'll look at the previous revision instead. + exam_rev = rev + if request.roottype == 'svn' and change_type == 'Remove': + exam_rev = rev_prev + + # Check path access (since the commits database logic bypasses the + # vclib layer and, thus, the vcauth stuff that layer uses). + path_parts = _path_parts(where) + if path_parts: + # Skip files in CVSROOT if asked to hide such. + if cfg.options.hide_cvsroot \ + and is_cvsroot_path(request.roottype, path_parts): + found_unreadable = 1 + continue + + # We have to do a rare authz check here because this data comes + # from the CVSdb, not from the vclib providers. + # + # WARNING: The Subversion CVSdb integration logic is weak, weak, + # weak. It has no ability to track copies, so complex + # situations like a copied directory with a deleted subfile (all + # in the same revision) are very ... difficult. We've no choice + # but to omit as unauthorized paths the authorization logic + # can't find. + try: + readable = vclib.check_path_access(request.repos, path_parts, + None, exam_rev) + except vclib.ItemNotFound: + readable = 0 + if not readable: + found_unreadable = 1 + continue + + if request.roottype == 'svn': + params = { 'pathrev': exam_rev } + else: + params = { 'revision': exam_rev, 'pathrev': f.GetBranch() or None } + + dir_href = request.get_url(view_func=view_directory, + where=dirname, pathtype=vclib.DIR, + params=params, escape=1) + log_href = request.get_url(view_func=view_log, + where=where, pathtype=vclib.FILE, + params=params, escape=1) + diff_href = view_href = download_href = None + if 'markup' in cfg.options.allowed_views: + view_href = request.get_url(view_func=view_markup, + where=where, pathtype=vclib.FILE, + params=params, escape=1) + if 'co' in cfg.options.allowed_views: + download_href = request.get_url(view_func=view_checkout, + where=where, pathtype=vclib.FILE, + params=params, escape=1) + if change_type == 'Change': + diff_href_params = params.copy() + diff_href_params.update({ + 'r1': rev_prev, + 'r2': rev, + 'diff_format': None + }) + diff_href = request.get_url(view_func=view_diff, + where=where, pathtype=vclib.FILE, + params=diff_href_params, escape=1) + mime_type = calculate_mime_type(request, path_parts, exam_rev) + prefer_markup = ezt.boolean(default_view(mime_type, cfg) == view_markup) + + # Update plus/minus line change count. + plus = int(f.GetPlusCount()) + minus = int(f.GetMinusCount()) + plus_count = plus_count + plus + minus_count = minus_count + minus + + num_allowed = num_allowed + 1 + if max_files and num_allowed > max_files: + continue + + commit_files.append(_item(date=commit_time, + dir=request.server.escape(dirname), + file=request.server.escape(filename), + author=request.server.escape(f.GetAuthor()), + rev=rev, + branch=f.GetBranch(), + plus=plus, + minus=minus, + type=change_type, + dir_href=dir_href, + log_href=log_href, + view_href=view_href, + download_href=download_href, + prefer_markup=prefer_markup, + diff_href=diff_href)) + + # No files survived authz checks? Let's just pretend this + # little commit didn't happen, shall we? + if not len(commit_files): + return None + + commit = _item(num_files=len(commit_files), files=commit_files, + plus=plus_count, minus=minus_count) + commit.limited_files = ezt.boolean(num_allowed > len(commit_files)) + + # We'll mask log messages in commits which contain unreadable paths, + # but even that is kinda iffy. If a person searches for + # '/some/hidden/path' across log messages, then gets a response set + # that shows commits lacking log message, said person can reasonably + # assume that the log messages contained the hidden path, and that + # this is likely because they are referencing a real path in the + # repository -- a path the user isn't supposed to even know about. + if found_unreadable: + commit.log = None + commit.short_log = None + else: + commit.log = htmlify(desc) + commit.short_log = format_log(desc, cfg, format != 'rss') + commit.author = request.server.escape(author) + commit.rss_date = make_rss_time_string(date, request.cfg) + if request.roottype == 'svn': + commit.rev = commit_rev + commit.rss_url = '%s://%s%s' % \ + (request.server.getenv("HTTPS") == "on" and "https" or "http", + request.server.getenv("HTTP_HOST"), + request.get_url(view_func=view_revision, + params={'revision': commit.rev}, + escape=1)) + else: + commit.rev = None + commit.rss_url = None + return commit + +def query_backout(request, commits): + request.server.header('text/plain') + if commits: + print '# This page can be saved as a shell script and executed.' + print '# It should be run at the top of your work area. It will update' + print '# your working copy to back out the changes selected by the' + print '# query.' + print + else: + print '# No changes were selected by the query.' + print '# There is nothing to back out.' + return + for commit in commits: + for fileinfo in commit.files: + if request.roottype == 'cvs': + print 'cvs update -j %s -j %s %s/%s' \ + % (fileinfo.rev, prev_rev(fileinfo.rev), + fileinfo.dir, fileinfo.file) + elif request.roottype == 'svn': + print 'svn merge -r %s:%s %s/%s' \ + % (fileinfo.rev, prev_rev(fileinfo.rev), + fileinfo.dir, fileinfo.file) + +def view_query(request): + if not is_query_supported(request): + raise debug.ViewVCException('Can not query project root "%s" at "%s".' + % (request.rootname, request.where), + '403 Forbidden') + + cfg = request.cfg + + # get form data + branch = request.query_dict.get('branch', '') + branch_match = request.query_dict.get('branch_match', 'exact') + dir = request.query_dict.get('dir', '') + file = request.query_dict.get('file', '') + file_match = request.query_dict.get('file_match', 'exact') + who = request.query_dict.get('who', '') + who_match = request.query_dict.get('who_match', 'exact') + comment = request.query_dict.get('comment', '') + comment_match = request.query_dict.get('comment_match', 'exact') + querysort = request.query_dict.get('querysort', 'date') + date = request.query_dict.get('date', 'hours') + hours = request.query_dict.get('hours', '2') + mindate = request.query_dict.get('mindate', '') + maxdate = request.query_dict.get('maxdate', '') + format = request.query_dict.get('format') + limit = int(request.query_dict.get('limit', 0)) + limit_changes = int(request.query_dict.get('limit_changes', + cfg.options.limit_changes)) + + match_types = { 'exact':1, 'like':1, 'glob':1, 'regex':1, 'notregex':1 } + sort_types = { 'date':1, 'author':1, 'file':1 } + date_types = { 'hours':1, 'day':1, 'week':1, 'month':1, + 'all':1, 'explicit':1 } + + # parse various fields, validating or converting them + if not match_types.has_key(branch_match): branch_match = 'exact' + if not match_types.has_key(file_match): file_match = 'exact' + if not match_types.has_key(who_match): who_match = 'exact' + if not match_types.has_key(comment_match): comment_match = 'exact' + if not sort_types.has_key(querysort): querysort = 'date' + if not date_types.has_key(date): date = 'hours' + mindate = parse_date(mindate) + maxdate = parse_date(maxdate) + + global cvsdb + import cvsdb + + db = cvsdb.ConnectDatabaseReadOnly(cfg) + repos_root, repos_dir = cvsdb.FindRepository(db, request.rootpath) + if not repos_root: + raise debug.ViewVCException( + "The root '%s' was not found in the commit database " + % request.rootname) + + # create the database query from the form data + query = cvsdb.CreateCheckinQuery() + query.SetRepository(repos_root) + # treat "HEAD" specially ... + if branch_match == 'exact' and branch == 'HEAD': + query.SetBranch('') + elif branch: + query.SetBranch(branch, branch_match) + if dir: + for subdir in string.split(dir, ','): + path = (_path_join(repos_dir + request.path_parts + + _path_parts(string.strip(subdir)))) + query.SetDirectory(path, 'exact') + query.SetDirectory('%s/%%' % cvsdb.EscapeLike(path), 'like') + else: + where = _path_join(repos_dir + request.path_parts) + if where: # if we are in a subdirectory ... + query.SetDirectory(where, 'exact') + query.SetDirectory('%s/%%' % cvsdb.EscapeLike(where), 'like') + if file: + query.SetFile(file, file_match) + if who: + query.SetAuthor(who, who_match) + if comment: + query.SetComment(comment, comment_match) + query.SetSortMethod(querysort) + if date == 'hours': + query.SetFromDateHoursAgo(int(hours)) + elif date == 'day': + query.SetFromDateDaysAgo(1) + elif date == 'week': + query.SetFromDateDaysAgo(7) + elif date == 'month': + query.SetFromDateDaysAgo(31) + elif date == 'all': + pass + elif date == 'explicit': + if mindate is not None: + query.SetFromDateObject(mindate) + if maxdate is not None: + query.SetToDateObject(maxdate) + if limit: + query.SetLimit(limit) + elif format == 'rss': + query.SetLimit(cfg.cvsdb.rss_row_limit) + + # run the query + db.RunQuery(query) + + sql = request.server.escape(db.CreateSQLQueryString(query)) + + # gather commits + commits = [] + plus_count = 0 + minus_count = 0 + mod_time = -1 + if query.commit_list: + files = [] + limited_files = 0 + current_desc = query.commit_list[0].GetDescriptionID() + current_rev = query.commit_list[0].GetRevision() + dir_strip = _path_join(repos_dir) + + for commit in query.commit_list: + commit_desc = commit.GetDescriptionID() + commit_rev = commit.GetRevision() + + # base modification time on the newest commit + if commit.GetTime() > mod_time: + mod_time = commit.GetTime() + + # For CVS, group commits with the same commit message. + # For Subversion, group them only if they have the same revision number + if request.roottype == 'cvs': + if current_desc == commit_desc: + files.append(commit) + continue + else: + if current_rev == commit_rev: + files.append(commit) + continue + + # append this grouping + commit_item = build_commit(request, files, limit_changes, + dir_strip, format) + if commit_item: + # update running plus/minus totals + plus_count = plus_count + commit_item.plus + minus_count = minus_count + commit_item.minus + commits.append(commit_item) + + files = [ commit ] + limited_files = 0 + current_desc = commit_desc + current_rev = commit_rev + + # we need to tack on our last commit grouping, if any + commit_item = build_commit(request, files, limit_changes, + dir_strip, format) + if commit_item: + # update running plus/minus totals + plus_count = plus_count + commit_item.plus + minus_count = minus_count + commit_item.minus + commits.append(commit_item) + + # only show the branch column if we are querying all branches + # or doing a non-exact branch match on a CVS repository. + show_branch = ezt.boolean(request.roottype == 'cvs' and + (branch == '' or branch_match != 'exact')) + + # backout link + params = request.query_dict.copy() + params['format'] = 'backout' + backout_href = request.get_url(params=params, + escape=1) + + # link to zero limit_changes value + params = request.query_dict.copy() + params['limit_changes'] = 0 + limit_changes_href = request.get_url(params=params, escape=1) + + # if we got any results, use the newest commit as the modification time + if mod_time >= 0: + if check_freshness(request, mod_time): + return + + if format == 'backout': + query_backout(request, commits) + return + + data = common_template_data(request) + data.update({ + 'sql': sql, + 'english_query': english_query(request), + 'queryform_href': request.get_url(view_func=view_queryform, escape=1), + 'backout_href': backout_href, + 'plus_count': plus_count, + 'minus_count': minus_count, + 'show_branch': show_branch, + 'querysort': querysort, + 'commits': commits, + 'limit_changes': limit_changes, + 'limit_changes_href': limit_changes_href, + 'rss_link_href': request.get_url(view_func=view_query, + params={'date': 'month'}, + escape=1, + prefix=1), + }) + + if format == 'rss': + generate_page(request, "rss", data, "application/rss+xml") + else: + generate_page(request, "query_results", data) + +_views = { + 'annotate': view_annotate, + 'co': view_checkout, + 'diff': view_diff, + 'dir': view_directory, + 'graph': view_cvsgraph, + 'graphimg': view_cvsgraph_image, + 'log': view_log, + 'markup': view_markup, + 'patch': view_patch, + 'query': view_query, + 'queryform': view_queryform, + 'revision': view_revision, + 'roots': view_roots, + 'tar': download_tarball, + 'redirect_pathrev': redirect_pathrev, +} + +_view_codes = {} +for code, view in _views.items(): + _view_codes[view] = code + +def list_roots(request): + cfg = request.cfg + allroots = { } + + # Add the viewable Subversion roots + for root in cfg.general.svn_roots.keys(): + auth = setup_authorizer(cfg, request.username, root) + try: + vclib.svn.SubversionRepository(root, cfg.general.svn_roots[root], auth, + cfg.utilities, cfg.options.svn_config_dir) + except vclib.ReposNotFound: + continue + allroots[root] = [cfg.general.svn_roots[root], 'svn'] + + # Add the viewable CVS roots + for root in cfg.general.cvs_roots.keys(): + auth = setup_authorizer(cfg, request.username, root) + try: + vclib.ccvs.CVSRepository(root, cfg.general.cvs_roots[root], auth, + cfg.utilities, cfg.options.use_rcsparse) + except vclib.ReposNotFound: + continue + allroots[root] = [cfg.general.cvs_roots[root], 'cvs'] + + return allroots + +def expand_root_parents(cfg): + """Expand the configured root parents into individual roots.""" + + # Each item in root_parents is a "directory : repo_type" string. + for pp in cfg.general.root_parents: + pos = string.rfind(pp, ':') + if pos < 0: + raise debug.ViewVCException( + "The path '%s' in 'root_parents' does not include a " + "repository type." % (pp)) + + repo_type = string.strip(pp[pos+1:]) + pp = os.path.normpath(string.strip(pp[:pos])) + + if repo_type == 'cvs': + roots = vclib.ccvs.expand_root_parent(pp) + if cfg.options.hide_cvsroot and roots.has_key('CVSROOT'): + del roots['CVSROOT'] + cfg.general.cvs_roots.update(roots) + elif repo_type == 'svn': + roots = vclib.svn.expand_root_parent(pp) + cfg.general.svn_roots.update(roots) + else: + raise debug.ViewVCException( + "The path '%s' in 'root_parents' has an unrecognized " + "repository type." % (pp)) + +def find_root_in_parents(cfg, rootname, roottype): + """Return the rootpath for configured ROOTNAME of ROOTTYPE.""" + + # Easy out: caller wants rootname "CVSROOT", and we're hiding those. + if rootname == 'CVSROOT' and cfg.options.hide_cvsroot: + return None + + for pp in cfg.general.root_parents: + pos = string.rfind(pp, ':') + if pos < 0: + continue + repo_type = string.strip(pp[pos+1:]) + if repo_type != roottype: + continue + pp = os.path.normpath(string.strip(pp[:pos])) + + if roottype == 'cvs': + roots = vclib.ccvs.expand_root_parent(pp) + elif roottype == 'svn': + roots = vclib.svn.expand_root_parent(pp) + else: + roots = {} + if roots.has_key(rootname): + return roots[rootname] + return None + +def locate_root(cfg, rootname): + """Return a 2-tuple ROOTTYPE, ROOTPATH for configured ROOTNAME.""" + if cfg.general.cvs_roots.has_key(rootname): + return 'cvs', cfg.general.cvs_roots[rootname] + path_in_parent = find_root_in_parents(cfg, rootname, 'cvs') + if path_in_parent: + cfg.general.cvs_roots[rootname] = path_in_parent + return 'cvs', path_in_parent + if cfg.general.svn_roots.has_key(rootname): + return 'svn', cfg.general.svn_roots[rootname] + path_in_parent = find_root_in_parents(cfg, rootname, 'svn') + if path_in_parent: + cfg.general.svn_roots[rootname] = path_in_parent + return 'svn', path_in_parent + return None, None + +def load_config(pathname=None, server=None): + debug.t_start('load-config') + + if pathname is None: + pathname = (os.environ.get("VIEWVC_CONF_PATHNAME") + or os.environ.get("VIEWCVS_CONF_PATHNAME") + or os.path.join(os.path.dirname(os.path.dirname(__file__)), + "viewvc.conf")) + + cfg = config.Config() + cfg.set_defaults() + cfg.load_config(pathname, server and server.getenv("HTTP_HOST")) + + # load mime types file + if cfg.general.mime_types_file: + mimetypes.init([cfg.general.mime_types_file]) + + debug.t_end('load-config') + return cfg + + +def view_error(server, cfg): + exc_dict = debug.GetExceptionData() + status = exc_dict['status'] + if exc_dict['msg']: + exc_dict['msg'] = htmlify(exc_dict['msg'], mangle_email_addrs=0) + if exc_dict['stacktrace']: + exc_dict['stacktrace'] = htmlify(exc_dict['stacktrace'], + mangle_email_addrs=0) + handled = 0 + + # use the configured error template if possible + try: + if cfg and not server.headerSent: + server.header(status=status) + template = get_view_template(cfg, "error") + template.generate(server.file(), exc_dict) + handled = 1 + except: + pass + + # but fallback to the old exception printer if no configuration is + # available, or if something went wrong + if not handled: + debug.PrintException(server, exc_dict) + +def main(server, cfg): + try: + debug.t_start('main') + try: + # build a Request object, which contains info about the HTTP request + request = Request(server, cfg) + request.run_viewvc() + except SystemExit, e: + return + except: + view_error(server, cfg) + + finally: + debug.t_end('main') + debug.dump() + debug.DumpChildren(server) + + +class _item: + def __init__(self, **kw): + vars(self).update(kw) diff --git a/lib/win32popen.py b/lib/win32popen.py new file mode 100644 index 00000000..281b43cc --- /dev/null +++ b/lib/win32popen.py @@ -0,0 +1,235 @@ +# -*-python-*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- +# +# Utilities for controlling processes and pipes on win32 +# +# ----------------------------------------------------------------------- + +import os, sys, traceback, string, 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 win32process, win32pipe, win32con +import win32event, win32file, winerror +import pywintypes, msvcrt + +# Buffer size for spooling +SPOOL_BYTES = 4096 + +# File object to write error messages +SPOOL_ERROR = sys.stderr +#SPOOL_ERROR = open("m:/temp/error.txt", "wt") + +def CommandLine(command, args): + """Convert an executable path and a sequence of arguments into a command + line that can be passed to CreateProcess""" + + cmd = "\"" + string.replace(command, "\"", "\"\"") + "\"" + for arg in args: + cmd = cmd + " \"" + string.replace(arg, "\"", "\"\"") + "\"" + return cmd + +def CreateProcess(cmd, hStdInput, hStdOutput, hStdError): + """Creates a new process which uses the specified handles for its standard + input, output, and error. The handles must be inheritable. 0 can be passed + as a special handle indicating that the process should inherit the current + process's input, output, or error streams, and None can be passed to discard + the child process's output or to prevent it from reading any input.""" + + # initialize new process's startup info + si = win32process.STARTUPINFO() + si.dwFlags = win32process.STARTF_USESTDHANDLES + + if hStdInput == 0: + si.hStdInput = win32api.GetStdHandle(win32api.STD_INPUT_HANDLE) + else: + si.hStdInput = hStdInput + + if hStdOutput == 0: + si.hStdOutput = win32api.GetStdHandle(win32api.STD_OUTPUT_HANDLE) + else: + si.hStdOutput = hStdOutput + + if hStdError == 0: + si.hStdError = win32api.GetStdHandle(win32api.STD_ERROR_HANDLE) + else: + si.hStdError = hStdError + + # create the process + phandle, pid, thandle, tid = win32process.CreateProcess \ + ( None, # appName + cmd, # commandLine + None, # processAttributes + None, # threadAttributes + 1, # bInheritHandles + win32con.NORMAL_PRIORITY_CLASS, # dwCreationFlags + None, # newEnvironment + None, # currentDirectory + si # startupinfo + ) + + if hStdInput and hasattr(hStdInput, 'Close'): + hStdInput.Close() + + if hStdOutput and hasattr(hStdOutput, 'Close'): + hStdOutput.Close() + + if hStdError and hasattr(hStdError, 'Close'): + hStdError.Close() + + return phandle, pid, thandle, tid + +def CreatePipe(readInheritable, writeInheritable): + """Create a new pipe specifying whether the read and write ends are + inheritable and whether they should be created for blocking or nonblocking + I/O.""" + + r, w = win32pipe.CreatePipe(None, SPOOL_BYTES) + if readInheritable: + r = MakeInheritedHandle(r) + if writeInheritable: + w = MakeInheritedHandle(w) + return r, w + +def File2FileObject(pipe, mode): + """Make a C stdio file object out of a win32 file handle""" + if string.find(mode, 'r') >= 0: + wmode = os.O_RDONLY + elif string.find(mode, 'w') >= 0: + wmode = os.O_WRONLY + if string.find(mode, 'b') >= 0: + wmode = wmode | os.O_BINARY + if string.find(mode, 't') >= 0: + wmode = wmode | os.O_TEXT + return os.fdopen(msvcrt.open_osfhandle(pipe.Detach(),wmode),mode) + +def FileObject2File(fileObject): + """Get the win32 file handle from a C stdio file object""" + return win32file._get_osfhandle(fileObject.fileno()) + +def DuplicateHandle(handle): + """Duplicates a win32 handle.""" + proc = win32api.GetCurrentProcess() + return win32api.DuplicateHandle(proc,handle,proc,0,0,win32con.DUPLICATE_SAME_ACCESS) + +def MakePrivateHandle(handle, replace = 1): + """Turn an inherited handle into a non inherited one. This avoids the + handle duplication that occurs on CreateProcess calls which can create + uncloseable pipes.""" + + ### Could change implementation to use SetHandleInformation()... + + flags = win32con.DUPLICATE_SAME_ACCESS + proc = win32api.GetCurrentProcess() + if replace: flags = flags | win32con.DUPLICATE_CLOSE_SOURCE + newhandle = win32api.DuplicateHandle(proc,handle,proc,0,0,flags) + if replace: handle.Detach() # handle was already deleted by the last call + return newhandle + +def MakeInheritedHandle(handle, replace = 1): + """Turn a private handle into an inherited one.""" + + ### Could change implementation to use SetHandleInformation()... + + flags = win32con.DUPLICATE_SAME_ACCESS + proc = win32api.GetCurrentProcess() + if replace: flags = flags | win32con.DUPLICATE_CLOSE_SOURCE + newhandle = win32api.DuplicateHandle(proc,handle,proc,0,1,flags) + if replace: handle.Detach() # handle was deleted by the last call + return newhandle + +def MakeSpyPipe(readInheritable, writeInheritable, outFiles = None, doneEvent = None): + """Return read and write handles to a pipe that asynchronously writes all of + its input to the files in the outFiles sequence. doneEvent can be None, or a + a win32 event handle that will be set when the write end of pipe is closed. + """ + + if outFiles is None: + return CreatePipe(readInheritable, writeInheritable) + + r, writeHandle = CreatePipe(0, writeInheritable) + if readInheritable is None: + readHandle, w = None, None + else: + readHandle, w = CreatePipe(readInheritable, 0) + + thread.start_new_thread(SpoolWorker, (r, w, outFiles, doneEvent)) + + return readHandle, writeHandle + +def SpoolWorker(srcHandle, destHandle, outFiles, doneEvent): + """Thread entry point for implementation of MakeSpyPipe""" + try: + buffer = win32file.AllocateReadBuffer(SPOOL_BYTES) + + while 1: + try: + #print >> SPOOL_ERROR, "Calling ReadFile..."; SPOOL_ERROR.flush() + hr, data = win32file.ReadFile(srcHandle, buffer) + #print >> SPOOL_ERROR, "ReadFile returned '%s', '%s'" % (str(hr), str(data)); SPOOL_ERROR.flush() + if hr != 0: + raise "win32file.ReadFile returned %i, '%s'" % (hr, data) + elif len(data) == 0: + break + except pywintypes.error, e: + #print >> SPOOL_ERROR, "ReadFile threw '%s'" % str(e); SPOOL_ERROR.flush() + if e.args[0] == winerror.ERROR_BROKEN_PIPE: + break + else: + raise e + + #print >> SPOOL_ERROR, "Writing to %i file objects..." % len(outFiles); SPOOL_ERROR.flush() + for f in outFiles: + f.write(data) + #print >> SPOOL_ERROR, "Done writing to file objects."; SPOOL_ERROR.flush() + + #print >> SPOOL_ERROR, "Writing to destination %s" % str(destHandle); SPOOL_ERROR.flush() + if destHandle: + #print >> SPOOL_ERROR, "Calling WriteFile..."; SPOOL_ERROR.flush() + hr, bytes = win32file.WriteFile(destHandle, data) + #print >> SPOOL_ERROR, "WriteFile() passed %i bytes and returned %i, %i" % (len(data), hr, bytes); SPOOL_ERROR.flush() + if hr != 0 or bytes != len(data): + raise "win32file.WriteFile() passed %i bytes and returned %i, %i" % (len(data), hr, bytes) + + srcHandle.Close() + + if doneEvent: + win32event.SetEvent(doneEvent) + + if destHandle: + destHandle.Close() + + except: + info = sys.exc_info() + SPOOL_ERROR.writelines(apply(traceback.format_exception, info), '') + SPOOL_ERROR.flush() + del info + +def NullFile(inheritable): + """Create a null file handle.""" + if inheritable: + sa = pywintypes.SECURITY_ATTRIBUTES() + sa.bInheritHandle = 1 + else: + sa = None + + return win32file.CreateFile("nul", + win32file.GENERIC_READ | win32file.GENERIC_WRITE, + win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE, + sa, win32file.OPEN_EXISTING, 0, None) diff --git a/templates-contrib/README b/templates-contrib/README new file mode 100644 index 00000000..b36d93d1 --- /dev/null +++ b/templates-contrib/README @@ -0,0 +1,5 @@ +This directory contains ViewVC template sets contributed by their +respective authors and expected to work against ViewVC 1.0.x. They +are not necessarily supported by the ViewVC development community, and +do not necessarily carry the same license or copyright as ViewVC +itself. diff --git a/templates-contrib/newvc/README b/templates-contrib/newvc/README new file mode 100644 index 00000000..f00ceb6d --- /dev/null +++ b/templates-contrib/newvc/README @@ -0,0 +1,7 @@ +Template Set: newvc +Author(s): C. Michael Pilato +Compatibility: ViewVC 1.1 + +The "newvc" template set uses top navigation tabs to flip between +various views of a file or directory, and aims for a clean-yet-modern +look and feel. diff --git a/templates-contrib/newvc/templates/diff.ezt b/templates-contrib/newvc/templates/diff.ezt new file mode 100644 index 00000000..0072bbf7 --- /dev/null +++ b/templates-contrib/newvc/templates/diff.ezt @@ -0,0 +1,128 @@ +[# Setup page definitions] + [define page_title]Diff of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "diff"] + +
+
+ [for diff_format_hidden_values][end] + + + (Generate patch) +
+
+ +
+ + +[if-any raw_diff] +
[raw_diff]
+[else] + +[define change_right][end] +[define last_change_type][end] + +[# these should live in stylesheets] + + +[for changes] + [is changes.type "change"][else] + [if-any change_right][change_right][define change_right][end][end] + [end] + [is changes.type "header"] + + + + + [else] + [is changes.type "add"] + + + + + + [else] + [is changes.type "remove"] + + + + + + [else] + [is changes.type "change"] + [if-any changes.have_left] + + + + + + [end] + [define change_right][change_right] + [if-any changes.have_right] + + + + + [end] + [end] + [else] + [is changes.type "no-changes"] + + [else] + [is changes.type "binary-diff"] + + [else] + [is changes.type "error"] + + [else][# a line of context] + + + + + + [end][end][end][end][end][end][end] + [define last_change_type][changes.type][end] +[end] +[if-any change_right][change_right][end] +
# + Line [changes.line_info_left] | + Line [changes.line_info_right] +
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]+[changes.right]
[changes.line_number][changes.left]
[changes.line_number]<[changes.left]
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]>[changes.right]
- No changes -
- Binary file revisions differ -
- ViewVC depends on rcsdiff and GNU diff + to create this page. ViewVC cannot find GNU diff. Even if you + have GNU diff installed, the rcsdiff program must be configured + and compiled with the GNU diff location. -
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.right]
+ +

Diff Legend

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

ViewVC Help: Directory View

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

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

+ +

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

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

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

+ +

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

+ +

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

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

ViewVC Help: Log View

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+

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

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

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

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

ViewVC Help: Query The Commit Database

+
+

Other Help:

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

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

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

+

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

+

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

+

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

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

ViewVC Help: General

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

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

+ + + +

Multiple Repositories

+ +

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

+ +

Sticky Revision and Tag

+ +

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

+ +

Dead Files

+ +

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

+ +

Artificial Tags

+ +

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

+ +

More Information

+ +

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

+ +

Documentation about CVS

+
+

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

+
+ +

Documentation about Subversion

+
+

+ Version Control with + Subversion
+

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

An Exception Has Occurred

+ +[if-any msg] +

[msg]

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

HTTP Response Status

+

[status]

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

Python Traceback

+

+[stacktrace]
+

+[end] + +[# Here follows a bunch of space characters, present to ensure that + our error message is larger than 512 bytes so that IE's "Friendly + Error Message" won't show. For more information, see + http://oreillynet.com/onjava/blog/2002/09/internet_explorer_subverts_err.html] + + + + + + + + + + + + + + + + + + + + diff --git a/templates-contrib/newvc/templates/file.ezt b/templates-contrib/newvc/templates/file.ezt new file mode 100644 index 00000000..218b47d6 --- /dev/null +++ b/templates-contrib/newvc/templates/file.ezt @@ -0,0 +1,61 @@ +[# setup page definitions] + [define page_title]Annotation of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "annotate"] +[include "include/fileview.ezt"] + +
+ + +[define last_rev]0[end] +[define rowclass]vc_row_odd[end] + +[if-any lines] +
+ + + +[is annotation "annotated"] + + +[end] + + +[for lines] + [is lines.rev last_rev] + [else] + [is rowclass "vc_row_even"] + [define rowclass]vc_row_odd[end] + [else] + [define rowclass]vc_row_even[end] + [end] + [end] + + + +[is annotation "annotated"] + + +[end] + + + [define last_rev][lines.rev][end] +[end] +
LineUserRevFile contents
[lines.line_number][is lines.rev last_rev] [else][lines.author][end][is lines.rev last_rev] [else][if-any lines.diff_href][end][lines.rev][if-any lines.diff_href][end][end][lines.text]
+
+ +[else] +[if-any image_src_href] +
+ +
+[end] +[end] + +[include "include/props.ezt"] + + +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/graph.ezt b/templates-contrib/newvc/templates/graph.ezt new file mode 100644 index 00000000..1896793d --- /dev/null +++ b/templates-contrib/newvc/templates/graph.ezt @@ -0,0 +1,15 @@ +[# setup page definitions] + [define page_title]Graph of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "graph"] + +
+[imagemap] +Revisions of [where] +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/include/diff_form.ezt b/templates-contrib/newvc/templates/include/diff_form.ezt new file mode 100644 index 00000000..239e95d8 --- /dev/null +++ b/templates-contrib/newvc/templates/include/diff_form.ezt @@ -0,0 +1,70 @@ + +
+

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

+ +
+ + + + + + + + + + +
  + [for diff_select_hidden_values][end] + Diffs between +[if-any tags] + + +[else] + +[end] + + and +[if-any tags] + + +[else] + +[end] +
  + Type of Diff should be a + + +
+
+
diff --git a/templates-contrib/newvc/templates/include/fileview.ezt b/templates-contrib/newvc/templates/include/fileview.ezt new file mode 100644 index 00000000..fc718719 --- /dev/null +++ b/templates-contrib/newvc/templates/include/fileview.ezt @@ -0,0 +1,74 @@ + + + + + + + + + + +[if-any orig_path] + + + + +[end] +[if-any branches] + + + + +[end] +[if-any tags] + + + + +[end] +[if-any branch_points] + + + + +[end] +[is roottype "cvs"][if-any changed] + + + + +[end][end] +[is roottype "svn"][if-any size] + + + +[end][end] +[if-any lockinfo] + + +[end] +[is state "dead"] + + + + +[end] +[if-any annotation] +[is annotation "binary"] + + + +[end] +[is annotation "error"] + + + +[end] +[end] +[if-any log] + + + + +[end] +
Revision:[if-any revision_href][rev][else][rev][end] [if-any vendor_branch] (vendor branch)[end]
Committed:[if-any date][date] [end][if-any ago]([ago] ago) [end][if-any author]by [author][end]
Original Path:[orig_path]
Branch:[branches]
CVS Tags:[tags]
Branch point for:[branch_points]
Changes since [prev]:[changed] lines
File size:[size] byte(s)
Lock status:[lockinfo]
State:FILE REMOVED
Unable to calculate annotation data on binary file contents.
Error occurred while calculating annotation data.
Log Message:
[log]
diff --git a/templates-contrib/newvc/templates/include/footer.ezt b/templates-contrib/newvc/templates/include/footer.ezt new file mode 100644 index 00000000..753ddf89 --- /dev/null +++ b/templates-contrib/newvc/templates/include/footer.ezt @@ -0,0 +1,10 @@ + + + + + + diff --git a/templates-contrib/newvc/templates/include/header.ezt b/templates-contrib/newvc/templates/include/header.ezt new file mode 100644 index 00000000..74d45cc4 --- /dev/null +++ b/templates-contrib/newvc/templates/include/header.ezt @@ -0,0 +1,63 @@ + + + + + + + [[]ViewVC] [page_title] [if-any rootname][rootname][if-any where]/[where][end][end] + + + + [if-any rss_href] + + [end] + + + + +
+ +
+[if-any username]Logged in as: [username] |[end] +ViewVC Help +
+ + + +
+[is pathtype "dir"] + View Directory + [if-any log_href] + | Revision Log + [end] + [if-any queryform_href] + | Commit Query + [end] + [if-any tarball_href] + | Download Tarball + [end] +[end] +[is pathtype "file"] + View File + | Revision Log + | Show Annotations + [if-any graph_href] + | Revision Graph + [end] + | Download File +[end] +[if-any revision_href] + | View Changeset +[end] + | Root Listing +
+ +
+[if-any roots_href]root[end][if-any nav_path]/[for nav_path][if-any nav_path.href][end][if-index nav_path last][end][nav_path.name][if-index nav_path last][end][if-any nav_path.href][end][if-index nav_path last][else]/[end][end][end] +
+ +
+ +
diff --git a/templates-contrib/newvc/templates/include/pathrev_form.ezt b/templates-contrib/newvc/templates/include/pathrev_form.ezt new file mode 100644 index 00000000..301e610b --- /dev/null +++ b/templates-contrib/newvc/templates/include/pathrev_form.ezt @@ -0,0 +1,53 @@ +
+
+[for pathrev_hidden_values][end] +[is roottype "cvs"] + [define pathrev_selected][pathrev][end] + +[else] + +[end] + +
+
+ +[if-any pathrev] +
+
+[for pathrev_clear_hidden_values][end] +[if-any lastrev] + [is pathrev lastrev][else][end] + (Current path doesn't exist after revision [lastrev]) +[else] + +[end] +
+
+[end] diff --git a/templates-contrib/newvc/templates/include/props.ezt b/templates-contrib/newvc/templates/include/props.ezt new file mode 100644 index 00000000..81f175d6 --- /dev/null +++ b/templates-contrib/newvc/templates/include/props.ezt @@ -0,0 +1,26 @@ +[if-any properties] +
+
+

Properties

+ + + + + + + + +[for properties] + + + [if-any properties.undisplayable] + + [else] + + [end] + +[end] + +
NameValue
[properties.name]Property value is undisplayable.[properties.value]
+
+[end] diff --git a/templates-contrib/newvc/templates/log.ezt b/templates-contrib/newvc/templates/log.ezt new file mode 100644 index 00000000..879d5829 --- /dev/null +++ b/templates-contrib/newvc/templates/log.ezt @@ -0,0 +1,247 @@ +[# setup page definitions] + [define page_title]Log of:[end] + [define help_href][docroot]/help_log.html[end] +[# end] +[include "include/header.ezt" "log"] + + + +[if-any default_branch] + + + + +[end] + +[is pathtype "file"] +[if-any view_href] + + + + +[end] + +[if-any tag_view_href] + + + + +[end] +[end] + + + + + + +[is cfg.options.use_pagesize "0"][else][is picklist_len "1"][else] + + + + +[end][end] + + + + + + +
Default branch:[for default_branch][default_branch.name][if-index default_branch last][else], [end] +[end]
Links to HEAD: + (view) + [if-any download_href](download)[end] + [if-any download_text_href](as text)[end] + [if-any annotate_href](annotate)[end] +
Links to [pathrev]: + (view) + [if-any tag_download_href](download)[end] + [if-any tag_download_text_href](as text)[end] + [if-any tag_annotate_href](annotate)[end] +
Sticky [is roottype "cvs"]Tag[else]Revision[end]:[include "include/pathrev_form.ezt"]
Jump to page:
+ [for log_paging_hidden_values][end] + + +
+
Sort logs by:
+
+ + [for logsort_hidden_values][end] + + +
+
+
+ +
+ + +[define first_revision][end] +[define last_revision][end] + +[for entries] + +[if-index entries first][define first_revision][entries.rev][end][end] +[if-index entries last] +[define last_revision][entries.rev][end] +
+[else] +
+[end] + + [is entries.state "dead"] + Revision [entries.rev] + [else] + + [for entries.tag_names] + [end] + [for entries.branch_names] + [end] + + Revision [is roottype "svn"][entries.rev][else][entries.rev][end] - + [if-any entries.view_href] + [is pathtype "file"] + (view) + [else] + Directory Listing + [end] + [end] + [if-any entries.download_href](download)[end] + [if-any entries.download_text_href](as text)[end] + [if-any entries.annotate_href](annotate)[end] + + [is pathtype "file"] + [# if you don't want to allow select for diffs then remove this section] + [is entries.rev rev_selected] + - [[]selected] + [else] + - [[]select for diffs] + [end] + [end] + [end] + + [if-any entries.vendor_branch] + (vendor branch) + [end] + +
+ + [is roottype "svn"] + [if-index entries last]Added[else]Modified[end] + [end] + + [if-any entries.date][entries.date][else](unknown date)[end] + [if-any entries.ago]([entries.ago] ago)[end] + by [if-any entries.author][entries.author][else](unknown author)[end] + + [if-any entries.orig_path] +
Original Path: [entries.orig_path] + [end] + + [if-any entries.branches] +
Branch: + [for entries.branches] + [entries.branches.name][if-index entries.branches last][else],[end] + [end] + [end] + + [if-any entries.tags] +
CVS Tags: + [for entries.tags] + [entries.tags.name][if-index entries.tags last][else],[end] + [end] + [end] + + [if-any entries.branch_points] +
Branch point for: + [for entries.branch_points] + [entries.branch_points.name][if-index entries.branch_points last][else],[end] + [end] + [end] + + [if-any entries.prev] + [if-any entries.changed] + [is roottype "cvs"] +
Changes since [entries.prev]: [entries.changed] lines + [end] + [end] + [end] + + [if-any entries.lockinfo] +
Lock status: [entries.lockinfo] + [end] + + [is roottype "svn"] + [if-any entries.size] +
File length: [entries.size] byte(s) + [end] + + [if-any entries.copy_path] +
Copied from: [entries.copy_path] revision [entries.copy_rev] + [end] + [end] + + [is entries.state "dead"] +
FILE REMOVED + [else] + [is pathtype "file"] + [if-any entries.prev] +
Diff to previous [entries.prev] + [if-any human_readable] + [else] + (colored) + [end] + [end] + + [is roottype "cvs"] + [if-any entries.branch_point] + , to branch point [entries.branch_point] + [if-any human_readable] + [else] + (colored) + [end] + [end] + + [if-any entries.next_main] + , to next main [entries.next_main] + [if-any human_readable] + [else] + (colored) + [end] + [end] + [end] + + [if-any entries.diff_to_sel_href] + [if-any entries.prev], [else]
Diff[end] + to selected [rev_selected] + [if-any human_readable] + [else] + (colored) + [end] + [end] + [end] + [end] + +
[entries.log]
+
+[end] + + +
+ +[is pathtype "file"] + [include "include/diff_form.ezt"] +[end] + +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/markup.ezt b/templates-contrib/newvc/templates/markup.ezt new file mode 100644 index 00000000..af048fdb --- /dev/null +++ b/templates-contrib/newvc/templates/markup.ezt @@ -0,0 +1,18 @@ +[# setup page definitions] + [define page_title]View of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "markup"] +[include "include/fileview.ezt"] + +
+ + +
[markup]
+ +[include "include/props.ezt"] + + +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/query.ezt b/templates-contrib/newvc/templates/query.ezt new file mode 100644 index 00000000..fa33c004 --- /dev/null +++ b/templates-contrib/newvc/templates/query.ezt @@ -0,0 +1,241 @@ + + + + + Checkin Database Query + + + + + +[# setup page definitions] + [define help_href][docroot]/help_query.html[end] +[# end] + +

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

+

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

+

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

+

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

+ +
+ +
+ + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
CVS Repository: + +
CVS Branch: + +
Directory: + +
File: + +
Author: + +
+ +
+ + + + + + + + + +
Sort By: + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Date:
In the last + hours +
In the last day
In the last week
In the last month
Since the beginning of time
+
+ +
+
+ +
+
+ +
+ +[is query "skipped"] +[else] +

[num_commits] matches found.

+ +[if-any commits] + + + + + + + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + + [for commits.files] + + + + + + + + +[# uncommment, if you want a separate Description column: + {if-index commits.files first{ + + {end} + + (substitute brackets for the braces) +] + +[# and also take the following out in the "Description column"-case:] + [if-index commits.files last] + + + + + [end] +[# ---] + [end] + +[end] + + + + + + + + +[# uncommment, if you want a separate Description column: + +] + +
RevisionFileBranch+/-DateAuthorDescription
+ [if-any commits.files.rev][commits.files.rev][else] [end] + [commits.files.link] + [if-any commits.files.branch][commits.files.branch][else] [end] + + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] + + {commits.log} +
 Log:
+
[commits.log]
       
+[end] +[end] +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/query_form.ezt b/templates-contrib/newvc/templates/query_form.ezt new file mode 100644 index 00000000..8060d0ba --- /dev/null +++ b/templates-contrib/newvc/templates/query_form.ezt @@ -0,0 +1,201 @@ +[# setup page definitions] + [define page_title]Query on:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "query"] + +
+ +
+ [for query_hidden_values][end] + + [is roottype "cvs"] + [# For subversion, the branch field is not used ] + + + + + [end] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Branch: + + + + + +
Subdirectory: + + (You can list multiple directories separated by commas.) +
File: + + + + + +
Who: + + + + + +
Comment: + + + + + +
Sort By: + +
Date: + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + hours +
+ + + and + +
+ (use the form yyyy-mm-dd hh:mm:ss) +
+
Limit: + Show at most + + changed files per commit. (Use 0 to show all files.) +
+
+ +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/query_results.ezt b/templates-contrib/newvc/templates/query_results.ezt new file mode 100644 index 00000000..75945b94 --- /dev/null +++ b/templates-contrib/newvc/templates/query_results.ezt @@ -0,0 +1,86 @@ +[# setup page definitions] + [define page_title]Query results in:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt"] + +

[english_query]

+[# ] +

Modify query

+

Show commands which could be used to back out these changes

+ +

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

+ +[if-any commits] + + + + + +[if-any show_branch] + +[end] + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + [for commits.files] + + + + +[if-any show_branch] + +[end] + + + + + [end] + [if-any commits.limited_files] + + + + [end] + + + + + +[end] +
RevisionFileBranch+/-DateAuthorDescription
+ [define rev_href][if-any commits.files.prefer_markup][commits.files.view_href][else][if-any commits.files.download_href][commits.files.download_href][end][end][end] + [if-any commits.files.rev][if-any rev_href][end][commits.files.rev][if-any rev_href][end][else] [end] + + [commits.files.dir]/ + [commits.files.file] + + [if-any commits.files.branch][commits.files.branch][else] [end] + + [# only show a diff link for changes ] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] +
  + Only first [commits.num_files] files shown. + Show all files or + adjust limit. +
 Log:
+
[commits.log]
+[end] + +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/revision.ezt b/templates-contrib/newvc/templates/revision.ezt new file mode 100644 index 00000000..a0cf197c --- /dev/null +++ b/templates-contrib/newvc/templates/revision.ezt @@ -0,0 +1,83 @@ +[# setup page definitions] + [define page_title]Revision [rev] of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "revision"] + +
+ + + + + + + + + + + + + + + + + +
Jump to revision: + [for jump_rev_hidden_values][end] + + + [if-any prev_href] + Previous[end] + [if-any next_href] Next[end] +
Author:[if-any author][author][else](unknown author)[end]
Date:[if-any date][date][else](unknown date)[end] + [if-any ago]([ago] ago)[end]
Log Message:
[log]
+
+ + + +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/roots.ezt b/templates-contrib/newvc/templates/roots.ezt new file mode 100644 index 00000000..300e8fcd --- /dev/null +++ b/templates-contrib/newvc/templates/roots.ezt @@ -0,0 +1,33 @@ +[# setup page definitions] + [define page_title]Repository Listing[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "directory"] + +
+ + + + + + + + + + +[if-any roots] +[for roots] + + + +[end] +[end] + + +
Name
[roots.name]
+ + +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/newvc/templates/rss.ezt b/templates-contrib/newvc/templates/rss.ezt new file mode 100644 index 00000000..7accf6e2 --- /dev/null +++ b/templates-contrib/newvc/templates/rss.ezt @@ -0,0 +1,17 @@ + + + + [rss_link_href] + [rootname] checkins[if-any where] (in [where])[end] + + [is roottype "svn"]Subversion[else]CVS[end] commits to the[if-any where] [where] directory of the[end] [rootname] repository + + [for commits] + [if-any commits.rev][commits.rev]: [end][[commits.author]] [commits.short_log] + [if-any commits.rss_url][commits.rss_url][end] + [commits.author] + [if-any commits.rss_date][commits.rss_date][else](unknown date)[end] + <pre>[format "xml"][commits.log][end]</pre> + [end] + + diff --git a/templates-contrib/tabbed/README b/templates-contrib/tabbed/README new file mode 100644 index 00000000..937d6051 --- /dev/null +++ b/templates-contrib/tabbed/README @@ -0,0 +1,8 @@ +Template Set: tabbed +Author(s): C. Michael Pilato , + Russell Yanofsky +Compatibility: ViewVC 1.1 + +The "tabbed" template set uses top navigation tabs to flip between +various views of a file or directory. + diff --git a/templates-contrib/tabbed/templates/annotate.ezt b/templates-contrib/tabbed/templates/annotate.ezt new file mode 100644 index 00000000..57a8544f --- /dev/null +++ b/templates-contrib/tabbed/templates/annotate.ezt @@ -0,0 +1,46 @@ +[# setup page definitions] + [define page_title]Annotation of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "annotate"] +[include "include/fileview.ezt"] + +
+ + +[define last_rev]0[end] +[define rowclass]vc_row_odd[end] + + + + + + + + +[for lines] + [is lines.rev last_rev] + [else] + [is rowclass "vc_row_even"] + [define rowclass]vc_row_odd[end] + [else] + [define rowclass]vc_row_even[end] + [end] + [end] + + + + + + + + [define last_rev][lines.rev][end] +[end] +
LineUserRevFile contents
[lines.text]
+ +[include "include/props.ezt"] + + +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/diff.ezt b/templates-contrib/tabbed/templates/diff.ezt new file mode 100644 index 00000000..0072bbf7 --- /dev/null +++ b/templates-contrib/tabbed/templates/diff.ezt @@ -0,0 +1,128 @@ +[# Setup page definitions] + [define page_title]Diff of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "diff"] + +
+ +
+ +
+ + +[if-any raw_diff] +
[raw_diff]
+[else] + +[define change_right][end] +[define last_change_type][end] + +[# these should live in stylesheets] + + +[for changes] + [is changes.type "change"][else] + [if-any change_right][change_right][define change_right][end][end] + [end] + [is changes.type "header"] + + + + + [else] + [is changes.type "add"] + + + + + + [else] + [is changes.type "remove"] + + + + + + [else] + [is changes.type "change"] + [if-any changes.have_left] + + + + + + [end] + [define change_right][change_right] + [if-any changes.have_right] + + + + + [end] + [end] + [else] + [is changes.type "no-changes"] + + [else] + [is changes.type "binary-diff"] + + [else] + [is changes.type "error"] + + [else][# a line of context] + + + + + + [end][end][end][end][end][end][end] + [define last_change_type][changes.type][end] +[end] +[if-any change_right][change_right][end] +
# + Line [changes.line_info_left] | + Line [changes.line_info_right] +
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]+[changes.right]
[changes.line_number][changes.left]
[changes.line_number]<[changes.left]
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end]>[changes.right]
- No changes -
- Binary file revisions differ -
- ViewVC depends on rcsdiff and GNU diff + to create this page. ViewVC cannot find GNU diff. Even if you + have GNU diff installed, the rcsdiff program must be configured + and compiled with the GNU diff location. -
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.right]
+ +

Diff Legend

+ + + + + + + + + + + + + + + + + +
Removed lines
+Added lines
<Changed lines
>Changed lines
+ +[end] + + +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/directory.ezt b/templates-contrib/tabbed/templates/directory.ezt new file mode 100644 index 00000000..f9868837 --- /dev/null +++ b/templates-contrib/tabbed/templates/directory.ezt @@ -0,0 +1,132 @@ +[include "include/dir_header.ezt"] + + + + + + + + + + + +[if-any up_href] + + + + + + [end] +[for entries] + + + + [if-any entries.errors] + + [else] + [define view_icon_link][end] + [define graph_icon_link][end] + [define download_icon_link][end] + [define annotate_icon_link][end] + [define log_icon_link][if-any entries.log_href]View Log[end][end] + + [is entries.pathtype "dir"] + [is roottype "cvs"] + [# no point in showing icon when there's only one to choose from] + [else] + [define view_icon_link]View Directory Listing[end] + [end] + [end] + + [is entries.pathtype "file"] + [define view_icon_link][if-any entries.view_href]View File[end][end] + + [define graph_icon_link][if-any entries.graph_href]View Revision Graph[end][end] + + [define download_icon_link][if-any entries.download_href]Download File[end][end] + + [define annotate_icon_link][if-any entries.annotate_href]Annotate File[end][end] + [end] + + + + + [end] + +[end] + + +
+ File + [is sortby "file"] + [is sortdir + [end] + + + Last Change + [is sortby "rev"] + [is sortdir + [end] + +
+ +  Parent Directory +  
+ + + [entries.name][is entries.pathtype "dir"]/[end] + [is entries.state "dead"](dead)[end] + [for entries.errors][entries.errors][end][# Icon column. We might want to add more icons like a tarball + # icon for directories or a diff to previous icon for files. + # Make sure this sucker has no whitespace in it, or the fixed + # widthness of will suffer for large font sizes + ][log_icon_link][view_icon_link][graph_icon_link][download_icon_link][annotate_icon_link] + [if-any entries.rev] + [if-any entries.revision_href][entries.rev][else][entries.rev][end] + ([entries.ago] ago) + by [entries.author]: + [entries.log] + [is entries.pathtype "dir"][is roottype "cvs"] + (from [entries.log_file]/[entries.log_rev]) + [end][end] + [end] +
+ +[include "include/props.ezt"] +[include "include/dir_footer.ezt"] diff --git a/templates-contrib/tabbed/templates/docroot/help.css b/templates-contrib/tabbed/templates/docroot/help.css new file mode 100644 index 00000000..9adde072 --- /dev/null +++ b/templates-contrib/tabbed/templates/docroot/help.css @@ -0,0 +1,14 @@ +/************************************/ +/*** ViewVC Help CSS Stylesheet ***/ +/************************************/ + +/*** Standard Tags ***/ +body { + margin: 0.5em; +} +img { border: none; } + +table { width: 100%; } +td { vertical-align: top; } + +col.menu { width:12em; } diff --git a/templates-contrib/tabbed/templates/docroot/help_dirview.html b/templates-contrib/tabbed/templates/docroot/help_dirview.html new file mode 100644 index 00000000..1b3a3d89 --- /dev/null +++ b/templates-contrib/tabbed/templates/docroot/help_dirview.html @@ -0,0 +1,130 @@ + + + + ViewVC Help: Directory View + + + + + + + + + + +
ViewVC logotype + +

ViewVC Help: Directory View

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

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

+ +

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

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

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

+ +

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

+ +

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

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

ViewVC Help: Log View

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+

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

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

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

+
+
+
ViewVC Users Mailinglist
+ + diff --git a/templates-contrib/tabbed/templates/docroot/help_query.html b/templates-contrib/tabbed/templates/docroot/help_query.html new file mode 100644 index 00000000..af9657e1 --- /dev/null +++ b/templates-contrib/tabbed/templates/docroot/help_query.html @@ -0,0 +1,67 @@ + + + + ViewVC Help: Query The Commit Database + + + + + + + + + + +
ViewVC logotype +

ViewVC Help: Query The Commit Database

+

Other Help:

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

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

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

+

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

+

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

+

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

+
+ diff --git a/templates-contrib/tabbed/templates/docroot/help_rootview.html b/templates-contrib/tabbed/templates/docroot/help_rootview.html new file mode 100644 index 00000000..78e068e9 --- /dev/null +++ b/templates-contrib/tabbed/templates/docroot/help_rootview.html @@ -0,0 +1,169 @@ + + + + ViewVC Help: General + + + + + + + + + + +
ViewVC logotype + +

ViewVC Help: General

+
+

Help

+ General
+ Directory View
+ Log View
+ +

Internet

+ Home
+ Upgrading
+ Contributing
+ License
+
+ +

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

+ + + +

Multiple Repositories

+ +

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

+ +

Sticky Revision and Tag

+ +

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

+ +

Dead Files

+ +

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

+ +

Artificial Tags

+ +

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

+ +

More Information

+ +

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

+ +

Documentation about CVS

+
+

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

+
+ +

Documentation about Subversion

+
+

+ Version Control with + Subversion
+

+
+ +
+
+
ViewVC Users Mailinglist
+ + diff --git a/templates-contrib/tabbed/templates/docroot/images/annotate.png b/templates-contrib/tabbed/templates/docroot/images/annotate.png new file mode 100644 index 00000000..ed2d33b9 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/annotate.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/back.png b/templates-contrib/tabbed/templates/docroot/images/back.png new file mode 100644 index 00000000..65f46318 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/back.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/back_small.png b/templates-contrib/tabbed/templates/docroot/images/back_small.png new file mode 100644 index 00000000..a057c3f8 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/back_small.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/broken.png b/templates-contrib/tabbed/templates/docroot/images/broken.png new file mode 100644 index 00000000..cdaf2362 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/broken.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/chalk.jpg b/templates-contrib/tabbed/templates/docroot/images/chalk.jpg new file mode 100644 index 00000000..73c9533f Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/chalk.jpg differ diff --git a/templates-contrib/tabbed/templates/docroot/images/cvs-logo.png b/templates-contrib/tabbed/templates/docroot/images/cvs-logo.png new file mode 100644 index 00000000..c00e24c4 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/cvs-logo.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/cvsgraph_16x16.png b/templates-contrib/tabbed/templates/docroot/images/cvsgraph_16x16.png new file mode 100644 index 00000000..6f5bece2 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/cvsgraph_16x16.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/cvsgraph_32x32.png b/templates-contrib/tabbed/templates/docroot/images/cvsgraph_32x32.png new file mode 100644 index 00000000..f1ccc45b Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/cvsgraph_32x32.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/diff.png b/templates-contrib/tabbed/templates/docroot/images/diff.png new file mode 100644 index 00000000..9047bfe9 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/diff.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/dir.png b/templates-contrib/tabbed/templates/docroot/images/dir.png new file mode 100644 index 00000000..a11e7eb1 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/dir.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/down.png b/templates-contrib/tabbed/templates/docroot/images/down.png new file mode 100644 index 00000000..5644d63b Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/down.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/download.png b/templates-contrib/tabbed/templates/docroot/images/download.png new file mode 100644 index 00000000..0fbfe435 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/download.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/feed-icon-16x16.jpg b/templates-contrib/tabbed/templates/docroot/images/feed-icon-16x16.jpg new file mode 100644 index 00000000..0c72133f Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/feed-icon-16x16.jpg differ diff --git a/templates-contrib/tabbed/templates/docroot/images/forward.png b/templates-contrib/tabbed/templates/docroot/images/forward.png new file mode 100644 index 00000000..d8185ac9 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/forward.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/list.png b/templates-contrib/tabbed/templates/docroot/images/list.png new file mode 100644 index 00000000..7995fdd5 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/list.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/lock.png b/templates-contrib/tabbed/templates/docroot/images/lock.png new file mode 100644 index 00000000..9e3bf42a Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/lock.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/log.png b/templates-contrib/tabbed/templates/docroot/images/log.png new file mode 100644 index 00000000..d2da45b5 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/log.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/svn-logo.png b/templates-contrib/tabbed/templates/docroot/images/svn-logo.png new file mode 100644 index 00000000..eaa0766a Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/svn-logo.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/text.png b/templates-contrib/tabbed/templates/docroot/images/text.png new file mode 100644 index 00000000..6e050cd6 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/text.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/up.png b/templates-contrib/tabbed/templates/docroot/images/up.png new file mode 100644 index 00000000..625819f9 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/up.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/view.png b/templates-contrib/tabbed/templates/docroot/images/view.png new file mode 100644 index 00000000..a168c38f Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/view.png differ diff --git a/templates-contrib/tabbed/templates/docroot/images/viewvc-logo.png b/templates-contrib/tabbed/templates/docroot/images/viewvc-logo.png new file mode 100644 index 00000000..29044fb6 Binary files /dev/null and b/templates-contrib/tabbed/templates/docroot/images/viewvc-logo.png differ diff --git a/templates-contrib/tabbed/templates/docroot/styles.css b/templates-contrib/tabbed/templates/docroot/styles.css new file mode 100644 index 00000000..637fdc25 --- /dev/null +++ b/templates-contrib/tabbed/templates/docroot/styles.css @@ -0,0 +1,248 @@ +/*******************************/ +/*** ViewVC CSS Stylesheet ***/ +/*******************************/ + +/*** Standard Tags ***/ +html, body { + background-color: rgb(180,193,205); + color: black; + font-family: arial, sans-serif; + font-size: 90%; + margin: 5px; +} + +a { text-decoration: none; color: rgb(30%,30%,60%); } +img { border: none; } +table { + width: 100%; + margin: 0; + border: none; +} +tr, td, th { vertical-align: top; } +th { white-space: nowrap; } +table.auto { + width: auto; +} +table.fixed { + width: 100%; + table-layout: fixed; +} +table.fixed td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +form { margin: 0; } + + +/*** Icons ***/ +.vc_icon { + width: 16px; + height: 16px; + border: none; + padding: 0 1px; +} + +#vc_root_select_form { + display: inline; +} + +#vc_current_path { + padding: 5px 0; + font-size: larger; +} + +#vc_view_selection_group { + margin: 20px 0 5px 0; +} + +#vc_main { + border: 1px solid black; + border-right-width: 2px; + border-bottom-width: 2px; + background-color: rgb(250,255,210); + padding: 5px 0; +} + +#vc_main_body { + border: 1px solid black; + border-width: 1px 0; + background: white; + padding: 5px 5px 20px 5px; +} + +.vc_view_link, .vc_view_link_this { + border: 1px solid black; + border-right-width: 2px; + display: inline; + padding: 5px; + margin: 0 2px 0 0; +} + +.vc_view_link { + background-color: rgb(210,215,175); +} + +.vc_view_link:hover { + background-color: rgb(225,230,190); +} + +.vc_view_link_this { + background-color: rgb(250,255,210); + border-bottom: 1px solid rgb(250,255,210); +} + +.vc_view_link a:hover, .vc_view_link_this a:hover { + text-decoration: none; +} + +/*** Table Headers ***/ +.vc_header { + text-align: left; + vertical-align: top; + background-color: #cccccc; + border-bottom: 1px solid black; +} +.vc_header_sort { + text-align: left; + background-color: #88ff88; + border-bottom: 1px solid black; +} + + +/*** Table Rows ***/ +.vc_row_even { + background-color: rgb(95%,95%,95%); +} +.vc_row_odd { + background-color: rgb(90%,90%,90%); +} +.vc_row_even:hover, .vc_row_odd:hover { + background-color: white; +} + +/*** Log messages ***/ +.vc_log { + /* unfortunately, white-space: pre-wrap isn't widely supported ... */ + white-space: -moz-pre-wrap; /* Mozilla based browsers */ + white-space: -pre-wrap; /* Opera 4 - 6 */ + white-space: -o-pre-wrap; /* Opera >= 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ +} + + +/*** Properties Listing ***/ +.vc_properties { + margin: 1em 0; +} +.vc_properties h2 { + font-size: 115%; +} + + +/*** Markup Summary Header ***/ +.vc_summary { + background-color: #eeeeee; +} + +/*** Highlight Markup Styles ***/ +#vc_markup .num { color: #000000; } +#vc_markup .esc { color: #bd8d8b; } +#vc_markup .str { color: #bd8d8b; } +#vc_markup .dstr { color: #bd8d8b; } +#vc_markup .slc { color: #ac2020; font-style: italic; } +#vc_markup .com { color: #ac2020; font-style: italic; } +#vc_markup .dir { color: #000000; } +#vc_markup .sym { color: #000000; } +#vc_markup .line { color: #555555; } +#vc_markup .kwa { color: #9c20ee; font-weight: bold; } +#vc_markup .kwb { color: #208920; } +#vc_markup .kwc { color: #0000ff; } +#vc_markup .kwd { color: #404040; } + +/*** Py2html Markup Styles ***/ +#vc_markup .PY_STRING { color: #bd8d8b; } +#vc_markup .PY_COMMENT { color: #ac2020; font-style: italic; } +#vc_markup .PY_KEYWORD { color: #9c20ee; font-weight: bold; } +#vc_markup .PY_IDENTIFIER { color: #404040; } + +/*** Line numbers outputted by highlight colorizer ***/ +.line { + border-right-width: 1px; + border-right-style: solid; + border-right-color: #505050; + padding: 1px; + background-color: #eeeeee; + color: #505050; + text-decoration: none; + font-weight: normal; + font-style: normal; +} + +/*** Diff Styles ***/ +.vc_diff_plusminus { width: 1em; } +.vc_diff_remove, .vc_diff_add, .vc_diff_changes1, .vc_diff_changes2 { + font-family: monospace; + white-space: pre; +} +.vc_diff_remove { background: rgb(100%,60%,60%); } +.vc_diff_add { background: rgb(60%,100%,60%); } +.vc_diff_changes1 { background: rgb(100%,100%,70%); color: rgb(50%,50%,50%); text-decoration: line-through; } +.vc_diff_changes2 { background: rgb(100%,100%,0%); } + +.vc_diff_nochange, .vc_diff_binary, .vc_diff_error { + font-family: sans-serif; + font-size: smaller; +} + +/*** Intraline Diff Styles ***/ + +.vc_idiff_add { + background-color: #aaffaa; +} +.vc_idiff_change { + background-color:#ffff77; +} +.vc_idiff_remove { + background-color:#ffaaaa; +} +.vc_idiff_empty { + background-color:#e0e0e0; +} + +table.vc_idiff col.content { + width: 50%; +} +table.vc_idiff tbody { + font-family: monospace; + /* unfortunately, white-space: pre-wrap isn't widely supported ... */ + white-space: -moz-pre-wrap; /* Mozilla based browsers */ + white-space: -pre-wrap; /* Opera 4 - 6 */ + white-space: -o-pre-wrap; /* Opera >= 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ +} +table.vc_idiff tbody th { + background-color:#e0e0e0; + text-align:right; +} + +/*** Annotate Styles ***/ +.vc_blame_metadata { + border-bottom: 1px dotted rgb(80%,80%,80%); + padding-right: 2em; + font-family: monospace; + text-align: right; +} +.vc_blame_textdata { + border-bottom: 1px dotted rgb(80%,80%,80%); + border-left: 1px solid black; + font-family: monospace; + text-align: left; + padding-left: 1em; +} + +/*** Query Form ***/ +.vc_query_form { +} diff --git a/templates-contrib/tabbed/templates/error.ezt b/templates-contrib/tabbed/templates/error.ezt new file mode 100644 index 00000000..e1d61af7 --- /dev/null +++ b/templates-contrib/tabbed/templates/error.ezt @@ -0,0 +1,51 @@ + + + + +ViewVC Exception + + +

An Exception Has Occurred

+ +[if-any msg] +

[msg]

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

HTTP Response Status

+

[status]

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

Python Traceback

+

+[stacktrace]
+

+[end] + +[# Here follows a bunch of space characters, present to ensure that + our error message is larger than 512 bytes so that IE's "Friendly + Error Message" won't show. For more information, see + http://oreillynet.com/onjava/blog/2002/09/internet_explorer_subverts_err.html] + + + + + + + + + + + + + + + + + + + + diff --git a/templates-contrib/tabbed/templates/graph.ezt b/templates-contrib/tabbed/templates/graph.ezt new file mode 100644 index 00000000..b2df4021 --- /dev/null +++ b/templates-contrib/tabbed/templates/graph.ezt @@ -0,0 +1,18 @@ +[# setup page definitions] + [define page_title]Graph of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "graph"] +[include "include/file_header.ezt"] + +
+ +
+[imagemap] +Revisions of [where] +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/include/diff_form.ezt b/templates-contrib/tabbed/templates/include/diff_form.ezt new file mode 100644 index 00000000..239e95d8 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/diff_form.ezt @@ -0,0 +1,70 @@ + +
+

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

+ +
+ + + + + + + + + + +
  + [for diff_select_hidden_values][end] + Diffs between +[if-any tags] + + +[else] + +[end] + + and +[if-any tags] + + +[else] + +[end] +
  + Type of Diff should be a + + +
+
+
diff --git a/templates-contrib/tabbed/templates/include/dir_footer.ezt b/templates-contrib/tabbed/templates/include/dir_footer.ezt new file mode 100644 index 00000000..12ab1097 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/dir_footer.ezt @@ -0,0 +1,35 @@ + +
+ +[if-any search_re_form] + [# this table holds the selectors on the left, and reset on the right ] + + + + + + [if-any search_re] + + + + + [end] +
Show files containing the regular expression: +
+
+ [for search_re_hidden_values][end] + + +
+
+
  +
+
+ [for search_tag_hidden_values][end] + +
+
+
+[end] + +[include "footer.ezt"] diff --git a/templates-contrib/tabbed/templates/include/dir_header.ezt b/templates-contrib/tabbed/templates/include/dir_header.ezt new file mode 100644 index 00000000..966cecf8 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/dir_header.ezt @@ -0,0 +1,63 @@ +[# setup page definitions] + [define page_title]Index of:[end] + [define help_href][docroot]/help_[if-any where]dir[else]root[end]view.html[end] +[# end] +[include "header.ezt" "directory"] + +[if-any where][else] + +[end] + + + + +[is roottype "svn"] + + + + +[end] + + + + +[if-any search_re] + +[end] + +[if-any queryform_href] + + + + +[end] + +[is cfg.options.use_pagesize "0"][else][is picklist_len "1"][else] + + + + +[end][end] + +
Files shown:[files_shown] +[is num_dead "0"] +[else] + [if-any attic_showing] + (Hide [num_dead] dead files) + [else] + (Show [num_dead] dead files) + [end] +[end] +
Directory revision:[tree_rev][if-any youngest_rev] (of [youngest_rev])[end]
Sticky [is roottype "cvs"]Tag[else]Revision[end]:[include "pathrev_form.ezt"]
Current search:[search_re]
Query:Query revision history
Jump to page:
+ [for dir_paging_hidden_values][end] + + +
+
+ +
+ diff --git a/templates-contrib/tabbed/templates/include/file_header.ezt b/templates-contrib/tabbed/templates/include/file_header.ezt new file mode 100644 index 00000000..c4180a73 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/file_header.ezt @@ -0,0 +1,16 @@ +

+[is pathtype "file"] +Parent Directory Parent Directory +[if-any log_href] + | Revision Log Revision Log +[end] +[if-any graph_href] + | View Revision Graph Revision Graph +[end] +[is view "diff"] + | View Patch Patch +[end] +[else] +View Directory Listing Directory Listing +[end] +

diff --git a/templates-contrib/tabbed/templates/include/fileview.ezt b/templates-contrib/tabbed/templates/include/fileview.ezt new file mode 100644 index 00000000..e774ea3e --- /dev/null +++ b/templates-contrib/tabbed/templates/include/fileview.ezt @@ -0,0 +1,62 @@ + + + + + + + + + + +[if-any orig_path] + + + + +[end] +[if-any branches] + + + + +[end] +[if-any tags] + + + + +[end] +[if-any branch_points] + + + + +[end] +[is roottype "cvs"][if-any changed] + + + + +[end][end] +[is roottype "svn"][if-any size] + + + +[end][end] +[if-any lockinfo] + + +[end] +[is state "dead"] + + + + +[end] +[if-any log] + + + + +[end] +
Revision:[if-any revision_href][rev][else][rev][end] [if-any vendor_branch] (vendor branch)[end]
Committed:[if-any date][date] [end][if-any ago]([ago] ago) [end][if-any author]by [author][end]
Original Path:[orig_path]
Branch:[branches]
CVS Tags:[tags]
Branch point for:[branch_points]
Changes since [prev]:[changed] lines
File size:[size] byte(s)
Lock status:Locked [lockinfo]
State:FILE REMOVED
Log Message:
[log]
diff --git a/templates-contrib/tabbed/templates/include/footer.ezt b/templates-contrib/tabbed/templates/include/footer.ezt new file mode 100644 index 00000000..109c530f --- /dev/null +++ b/templates-contrib/tabbed/templates/include/footer.ezt @@ -0,0 +1,17 @@ +
+ +[# standard footer used by all ViewVC pages ] + + + + + + + + + + +
[if-any cfg.general.address]
[cfg.general.address]
[else] [end]
ViewVC Help
Powered by ViewVC [vsn][if-any rss_href]RSS 2.0 feed[else] [end]
+ + + diff --git a/templates-contrib/tabbed/templates/include/header.ezt b/templates-contrib/tabbed/templates/include/header.ezt new file mode 100644 index 00000000..4588f656 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/header.ezt @@ -0,0 +1,80 @@ + + + + + + + [[]ViewVC] [page_title] [if-any rootname][rootname][if-any where]/[where][end][end] + + + [if-any rss_href] + + [end] + + + + +
+ +ViewVC logotype +
+ +
+[page_title] +[if-any nav_path] + [if-any roots_href]/[else]/[end] + [for nav_path] + [if-any nav_path.href][end] + [nav_path.name][if-any nav_path.href][end] + [if-index nav_path last][else]/[end] + [end] +[end] +
+ +
+[is pathtype "dir"] + + [if-any revision_href] + + [end] + [if-any queryform_href] + + [end] + [if-any tarball_href] + + [end] +[end] + +[is pathtype "file"] +
+ View File +
+ + + [if-any graph_href] + + [end] + +[end] +
+ +
diff --git a/templates-contrib/tabbed/templates/include/log_footer.ezt b/templates-contrib/tabbed/templates/include/log_footer.ezt new file mode 100644 index 00000000..959909a2 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/log_footer.ezt @@ -0,0 +1,9 @@ + +
+ +[is pathtype "file"] + [include "diff_form.ezt"] +[end] + +[include "footer.ezt"] + diff --git a/templates-contrib/tabbed/templates/include/log_header.ezt b/templates-contrib/tabbed/templates/include/log_header.ezt new file mode 100644 index 00000000..9d129079 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/log_header.ezt @@ -0,0 +1,89 @@ +[# setup page definitions] + [define page_title]Log of:[end] + [define help_href][docroot]/help_log.html[end] +[# end] +[include "header.ezt" "log"] + + + +[if-any default_branch] + + + + +[end] + +[is pathtype "file"] +[if-any view_href] + + + + +[end] + +[if-any tag_view_href] + + + + +[end] +[end] + + + + + + +[is cfg.options.use_pagesize "0"][else][is picklist_len "1"][else] + + + + +[end][end] + + + + + + +
Default branch:[for default_branch][default_branch.name][if-index default_branch last][else], [end] +[end]
Links to HEAD: + (view) + [if-any download_href](download)[end] + [if-any download_text_href](as text)[end] + [if-any annotate_href](annotate)[end] +
Links to [pathrev]: + (view) + [if-any tag_download_href](download)[end] + [if-any tag_download_text_href](as text)[end] + [if-any tag_annotate_href](annotate)[end] +
Sticky [is roottype "cvs"]Tag[else]Revision[end]:[include "pathrev_form.ezt"]
Jump to page:
+ [for log_paging_hidden_values][end] + + +
+
Sort logs by:
+
+ + [for logsort_hidden_values][end] + + +
+
+
+ +
+ + diff --git a/templates-contrib/tabbed/templates/include/pathrev_form.ezt b/templates-contrib/tabbed/templates/include/pathrev_form.ezt new file mode 100644 index 00000000..33ff9618 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/pathrev_form.ezt @@ -0,0 +1,53 @@ +
+
+[for pathrev_hidden_values][end] +[is roottype "cvs"] + [define pathrev_selected][pathrev][end] + +[else] + +[end] + +
+
+ +[if-any pathrev] +
+
+[for pathrev_clear_hidden_values][end] +[if-any lastrev] + [is pathrev lastrev][else][end] + (Current path doesn't exist after revision [lastrev]) +[else] + +[end] +
+
+[end] diff --git a/templates-contrib/tabbed/templates/include/props.ezt b/templates-contrib/tabbed/templates/include/props.ezt new file mode 100644 index 00000000..aa7fd490 --- /dev/null +++ b/templates-contrib/tabbed/templates/include/props.ezt @@ -0,0 +1,26 @@ +[if-any properties] +
+
+

Properties

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

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

+

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

+

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

+

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

+ +
+ +
+ + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
CVS Repository: + +
CVS Branch: + +
Directory: + +
File: + +
Author: + +
+ +
+ + + + + + + + + +
Sort By: + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Date:
In the last + hours +
In the last day
In the last week
In the last month
Since the beginning of time
+
+ +
+
+ +
+
+ +
+ +[is query "skipped"] +[else] +

[num_commits] matches found.

+ +[if-any commits] + + + + + + + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + + [for commits.files] + + + + + + + + +[# uncommment, if you want a separate Description column: + {if-index commits.files first{ + + {end} + + (substitute brackets for the braces) +] + +[# and also take the following out in the "Description column"-case:] + [if-index commits.files last] + + + + + [end] +[# ---] + [end] + +[end] + + + + + + + + +[# uncommment, if you want a separate Description column: + +] + +
RevisionFileBranch+/-DateAuthorDescription
+ [if-any commits.files.rev][commits.files.rev][else] [end] + [commits.files.link] + [if-any commits.files.branch][commits.files.branch][else] [end] + + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] + + {commits.log} +
 Log:
+
[commits.log]
       
+[end] +[end] +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/query_form.ezt b/templates-contrib/tabbed/templates/query_form.ezt new file mode 100644 index 00000000..8060d0ba --- /dev/null +++ b/templates-contrib/tabbed/templates/query_form.ezt @@ -0,0 +1,201 @@ +[# setup page definitions] + [define page_title]Query on:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "query"] + +
+ +
+ [for query_hidden_values][end] + + [is roottype "cvs"] + [# For subversion, the branch field is not used ] + + + + + [end] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Branch: + + + + + +
Subdirectory: + + (You can list multiple directories separated by commas.) +
File: + + + + + +
Who: + + + + + +
Comment: + + + + + +
Sort By: + +
Date: + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + hours +
+ + + and + +
+ (use the form yyyy-mm-dd hh:mm:ss) +
+
Limit: + Show at most + + changed files per commit. (Use 0 to show all files.) +
+
+ +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/query_results.ezt b/templates-contrib/tabbed/templates/query_results.ezt new file mode 100644 index 00000000..75945b94 --- /dev/null +++ b/templates-contrib/tabbed/templates/query_results.ezt @@ -0,0 +1,86 @@ +[# setup page definitions] + [define page_title]Query results in:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt"] + +

[english_query]

+[# ] +

Modify query

+

Show commands which could be used to back out these changes

+ +

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

+ +[if-any commits] + + + + + +[if-any show_branch] + +[end] + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + [for commits.files] + + + + +[if-any show_branch] + +[end] + + + + + [end] + [if-any commits.limited_files] + + + + [end] + + + + + +[end] +
RevisionFileBranch+/-DateAuthorDescription
+ [define rev_href][if-any commits.files.prefer_markup][commits.files.view_href][else][if-any commits.files.download_href][commits.files.download_href][end][end][end] + [if-any commits.files.rev][if-any rev_href][end][commits.files.rev][if-any rev_href][end][else] [end] + + [commits.files.dir]/ + [commits.files.file] + + [if-any commits.files.branch][commits.files.branch][else] [end] + + [# only show a diff link for changes ] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] +
  + Only first [commits.num_files] files shown. + Show all files or + adjust limit. +
 Log:
+
[commits.log]
+[end] + +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/revision.ezt b/templates-contrib/tabbed/templates/revision.ezt new file mode 100644 index 00000000..a0cf197c --- /dev/null +++ b/templates-contrib/tabbed/templates/revision.ezt @@ -0,0 +1,83 @@ +[# setup page definitions] + [define page_title]Revision [rev] of:[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] +[include "include/header.ezt" "revision"] + +
+ + + + + + + + + + + + + + + + + +
Jump to revision: + [for jump_rev_hidden_values][end] + + + [if-any prev_href] + Previous[end] + [if-any next_href] Next[end] +
Author:[if-any author][author][else](unknown author)[end]
Date:[if-any date][date][else](unknown date)[end] + [if-any ago]([ago] ago)[end]
Log Message:
[log]
+
+ + + +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/roots.ezt b/templates-contrib/tabbed/templates/roots.ezt new file mode 100644 index 00000000..0fe9a53d --- /dev/null +++ b/templates-contrib/tabbed/templates/roots.ezt @@ -0,0 +1,31 @@ +[# setup page definitions] + [define page_title]Repository Listing[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "directory"] + + + + + + + + + +[if-any roots] + [for roots] + + + + [end] +[end] + + +
Name
+ + + [roots.name] +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/tabbed/templates/rss.ezt b/templates-contrib/tabbed/templates/rss.ezt new file mode 100644 index 00000000..7accf6e2 --- /dev/null +++ b/templates-contrib/tabbed/templates/rss.ezt @@ -0,0 +1,17 @@ + + + + [rss_link_href] + [rootname] checkins[if-any where] (in [where])[end] + + [is roottype "svn"]Subversion[else]CVS[end] commits to the[if-any where] [where] directory of the[end] [rootname] repository + + [for commits] + [if-any commits.rev][commits.rev]: [end][[commits.author]] [commits.short_log] + [if-any commits.rss_url][commits.rss_url][end] + [commits.author] + [if-any commits.rss_date][commits.rss_date][else](unknown date)[end] + <pre>[format "xml"][commits.log][end]</pre> + [end] + + diff --git a/templates-contrib/viewsvn/INSTALL b/templates-contrib/viewsvn/INSTALL new file mode 100644 index 00000000..2a5368c6 --- /dev/null +++ b/templates-contrib/viewsvn/INSTALL @@ -0,0 +1,132 @@ +I. BASIC INSTALLATION + + +Let ViewVC use the viewsvn templates folder as templates. You can either delete the original +templates folder in the ViewVC installation directory and copy the one from this archive to +the ViewVC installation directory or copy the templates folder from this archive with a different +name to the ViewVC installation directory and change the template_dir option in viewvc.conf + +template_dir = templates-viewsvn + +Leave the section [templates] as is, thus do not override any templates. + + + + +II. TWEAKS + + +1. Set your own favicon + +Modify include/header.ezt, line 9, to include your favicon. + + +2. Let your own logo appear in the upper right area and link to your homepage. + +Modify include/header.ezt, line 78, to include your link and logo. + + +3. Enable TortoiseSVN checkout links. + +If you are certain that most of your guest use TortoiseSVN you can give them the ability +to use TortoiseSVN checkout links +(see http://tortoisesvn.net/docs/release/TortoiseSVN_en/tsvn-repository-links.html). This wil +only work if all your repositories share the same base location, e.g. http://svn.server/svn/. +This is mostly the case if you are using the mod_dav_svn SVNParentPath directive. + +Modify include/dir_footer.ezt, line 44. Enable this section and configure your subversion location. + + + + +III. EXTRAS + + +1. fix-blame-output.diff + +If you are running ViewVC 1.0 and you are suffering from a broken blame/annotate output because of +long author names you can apply this patch. Note that Subversion 1.3.1 or higher is required. + + +2. svnindex.xsl & svnindex.css + +If you are using mod_dav_svn to host your repository you can teach it to use a stylesheet if you are +browsing the repository directly. Take a look at the SVNIndexXSLT directive for more information. +You can use the .xsl and .css from the extras folder to get a look & feel that is similiar to this +ViewVC template. Note that these stylesheets knows nothing about ViewVC. You have to teach +svnindex.xsl where the [docroot] folder exists. If it is /docroot you have to change nothing, +otherwise edit line 68, 82 and 97. + + + + +IV. EXAMPLE VIEWVC.CONF + + +The following viewvc.conf setting were used to create the templates, do not copy these blindly, +just check them if something seems wrong: + +[general] +root_parents = [REPOSPATH] : svn +use_rcsparse = 0 +mime_types_file = [PATHTO]\mime.types +address =
Admin +forbidden = +kv_files = +languages = en-us +[options] +root_as_url_component = 0 +default_file_view = log +checkout_magic = 0 +http_expiration_time = 600 +generate_etags = 1 +sort_by = file +sort_group_dirs = 1 +hide_attic = 1 +log_sort = date +diff_format = h +hide_cvsroot = 1 +hr_breakable = 1 +hr_funout = 0 +hr_ignore_white = 1 +hr_ignore_keyword_subst = 1 +hr_intraline = 0 +allow_annotate = 1 +allow_markup = 1 +allow_compress = 1 +template_dir = templates-viewsvn +docroot = [PATHTO]/docroot +show_subdir_lastmod = 0 +show_logs = 1 +show_log_in_markup = 1 +cross_copies = 0 +use_localtime = 1 +py2html_path = . +short_log_len = 80 +use_enscript = 0 +enscript_path = +use_highlight = 1 +highlight_path = [PATH] +highlight_line_numbers = 1 +highlight_convert_tabs = 2 +use_php = 0 +php_exe_path = php +allow_tar = 0 +use_cvsgraph = 0 +cvsgraph_path = +cvsgraph_conf = cvsgraph.conf +use_re_search = 0 +use_pagesize = 200 +limit_changes = 100 +[templates] +[cvsdb] +enabled = 1 +host = localhost +port = 3306 +database_name = ViewVC +user = [UID] +passwd = [PWD] +readonly_user = [UID] +readonly_passwd = [PWD] +#row_limit = 1000 +[vhosts] diff --git a/templates-contrib/viewsvn/README b/templates-contrib/viewsvn/README new file mode 100644 index 00000000..6bc13bef --- /dev/null +++ b/templates-contrib/viewsvn/README @@ -0,0 +1,52 @@ +I. INTRODUCTION + + +ViewVC is a Subversion and CVS webbased repository viewer. Templates-viewsvn is +a set of templates for a different look & feel. + +Warning: +Do not use this template set if you plan to view cvs repositories through ViewVC! +This template-set is heavily trimmed for Subversion needs. Special CVS features are +untested, possibly disabled or broken. + +Please report enhancemants back to the ViewVC project or to me. +jpeters7677@gmx.de + + + + +II. COMPATABILITY + + +This template is compatible with ViewVC 1.0.x + + + + +III. CREDITS + + +Stylesheet color codes: +Subversion Mailing List Archive +http://svn.haxx.se/ + +Images: +All images in templates/docroot/images/tortoisesvn are licensed under the +ToirtoiseSVN Icon license. See license.txt in this folder for more details. + + + + +IV. LINKS + + +ViewVC: +http://www.viewvc.org/ + +Subversion: +http://subversion.tigris.org/ + +TortoiseSVN: +http://tortoisesvn.net/ + + diff --git a/templates-contrib/viewsvn/extras/svnindex.css b/templates-contrib/viewsvn/extras/svnindex.css new file mode 100644 index 00000000..87fdedb9 --- /dev/null +++ b/templates-contrib/viewsvn/extras/svnindex.css @@ -0,0 +1,76 @@ + +html, body { + color: #000000; + background-color: #ffffff; + font-family: arial,helvetica,sans-serif; + font-size: 12px; +} + +body{ + margin: 0; + padding: 0; +} + +a { text-decoration:none;} +a:hover { text-decoration:underline;} +a:link { color: #0000ff; } +a:visited { color: #880088; } +a:active { color: #0000ff; } + +.img a:hover { text-decoration:none;} + +img { + border: none; +} + +.footer { + margin-top: 8em; + padding: 0.5em 1em 0.5em; + clear: both; + font-size: smaller; + font-style: italic; +} + +.svn { + margin: 3em; +} + +.rev { + margin-right: 3px; + margin-bottom: 15px; + padding-left: 3px; + + text-align: left; + font-size: 24px; + font-weight: bold; + color: #000066 +} + +.path { + margin: 3px; + padding: 3px; + background: #e0e0ff; +font-weight: bold; +} + +.updir { + margin: 3px; + padding: 3px; + margin-left: 3em; + background: #eeeeee; +} + +.file { + margin: 3px; + padding: 3px; + margin-left: 3em; + background: #eeeeee; +} + +.dir { + margin: 3px; + padding: 3px; + margin-left: 3em; + background: #eeeeee; +} + diff --git a/templates-contrib/viewsvn/extras/svnindex.xsl b/templates-contrib/viewsvn/extras/svnindex.xsl new file mode 100644 index 00000000..1821e987 --- /dev/null +++ b/templates-contrib/viewsvn/extras/svnindex.xsl @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + <xsl:if test="string-length(index/@name) != 0"> + <xsl:value-of select="index/@name"/> + <xsl:text>: </xsl:text> + </xsl:if> + <xsl:value-of select="index/@path"/> + + + + + +
+ +
+ + + +
+ + +
+ + + + + + + + Revision + + +
+
+ +
+ + + +
+ + +
+ Parent Directory + + [ + + .. + Parent Directory + + ] +
+ +
+ + +
+ {@name} + + + + + + + / + +
+ +
+ + +
+ {@name} + + + + + + + +
+ +
+ +
diff --git a/templates-contrib/viewsvn/screenshots/diff.png b/templates-contrib/viewsvn/screenshots/diff.png new file mode 100644 index 00000000..63225b16 Binary files /dev/null and b/templates-contrib/viewsvn/screenshots/diff.png differ diff --git a/templates-contrib/viewsvn/screenshots/dir.png b/templates-contrib/viewsvn/screenshots/dir.png new file mode 100644 index 00000000..97330616 Binary files /dev/null and b/templates-contrib/viewsvn/screenshots/dir.png differ diff --git a/templates-contrib/viewsvn/screenshots/log.png b/templates-contrib/viewsvn/screenshots/log.png new file mode 100644 index 00000000..8db92f99 Binary files /dev/null and b/templates-contrib/viewsvn/screenshots/log.png differ diff --git a/templates-contrib/viewsvn/screenshots/markup.png b/templates-contrib/viewsvn/screenshots/markup.png new file mode 100644 index 00000000..0f2bca94 Binary files /dev/null and b/templates-contrib/viewsvn/screenshots/markup.png differ diff --git a/templates-contrib/viewsvn/templates/diff.ezt b/templates-contrib/viewsvn/templates/diff.ezt new file mode 100644 index 00000000..6b59f93e --- /dev/null +++ b/templates-contrib/viewsvn/templates/diff.ezt @@ -0,0 +1,234 @@ +[# setup page definitions] + [define page_title]Diff of /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "diff"] +[include "include/file_header.ezt"] + +

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

+ - No changes -
 
 

+ - Binary file revisions differ -
 
 

+ - ViewVC depends on rcsdiff and GNU diff to create + this page. ViewVC cannot find GNU diff. Even if you + have GNU diff installed, the rcsdiff program must be + configured and compiled with the GNU diff location. + -
 
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.left] [changes.right]
+[end] + +[if-any sidebyside] + + + + + + + + + + + [for sidebyside] + [if-any sidebyside.gap] + + + + + [end] + + [for sidebyside.columns] + [for sidebyside.columns.segments][if-any sidebyside.columns.segments.type][sidebyside.columns.segments.text][else][sidebyside.columns.segments.text][end][end] + [end] + + [end] + +
+ [is left.path right.path][else][left.path][end] + Revision [left.rev] + + [is left.path right.path][else][right.path][end] + Revision [right.rev] +
[sidebyside.columns.line_number]
+[end] + +[if-any unified] + + + + + + + + + + [for unified] + [if-any unified.gap] + + + + + + [end] + + + + [for unified.segments][if-any unified.segments.type][unified.segments.text][else][unified.segments.text][end][end] + + [end] + +
r[left.rev]r[right.rev]
[unified.left_number][unified.right_number]
+[end] + +
+ + + + + + +
+
+
+ [for diff_format_hidden_values][end] + + +
+
+
+[if-any raw_diff] +   +[else] + + + + +
Legend:
+ + + + + + + + + + + + +
Removed from v.[left.rev] 
changed lines
 Added in v.[right.rev]
+
+[end] +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/directory.ezt b/templates-contrib/viewsvn/templates/directory.ezt new file mode 100644 index 00000000..f13bee75 --- /dev/null +++ b/templates-contrib/viewsvn/templates/directory.ezt @@ -0,0 +1,136 @@ +[include "include/dir_header.ezt"] + + + + + + + + +[is cfg.options.show_logs "1"] + +[end] + + + + +[if-any up_href] + + + + + + [is cfg.options.show_logs "1"] + + [end] + + [end] + [for entries] + + + [if-any entries.errors] + + [else] + [is entries.pathtype "dir"] + + [else] + + [end] + + + [is cfg.options.show_logs "1"] + [if-any entries.log] + + [else] + + [end] + [end] + [end] + +[end] + + +
+ Name + + [is sortby "file"] + [is sortdir + [end] + + + Rev. + + [is sortby "rev"] + [is sortdir + [end] + + + Age + + [is sortby "date"] + [is sortdir + [end] + + + Author + + [is sortby "author"] + [is sortdir + [end] + + + Last log entry + + [is sortby "log"] + [is sortdir + [end] + +
+ + + + + +
Parent Directory
+
    
+ + + + + + +
+ [is entries.pathtype "dir"] + + View directory contents + + [else] + [if-any entries.view_href][end] + View file contents + [if-any entries.view_href][end] + [end] + + [is entries.pathtype "dir"] + [entries.name] + [else] + [if-any entries.view_href][end] + [entries.name] + [if-any entries.view_href][end] + [end] +
+ +
+ [for entries.errors][entries.errors][end] +  [if-any entries.rev][entries.rev][end] [if-any entries.rev][entries.rev][end] [entries.ago] [entries.author] [entries.log] 
+ +[include "include/dir_footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/docroot/images/broken.png b/templates-contrib/viewsvn/templates/docroot/images/broken.png new file mode 100644 index 00000000..cdaf2362 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/broken.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/dir.png b/templates-contrib/viewsvn/templates/docroot/images/dir.png new file mode 100644 index 00000000..d14d84ac Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/dir.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/down.png b/templates-contrib/viewsvn/templates/docroot/images/down.png new file mode 100644 index 00000000..5644d63b Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/down.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/download.png b/templates-contrib/viewsvn/templates/docroot/images/download.png new file mode 100644 index 00000000..0fbfe435 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/download.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/favicon-svn.ico b/templates-contrib/viewsvn/templates/docroot/images/favicon-svn.ico new file mode 100644 index 00000000..aed45c92 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/favicon-svn.ico differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/feed-icon-16x16.jpg b/templates-contrib/viewsvn/templates/docroot/images/feed-icon-16x16.jpg new file mode 100644 index 00000000..0c72133f Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/feed-icon-16x16.jpg differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/logo-svn.png b/templates-contrib/viewsvn/templates/docroot/images/logo-svn.png new file mode 100644 index 00000000..0de85f39 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/logo-svn.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/logo-viewvc.png b/templates-contrib/viewsvn/templates/docroot/images/logo-viewvc.png new file mode 100644 index 00000000..c4a9d3aa Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/logo-viewvc.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/svn.png b/templates-contrib/viewsvn/templates/docroot/images/svn.png new file mode 100644 index 00000000..18a327c9 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/svn.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/text.png b/templates-contrib/viewsvn/templates/docroot/images/text.png new file mode 100644 index 00000000..d80a7424 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/text.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/back.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/back.png new file mode 100644 index 00000000..837c92d3 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/back.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/back_small.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/back_small.png new file mode 100644 index 00000000..837c92d3 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/back_small.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/blame.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/blame.png new file mode 100644 index 00000000..a3d07522 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/blame.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/checkout.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/checkout.png new file mode 100644 index 00000000..62d16951 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/checkout.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/diff.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/diff.png new file mode 100644 index 00000000..f0d3f7a1 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/diff.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/forward.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/forward.png new file mode 100644 index 00000000..d0724c8a Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/forward.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/license.txt b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/license.txt new file mode 100644 index 00000000..dde7a8c4 --- /dev/null +++ b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/license.txt @@ -0,0 +1,39 @@ +The icons in these folders are available either under the terms +of the GNU Public License, or under the terms of the TortoiseSVN +Icon License below, unless the folder contains an alternative +license file. + +==================================================================== +Copyright (c) 2006 The TortoiseSVN Project. All rights reserved. + +Redistribution and use of these icons, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of these icons as part of a source or binary + distribution must retain the above copyright notice and this + list of conditions. + +2. The icons may only be used for a Subversion client application, + or for an application with Subversion support (e.g. an IDE). + You may not use these icons for to represent actions which are + not directly related to Subversion. If the application supports + other version control systems besides Subversion, the icons may + be used for those other version control systems too. + +3. Software using these icons must provide an acknowledgement + that the icons were derived from the TortoiseSVN project, + with a link to the project website (tortoisesvn.tigris.org) + in one or more of the following places: + a) an "about" box. + b) the product user manual. + c) a textfile in the installation folder of the application. + d) a contributors page on the product web page. + +4. The name "TortoiseSVN" must not be used to endorse or + promote products using these icons without prior written + permission. For written permission, please contact + dev@tortoisesvn.tigris.org. + +5. If you provide additional icon sets for Subversion actions or + status besides those you use from us, you must give us + permission to use your icon sets too. diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/log.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/log.png new file mode 100644 index 00000000..66e46d02 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/log.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/patch.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/patch.png new file mode 100644 index 00000000..6a120cae Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/patch.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/repos.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/repos.png new file mode 100644 index 00000000..98c20b56 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/repos.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/tsvn.png b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/tsvn.png new file mode 100644 index 00000000..34dd0f15 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/tortoisesvn/tsvn.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/up.png b/templates-contrib/viewsvn/templates/docroot/images/up.png new file mode 100644 index 00000000..625819f9 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/up.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/images/viewvc.png b/templates-contrib/viewsvn/templates/docroot/images/viewvc.png new file mode 100644 index 00000000..630b0f53 Binary files /dev/null and b/templates-contrib/viewsvn/templates/docroot/images/viewvc.png differ diff --git a/templates-contrib/viewsvn/templates/docroot/styles.css b/templates-contrib/viewsvn/templates/docroot/styles.css new file mode 100644 index 00000000..05386624 --- /dev/null +++ b/templates-contrib/viewsvn/templates/docroot/styles.css @@ -0,0 +1,263 @@ +/*******************************/ +/*** ViewVC CSS Stylesheet ***/ +/*******************************/ + +/*** Standard Tags ***/ +html, body { + color: #000000; + background-color: #ffffff; + font-family: arial,helvetica,sans-serif; + font-size: 12px; +} + +a { text-decoration:none;} +a:hover { text-decoration:underline;} +a:link { color: #0000ff; } +a:visited { color: #880088; } +a:active { color: #0000ff; } +.img a:hover { text-decoration:none;} + +img { border: none; } + +table { + width: 100%; + margin: 0; + border: none; +} +table.props { + width: 50%; +} +table.auto { + width: auto; +} +tr, td, th { vertical-align: top; } +form { margin: 0; } + +hr{ + color: #eeeeee; + background-color: #eeeeee; + height: 1px; + border: none; +} + +h1{ + font-size: 24px; + color: #000066 +} +h2{ + font-size: 18px; + color: #000066 +} + +/** Navigation Headers ***/ +.vc_navheader { + background-color: #e0e0ff; + font-size: 13px; + vertical-align:middle; +} +.vc_navheader td{ + vertical-align:middle; +} + +/*** Table Headers ***/ +.vc_header { + text-align: left; + vertical-align: top; + background-color: #e0e0ff; +} +.vc_header_sort { + text-align: left; + background-color: #e0e0ff; +} + + +/*** Table Rows ***/ +.vc_row_even { + background-color: #ffffff; +} +.vc_row_odd { + background-color: #eeeeee; +} + + +/*** Log messages ***/ +.vc_log { + white-space: pre-wrap; + background-color: #eeeeee; +} + +/*** Properties Listing ***/ +.vc_properties { + margin: 1em 0; +} +.vc_properties h2 { + font-size: 115%; +} + +/*** File Content Markup Styles ***/ +.vc_summary { + background-color: #eeeeee; +} +#vc_file td { + border-right-style: solid; + border-right-color: #505050; + text-decoration: none; + font-weight: normal; + font-style: normal; + padding: 1px 5px; +} +.vc_file_line_number { + border-right-width: 1px; + background-color: #eeeeee; + color: #505050; + text-align: right; +} +.vc_file_line_author, .vc_file_line_rev { + border-right-width: 1px; + text-align: right; +} +.vc_file_line_text { + border-right-width: 0px; + background-color: white; + font-family: monospace; + text-align: left; + white-space: pre; + width: 100%; +} +.pygments-c { color: #408080; font-style: italic } /* Comment */ +.pygments-err { border: 1px solid #FF0000 } /* Error */ +.pygments-k { color: #008000; font-weight: bold } /* Keyword */ +.pygments-o { color: #666666 } /* Operator */ +.pygments-cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.pygments-cp { color: #BC7A00 } /* Comment.Preproc */ +.pygments-c1 { color: #408080; font-style: italic } /* Comment.Single */ +.pygments-cs { color: #408080; font-style: italic } /* Comment.Special */ +.pygments-gd { color: #A00000 } /* Generic.Deleted */ +.pygments-ge { font-style: italic } /* Generic.Emph */ +.pygments-gr { color: #FF0000 } /* Generic.Error */ +.pygments-gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.pygments-gi { color: #00A000 } /* Generic.Inserted */ +.pygments-go { color: #808080 } /* Generic.Output */ +.pygments-gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.pygments-gs { font-weight: bold } /* Generic.Strong */ +.pygments-gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.pygments-gt { color: #0040D0 } /* Generic.Traceback */ +.pygments-kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.pygments-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.pygments-kp { color: #008000 } /* Keyword.Pseudo */ +.pygments-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.pygments-kt { color: #B00040 } /* Keyword.Type */ +.pygments-m { color: #666666 } /* Literal.Number */ +.pygments-s { color: #BA2121 } /* Literal.String */ +.pygments-na { color: #7D9029 } /* Name.Attribute */ +.pygments-nb { color: #008000 } /* Name.Builtin */ +.pygments-nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.pygments-no { color: #880000 } /* Name.Constant */ +.pygments-nd { color: #AA22FF } /* Name.Decorator */ +.pygments-ni { color: #999999; font-weight: bold } /* Name.Entity */ +.pygments-ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.pygments-nf { color: #0000FF } /* Name.Function */ +.pygments-nl { color: #A0A000 } /* Name.Label */ +.pygments-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.pygments-nt { color: #008000; font-weight: bold } /* Name.Tag */ +.pygments-nv { color: #19177C } /* Name.Variable */ +.pygments-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.pygments-w { color: #bbbbbb } /* Text.Whitespace */ +.pygments-mf { color: #666666 } /* Literal.Number.Float */ +.pygments-mh { color: #666666 } /* Literal.Number.Hex */ +.pygments-mi { color: #666666 } /* Literal.Number.Integer */ +.pygments-mo { color: #666666 } /* Literal.Number.Oct */ +.pygments-sb { color: #BA2121 } /* Literal.String.Backtick */ +.pygments-sc { color: #BA2121 } /* Literal.String.Char */ +.pygments-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.pygments-s2 { color: #BA2121 } /* Literal.String.Double */ +.pygments-se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.pygments-sh { color: #BA2121 } /* Literal.String.Heredoc */ +.pygments-si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.pygments-sx { color: #008000 } /* Literal.String.Other */ +.pygments-sr { color: #BB6688 } /* Literal.String.Regex */ +.pygments-s1 { color: #BA2121 } /* Literal.String.Single */ +.pygments-ss { color: #19177C } /* Literal.String.Symbol */ +.pygments-bp { color: #008000 } /* Name.Builtin.Pseudo */ +.pygments-vc { color: #19177C } /* Name.Variable.Class */ +.pygments-vg { color: #19177C } /* Name.Variable.Global */ +.pygments-vi { color: #19177C } /* Name.Variable.Instance */ +.pygments-il { color: #666666 } /* Literal.Number.Integer.Long */ + + +/*** Diff Styles ***/ +.vc_diff_header { + background-color: #ffffff; +} +.vc_diff_chunk_header { + background-color: #eeeeee; +} +.vc_diff_chunk_extra { + font-size: smaller; +} +.vc_diff_empty { + background-color: #c8c8c8; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_add { + background-color: #ffff00; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_remove { + background-color: #ffc864; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_change { + background-color: #ffff96; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_change_empty { + background-color: #eeee77; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_nochange { + font-family: sans-serif; + font-size: smaller; +} +.vc_raw_diff { + background-color: #eeeeee; + font-size: 12px; +} + +/*** Intraline Diff Styles ***/ + +.vc_idiff_add { + background-color: #aaffaa; +} +.vc_idiff_change { + background-color:#ffff77; +} +.vc_idiff_remove { + background-color:#ffaaaa; +} +.vc_idiff_empty { + background-color:#e0e0e0; +} + +table.vc_idiff col.content { + width: 50%; +} +table.vc_idiff tbody { + font-family: monospace; + /* unfortunately, white-space: pre-wrap isn't widely supported ... */ + white-space: pre-wrap; /* CSS3 */ +} +table.vc_idiff tbody th { + background-color:#e0e0e0; + text-align:right; +} + +/*** Query Form ***/ +.vc_query_form { + background-color: #e0e0ff; +} diff --git a/templates-contrib/viewsvn/templates/error.ezt b/templates-contrib/viewsvn/templates/error.ezt new file mode 100644 index 00000000..2ba190c5 --- /dev/null +++ b/templates-contrib/viewsvn/templates/error.ezt @@ -0,0 +1,24 @@ + + + + +ViewVC Exception + + +

An Exception Has Occurred

+ +[if-any msg] +

[msg]

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

HTTP Response Status

+

[status]

+
+[end] +

Python Traceback

+

+[stacktrace]
+

+ + diff --git a/templates-contrib/viewsvn/templates/file.ezt b/templates-contrib/viewsvn/templates/file.ezt new file mode 100644 index 00000000..74bb5da3 --- /dev/null +++ b/templates-contrib/viewsvn/templates/file.ezt @@ -0,0 +1,79 @@ +[# setup page definitions] + [define page_title]View of /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "markup"] +[include "include/file_header.ezt"] +
+
+Revision [if-any revision_href][rev][else][rev][end]  +[if-any download_href]Download[end] + + +[is view "markup"] + [if-any annotate_href]Blame[end] +[end] +[is view "annotate"] + View +[end] + +[if-any orig_path] +
Original Path: [orig_path] +[end] +[if-any mime_type] +
File MIME type: [mime_type] +[end] +[if-any size] +
File size: [size] byte(s) +[end] +[if-any log] +
[log]
+[end] +
+ + +[define last_rev]0[end] +[define rowclass]vc_row_even[end] + +[if-any lines] + +
+ +[for lines] + [is lines.rev last_rev] + [else] + [is lines.rev rev] + [define rowclass]vc_row_special[end] + [else] + [is rowclass "vc_row_even"] + [define rowclass]vc_row_odd[end] + [else] + [define rowclass]vc_row_even[end] + [end] + [end] + [end] + + + +[is annotation "annotated"] + + +[end] + + + [define last_rev][lines.rev][end] +[end] +
[lines.line_number][is lines.rev last_rev] [else][lines.author][end][is lines.rev last_rev] [else][if-any lines.diff_href][end][lines.rev][if-any lines.diff_href][end][end][lines.text]
+
+ +[else] +[if-any image_src_href] +
+ +
+[end] +[end] + +[include "include/props.ezt"] +[include "include/footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/include/diff_form.ezt b/templates-contrib/viewsvn/templates/include/diff_form.ezt new file mode 100644 index 00000000..c481e269 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/diff_form.ezt @@ -0,0 +1,34 @@ +
+

+ This form allows you to request diffs between any two revisions of this file. + For each of the two "sides" of the diff, + enter a numeric revision. +

+
+ + + + + + + + + + +
  + [for diff_select_hidden_values][end] + Diffs between + + + and + +
  + Type of Diff should be a + + +
+
diff --git a/templates-contrib/viewsvn/templates/include/dir_footer.ezt b/templates-contrib/viewsvn/templates/include/dir_footer.ezt new file mode 100644 index 00000000..d1ba1939 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/dir_footer.ezt @@ -0,0 +1,28 @@ +[if-any where] + + + + +
+ [# + Enable this section and replace protocoll://svnparentlocation with your SVN parent URL location + (e.g. https://server/svn) to enable direct TortoiseSVN checkouts. This works only if all your + repositories have the same location, e.g. if you use the SVNParentPath directive in mod_dav_svn + ] + + [if-any tarball_href] + + Download as GNU tarball + + [end] +   +
+[end] + +[include "props.ezt"] +[include "footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/include/dir_header.ezt b/templates-contrib/viewsvn/templates/include/dir_header.ezt new file mode 100644 index 00000000..ad00347c --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/dir_header.ezt @@ -0,0 +1,53 @@ +[# setup page definitions] + [define page_title]Index of /[where][end] + [define help_href][docroot]/help_[if-any where]dir[else]root[end]view.html[end] +[# end] + +[include "header.ezt" "directory"] + +[if-any where][else] + +[end] + + + + + + + + + + + +[if-any search_re] + +[end] + +[if-any queryform_href] + + + + +[end] +
Directory revision:[tree_rev][if-any youngest_rev] (of [youngest_rev])[end]
Sticky Revision:[include "pathrev_form.ezt"]
Current search:[search_re]
Query:Query revision history
+ +[is cfg.options.use_pagesize "0"] +[else] + [is picklist_len "1"] + [else] +
+ [for dir_paging_hidden_values][end] + + +
+ [end] +[end] + +

+ +
+ diff --git a/templates-contrib/viewsvn/templates/include/file_header.ezt b/templates-contrib/viewsvn/templates/include/file_header.ezt new file mode 100644 index 00000000..e1e09368 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/file_header.ezt @@ -0,0 +1,24 @@ +

+Parent Directory Parent Directory + +[is view "markup"] + [if-any log_href] + Revision Log Revision Log + [end] +[end] +[is view "annotate"] + [if-any log_href] + Revision Log Revision Log + [end] +[end] + + +[is view "diff"] + View Patch Patch +[end] + +[is pathtype "file"] +[else] + View Directory Listing Directory Listing +[end] +

diff --git a/templates-contrib/viewsvn/templates/include/footer.ezt b/templates-contrib/viewsvn/templates/include/footer.ezt new file mode 100644 index 00000000..724d6a42 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/footer.ezt @@ -0,0 +1,21 @@ +[# standard footer used by all ViewVC pages ] + +
+ + + + + + + + + + +
[if-any cfg.general.address]
[cfg.general.address]
[else] [end]
+ Subversion  + TortoiseSVN  + ViewVC +
 [if-any rss_href]RSS 2.0 feed[else] [end]
+ + + diff --git a/templates-contrib/viewsvn/templates/include/header.ezt b/templates-contrib/viewsvn/templates/include/header.ezt new file mode 100644 index 00000000..cc22237c --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/header.ezt @@ -0,0 +1,49 @@ + + + + + [if-any rootname][[][rootname]][else]Subversion[end] [page_title] + + [# set your favicon here ] + + + [if-any rss_href][end] + + + +[is roottype "svn"][else][if-any roots][else] +

+Do not use this template set if you plan to view cvs repositories through ViewVC! +This template-set is heavily trimmed for Subversion needs. Special CVS features are +untested, possibly disabled or broken. +

+[end][end] + +
+ [# insert your logo and link to your organisation here ] + +
+ +
+ [if-any roots_href][[]Repository Listing] /[else]/[end] + [if-any nav_path] + [for nav_path] + [if-any nav_path.href][end] + [if-index nav_path first] + [[][nav_path.name]][else] + [nav_path.name][end][if-any nav_path.href][end] + [if-index nav_path last][else]/[end] + [end] + + [end] +
+ + +

[page_title]

+ + diff --git a/templates-contrib/viewsvn/templates/include/log_footer.ezt b/templates-contrib/viewsvn/templates/include/log_footer.ezt new file mode 100644 index 00000000..b76db886 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/log_footer.ezt @@ -0,0 +1,7 @@ +[include "paging.ezt"] + +[is pathtype "file"] + [include "diff_form.ezt"] +[end] + +[include "footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/include/log_header.ezt b/templates-contrib/viewsvn/templates/include/log_header.ezt new file mode 100644 index 00000000..32d96a45 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/log_header.ezt @@ -0,0 +1,45 @@ +[# setup page definitions] + [define page_title]Log of /[where][end] + [define help_href][docroot]/help_log.html[end] +[# end] + +[include "header.ezt" "log"] +[include "file_header.ezt"] + +
+ + + + +[is pathtype "file"] +[if-any view_href] + + + + +[end] + +[if-any tag_view_href] + + + + +[end] +[end] + + + + + + +
Links to HEAD: + View + [if-any download_href]Download[end] + [if-any annotate_href]Blame[end] +
Links to [pathrev]: + View + [if-any tag_download_href]Download[end] + [if-any tag_annotate_href]Blame[end] +
Sticky [is roottype "cvs"]Tag[else]Revision[end]:[include "pathrev_form.ezt"]
+ +[include "paging.ezt"] diff --git a/templates-contrib/viewsvn/templates/include/paging.ezt b/templates-contrib/viewsvn/templates/include/paging.ezt new file mode 100644 index 00000000..0055d4b3 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/paging.ezt @@ -0,0 +1,22 @@ + [is cfg.options.use_pagesize "0"] + [else] + [is picklist_len "1"] + [else] +
+
+ [for log_paging_hidden_values][end] + + +
+ [end] + [end] + + diff --git a/templates-contrib/viewsvn/templates/include/pathrev_form.ezt b/templates-contrib/viewsvn/templates/include/pathrev_form.ezt new file mode 100644 index 00000000..084e830f --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/pathrev_form.ezt @@ -0,0 +1,21 @@ +
+
+[for pathrev_hidden_values][end] + + +
+
+ +[if-any pathrev] +
+
+[for pathrev_clear_hidden_values][end] +[if-any lastrev] + [is pathrev lastrev][else][end] + (Current path doesn't exist after revision [lastrev]) +[else] + +[end] +
+
+[end] diff --git a/templates-contrib/viewsvn/templates/include/props.ezt b/templates-contrib/viewsvn/templates/include/props.ezt new file mode 100644 index 00000000..eaa78416 --- /dev/null +++ b/templates-contrib/viewsvn/templates/include/props.ezt @@ -0,0 +1,26 @@ +[if-any properties] +
+
+

Properties

+ + + + + + + + +[for properties] + + + [if-any properties.undisplayable] + + [else] + + [end] + +[end] + +
NameValue
[properties.name]Property value is undisplayable.[properties.value]
+
+[end] diff --git a/templates-contrib/viewsvn/templates/log.ezt b/templates-contrib/viewsvn/templates/log.ezt new file mode 100644 index 00000000..37b958b3 --- /dev/null +++ b/templates-contrib/viewsvn/templates/log.ezt @@ -0,0 +1,53 @@ +[include "include/log_header.ezt"] + +[define first_revision][end] +[define last_revision][end] + +[for entries] +[if-index entries first][define first_revision][entries.rev][end][end] +[if-index entries last][define last_revision][entries.rev][end][end] + +
+
+ + + + Revision [entries.rev]  + [is pathtype "file"] + [if-any entries.view_href]View[end] + [else] + Directory Listing + [end] + [if-any entries.download_href]Download[end] + + [if-any entries.annotate_href]Blame[end] +
+ + [if-index entries last]Added[else]Modified[end] + + [entries.date] ([entries.ago] ago) by [entries.author] + + [if-any entries.orig_path] +
Original Path: [entries.orig_path] + [end] + + [if-any entries.size] +
File length: [entries.size] byte(s) + [end] + + [if-any entries.copy_path] +
Copied from: [entries.copy_path] revision [entries.copy_rev] + [end] + + + [is pathtype "file"] + [if-any entries.prev] +
Diff to previous [entries.prev] + [end] + [end] + +
[entries.log]
+
+[end] + +[include "include/log_footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/query.ezt b/templates-contrib/viewsvn/templates/query.ezt new file mode 100644 index 00000000..fa33c004 --- /dev/null +++ b/templates-contrib/viewsvn/templates/query.ezt @@ -0,0 +1,241 @@ + + + + + Checkin Database Query + + + + + +[# setup page definitions] + [define help_href][docroot]/help_query.html[end] +[# end] + +

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

+

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

+

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

+

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

+ +
+ +
+ + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
CVS Repository: + +
CVS Branch: + +
Directory: + +
File: + +
Author: + +
+ +
+ + + + + + + + + +
Sort By: + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Date:
In the last + hours +
In the last day
In the last week
In the last month
Since the beginning of time
+
+ +
+
+ +
+
+ +
+ +[is query "skipped"] +[else] +

[num_commits] matches found.

+ +[if-any commits] + + + + + + + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + + [for commits.files] + + + + + + + + +[# uncommment, if you want a separate Description column: + {if-index commits.files first{ + + {end} + + (substitute brackets for the braces) +] + +[# and also take the following out in the "Description column"-case:] + [if-index commits.files last] + + + + + [end] +[# ---] + [end] + +[end] + + + + + + + + +[# uncommment, if you want a separate Description column: + +] + +
RevisionFileBranch+/-DateAuthorDescription
+ [if-any commits.files.rev][commits.files.rev][else] [end] + [commits.files.link] + [if-any commits.files.branch][commits.files.branch][else] [end] + + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] + + {commits.log} +
 Log:
+
[commits.log]
       
+[end] +[end] +[include "include/footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/query_form.ezt b/templates-contrib/viewsvn/templates/query_form.ezt new file mode 100644 index 00000000..886b3aa6 --- /dev/null +++ b/templates-contrib/viewsvn/templates/query_form.ezt @@ -0,0 +1,152 @@ +[# setup page definitions] + [define page_title]Query on /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "query"] + +

+Browse Directory  +Browse Directory

+ +
+ +
+ [query_hidden_values] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Subdirectory: +
+ (you can list multiple directories separated by commas) +
File: +
+ + + + +
Who: +
+ + + + +
Sort By: + +
Date: + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + hours +
+ + + and + +
+ (use the form yyyy-mm-dd hh:mm:ss) +
+
Limit: + Show at most + + changed files per commit.
+ (use 0 to show all files) +
+
+ +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/query_results.ezt b/templates-contrib/viewsvn/templates/query_results.ezt new file mode 100644 index 00000000..33524dea --- /dev/null +++ b/templates-contrib/viewsvn/templates/query_results.ezt @@ -0,0 +1,90 @@ +[# setup page definitions] + [define page_title]Query results on /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt"] + +

Modify query Modify query

+

[english_query]

+[# ] + + +

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

+ +[if-any commits] + + + + + + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + [for commits.files] + + + + + + + + + [end] + [if-any commits.limited_files] + + + + [end] + + + + + +[end] +
RevisionFile+/-DateAuthorDescription
+ [if-any commits.files.rev] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [commits.files.rev] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [else] [end] + + [commits.files.dir]/ + + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [commits.files.file] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + + + + [# only show a diff link for changes ] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] +
  + Only first [commits.num_files] files shown. + Show all files or + adjust limit. +
 Log:
+
[commits.log]
+[end] + +[include "include/footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/revision.ezt b/templates-contrib/viewsvn/templates/revision.ezt new file mode 100644 index 00000000..932e9643 --- /dev/null +++ b/templates-contrib/viewsvn/templates/revision.ezt @@ -0,0 +1,85 @@ +[# setup page definitions] + [define page_title]Revision [rev][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "revision"] + +
+
+ + + + + + + + + + + + + + + + + +
Jump to revision: + [for jump_rev_hidden_values][end] + + + [if-any prev_href] + Previous[end] + [if-any next_href] Next[end] +
Author:[if-any author][author][else](unknown author)[end]
Date:[date] ([ago] ago)
Log Message:
[log]
+
+ +
+ +

Changed paths:

+ +[if-any more_changes] +
+ Only [limit_changes] changes shown, + display [more_changes] more changes... +
+[end] + +[if-any first_changes] + +[end] + + + + + + + + + + + [if-any changes] + [for changes] + + + + + + [end] + [else] + + + + [end] + +
PathAction
[if-any changes.view_href][end][changes.path] [changes.path][is changes.pathtype "dir"]/[end][if-any changes.view_href][end] + [if-any changes.is_copy]
(Copied from [changes.copy_path], r[changes.copy_rev])[end] +
+ [changes.action] + [if-any changes.prop_mods], props changed[end] + + Revision Log + [if-any changes.diff_href]Diff to previous[end] +
No changed paths.
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/roots.ezt b/templates-contrib/viewsvn/templates/roots.ezt new file mode 100644 index 00000000..c64fc9b2 --- /dev/null +++ b/templates-contrib/viewsvn/templates/roots.ezt @@ -0,0 +1,28 @@ +[# setup page definitions] + [define page_title]Repository Listing[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "directory"] + + + + + + + + + +[for roots] + + + +[end] + + +
Name
+ [roots.name] + [roots.name] +
+ +[include "include/footer.ezt"] diff --git a/templates-contrib/viewsvn/templates/rss.ezt b/templates-contrib/viewsvn/templates/rss.ezt new file mode 100644 index 00000000..b267a720 --- /dev/null +++ b/templates-contrib/viewsvn/templates/rss.ezt @@ -0,0 +1,16 @@ + + + + [rootname] checkins[if-any where] (in [where])[end] + + Subversion commits to the[if-any where] [where] directory of the[end] [rootname] repository + + [for commits] + [if-any commits.rev][commits.rev]: [end][[commits.author]] [commits.short_log] + [if-any commits.rss_url][commits.rss_url][end] + [commits.author] + [commits.rss_date] + <pre>[format "xml"][commits.log][end]</pre> + [end] + + diff --git a/templates/diff.ezt b/templates/diff.ezt new file mode 100644 index 00000000..22e4351e --- /dev/null +++ b/templates/diff.ezt @@ -0,0 +1,240 @@ +[# setup page definitions] + [define page_title]Diff of /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "diff"] +[include "include/file_header.ezt"] + +

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

+ - No changes -
 
 

+ - Binary file revisions differ -
 
 

+ - ViewVC depends on rcsdiff and GNU diff to create + this page. ViewVC cannot find GNU diff. Even if you + have GNU diff installed, the rcsdiff program must be + configured and compiled with the GNU diff location. + -
 
[if-any right.annotate_href][changes.line_number][else][changes.line_number][end] [changes.left] [changes.right]
+[end] + +[if-any sidebyside] + + + + + + + + + + + [for sidebyside] + [if-any sidebyside.gap] + + + + + [end] + + [for sidebyside.columns] + [for sidebyside.columns.segments][if-any sidebyside.columns.segments.type][sidebyside.columns.segments.text][else][sidebyside.columns.segments.text][end][end] + [end] + + [end] + +
+ [is left.path right.path][else][left.path][end] + Revision [left.rev] + + [is left.path right.path][else][right.path][end] + Revision [right.rev] +
[sidebyside.columns.line_number]
+[end] + +[if-any unified] + + + + + + + + + + [for unified] + [if-any unified.gap] + + + + + + [end] + + + + [for unified.segments][if-any unified.segments.type][unified.segments.text][else][unified.segments.text][end][end] + + [end] + +
r[left.rev]r[right.rev]
[unified.left_number][unified.right_number]
+[end] + +
+ + + + + + +
+
+
+ [for diff_format_hidden_values][end] + + +
+
+
+[if-any raw_diff] +   +[else] + + + + +
Legend:
+ + + + + + + + + + + + +
Removed from v.[left.rev] 
changed lines
 Added in v.[right.rev]
+
+[end] +
+ +[include "include/footer.ezt"] diff --git a/templates/dir_new.ezt b/templates/dir_new.ezt new file mode 100644 index 00000000..838ea85d --- /dev/null +++ b/templates/dir_new.ezt @@ -0,0 +1,132 @@ +[include "include/dir_header.ezt"] + + + + + + + + + + + +[if-any up_href] + + + + + + [end] +[for entries] + + + + [if-any entries.errors] + + [else] + [define view_icon_link][end] + [define graph_icon_link][end] + [define download_icon_link][end] + [define annotate_icon_link][end] + [define log_icon_link][if-any entries.log_href]View Log[end][end] + + [is entries.pathtype "dir"] + [is roottype "cvs"] + [# no point in showing icon when there's only one to choose from] + [else] + [define view_icon_link]View Directory Listing[end] + [end] + [end] + + [is entries.pathtype "file"] + [define view_icon_link][if-any entries.view_href]View File[end][end] + + [define graph_icon_link][if-any entries.graph_href]View Revision Graph[end][end] + + [define download_icon_link][if-any entries.download_href]Download File[end][end] + + [define annotate_icon_link][if-any entries.annotate_href]Annotate File[end][end] + [end] + + + + + [end] + +[end] + + +
+ File + [is sortby "file"] + [is sortdir + [end] + + + Last Change + [is sortby "rev"] + [is sortdir + [end] + +
+ +  Parent Directory +  
+ + + [entries.name][is entries.pathtype "dir"]/[end] + [if-any entries.lockinfo]locked[end] + [is entries.state "dead"](dead)[end] + [for entries.errors][entries.errors][end][# Icon column. We might want to add more icons like a tarball + # icon for directories or a diff to previous icon for files. + # Make sure this sucker has no whitespace in it, or the fixed + # widthness of will suffer for large font sizes + ][log_icon_link][view_icon_link][graph_icon_link][download_icon_link][annotate_icon_link] + [if-any entries.rev] + [if-any entries.revision_href][entries.rev][else][entries.rev][end] + ([entries.ago] ago) + by [entries.author]: + [entries.log] + [is entries.pathtype "dir"][is roottype "cvs"] + (from [entries.log_file]/[entries.log_rev]) + [end][end] + [end] +
+ +[include "include/dir_footer.ezt"] diff --git a/templates/directory.ezt b/templates/directory.ezt new file mode 100644 index 00000000..8c7814a7 --- /dev/null +++ b/templates/directory.ezt @@ -0,0 +1,121 @@ +[include "include/dir_header.ezt"] + + + + + + + + +[is cfg.options.show_logs "1"] + +[end] + + + + +[if-any up_href] + + + + + + [is cfg.options.show_logs "1"] + + [end] + + [end] + [for entries] + + + [is entries.pathtype "dir"] + + [else] + + [end] + + [entries.name][is entries.pathtype "dir"]/[end] + [is entries.state "dead"](dead)[end] + + [if-any entries.graph_href] + + [end] + [if-any entries.errors] + + [else] + [is entries.pathtype "dir"] + + [else] + [define rev_href][if-any entries.prefer_markup][entries.view_href][else][if-any entries.download_href][entries.download_href][end][end][end] + + [end] + + + [is cfg.options.show_logs "1"] + [if-any entries.short_log] + + [else] + + [end] + [end] + [end] + +[end] + + +
+ File + [is sortby "file"] + [is sortdir + [end] + + + Rev. + [is sortby "rev"] + [is sortdir + [end] + + + Age + [is sortby "date"] + [is sortdir + [end] + + + Author + [is sortby "author"] + [is sortdir + [end] + + + Last log entry + [is sortby "log"] + [is sortdir + [end] + +
+ +  Parent Directory +     
View Revision Graph + + [for entries.errors][entries.errors][end] +  [if-any entries.rev][entries.rev][end] [if-any entries.rev][if-any rev_href][end][entries.rev][if-any rev_href][end][end] + [if-any entries.lockinfo]locked[end] +  [entries.ago] [entries.author] [entries.short_log][is entries.pathtype "dir"][is roottype "cvs"] + (from [entries.log_file]/[entries.log_rev])[end][end] 
+ +[include "include/dir_footer.ezt"] diff --git a/templates/docroot/help.css b/templates/docroot/help.css new file mode 100644 index 00000000..9adde072 --- /dev/null +++ b/templates/docroot/help.css @@ -0,0 +1,14 @@ +/************************************/ +/*** ViewVC Help CSS Stylesheet ***/ +/************************************/ + +/*** Standard Tags ***/ +body { + margin: 0.5em; +} +img { border: none; } + +table { width: 100%; } +td { vertical-align: top; } + +col.menu { width:12em; } diff --git a/templates/docroot/help_dirview.html b/templates/docroot/help_dirview.html new file mode 100644 index 00000000..f86cb4a9 --- /dev/null +++ b/templates/docroot/help_dirview.html @@ -0,0 +1,122 @@ + + + + ViewVC Help: Directory View + + + + + +
ViewVC logotype
+ + + +
+

Help

+ General
+ Directory View
+ Log View
+ Query Database
+
+ +

ViewVC Help: Directory View

+ +

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

+ +

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

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

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

+ +

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

+ +

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

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

Help

+ General
+ Directory View
+ Log View
+ Query Database
+
+ +

ViewVC Help: Log View

+ +

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

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

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

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

Help:

+ General
+ Directory View
+ Log View
+ Query Database +
+ +

ViewVC Help: Query The Commit Database

+ +

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

+

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

+

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

+

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

+
+
+
ViewVC Users Mailinglist
+ + diff --git a/templates/docroot/help_rootview.html b/templates/docroot/help_rootview.html new file mode 100644 index 00000000..24457e94 --- /dev/null +++ b/templates/docroot/help_rootview.html @@ -0,0 +1,157 @@ + + + + ViewVC Help: General + + + + + +
ViewVC logotype
+ + + +
+

Help

+ General
+ Directory View
+ Log View
+ Query Database
+
+ +

ViewVC Help: General

+ +

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

+ + + +

Sticky Revision and Tag

+ +

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

+ +

Dead Files

+ +

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

+ +

Artificial Tags

+ +

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

+ +

More Information

+ +

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

+ +

Documentation about CVS

+
+

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

+
+ +

Documentation about Subversion

+
+

+ Version Control with + Subversion
+

+
+
+
+
ViewVC Users Mailinglist
+ + diff --git a/templates/docroot/images/annotate.png b/templates/docroot/images/annotate.png new file mode 100644 index 00000000..ed2d33b9 Binary files /dev/null and b/templates/docroot/images/annotate.png differ diff --git a/templates/docroot/images/back.png b/templates/docroot/images/back.png new file mode 100644 index 00000000..65f46318 Binary files /dev/null and b/templates/docroot/images/back.png differ diff --git a/templates/docroot/images/back_small.png b/templates/docroot/images/back_small.png new file mode 100644 index 00000000..a057c3f8 Binary files /dev/null and b/templates/docroot/images/back_small.png differ diff --git a/templates/docroot/images/broken.png b/templates/docroot/images/broken.png new file mode 100644 index 00000000..cdaf2362 Binary files /dev/null and b/templates/docroot/images/broken.png differ diff --git a/templates/docroot/images/chalk.jpg b/templates/docroot/images/chalk.jpg new file mode 100644 index 00000000..73c9533f Binary files /dev/null and b/templates/docroot/images/chalk.jpg differ diff --git a/templates/docroot/images/cvsgraph_16x16.png b/templates/docroot/images/cvsgraph_16x16.png new file mode 100644 index 00000000..6f5bece2 Binary files /dev/null and b/templates/docroot/images/cvsgraph_16x16.png differ diff --git a/templates/docroot/images/cvsgraph_32x32.png b/templates/docroot/images/cvsgraph_32x32.png new file mode 100644 index 00000000..f1ccc45b Binary files /dev/null and b/templates/docroot/images/cvsgraph_32x32.png differ diff --git a/templates/docroot/images/diff.png b/templates/docroot/images/diff.png new file mode 100644 index 00000000..9047bfe9 Binary files /dev/null and b/templates/docroot/images/diff.png differ diff --git a/templates/docroot/images/dir.png b/templates/docroot/images/dir.png new file mode 100644 index 00000000..a11e7eb1 Binary files /dev/null and b/templates/docroot/images/dir.png differ diff --git a/templates/docroot/images/down.png b/templates/docroot/images/down.png new file mode 100644 index 00000000..5644d63b Binary files /dev/null and b/templates/docroot/images/down.png differ diff --git a/templates/docroot/images/download.png b/templates/docroot/images/download.png new file mode 100644 index 00000000..0fbfe435 Binary files /dev/null and b/templates/docroot/images/download.png differ diff --git a/templates/docroot/images/favicon.ico b/templates/docroot/images/favicon.ico new file mode 100644 index 00000000..9ba7f806 Binary files /dev/null and b/templates/docroot/images/favicon.ico differ diff --git a/templates/docroot/images/feed-icon-16x16.jpg b/templates/docroot/images/feed-icon-16x16.jpg new file mode 100644 index 00000000..0c72133f Binary files /dev/null and b/templates/docroot/images/feed-icon-16x16.jpg differ diff --git a/templates/docroot/images/forward.png b/templates/docroot/images/forward.png new file mode 100644 index 00000000..d8185ac9 Binary files /dev/null and b/templates/docroot/images/forward.png differ diff --git a/templates/docroot/images/list.png b/templates/docroot/images/list.png new file mode 100644 index 00000000..7995fdd5 Binary files /dev/null and b/templates/docroot/images/list.png differ diff --git a/templates/docroot/images/lock.png b/templates/docroot/images/lock.png new file mode 100644 index 00000000..9e3bf42a Binary files /dev/null and b/templates/docroot/images/lock.png differ diff --git a/templates/docroot/images/log.png b/templates/docroot/images/log.png new file mode 100644 index 00000000..d2da45b5 Binary files /dev/null and b/templates/docroot/images/log.png differ diff --git a/templates/docroot/images/text.png b/templates/docroot/images/text.png new file mode 100644 index 00000000..6e050cd6 Binary files /dev/null and b/templates/docroot/images/text.png differ diff --git a/templates/docroot/images/up.png b/templates/docroot/images/up.png new file mode 100644 index 00000000..625819f9 Binary files /dev/null and b/templates/docroot/images/up.png differ diff --git a/templates/docroot/images/view.png b/templates/docroot/images/view.png new file mode 100644 index 00000000..a168c38f Binary files /dev/null and b/templates/docroot/images/view.png differ diff --git a/templates/docroot/images/viewvc-logo.png b/templates/docroot/images/viewvc-logo.png new file mode 100644 index 00000000..6e16f3b1 Binary files /dev/null and b/templates/docroot/images/viewvc-logo.png differ diff --git a/templates/docroot/styles.css b/templates/docroot/styles.css new file mode 100644 index 00000000..4c82d3d0 --- /dev/null +++ b/templates/docroot/styles.css @@ -0,0 +1,272 @@ +/*******************************/ +/*** ViewVC CSS Stylesheet ***/ +/*******************************/ + +/*** Standard Tags ***/ +html, body { + color: #000000; + background-color: #ffffff; + font-family: sans-serif; +} + +a:link { color: #0000ff; } +a:visited { color: #880088; } +a:active { color: #0000ff; } + +img { border: none; } +table { + width: 100%; + margin: 0; + border: none; +} +table.auto { + width: auto; +} +table.fixed { + width: 100%; + table-layout: fixed; +} +table.fixed td { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +tr, td, th { vertical-align: top; } +th { white-space: nowrap; } +form { margin: 0; } + + +/*** Icons ***/ +.vc_icon { + width: 16px; + height: 16px; + border: none; + padding: 0 1px; +} + + +/*** Navigation Headers ***/ +.vc_navheader { + background-color: #cccccc; + padding: .25em; +} +.vc_navheader .pathdiv { + padding: 0 3px; +} + + +/*** Table Headers ***/ +.vc_header { + text-align: left; + vertical-align: top; + background-color: #cccccc; +} +.vc_header_sort { + text-align: left; + background-color: #88ff88; +} + + +/*** Table Rows ***/ +.vc_row_even { + background-color: #ffffff; +} +.vc_row_odd { + background-color: #f0f0f0; +} +.vc_row_special { + background-color: #ffff7f; +} + + +/*** Log messages ***/ +.vc_log { + /* unfortunately, white-space: pre-wrap isn't widely supported ... */ + white-space: -moz-pre-wrap; /* Mozilla based browsers */ + white-space: -pre-wrap; /* Opera 4 - 6 */ + white-space: -o-pre-wrap; /* Opera >= 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ +} + + +/*** Properties Listing ***/ +.vc_properties { + margin: 1em 0; +} +.vc_properties h2 { + font-size: 115%; +} + + +/*** File Content Markup Styles ***/ +.vc_summary { + background-color: #eeeeee; +} +#vc_file td { + border-right-style: solid; + border-right-color: #505050; + text-decoration: none; + font-weight: normal; + font-style: normal; + padding: 1px 5px; +} +.vc_file_line_number { + border-right-width: 1px; + background-color: #eeeeee; + color: #505050; + text-align: right; +} +.vc_file_line_author, .vc_file_line_rev { + border-right-width: 1px; + text-align: right; +} +.vc_file_line_text { + border-right-width: 0px; + background-color: white; + font-family: monospace; + text-align: left; + white-space: pre; + width: 100%; +} +.pygments-c { color: #408080; font-style: italic } /* Comment */ +.pygments-err { border: 1px solid #FF0000 } /* Error */ +.pygments-k { color: #008000; font-weight: bold } /* Keyword */ +.pygments-o { color: #666666 } /* Operator */ +.pygments-cm { color: #408080; font-style: italic } /* Comment.Multiline */ +.pygments-cp { color: #BC7A00 } /* Comment.Preproc */ +.pygments-c1 { color: #408080; font-style: italic } /* Comment.Single */ +.pygments-cs { color: #408080; font-style: italic } /* Comment.Special */ +.pygments-gd { color: #A00000 } /* Generic.Deleted */ +.pygments-ge { font-style: italic } /* Generic.Emph */ +.pygments-gr { color: #FF0000 } /* Generic.Error */ +.pygments-gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.pygments-gi { color: #00A000 } /* Generic.Inserted */ +.pygments-go { color: #808080 } /* Generic.Output */ +.pygments-gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.pygments-gs { font-weight: bold } /* Generic.Strong */ +.pygments-gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.pygments-gt { color: #0040D0 } /* Generic.Traceback */ +.pygments-kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.pygments-kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.pygments-kp { color: #008000 } /* Keyword.Pseudo */ +.pygments-kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.pygments-kt { color: #B00040 } /* Keyword.Type */ +.pygments-m { color: #666666 } /* Literal.Number */ +.pygments-s { color: #BA2121 } /* Literal.String */ +.pygments-na { color: #7D9029 } /* Name.Attribute */ +.pygments-nb { color: #008000 } /* Name.Builtin */ +.pygments-nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.pygments-no { color: #880000 } /* Name.Constant */ +.pygments-nd { color: #AA22FF } /* Name.Decorator */ +.pygments-ni { color: #999999; font-weight: bold } /* Name.Entity */ +.pygments-ne { color: #D2413A; font-weight: bold } /* Name.Exception */ +.pygments-nf { color: #0000FF } /* Name.Function */ +.pygments-nl { color: #A0A000 } /* Name.Label */ +.pygments-nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.pygments-nt { color: #008000; font-weight: bold } /* Name.Tag */ +.pygments-nv { color: #19177C } /* Name.Variable */ +.pygments-ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.pygments-w { color: #bbbbbb } /* Text.Whitespace */ +.pygments-mf { color: #666666 } /* Literal.Number.Float */ +.pygments-mh { color: #666666 } /* Literal.Number.Hex */ +.pygments-mi { color: #666666 } /* Literal.Number.Integer */ +.pygments-mo { color: #666666 } /* Literal.Number.Oct */ +.pygments-sb { color: #BA2121 } /* Literal.String.Backtick */ +.pygments-sc { color: #BA2121 } /* Literal.String.Char */ +.pygments-sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.pygments-s2 { color: #BA2121 } /* Literal.String.Double */ +.pygments-se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ +.pygments-sh { color: #BA2121 } /* Literal.String.Heredoc */ +.pygments-si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ +.pygments-sx { color: #008000 } /* Literal.String.Other */ +.pygments-sr { color: #BB6688 } /* Literal.String.Regex */ +.pygments-s1 { color: #BA2121 } /* Literal.String.Single */ +.pygments-ss { color: #19177C } /* Literal.String.Symbol */ +.pygments-bp { color: #008000 } /* Name.Builtin.Pseudo */ +.pygments-vc { color: #19177C } /* Name.Variable.Class */ +.pygments-vg { color: #19177C } /* Name.Variable.Global */ +.pygments-vi { color: #19177C } /* Name.Variable.Instance */ +.pygments-il { color: #666666 } /* Literal.Number.Integer.Long */ + + +/*** Diff Styles ***/ +.vc_diff_header { + background-color: #ffffff; +} +.vc_diff_chunk_header { + background-color: #99cccc; +} +.vc_diff_chunk_extra { + font-size: smaller; +} +.vc_diff_empty { + background-color: #cccccc; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_add { + background-color: #aaffaa; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_remove { + background-color: #ffaaaa; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_change { + background-color: #ffff77; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_change_empty { + background-color: #eeee77; + font-family: sans-serif; + font-size: smaller; +} +.vc_diff_nochange { + font-family: sans-serif; + font-size: smaller; +} +.vc_raw_diff { + background-color: #cccccc; + font-size: smaller; +} + + +/*** Intraline Diff Styles ***/ +.vc_idiff_add { + background-color: #aaffaa; +} +.vc_idiff_change { + background-color:#ffff77; +} +.vc_idiff_remove { + background-color:#ffaaaa; +} +.vc_idiff_empty { + background-color:#e0e0e0; +} +table.vc_idiff col.content { + width: 50%; +} +table.vc_idiff tbody { + font-family: monospace; + /* unfortunately, white-space: pre-wrap isn't widely supported ... */ + white-space: -moz-pre-wrap; /* Mozilla based browsers */ + white-space: -pre-wrap; /* Opera 4 - 6 */ + white-space: -o-pre-wrap; /* Opera >= 7 */ + white-space: pre-wrap; /* CSS3 */ + word-wrap: break-word; /* IE 5.5+ */ +} +table.vc_idiff tbody th { + background-color:#e0e0e0; + text-align:right; +} + + +/*** Query Form ***/ +.vc_query_form { + background-color: #e6e6e6; +} diff --git a/templates/error.ezt b/templates/error.ezt new file mode 100644 index 00000000..e1d61af7 --- /dev/null +++ b/templates/error.ezt @@ -0,0 +1,51 @@ + + + + +ViewVC Exception + + +

An Exception Has Occurred

+ +[if-any msg] +

[msg]

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

HTTP Response Status

+

[status]

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

Python Traceback

+

+[stacktrace]
+

+[end] + +[# Here follows a bunch of space characters, present to ensure that + our error message is larger than 512 bytes so that IE's "Friendly + Error Message" won't show. For more information, see + http://oreillynet.com/onjava/blog/2002/09/internet_explorer_subverts_err.html] + + + + + + + + + + + + + + + + + + + + diff --git a/templates/file.ezt b/templates/file.ezt new file mode 100644 index 00000000..79c454c9 --- /dev/null +++ b/templates/file.ezt @@ -0,0 +1,106 @@ +[# setup page definitions] + [define page_title]Annotate of /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "markup"] +[include "include/file_header.ezt"] +
+
+Revision [if-any revision_href][rev][else][rev][end] - +([if-any annotation][is annotation "annotated"]hide annotations[end][else]show annotations[end]) +[if-any download_href](download)[end] +[if-any download_text_href](as text)[end] + +[if-any vendor_branch] (vendor branch)[end] +
[if-any date][date][else](unknown date)[end] + [if-any ago]([ago] ago)[end] + by [if-any author][author][else](unknown author)[end] +[if-any orig_path] +
Original Path: [orig_path] +[end] + +[if-any branches] +
Branch: [branches] +[end] +[if-any tags] +
CVS Tags: [tags] +[end] +[if-any branch_points] +
Branch point for: [branch_points] +[end] +[is roottype "cvs"] + [if-any changed] +
Changes since [prev]: [changed] lines + [end] +[end] +[if-any mime_type] +
File MIME type: [mime_type] +[end] +[is roottype "svn"][if-any size] +
File size: [size] byte(s) +[end][end] +[if-any lockinfo] +
Lock status: Locked [lockinfo] +[end] +[if-any annotation] +[is annotation "binary"] +
Unable to calculate annotation data on binary file contents. +[end] +[is annotation "error"] +
Error occurred while calculating annotation data. +[end] +[end] +[is state "dead"] +
FILE REMOVED +[end] +[if-any log] +
[log]
+[end] +
+ + +[define last_rev]0[end] +[define rowclass]vc_row_even[end] + +[if-any lines] + +
+ +[for lines] + [is lines.rev last_rev] + [else] + [is lines.rev rev] + [define rowclass]vc_row_special[end] + [else] + [is rowclass "vc_row_even"] + [define rowclass]vc_row_odd[end] + [else] + [define rowclass]vc_row_even[end] + [end] + [end] + [end] + + + +[is annotation "annotated"] + + +[end] + + + [define last_rev][lines.rev][end] +[end] +
[lines.line_number][is lines.rev last_rev] [else][lines.author][end][is lines.rev last_rev] [else][if-any lines.diff_href][end][lines.rev][if-any lines.diff_href][end][end][lines.text]
+
+ +[else] +[if-any image_src_href] +
+ +
+[end] +[end] + +[include "include/props.ezt"] +[include "include/footer.ezt"] diff --git a/templates/graph.ezt b/templates/graph.ezt new file mode 100644 index 00000000..5227abc5 --- /dev/null +++ b/templates/graph.ezt @@ -0,0 +1,18 @@ +[# setup page definitions] + [define page_title]Graph of /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "graph"] +[include "include/file_header.ezt"] + +
+ +
+[imagemap] +Revisions of [where] +
+ +[include "include/footer.ezt"] diff --git a/templates/include/diff_form.ezt b/templates/include/diff_form.ezt new file mode 100644 index 00000000..7bf4cdd3 --- /dev/null +++ b/templates/include/diff_form.ezt @@ -0,0 +1,67 @@ +
+

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

+
+ + + + + + + + + + +
  + [for diff_select_hidden_values][end] + Diffs between +[if-any tags] + + +[else] + +[end] + + and +[if-any tags] + + +[else] + +[end] +
  + Type of Diff should be a + + +
+
diff --git a/templates/include/dir_footer.ezt b/templates/include/dir_footer.ezt new file mode 100644 index 00000000..5f25e691 --- /dev/null +++ b/templates/include/dir_footer.ezt @@ -0,0 +1,9 @@ +[# if you want to disable tarball generation remove the following: ] +[if-any tarball_href] +
+

Download GNU tarball

+[end] + +[include "props.ezt"] +[include "footer.ezt"] + diff --git a/templates/include/dir_header.ezt b/templates/include/dir_header.ezt new file mode 100644 index 00000000..6323cc8a --- /dev/null +++ b/templates/include/dir_header.ezt @@ -0,0 +1,84 @@ +[# setup page definitions] + [define page_title]Index of /[where][end] + [define help_href][docroot]/help_[if-any where]dir[else]root[end]view.html[end] +[# end] + +[include "header.ezt" "directory"] + +[if-any where][else] + +[end] + + + + +[is roottype "svn"] + + + + +[end] + + + + + +[if-any search_re_form] + + + + +[end] + +[if-any queryform_href] + + + + +[end] + +
Files shown:[files_shown] +[is num_dead "0"] +[else] + [if-any attic_showing] + (Hide [num_dead] dead files) + [else] + (Show [num_dead] dead files) + [end] +[end] +
Directory revision:[tree_rev][if-any youngest_rev] (of [youngest_rev])[end]
Sticky [is roottype "cvs"]Tag[else]Revision[end]:[include "pathrev_form.ezt"]
Filter files by content:
+
+ [for search_re_hidden_values][end] + + +
+
+ [if-any search_re] +
+
+ [for search_re_hidden_values][end] + +
+
+ [end] +
Query:Query revision history
+ +[is cfg.options.use_pagesize "0"] +[else] + [is picklist_len "1"] + [else] +
+ [for dir_paging_hidden_values][end] + + +
+ [end] +[end] + +

+
+ diff --git a/templates/include/file_header.ezt b/templates/include/file_header.ezt new file mode 100644 index 00000000..c4180a73 --- /dev/null +++ b/templates/include/file_header.ezt @@ -0,0 +1,16 @@ +

+[is pathtype "file"] +Parent Directory Parent Directory +[if-any log_href] + | Revision Log Revision Log +[end] +[if-any graph_href] + | View Revision Graph Revision Graph +[end] +[is view "diff"] + | View Patch Patch +[end] +[else] +View Directory Listing Directory Listing +[end] +

diff --git a/templates/include/footer.ezt b/templates/include/footer.ezt new file mode 100644 index 00000000..9ad673bb --- /dev/null +++ b/templates/include/footer.ezt @@ -0,0 +1,17 @@ +[# standard footer used by all ViewVC pages ] + +
+ + + + + + + + + + +
[if-any cfg.general.address]
[cfg.general.address]
[else] [end]
ViewVC Help
Powered by ViewVC [vsn][if-any rss_href]RSS 2.0 feed[else] [end]
+ + + diff --git a/templates/include/header.ezt b/templates/include/header.ezt new file mode 100644 index 00000000..67841145 --- /dev/null +++ b/templates/include/header.ezt @@ -0,0 +1,23 @@ + + + + + [if-any rootname][[][rootname]][else]ViewVC[end] [page_title] + + + + [if-any rss_href][end] + + +
+ + + +
[if-any roots_href]/[else]/[end][if-any nav_path][for nav_path][if-any nav_path.href][end][if-index nav_path first][[][nav_path.name]][else][nav_path.name][end][if-any nav_path.href][end][if-index nav_path last][else]/[end][end][end][if-any username]Logged in as: [username][end]
+
+ +
ViewVC logotype
+

[page_title]

+ + diff --git a/templates/include/log_footer.ezt b/templates/include/log_footer.ezt new file mode 100644 index 00000000..b39c8612 --- /dev/null +++ b/templates/include/log_footer.ezt @@ -0,0 +1,10 @@ +[include "paging.ezt"] + +[is pathtype "file"] + [include "diff_form.ezt"] +[end] + +[include "sort.ezt"] + +[include "footer.ezt"] + diff --git a/templates/include/log_header.ezt b/templates/include/log_header.ezt new file mode 100644 index 00000000..34ed3629 --- /dev/null +++ b/templates/include/log_header.ezt @@ -0,0 +1,54 @@ +[# setup page definitions] + [define page_title]Log of /[where][end] + [define help_href][docroot]/help_log.html[end] +[# end] + +[include "header.ezt" "log"] +[include "file_header.ezt"] + +
+ + + +[if-any default_branch] + + + + +[end] + +[is pathtype "file"] +[if-any head_view_href] + + + + +[end] + +[if-any tag_view_href] + + + + +[end] +[end] + + + + + + +
Default branch:[for default_branch][default_branch.name][if-index default_branch last][else], [end] +[end]
Links to HEAD: + (view) + [if-any head_download_href](download)[end] + [if-any head_download_text_href](as text)[end] + [if-any head_annotate_href](annotate)[end] +
Links to [pathrev]: + (view) + [if-any tag_download_href](download)[end] + [if-any tag_download_text_href](as text)[end] + [if-any tag_annotate_href](annotate)[end] +
Sticky [is roottype "cvs"]Tag[else]Revision[end]:[include "pathrev_form.ezt"]
+ +[include "paging.ezt"] diff --git a/templates/include/paging.ezt b/templates/include/paging.ezt new file mode 100644 index 00000000..da222362 --- /dev/null +++ b/templates/include/paging.ezt @@ -0,0 +1,22 @@ + [is cfg.options.use_pagesize "0"] + [else] + [is picklist_len "1"] + [else] +
+
+ [for log_paging_hidden_values][end] + + +
+ [end] + [end] + + diff --git a/templates/include/pathrev_form.ezt b/templates/include/pathrev_form.ezt new file mode 100644 index 00000000..33ff9618 --- /dev/null +++ b/templates/include/pathrev_form.ezt @@ -0,0 +1,53 @@ +
+
+[for pathrev_hidden_values][end] +[is roottype "cvs"] + [define pathrev_selected][pathrev][end] + +[else] + +[end] + +
+
+ +[if-any pathrev] +
+
+[for pathrev_clear_hidden_values][end] +[if-any lastrev] + [is pathrev lastrev][else][end] + (Current path doesn't exist after revision [lastrev]) +[else] + +[end] +
+
+[end] diff --git a/templates/include/props.ezt b/templates/include/props.ezt new file mode 100644 index 00000000..0f92c386 --- /dev/null +++ b/templates/include/props.ezt @@ -0,0 +1,26 @@ +[if-any properties] +
+
+

Properties

+ + + + + + + + +[for properties] + + + [if-any properties.undisplayable] + + [else] + + [end] + +[end] + +
NameValue
[properties.name]Property value is undisplayable.[properties.value]
+
+[end] diff --git a/templates/include/sort.ezt b/templates/include/sort.ezt new file mode 100644 index 00000000..6d6d02fc --- /dev/null +++ b/templates/include/sort.ezt @@ -0,0 +1,17 @@ +[is roottype "svn"] +[else] +
+
+
+ + [for logsort_hidden_values][end] + Sort log by: + + +
+
+[end] \ No newline at end of file diff --git a/templates/log.ezt b/templates/log.ezt new file mode 100644 index 00000000..2332ee71 --- /dev/null +++ b/templates/log.ezt @@ -0,0 +1,150 @@ +[include "include/log_header.ezt"] + +[define first_revision][end] +[define last_revision][end] + +[for entries] +[if-index entries first][define first_revision][entries.rev][end][end] +[if-index entries last][define last_revision][entries.rev][end][end] + +
+
+ + [is entries.state "dead"] + Revision [entries.rev] + [else] + + [for entries.tag_names] + [end] + [for entries.branch_names] + [end] + + Revision [is roottype "svn"][entries.rev][else][entries.rev][end] - + [if-any entries.view_href] + [is pathtype "file"] + (view) + [else] + Directory Listing + [end] + [end] + [if-any entries.download_href](download)[end] + [if-any entries.download_text_href](as text)[end] + [if-any entries.annotate_href](annotate)[end] + + [is pathtype "file"] + [# if you don't want to allow select for diffs then remove this section] + [is entries.rev rev_selected] + - [[]selected] + [else] + - [[]select for diffs] + [end] + [end] + [end] + + [if-any entries.vendor_branch] + (vendor branch) + [end] + +
+ + [is roottype "svn"] + [if-index entries last]Added[else]Modified[end] + [end] + + [if-any entries.date][entries.date][else](unknown date)[end] + [if-any entries.ago]([entries.ago] ago)[end] + by [if-any entries.author][entries.author][else](unknown author)[end] + + [if-any entries.orig_path] +
Original Path: [entries.orig_path] + [end] + + [if-any entries.branches] +
Branch: + [for entries.branches] + [entries.branches.name][if-index entries.branches last][else],[end] + [end] + [end] + + [if-any entries.tags] +
CVS Tags: + [for entries.tags] + [entries.tags.name][if-index entries.tags last][else],[end] + [end] + [end] + + [if-any entries.branch_points] +
Branch point for: + [for entries.branch_points] + [entries.branch_points.name][if-index entries.branch_points last][else],[end] + [end] + [end] + + [if-any entries.prev] + [if-any entries.changed] + [is roottype "cvs"] +
Changes since [entries.prev]: [entries.changed] lines + [end] + [end] + [end] + + [is roottype "svn"] + [if-any entries.size] +
File length: [entries.size] byte(s) + [end] + + [if-any entries.copy_path] +
Copied from: [entries.copy_path] revision [entries.copy_rev] + [end] + [end] + + [if-any entries.lockinfo] +
Lock status: Locked [entries.lockinfo] + [end] + + [is entries.state "dead"] +
FILE REMOVED + [else] + [is pathtype "file"] + [if-any entries.prev] +
Diff to previous [entries.prev] + [if-any human_readable] + [else] + (colored) + [end] + [end] + + [is roottype "cvs"] + [if-any entries.branch_point] + , to branch point [entries.branch_point] + [if-any human_readable] + [else] + (colored) + [end] + [end] + + [if-any entries.next_main] + , to next main [entries.next_main] + [if-any human_readable] + [else] + (colored) + [end] + [end] + [end] + + [if-any entries.diff_to_sel_href] + [if-any entries.prev], [else]
Diff[end] + to selected [rev_selected] + [if-any human_readable] + [else] + (colored) + [end] + [end] + [end] + [end] + +
[entries.log]
+
+[end] + +[include "include/log_footer.ezt"] diff --git a/templates/log_table.ezt b/templates/log_table.ezt new file mode 100644 index 00000000..192fdf18 --- /dev/null +++ b/templates/log_table.ezt @@ -0,0 +1,176 @@ +[include "include/log_header.ezt"] + +
+ + + + + + [is pathtype "file"] + + [end] + [is roottype "cvs"] + + [end] + + + + + +[define first_revision][end] +[define last_revision][end] + +[for entries] +[if-index entries first][define first_revision][entries.rev][end][end] +[if-index entries last][define last_revision][entries.rev][end][end] + + + + [# Revision column] + + + [# Tasks column] + + + [is pathtype "file"] + + [end] + [is roottype "cvs"] + + [end] + + [# Time column] + + + [# Author column] + + + + + + + +[end] +
RevisionTasksDiffsBranches/
Tags
AgeAuthor
+ [is roottype "svn"][entries.rev][else][entries.rev][end] + + + [if-any entries.view_href] + [is pathtype "file"] + View
+ [else] + Directory Listing
+ [end] + [end] + [if-any entries.download_href]Download
[end] + [if-any entries.download_text_href]As text
[end] + [if-any entries.annotate_href]Annotate
[end] +
+ [# Diffs column] + [is entries.state "dead"] + FILE REMOVED + [else] + [# if you don't want to allow select for diffs then remove this section] + [is entries.rev rev_selected] + [[]selected]
+ [else] + [[]select for diffs]
+ [end] + [if-any entries.diff_to_sel_href] + Diff to selected [rev_selected] + [if-any human_readable] + [else] + (colored) + [end]
+ [end] + [if-any entries.prev] + Diff to previous [entries.prev] + [if-any human_readable] + [else] + (colored) + [end]
+ [end] + [end] +
+ [# Branches column] + [if-any entries.vendor_branch] + vendor branch
+ [end] + [if-any entries.branches] + [for entries.branches] + [entries.branches.name]
+ [end] + [end] + [if-any entries.branch_points] + Branch point for: + [for entries.branch_points] + [entries.branch_points.name]
+ [end] + [end] + [if-any entries.next_main] + Diff to next MAIN [entries.next_main] + [if-any human_readable] + [else] + (colored) + [end]
+ [end] + [if-any entries.branch_point] + Diff to branch point [entries.branch_point] + [if-any human_readable] + [else] + (colored) + [end]
+ [end] + + [# Tags ] + [if-any entries.tags] +
+ [for pathrev_hidden_values][end] + +
+ [else]  + [end] +
+ [is roottype "svn"] + [if-index entries last]Added[else]Modified[end] + [end] + [if-any entries.ago][entries.ago] ago
[end] + [if-any entries.date][entries.date][end] + [is roottype "cvs"] + [if-any entries.prev] + [if-any entries.changed] +
Changes since [entries.prev]: [entries.changed] lines + [end] + [end] + [end] +
+ [entries.author] +
+ + [if-any entries.lockinfo] + Lock status: Locked [entries.lockinfo]
+ [end] + + [is roottype "svn"] + [if-any entries.orig_path] + Original Path: [entries.orig_path]
+ [end] + + [if-any entries.size] + File length: [entries.size] byte(s)
+ [end] + + [if-any entries.copy_path] + Copied from: [entries.copy_path] revision [entries.copy_rev]
+ [end] + [end] + + Log:
[entries.log]
+
+ +[include "include/log_footer.ezt"] diff --git a/templates/query.ezt b/templates/query.ezt new file mode 100644 index 00000000..fa33c004 --- /dev/null +++ b/templates/query.ezt @@ -0,0 +1,241 @@ + + + + + Checkin Database Query + + + + + +[# setup page definitions] + [define help_href][docroot]/help_query.html[end] +[# end] + +

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

+

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

+

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

+

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

+ +
+ +
+ + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +
CVS Repository: + +
CVS Branch: + +
Directory: + +
File: + +
Author: + +
+ +
+ + + + + + + + + +
Sort By: + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Date:
In the last + hours +
In the last day
In the last week
In the last month
Since the beginning of time
+
+ +
+
+ +
+
+ +
+ +[is query "skipped"] +[else] +

[num_commits] matches found.

+ +[if-any commits] + + + + + + + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + + [for commits.files] + + + + + + + + +[# uncommment, if you want a separate Description column: + {if-index commits.files first{ + + {end} + + (substitute brackets for the braces) +] + +[# and also take the following out in the "Description column"-case:] + [if-index commits.files last] + + + + + [end] +[# ---] + [end] + +[end] + + + + + + + + +[# uncommment, if you want a separate Description column: + +] + +
RevisionFileBranch+/-DateAuthorDescription
+ [if-any commits.files.rev][commits.files.rev][else] [end] + [commits.files.link] + [if-any commits.files.branch][commits.files.branch][else] [end] + + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] + + {commits.log} +
 Log:
+
[commits.log]
       
+[end] +[end] +[include "include/footer.ezt"] diff --git a/templates/query_form.ezt b/templates/query_form.ezt new file mode 100644 index 00000000..be10014a --- /dev/null +++ b/templates/query_form.ezt @@ -0,0 +1,207 @@ +[# setup page definitions] + [define page_title]Query on /[where][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "query"] + +

+Directory +Browse Directory

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

[english_query]

+[# ] +

Modify query

+

Show commands which could be used to back out these changes

+ +

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

+ +[if-any commits] + + + + + +[if-any show_branch] + +[end] + + + +[# uncommment, if you want a separate Description column: (also see below) + +] + + +[for commits] + [for commits.files] + + + + +[if-any show_branch] + +[end] + + + + + [end] + [if-any commits.limited_files] + + + + [end] + + + + + +[end] +
RevisionFileBranch+/-DateAuthorDescription
+ [define rev_href][if-any commits.files.prefer_markup][commits.files.view_href][else][if-any commits.files.download_href][commits.files.download_href][end][end][end] + [if-any commits.files.rev][if-any rev_href][end][commits.files.rev][if-any rev_href][end][else] [end] + + [commits.files.dir]/ + [commits.files.file] + + [if-any commits.files.branch][commits.files.branch][else] [end] + + [# only show a diff link for changes ] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + [commits.files.plus]/[commits.files.minus] + [is commits.files.type "Add"][end] + [is commits.files.type "Change"][end] + [is commits.files.type "Remove"][end] + + [if-any commits.files.date][commits.files.date][else] [end] + + [if-any commits.files.author][commits.files.author][else] [end] +
  + Only first [commits.num_files] files shown. + Show all files or + adjust limit. +
 Log:
+
[commits.log]
+[end] + +[include "include/footer.ezt"] diff --git a/templates/revision.ezt b/templates/revision.ezt new file mode 100644 index 00000000..f8f7ecb1 --- /dev/null +++ b/templates/revision.ezt @@ -0,0 +1,81 @@ +[# setup page definitions] + [define page_title]Revision [rev][end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "revision"] + +
+
+ + + + + + + + + + + + + + + + + +
Jump to revision: + [for jump_rev_hidden_values][end] + + + [if-any prev_href] + Previous[end] + [if-any next_href] Next[end] +
Author:[if-any author][author][else](unknown author)[end]
Date:[if-any date][date][else](unknown date)[end] + [if-any ago]([ago] ago)[end]
Log Message:
[log]
+
+ +
+ +

Changed paths:

+ +[if-any more_changes] +
+ Only [limit_changes] changes shown, + display [more_changes] more changes... +
+[end] + +[if-any first_changes] + +[end] + + + + + + + + + + [if-any changes] + [for changes] + + + + + [end] + [else] + + + + [end] + +
PathDetails
[if-any changes.view_href][end]Directory[changes.path][is changes.pathtype "dir"]/[end][if-any changes.view_href][end] + [if-any changes.is_copy]
(Copied from [changes.copy_path], r[changes.copy_rev])[end] +
[if-any changes.log_href][end][changes.action][if-any changes.log_href][end] + [if-any changes.text_mods], [if-any changes.diff_href][end]text changed[if-any changes.diff_href][end][end] + [if-any changes.prop_mods], props changed[end] +
No changed paths.
+ +[include "include/footer.ezt"] diff --git a/templates/roots.ezt b/templates/roots.ezt new file mode 100644 index 00000000..5d939ca3 --- /dev/null +++ b/templates/roots.ezt @@ -0,0 +1,29 @@ +[# setup page definitions] + [define page_title]Repository Listing[end] + [define help_href][docroot]/help_rootview.html[end] +[# end] + +[include "include/header.ezt" "directory"] + + + + + + + + + +[for roots] + + + +[end] + + +
Name
+ + + [roots.name] +
+ +[include "include/footer.ezt"] diff --git a/templates/rss.ezt b/templates/rss.ezt new file mode 100644 index 00000000..7accf6e2 --- /dev/null +++ b/templates/rss.ezt @@ -0,0 +1,17 @@ + + + + [rss_link_href] + [rootname] checkins[if-any where] (in [where])[end] + + [is roottype "svn"]Subversion[else]CVS[end] commits to the[if-any where] [where] directory of the[end] [rootname] repository + + [for commits] + [if-any commits.rev][commits.rev]: [end][[commits.author]] [commits.short_log] + [if-any commits.rss_url][commits.rss_url][end] + [commits.author] + [if-any commits.rss_date][commits.rss_date][else](unknown date)[end] + <pre>[format "xml"][commits.log][end]</pre> + [end] + + diff --git a/viewvc-install b/viewvc-install new file mode 100755 index 00000000..353bfb26 --- /dev/null +++ b/viewvc-install @@ -0,0 +1,473 @@ +#!/usr/bin/env python +# -*- Mode: python -*- +# +# Copyright (C) 1999-2007 The ViewCVS Group. All Rights Reserved. +# +# By using this file, you agree to the terms and conditions set forth in +# the LICENSE.html file which can be found at the top level of the ViewVC +# distribution or at http://viewvc.org/license-1.html. +# +# For more information, visit http://viewvc.org/ +# +# ----------------------------------------------------------------------- +# +# Install script for ViewVC +# +# ----------------------------------------------------------------------- + +import os +import sys +import string +import re +import traceback +import py_compile +import getopt +import StringIO + +# Get access to our library modules. +sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), 'lib')) + +import compat +import viewvc +import compat_ndiff +version = viewvc.__version__ + + +## Installer defaults. +DESTDIR = None +ROOT_DIR = None +CLEAN_MODE = None + + +## List of files for installation. +## tuple (source path, +## destination path, +## mode, +## boolean -- search-and-replace? +## boolean -- prompt before replacing? +## boolean -- compile?) +FILE_INFO_LIST = [ + ("bin/cgi/viewvc.cgi", "bin/cgi/viewvc.cgi", 0755, 1, 0, 0), + ("bin/cgi/query.cgi", "bin/cgi/query.cgi", 0755, 1, 0, 0), + ("bin/mod_python/viewvc.py", "bin/mod_python/viewvc.py", 0755, 1, 0, 0), + ("bin/mod_python/query.py", "bin/mod_python/query.py", 0755, 1, 0, 0), + ("bin/mod_python/handler.py", "bin/mod_python/handler.py", 0755, 1, 0, 0), + ("bin/mod_python/.htaccess", "bin/mod_python/.htaccess", 0755, 0, 0, 0), + ("bin/standalone.py", "bin/standalone.py", 0755, 1, 0, 0), + ("bin/loginfo-handler", "bin/loginfo-handler", 0755, 1, 0, 0), + ("bin/cvsdbadmin", "bin/cvsdbadmin", 0755, 1, 0, 0), + ("bin/svndbadmin", "bin/svndbadmin", 0755, 1, 0, 0), + ("bin/make-database", "bin/make-database", 0755, 1, 0, 0), + ("viewvc.conf.dist", "viewvc.conf.dist", 0644, 0, 0, 0), + ("viewvc.conf.dist", "viewvc.conf", 0644, 0, 1, 0), + ("cvsgraph.conf.dist", "cvsgraph.conf.dist", 0644, 0, 0, 0), + ("cvsgraph.conf.dist", "cvsgraph.conf", 0644, 0, 1, 0), + ] +if sys.platform == "win32": + FILE_INFO_LIST.extend([ + ("bin/asp/viewvc.asp", "bin/asp/viewvc.asp", 0755, 1, 0, 0), + ("bin/asp/query.asp", "bin/asp/query.asp", 0755, 1, 0, 0), + ]) + + +## List of directories for installation. +## type (source path, +## destination path, +## boolean -- prompt before replacing?) +TREE_LIST = [ + ("lib", "lib", 0), + ("templates", "templates", 1), + ("templates-contrib", "templates-contrib", 1), + ] + + +## List of file extensions we can't show diffs for. +BINARY_FILE_EXTS = [ + '.png', + '.gif', + '.jpg', + ] + + +def _escape(str): + """Callback function for re.sub(). + + re.escape() is no good because it blindly puts backslashes in + front of anything that is not a number or letter regardless of + whether the resulting sequence will be interpreted.""" + return string.replace(str, "\\", "\\\\") + + +def _actual_src_path(path): + """Return the real on-disk location of PATH, which is relative to + the ViewVC source directory.""" + return os.path.join(os.path.dirname(sys.argv[0]), + string.replace(path, '/', os.sep)) + + +def error(text, etype=None, evalue=None): + """Print error TEXT to stderr, pretty printing the optional + exception type and value (ETYPE and EVALUE, respective), and then + exit the program with an errorful code.""" + sys.stderr.write("\n[ERROR] %s\n" % (text)) + if etype: + traceback.print_exception(etype, evalue, None, file=sys.stderr) + sys.exit(1) + + +def replace_var(contents, var, value): + """Replace instances of the variable VAR as found in file CONTENTS + with VALUE.""" + pattern = re.compile('^' + var + r'\s*=\s*.*$', re.MULTILINE) + repl = '%s = r"%s"' % (var, os.path.join(ROOT_DIR, value)) + return re.sub(pattern, _escape(repl), contents) + + +def replace_paths(contents): + """Replace all ViewVC path placeholders found in file CONTENTS.""" + if contents[:2] == '#!': + shbang = '#!' + sys.executable + contents = re.sub('^#![^\n]*', _escape(shbang), contents) + contents = replace_var(contents, 'LIBRARY_DIR', 'lib') + contents = replace_var(contents, 'CONF_PATHNAME', 'viewvc.conf') + return contents + + +def install_file(src_path, dst_path, mode, subst_path_vars, + prompt_replace, compile_it): + """Install a single file whose source is at SRC_PATH (which is + relative to the ViewVC source directory) into the location + DST_PATH (which is relative both to the global ROOT_DIR and + DESTDIR settings), and set the file's MODE. If SUBST_PATH_VARS is + set, substitute path variables in the file's contents. If + PROMPT_REPLACE is set (and is not overridden by global setting + CLEAN_MODE), prompt the user for how to deal with already existing + files that differ from the to-be-installed version. If COMPILE_IT + is set, compile the file as a Python module.""" + + src_path = _actual_src_path(src_path) + dst_path = os.path.join(ROOT_DIR, string.replace(dst_path, '/', os.sep)) + destdir_path = DESTDIR + dst_path + + overwrite = None + if not (prompt_replace and os.path.exists(destdir_path)): + # If the file doesn't already exist, or we've been instructed to + # replace it without prompting, then drop in the new file and get + # outta here. + overwrite = 1 + else: + # If we're here, then the file already exists, and we've possibly + # got to prompt the user for what to do about that. + + # Collect ndiff output from ndiff + sys.stdout = StringIO.StringIO() + compat_ndiff.main([destdir_path, src_path]) + ndiff_output = sys.stdout.getvalue() + + # Return everything to normal + sys.stdout = sys.__stdout__ + + # Collect the '+ ' and '- ' lines. + diff_lines = [] + looking_at_diff_lines = 0 + for line in string.split(ndiff_output, '\n'): + # Print line if it is a difference line + if line[:2] == "+ " or line[:2] == "- " or line[:2] == "? ": + diff_lines.append(line) + looking_at_diff_lines = 1 + else: + # Compress lines that are the same to print one blank line + if looking_at_diff_lines: + diff_lines.append("") + looking_at_diff_lines = 0 + + # If there are no differences, we're done here. + if not diff_lines: + overwrite = 1 + else: + # If we get here, there are differences. + if CLEAN_MODE == 'true': + overwrite = 1 + elif CLEAN_MODE == 'false': + overwrite = 0 + else: + print "File %s exists and is different from source file." \ + % (destdir_path) + while 1: + name, ext = os.path.splitext(src_path) + if ext in BINARY_FILE_EXTS: + temp = raw_input("Do you want to [O]verwrite or " + "[D]o not overwrite? ") + else: + temp = raw_input("Do you want to [O]verwrite, [D]o " + "not overwrite, or [V]iew " + "differences? ") + temp = string.lower(temp[0]) + if temp == "v" and ext not in BINARY_FILE_EXTS: + print """ +---------------------------------------------------------------------------""" + print string.join(diff_lines, '\n') + '\n' + print """ +LEGEND + A leading '- ' indicates line to remove from installed file + A leading '+ ' indicates line to add to installed file + A leading '? ' shows intraline differences. +---------------------------------------------------------------------------""" + elif temp == "d": + overwrite = 0 + elif temp == "o": + overwrite = 1 + + if overwrite is not None: + break + + assert overwrite is not None + if not overwrite: + print " preserved %s" % (dst_path) + return + + ### If we get here, we're creating or overwriting the existing file. + + # Read the source file's contents. + try: + contents = open(src_path, "rb").read() + except IOError, e: + error(str(e)) + + # (Optionally) substitute ViewVC path variables. + if subst_path_vars: + contents = replace_paths(contents) + + # Ensure the existence of the containing directories. + dst_parent = os.path.dirname(destdir_path) + if not os.path.exists(dst_parent): + try: + compat.makedirs(dst_parent) + print " created %s%s" % (dst_parent, os.sep) + except os.error, e: + if e.errno == 17: # EEXIST: file exists + return + if e.errno == 13: # EACCES: permission denied + error("You do not have permission to create directory %s" \ + % (dst_parent)) + error("Unknown error creating directory %s" \ + % (dst_parent, OSError, e)) + + # Now, write the file contents to their destination. + try: + exists = os.path.exists(destdir_path) + open(destdir_path, "wb").write(contents) + print " %s %s" \ + % (exists and 'replaced ' or 'installed', dst_path) + except IOError, e: + if e.errno == 13: + # EACCES: permission denied + error("You do not have permission to write file %s" % (dst_path)) + error("Unknown error writing file %s" % (dst_path, IOError, e)) + + # Set the files's mode. + os.chmod(destdir_path, mode) + + # (Optionally) compile the file. + if compile_it: + py_compile.compile(destdir_path, destdir_path + "c" , dst_path) + + +def install_tree(src_path, dst_path, prompt_replace): + """Install a tree whose source is at SRC_PATH (which is relative + to the ViewVC source directory) into the location DST_PATH (which + is relative both to the global ROOT_DIR and DESTDIR settings). If + PROMPT_REPLACE is set (and is not overridden by global setting + CLEAN_MODE), prompt the user for how to deal with already existing + files that differ from the to-be-installed version.""" + + orig_src_path = src_path + orig_dst_path = dst_path + src_path = _actual_src_path(src_path) + dst_path = os.path.join(ROOT_DIR, string.replace(dst_path, '/', os.sep)) + destdir_path = os.path.join(DESTDIR + dst_path) + + # Get a list of items in the directory. + files = os.listdir(src_path) + files.sort() + for fname in files: + # Ignore some stuff found in development directories, but not + # intended for installation. + if fname == 'CVS' or fname == '.svn' or fname == '_svn' \ + or fname[-4:] == '.pyc' or fname[-5:] == '.orig' \ + or fname[-4:] == '.rej' or fname[0] == '.' \ + or fname[-1] == '~': + continue + + orig_src_child = orig_src_path + '/' + fname + orig_dst_child = orig_dst_path + '/' + fname + + # If the item is a subdirectory, recurse. Otherwise, install the file. + if os.path.isdir(os.path.join(src_path, fname)): + install_tree(orig_src_child, orig_dst_child, prompt_replace) + else: + set_paths = 0 + compile_it = fname[-3:] == '.py' + install_file(orig_src_child, orig_dst_child, 0644, + set_paths, prompt_replace, compile_it) + + # Check for .py and .pyc files that don't belong in installation. + for fname in os.listdir(destdir_path): + if not os.path.isfile(os.path.join(destdir_path, fname)) or \ + not ((fname[-3:] == '.py' and fname not in files) or + (fname[-4:] == '.pyc' and fname[:-1] not in files)): + continue + + # If we get here, there's cruft. + delete = None + if CLEAN_MODE == 'true': + delete = 1 + elif CLEAN_MODE == 'false': + delete = 0 + else: + print "File %s does not belong in ViewVC %s." \ + % (dst_path, version) + while 1: + temp = raw_input("Do you want to [D]elete it, or [L]eave " + "it as is? ") + temp = string.lower(temp[0]) + if temp == "l": + delete = 0 + elif temp == "d": + delete = 1 + + if delete is not None: + break + + assert delete is not None + if delete: + print " deleted %s" % (os.path.join(dst_path, fname)) + os.unlink(os.path.join(destdir_path, fname)) + else: + print " preserved %s" % (os.path.join(dst_path, fname)) + + + +def usage_and_exit(errstr=None): + stream = errstr and sys.stderr or sys.stdout + stream.write("""Usage: %s [OPTIONS] + +Installs the ViewVC web-based version control repository browser. + +Options: + + --help, -h, -? Show this usage message and exit. + + --prefix=DIR Install ViewVC into the directory DIR. If not provided, + the script will prompt for this information. + + --destdir=DIR Use DIR as the DESTDIR. This is generally only used + by package maintainers. If not provided, the script will + prompt for this information. + + --clean-mode= If 'true', overwrite existing ViewVC configuration files + found in the target directory, and purge Python modules + from the target directory that aren't part of the ViewVC + distribution. If 'false', do not overwrite configuration + files, and do not purge any files from the target + directory. If not specified, the script will prompt + for the appropriate action on a per-file basis. + +""" % (os.path.basename(sys.argv[0]))) + if errstr: + stream.write("ERROR: %s\n\n" % (errstr)) + sys.exit(1) + else: + sys.exit(0) + + +if __name__ == "__main__": + # Option parsing. + try: + optlist, args = getopt.getopt(sys.argv[1:], "h?", + ['prefix=', + 'destdir=', + 'clean-mode=', + 'help']) + except getopt.GetoptError, e: + usage_and_exit(str(e)) + for opt, arg in optlist: + if opt == '--help' or opt == '-h' or opt == '-?': + usage_and_exit() + if opt == '--prefix': + ROOT_DIR = arg + if opt == '--destdir': + DESTDIR = arg + if opt == '--clean-mode': + arg = arg.lower() + if arg not in ('true', 'false'): + usage_and_exit("Invalid value for --overwrite parameter.") + CLEAN_MODE = arg + + # Print the header greeting. + print """This is the ViewVC %s installer. + +It will allow you to choose the install path for ViewVC. You will now +be asked some installation questions. Defaults are given in square brackets. +Just hit [Enter] if a default is okay. +""" % version + + # Prompt for ROOT_DIR if none provided. + if ROOT_DIR is None: + if sys.platform == "win32": + pf = os.getenv("ProgramFiles", "C:\\Program Files") + default = os.path.join(pf, "viewvc-" + version) + else: + default = "/usr/local/viewvc-" + version + temp = string.strip(raw_input("Installation path [%s]: " \ + % default)) + print + if len(temp): + ROOT_DIR = temp + else: + ROOT_DIR = default + + # Prompt for DESTDIR if none provided. + if DESTDIR is None: + default = '' + temp = string.strip(raw_input( + "DESTDIR path (generally only used by package " + "maintainers) [%s]: " \ + % default)) + print + if len(temp): + DESTDIR = temp + else: + DESTDIR = default + + # Install the files. + print "Installing ViewVC to %s%s:" \ + % (ROOT_DIR, DESTDIR and " (DESTDIR = %s)" % (DESTDIR) or "") + for args in FILE_INFO_LIST: + apply(install_file, args) + for args in TREE_LIST: + apply(install_tree, args) + + # Print some final thoughts. + print """ + +ViewVC file installation complete. + +Consult the INSTALL document for detailed information on completing the +installation and configuration of ViewVC on your system. Here's a brief +overview of the remaining steps: + + 1) Edit the %s file. + + 2) Either configure an existing web server to run + %s. + + Or, copy %s to an + already-configured cgi-bin directory. + + Or, use the standalone server provided by this distribution at + %s. +""" % (os.path.join(ROOT_DIR, 'viewvc.conf'), + os.path.join(ROOT_DIR, 'bin', 'cgi', 'viewvc.cgi'), + os.path.join(ROOT_DIR, 'bin', 'cgi', 'viewvc.cgi'), + os.path.join(ROOT_DIR, 'bin', 'standalone.py')) diff --git a/viewvc.conf.dist b/viewvc.conf.dist new file mode 100644 index 00000000..d163a249 --- /dev/null +++ b/viewvc.conf.dist @@ -0,0 +1,784 @@ +#--------------------------------------------------------------------------- +# +# Configuration file for ViewVC +# +# Information on ViewVC is located at the following web site: +# http://viewvc.org/ +# +#--------------------------------------------------------------------------- + +# THE FORMAT OF THIS CONFIGURATION FILE +# +# This file is delineated by sections, specified in [brackets]. Within +# each section, are a number of configuration settings. These settings +# take the form of: name = value. Values may be continued on the +# following line by indenting the continued line. +# +# WARNING: Indentation *always* means continuation. Name=value lines +# should always start in column zero. +# +# Comments should always start in column zero, and are identified +# with "#". +# +# Certain configuration settings may have multiple values. These should +# be separated by a comma. The settings where this is allowed are noted +# below. Any other setting that requires special syntax is noted at that +# setting. +# +# +# SOME TERMINOLOGY USED HEREIN +# +# "root" - This is a CVS or Subversion repository. For Subversion, the +# meaning is pretty clear, as the virtual, versioned directory tree +# stored inside a Subversion repository looks nothing like the actual +# tree visible with shell utilities that holds the repository. For +# CVS, this is more confusing, because CVS's repository layout mimics +# (actually, defines) the layout of the stuff housed in the repository. +# But a CVS repository can be identified by the presence of a CVSROOT +# subdirectory in its root directory. +# +# "module" - A module is a top-level subdirectory of a root, usually +# associated with the concept of a single "project" among many housed +# within a single repository. +# +# +# BASIC VIEWVC CONFIGURATION HINTS +# +# While ViewVC has quite a few configuration options, you generally +# only need to change a small subset of them to get your ViewVC +# installation working properly. Here are some options that we +# recommend you pay attention to. Of course, don't try to change the +# options here -- do so in the relevant section of the configuration +# file below. +# +# For correct operation, you will probably need to change the following +# configuration variables: +# +# cvs_roots (for CVS) +# svn_roots (for Subversion) +# root_parents (for CVS or Subversion) +# default_root +# root_as_url_component +# rcs_dir +# mime_types_file +# the many options in the [utilities] section +# +# It is usually desirable to change the following variables: +# +# address +# forbidden +# +# To optimize delivery of ViewVC static files: +# +# docroot +# +# To customize the display of ViewVC for your site: +# +# template_dir +# the [templates] override section +# + +#--------------------------------------------------------------------------- +[general] + +# +# This setting specifies each of the CVS roots on your system and assigns +# names to them. Each root should be given by a "name: path" value. Multiple +# roots should be separated by commas and can be placed on separate lines. +# +cvs_roots = cvs: /home/cvsroot + +# +# This setting specifies each of the Subversion roots (repositories) +# on your system and assigns names to them. Each root should be given +# by a "name: path" value. Multiple roots should be separated by +# commas and can be placed on separate lines. +# +#svn_roots = svn: /home/svnrepos + +# The 'root_parents' setting specifies a list of directories in which +# any number of repositories may reside. Rather than force you to add +# a new entry to 'cvs_roots' or 'svn_roots' each time you create a new +# repository, ViewVC rewards you for organising all your repositories +# under a few parent directories by allowing you to simply specifiy +# just those parent directories. ViewVC will then notice each +# repository in that directory as a new root whose name is the +# subdirectory of the parent path in which that repository lives. +# +# You can specify multiple parent paths separated by commas or new lines. +# +# Caution: these names can, of course, clash with names you have +# defined in your cvs_roots or svn_roots configuration items. If this +# occurs, you can either rename the offending repository on disk, or +# grant new names to the clashing item in cvs_roots or svn_roots. +# Each parent path is processed sequentially, so repositories under +# later parent paths may override earlier ones. +# +#root_parents = /home/svn-repositories : svn, +# /home/cvs-repositories : cvs + +# This is the name of the default root. Valid names include those +# explicitly listed in the cvs_roots and svn_roots configuration +# options, as well as those implicitly indicated by virtue of being +# the basenames of repositories found in the root_parents option +# locations. +# +# Note: This setting is ignored when root_as_url_component is enabled. +default_root = cvs + +# +# This is a pathname to a MIME types file to help viewvc to guess the +# correct MIME type on checkout. If you are having problems with the +# default guess on the MIME type, then uncomment this option and point +# it at a MIME type file. +# +# For example, you can use the mime.types provided by Apache here: +#mime_types_file = /usr/local/apache2/conf/mime.types + +# The address of the local repository maintainer. (This option is +# provided only as a convenience for ViewVC installations which are +# using the default template set, where the value of this option will +# be displayed in the footer of every ViewVC page.) +address = + +# +# This option provides a mechanism for custom key/value pairs to be +# available to templates. These are stored in key/value (KV) files. +# +# The paths of the KV files are listed here, specified either as +# absolute paths or relative to this configuration file. The files +# use the same format as this configuration file, containing one or +# more user-defined sections, and user-defined options in those +# sections. ViewVC makes these options available to template authors +# as: +# +# kv.SECTION.OPTION +# +# Note that an option name can be dotted. For example: +# +# [my_images] +# logos.small = /images/small-logo.png +# logos.big = /images/big-logo.png +# +# Templates can use these with a directive like: [kv.my_images.logos.small] +# +# Note that section names which are common to multiple KV files will +# be merged. If two files have a [my_images] section, then the +# options in those two like-named sections will be merged together. +# If two files have the same option name in a section, then one will +# overwrite the other (and which one "wins" is unspecified). +# +# To further categorize the KV files, and how the values are provided to +# the templates, a KV file name may be annotated with an additional level +# of dotted naming. For example: +# +# kv_files = [asf]kv/images.conf +# +# Assuming the same section as above, the template would refer to an image +# using [kv.asf.my_images.logos.small] +# +# Lastly, it is possible to use %lang% in the filenames to specify a +# substitution of the selected language-tag. +# +# Example: +# kv_files = kv/file1.conf, kv/file2.conf, [i18n]kv/%lang%_data.conf +# +kv_files = + +# +# This option is a comma-separated list of language-tag values +# available to ViewVC. The first language-tag listed is the default +# language, and will be used if an Accept-Language header is not +# present in the request, or none of the user's requested languages +# are available. If there are ties on the selection of a language, +# then the first to appear in the list is chosen. +# +# Some examples: +# +# languages = en-us, de +# languages = en-us, en-gb, de +# languages = de, fr, en-us +# +languages = en-us + + +#--------------------------------------------------------------------------- +[utilities] + +# ViewVC uses (sometimes optionally) various third-party programs to do some +# of the heavy lifting. Generally, it will attempt to execute those utility +# programs in such a way that if they are found in ViewVC's executable +# search path ($PATH, %PATH%, etc.) all is well. But sometimes these tools +# aren't installed in the executable search path, so here's where you can +# tell ViewVC where to find them. +# +# NOTE: Options with a "_dir" suffix are for configuring the directories +# in which certain programs live; otherwise, the option value should +# point to the actual program. + +# RCS utilities, used for viewing CVS repositories +rcs_dir = +# rcs_dir = /usr/bin/ + +# ViewVC can use CVSNT (www.cvsnt.org) instead of the RCS utilities to +# retrieve information from CVS repositories. To enable use of CVSNT, +# set the "cvsnt" value to the path of the CVSNT executable. (If CVSNT +# is on the standard path, you can also set it to the name of the +# CVSNT executable). By default "cvsnt" is set to "cvs" on Windows and +# is not set on other platforms. +cvsnt = +# cvsnt = +# cvsnt = cvs +# cvsnt = K:\Program Files\cvsnt\cvs.exe +# cvsnt = = /usr/bin/cvs + +# Subversion command-line client, used for viewing Subversion repositories +svn = +# svn = /usr/bin/svn + +# GNU diff, used for showing file version differences +diff = +# diff = /usr/bin/diff + +# CvsGraph, a graphical CVS version graph generator (see options.use_cvsgraph) +cvsgraph = +# cvsgraph = /usr/local/bin/cvsgraph + + +#--------------------------------------------------------------------------- +[options] + +# root_as_url_component: Interpret the first path component in the URL +# after the script location as the root to use. This is an +# alternative to using the "root=" query key. If ViewVC is configured +# with multiple repositories, this results in more natural looking +# ViewVC URLs. +# Note: Enabling this option will break backwards compatibility with +# any old ViewCVS URL which doesn't have an explicit "root" parameter. +root_as_url_component = 1 + +# checkout_magic: Use checkout links with magic /*checkout*/ prefixes so +# checked out HTML pages can have working links to other repository files +# Note: This option is DEPRECATED and should not be used in new ViewVC +# installations. Setting "default_file_view = co" achieves the same effect +checkout_magic = 0 + +# allowed_views: List the ViewVC views which are enabled. Views not +# in this comma-delited list will not be served (or, will return an +# error on attempted access). +# Possible values: "tar", "annotate", "co", "markup", "roots" +allowed_views = markup, annotate, roots + +# authorizer: The name of the ViewVC authorizer plugin to use when +# authorizing access to repository contents. This value must be the +# name of a Python module addressable as vcauth.MODULENAME (most +# easily accomplished by placing it in ViewVC's lib/vcauth/ directory) +# and which implements a ViewVCAuthorizer class (as a subclass of +# vcauth.GenericViewVCAuthorizer). You can provide custom parameters +# to the authorizer module by defining configuration sections named +# authz-MODULENAME and adding the parameter keys and values there. +# +# ViewVC provides the following modules: +# svnauthz - based on Subversion authz files +# forbidden - simple path glob matches against top-level root directories +# forbiddenre - root and path matches against regular expressions +# +# NOTE: Only one authorizer may be in use for a given ViewVC request. +# It doesn't matter if you configure the parameters of multiple +# authorizer plugins -- only the authorizer whose name is configured +# here (or effectively configured here via vhost configuration) will +# be activated. +authorizer = forbidden + +# hide_cvsroot: Don't show the CVSROOT directory +# 1 Hide CVSROOT directory +# 0 Show CVSROOT directory +# NOTE: Someday this option may be removed in favor of letting +# individual authorizer plugin hide the CVSROOT. +hide_cvsroot = 1 + +# mangle_email_addresses: Mangle email addresses in marked-up output. +# There are various levels of mangling available: +# 0 - No mangling; markup un-mangled email addresses as hyperlinks +# 1 - Obfuscation (using entity encoding); no hyperlinking +# 2 - Data-dropping address truncation; no hyperlinking +# Note: this will not effect the display of versioned file contents, only +# addresses that appear in version control metadata (e.g. log messages). +mangle_email_addresses = 0 + +# default_file_view: "log", "co", or "markup" +# Controls whether the default view for file URLs is a checkout view or +# a log view. "log" is the default for backwards compatibility with old +# ViewCVS URLs, but "co" has the advantage that it allows ViewVC to serve +# static HTML pages directly from a repository with working links +# to other repository files +# Note: Changing this option may break compatibility with existing +# bookmarked URLs. +# Also note: If you choose one of the "co" or "markup" views, be sure +# to enable it (via the allowed_views option) +default_file_view = log + +# http_expiration_time: Expiration time (in seconds) for cacheable +# pages served by ViewVC. Note that in most cases, a cache aware +# client will only revalidate the page after it expires (using the +# If-Modified-Since and/or If-None-Match headers) and that browsers +# will also revalidate the page when the reload button is pressed. +# Set to 0 to disable the transmission of these caching headers. +http_expiration_time = 600 + +# generate_etags: Generate Etag headers for relevant pages to assist +# in browser caching. +# 1 Generate Etags +# 0 Don't generate Etags +generate_etags = 1 + +# svn_config_dir: Path of the Subversion runtime configuration +# directory ViewVC should consult for various things, including cached +# remote authentication credentials. If unset, Subversion will use +# the default location(s) ($HOME/.subversion, etc.) +svn_config_dir = + +# use the rcsparse Python module to retrieve CVS repository +# information instead of invoking rcs utilities [EXPERIMENTAL] +use_rcsparse = 0 + +# sort_by: File sort order +# file Sort by filename +# rev Sort by revision number +# date Sort by commit date +# author Sort by author +# log Sort by log message +sort_by = file + +# sort_group_dirs: Group directories when sorting +# 1 Group directories together +# 0 No grouping -- sort directories as any other item would be sorted +sort_group_dirs = 1 + +# hide_attic: Hide or show the contents of the Attic subdirectory +# 1 Hide dead files inside Attic subdir +# 0 Show the files which are inside the Attic subdir +hide_attic = 1 + +# hide_errorful_entries: Hide or show errorful directory entries +# (perhaps due to not being readable, or some other rlog parsing +# error, etc.) +# 1 Hide errorful entries from the directory display +# 0 Show errorful entries (with their errors) in the directory display +hide_errorful_entries = 0 + +# log_sort: Sort order for log messages +# date Sort revisions by date +# rev Sort revision by revision number +# none Use the version control system's ordering +log_sort = date + +# diff_format: Default diff format +# h Human readable +# u Unified diff +# c Context diff +# s Side by side +# l Long human readable (more context) +# f Full human readable (entire file) +diff_format = h + +# hr_breakable: Diff view line breaks +# 1 lines break at spaces +# 0 no line breaking +# Or, use a positive integer > 1 to cut lines after that many characters +hr_breakable = 1 + +# give out function names in human readable diffs +# this just makes sense if we have C-files, otherwise +# diff's heuristic doesn't work well .. +# ( '-p' option to diff) +hr_funout = 0 + +# ignore whitespaces for human readable diffs +# (indendation and stuff ..) +# ( '-w' option to diff) +hr_ignore_white = 0 + +# ignore diffs which are caused by +# keyword-substitution like $Id - Stuff +# ( '-kk' option to rcsdiff) +hr_ignore_keyword_subst = 1 + +# Enable highlighting of intraline changes in human readable diffs +# this feature is experimental and currently requires python 2.4 +hr_intraline = 0 + +# allow compression with gzip of output if the Browser accepts it +# (HTTP_ACCEPT_ENCODING contains "gzip") +#allow_compress = 1 + +# The directory which contains the EZT templates used by ViewVC to +# customize the display of the various output views. ViewVC looks in +# this directory for files with names that match the name of the view +# ("log", "directory", etc.) plus the ".ezt" extension. If specified +# as a relative path, it is relative to the directory where this config +# file resides; absolute paths may be used as well. +# +# If %lang% occurs in the pathname, then the selected language will be +# substituted. +# +# See also the [templates] configuration section, where you can +# override templates on a per-view basis. +# +template_dir = templates + +# Web path to a directory that contains ViewVC static files +# (stylesheets, images, etc.) If set, static files will get +# downloaded directory from this location. If unset, static files +# will be served by the ViewVC script (at a likely performance +# penalty, and from the "docroot" subdirectory of the directory +# specified by the "template_dir" option). +#docroot = /docroot + +# Show last changelog message for CVS subdirectories +# NOTE: The current implementation makes many assumptions and may show +# the incorrect file at some times. The main assumption is that the +# last modified file has the newest filedate. But some CVS operations +# touches the file without even when a new version is not checked in, +# and TAG based browsing essentially puts this out of order, unless +# the last checkin was on the same tag as you are viewing. Enable +# this if you like the feature, but don't rely on correct results. +# +# ** WARNING: Enabling this will currently leak unauthorized path names ** +show_subdir_lastmod = 0 + +# Show the most recent log entry in directory listings. +show_logs = 1 + +# Show log when viewing file contents +show_log_in_markup = 1 + +# Cross filesystem copies when traversing Subversion file revision histories. +cross_copies = 1 + +# Display dates as UTC or in local time zone +use_localtime = 0 +#use_localtime = 1 + +### CONFIGURATION DEFAULTS ### +### +### Defaults for configuration variables that shouldn't need +### to be configured.. + +# the length to which the most recent log entry should be truncated when +# shown in the directory view +short_log_len = 80 + +# should we colorize known file content syntaxes? (requires Pygments module) +enable_syntax_coloration = 1 + +# Use CvsGraph. See http://www.akhphd.au.dk/~bertho/cvsgraph/ for +# documentation and download. +use_cvsgraph = 0 +#use_cvsgraph = 1 + +# Location of the customized cvsgraph configuration file. +cvsgraph_conf = cvsgraph.conf + +# +# Set to enable regular expression search of all files in a directory +# +# WARNING: +# +# Enabling this option can consume HUGE amounts of server time. A +# "checkout" must be performed on *each* file in a directory, and +# the result needs to be searched for a match against the regular +# expression. +# +# +# SECURITY WARNING: Denial Of Service +# +# Since a user can enter the regular expression, it is possible for +# them to enter an expression with many alternatives and a lot of +# backtracking. Executing that search over thousands of lines over +# dozens of files can easily tie up a server for a long period of +# time. +# +# This option should only be used on sites with trusted users. It is +# highly inadvisable to use this on a public site. +# +use_re_search = 0 +# use_re_search = 1 + +# +# Split directories and logs into pages. +# Allows ViewVC to present discrete pages to the users instead of the +# entire log or directory. +# Set use_pagesize to the number of entries you want displayed on a page. +# +use_pagesize = 0 +# use_pagesize = 20 + +# Limit number of changed paths shown per commit in the Subversion revision +# view and in query results. This is not a hard limit (the UI provides +# options to show all changed paths), but it prevents ViewVC from generating +# enormous and hard to read pages by default when they happen to contain +# import or merge commits affecting hundreds or thousands of files. +# Set to 0 to disable the limit. +limit_changes = 100 + +#--------------------------------------------------------------------------- +[templates] + +# You can override the templates used by various ViewVC views in this +# section. By default, ViewVC will look for templates in the +# directory specified by the "template_dir" configuration option (see +# the documentation for that option for details). But if you want to +# use a different template for a particular view, simply uncomment the +# appropriate option below and specify the currect location of the EZT +# template file you wish to use for that view. +# +# Templates are specified relative to the configured template +# directory (see the "template_dir" option), but absolute paths may +# also be used as well. +# +# If %lang% occurs in the pathname, then the selected language will be +# substituted. +# +# Note: the selected language is defined by the "languages" item in the +# [general] section, and based on the request's Accept-Language +# header. +# +#diff = diff.ezt +#directory = directory.ezt +### an alternative directory view +#directory = dir_new.ezt +#error = error.ezt +#file = file.ezt +#graph = graph.ezt +#log = log.ezt +### a table-based alternative log view +#log = log_table.ezt +#query = query.ezt +#query_form = query_form.ezt +#query_results = query_results.ezt +#revision = revision.ezt +#roots = roots.ezt + +#--------------------------------------------------------------------------- +[cvsdb] + +# Set to 1 to enable the database integration feature, 0 otherwise. +enabled = 0 + +# Database hostname and port. +#host = localhost +#port = 3306 + +# ViewVC database name. +#database_name = ViewVC + +# Username and password of user with read/write privileges to the ViewVC +# database. +#user = +#passwd = + +# Username and password of user with read privileges to the ViewVC +# database. +#readonly_user = +#readonly_passwd = + +# Limit the number of rows returned by a given query to this number. +#row_limit = 1000 + +# Limit the number of rows returned by a given query made as part an +# RSS feed request to this number. (Keeping in mind that RSS readers +# tend to poll regularly for new data, you might want to keep this set +# to a conservative number.) +#rss_row_limit = 100 + +# Check if the repository is found in the database before showing +# the query link and RSS feeds. Set to 1 to enable check. +# +# WARNING: Enabling this check adds the cost of a database connection +# and query to most ViewVC requests. If all your roots are represented +# in the commits database, or if you don't care about the creation of +# RSS and query links that might lead ultimately to error pages for +# certain of your roots, or if you simply don't want to add this extra +# cost to your ViewVC requests, leave this disabled. +#check_database_for_root = 0 + +#--------------------------------------------------------------------------- +[vhosts] + +# Virtual hosts are individual logical servers accessible via +# different hostnames, but which are all really the same physical +# computer. For example, you might have your web server configured to +# accept incoming traffic for both http://www.yourdomain.com/ and +# http://viewvc.yourdomain.com/. Users pointing their web browsers at +# each of those two URLs might see entirely different content via one +# URL versus the other, but all that content actually lives on the +# same computer, is served up via the same web server, and so +# on. It just *looks* like its coming from multiple servers. +# +# ViewVC allows you to customize its configuration options for +# individual virtual hosts. You might, for example, wish to expose +# all of your Subversion repositories at http://svn.yourdomain.com/viewvc/ +# and all your CVS ones at http://cvs.yourdomain.com/viewvc/, with no +# cross-exposure. Using ViewVC's virtual host (vhost) configuration +# support, you can do this. Simply create two vhost configurations +# (one for each of your hostnames), then configure the cvs_roots +# option only for the vhost associated with cvs.yourdomain.com, and +# configure the svn_roots option only for the vhost associated with +# svn.yourdomain.com. +# +# This section is a freeform configuration section, where you create +# both the option names and their values. The names of the options +# are then treated as canonical names of virtual hosts, and their +# values are defined to be comma-delimited lists of hostname globs +# against which incoming ViewVC requests will be matched to figure out +# which vhost they apply to. +# +# After you've named and defined your vhosts, you may then create new +# configuration sections whose names are of the form +# vhost-VHOSTNAME/CONFIGSECTION. VHOSTNAME here is the canonical name +# of one of the virtual hosts you defined under the [vhosts] section. +# Inside those configuration sections, you override the standard +# ViewVC options typically found in the base configuration section +# named CONFIGSECTION ("general", "option", etc.) +# +# Here is an example: +# +# [vhosts] +# libs = libs.yourdomain.*, *.yourlibs.* +# gui = guiproject.yourdomain.* +# +# [vhost-libs/general] +# cvs_roots = +# svn_roots = svnroot: /var/svn/libs-repos +# default_root = svnroot +# +# [vhost-libs/options] +# show_logs = 1 +# +# [vhost-gui/general] +# cvs_roots = cvsroot: /var/cvs/guiproject +# svn_roots = +# default_root = cvsroot +# + +#--------------------------------------------------------------------------- +# ViewVC recognizes per-root configuration overrides, too. To +# override the value of a configuration parameter only for a single +# root, create a configuration section whose names is of the form +# root-ROOTNAME/CONFIGSECTION. ROOTNAME here is the name of the root +# as defined explicitly in cvs_roots or svn_roots or implicitly as the +# basename of a root path in root_parents. Options found in this new +# configuration section override for this one root the corresponding +# options found in the base configuration section CONFIGSECTION +# ("options", "authz-*", etc.) +# +# Here is an example showing how to enable Subversion authz-based +# authorization for only the single root named "svnroot": +# +# [root-svnroot/options] +# authorizer = svnauthz +# +# [root-svnroot/authz-svnauthz] +# authzfile = /path/to/authzfile +# + +#--------------------------------------------------------------------------- +[authz-forbidden] + +# The "forbidden" authorizer forbids access to repository modules, +# defined to be top-level subdirectories in a repository. You can use +# a simple list of modules, or something more complex: +# +# *) The "!" can be used before a module to explicitly state that it +# is NOT forbidden. Whenever this form is seen, then all modules will +# be forbidden unless one of the "!" modules match. +# +# *) Shell-style "glob" expressions may be used. "*" will match any +# sequence of zero or more characters, "?" will match any single +# character, "[seq]" will match any character in seq, and "[!seq]" +# will match any character not in seq. +# +# *) Tests are performed in sequence. The first match will terminate the +# testing. This allows for more complex allow/deny patterns. +# +# Tests are case-sensitive. +# +# NOTE: Again, this is for the hiding of modules within repositories, *not* +# for the hiding of repositories (roots) themselves. +# +# Some examples: +# +# Disallow "example" but allow all others: +# forbidden = example +# +# Disallow "example1" and "example2" but allow all others: +# forbidden = example1, example2 +# +# Allow *only* "example1" and "example2": +# forbidden = !example1, !example2 +# +# Forbid modules starting with "x": +# forbidden = x* +# +# Allow modules starting with "x" but no others: +# forbidden = !x* +# +# Allow "xml", forbid other modules starting with "x", and allow the rest: +# forbidden = !xml, x*, !* +# +forbidden = + +#--------------------------------------------------------------------------- +[authz-forbiddenre] + +# The "forbiddenre" authorizer forbids access to repositories and +# repository paths by comparing a list of regular expressions +# (separated by commas) against paths consisting of the repository (or +# root) name plus the path of the versioned file or directory to be +# tested. For example, to see if the user is authorized to see the +# path "/trunk/www/index.html" in the repository whose root name is +# "svnrepos", this authorizer will check the path +# "svnrepos/trunk/www/index.html" against the list of forbidden +# regular expressions. Directory paths will be terminated by a forward +# slash. +# +# Like the "forbidden" authorizer... +# +# *) The "!" can be used before a module to explicitly state that it +# is NOT forbidden. Whenever this form is seen, then all modules will +# be forbidden unless one of the "!" modules match. +# +# *) Tests are performed in sequence. The first match will terminate the +# testing. This allows for more complex allow/deny patterns. +# +# Unlike the "forbidden" authorizer, you can can use this to hide roots, too. +# +# Some examples: +# +# Disallow files named "PRIVATE", but allow all others: +# forbiddenre = /PRIVATE$ +# +# Disallow the "hidden" repository, allowing all others: +# forbiddenre = ^hidden(/|$) +# +# Allow only the "example1" and "example2" roots and the paths inside them, +# disallowing all others (which can be done in multiple ways): +# forbiddenre = !^example1(/|$), !^example2(/|$)/ +# forbiddenre = !^example[12](/|$) +# +# Only allow visibility of HTML files and the directories that hold them: +# forbiddenre = !^([^/]+|.*(/|\.html))$ +# +forbiddenre = + +#--------------------------------------------------------------------------- +[authz-svnauthz] + +# The "svnauthz" authorizer uses a Subversion authz configuration file +# to determine access to repository paths. This option specifies the +# location of that file using an absolute path. +# +authzfile = + +#--------------------------------------------------------------------------- diff --git a/windows/README b/windows/README new file mode 100644 index 00000000..67558743 --- /dev/null +++ b/windows/README @@ -0,0 +1,521 @@ +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +DESCRIPTION + +This file contains special instructions for setting up ViewVC on +Windows. It will take you through a basic installation and tell you +how to set up optional features like code colorizing and the MySQL +commit database. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +REQUIREMENTS + +ViewVC requires the Python interpreter which you can download from + + http://python.org/ + +and the Python for Windows Extensions which are at + + http://sourceforge.net/projects/pywin32/ + +For CVS support, ViewVC also requires that the CVSNT client (cvs.exe) +OR the RCS tools (rlog.exe, rcsdiff.exe, and co.exe) be installed on +your computer. CVSNT is available from + + http://www.cvsnt.org/wiki + +and RCS can be obtained from: + + http://www.cs.purdue.edu/homes/trinkle/RCS/ + +For Subversion support, you'll need to have the Subversion Python +bindings installed. Binaries are available from the Subversion website +at: + + http://subversion.tigris.org/servlets/ProjectDocumentList?folderID=91 + +Note that if you use binaries, you have to be running the same version +of python as the binaries were built for. For example, you cannot use +Subversion bindings built for Python 2.3 with Python 2.4. Instructions +for building the binaries from source are available here: + + http://svn.collab.net/repos/svn/trunk/subversion/bindings/swig/INSTALL + +The Subversion bindings also require you to have diff.exe installed in +a directory on your system PATH. diff.exe is available as part of the +GnuWin32 project's DiffUtils package at http://gnuwin32.sf.net/. + +Once you've got Python and CVSNT or RCS or the Subversion bindings +installed, you can test out ViewVC before you install it by running: + + python bin\standalone.py -r + +The standalone server has a number of features (including a GUI +interface) which you can find out about by running + + python bin\standalone.py --help + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +BASIC INSTALLATION + +Run the ViewVC install script with + + python viewvc-install + +The script will copy the source files into an installation directory +that you specify, store some path information, and compile the ViewVC +library files into Python bytecode. + +After the installation is finished you will need to edit the +viewvc.conf file in the folder you installed to. The comments in that +file tell you exactly what to do. + +With the config file set up you should be able to double-click +standalone.py and access your repository with a web browser. + +See the sections below for information on setting up optional features +and troubleshooting. From here on will stand for the Python +root directory (usually something like C:\Python22) and + will represent the directory where ViewVC has +been installed to (default is C:\Program Files\viewvc-VERSION). + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +SERVER CONFIGURATION + +If you want to make ViewVC available to a network (rather than using +it on a standalone machine), you will need to configure a web server +to host it. This section includes instructions for setting up ViewVC +on two commonly used Windows web servers, IIS and Apache. + +With IIS, you can run ViewVC in CGI mode or ASP mode (or both +modes). ASP mode gives better performance (faster page loads), but is +harder to set up and may be less stable. CGI mode is stable and easy +to set up but slower. + +On Apache, you can use CGI mode or Mod_Python mode or both modes at +once. Mod_Python mode is faster than CGI mode. + +CGI Mode On IIS + + 1) Copy viewvc.cgi and query.cgi from \bin\cgi to a + folder that is accessible through your web server. + + 2) Start up the IIS "Internet Services Manager" and right click a + virtual server or virtual directory that contains the files you + just copied. Choose "Properties" from the context menu that + appears. + + 3) On the properties dialog that appears, navigate to [Home | + Virtual] Directory -> Application Settings -> + Configuration. This will bring up another dialog box called + "Application Configuration". + + 4) On the "App Mappings" tab choose "Add". Fill in the following + information: + + Executable: \python.exe "%s" + Extension: cgi + Script Engine: checked + Check that file exists: unchecked + + That is all. Assuming you've set up viewvc.conf to point to your + repositories, the CGI pages should run. See the Troubleshooting + section below if there are any problems. + +ASP Mode On IIS + + In order to run ViewVC with ASP, you will need to enable Python + ActiveX scripting and to install the included Aspfool ISAPI filter + on whatever virtual server is being used to serve the viewvc + pages. Step by step instructions follow below. Aspfool is located + in the windows\aspfool folder. + + To set up ASP mode, follow these steps: + + 1) Run \Lib\site-packages\win32comext\axscript\client\pyscript.py + to register Python as an ASP scripting language. (More + documentation on this is at + http://www.python.org/windows/win32com/ActiveXScripting.html) + + 2) Copy the viewvc.asp and query.asp files from + \bin\asp to a folder that is accessible + through your web server. + + 3) Start up the IIS "Internet Services Manager" and right click on + the virtual server that contains the files you just + copied. Choose "Properties" from the context menu that appears. + + 4) On the properties dialog that appears, click the "ISAPI Filters" + tab. Click the "add" button and enter the following + information: + + Filter Name: aspfool + Executable: aspfool.dll + + After you save these changes, the ViewVC ASP pages should begin to + work. + +CGI Mode on Apache + + Follow the instructions under "Apache Configuration" in the ViewVC + INSTALL file. + +Mod_Python Mode on Apache + + There are probably ten thousand different ways to set up Apache, + mod_python, and ViewVC together. Here are some instructions that + work for me using Mod_Python 3.0.3 and Apache 2.0.46. If any Apache + gurus want to contribute better instructions, I'd be happy to + include them here. + + 1) Run the win32 mod_python installer from www.modpython.org. + + 2) Add the following line to the "Global Environment" section of + httpd.conf: + + LoadModule python_module modules/mod_python.so + + 3) Copy viewvc.py, query.py, handler.py, and .htaccess from + \bin\mod_python to a folder being served by + apache. Make sure overrides are allowed in this folder. The + relevant parent directory in httpd.conf should have + "AllowOverride All" set, or at least "AllowOverride FileInfo + Options". + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +ENSCRIPT HIGHLIGHTING + +To use enscript, you'll have to install the enscript, libintl, +libiconv, and sed packages from the gnuwin32 project +(http://gnuwin32.sourceforge.net/). Detailed instructions are on +their site, but here is the basic procedure. + +1) Extract all packages to a folder on your hard drive, for example + c:\gnuwin32 + +2) Add "c:\gnuwin32\bin" to the system "PATH" environment variable. If + ViewVC is running as part of a system service like IIS you will + need to reboot the computer so it is able to see the value. See the + "Troubleshooting" section below for specific information on when a + reboot is neccessary. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +BONSAI-LIKE CHECKIN DATABASE + +To use the checkin database, you'll need to install MySQL and the +MySQL-Python interface. MySQL can be downloaded from +www.mysql.com. The MySql-Python adapter is available from +http://sf.net/projects/mysql-python/. Make sure to grab the the latest +version from the "Files" section. (The "Home Page" link takes you to +an outdated page that only links to very old versions.) Both packages +come with GUI installers. Once you have MySQL running and set up with +a username and password, follow these instructions to set up ViewVC. + +1) Open a command prompt and type these commands: + + cd /d + python bin\make-database + + The script that comes up will prompt you for the MySQL username and + password (you should have created these during the MySQL + installation), and the name of the database to create. The default + database name "ViewVC" should be fine unless for some reason a + database with that name already exists. + +2) Enter the username, password, and database name into the [cvsdb] + section of the \viewvc.conf file. + +3) At the command prompt run + + python bin\cvsdbadmin rebuild + + where is the path to your CVS repository. + +4) If you want the checkin database to be dynamically updated with + every checkin, add the following line to your CVSROOT/loginfo file: + + ALL python "\bin\loginfo-handler" %{sVv} + + If you decide not to enable dynamic updates, you can periodically + refresh the database with "python bin\cvsdbadmin update " + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +CVSGRAPH + +To use CvsGraph with ViewVC, just put cvsgraph.exe in a directory on +your system PATH and set the use_cvsgraph option to 1 in your +viewvc.conf file. + +The CvsGraph home page is http://www.akhphd.au.dk/~bertho/cvsgraph/. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +DOCROOT OPTIMIZATION + +By default ViewVC serves up image and stylesheet files in +\templates\docroot\ on its own instead of relying +on the webserver to deliver them. This simplifies web server +configuration, but is inefficient because it means the Python +interpreter has to run each time one of these files is +downloaded. This causes ViewVC pages to load more slowly, especially +when ViewVC is running under CGI on Windows. + +To make things more efficient, you can make the +\templates\docroot directory available on your web +server and then set the "docroot" value in viewvc.conf to point to the +web address of the directory. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +TROUBLESHOOTING + +- By far the most common cause of errors in ViewVC is failure to + successfully execute the programs it depends on (cvs, rlog, rcsdiff, + co, enscript, sed, and cvsgraph). To help deal with this problem, + ViewVC includes a special debugging mode that displays output from + the programs it executes on every web page. This allows you to see + error messages and other information that isn't normally visible. To + enable the debugging mode, change line 23 of + \lib\debug.py from: + + SHOW_CHILD_PROCESSES = 0 + + to: + + SHOW_CHILD_PROCESSES = 1 + + Important: You may need to restart your web server before this + change takes effect. See "Changes made to..." later in this section. + +- If you see the following error: + + error: (2, 'CreateProcess', The system cannot find the file specified.') + + it means that a program ViewVC has tried to execute could not be + found by Windows. The fix to this is usually to install the program + if it isn't already installed or to update the path to the program + in viewvc.conf. Enabling the SHOW_CHILD_PROCESSES mode as described + above can provide helpful diagnostic information such as the command + line ViewVC is using to invoke the program and the value of the PATH + environment variable in the environment ViewVC is running under. + +- A common cause of server errors under IIS is permissions + problems. You need to make sure that the virtual directory + containing the CGI or ASP files has script execution enabled. You + also need to make sure that the web server user accounts + (IUSR_machine_name and IWAM_machine_name, where machine_name is your + computer name) have read and execute access to the .asp or .cgi stub + scripts, the ViewVC lib/ folder, the paths where external tools like + cvs, rcs, enscript, sed, and cvsgraph live, and the CVS + repositories. NTFS auditing makes it very easy to track down + permissions problems. Also look for IIS messages in the event log. + +- Certain Apache configurations may hide some environment variables + from the ViewVC CGI scripts and the programs they launch. You can + see whether an environment variable is visible from the CGI + environment by enabling the SHOW_CHILD_PROCESSES debug mode + described above. You can force Apache to let variables through with + the PassEnv directive + (http://httpd.apache.org/docs/mod/mod_env.html#passenv). + +- Changes made to environment variables, ViewVC source files and the + ViewVC configuration file do not always take effect immediately. The + table below shows what actions you need to take after changing any + of these things before they will have an effect. + ++----------------+----------------+----------------+-------------------------+ +| | Environment | ViewVC | ViewVC | +| | Variables | Source | Configuration | ++----------------+----------------+----------------+-------------------------+ +| Standalone | restart | restart | restart | +| Server | standalone.py* | standalone.py | standalone.py | ++----------------+----------------+----------------+-------------------------+ +| CGI mode under | reboot | nothing | nothing | +| apache or IIS | computer | | | ++----------------+----------------+----------------+-------------------------+ +| mod_python or | reboot | restart Apache | restart Apache | +| under apache | computer | | OR | +| | | | reload(viewvc) in stub | ++----------------+----------------+----------------+-------------------------+ +| asp mode under | reboot | restart IIS | restart IIS | +| IIS | computer | OR | OR | +| | | Unload ASP App | Unload ASP App | +| | | | OR | +| | | | reload(viewvc) in stub | ++----------------+----------------+----------------+-------------------------+ + * If standalone.py was launched from a command prompt and you set + the environment variable through the control panel, you'll need to + open a new command prompt. + + Notes: + + Under ASP, changes made to the stub scripts inside the web root do + take effect immediately, you only need to take additional action + when you make changes to the main source files in \lib + + To "Unload ASP App", go to the IIS properties dialog for the + application directory containing the ViewVC .asp files (in Internet + Services Manager). Switch to the [Home] | [Virtual] Directory tab + and click the "Unload" button under "Application Settings". + + To "reload(viewvc) in stub", put these lines in one of the ASP or + Mod_Python stub scripts: + + import viewvc + reload (viewvc) + + then load the page in a web browser. + +- If you have problems getting ViewVC to work with mod_python, you can + first make sure mod_python works standalone with the testing + instructions at + http://www.modpython.org/live/current/doc-html/inst-testing.html. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +KNOWN ISSUES + +- If you see ViewVC errors like + + Error parsing rlog output. Expected RCS file + "c:\cvsroot\dir\file,v", found "c:\cvsroot\dir\RCS/file,v" + + it's because your RCS utilities don't recognize the RCS file suffix + and are treating all files specified on the command line like + working copies even when they end in ",v". You can fix this by + including the following string in your RCSINIT environment variable: + + -x,v + + Important: You may need to reboot your computer before the + environment variable has an effect. See "Changes made to..." in the + TROUBLESHOOTING section. + +- The GNU RCS utilities won't work with repository files that use + CVSNT's unicode expansion mode (-ku). Files that use this mode will + show up with an "rlog error: unknown expand mode u" error message in + ViewVC directory listings. To work around this, you can set up + ViewVC to use the CVSNT executable (cvs.exe) or CVSNT RCS tools + (co.exe, rlog.exe, and rcsdiff.exe) instead of the GNU tools. + +- The standalone server will not run under Cygwin Python because it + does not support threads. ASP pages can't be run with Cygwin Python + because it does not support ActiveX. To use either of these features + you should install a native Python interpreter. + +- On Windows XP and Windows 2003 Server under IIS, enscript might give + an error like: + + enscript: couldn't open input filter "states -f + "K:/gnuwin32/share/enscript/hl/enscript.st" -p + "C://.enscript;K:/gnuwin32/share/enscript/hl" -shtml -Dcolor=1 + -Dstyle=emacs -Dlanguage=html -Dnum_input_files=1 + -Ddocument_title="Enscript Output" -Dtoc=0 -" for file "": No error + no output generated + + The solution is to give read & execute permissions on cmd.exe to the + IUSR_computername and IWAM_computername user accounts. (Enscript + uses cmd.exe internally to launch its little helper program, + states.exe). + +- By default, ASP will set session cookies at each page load. ViewVC + does not use these cookies and they can be safely disabled. You can + do this by opening the IIS properties dialog for the application + directory containing the ViewVC .asp files. Go to the [Home] | + [Virtual] Directory tab and click the "Configuration" button under + "Application Settings". On the dialog that comes up, uncheck "Enable + Session State" under "App Options" -> "Application Configuration". + +- Python support for ASP can be a little flaky. If you get strange + errors, it can sometimes help to uninstall and reinstall it with + pyscript.py. A number of people have also encountered a problem in + ActivePython 2.2 where the first loads of any Python ASP page would + work, but subsequent loads of the same page would always return + nothing (leaving the screen blank). There were a number of + workarounds for this problem, but the fix is to download and install + the latest python win32 extensions from + http://sourceforge.net/projects/pywin32/ + +- ViewVC can't convert timestamps on diff pages to local time when it + is used with CVSNT. This is caused by a CVSNT bug, which is + described at + http://www.cvsnt.org/mantis/bug_view_page.php?bug_id=0000110 + +- Old versions of CVSNT (2.0.11 and earlier) have a bug in their rlog + emulation which causes them to output truncated paths to RCS + files. In ViewVC, this causes errors like + + Error parsing rlog output. Expected RCS file + "c:\cvsroot\dir\file,v", found "file,v" + +- Old versions of CVSNT (2.0.11 and earlier) have a bug in their + standalone RCS tools (rlog.exe, co.exe, and rcsdiff.exe) which + causes them not to properly interpret arguments with spaces. This + can result in ViewVC errors in repositories that have spaces in file + or directory names. This bug only occurs when ViewVC is configured + to use the standalone utilities, not when it uses cvs.exe directly + as it does by default. + +- Old versions of CVSNT (1.11.1.3-76 and earlier) don't have any RCS + emulation, so they can't be used as RCS parsers for ViewVC. + +- Very old versions of CVSNT (1.11.1.3-57g and earlier) won't work + reliably with our loginfo handler because they have a bug which + makes them escape spaces and other special characters in filenames + twice. This bug can result in loginfo errors or invalid data being + inserted into the database. + +- Old versions of Highlight (2.4.3 and earlier) will not show line + numbers for .txt files or files of unknown type even when the + "highlight_line_numbers" option is enabled. + +- Highlight versions 2.4.2 and 2.4.3 start line numbering for all file + types at 0 instead of 1 by default. A workaround is to make ViewVC + pass an explicit --line-number-start=1 option to Highlight + +- Highlight version 2.4.4 starts line numbering for .txt files at 0 + instead of 1. It also misinterprets the --line-number-start option + for those files, starting numbering one number before whatever + number is specified. Since this behavior does not affect unknown + file types, a simple workaround is just to not pass a --syntax + option to Highlight for plain text files (instead of passing + --syntax=txt). + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +THANKS + +- Bryan T. Vold for improving the original ViewCVS patch by adding + support for enscript and tarball generation. + +- David Resnick for tracking down the cause of re_search failures in + repositories with non-rcs files and for bringing a bug in + sapi.AspFile.header() to my attention + +- Matt Bunch for finding a better way to address the ASP blank page + problem, and Keith D. Zimmerman for finding another workaround. + +- Rüdiger Koch for reporting a bug in viewvc PATH_INFO parsing code + with Apache for Windows as well as Jelle Ouwerkerk and Steffen Yount + for providing fixes. + +- Nick Minutello and Rüdiger Koch for providing workarounds for + setting enscript_library environment variable with apache. David + Duminy for providing the first bug report on this. + +- Gyula Faller and Tony Cook for independently coming up with CVSNT + loginfo handlers that accept spaces and don't rely on unix-style + echo commands. Tony Cook's patch also eliminated the dependency on + cat.exe. + +- Mathieu Mazerolle for making the unix loginfo handler handle spaces + in filenames. + +- Paul Russell for analyzing problems with new fields in CVSNT RCS + files. Terry.Ninnis@pgen.com for coming up with a partial solution + +- Bo Berglund for tracking down the cause of a case-sensitivity issue + that could lead to problems in the commit database and for patiently + working with me to finally fix the CVSNT RCS fields problem and + another problem with enscript. + +- Ivo Roessling for finding and fixing a bug in the query page's + commit grouping code. + +- Keith D. Zimmerman for experimenting with enscript and finding some + new ways to make it work. diff --git a/windows/aspfool/Makefile b/windows/aspfool/Makefile new file mode 100644 index 00000000..1dec8eea --- /dev/null +++ b/windows/aspfool/Makefile @@ -0,0 +1,8 @@ +aspfool.dll : aspfool.o aspfool.def + g++ -shared -o aspfool.dll aspfool.o --def aspfool.def -Wl,--add-stdcall-alias + +distribution.o : aspfool.cpp + g++ -O3 -o aspfool.o -c aspfool.cpp + +clean : + rm -f aspfool.o aspfool.dll diff --git a/windows/aspfool/README b/windows/aspfool/README new file mode 100644 index 00000000..dfd387ef --- /dev/null +++ b/windows/aspfool/README @@ -0,0 +1,18 @@ +Some script interpreters for IIS (like ASP and PHP) fail when they recieve requests like + + /script.asp/extra + /script.php/fake/path + +Aspfool is an ISAPI filter dll that intercepts requests with fake paths and maps them to valid local paths that the script interpreters should be able to handle more readily. The extended path information is still available to the actual scripts through the server variables, which are not altered. + +Aspfool does not currently distinguish between scripts and other types of files. As a result, requests like + + /page.html/blah/blah + /image.jpg/ha ha ha + +will return actual files, instead of 404 not found errors. + +Aspfool is known to compile with Visual C++ 6.0 and GCC 2.95 (mingw). Makefiles and project files are included. + +Russ Yanofsky +rey4@columbia.edu diff --git a/windows/aspfool/aspfool.cpp b/windows/aspfool/aspfool.cpp new file mode 100644 index 00000000..9aad357b --- /dev/null +++ b/windows/aspfool/aspfool.cpp @@ -0,0 +1,59 @@ +#define WIN32_LEAN_AND_MEAN + +#include +#include +#include + +// Returns 0 if doesn't exist, 1 if it is a file, 2 if it is a directory +int inline file_exists(TCHAR const * filename) +{ + WIN32_FIND_DATA fd; + HANDLE fh = FindFirstFile(filename, &fd); + if (fh == INVALID_HANDLE_VALUE) + return 0; + else + { + FindClose(fh); + return fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ? 2 : 1; + } +} + +BOOL WINAPI GetFilterVersion(HTTP_FILTER_VERSION * pVer) +{ + pVer->dwFilterVersion = HTTP_FILTER_REVISION; + pVer->dwFlags = SF_NOTIFY_URL_MAP | SF_NOTIFY_ORDER_DEFAULT; + return TRUE; +} + +DWORD WINAPI HttpFilterProc(PHTTP_FILTER_CONTEXT pfc, DWORD notificationType, LPVOID pn) +{ + switch(notificationType) + { + case SF_NOTIFY_URL_MAP: + HTTP_FILTER_URL_MAP & um = *((HTTP_FILTER_URL_MAP *)pn); + + if (!file_exists(um.pszPhysicalPath)) + { + size_t pathlen = _tcslen(um.pszPhysicalPath); + size_t m = pathlen - _tcslen(um.pszURL); + + for(size_t i = pathlen - 1; i > m; --i) + { + TCHAR c = um.pszPhysicalPath[i]; + if (c == '\\') + { + um.pszPhysicalPath[i] = 0; + int r = file_exists(um.pszPhysicalPath); + if (r == 1) + break; + else + { + um.pszPhysicalPath[i] = c; + if (r == 2) break; + } + } + } + } + } + return SF_STATUS_REQ_NEXT_NOTIFICATION; +} \ No newline at end of file diff --git a/windows/aspfool/aspfool.def b/windows/aspfool/aspfool.def new file mode 100644 index 00000000..0fd869de --- /dev/null +++ b/windows/aspfool/aspfool.def @@ -0,0 +1,7 @@ +LIBRARY "aspfool" +DESCRIPTION 'aspfool' + +EXPORTS + ; Explicit exports can go here + GetFilterVersion @1 + HttpFilterProc @2 diff --git a/windows/aspfool/aspfool.dll b/windows/aspfool/aspfool.dll new file mode 100644 index 00000000..6b8c08fa Binary files /dev/null and b/windows/aspfool/aspfool.dll differ diff --git a/windows/aspfool/aspfool.dsp b/windows/aspfool/aspfool.dsp new file mode 100644 index 00000000..f65245be --- /dev/null +++ b/windows/aspfool/aspfool.dsp @@ -0,0 +1,110 @@ +# Microsoft Developer Studio Project File - Name="aspfool" - Package Owner=<4> +# Microsoft Developer Studio Generated Build File, Format Version 6.00 +# ** DO NOT EDIT ** + +# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102 + +CFG=aspfool - Win32 Debug +!MESSAGE This is not a valid makefile. To build this project using NMAKE, +!MESSAGE use the Export Makefile command and run +!MESSAGE +!MESSAGE NMAKE /f "aspfool.mak". +!MESSAGE +!MESSAGE You can specify a configuration when running NMAKE +!MESSAGE by defining the macro CFG on the command line. For example: +!MESSAGE +!MESSAGE NMAKE /f "aspfool.mak" CFG="aspfool - Win32 Debug" +!MESSAGE +!MESSAGE Possible choices for configuration are: +!MESSAGE +!MESSAGE "aspfool - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE "aspfool - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library") +!MESSAGE + +# Begin Project +# PROP AllowPerConfigDependencies 0 +# PROP Scc_ProjName "" +# PROP Scc_LocalPath "" +CPP=cl.exe +MTL=midl.exe +RSC=rc.exe + +!IF "$(CFG)" == "aspfool - Win32 Release" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 0 +# PROP BASE Output_Dir "Release" +# PROP BASE Intermediate_Dir "Release" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 0 +# PROP Output_Dir "Release" +# PROP Intermediate_Dir "Release" +# PROP Ignore_Export_Lib 0 +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /c +# ADD CPP /nologo /MD /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /c +# ADD BASE MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "NDEBUG" +# ADD RSC /l 0x409 /d "NDEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 +# ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /machine:I386 + +!ELSEIF "$(CFG)" == "aspfool - Win32 Debug" + +# PROP BASE Use_MFC 0 +# PROP BASE Use_Debug_Libraries 1 +# PROP BASE Output_Dir "Debug" +# PROP BASE Intermediate_Dir "Debug" +# PROP BASE Target_Dir "" +# PROP Use_MFC 0 +# PROP Use_Debug_Libraries 1 +# PROP Output_Dir "Debug" +# PROP Intermediate_Dir "Debug" +# PROP Target_Dir "" +# ADD BASE CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /GZ /c +# ADD CPP /nologo /MDd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "_MBCS" /D "_USRDLL" /D "CUBMOVE_EXPORTS" /YX /FD /GZ /c +# ADD BASE MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32 +# ADD BASE RSC /l 0x409 /d "_DEBUG" +# ADD RSC /l 0x409 /d "_DEBUG" +BSC32=bscmake.exe +# ADD BASE BSC32 /nologo +# ADD BSC32 /nologo +LINK32=link.exe +# ADD BASE LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept +# ADD LINK32 kernel32.lib user32.lib gdi32.lib winspool.lib comdlg32.lib advapi32.lib shell32.lib ole32.lib oleaut32.lib uuid.lib odbc32.lib odbccp32.lib /nologo /dll /debug /machine:I386 /pdbtype:sept + +!ENDIF + +# Begin Target + +# Name "aspfool - Win32 Release" +# Name "aspfool - Win32 Debug" +# Begin Group "Source Files" + +# PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" +# Begin Source File + +SOURCE=.\aspfool.CPP +# End Source File +# Begin Source File + +SOURCE=.\aspfool.def +# End Source File +# End Group +# Begin Group "Header Files" + +# PROP Default_Filter "h;hpp;hxx;hm;inl" +# End Group +# Begin Group "Resource Files" + +# PROP Default_Filter "ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe" +# End Group +# End Target +# End Project diff --git a/windows/aspfool/aspfool.dsw b/windows/aspfool/aspfool.dsw new file mode 100644 index 00000000..34123c35 --- /dev/null +++ b/windows/aspfool/aspfool.dsw @@ -0,0 +1,29 @@ +Microsoft Developer Studio Workspace File, Format Version 6.00 +# WARNING: DO NOT EDIT OR DELETE THIS WORKSPACE FILE! + +############################################################################### + +Project: "aspfool"=".\aspfool.dsp" - Package Owner=<4> + +Package=<5> +{{{ +}}} + +Package=<4> +{{{ +}}} + +############################################################################### + +Global: + +Package=<5> +{{{ +}}} + +Package=<3> +{{{ +}}} + +############################################################################### +