Trac-1.0.1/0000755000175200017520000000000012102610436011375 5ustar cbooscboosTrac-1.0.1/RELEASE0000644000175200017520000000235112102610255012400 0ustar cbooscboosRelease Notes for Trac 1.0 'Cell' Release ========================================= September 7, 2012 Highlights ---------- * Refreshed user interface * Git support * Branching structure displayed in the revision log * Ticket batch modification support Detailed User Visible Changes ----------------------------- A detailed view of the API changes since 0.12.x can be found in http://trac.edgewall.org/wiki/TracDev/ReleaseNotes/1.0 Developer-visible changes ------------------------- A detailed view of the API changes since 0.12.x can be found in http://trac.edgewall.org/wiki/TracDev/ApiChanges/1.0 Acknowledgements ---------------- Many thanks to the growing number of people who have, and continue to, support the project. Also our thanks to all people providing feedback and bug reports that helps us making Trac better, easier to use and more effective. Starting with the 0.12 release, special thanks go to our many translators. Without your invaluable help, Trac would not evolve. Thank you all. Finally, we offer hope that Trac will prove itself useful to like-minded programmers around the world, and that this release will prove an improvement over the last version. Please let us know. :-) /The Trac Team http://trac.edgewall.org/ Trac-1.0.1/UPGRADE0000644000175200017520000002157512102610255012420 0ustar cbooscboosUpgrade Instructions ==================== A Trac environment sometimes needs to be upgraded before it can be used with a new version of Trac. This document describes the steps necessary to upgrade an environment. **You should also read the trac/wiki/default-pages/TracUpgrade documentation file present in the source distribution.** Note that environment upgrades are not necessary for minor version releases unless otherwise noted. For example, there's no need to upgrade a Trac environment created with (or upgraded) 0.8.0 when installing 0.8.4 (or any other 0.8.x release). General Instructions -------------------- Typically, there are four steps involved in upgrading to a newer version of Trac: 1. Update the Trac Code Get the new version of Trac, either by downloading an offical release package or by checking it out from the Subversion repository. If you have a source distribution, you need to run:: python setup.py install to install the new version. If you've downloaded the Windows installer, you execute it, and so on. In any case, if you're doing a major version upgrade (such as from 0.8 to 0.9), it is highly recommended that you first remove the existing Trac code. To do this, you need to delete the `trac` directory from the Python `lib/site-packages` directory. You may also want to remove the Trac `cgi-bin`, `htdocs` and `templates` directories that are commonly found in a directory called `share/trac` (the exact location depends on your platform). 2. Upgrade the Trac Environment Unless noted otherwise, upgrading between major versions (such as 0.8 and 0.9) involves changes to the database schema, and possibly the layout of the environment. Fortunately, Trac provides automated upgrade scripts to ease the pain. These scripts are run via `trac-admin`:: trac-admin /path/to/projenv upgrade This command will do nothing if the environment is already up-to-date. 3. Update the Trac Documentation Every Trac environment includes a copy of the Trac documentation for the installed version. As you probably want to keep the included documentation in sync with the installed version of Trac, `trac-admin` provides a command to upgrade the documentation:: trac-admin /path/to/projenv wiki upgrade Note that this procedure will of course leave your `WikiStart` page intact. 4. Restart the Web Server In order to reload the new Trac code you will need to restart your web server (note this is not necessary for CGI). The following sections discuss any extra actions that may need to be taken to upgrade to specific versions of Trac. From 0.12.x to 1.0 ------------------ Note that Clearsilver based plugin are now no longer supported. The content of ``attachments`` folder has been restructured and been moved to ``files/attachments``. The migration is handled automatically on upgrade, but in some cases you might need to clean-up the old ``attachments`` directory manually. The Subversion components have been moved to ``tracopt/versioncontrol.svn``. The upgrade will take care of re-enabling those components if you haven't disabled the older `trac.versioncontrol` ones. From 0.11.x to 0.12.x --------------------- For this upgrade, the detailed information can be found either online at http://trac.edgewall.org/wiki/TracUpgrade or within the set of default wiki pages, part of this source distribution in trac/wiki/default-pages/TracUpgrade. From 0.10.x to 0.11.x --------------------- There should not be any serious problems... However, take a look at the following documented caveats:: http://trac.edgewall.org/wiki/TracDev/ReleaseNotes/0.11#Caveats Also, you should be careful to check that the plugins you depend on have been ported to 0.11, as they most probably won't work without adaptation due to the numerous internal changes that occurred during 0.11 development. See: http://trac.edgewall.org/wiki/TracDev/ApiChanges/0.11 From 0.9.x to 0.10.x -------------------- Due to some changes in the Wiki syntax, you may notice that certain parts of your pages no longer work as expected: * Previously, links to images would result in that image being embedded into the page. Since 0.10, links to images remain plain links. If you want to embed an image in the page, use the [[Image]] macro. * You can no longer use %20 in wiki links to encode spaces. Instead, you should quote the name containing spaces (for example, use wiki:"My page" instead of wiki:My%20page.) Several enhancements have been made to the version control subsystem, in particular for the support of scoped repositories has been improved. It is recommended that you perform a "trac-admin resync" operation to take advantage of these improvements. Also note that the argument list of the "trac-admin initenv" command has changed: there's a new argument for determining the type of version control system. The old usage was:: initenv The new usage is:: initenv If you're using any scripts that automate the creation of Trac environments, you will need to update them. If you're using Subversion, specify "svn" for the argument. From 0.9.3 to 0.9.4 ------------------- There is a bug in Pysqlite 1.x that causes reports using the "%" character for LIKE clauses or date formatting to fail. You will need to use escape the percent characters with another: "%%". From 0.9.x to 0.9.3 ------------------- If you are using plugins you might need to upgrade them. See http://trac.edgewall.org/milestone/0.9.3 for further details. From 0.9-beta to 0.9 -------------------- If inclusion of the static resources (style sheets, javascript, images) is not working, check the value of the `htdocs_location` in trac.ini. For mod_python, Tracd and FastCGI, you can simply remove the option altogether. For CGI, you should fix it to point to the URL you mapped the Trac `htdocs` directory to. If you've been using plugins with a beta release of Trac 0.9, or have disabled some of the built-in components, you might have to update the rules for disabling/enabling components in trac.ini. In particular, globally installed plugins now need to be enabled explicitly. See the TracPlugins and TracIni wiki pages for more information. If you want to enable the display of all ticket changes in the timeline (the Ticket Details option), you now have to explicitly enable that in trac.ini, too:: [timeline] ticket_show_details = true From 0.8.x to 0.9 ----------------- mod_python users will need to change the name of the mod_python handler in the Apache HTTPD configuration:: from: PythonHandler trac.ModPythonHandler to: PythonHandler trac.web.modpython_frontend If you have PySQLite 2.x installed, Trac will now try to open your SQLite database using the SQLite 3.x file format. The database formats used by SQLite 2.8.x and SQLite 3.x are incompatible. If you get an error like "file is encrypted or is not a database" after upgrading, then you must convert your database file. To do this, you need to have both SQLite 2.8.x and SQLite 3.x installed (they have different filenames so can coexist on the same system). Then use the following commands:: mv trac.db trac2.db sqlite trac2.db .dump | sqlite3 trac.db After testing that the conversion was successful, the `trac2.db` file can be deleted. For more information on the SQLite upgrade see http://www.sqlite.org/version3.html. From 0.7.x to 0.8 ----------------- 0.8 adds a new roadmap feature which requires additional permissions. While a fresh installation will by default grant `ROADMAP_VIEW` and `MILESTONE_VIEW` permissions to anonymous, these permissions have to be granted manually when upgrading:: trac-admin /path/to/projectenv permission add anonymous MILESTONE_VIEW trac-admin /path/to/projectenv permission add anonymous ROADMAP_VIEW From 0.6.x to 0.7 ----------------- Trac 0.7 introduced a new database format, requiring manual upgrade. Previous versions of Trac stored wiki pages, ticket, reports, settings, etc. in a single SQLite database file. Trac 0.7 replaces this file with a new backend storage format; the 'Trac Environment', which is a directory containing an SQLite database, a human-readable configuration file, log-files and attachments. Fear not though, old-style Trac databases can easily be converted to Environments using the included `tracdb2env` program as follows:: tracdb2env /path/to/old/project.db /path/to/new/projectenv `tracdb2env` will create a new environment and copy the information from the old database to the new environment. The existing database will not be modified. You also need to update your apache configuration: Change the line:: SetEnv TRAC_DB "/path/to/old/project.db" to:: SetEnv TRAC_ENV "/path/to/new/projectenv" ---- If you have trouble upgrading Trac, please ask questions on the mailing list: Or for other support options, see: Trac-1.0.1/messages-js.cfg0000644000175200017520000000035012102610255014274 0ustar cbooscboos# mapping file for extracting messages from javascript files into # trac/locale/messages-js.pot (see setup.cfg) [javascript: **.js] [extractors] javascript_script = trac.dist:extract_javascript_script [javascript_script: **.html] Trac-1.0.1/ChangeLog0000644000175200017520000007611412102610255013157 0ustar cbooscboosTrac 1.0.1 (February 1, 2013) http://svn.edgewall.org/repos/trac/tags/trac-1.0.1 - Fix zip source download for large directories in Subversion repositories - Performance improvement for the Roadmap, by caching milestone properties - Added a ''select all'' checkbox to table of components for each plugin on the Plugins admin panel - Restore the ''Modify'' link at the top of the ticket page, as it was in Trac 0.12 - `ListOption` keeps values other than empty string and None in raw list as default - Prevent possibility of multiple identical info or warning messages being presented to the user - The BatchModify select-all checkboxes are toggled with tri-state behavior when the ticket checkboxes are toggled - Update the ticket changetime to the current time when deleting a ticket comment - ... and quite more! In particular, see also the changes for 0.12.5 which are also integrated and new since 1.0 Trac 1.0 'Cell' (September 7, 2012) http://svn.edgewall.org/repos/trac/tags/trac-1.0 Trac 1.0 is a major release adding refreshed user interface and improved DVCS repository support as the most visible changes. The following list contains only a few highlights: - The default theme looks more modern, especially on recent browsers (no effort has been made to make it look better on older browsers like IE6 or 7) - The TracHacks GitPlugin has been donated by Herbert Valerio Riedel to the Trac project (many thanks!) and is now maintained here as an optional component - As a consequence, the Subversion support has been moved below `tracopt.versioncontrol` as well - The Git and Mercurial log view feature a visualization of the branching structure - Usability improvements for the tickets, with a better support for conflict detection and resolution - Integration of the TracHacks BatchModifyPlugin, contributed by Brian Meeker (many thanks!) and is now maintained there as a default component - jQuery/UI integration, featuring a date picker for date fields - Improved integration with Pygments syntax highlighting - ... and numerous smaller features added and bugs fixed since 0.12! Trac 1.0-rc1 'Cell' (September 1, 2012) http://svn.edgewall.org/repos/trac/tags/trac-1.0rc1 See 1.0 Trac 1.0-beta1 'Cell' (August 6, 2012) http://svn.edgewall.org/repos/trac/tags/trac-1.0b1 See 1.0-rc1 ----------------------------------------------------------------------------- Trac 0.12.5 (January 15, 2013) http://svn.edgewall.org/repos/trac/tags/trac-0.12.5 Trac 0.12.5 is a maintenance release and contains a few interesting fixes: - upload of .mht files (MHTML web page archive files) now works (#9880) - more robust parsing of attachment URLs (#10280) and uploaded file names (#10850) - lots of improvement to the date formatting code, which is now much more robust when timezone and daylight saving time computations are involved (#10768, #10863, #10864, #10912, #10920) - no longer generate invalid JSON encoded data with Python 2.4 and 2.5 (#10877) - ... and a few more! Trac 0.12.4 (September 7, 2012) http://svn.edgewall.org/repos/trac/tags/trac-0.12.4 Trac 0.12.4 is a regular maintenance release and contains a few minor fixes. Trac 0.12.3 (February 6, 2012) http://svn.edgewall.org/repos/trac/tags/trac-0.12.3 Trac 0.12.3 contains a few minor fixes and a few minor features: - added compatibility with Subversion 1.7 (#10414) - make it easier to troubleshoot common startup errors (#10024) - jQuery upgraded to 1.4.4 (#10001) - improved fine-grained permission handling in the source browser (#9976, #10208, #10110) - added compatibility with MySQL 5.5.3 utf8mb4 databases (#9766) - ... and dozens more fixes! Trac 0.12.2 (January 31, 2011) http://svn.edgewall.org/repos/trac/tags/trac-0.12.2 This list contains only a few highlights: - install: improved robustness of Trac installation if Babel is installed after the fact (#9439, #9595, #9961) - notifications: support for Asian character width (#4717) - roadmap: fix display of progress bar in some corner cases (#9718) and respect the overall_completion milestone group setting (#9721) - reports: reports and queries look much better, as the columns now keep the same width across groups; the absence of word wrapping in reports has been fixed (#9825) - web admin: improved layout (#8866, #9963) - web: it's now possible to log in different Trac instances sharing the same URL prefix (e.g. /project and /project-test) (#9951) Trac 0.12.1 (October 9, 2010) http://svn.edgewall.org/repos/trac/tags/trac-0.12.1 This list contains only a few highlights: - db: improve concurrency behavior (#9111) - fcgi: add an environment variable `TRAC_USE_FLUP` to control the usage of flup vs. bundled _fcgi.py (defaults to 0, i.e. use bundled as before) - svn authz: improve compatibility with svn 1.5 format (#8289) - milestone: allow to set the time for the due date (#6369, #9582) - ticket: fixes for the CC: property (#8597, #9522) - notification: improved the formatting of ticket fields in notification e-mails (#9484, #9494) - i18n: added a configuration option to set the default language (#8117) - several fixes for upgrade (#9400, #9416, #9483, #9556) Trac 0.12 'Babel' (June 13, 2010) http://svn.edgewall.org/repos/trac/tags/trac-0.12 Trac 0.12 is a major release introducing i18n and multiple repository support as the most visible changes. The following list contains only a few highlights: - The user interface is translated in a dozen of languages, provided the [Babel:] package is installed - Multiple repositories can be associated to a single Trac environment; the repositories can be of heterogeneous types (svn, hg, git, darcs...) - Usability improvements for the Wiki, with a nice side-by-side edit mode with automatic preview - Richer Wiki syntax, with much improved support for tables, partial WikiCreole compatibility and numerous smaller improvements - Usability improvements for the Ticket module, with automatic preview of comments while you type and possibility to edit or remove them later - Improved Custom Queries (time fields, multiple disjoint conditions, a.k.a. OR queries) - Timeline filtering by user - ... and numerous smaller features added and bugs fixed since 0.11! Trac 0.12rc1 'Babel' (June 03, 2010) http://svn.edgewall.org/repos/trac/tags/trac-0.12rc1 See 0.12. ----------------------------------------------------------------------------- Trac 0.11.7 (March 10, 2010) http://svn.edgewall.org/repos/trac/tags/trac-0.11.7 Trac 0.11.7 is nearly identical to 0.11.7rc1 except for a few more fixes: - avoid an infinite loop when using an AtomicFile in a read-only directory (#9081) - don't report client disconnects during writes as internal errors (#9103) - don't reuse a closed cursor, which could happen in one specific case for the pysqlite backend (#9104) Trac 0.11.7rc1 (February 23, 2010) http://svn.edgewall.org/repos/trac/tags/trac-0.11.7rc1 Security fixes: - Fixed a ticket validation issue that would allow unauthorized users to modify the status and resolution of a ticket (#8884) Performance improvements: - Trac wiki had some trouble handling very long unicode words (#9025) [[comment(intentionally kept vague, we don't want to advertize a DOS, do we?)]] - Full text search was very slow if lots of custom fields were used (#8935) Bug fixes: - Fixed a race condition that could lead to the destruction of the trac.ini file (#8623) - Fixed creation of new milestone which could have been a rename if performed after a name clash has been detected (#8816) - Fixed display of value 0 in report cells (#7512) Trac 0.11.6 (November 28, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.6 Trac 0.11.6 contains two security fixes and a number of bug fixes, performance improvements and minor enhancements. The following list contains only a few highlights: Security fixes: * Fixed the policy checks in report results when using alternate formats. * Added a check for the "raw" role that is missing in docutils < 0.6. Performance improvements: * Re-enabled connection pooling with SQLite (#3446). * Added caching of configuration options (#8510). Bug fixes: * Fixed the "database is locked" issue with SQLite (#3446, #8468). * Deprecated SQLite 2.x support (#8625). * Fixed hanlding of times in timezones with DST (#8240). * Avoid corruption of trac.ini during write (#8623). * Improved support for revision ranges in revision log view (#8349) Trac 0.11.5 (July 17, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.5 Trac 0.11.5 is identical to 0.11.5rc2 except for fixing a minor incompatibility issue when using IIS via AJP as frontend (#8475). Trac 0.11.5rc2 (July 11, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.5rc2 Trac 0.11.5rc2 fixes two regressions found in rc1 and one minor enhancement: * Fixed workaround for zipped Genshi eggs, [8354], #7823 * Fixed internal error when removing a wiki page version [8343]. * Fixed display of merge properties for scoped repositories #7715. Trac 0.11.5rc1 (June 30, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.5rc1 Trac 0.11.5rc1 contains a number of bug fixes and minor enhancements. The following list contains only a few highlights: Bug fixes: * Implemented pre-upgrade backup support for PostgreSQL and MySQL (#2304) * Fixed PostgreSQL upgrade issue (#8378) * More robust diff parsing (#2672) * Avoid intermittent hangs by not calling apr_terminate explicitly (#7785) Trac 0.11.4 (March 30, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.4 Trac 0.11.4 is identical to the second release candidate. Trac 0.11.4 contains a number of bug fixes and minor enhancements. The following list contains only a few highlights: * Custom mainnav links regression (#8153) * Fixed Python 2.3 incompatibility (#8061) * Fixed Python 2.6 notification issue (#8083) * Fixed PostgreSQL multi column index issue (#7600) Trac 0.11.4rc2 (March 25, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.4rc2 * Custom mainnav links regression (#8153) Trac 0.11.4rc1 (March 18, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.4rc1 Trac 0.11.4rc1 contains a number of bug fixes and minor enhancements. The following list contains only a few highlights: Bug fixes: * Fixed Python 2.3 incompatibility (#8061) * Fixed Python 2.6 notification issue (#8083) * Fixed PostgreSQL multi column index issue (#7600) Trac 0.11.3 (February 15, 2009) http://svn.edgewall.org/repos/trac/tags/trac-0.11.3 Trac 0.11.3 contains a number of bug fixes and minor enhancements. The following list contains only a few highlights: Bug fixes: * Compatibility with Python 2.6 (#7876, #7458) * PostgreSQL db backend improvement (#4987, #7600) * Highlighting of search results is more robust (#7324, #7830) * Unicode related fixes (#7672, #7959, #7845, #7935, #8024) * Fixed Trac link rendering in ReST (#7712) Minor improvements: * Custom fields can now contain wiki text (#1791) * A few usability and documentation improvements (#8000, #8004, #8016) Trac 0.11.2.1 (November 17, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.11.2.1 Trac 0.11.2.1 fixes a Python 2.3 incompatibility introduced in Trac 0.11.2. Python 2.4+ users already running Trac 0.11.2 do not need to upgrade. Trac 0.11.2 (November 8, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.11.2 Trac 0.11.2 contains two security fixes and a couple of bug fixes. The following list contains only a few highlights: Bug fixes: * Fixes potential DOS vulnerability with certain wiki markup. Reported by Matt Murphy. * Improved HTML sanitizer filter to detect possible phishing attempts. Reported by Simon Willison. * MySQL db backend improvement (reconnect after idle timeout #4465) * TicketQuery speed improvements (#6436) * Fixes for RSS feeds (timeline entries no longer truncated #7316, no longer download some feeds under Firefox #3899) * Search now works for custom fields (#2530) * Same order for ticket fields for new and existing tickets (#7018) * Enforce fine-grained permission for "quickjump" search results (#7655) * E-mail obfuscation was not done in a few remaining places (#7688, #6532) * Uninstall of plugins from WebAdmin was not working - feature disabled for now * More robust pagination of results for reports and custom queries (#7424, #7544) * Support for newer version of pygments (#7622) * Documentation updated (#7603, #7205, #7318) Minor improvements: * Better support for Wiki page hierarchy (show path #2780, link to parent #2150) * Custom query allow to search in description and other text fields (#4824) Trac 0.11.1 (August 6, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.11.1 Trac 0.11.1 contains a number of bug fixes and minor enhancements. The following list contains only a few highlights: * Improved DB connection handling (new connection pool) * Better MySQL backend unicode support. "utf8" and "utf8_bin" is the recommended database charset and collation settings. * Fixes intermittent "constraint violation" and "invalid form token" error messages. * Fixes roadmap layout glitch in Firefox 3. * Safer default umask value for tracd (can be set using --umask option) * Better default PYTHON_EGG_CACHE value. The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.11.1 Trac 0.11 'Genshi' (June 22, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.11 Trac 0.11 contains a great number of new features, improvements and bug fixes. The following list contains only a few highlights: * New template engine for generating content (Genshi) * New configurable workflow in the ticket subsystem * Finer-grained control of permissions * Support for Pygments as the default syntax highlighter * Improved repository browser ("blame" support, dynamic in-place expansion of folders) * Improved user preferences subsystem, among which the possibility for * any user to select their time zone and disable access keys * The WebAdmin plugin is now an integral part of Trac * Paging of timeline and query results. A more complete list of new features can be found in the RELEASE file. The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.11 Trac 0.11rc2 'Genshi' (June 9, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.11rc2 See 0.11 Trac 0.11rc1 'Genshi' (April 30, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.11rc1 See 0.11rc2 Trac 0.11b2 'Genshi' (March 12, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.11b2 See 0.11rc1 Trac 0.11b1 'Genshi' (December 18, 2007) http://svn.edgewall.org/repos/trac/tags/trac-0.11b1 See 0.11b2. ----------------------------------------------------------------------------- Trac 0.10.5 (Jun 23, 2008) http://svn.edgewall.org/repos/trac/tags/trac-0.10.5 Trac 0.10.5 contains two security fixes and a couple of bug fixes. The following list contains only a few highlights: * Fixes a cross-site redirection vulnerability in the quickjump function reported by Russ McRee. * Fixes a wiki engine XSS vulnerability found by Nathan Collins. * Added PostgreSQL 8.3 support. * Fixes FineGrainedPermissions for scoped repositories. * Fixes problem with repository syncing raising exceptions. The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.10.5 Trac 0.10.4 (April 20, 2007) http://svn.edgewall.org/repos/trac/tags/trac-0.10.4 Trac 0.10.4 is a bug fix release. The following list contains only a few highlights: * Repository cache improvements. The new syncing scheme is incompatible with the previous one and requires a database schema upgrade in order to prevent the old and the new codebase to be mixed. A repository resync is not needed, though. The 0.10.4 scheme is compatible with the 0.11 one. (#3837, #4043 and #4586) * Fix a possible freeze under heavy load (#4465) The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.10.4 Trac 0.10.3.1 (Mar 8, 2007) http://svn.edgewall.org/repos/trac/tags/trac-0.10.3.1 Trac 0.10.3.1 is a security fix release. * Always send "Content-Disposition: attachment" headers where potentially unsafe (user provided) content is available for download. This behaviour can be altered using the "render_unsafe_content" option in the "attachment" and "browser" sections of trac.ini. * Fixed XSS vulnerability in "download wiki page as text" in combination with Microsoft IE. Reported by Yoshinori Oota, Business Architects Inc. Trac 0.10.3 (Dec 12, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.10.3 Trac 0.10.3 is a bug fix release. The following list contains only a few highlights: * Timeline fail to load with a "!NoSuchChangeset" error message (#4132). * Timed out MySQL connections not handled properly (#3645). * Subversion repository resync broken. (#4204). The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.10.3 Trac 0.10.3rc1 (Dec 7, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.10.3rc1 See 0.10.3 Trac 0.10.2 (Nov 13, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.10.2 Trac 0.10.2 is a bug fix release. The following list contains only a few highlights: * Fixes deadlock when using authz_file config option (#3996). * Makes the CSRF code play nice with the XmlRpcPlugin (#4122). * Fixes Timeline breakage after svn commit when using sqlite (#4120). The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.10.2 Trac 0.10.1 (Nov 8, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.10.1 Trac 0.10.1 contains a security fix and a number of bug fixes. The following list contains only a few highlights: * Fixed CSRF vulnerability (#4049), reported by Daniel Kahn Gillmor. * Improved DB connection handling (#3503) * Tracd no longer tries to resolve client's IP address (#3481). The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.10.1 Trac 0.10 'Zengia' (Sep 28, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.10 Trac 0.10 contains a great number of new features, improvements and bug fixes. The following list contains only a few highlights: * Support for spam protection. * Advanced diff support. * InterWiki and InterTrac support. * Improved notification system. * WSGI Used as web server protocol. A more complete list of new features can be found in the RELEASE file. The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.10 Trac 0.10-rc1 (Sep 22, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.10rc1 See 0.10 Trac 0.10-beta1 (Aug 28, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.10b1 See 0.10 ----------------------------------------------------------------------------- Trac 0.9.6 (Jul 6, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.9.6 * Fixed reStructuredText breach of privacy and denial of service vulnerability found by Felix Wiemann. * trac-post-commit-hook fixes. * Fixed bugs: #2894, #3058, #3209 #3325. Trac 0.9.5 (Apr 18, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.9.5 * Fixed wiki macro XSS vulnerability found by Mr. Kazuhiro Nishiyama: http://jvn.jp/jp/JVN%2384091359/index.html * Smaller memory usage when accessing subversion history. * Fixed issue with incorrectly generated urls when installed behind a web proxy (#2531). * Fixed bugs: #2531, #2777, #3020. Trac 0.9.4 (Feb 15, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.9.4 * Deletion of reports has been fixed. * Various encoding issues with the timeline RSS feed have been fixed. * Fixed a memory leak when syncing with the repository. * Milestones in the roadmap are now ordered more intelligently. * Fixed bugs: #1064, #1150, #2006, #2253, #2324, #2330, #2408, #2430, #2431, #2459, #2544, #2459, #2481, #2485, #2536, #2544, #2553, #2580, #2583, #2606, #2613, #2621, #2664, #2666, #2680, #2706, #2707, #2735 Trac 0.9.3 (Jan 8, 2006) http://svn.edgewall.org/repos/trac/tags/trac-0.9.3 * Fixed XSS vulnerabilities. * Timeline RSS feed validity issue resolved. * "trac-admin initenv" now handles empty repositories. * Textile unicode support. * Fixed bugs: #1158, #2290, #2337, #2416, #2440, #2468, #2473, #2484, #2490, #2493, #2512, #2517, #2519, #2527, #2548, #2558, #2558 Trac 0.9.2 (Dec 5, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.9.2 * Fixed SQL injection vulnerability in ticket search module. * Fixed broken ticket email notifications. Trac 0.9.1 (Dec 1, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.9.1 * Fixed SQL injection vulnerability in ticket query module. * Fixed bugs: #1633, #2167, #2283, #2284, #2285, #2291, #2292, #2300, #2318, #2329, #2366, #2369, #2373, #2383, #2416, #2457 Trac 0.9 'Vodun' (Oct 31, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.9 * Support for a global trac.ini configuration file. * Changed logic for enabling plugins/components. * Improved support for handling repository subsets. * Fixes problems with Trac links when using multiple environments in the same Python interpreter. * Improvements to email notification layout and encoding. * Fixes for database locking with SQLite, in particular in a multi-threaded environment. * PostgreSQL compatibility fixes. * Fixed bugs: #804, #861, #927, #1044, #1051, #1123, #1153, #1169, #1239, #1344, #1463, #1562, #1881, #1886, #1895, #1909, #1921, #1930, #1983, #1988, #2019, #2051, #2061, #2229, #2106, #2107, #2116, #2120, #2124, #2129, #2135, #2136, #2138, #2140, #2144, #2164, #2166, #2170, #2172, #2191, #2192, #2196, #2201, #2202, #2203, #2208, #2215, #2218, #2223, #2230, #2232, #2239, #2240, #2241, #2243, #2251, Trac 0.9-beta2 (Sept 25, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.9b2 * Support for setuptools 0.6. * Allow insertion of a custom HTML snippet above the new ticket form to explain site-specific policies and/or guidelines. * Much improved Bugzilla import script. * Fixed a bug where deleting a wiki page version would sometimes delete the entire page. * Fixes for the rendering of diffs and patches. * Fixes for the Subversion authz support. * Fixed bugs: #2008, #2032, #2034, #1801, #1893, #1040, #2040, #1036, #1944, #1081, #1863, #2052, #2066, #2016, #2090, #1985, #2012, #2089, #2079, #1999, #2029, #2079, #1960, #2080, #2021, #2042, #2088, #1345, #2011, #2100, #2103, #2113, #2116, #2109 Trac 0.9-beta1 (Sept 5, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.9b1 Trac 0.9 contains a great number of new features, improvements and bug fixes. The following list contains only a few highlights: * License changed from GPL to modified BSD (See the file COPYING). * Improved modularity and extendibility (plugin support). * Support for both pysqlite 1.x and pysqlite 2.x. * Postgresql database support (with psycopg or pyPgSQL). * Repository subsets. Multiple Trac environments can share a single repository. * Version control abstraction layer making it possible to support other version control systems besides subversion in the future. * FastCGI frontend support. * Python version >= 2.3 is now required. The complete list of closed tickets can be found here: http://trac.edgewall.org/query?status=closed&milestone=0.9 ----------------------------------------------------------------------------- Trac 0.8.3 (Jun 15, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.8.3 * Fix compatibility of 'trac-admin resync' with Subversion >= 1.2. * Settings page now works correctly when Trac is deployed at the root of a host. * Windows packaging issues resolved. * Fixed bugs: #1282, #1500, #1648 Trac 0.8.2 (Jun 1, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.8.2 * Compatibility with Subversion >= 1.2 fixed. * Compatibility with Docutils >= 0.3.7 fixed. * Fixed bugs: #1020, #1302, #1500, #1182, #1339, #1518 #1525, #1618 Trac 0.8.1 (Feb 28, 2005) http://svn.edgewall.org/repos/trac/tags/trac-0.8.1 * Improved Python 2.1 compatibility. * Layout of navigation bar in Opera fixed. * Execution of Javascript through event handler attributes in HTML code is now forbidden. * Fixed bugs: #157, #371, #556, #683, #970, #971, #972, #974, #979, #983, #1001, #1003, #1007, #1008, #1011, #1020, #1026, #1030, #1045, #1054, #1070, #1072, #1074, #1076, #1087, #1090, #1103, #1108, #1111, #1136, #1159, #1164, #1190, #1195, #1220 Trac 0.8 'Qualia' (Nov 15, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.8 * Roadmap module. * Support for custom ticket properties. * Wiki administration features. * Advanced ticket queries. * Improved diff display. * User preferences. * Wiki editing (near-wysiwyg) aids a la wikipedia. * Improved email notification. * Fixed bugs: #13, #63, #99, #100, #158, #164, #203, #210, #225, #264, #304, #306, #326, #346, #347, #351, #352, #364, #373, #375, #405, #411, #416, #431, #433, #434, #436, #438, #443, #445, #446, #447, #450, #452, #453, #455, #458, #460, #465, #468, #471, #472, #473, #474, #477, #478, #479, #480, #482, #483, #486, #487, #489, #491, #492, #494, #496, #501, #503, #506, #510, #512, #513, #514, #516, #522, #524, #526, #527, #528, #530, #532, #536, #537, #538, #539, #542, #543, #545, #546, #550, #551, #552, #553, #555, #556, #557, #558, #559, #560, #565, #567, #568, #570, #572, #574, #577, #578, #580, #581, #583, #587, #589, #591, #593, #594, #597, #598, #599, #600, #601, #602, #606, #609, #610, #612, #613, #616, #618, #619, #620, #622, #623, #626, #627, #628, #630, #631, #634, #644, #647, #648, #651, #652, #657, #658, #660, #664, #668, #669, #670, #671, #674, #675, #676, #677, #678, #680, #690, #692, #696, #698, #699, #703, #705, #706, #708, #709, #713, #714, #715, #716, #718, #720, #721, #722, #726, #727, #730, #732, #734, #735, #736, #737, #738, #741, #742, #743, #744, #745, #748, #749, #750, #751, #752, #759, #762, #764, #768, #769, #770, #771, #774, #775, #776, #778, #779, #780, #785, #789, #793, #798, #800, #806, #807, #815, #816, #817, #818, #829, #830, #831, #833, #836, #844, #846, #848, #850, #851, #852, #872, #873, #877, #878, #885, #888, #889, #892, #901, #903, #907, #912, #916, #923, #929, #931, #932, #935 ----------------------------------------------------------------------------- Trac 0.7.1 'Argento' (Jun, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.7.1 * Bugfixes for 0.7 * Fixes security hole in auth.py * Experimental support for mod_python * Improved MIME-types * Fixed bugs: #93, #202, #307, #312, #342, #345, #350, #353, #355, #391, #393, #401, #404, #406, #415, #417, #419, #420, #421, #422, #424, #425, #428, #429, #432, #435, #437, #441, #442, #448, #451, #452, #456, #457, #461, #463, #466, #467, #470, #497, #498, #502, #504 Trac 0.7 'Fulci' (May 18, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.7 * Revised database format (requires manual upgrade). * Trac standalone daemon, tracd (Experimental). * Greatly improved browser. * Many usability improvements. * Clean-up of CSS and templates. * UTF-8 character encoding support. * Wiki page attachments. * Syntax coloring supporting >35 languages, using SilverCity or GNU Enscript. * Better support for ReStructuredText. * Logging support, including syslog and windows eventlog. * Ticket attachments. * Import tickets from Bugzilla (contributed by Mark Rowe). * Import tickets from SourceForge (contributed by Dmitry Yusupov). * New ticket field: keywords * Ticket email notification. * Localized date and time display. * Viewable SQL for reports. * Improved search facilities. * Windows installer package. * More documentation. * Fixed bugs: #14, #19, #27, #62, #87, #96, #106, #111, #115, #127, #146, #161, #166, #171, #180, #182, #183, #188, #190, #191, #192, #193, #195, #196, #197, #201, #205, #207, #211, #212, #213, #220, #224, #227, #228, #231, #233, #235, #236, #240, #241, #243, #244, #246, #247, #248, #249, #251, #252, #253, #254, #255, #258, #259, #261, #262, #263, #265, #270, #271, #273, #275, #277, #278, #281, #284, #285, #88, #289, #292, #293, #294, #296, #300, #302, #310, #313, #314, #315, #316, #320, #322, #328, #332, #333, #337, #338, #339, #340, #341, #344, #348, #349, #358, #361, #362, #363, #368, #370, #371, #372, #376, #377, #378, #381, #384, #385, #386, #387, #388, #392, #394, #396, #397, #398, #399, #402, #403, #410 ----------------------------------------------------------------------------- Trac 0.6.1 '245 Trioxin' (April 12, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.6.1 * RSS now escapes entities in summary. * Search results won't highlight dates anymore. * RPM for SuSE Linux. * Preliminary Windows Installer. * More documentation. * Fixed bugs: #163, #165, #189, #198, #200, #206, #209, #214, #223 Trac 0.6 'Solanum' (March 23, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.6 * View diffs between wiki page edits. * Improved Search module. * Support for tables in the wiki. (Thanks to Stephen Hansen) * Colored reports. Use colors to show priority, etc. * Support for custom wiki processor macros. * ReStructuredText markup support (through a processor macro) * HTML markup support (through a processor macro) * Report groups. Group results by a column. * Multi-line report rows. * Download report in CSV (Comma Separated Value) and tab-separated format * RSS 2.0 content syndication support in Timeline, Reports and Log/Browser * Better, locale-based date and time formatting. * Wiki RecentChanges support. * Overall usability, consistency and cosmetic improvements. * More documentation. * Fixed bugs: #16, #68, #81, #88, #98, #101, #102, #103, #104, #105, #110, #112, #113, #114, #117, #119, #120, #131, #132, #134, #135, #136, #138, #142, #145, #147, #151, #155, #170, #173, #174, #175, #177, #179 ----------------------------------------------------------------------------- Trac 0.5.2 'Nameless' (March 2, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.5.2 * Performance improvements. * Better unicode support in commit-messages. * TRAC_ADMIN is now a real "meta-permission" containing all other permissions. * Wiki-links of the svn:/path format can now also link to directories. * Handle subversion changesets without any "author" specified. * "view" checkboxes in the timeline view now reflect the current state. * The subversion repository is now indexed by "svnadmin initdb" instead of trac.cgi at first execution. * trac-admin now has a 'wiki dump' and 'wiki load' commands to export/import all pages to/from a directory. * Most of the inline css is removed. * IE6 navbar problem fixed. * Fixed bugs: #69, #73, #77, #78, #79, #80, #84, #85, #86, #89, #90, #91, #93, #97 Trac 0.5.1 'Unnamed' (February 25, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.5.1 * Navbar now works properly on error pages. * Cleaned up source code. Removed ugly tabs. * Added missing COPYING, AUTHORS etc. Cleaned up package. * trac-admin now works with python 2.1. * Fixed bugs: #74, #75, #76, #77 Trac 0.5 'Incognito' (February 23, 2004) http://svn.edgewall.org/repos/trac/tags/trac-0.5 * First release. Trac-1.0.1/.gitignore0000644000175200017520000000035612102610255013370 0ustar cbooscboos*~ .*.sw[op] .svn .hg build dist Trac.egg-info *.pyc testenv testing.log functional-testing.log html htmlcov figleaf .coverage .figleaf* *.figleaf Makefile.cfg doc/.build *.mo trac/htdocs/js/messages/*.js .project .pydevproject .settings Trac-1.0.1/THANKS0000644000175200017520000001436512102610255012320 0ustar cbooscboosThe following people all made noteworthy contributions to Trac, which wouldn't be what it is now without their help: * Ants Aasma ants.aasma@gmail.com * Pedro Algarvio (s0undt3ch) ufs@ufsoft.org * Brad Anderson brad@dsource.org * Roberto Aragón robarago@gmail.com * Christopher Armstrong radix * Jani Averbach jaa@jaa.iki.fi * Juanma Barranquero lektu@terra.es * Anton Batenev * Remy Blank remy.blank@pobox.com * Christian Boos cboos@bct-technology.com * Erik Bray hyugaricdeau@gmail.com * Toni Brkic toni.brkic@switchcore.com * Rocky Burt rocky.burt@myrealbox.com * Tomáš Čapek soulcharmer@gmail.com * Shane Caraveo shanec@activestate.com * Eli Carter eli.carter@commprove.com * José manuel Castroagudín Silva * Michele Cella * Sergey S. Chernov sergeych@tancher.com * Felix Colins felix@keyghost.com * Simon Cross hodgestar@gmail.com * Wesley Crucius wcrucius@sandc.com * Wolfram Diestel diestel@steloj.de * Mujdat Dinc * dju' * Ismael de Esteban ismael@tuenti.com * Juracy Filho juracy@gmail.com * Daragh Fitzpatrick Daragh@i2i-Tech.com * Justin Francis * Markus Fuchs * Lele Gaifax * Sergii Galashyn trovich@gmail.com * Raúl García * Axel Gembe * Eric Gillespie epg@netbsd.org * Daniel Kahn Gillmor * Matthew Good trac@matt-good.net * Shun-ichi Goto gotoh@taiyo.co.jp * Heiko Graeber * Chris Green cmgreen@uab.edu * Mikael Hallendal micke@imendio.com * Jeff Hammel jhammel@openplans.org * Stephen Hansen shansen@advpubtech.com * Laurie Harper zodiac@holoweb.net * Francois Harvey fharvey@securiweb.net * Tim Hatch trac@timhatch.com * Oren Held oren@held.org.il * Mikko Hellsing * Michael Hope michael.hope@hamjet.co.nz * Laurens Holst laurens@grauw.nl * David Huang khym@azeotrope.org * Zheng Hui Hu huzhengh@gmail.com * Richard Hult richard@imendio.com * InterAct Trac-ja Team trac-ja@i-act.co.jp * Paul Irish paul.irishEWWSPAM@gmail.com * Masaharu Iwai * Ethan Jucovy ethan.jucovy@gmail.com * Noah Kantrowitz (coderanger) coderanger@yahoo.com * Alexey Kinyov * Tomas Kopecek * Waldemar Kornewald wkornew@gmx.net * Nuutti Kotivuori naked@iki.fi * Leho Kraav leho@kraav.com * Jasmin Lapalme * Ian Leader ian.leader@line.co.uk * Christopher Lenz cmlenz@gmx.de * Richard Liao * David Lodge * Ivo Looser ivo.looser@gmail.com * Rui Lopes rgl ruilopes com * Tilemahos Manolatos * Angel Marin anmar@gmx.net * Simon Martin * Narine Martirosyan narine_martirosyan@instigatedesign.com * Franz Mayer franz.mayer@gefasoft.de * Mark Mc Mahon mark.m.mcmahon@gmail.com * Brian Meeker meeker.brian@gmail.com * Aristotelis Mertis * Wojciech Michalski * Keir Mierle keir@cs.utoronto.ca * James Moger jamesm@transonic.com * Tim Moloney moloney@mrsl.com * Ramiro Morales cramm0@gmail.com * Manuel Muradas * Jennifer Murtell jen@jmurtell.com * Jacob Norda jacobnorda@gmail.com * Dirkjan Ochtman dirkjan@ochtman.nl * Ryan J Ollos ryano@physiosonics.com * Jun Omae jun66j5@gmail.com * Itamar Ostricher itamarost@gmail.com * Bas van Oostveen v.oostveen@gmail.com * Seungseo Park sseopark@gmail.com * Cap Petschulat cap@cdres.com * pkou pkou@ua.fm * Alexandr Prudnikov al.prudnikov@gmail.com * Gruffudd Prys cbs201@bangor.ac.uk * Mikko Rantalainen * Mikael Relbe mikael@relbe.se * Herbert Valerio Riedel hvr@gnu.org * Nicholas Riley sabi * Armin Ronacher (mitsuhiko) armin.ronacher@active-4.com * Mark Rowe mark.rowe@bdash.net.nz * Guy Rozendorn guy@rzn.co.il * Jeroen Ruigrok van der Werven asmodai@in-nomine.org * Olliver Rutherfurd ollie * Andres Salomon dilinger@athenacr.com * Michael Scherer misc@mandrake.org * Andreas Schrattenecker vittorio * Jan Schukat shookie@email.de * Felix Schwarz felix.schwarz@oss.schwarz.eu * Emmeran Seehuber rototor@rototor.de * Odd Simon Simonsen simon-code@bvnetwork.no * Noah Slater nslater@gmail.com * Carlos Sobrinho * Bill Soudan bill@soudan.net * Ludvig Strigeus * Peter Suter petsuter@gmail.com * Alexandru Szasz * Markus Tacker m@tacker.org * Kim Taedong tdkim@unimo.co.kr * Kyosuke Takayama support@mc.neweb.ne.jp * Rayentray Tappa * Alec Thomas alec@swapoff.org * Jani Tiainen redetin@luukku.com * Ghassem Tofighi tofighi@gmail.com * Andrea Tomasini andrea.tomasini@agile42.com * Zilvinas Valinskas zilvinas@gemtek.lt * Jason Vasquez jason@mugfu.com * Venčeslav Vezjak * Leslie H. Watter * Gregor Wegberg * Jeff Weiss trac@jeffweiss.org * Tính Trương Xuân * Dmitry Yusupov dmitry_yus@yahoo.com The ever so elusive Anonymous. Diggs and Lula (official pawprint contributors) And everyone who keeps sending feedback, helping us improve Trac. ---- Our apologies to everyone we forgot to mention, but without whose invaluable help, Trac would not continue to rapidly evolve. Trac-1.0.1/setup.py0000755000175200017520000001332412102610255013114 0ustar cbooscboos#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2003-2013 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. import sys from setuptools import setup, find_packages min_python = (2, 5) if sys.version_info < min_python: print "Trac requires Python %d.%d or later" % min_python sys.exit(1) if sys.version_info >= (3,): print "Trac doesn't support Python 3 (yet)" sys.exit(1) extra = {} try: import babel extractors = [ ('**.py', 'trac.dist:extract_python', None), ('**/templates/**.html', 'genshi', None), ('**/templates/**.txt', 'genshi', {'template_class': 'genshi.template:NewTextTemplate'}), ] extra['message_extractors'] = { 'trac': extractors, 'tracopt': extractors, } from trac.dist import get_l10n_trac_cmdclass extra['cmdclass'] = get_l10n_trac_cmdclass() except ImportError: pass try: import genshi except ImportError: print "Genshi is needed by Trac setup, pre-installing" # give some context to the warnings we might get when installing Genshi setup( name = 'Trac', version = '1.0.1', description = 'Integrated SCM, wiki, issue tracker and project environment', long_description = """ Trac is a minimalistic web-based software project management and bug/issue tracking system. It provides an interface to the Subversion revision control systems, an integrated wiki, flexible issue tracking and convenient report facilities. """, author = 'Edgewall Software', author_email = 'trac-dev@googlegroups.com', license = 'BSD', url = 'http://trac.edgewall.org/', download_url = 'http://trac.edgewall.org/wiki/TracDownload', classifiers = [ 'Environment :: Web Environment', 'Framework :: Trac', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Software Development :: Bug Tracking', 'Topic :: Software Development :: Version Control', ], packages = find_packages(exclude=['*.tests']), package_data = { '': ['templates/*'], 'trac': ['htdocs/*.*', 'htdocs/README', 'htdocs/js/*.*', 'htdocs/js/messages/*.*', 'htdocs/css/*.*', 'htdocs/css/jquery-ui/*.*', 'htdocs/css/jquery-ui/images/*.*', 'htdocs/guide/*', 'locale/*/LC_MESSAGES/messages.mo', 'locale/*/LC_MESSAGES/tracini.mo'], 'trac.wiki': ['default-pages/*'], 'trac.ticket': ['workflows/*.ini'], }, test_suite = 'trac.test.suite', zip_safe = True, setup_requires = [ 'Genshi>=0.6', ], install_requires = [ 'setuptools>=0.6b1', 'Genshi>=0.6', ], extras_require = { 'Babel': ['Babel>=0.9.5'], 'Pygments': ['Pygments>=0.6'], 'reST': ['docutils>=0.3'], 'SilverCity': ['SilverCity>=0.9.4'], 'Textile': ['textile>=2.0'], }, entry_points = """ [console_scripts] trac-admin = trac.admin.console:run tracd = trac.web.standalone:main [trac.plugins] trac.about = trac.about trac.admin.console = trac.admin.console trac.admin.web_ui = trac.admin.web_ui trac.attachment = trac.attachment trac.db.mysql = trac.db.mysql_backend trac.db.postgres = trac.db.postgres_backend trac.db.sqlite = trac.db.sqlite_backend trac.mimeview.patch = trac.mimeview.patch trac.mimeview.pygments = trac.mimeview.pygments[Pygments] trac.mimeview.rst = trac.mimeview.rst[reST] trac.mimeview.txtl = trac.mimeview.txtl[Textile] trac.prefs = trac.prefs.web_ui trac.search = trac.search.web_ui trac.ticket.admin = trac.ticket.admin trac.ticket.batch = trac.ticket.batch trac.ticket.query = trac.ticket.query trac.ticket.report = trac.ticket.report trac.ticket.roadmap = trac.ticket.roadmap trac.ticket.web_ui = trac.ticket.web_ui trac.timeline = trac.timeline.web_ui trac.versioncontrol.admin = trac.versioncontrol.admin trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz trac.versioncontrol.web_ui = trac.versioncontrol.web_ui trac.web.auth = trac.web.auth trac.web.session = trac.web.session trac.wiki.admin = trac.wiki.admin trac.wiki.interwiki = trac.wiki.interwiki trac.wiki.macros = trac.wiki.macros trac.wiki.web_ui = trac.wiki.web_ui trac.wiki.web_api = trac.wiki.web_api tracopt.mimeview.enscript = tracopt.mimeview.enscript tracopt.mimeview.php = tracopt.mimeview.php tracopt.mimeview.silvercity = tracopt.mimeview.silvercity[SilverCity] tracopt.perm.authz_policy = tracopt.perm.authz_policy tracopt.perm.config_perm_provider = tracopt.perm.config_perm_provider tracopt.ticket.clone = tracopt.ticket.clone tracopt.ticket.commit_updater = tracopt.ticket.commit_updater tracopt.ticket.deleter = tracopt.ticket.deleter tracopt.versioncontrol.git.git_fs = tracopt.versioncontrol.git.git_fs tracopt.versioncontrol.svn.svn_fs = tracopt.versioncontrol.svn.svn_fs tracopt.versioncontrol.svn.svn_prop = tracopt.versioncontrol.svn.svn_prop """, **extra ) Trac-1.0.1/tracopt/0000755000175200017520000000000012102610436013051 5ustar cbooscboosTrac-1.0.1/tracopt/ticket/0000755000175200017520000000000012102610436014334 5ustar cbooscboosTrac-1.0.1/tracopt/ticket/templates/0000755000175200017520000000000012102610436016332 5ustar cbooscboosTrac-1.0.1/tracopt/ticket/templates/ticket_delete.html0000644000175200017520000000553012102610255022027 0ustar cbooscboos <py:when test="'delete'"><i18n:msg params="id">Delete Ticket #$ticket.id</i18n:msg></py:when> <py:otherwise><i18n:msg params="num, id">Delete comment $cnum on Ticket #$ticket.id</i18n:msg></py:otherwise>

Delete Ticket #$ticket.id ($ticket.status $ticket.type: $ticket.resolution)

Are you sure you want to delete this ticket? (comments: ${sum(1 for change in changes if 'cnum' in change)}, attachments: ${sum(1 for change in changes if 'cnum' not in change)})
This is an irreversible operation.

Delete comment $cnum on Ticket #$ticket.id

Are you sure you want to delete this ticket comment?
This is an irreversible operation.

Trac-1.0.1/tracopt/ticket/commit_updater.py0000644000175200017520000003034412102610255017725 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2009 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # This plugin was based on the contrib/trac-post-commit-hook script, which # had the following copyright notice: # ---------------------------------------------------------------------------- # Copyright (c) 2004 Stephen Hansen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # ---------------------------------------------------------------------------- from __future__ import with_statement from datetime import datetime import re from genshi.builder import tag from trac.config import BoolOption, Option from trac.core import Component, implements from trac.perm import PermissionCache from trac.resource import Resource from trac.ticket import Ticket from trac.ticket.notification import TicketNotifyEmail from trac.util.datefmt import utc from trac.util.text import exception_to_unicode from trac.util.translation import cleandoc_ from trac.versioncontrol import IRepositoryChangeListener, RepositoryManager from trac.versioncontrol.web_ui.changeset import ChangesetModule from trac.wiki.formatter import format_to_html from trac.wiki.macros import WikiMacroBase class CommitTicketUpdater(Component): """Update tickets based on commit messages. This component hooks into changeset notifications and searches commit messages for text in the form of: {{{ command #1 command #1, #2 command #1 & #2 command #1 and #2 }}} Instead of the short-hand syntax "#1", "ticket:1" can be used as well, e.g.: {{{ command ticket:1 command ticket:1, ticket:2 command ticket:1 & ticket:2 command ticket:1 and ticket:2 }}} In addition, the ':' character can be omitted and issue or bug can be used instead of ticket. You can have more than one command in a message. The following commands are supported. There is more than one spelling for each command, to make this as user-friendly as possible. close, closed, closes, fix, fixed, fixes:: The specified tickets are closed, and the commit message is added to them as a comment. references, refs, addresses, re, see:: The specified tickets are left in their current status, and the commit message is added to them as a comment. A fairly complicated example of what you can do is with a commit message of: Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12. This will close #10 and #12, and add a note to #12. """ implements(IRepositoryChangeListener) envelope = Option('ticket', 'commit_ticket_update_envelope', '', """Require commands to be enclosed in an envelope. Must be empty or contain two characters. For example, if set to "[]", then commands must be in the form of [closes #4].""") commands_close = Option('ticket', 'commit_ticket_update_commands.close', 'close closed closes fix fixed fixes', """Commands that close tickets, as a space-separated list.""") commands_refs = Option('ticket', 'commit_ticket_update_commands.refs', 'addresses re references refs see', """Commands that add a reference, as a space-separated list. If set to the special value , all tickets referenced by the message will get a reference to the changeset.""") check_perms = BoolOption('ticket', 'commit_ticket_update_check_perms', 'true', """Check that the committer has permission to perform the requested operations on the referenced tickets. This requires that the user names be the same for Trac and repository operations.""") notify = BoolOption('ticket', 'commit_ticket_update_notify', 'true', """Send ticket change notification when updating a ticket.""") ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)' ticket_reference = ticket_prefix + '[0-9]+' ticket_command = (r'(?P[A-Za-z]*)\s*.?\s*' r'(?P%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' % (ticket_reference, ticket_reference)) @property def command_re(self): (begin, end) = (re.escape(self.envelope[0:1]), re.escape(self.envelope[1:2])) return re.compile(begin + self.ticket_command + end) ticket_re = re.compile(ticket_prefix + '([0-9]+)') _last_cset_id = None # IRepositoryChangeListener methods def changeset_added(self, repos, changeset): if self._is_duplicate(changeset): return tickets = self._parse_message(changeset.message) comment = self.make_ticket_comment(repos, changeset) self._update_tickets(tickets, changeset, comment, datetime.now(utc)) def changeset_modified(self, repos, changeset, old_changeset): if self._is_duplicate(changeset): return tickets = self._parse_message(changeset.message) old_tickets = {} if old_changeset is not None: old_tickets = self._parse_message(old_changeset.message) tickets = dict(each for each in tickets.iteritems() if each[0] not in old_tickets) comment = self.make_ticket_comment(repos, changeset) self._update_tickets(tickets, changeset, comment, datetime.now(utc)) def _is_duplicate(self, changeset): # Avoid duplicate changes with multiple scoped repositories cset_id = (changeset.rev, changeset.message, changeset.author, changeset.date) if cset_id != self._last_cset_id: self._last_cset_id = cset_id return False return True def _parse_message(self, message): """Parse the commit message and return the ticket references.""" cmd_groups = self.command_re.findall(message) functions = self._get_functions() tickets = {} for cmd, tkts in cmd_groups: func = functions.get(cmd.lower()) if not func and self.commands_refs.strip() == '': func = self.cmd_refs if func: for tkt_id in self.ticket_re.findall(tkts): tickets.setdefault(int(tkt_id), []).append(func) return tickets def make_ticket_comment(self, repos, changeset): """Create the ticket comment from the changeset data.""" revstring = str(changeset.rev) if repos.reponame: revstring += '/' + repos.reponame return """\ In [changeset:"%s"]: {{{ #!CommitTicketReference repository="%s" revision="%s" %s }}}""" % (revstring, repos.reponame, changeset.rev, changeset.message.strip()) def _update_tickets(self, tickets, changeset, comment, date): """Update the tickets with the given comment.""" perm = PermissionCache(self.env, changeset.author) for tkt_id, cmds in tickets.iteritems(): try: self.log.debug("Updating ticket #%d", tkt_id) save = False with self.env.db_transaction: ticket = Ticket(self.env, tkt_id) ticket_perm = perm(ticket.resource) for cmd in cmds: if cmd(ticket, changeset, ticket_perm) is not False: save = True if save: ticket.save_changes(changeset.author, comment, date) if save: self._notify(ticket, date) except Exception, e: self.log.error("Unexpected error while processing ticket " "#%s: %s", tkt_id, exception_to_unicode(e)) def _notify(self, ticket, date): """Send a ticket update notification.""" if not self.notify: return try: tn = TicketNotifyEmail(self.env) tn.notify(ticket, newticket=False, modtime=date) except Exception, e: self.log.error("Failure sending notification on change to " "ticket #%s: %s", ticket.id, exception_to_unicode(e)) def _get_functions(self): """Create a mapping from commands to command functions.""" functions = {} for each in dir(self): if not each.startswith('cmd_'): continue func = getattr(self, each) for cmd in getattr(self, 'commands_' + each[4:], '').split(): functions[cmd] = func return functions # Command-specific behavior # The ticket isn't updated if all extracted commands return False. def cmd_close(self, ticket, changeset, perm): if self.check_perms and not 'TICKET_MODIFY' in perm: self.log.info("%s doesn't have TICKET_MODIFY permission for #%d", changeset.author, ticket.id) return False ticket['status'] = 'closed' ticket['resolution'] = 'fixed' if not ticket['owner']: ticket['owner'] = changeset.author def cmd_refs(self, ticket, changeset, perm): if self.check_perms and not 'TICKET_APPEND' in perm: self.log.info("%s doesn't have TICKET_APPEND permission for #%d", changeset.author, ticket.id) return False class CommitTicketReferenceMacro(WikiMacroBase): _domain = 'messages' _description = cleandoc_( """Insert a changeset message into the output. This macro must be called using wiki processor syntax as follows: {{{ {{{ #!CommitTicketReference repository="reponame" revision="rev" }}} }}} where the arguments are the following: - `repository`: the repository containing the changeset - `revision`: the revision of the desired changeset """) def expand_macro(self, formatter, name, content, args={}): reponame = args.get('repository') or '' rev = args.get('revision') repos = RepositoryManager(self.env).get_repository(reponame) try: changeset = repos.get_changeset(rev) message = changeset.message rev = changeset.rev resource = repos.resource except Exception: message = content resource = Resource('repository', reponame) if formatter.context.resource.realm == 'ticket': ticket_re = CommitTicketUpdater.ticket_re if not any(int(tkt_id) == int(formatter.context.resource.id) for tkt_id in ticket_re.findall(message)): return tag.p("(The changeset message doesn't reference this " "ticket)", class_='hint') if ChangesetModule(self.env).wiki_format_messages: return tag.div(format_to_html(self.env, formatter.context.child('changeset', rev, parent=resource), message, escape_newlines=True), class_='message') else: return tag.pre(message, class_='message') Trac-1.0.1/tracopt/ticket/deleter.py0000644000175200017520000001545712102610255016345 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2010 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. from genshi.builder import tag from genshi.filters import Transformer from genshi.filters.transform import StreamBuffer from trac.core import Component, TracError, implements from trac.ticket.model import Ticket from trac.ticket.web_ui import TicketModule from trac.util import get_reporter_id from trac.util.datefmt import from_utimestamp from trac.util.presentation import captioned_button from trac.util.translation import _ from trac.web.api import IRequestFilter, IRequestHandler, ITemplateStreamFilter from trac.web.chrome import ITemplateProvider, add_notice, add_stylesheet class TicketDeleter(Component): """Ticket and ticket comment deleter. This component allows deleting ticket comments and complete tickets. For users having `TICKET_ADMIN` permission, it adds a "Delete" button next to each "Reply" button on the page. The button in the ticket description requests deletion of the complete ticket, and the buttons in the change history request deletion of a single comment. '''Comment and ticket deletion are irreversible (and therefore ''dangerous'') operations.''' For that reason, a confirmation step is requested. The confirmation page shows the ticket box (in the case of a ticket deletion) or the ticket change (in the case of a comment deletion). """ implements(ITemplateProvider, ITemplateStreamFilter, IRequestFilter, IRequestHandler) # ITemplateProvider methods def get_htdocs_dirs(self): return [] def get_templates_dirs(self): from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if filename not in ('ticket.html', 'ticket_preview.html'): return stream ticket = data.get('ticket') if not (ticket and ticket.exists and 'TICKET_ADMIN' in req.perm(ticket.resource)): return stream # Insert "Delete" buttons for ticket description and each comment def delete_ticket(): return tag.form( tag.div( tag.input(type='hidden', name='action', value='delete'), tag.input(type='submit', value=captioned_button(req, u'–', # 'EN DASH' _("Delete")), title=_('Delete ticket'), class_="trac-delete"), class_="inlinebuttons"), action='#', method='get') def delete_comment(): for event in buffer: cnum, cdate = event[1][1].get('id')[12:].split('-', 1) return tag.form( tag.div( tag.input(type='hidden', name='action', value='delete-comment'), tag.input(type='hidden', name='cnum', value=cnum), tag.input(type='hidden', name='cdate', value=cdate), tag.input(type='submit', value=captioned_button(req, u'–', # 'EN DASH' _("Delete")), title=_('Delete comment %(num)s', num=cnum), class_="trac-delete"), class_="inlinebuttons"), action='#', method='get') buffer = StreamBuffer() return stream | Transformer('//div[@class="description"]' '/h3[@id="comment:description"]') \ .after(delete_ticket).end() \ .select('//div[starts-with(@class, "change")]/@id') \ .copy(buffer).end() \ .select('//div[starts-with(@class, "change") and @id]' '/div[@class="trac-ticket-buttons"]') \ .prepend(delete_comment) # IRequestFilter methods def pre_process_request(self, req, handler): if handler is not TicketModule(self.env): return handler action = req.args.get('action') if action in ('delete', 'delete-comment'): return self else: return handler def post_process_request(self, req, template, data, content_type): return template, data, content_type # IRequestHandler methods def match_request(self, req): return False def process_request(self, req): id = int(req.args.get('id')) req.perm('ticket', id).require('TICKET_ADMIN') ticket = Ticket(self.env, id) action = req.args['action'] cnum = req.args.get('cnum') if req.method == 'POST': if 'cancel' in req.args: href = req.href.ticket(id) if action == 'delete-comment': href += '#comment:%s' % cnum req.redirect(href) if action == 'delete': ticket.delete() add_notice(req, _('The ticket #%(id)s has been deleted.', id=ticket.id)) req.redirect(req.href()) elif action == 'delete-comment': cdate = from_utimestamp(long(req.args.get('cdate'))) ticket.delete_change(cdate=cdate) add_notice(req, _('The ticket comment %(num)s on ticket ' '#%(id)s has been deleted.', num=cnum, id=ticket.id)) req.redirect(req.href.ticket(id)) tm = TicketModule(self.env) data = tm._prepare_data(req, ticket) tm._insert_ticket_data(req, ticket, data, get_reporter_id(req, 'author'), {}) data.update(action=action, cdate=None) if action == 'delete-comment': data['cdate'] = req.args.get('cdate') cdate = from_utimestamp(long(data['cdate'])) for change in data['changes']: if change.get('date') == cdate: data['change'] = change data['cnum'] = change.get('cnum') break else: raise TracError(_('Comment %(num)s not found', num=cnum)) add_stylesheet(req, 'common/css/ticket.css') return 'ticket_delete.html', data, None Trac-1.0.1/tracopt/ticket/__init__.py0000644000175200017520000000000012102610255016432 0ustar cbooscboosTrac-1.0.1/tracopt/ticket/clone.py0000644000175200017520000000510212102610255016003 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2011 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. from genshi.builder import tag from genshi.filters import Transformer from trac.core import Component, implements from trac.web.api import ITemplateStreamFilter from trac.util.presentation import captioned_button from trac.util.translation import _ class TicketCloneButton(Component): """Add a 'Clone' button to the ticket box. This button is located next to the 'Reply' to description button, and pressing it will send a request for creating a new ticket which will be based on the cloned one. """ implements(ITemplateStreamFilter) # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): if filename == 'ticket.html': ticket = data.get('ticket') if ticket and ticket.exists and \ 'TICKET_ADMIN' in req.perm(ticket.resource): filter = Transformer('//h3[@id="comment:description"]') stream |= filter.after(self._clone_form(req, ticket, data)) return stream def _clone_form(self, req, ticket, data): fields = {} for f in data.get('fields', []): name = f['name'] if name == 'summary': fields['summary'] = _("%(summary)s (cloned)", summary=ticket['summary']) elif name == 'description': fields['description'] = \ _("Cloned from #%(id)s:\n----\n%(description)s", id=ticket.id, description=ticket['description']) else: fields[name] = ticket[name] return tag.form( tag.div( tag.input(type="submit", name="clone", value=captioned_button(req, '+#', _("Clone")), title=_("Create a copy of this ticket")), [tag.input(type="hidden", name='field_' + n, value=v) for n, v in fields.iteritems()], tag.input(type="hidden", name='preview', value=''), class_="inlinebuttons"), method="post", action=req.href.newticket()) Trac-1.0.1/tracopt/versioncontrol/0000755000175200017520000000000012102610436016137 5ustar cbooscboosTrac-1.0.1/tracopt/versioncontrol/svn/0000755000175200017520000000000012102610436016745 5ustar cbooscboosTrac-1.0.1/tracopt/versioncontrol/svn/svn_prop.py0000644000175200017520000004303512102610255021171 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2005-2009 Edgewall Software # Copyright (C) 2005 Christopher Lenz # Copyright (C) 2005-2007 Christian Boos # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Christopher Lenz # Christian Boos import posixpath from genshi.builder import tag from trac.config import ConfigSection from trac.core import * from trac.versioncontrol.api import NoSuchNode, RepositoryManager from trac.versioncontrol.web_ui.browser import IPropertyRenderer from trac.versioncontrol.web_ui.changeset import IPropertyDiffRenderer from trac.util import Ranges, to_ranges from trac.util.translation import _, tag_ from tracopt.versioncontrol.svn.svn_fs import _path_within_scope class SubversionPropertyRenderer(Component): implements(IPropertyRenderer) svn_externals_section = ConfigSection('svn:externals', """The TracBrowser for Subversion can interpret the `svn:externals` property of folders. By default, it only turns the URLs into links as Trac can't browse remote repositories. However, if you have another Trac instance (or an other repository browser like [http://www.viewvc.org/ ViewVC]) configured to browse the target repository, then you can instruct Trac which other repository browser to use for which external URL. This mapping is done in the `[svn:externals]` section of the TracIni. Example: {{{ [svn:externals] 1 = svn://server/repos1 http://trac/proj1/browser/$path?rev=$rev 2 = svn://server/repos2 http://trac/proj2/browser/$path?rev=$rev 3 = http://theirserver.org/svn/eng-soft http://ourserver/viewvc/svn/$path/?pathrev=25914 4 = svn://anotherserver.com/tools_repository http://ourserver/tracs/tools/browser/$path?rev=$rev }}} With the above, the `svn://anotherserver.com/tools_repository/tags/1.1/tools` external will be mapped to `http://ourserver/tracs/tools/browser/tags/1.1/tools?rev=` (and `rev` will be set to the appropriate revision number if the external additionally specifies a revision, see the [http://svnbook.red-bean.com/en/1.4/svn.advanced.externals.html SVN Book on externals] for more details). Note that the number used as a key in the above section is purely used as a place holder, as the URLs themselves can't be used as a key due to various limitations in the configuration file parser. Finally, the relative URLs introduced in [http://subversion.apache.org/docs/release-notes/1.5.html#externals Subversion 1.5] are not yet supported. (''since 0.11'')""") def __init__(self): self._externals_map = {} # IPropertyRenderer methods def match_property(self, name, mode): if name in ('svn:externals', 'svn:needs-lock'): return 4 return 2 if name in ('svn:mergeinfo', 'svnmerge-blocked', 'svnmerge-integrated') else 0 def render_property(self, name, mode, context, props): if name == 'svn:externals': return self._render_externals(props[name]) elif name == 'svn:needs-lock': return self._render_needslock(context) elif name == 'svn:mergeinfo' or name.startswith('svnmerge-'): return self._render_mergeinfo(name, mode, context, props) def _render_externals(self, prop): if not self._externals_map: for dummykey, value in self.svn_externals_section.options(): value = value.split() if len(value) != 2: self.log.warn("svn:externals entry %s doesn't contain " "a space-separated key value pair, skipping.", dummykey) continue key, value = value self._externals_map[key] = value.replace('%', '%%') \ .replace('$path', '%(path)s') \ .replace('$rev', '%(rev)s') externals = [] for external in prop.splitlines(): elements = external.split() if not elements: continue localpath, rev, url = elements[0], '', elements[-1] if localpath.startswith('#'): externals.append((external, None, None, None, None)) continue if len(elements) == 3: rev = elements[1] rev = rev.replace('-r', '') # retrieve a matching entry in the externals map prefix = [] base_url = url while base_url: if base_url in self._externals_map or base_url == u'/': break base_url, pref = posixpath.split(base_url) prefix.append(pref) href = self._externals_map.get(base_url) revstr = ' at revision ' + rev if rev else '' if not href and (url.startswith('http://') or url.startswith('https://')): href = url.replace('%', '%%') if href: remotepath = '' if prefix: remotepath = posixpath.join(*reversed(prefix)) externals.append((localpath, revstr, base_url, remotepath, href % {'path': remotepath, 'rev': rev})) else: externals.append((localpath, revstr, url, None, None)) externals_data = [] for localpath, rev, url, remotepath, href in externals: label = localpath if url is None: title = '' elif href: if url: url = ' in ' + url label += rev + url title = ''.join((remotepath, rev, url)) else: title = _('No svn:externals configured in trac.ini') externals_data.append((label, href, title)) return tag.ul([tag.li(tag.a(label, href=href, title=title)) for label, href, title in externals_data]) def _render_needslock(self, context): return tag.img(src=context.href.chrome('common/lock-locked.png'), alt="needs lock", title="needs lock") def _render_mergeinfo(self, name, mode, context, props): rows = [] for row in props[name].splitlines(): try: (path, revs) = row.rsplit(':', 1) rows.append([tag.td(path), tag.td(revs.replace(',', u',\u200b'))]) except ValueError: rows.append(tag.td(row, colspan=2)) return tag.table(tag.tbody([tag.tr(row) for row in rows]), class_='props') class SubversionMergePropertyRenderer(Component): implements(IPropertyRenderer) # IPropertyRenderer methods def match_property(self, name, mode): return 4 if name in ('svn:mergeinfo', 'svnmerge-blocked', 'svnmerge-integrated') else 0 def render_property(self, name, mode, context, props): """Parse svn:mergeinfo and svnmerge-* properties, converting branch names to links and providing links to the revision log for merged and eligible revisions. """ has_eligible = name in ('svnmerge-integrated', 'svn:mergeinfo') revs_label = _('blocked') if name.endswith('blocked') else _('merged') revs_cols = 2 if has_eligible else None reponame = context.resource.parent.id target_path = context.resource.id repos = RepositoryManager(self.env).get_repository(reponame) target_rev = context.resource.version if has_eligible: node = repos.get_node(target_path, target_rev) branch_starts = {} for path, rev in node.get_copy_ancestry(): if path not in branch_starts: branch_starts[path] = rev + 1 rows = [] if name.startswith('svnmerge-'): sources = props[name].split() else: sources = props[name].splitlines() for line in sources: path, revs = line.split(':', 1) spath = _path_within_scope(repos.scope, path) if spath is None: continue revs = revs.strip() inheritable, non_inheritable = _partition_inheritable(revs) revs = ','.join(inheritable) deleted = False try: node = repos.get_node(spath, target_rev) resource = context.resource.parent.child('source', spath) if 'LOG_VIEW' in context.perm(resource): row = [_get_source_link(spath, context), _get_revs_link(revs_label, context, spath, revs)] if non_inheritable: non_inheritable = ','.join(non_inheritable) row.append(_get_revs_link(_('non-inheritable'), context, spath, non_inheritable, _('merged on the directory ' 'itself but not below'))) if has_eligible: first_rev = branch_starts.get(spath) if not first_rev: first_rev = node.get_branch_origin() eligible = set(xrange(first_rev or 1, target_rev + 1)) eligible -= set(Ranges(revs)) blocked = _get_blocked_revs(props, name, spath) if blocked: eligible -= set(Ranges(blocked)) if eligible: nrevs = repos._get_node_revs(spath, max(eligible), min(eligible)) eligible &= set(nrevs) eligible = to_ranges(eligible) row.append(_get_revs_link(_('eligible'), context, spath, eligible)) rows.append((False, spath, [tag.td(each) for each in row])) continue except NoSuchNode: deleted = True revs = revs.replace(',', u',\u200b') rows.append((deleted, spath, [tag.td('/' + spath), tag.td(revs, colspan=revs_cols)])) if not rows: return None rows.sort() has_deleted = rows[-1][0] if rows else None return tag(has_deleted and tag.a(_('(toggle deleted branches)'), class_='trac-toggledeleted', href='#'), tag.table(tag.tbody( [tag.tr(row, class_='trac-deleted' if deleted else None) for deleted, spath, row in rows]), class_='props')) def _partition_inheritable(revs): """Non-inheritable revision ranges are marked with a trailing '*'.""" inheritable, non_inheritable = [], [] for r in revs.split(','): if r and r[-1] == '*': non_inheritable.append(r[:-1]) else: inheritable.append(r) return inheritable, non_inheritable def _get_blocked_revs(props, name, path): """Return the revisions blocked from merging for the given property name and path. """ if name == 'svnmerge-integrated': prop = props.get('svnmerge-blocked', '') else: return "" for line in prop.splitlines(): try: p, revs = line.split(':', 1) if p.strip('/') == path: return revs except Exception: pass return "" def _get_source_link(spath, context): """Return a link to a merge source.""" reponame = context.resource.parent.id return tag.a('/' + spath, title=_('View merge source'), href=context.href.browser(reponame or None, spath, rev=context.resource.version)) def _get_revs_link(label, context, spath, revs, title=None): """Return a link to the revision log when more than one revision is given, to the revision itself for a single revision, or a `` with "no revision" for none. """ reponame = context.resource.parent.id if not revs: return tag.span(label, title=_('No revisions')) elif ',' in revs or '-' in revs: revs_href = context.href.log(reponame or None, spath, revs=revs) else: revs_href = context.href.changeset(revs, reponame or None, spath) revs = revs.replace(',', ', ') if title: title = _("%(title)s: %(revs)s", title=title, revs=revs) else: title = revs return tag.a(label, title=title, href=revs_href) class SubversionMergePropertyDiffRenderer(Component): implements(IPropertyDiffRenderer) # IPropertyDiffRenderer methods def match_property_diff(self, name): return 4 if name in ('svn:mergeinfo', 'svnmerge-blocked', 'svnmerge-integrated') else 0 def render_property_diff(self, name, old_context, old_props, new_context, new_props, options): # Build 5 columns table showing modifications on merge sources # || source || added || removed || added (ni) || removed (ni) || # || source || removed || rm = RepositoryManager(self.env) repos = rm.get_repository(old_context.resource.parent.id) def parse_sources(props): sources = {} for line in props[name].splitlines(): path, revs = line.split(':', 1) spath = _path_within_scope(repos.scope, path) if spath is not None: inheritable, non_inheritable = _partition_inheritable(revs) sources[spath] = (set(Ranges(inheritable)), set(Ranges(non_inheritable))) return sources old_sources = parse_sources(old_props) new_sources = parse_sources(new_props) # Go through new sources, detect modified ones or added ones blocked = name.endswith('blocked') added_label = [_("merged: "), _("blocked: ")][blocked] removed_label = [_("reverse-merged: "), _("un-blocked: ")][blocked] added_ni_label = _("marked as non-inheritable: ") removed_ni_label = _("unmarked as non-inheritable: ") def revs_link(revs, context): if revs: revs = to_ranges(revs) return _get_revs_link(revs.replace(',', u',\u200b'), context, spath, revs) modified_sources = [] for spath, (new_revs, new_revs_ni) in new_sources.iteritems(): if spath in old_sources: (old_revs, old_revs_ni), status = old_sources.pop(spath), None else: old_revs = old_revs_ni = set() status = _(' (added)') added = new_revs - old_revs removed = old_revs - new_revs added_ni = new_revs_ni - old_revs_ni removed_ni = old_revs_ni - new_revs_ni try: all_revs = set(repos._get_node_revs(spath)) # TODO: also pass first_rev here, for getting smaller a set # (this is an optmization fix, result is already correct) added &= all_revs removed &= all_revs added_ni &= all_revs removed_ni &= all_revs except NoSuchNode: pass if added or removed: modified_sources.append(( spath, [_get_source_link(spath, new_context), status], added and tag(added_label, revs_link(added, new_context)), removed and tag(removed_label, revs_link(removed, old_context)), added_ni and tag(added_ni_label, revs_link(added_ni, new_context)), removed_ni and tag(removed_ni_label, revs_link(removed_ni, old_context)) )) # Go through remaining old sources, those were deleted removed_sources = [] for spath, old_revs in old_sources.iteritems(): removed_sources.append((spath, _get_source_link(spath, old_context))) if modified_sources or removed_sources: modified_sources.sort() removed_sources.sort() changes = tag.table(tag.tbody( [tag.tr(tag.td(c) for c in cols[1:]) for cols in modified_sources], [tag.tr(tag.td(src), tag.td(_('removed'), colspan=4)) for spath, src in removed_sources]), class_='props') else: changes = tag.em(_(' (with no actual effect on merging)')) return tag.li(tag_('Property %(prop)s changed', prop=tag.strong(name)), changes) Trac-1.0.1/tracopt/versioncontrol/svn/svn_fs.py0000644000175200017520000012170512102610255020622 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2005-2011 Edgewall Software # Copyright (C) 2005 Christopher Lenz # Copyright (C) 2005-2007 Christian Boos # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Christopher Lenz # Christian Boos """ Note about Unicode ------------------ The Subversion bindings are not unicode-aware and they expect to receive UTF-8 encoded `string` parameters, On the other hand, all paths manipulated by Trac are `unicode` objects. Therefore: * before being handed out to SVN, the Trac paths have to be encoded to UTF-8, using `_to_svn()` * before being handed out to Trac, a SVN path has to be decoded from UTF-8, using `_from_svn()` Whenever a value has to be stored as utf8, we explicitly mark the variable name with "_utf8", in order to avoid any possible confusion. Warning: `SubversionNode.get_content()` returns an object from which one can read a stream of bytes. NO guarantees can be given about what that stream of bytes represents. It might be some text, encoded in some way or another. SVN properties *might* give some hints about the content, but they actually only reflect the beliefs of whomever set those properties... """ import os.path import weakref import posixpath from trac.config import ListOption from trac.core import * from trac.env import ISystemInfoProvider from trac.versioncontrol import Changeset, Node, Repository, \ IRepositoryConnector, \ NoSuchChangeset, NoSuchNode from trac.versioncontrol.cache import CachedRepository from trac.util import embedded_numbers from trac.util.text import exception_to_unicode, to_unicode from trac.util.translation import _ from trac.util.datefmt import from_utimestamp application_pool = None def _import_svn(): global fs, repos, core, delta, _kindmap from svn import fs, repos, core, delta _kindmap = {core.svn_node_dir: Node.DIRECTORY, core.svn_node_file: Node.FILE} # Protect svn.core methods from GC Pool.apr_pool_clear = staticmethod(core.apr_pool_clear) Pool.apr_pool_destroy = staticmethod(core.apr_pool_destroy) def _to_svn(pool, *args): """Expect a pool and a list of `unicode` path components. Returns an UTF-8 encoded string suitable for the Subversion python bindings (the returned path never starts with a leading "/") """ return core.svn_path_canonicalize('/'.join(args).lstrip('/') .encode('utf-8'), pool) def _from_svn(path): """Expect an UTF-8 encoded string and transform it to an `unicode` object But Subversion repositories built from conversion utilities can have non-UTF-8 byte strings, so we have to convert using `to_unicode`. """ return path and to_unicode(path, 'utf-8') # The following 3 helpers deal with unicode paths def _normalize_path(path): """Remove leading "/", except for the root.""" return path and path.strip('/') or '/' def _path_within_scope(scope, fullpath): """Remove the leading scope from repository paths. Return `None` if the path is not is scope. """ if fullpath is not None: fullpath = fullpath.lstrip('/') if scope == '/': return _normalize_path(fullpath) scope = scope.strip('/') if (fullpath + '/').startswith(scope + '/'): return fullpath[len(scope) + 1:] or '/' def _is_path_within_scope(scope, fullpath): """Check whether the given `fullpath` is within the given `scope`""" if scope == '/': return fullpath is not None fullpath = fullpath.lstrip('/') if fullpath else '' scope = scope.strip('/') return (fullpath + '/').startswith(scope + '/') # svn_opt_revision_t helpers def _svn_rev(num): value = core.svn_opt_revision_value_t() value.number = num revision = core.svn_opt_revision_t() revision.kind = core.svn_opt_revision_number revision.value = value return revision def _svn_head(): revision = core.svn_opt_revision_t() revision.kind = core.svn_opt_revision_head return revision # apr_pool_t helpers def _mark_weakpool_invalid(weakpool): if weakpool(): weakpool()._mark_invalid() class Pool(object): """A Pythonic memory pool object""" def __init__(self, parent_pool=None): """Create a new memory pool""" global application_pool self._parent_pool = parent_pool or application_pool # Create pool if self._parent_pool: self._pool = core.svn_pool_create(self._parent_pool()) else: # If we are an application-level pool, # then initialize APR and set this pool # to be the application-level pool core.apr_initialize() application_pool = self self._pool = core.svn_pool_create(None) self._mark_valid() def __call__(self): return self._pool def valid(self): """Check whether this memory pool and its parents are still valid""" return hasattr(self,"_is_valid") def assert_valid(self): """Assert that this memory_pool is still valid.""" assert self.valid() def clear(self): """Clear embedded memory pool. Invalidate all subpools.""" self.apr_pool_clear(self._pool) self._mark_valid() def destroy(self): """Destroy embedded memory pool. If you do not destroy the memory pool manually, Python will destroy it automatically.""" global application_pool self.assert_valid() # Destroy pool self.apr_pool_destroy(self._pool) # Clear application pool and terminate APR if necessary if not self._parent_pool: application_pool = None self._mark_invalid() def __del__(self): """Automatically destroy memory pools, if necessary""" if self.valid(): self.destroy() def _mark_valid(self): """Mark pool as valid""" if self._parent_pool: # Refer to self using a weakreference so that we don't # create a reference cycle weakself = weakref.ref(self) # Set up callbacks to mark pool as invalid when parents # are destroyed self._weakref = weakref.ref(self._parent_pool._is_valid, lambda x: \ _mark_weakpool_invalid(weakself)) # mark pool as valid self._is_valid = lambda: 1 def _mark_invalid(self): """Mark pool as invalid""" if self.valid(): # Mark invalid del self._is_valid # Free up memory del self._parent_pool if hasattr(self, "_weakref"): del self._weakref class SvnCachedRepository(CachedRepository): """Subversion-specific cached repository, zero-pads revision numbers in the cache tables. """ has_linear_changesets = True def db_rev(self, rev): return '%010d' % rev def rev_db(self, rev): return int(rev or 0) class SubversionConnector(Component): implements(ISystemInfoProvider, IRepositoryConnector) branches = ListOption('svn', 'branches', 'trunk, branches/*', doc= """Comma separated list of paths categorized as branches. If a path ends with '*', then all the directory entries found below that path will be included. Example: `/trunk, /branches/*, /projectAlpha/trunk, /sandbox/*` """) tags = ListOption('svn', 'tags', 'tags/*', doc= """Comma separated list of paths categorized as tags. If a path ends with '*', then all the directory entries found below that path will be included. Example: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1` """) error = None def __init__(self): self._version = None try: _import_svn() self.log.debug('Subversion bindings imported') except ImportError, e: self.error = e self.log.info('Failed to load Subversion bindings', exc_info=True) else: version = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO) self._version = '%d.%d.%d' % version + core.SVN_VER_TAG if version[0] < 1: self.error = _("Subversion >= 1.0 required, found %(version)s", version=self._version) Pool() # ISystemInfoProvider methods def get_system_info(self): if self._version is not None: yield 'Subversion', self._version # IRepositoryConnector methods def get_supported_types(self): prio = 1 if self.error: prio = -1 yield ("direct-svnfs", prio * 4) yield ("svnfs", prio * 4) yield ("svn", prio * 2) def get_repository(self, type, dir, params): """Return a `SubversionRepository`. The repository is wrapped in a `CachedRepository`, unless `type` is 'direct-svnfs'. """ params.update(tags=self.tags, branches=self.branches) repos = SubversionRepository(dir, params, self.log) if type != 'direct-svnfs': repos = SvnCachedRepository(self.env, repos, self.log) return repos class SubversionRepository(Repository): """Repository implementation based on the svn.fs API.""" has_linear_changesets = True def __init__(self, path, params, log): self.log = log self.pool = Pool() # Remove any trailing slash or else subversion might abort if isinstance(path, unicode): path_utf8 = path.encode('utf-8') else: # note that this should usually not happen (unicode arg expected) path_utf8 = to_unicode(path).encode('utf-8') path_utf8 = os.path.normpath(path_utf8).replace('\\', '/') self.path = path_utf8.decode('utf-8') root_path_utf8 = repos.svn_repos_find_root_path(path_utf8, self.pool()) if root_path_utf8 is None: raise TracError(_("%(path)s does not appear to be a Subversion " "repository.", path=to_unicode(path_utf8))) try: self.repos = repos.svn_repos_open(root_path_utf8, self.pool()) except core.SubversionException, e: raise TracError(_("Couldn't open Subversion repository %(path)s: " "%(svn_error)s", path=to_unicode(path_utf8), svn_error=exception_to_unicode(e))) self.fs_ptr = repos.svn_repos_fs(self.repos) self.uuid = fs.get_uuid(self.fs_ptr, self.pool()) self.base = 'svn:%s:%s' % (self.uuid, _from_svn(root_path_utf8)) name = 'svn:%s:%s' % (self.uuid, self.path) Repository.__init__(self, name, params, log) # if root_path_utf8 is shorter than the path_utf8, the difference is # this scope (which always starts with a '/') if root_path_utf8 != path_utf8: self.scope = path_utf8[len(root_path_utf8):].decode('utf-8') if not self.scope[-1] == '/': self.scope += '/' else: self.scope = '/' assert self.scope[0] == '/' # we keep root_path_utf8 for RA ra_prefix = 'file:///' if os.name == 'nt' else 'file://' self.ra_url_utf8 = ra_prefix + root_path_utf8 self.clear() def clear(self, youngest_rev=None): """Reset notion of `youngest` and `oldest`""" self.youngest = None if youngest_rev is not None: self.youngest = self.normalize_rev(youngest_rev) self.oldest = None def __del__(self): self.close() def has_node(self, path, rev=None, pool=None): """Check if `path` exists at `rev` (or latest if unspecified)""" if not pool: pool = self.pool rev = self.normalize_rev(rev) rev_root = fs.revision_root(self.fs_ptr, rev, pool()) node_type = fs.check_path(rev_root, _to_svn(pool(), self.scope, path), pool()) return node_type in _kindmap def normalize_path(self, path): """Take any path specification and produce a path suitable for the rest of the API """ return _normalize_path(path) def normalize_rev(self, rev): """Take any revision specification and produce a revision suitable for the rest of the API """ if rev is None or isinstance(rev, basestring) and \ rev.lower() in ('', 'head', 'latest', 'youngest'): return self.youngest_rev else: try: rev = int(rev) if rev <= self.youngest_rev: return rev except (ValueError, TypeError): pass raise NoSuchChangeset(rev) def close(self): """Dispose of low-level resources associated to this repository.""" if self.pool: self.pool.destroy() self.repos = self.fs_ptr = self.pool = None def get_base(self): """Retrieve the base path corresponding to the Subversion repository itself. This is the same as the `.path` property minus the intra-repository scope, if one was specified. """ return self.base def _get_tags_or_branches(self, paths): """Retrieve known branches or tags.""" for path in self.params.get(paths, []): if path.endswith('*'): folder = posixpath.dirname(path) try: entries = [n for n in self.get_node(folder).get_entries()] for node in sorted(entries, key=lambda n: embedded_numbers(n.path.lower())): if node.kind == Node.DIRECTORY: yield node except Exception: # no right (TODO: use a specific Exception) pass else: try: yield self.get_node(path) except Exception: # no right pass def get_quickjump_entries(self, rev): """Retrieve known branches, as (name, id) pairs. Purposedly ignores `rev` and always takes the last revision. """ for n in self._get_tags_or_branches('branches'): yield 'branches', n.path, n.path, None for n in self._get_tags_or_branches('tags'): yield 'tags', n.path, n.created_path, n.created_rev def get_path_url(self, path, rev): """Retrieve the "native" URL from which this repository is reachable from Subversion clients. """ url = self.params.get('url', '').rstrip('/') if url: if not path or path == '/': return url return url + '/' + path.lstrip('/') def get_changeset(self, rev): """Produce a `SubversionChangeset` from given revision specification""" rev = self.normalize_rev(rev) return SubversionChangeset(self, rev, self.scope, self.pool) def get_changeset_uid(self, rev): """Build a value identifying the `rev` in this repository.""" return (self.uuid, rev) def get_node(self, path, rev=None): """Produce a `SubversionNode` from given path and optionally revision specifications. No revision given means use the latest. """ path = path or '' if path and path[-1] == '/': path = path[:-1] rev = self.normalize_rev(rev) or self.youngest_rev return SubversionNode(path, rev, self, self.pool) def _get_node_revs(self, path, last=None, first=None): """Return the revisions affecting `path` between `first` and `last` revs. If `first` is not given, it goes down to the revision in which the branch was created. """ node = self.get_node(path, last) revs = [] for (p, r, chg) in node.get_history(): if p != path or (first and r < first): break revs.append(r) return revs def _history(self, path, start, end, pool): """`path` is a unicode path in the scope. Generator yielding `(path, rev)` pairs, where `path` is an `unicode` object. Must start with `(path, created rev)`. (wraps ``fs.node_history``) """ path_utf8 = _to_svn(pool(), self.scope, path) if start < end: start, end = end, start if (start, end) == (1, 0): # only happens for empty repos return root = fs.revision_root(self.fs_ptr, start, pool()) # fs.node_history leaks when path doesn't exist (#6588) if fs.check_path(root, path_utf8, pool()) == core.svn_node_none: return tmp1 = Pool(pool) tmp2 = Pool(pool) history_ptr = fs.node_history(root, path_utf8, tmp1()) cross_copies = 1 while history_ptr: history_ptr = fs.history_prev(history_ptr, cross_copies, tmp2()) tmp1.clear() tmp1, tmp2 = tmp2, tmp1 if history_ptr: path_utf8, rev = fs.history_location(history_ptr, tmp2()) tmp2.clear() if rev < end: break path = _from_svn(path_utf8) yield path, rev del tmp1 del tmp2 def _previous_rev(self, rev, path='', pool=None): if rev > 1: # don't use oldest here, as it's too expensive for _, prev in self._history(path, 1, rev-1, pool or self.pool): return prev return None def get_oldest_rev(self): """Gives an approximation of the oldest revision.""" if self.oldest is None: self.oldest = 1 # trying to figure out the oldest rev for scoped repository # is too expensive and uncovers a big memory leak (#5213) # if self.scope != '/': # self.oldest = self.next_rev(0, find_initial_rev=True) return self.oldest def get_youngest_rev(self): """Retrieve the latest revision in the repository. (wraps ``fs.youngest_rev``) """ if not self.youngest: self.youngest = fs.youngest_rev(self.fs_ptr, self.pool()) if self.scope != '/': for path, rev in self._history('', 1, self.youngest, self.pool): self.youngest = rev break return self.youngest def previous_rev(self, rev, path=''): """Return revision immediately preceeding `rev`, eventually below given `path` or globally. """ # FIXME optimize for non-scoped rev = self.normalize_rev(rev) return self._previous_rev(rev, path) def next_rev(self, rev, path='', find_initial_rev=False): """Return revision immediately following `rev`, eventually below given `path` or globally. """ rev = self.normalize_rev(rev) next = rev + 1 youngest = self.youngest_rev subpool = Pool(self.pool) while next <= youngest: subpool.clear() for _, next in self._history(path, rev+1, next, subpool): return next else: if not find_initial_rev and \ not self.has_node(path, next, subpool): return next # a 'delete' event is also interesting... next += 1 return None def rev_older_than(self, rev1, rev2): """Check relative order between two revision specifications.""" return self.normalize_rev(rev1) < self.normalize_rev(rev2) def get_path_history(self, path, rev=None, limit=None): """Retrieve creation and deletion events that happened on given `path`. """ path = self.normalize_path(path) rev = self.normalize_rev(rev) expect_deletion = False subpool = Pool(self.pool) numrevs = 0 while rev and (not limit or numrevs < limit): subpool.clear() if self.has_node(path, rev, subpool): if expect_deletion: # it was missing, now it's there again: # rev+1 must be a delete numrevs += 1 yield path, rev+1, Changeset.DELETE newer = None # 'newer' is the previously seen history tuple older = None # 'older' is the currently examined history tuple for p, r in self._history(path, 1, rev, subpool): older = (_path_within_scope(self.scope, p), r, Changeset.ADD) rev = self._previous_rev(r, pool=subpool) if newer: numrevs += 1 if older[0] == path: # still on the path: 'newer' was an edit yield newer[0], newer[1], Changeset.EDIT else: # the path changed: 'newer' was a copy rev = self._previous_rev(newer[1], pool=subpool) # restart before the copy op yield newer[0], newer[1], Changeset.COPY older = (older[0], older[1], 'unknown') break newer = older if older: # either a real ADD or the source of a COPY numrevs += 1 yield older else: expect_deletion = True rev = self._previous_rev(rev, pool=subpool) def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=0): """Determine differences between two arbitrary pairs of paths and revisions. (wraps ``repos.svn_repos_dir_delta``) """ def key(value): return value[1].path if value[1] is not None else value[0].path return iter(sorted(self._get_changes(old_path, old_rev, new_path, new_rev, ignore_ancestry), key=key)) def _get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry): old_node = new_node = None old_rev = self.normalize_rev(old_rev) new_rev = self.normalize_rev(new_rev) if self.has_node(old_path, old_rev): old_node = self.get_node(old_path, old_rev) else: raise NoSuchNode(old_path, old_rev, 'The Base for Diff is invalid') if self.has_node(new_path, new_rev): new_node = self.get_node(new_path, new_rev) else: raise NoSuchNode(new_path, new_rev, 'The Target for Diff is invalid') if new_node.kind != old_node.kind: raise TracError(_('Diff mismatch: Base is a %(oldnode)s ' '(%(oldpath)s in revision %(oldrev)s) and ' 'Target is a %(newnode)s (%(newpath)s in ' 'revision %(newrev)s).', oldnode=old_node.kind, oldpath=old_path, oldrev=old_rev, newnode=new_node.kind, newpath=new_path, newrev=new_rev)) subpool = Pool(self.pool) if new_node.isdir: editor = DiffChangeEditor() e_ptr, e_baton = delta.make_editor(editor, subpool()) old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) def authz_cb(root, path, pool): return 1 text_deltas = 0 # as this is anyway re-done in Diff.py... entry_props = 0 # "... typically used only for working copy updates" repos.svn_repos_dir_delta(old_root, _to_svn(subpool(), self.scope, old_path), '', new_root, _to_svn(subpool(), self.scope, new_path), e_ptr, e_baton, authz_cb, text_deltas, 1, # directory entry_props, ignore_ancestry, subpool()) for path, kind, change in editor.deltas: path = _from_svn(path) old_node = new_node = None if change != Changeset.ADD: old_node = self.get_node(posixpath.join(old_path, path), old_rev) if change != Changeset.DELETE: new_node = self.get_node(posixpath.join(new_path, path), new_rev) else: kind = _kindmap[fs.check_path(old_root, _to_svn(subpool(), self.scope, old_node.path), subpool())] yield (old_node, new_node, kind, change) else: old_root = fs.revision_root(self.fs_ptr, old_rev, subpool()) new_root = fs.revision_root(self.fs_ptr, new_rev, subpool()) if fs.contents_changed(old_root, _to_svn(subpool(), self.scope, old_path), new_root, _to_svn(subpool(), self.scope, new_path), subpool()): yield (old_node, new_node, Node.FILE, Changeset.EDIT) class SubversionNode(Node): def __init__(self, path, rev, repos, pool=None, parent_root=None): self.fs_ptr = repos.fs_ptr self.scope = repos.scope self.pool = Pool(pool) pool = self.pool() self._scoped_path_utf8 = _to_svn(pool, self.scope, path) if parent_root: self.root = parent_root else: self.root = fs.revision_root(self.fs_ptr, rev, pool) node_type = fs.check_path(self.root, self._scoped_path_utf8, pool) if not node_type in _kindmap: raise NoSuchNode(path, rev) cp_utf8 = fs.node_created_path(self.root, self._scoped_path_utf8, pool) cp = _from_svn(cp_utf8) cr = fs.node_created_rev(self.root, self._scoped_path_utf8, pool) # Note: `cp` differs from `path` if the last change was a copy, # In that case, `path` doesn't even exist at `cr`. # The only guarantees are: # * this node exists at (path,rev) # * the node existed at (created_path,created_rev) # Also, `cp` might well be out of the scope of the repository, # in this case, we _don't_ use the ''create'' information. if _is_path_within_scope(self.scope, cp): self.created_rev = cr self.created_path = _path_within_scope(self.scope, cp) else: self.created_rev, self.created_path = rev, path # TODO: check node id Node.__init__(self, repos, path, rev, _kindmap[node_type]) def get_content(self): """Retrieve raw content as a "read()"able object.""" if self.isdir: return None pool = Pool(self.pool) s = core.Stream(fs.file_contents(self.root, self._scoped_path_utf8, pool())) # The stream object needs to reference the pool to make sure the pool # is not destroyed before the former. s._pool = pool return s def get_entries(self): """Yield `SubversionNode` corresponding to entries in this directory. (wraps ``fs.dir_entries``) """ if self.isfile: return pool = Pool(self.pool) entries = fs.dir_entries(self.root, self._scoped_path_utf8, pool()) for item in entries.keys(): path = posixpath.join(self.path, _from_svn(item)) yield SubversionNode(path, self.rev, self.repos, self.pool, self.root) def get_history(self, limit=None): """Yield change events that happened on this path""" newer = None # 'newer' is the previously seen history tuple older = None # 'older' is the currently examined history tuple pool = Pool(self.pool) numrevs = 0 for path, rev in self.repos._history(self.path, 1, self.rev, pool): path = _path_within_scope(self.scope, path) if rev > 0 and path: older = (path, rev, Changeset.ADD) if newer: if newer[0] == older[0]: # stay on same path change = Changeset.EDIT else: change = Changeset.COPY newer = (newer[0], newer[1], change) numrevs += 1 yield newer newer = older if limit and numrevs >= limit: break if newer and (not limit or numrevs < limit): yield newer def get_annotations(self): """Return a list the last changed revision for each line. (wraps ``client.blame2``) """ annotations = [] if self.isfile: def blame_receiver(line_no, revision, author, date, line, pool): annotations.append(revision) try: rev = _svn_rev(self.rev) start = _svn_rev(0) file_url_utf8 = posixpath.join(self.repos.ra_url_utf8, self._scoped_path_utf8) self.repos.log.info('opening ra_local session to %r', file_url_utf8) from svn import client client.blame2(file_url_utf8, rev, start, rev, blame_receiver, client.create_context(), self.pool()) except (core.SubversionException, AttributeError), e: # svn thinks file is a binary or blame not supported raise TracError(_('svn blame failed on %(path)s: %(error)s', path=self.path, error=to_unicode(e))) return annotations # def get_previous(self): # # FIXME: redo it with fs.node_history def get_properties(self): """Return `dict` of node properties at current revision. (wraps ``fs.node_proplist``) """ props = fs.node_proplist(self.root, self._scoped_path_utf8, self.pool()) for name, value in props.items(): # Note that property values can be arbitrary binary values # so we can't assume they are UTF-8 strings... props[_from_svn(name)] = to_unicode(value) return props def get_content_length(self): """Retrieve byte size of a file. Return `None` for a folder. (wraps ``fs.file_length``) """ if self.isdir: return None return fs.file_length(self.root, self._scoped_path_utf8, self.pool()) def get_content_type(self): """Retrieve mime-type property of a file. Return `None` for a folder. (wraps ``fs.revision_prop``) """ if self.isdir: return None return self._get_prop(core.SVN_PROP_MIME_TYPE) def get_last_modified(self): """Retrieve timestamp of last modification, in micro-seconds. (wraps ``fs.revision_prop``) """ _date = fs.revision_prop(self.fs_ptr, self.created_rev, core.SVN_PROP_REVISION_DATE, self.pool()) if not _date: return None return from_utimestamp(core.svn_time_from_cstring(_date, self.pool())) def _get_prop(self, name): return fs.node_prop(self.root, self._scoped_path_utf8, name, self.pool()) def get_branch_origin(self): """Return the revision in which the node's path was created. (wraps ``fs.revision_root_revision(fs.closest_copy)``) """ root_and_path = fs.closest_copy(self.root, self._scoped_path_utf8) if root_and_path: return fs.revision_root_revision(root_and_path[0]) def get_copy_ancestry(self): """Retrieve the list of `(path,rev)` copy ancestors of this node. Most recent ancestor first. Each ancestor `(path, rev)` corresponds to the path and revision of the source at the time the copy or move operation was performed. """ ancestors = [] previous = (self._scoped_path_utf8, self.rev, self.root) while previous: (previous_path, previous_rev, previous_root) = previous previous = None root_path = fs.closest_copy(previous_root, previous_path) if root_path: (root, path) = root_path path = path.lstrip('/') rev = fs.revision_root_revision(root) relpath = None if path != previous_path: # `previous_path` is a subfolder of `path` and didn't # change since `path` was copied relpath = previous_path[len(path):].strip('/') copied_from = fs.copied_from(root, path) if copied_from: (rev, path) = copied_from path = path.lstrip('/') root = fs.revision_root(self.fs_ptr, rev, self.pool()) if relpath: path += '/' + relpath ui_path = _path_within_scope(self.scope, _from_svn(path)) if ui_path: ancestors.append((ui_path, rev)) previous = (path, rev, root) return ancestors class SubversionChangeset(Changeset): def __init__(self, repos, rev, scope, pool=None): self.rev = rev self.scope = scope self.fs_ptr = repos.fs_ptr self.pool = Pool(pool) try: message = self._get_prop(core.SVN_PROP_REVISION_LOG) except core.SubversionException: raise NoSuchChangeset(rev) author = self._get_prop(core.SVN_PROP_REVISION_AUTHOR) # we _hope_ it's UTF-8, but can't be 100% sure (#4321) message = message and to_unicode(message, 'utf-8') author = author and to_unicode(author, 'utf-8') _date = self._get_prop(core.SVN_PROP_REVISION_DATE) if _date: ts = core.svn_time_from_cstring(_date, self.pool()) date = from_utimestamp(ts) else: date = None Changeset.__init__(self, repos, rev, message, author, date) def get_properties(self): """Retrieve `dict` of Subversion properties for this revision (revprops) """ props = fs.revision_proplist(self.fs_ptr, self.rev, self.pool()) properties = {} for k, v in props.iteritems(): if k not in (core.SVN_PROP_REVISION_LOG, core.SVN_PROP_REVISION_AUTHOR, core.SVN_PROP_REVISION_DATE): properties[k] = to_unicode(v) # Note: the above `to_unicode` has a small probability # to mess-up binary properties, like icons. return properties def get_changes(self): """Retrieve file changes for a given revision. (wraps ``repos.svn_repos_replay``) """ pool = Pool(self.pool) tmp = Pool(pool) root = fs.revision_root(self.fs_ptr, self.rev, pool()) editor = repos.RevisionChangeCollector(self.fs_ptr, self.rev, pool()) e_ptr, e_baton = delta.make_editor(editor, pool()) repos.svn_repos_replay(root, e_ptr, e_baton, pool()) idx = 0 copies, deletions = {}, {} changes = [] revroots = {} for path_utf8, change in editor.changes.items(): new_path = _from_svn(path_utf8) # Filtering on `path` if not _is_path_within_scope(self.scope, new_path): continue path_utf8 = change.path base_path_utf8 = change.base_path path = _from_svn(path_utf8) base_path = _from_svn(base_path_utf8) base_rev = change.base_rev change_action = getattr(change, 'action', None) # Ensure `base_path` is within the scope if not _is_path_within_scope(self.scope, base_path): base_path, base_rev = None, -1 # Determine the action if not path and not new_path and self.scope == '/': action = Changeset.EDIT # root property change elif not path or (change_action is not None and change_action == repos.CHANGE_ACTION_DELETE): if new_path: # deletion action = Changeset.DELETE deletions[new_path.lstrip('/')] = idx else: # deletion outside of scope, ignore continue elif change.added or not base_path: # add or copy action = Changeset.ADD if base_path and base_rev: action = Changeset.COPY copies[base_path.lstrip('/')] = idx else: action = Changeset.EDIT # identify the most interesting base_path/base_rev # in terms of last changed information (see r2562) if revroots.has_key(base_rev): b_root = revroots[base_rev] else: b_root = fs.revision_root(self.fs_ptr, base_rev, pool()) revroots[base_rev] = b_root tmp.clear() cbase_path_utf8 = fs.node_created_path(b_root, base_path_utf8, tmp()) cbase_path = _from_svn(cbase_path_utf8) cbase_rev = fs.node_created_rev(b_root, base_path_utf8, tmp()) # give up if the created path is outside the scope if _is_path_within_scope(self.scope, cbase_path): base_path, base_rev = cbase_path, cbase_rev kind = _kindmap[change.item_kind] path = _path_within_scope(self.scope, new_path or base_path) base_path = _path_within_scope(self.scope, base_path) changes.append([path, kind, action, base_path, base_rev]) idx += 1 moves = [] # a MOVE is a COPY whose `base_path` corresponds to a `new_path` # which has been deleted for k, v in copies.items(): if k in deletions: changes[v][2] = Changeset.MOVE moves.append(deletions[k]) offset = 0 moves.sort() for i in moves: del changes[i - offset] offset += 1 changes.sort() for change in changes: yield tuple(change) def _get_prop(self, name): return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool()) # # Delta editor for diffs between arbitrary nodes # # Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used # because 'repos.svn_repos_dir_delta' *doesn't* provide it. # # Note 2: the 'dir_baton' is the path of the parent directory # def DiffChangeEditor(): class DiffChangeEditor(delta.Editor): def __init__(self): self.deltas = [] # -- svn.delta.Editor callbacks def open_root(self, base_revision, dir_pool): return ('/', Changeset.EDIT) def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev, dir_pool): self.deltas.append((path, Node.DIRECTORY, Changeset.ADD)) return (path, Changeset.ADD) def open_directory(self, path, dir_baton, base_revision, dir_pool): return (path, dir_baton[1]) def change_dir_prop(self, dir_baton, name, value, pool): path, change = dir_baton if change != Changeset.ADD: self.deltas.append((path, Node.DIRECTORY, change)) def delete_entry(self, path, revision, dir_baton, pool): self.deltas.append((path, None, Changeset.DELETE)) def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision, dir_pool): self.deltas.append((path, Node.FILE, Changeset.ADD)) def open_file(self, path, dir_baton, dummy_rev, file_pool): self.deltas.append((path, Node.FILE, Changeset.EDIT)) return DiffChangeEditor() Trac-1.0.1/tracopt/versioncontrol/svn/tests/0000755000175200017520000000000012102610436020107 5ustar cbooscboosTrac-1.0.1/tracopt/versioncontrol/svn/tests/svn_fs.py0000644000175200017520000011720012102610255021757 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C)2005-2009 Edgewall Software # Copyright (C) 2005 Christopher Lenz # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Christopher Lenz from datetime import datetime import new import os.path import stat import shutil import tempfile import unittest from StringIO import StringIO try: from svn import core, repos has_svn = True except ImportError: has_svn = False from trac.test import EnvironmentStub, TestSetup from trac.core import TracError from trac.resource import Resource, resource_exists from trac.util.concurrency import get_thread_id from trac.util.datefmt import utc from trac.versioncontrol import DbRepositoryProvider, Changeset, Node, \ NoSuchChangeset from tracopt.versioncontrol.svn import svn_fs REPOS_PATH = os.path.join(tempfile.gettempdir(), 'trac-svnrepos') REPOS_NAME = 'repo' HEAD = 22 TETE = 21 class SubversionRepositoryTestSetup(TestSetup): def setUp(self): dumpfile = open(os.path.join(os.path.split(__file__)[0], 'svnrepos.dump')) svn_fs._import_svn() core.apr_initialize() pool = core.svn_pool_create(None) dumpstream = None try: if os.path.exists(REPOS_PATH): print 'trouble ahead with db/rep-cache.db... see #8278' r = repos.svn_repos_create(REPOS_PATH, '', '', None, None, pool) if hasattr(repos, 'svn_repos_load_fs2'): repos.svn_repos_load_fs2(r, dumpfile, StringIO(), repos.svn_repos_load_uuid_default, '', 0, 0, None, pool) else: dumpstream = core.svn_stream_from_aprfile(dumpfile, pool) repos.svn_repos_load_fs(r, dumpstream, None, repos.svn_repos_load_uuid_default, '', None, None, pool) finally: if dumpstream: core.svn_stream_close(dumpstream) core.svn_pool_destroy(pool) core.apr_terminate() def tearDown(self): repos.svn_repos_delete(REPOS_PATH) # -- Re-usable test mixins class NormalTests(object): def test_resource_exists(self): repos = Resource('repository', REPOS_NAME) self.assertEqual(True, resource_exists(self.env, repos)) self.assertEqual(False, resource_exists(self.env, repos(id='xxx'))) node = repos.child('source', u'tête') self.assertEqual(True, resource_exists(self.env, node)) self.assertEqual(False, resource_exists(self.env, node(id='xxx'))) cset = repos.child('changeset', HEAD) self.assertEqual(True, resource_exists(self.env, cset)) self.assertEqual(False, resource_exists(self.env, cset(id=123456))) def test_repos_normalize_path(self): self.assertEqual('/', self.repos.normalize_path('/')) self.assertEqual('/', self.repos.normalize_path('')) self.assertEqual('/', self.repos.normalize_path(None)) self.assertEqual(u'tête', self.repos.normalize_path(u'tête')) self.assertEqual(u'tête', self.repos.normalize_path(u'/tête')) self.assertEqual(u'tête', self.repos.normalize_path(u'tête/')) self.assertEqual(u'tête', self.repos.normalize_path(u'/tête/')) def test_repos_normalize_rev(self): self.assertEqual(HEAD, self.repos.normalize_rev('latest')) self.assertEqual(HEAD, self.repos.normalize_rev('head')) self.assertEqual(HEAD, self.repos.normalize_rev('')) self.assertRaises(NoSuchChangeset, self.repos.normalize_rev, 'something else') self.assertEqual(HEAD, self.repos.normalize_rev(None)) self.assertEqual(11, self.repos.normalize_rev('11')) self.assertEqual(11, self.repos.normalize_rev(11)) def test_rev_navigation(self): self.assertEqual(1, self.repos.oldest_rev) self.assertEqual(None, self.repos.previous_rev(0)) self.assertEqual(None, self.repos.previous_rev(1)) self.assertEqual(HEAD, self.repos.youngest_rev) self.assertEqual(6, self.repos.next_rev(5)) self.assertEqual(7, self.repos.next_rev(6)) # ... self.assertEqual(None, self.repos.next_rev(HEAD)) self.assertRaises(NoSuchChangeset, self.repos.normalize_rev, HEAD + 1) def test_rev_path_navigation(self): self.assertEqual(1, self.repos.oldest_rev) self.assertEqual(None, self.repos.previous_rev(0, u'tête')) self.assertEqual(None, self.repos.previous_rev(1, u'tête')) self.assertEqual(HEAD, self.repos.youngest_rev) self.assertEqual(6, self.repos.next_rev(5, u'tête')) self.assertEqual(13, self.repos.next_rev(6, u'tête')) # ... self.assertEqual(None, self.repos.next_rev(HEAD, u'tête')) # test accentuated characters self.assertEqual(None, self.repos.previous_rev(17, u'tête/R\xe9sum\xe9.txt')) self.assertEqual(17, self.repos.next_rev(16, u'tête/R\xe9sum\xe9.txt')) def test_has_node(self): self.assertEqual(False, self.repos.has_node(u'/tête/dir1', 3)) self.assertEqual(True, self.repos.has_node(u'/tête/dir1', 4)) self.assertEqual(True, self.repos.has_node(u'/tête/dir1')) def test_get_node(self): node = self.repos.get_node(u'/tête') self.assertEqual(u'tête', node.name) self.assertEqual(u'/tête', node.path) self.assertEqual(Node.DIRECTORY, node.kind) self.assertEqual(HEAD, node.rev) self.assertEqual(TETE, node.created_rev) self.assertEqual(datetime(2007, 4, 30, 17, 45, 26, 234375, utc), node.last_modified) node = self.repos.get_node(u'/tête/README.txt') self.assertEqual('README.txt', node.name) self.assertEqual(u'/tête/README.txt', node.path) self.assertEqual(Node.FILE, node.kind) self.assertEqual(HEAD, node.rev) self.assertEqual(3, node.created_rev) self.assertEqual(datetime(2005, 4, 1, 13, 24, 58, 234643, utc), node.last_modified) def test_get_node_specific_rev(self): node = self.repos.get_node(u'/tête', 1) self.assertEqual(u'tête', node.name) self.assertEqual(u'/tête', node.path) self.assertEqual(Node.DIRECTORY, node.kind) self.assertEqual(1, node.rev) self.assertEqual(datetime(2005, 4, 1, 10, 0, 52, 353248, utc), node.last_modified) node = self.repos.get_node(u'/tête/README.txt', 2) self.assertEqual('README.txt', node.name) self.assertEqual(u'/tête/README.txt', node.path) self.assertEqual(Node.FILE, node.kind) self.assertEqual(2, node.rev) self.assertEqual(datetime(2005, 4, 1, 13, 12, 18, 216267, utc), node.last_modified) def test_get_dir_entries(self): node = self.repos.get_node(u'/tête') entries = node.get_entries() self.assertEqual('dir1', entries.next().name) self.assertEqual('mpp_proc', entries.next().name) self.assertEqual('v2', entries.next().name) self.assertEqual('README3.txt', entries.next().name) self.assertEqual(u'R\xe9sum\xe9.txt', entries.next().name) self.assertEqual('README.txt', entries.next().name) self.assertRaises(StopIteration, entries.next) def test_get_file_entries(self): node = self.repos.get_node(u'/tête/README.txt') entries = node.get_entries() self.assertRaises(StopIteration, entries.next) def test_get_dir_content(self): node = self.repos.get_node(u'/tête') self.assertEqual(None, node.content_length) self.assertEqual(None, node.content_type) self.assertEqual(None, node.get_content()) def test_get_file_content(self): node = self.repos.get_node(u'/tête/README.txt') self.assertEqual(8, node.content_length) self.assertEqual('text/plain', node.content_type) self.assertEqual('A test.\n', node.get_content().read()) def test_get_dir_properties(self): f = self.repos.get_node(u'/tête') props = f.get_properties() self.assertEqual(1, len(props)) def test_get_file_properties(self): f = self.repos.get_node(u'/tête/README.txt') props = f.get_properties() self.assertEqual('native', props['svn:eol-style']) self.assertEqual('text/plain', props['svn:mime-type']) def test_created_path_rev(self): node = self.repos.get_node(u'/tête/README3.txt', 15) self.assertEqual(15, node.rev) self.assertEqual(u'/tête/README3.txt', node.path) self.assertEqual(14, node.created_rev) self.assertEqual(u'tête/README3.txt', node.created_path) def test_created_path_rev_parent_copy(self): node = self.repos.get_node('/tags/v1/README.txt', 15) self.assertEqual(15, node.rev) self.assertEqual('/tags/v1/README.txt', node.path) self.assertEqual(3, node.created_rev) self.assertEqual(u'tête/README.txt', node.created_path) # Revision Log / node history def test_get_node_history(self): node = self.repos.get_node(u'/tête/README3.txt') history = node.get_history() self.assertEqual((u'tête/README3.txt', 14, 'copy'), history.next()) self.assertEqual((u'tête/README2.txt', 6, 'copy'), history.next()) self.assertEqual((u'tête/README.txt', 3, 'edit'), history.next()) self.assertEqual((u'tête/README.txt', 2, 'add'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_node_history_limit(self): node = self.repos.get_node(u'/tête/README3.txt') history = node.get_history(2) self.assertEqual((u'tête/README3.txt', 14, 'copy'), history.next()) self.assertEqual((u'tête/README2.txt', 6, 'copy'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_node_history_follow_copy(self): node = self.repos.get_node('/tags/v1/README.txt') history = node.get_history() self.assertEqual(('tags/v1/README.txt', 7, 'copy'), history.next()) self.assertEqual((u'tête/README.txt', 3, 'edit'), history.next()) self.assertEqual((u'tête/README.txt', 2, 'add'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_copy_ancestry(self): node = self.repos.get_node('/tags/v1/README.txt') ancestry = node.get_copy_ancestry() self.assertEqual([(u'tête/README.txt', 6)], ancestry) for path, rev in ancestry: self.repos.get_node(path, rev) # shouldn't raise NoSuchNode node = self.repos.get_node(u'/tête/README3.txt') ancestry = node.get_copy_ancestry() self.assertEqual([(u'tête/README2.txt', 13), (u'tête/README.txt', 3)], ancestry) for path, rev in ancestry: self.repos.get_node(path, rev) # shouldn't raise NoSuchNode node = self.repos.get_node('/branches/v1x') ancestry = node.get_copy_ancestry() self.assertEqual([(u'tags/v1.1', 11), (u'branches/v1x', 9), (u'tags/v1', 7), (u'tête', 6)], ancestry) for path, rev in ancestry: self.repos.get_node(path, rev) # shouldn't raise NoSuchNode def test_get_copy_ancestry_for_move(self): node = self.repos.get_node(u'/tête/dir1/dir2', 5) ancestry = node.get_copy_ancestry() self.assertEqual([(u'tête/dir2', 4)], ancestry) for path, rev in ancestry: self.repos.get_node(path, rev) # shouldn't raise NoSuchNode def test_get_branch_origin(self): node = self.repos.get_node('/tags/v1/README.txt') self.assertEqual(7, node.get_branch_origin()) node = self.repos.get_node(u'/tête/README3.txt') self.assertEqual(14, node.get_branch_origin()) node = self.repos.get_node('/branches/v1x') self.assertEqual(12, node.get_branch_origin()) node = self.repos.get_node(u'/tête/dir1/dir2', 5) self.assertEqual(5, node.get_branch_origin()) # Revision Log / path history def test_get_path_history(self): history = self.repos.get_path_history(u'/tête/README2.txt', None) self.assertEqual((u'tête/README2.txt', 14, 'delete'), history.next()) self.assertEqual((u'tête/README2.txt', 6, 'copy'), history.next()) self.assertEqual((u'tête/README.txt', 3, 'unknown'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_path_history_copied_file(self): history = self.repos.get_path_history('/tags/v1/README.txt', None) self.assertEqual(('tags/v1/README.txt', 7, 'copy'), history.next()) self.assertEqual((u'tête/README.txt', 3, 'unknown'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_path_history_copied_dir(self): history = self.repos.get_path_history('/branches/v1x', None) self.assertEqual(('branches/v1x', 12, 'copy'), history.next()) self.assertEqual(('tags/v1.1', 10, 'unknown'), history.next()) self.assertEqual(('branches/v1x', 11, 'delete'), history.next()) self.assertEqual(('branches/v1x', 9, 'edit'), history.next()) self.assertEqual(('branches/v1x', 8, 'copy'), history.next()) self.assertEqual(('tags/v1', 7, 'unknown'), history.next()) self.assertRaises(StopIteration, history.next) # Diffs def _cmp_diff(self, expected, got): if expected[0]: old = self.repos.get_node(*expected[0]) self.assertEqual((old.path, old.rev), (got[0].path, got[0].rev)) if expected[1]: new = self.repos.get_node(*expected[1]) self.assertEqual((new.path, new.rev), (got[1].path, got[1].rev)) self.assertEqual(expected[2], (got[2], got[3])) def test_diff_file_different_revs(self): diffs = self.repos.get_changes(u'tête/README.txt', 2, u'tête/README.txt', 3) self._cmp_diff(((u'tête/README.txt', 2), (u'tête/README.txt', 3), (Node.FILE, Changeset.EDIT)), diffs.next()) self.assertRaises(StopIteration, diffs.next) def test_diff_file_different_files(self): diffs = self.repos.get_changes('branches/v1x/README.txt', 12, 'branches/v1x/README2.txt', 12) self._cmp_diff((('branches/v1x/README.txt', 12), ('branches/v1x/README2.txt', 12), (Node.FILE, Changeset.EDIT)), diffs.next()) self.assertRaises(StopIteration, diffs.next) def test_diff_file_no_change(self): diffs = self.repos.get_changes(u'tête/README.txt', 7, 'tags/v1/README.txt', 7) self.assertRaises(StopIteration, diffs.next) def test_diff_dir_different_revs(self): diffs = self.repos.get_changes(u'tête', 4, u'tête', 8) self._cmp_diff((None, (u'tête/README2.txt', 8), (Node.FILE, Changeset.ADD)), diffs.next()) self._cmp_diff((None, (u'tête/dir1/dir2', 8), (Node.DIRECTORY, Changeset.ADD)), diffs.next()) self._cmp_diff((None, (u'tête/dir1/dir3', 8), (Node.DIRECTORY, Changeset.ADD)), diffs.next()) self._cmp_diff(((u'tête/dir2', 4), None, (Node.DIRECTORY, Changeset.DELETE)), diffs.next()) self._cmp_diff(((u'tête/dir3', 4), None, (Node.DIRECTORY, Changeset.DELETE)), diffs.next()) self.assertRaises(StopIteration, diffs.next) def test_diff_dir_different_dirs(self): diffs = self.repos.get_changes(u'tête', 1, 'branches/v1x', 12) self._cmp_diff((None, ('branches/v1x/README.txt', 12), (Node.FILE, Changeset.ADD)), diffs.next()) self._cmp_diff((None, ('branches/v1x/README2.txt', 12), (Node.FILE, Changeset.ADD)), diffs.next()) self._cmp_diff((None, ('branches/v1x/dir1', 12), (Node.DIRECTORY, Changeset.ADD)), diffs.next()) self._cmp_diff((None, ('branches/v1x/dir1/dir2', 12), (Node.DIRECTORY, Changeset.ADD)), diffs.next()) self._cmp_diff((None, ('branches/v1x/dir1/dir3', 12), (Node.DIRECTORY, Changeset.ADD)), diffs.next()) self.assertRaises(StopIteration, diffs.next) def test_diff_dir_no_change(self): diffs = self.repos.get_changes(u'tête', 7, 'tags/v1', 7) self.assertRaises(StopIteration, diffs.next) # Changesets def test_changeset_repos_creation(self): chgset = self.repos.get_changeset(0) self.assertEqual(0, chgset.rev) self.assertEqual('', chgset.message) self.assertEqual('', chgset.author) self.assertEqual(datetime(2005, 4, 1, 9, 57, 41, 312767, utc), chgset.date) self.assertRaises(StopIteration, chgset.get_changes().next) def test_changeset_added_dirs(self): chgset = self.repos.get_changeset(1) self.assertEqual(1, chgset.rev) self.assertEqual('Initial directory layout.', chgset.message) self.assertEqual('john', chgset.author) self.assertEqual(datetime(2005, 4, 1, 10, 0, 52, 353248, utc), chgset.date) changes = chgset.get_changes() self.assertEqual(('branches', Node.DIRECTORY, Changeset.ADD, None, -1), changes.next()) self.assertEqual(('tags', Node.DIRECTORY, Changeset.ADD, None, -1), changes.next()) self.assertEqual((u'tête', Node.DIRECTORY, Changeset.ADD, None, -1), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_file_edit(self): chgset = self.repos.get_changeset(3) self.assertEqual(3, chgset.rev) self.assertEqual('Fixed README.\n', chgset.message) self.assertEqual('kate', chgset.author) self.assertEqual(datetime(2005, 4, 1, 13, 24, 58, 234643, utc), chgset.date) changes = chgset.get_changes() self.assertEqual((u'tête/README.txt', Node.FILE, Changeset.EDIT, u'tête/README.txt', 2), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_dir_moves(self): chgset = self.repos.get_changeset(5) self.assertEqual(5, chgset.rev) self.assertEqual('Moved directories.', chgset.message) self.assertEqual('kate', chgset.author) self.assertEqual(datetime(2005, 4, 1, 16, 25, 39, 658099, utc), chgset.date) changes = chgset.get_changes() self.assertEqual((u'tête/dir1/dir2', Node.DIRECTORY, Changeset.MOVE, u'tête/dir2', 4), changes.next()) self.assertEqual((u'tête/dir1/dir3', Node.DIRECTORY, Changeset.MOVE, u'tête/dir3', 4), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_file_copy(self): chgset = self.repos.get_changeset(6) self.assertEqual(6, chgset.rev) self.assertEqual('More things to read', chgset.message) self.assertEqual('john', chgset.author) self.assertEqual(datetime(2005, 4, 1, 18, 56, 46, 985846, utc), chgset.date) changes = chgset.get_changes() self.assertEqual((u'tête/README2.txt', Node.FILE, Changeset.COPY, u'tête/README.txt', 3), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_root_propset(self): chgset = self.repos.get_changeset(13) self.assertEqual(13, chgset.rev) self.assertEqual('Setting property on the repository_dir root', chgset.message) changes = chgset.get_changes() self.assertEqual(('/', Node.DIRECTORY, Changeset.EDIT, '/', 12), changes.next()) self.assertEqual((u'tête', Node.DIRECTORY, Changeset.EDIT, u'tête', 6), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_base_path_rev(self): chgset = self.repos.get_changeset(9) self.assertEqual(9, chgset.rev) changes = chgset.get_changes() self.assertEqual(('branches/v1x/README.txt', Node.FILE, Changeset.EDIT, u'tête/README.txt', 3), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_rename_and_edit(self): chgset = self.repos.get_changeset(14) self.assertEqual(14, chgset.rev) changes = chgset.get_changes() self.assertEqual((u'tête/README3.txt', Node.FILE, Changeset.MOVE, u'tête/README2.txt', 13), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_edit_after_wc2wc_copy__original_deleted(self): chgset = self.repos.get_changeset(16) self.assertEqual(16, chgset.rev) changes = chgset.get_changes() self.assertEqual(('branches/v2', Node.DIRECTORY, Changeset.COPY, 'tags/v1.1', 14), changes.next()) self.assertEqual(('branches/v2/README2.txt', Node.FILE, Changeset.EDIT, u'tête/README2.txt', 6), changes.next()) self.assertRaises(StopIteration, changes.next) def test_fancy_rename_double_delete(self): chgset = self.repos.get_changeset(19) self.assertEqual(19, chgset.rev) changes = chgset.get_changes() self.assertEqual((u'tête/mpp_proc', Node.DIRECTORY, Changeset.MOVE, u'tête/Xprimary_proc', 18), changes.next()) self.assertEqual((u'tête/mpp_proc/Xprimary_pkg.vhd', Node.FILE, Changeset.DELETE, u'tête/Xprimary_proc/Xprimary_pkg.vhd', 18), changes.next()) self.assertEqual((u'tête/mpp_proc/Xprimary_proc', Node.DIRECTORY, Changeset.COPY, u'tête/Xprimary_proc', 18), changes.next()) self.assertEqual((u'tête/mpp_proc/Xprimary_proc/Xprimary_pkg.vhd', Node.FILE, Changeset.DELETE, u'tête/Xprimary_proc/Xprimary_pkg.vhd', 18), changes.next()) self.assertRaises(StopIteration, changes.next) def test_copy_with_deletions_below_copy(self): """Regression test for #4900.""" chgset = self.repos.get_changeset(22) self.assertEqual(22, chgset.rev) changes = chgset.get_changes() self.assertEqual((u'branches/v3', 'dir', 'copy', u'tête', 21), changes.next()) self.assertEqual((u'branches/v3/dir1', 'dir', 'delete', u'tête/dir1', 21), changes.next()) self.assertEqual((u'branches/v3/mpp_proc', 'dir', 'delete', u'tête/mpp_proc', 21), changes.next()) self.assertEqual((u'branches/v3/v2', 'dir', 'delete', u'tête/v2', 21), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_utf_8(self): chgset = self.repos.get_changeset(20) self.assertEqual(20, chgset.rev) self.assertEqual(u'Chez moi ça marche\n', chgset.message) self.assertEqual(u'Jonas Borgström', chgset.author) class ScopedTests(object): def test_repos_normalize_path(self): self.assertEqual('/', self.repos.normalize_path('/')) self.assertEqual('/', self.repos.normalize_path('')) self.assertEqual('/', self.repos.normalize_path(None)) self.assertEqual('dir1', self.repos.normalize_path('dir1')) self.assertEqual('dir1', self.repos.normalize_path('/dir1')) self.assertEqual('dir1', self.repos.normalize_path('dir1/')) self.assertEqual('dir1', self.repos.normalize_path('/dir1/')) def test_repos_normalize_rev(self): self.assertEqual(TETE, self.repos.normalize_rev('latest')) self.assertEqual(TETE, self.repos.normalize_rev('head')) self.assertEqual(TETE, self.repos.normalize_rev('')) self.assertEqual(TETE, self.repos.normalize_rev(None)) self.assertEqual(5, self.repos.normalize_rev('5')) self.assertEqual(5, self.repos.normalize_rev(5)) def test_rev_navigation(self): self.assertEqual(1, self.repos.oldest_rev) self.assertEqual(None, self.repos.previous_rev(0)) self.assertEqual(1, self.repos.previous_rev(2)) self.assertEqual(TETE, self.repos.youngest_rev) self.assertEqual(2, self.repos.next_rev(1)) self.assertEqual(3, self.repos.next_rev(2)) # ... self.assertEqual(None, self.repos.next_rev(TETE)) def test_has_node(self): self.assertEqual(False, self.repos.has_node('/dir1', 3)) self.assertEqual(True, self.repos.has_node('/dir1', 4)) def test_get_node(self): node = self.repos.get_node('/dir1') self.assertEqual('dir1', node.name) self.assertEqual('/dir1', node.path) self.assertEqual(Node.DIRECTORY, node.kind) self.assertEqual(TETE, node.rev) self.assertEqual(5, node.created_rev) self.assertEqual(datetime(2005, 4, 1, 16, 25, 39, 658099, utc), node.last_modified) node = self.repos.get_node('/README.txt') self.assertEqual('README.txt', node.name) self.assertEqual('/README.txt', node.path) self.assertEqual(Node.FILE, node.kind) self.assertEqual(TETE, node.rev) self.assertEqual(3, node.created_rev) self.assertEqual(datetime(2005, 4, 1, 13, 24, 58, 234643, utc), node.last_modified) def test_get_node_specific_rev(self): node = self.repos.get_node('/dir1', 4) self.assertEqual('dir1', node.name) self.assertEqual('/dir1', node.path) self.assertEqual(Node.DIRECTORY, node.kind) self.assertEqual(4, node.rev) self.assertEqual(datetime(2005, 4, 1, 15, 42, 35, 450595, utc), node.last_modified) node = self.repos.get_node('/README.txt', 2) self.assertEqual('README.txt', node.name) self.assertEqual('/README.txt', node.path) self.assertEqual(Node.FILE, node.kind) self.assertEqual(2, node.rev) self.assertEqual(datetime(2005, 4, 1, 13, 12, 18, 216267, utc), node.last_modified) def test_get_dir_entries(self): node = self.repos.get_node('/') entries = node.get_entries() self.assertEqual('dir1', entries.next().name) self.assertEqual('mpp_proc', entries.next().name) self.assertEqual('v2', entries.next().name) self.assertEqual('README3.txt', entries.next().name) self.assertEqual(u'R\xe9sum\xe9.txt', entries.next().name) self.assertEqual('README.txt', entries.next().name) self.assertRaises(StopIteration, entries.next) def test_get_file_entries(self): node = self.repos.get_node('/README.txt') entries = node.get_entries() self.assertRaises(StopIteration, entries.next) def test_get_dir_content(self): node = self.repos.get_node('/dir1') self.assertEqual(None, node.content_length) self.assertEqual(None, node.content_type) self.assertEqual(None, node.get_content()) def test_get_file_content(self): node = self.repos.get_node('/README.txt') self.assertEqual(8, node.content_length) self.assertEqual('text/plain', node.content_type) self.assertEqual('A test.\n', node.get_content().read()) def test_get_dir_properties(self): f = self.repos.get_node('/dir1') props = f.get_properties() self.assertEqual(0, len(props)) def test_get_file_properties(self): f = self.repos.get_node('/README.txt') props = f.get_properties() self.assertEqual('native', props['svn:eol-style']) self.assertEqual('text/plain', props['svn:mime-type']) # Revision Log / node history def test_get_history_scope(self): """Regression test for #9504""" node = self.repos.get_node('/') history = list(node.get_history()) self.assertEqual(('/', 1, 'add'), history[-1]) initial_cset = self.repos.get_changeset(history[-1][1]) self.assertEqual(1, initial_cset.rev) def test_get_node_history(self): node = self.repos.get_node('/README3.txt') history = node.get_history() self.assertEqual(('README3.txt', 14, 'copy'), history.next()) self.assertEqual(('README2.txt', 6, 'copy'), history.next()) self.assertEqual(('README.txt', 3, 'edit'), history.next()) self.assertEqual(('README.txt', 2, 'add'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_node_history_follow_copy(self): node = self.repos.get_node('dir1/dir3', ) history = node.get_history() self.assertEqual(('dir1/dir3', 5, 'copy'), history.next()) self.assertEqual(('dir3', 4, 'add'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_copy_ancestry(self): node = self.repos.get_node(u'/README3.txt') ancestry = node.get_copy_ancestry() self.assertEqual([(u'README2.txt', 13), (u'README.txt', 3)], ancestry) for path, rev in ancestry: self.repos.get_node(path, rev) # shouldn't raise NoSuchNode def test_get_copy_ancestry_for_move(self): node = self.repos.get_node(u'/dir1/dir2', 5) ancestry = node.get_copy_ancestry() self.assertEqual([(u'dir2', 4)], ancestry) for path, rev in ancestry: self.repos.get_node(path, rev) # shouldn't raise NoSuchNode def test_get_branch_origin(self): node = self.repos.get_node(u'/README3.txt') self.assertEqual(14, node.get_branch_origin()) node = self.repos.get_node(u'/dir1/dir2', 5) self.assertEqual(5, node.get_branch_origin()) # Revision Log / path history def test_get_path_history(self): history = self.repos.get_path_history('dir3', None) self.assertEqual(('dir3', 5, 'delete'), history.next()) self.assertEqual(('dir3', 4, 'add'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_path_history_copied_file(self): history = self.repos.get_path_history('README3.txt', None) self.assertEqual(('README3.txt', 14, 'copy'), history.next()) self.assertEqual(('README2.txt', 6, 'unknown'), history.next()) self.assertRaises(StopIteration, history.next) def test_get_path_history_copied_dir(self): history = self.repos.get_path_history('dir1/dir3', None) self.assertEqual(('dir1/dir3', 5, 'copy'), history.next()) self.assertEqual(('dir3', 4, 'unknown'), history.next()) self.assertRaises(StopIteration, history.next) def test_changeset_repos_creation(self): chgset = self.repos.get_changeset(0) self.assertEqual(0, chgset.rev) self.assertEqual('', chgset.message) self.assertEqual('', chgset.author) self.assertEqual(datetime(2005, 4, 1, 9, 57, 41, 312767, utc), chgset.date) self.assertRaises(StopIteration, chgset.get_changes().next) def test_changeset_added_dirs(self): chgset = self.repos.get_changeset(4) self.assertEqual(4, chgset.rev) self.assertEqual('More directories.', chgset.message) self.assertEqual('john', chgset.author) self.assertEqual(datetime(2005, 4, 1, 15, 42, 35, 450595, utc), chgset.date) changes = chgset.get_changes() self.assertEqual(('dir1', Node.DIRECTORY, 'add', None, -1), changes.next()) self.assertEqual(('dir2', Node.DIRECTORY, 'add', None, -1), changes.next()) self.assertEqual(('dir3', Node.DIRECTORY, 'add', None, -1), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_file_edit(self): chgset = self.repos.get_changeset(3) self.assertEqual(3, chgset.rev) self.assertEqual('Fixed README.\n', chgset.message) self.assertEqual('kate', chgset.author) self.assertEqual(datetime(2005, 4, 1, 13, 24, 58, 234643, utc), chgset.date) changes = chgset.get_changes() self.assertEqual(('README.txt', Node.FILE, Changeset.EDIT, 'README.txt', 2), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_dir_moves(self): chgset = self.repos.get_changeset(5) self.assertEqual(5, chgset.rev) self.assertEqual('Moved directories.', chgset.message) self.assertEqual('kate', chgset.author) self.assertEqual(datetime(2005, 4, 1, 16, 25, 39, 658099, utc), chgset.date) changes = chgset.get_changes() self.assertEqual(('dir1/dir2', Node.DIRECTORY, Changeset.MOVE, 'dir2', 4), changes.next()) self.assertEqual(('dir1/dir3', Node.DIRECTORY, Changeset.MOVE, 'dir3', 4), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_file_copy(self): chgset = self.repos.get_changeset(6) self.assertEqual(6, chgset.rev) self.assertEqual('More things to read', chgset.message) self.assertEqual('john', chgset.author) self.assertEqual(datetime(2005, 4, 1, 18, 56, 46, 985846, utc), chgset.date) changes = chgset.get_changes() self.assertEqual(('README2.txt', Node.FILE, Changeset.COPY, 'README.txt', 3), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_root_propset(self): chgset = self.repos.get_changeset(13) self.assertEqual(13, chgset.rev) self.assertEqual('Setting property on the repository_dir root', chgset.message) changes = chgset.get_changes() self.assertEqual(('/', Node.DIRECTORY, Changeset.EDIT, '/', 6), changes.next()) self.assertRaises(StopIteration, changes.next) def test_changeset_copy_from_outside_and_delete(self): chgset = self.repos.get_changeset(21) self.assertEqual(21, chgset.rev) self.assertEqual('copy from outside of the scope + delete', chgset.message) changes = chgset.get_changes() self.assertEqual(('v2', Node.DIRECTORY, Changeset.ADD, None, -1), changes.next()) self.assertEqual(('v2/README2.txt', Node.FILE, Changeset.DELETE, None, -1), changes.next()) self.assertEqual(('v2/dir1', Node.DIRECTORY, Changeset.DELETE, None, -1), changes.next()) self.assertRaises(StopIteration, changes.next) class RecentPathScopedTests(object): def test_rev_navigation(self): self.assertEqual(False, self.repos.has_node('/', 1)) self.assertEqual(False, self.repos.has_node('/', 2)) self.assertEqual(False, self.repos.has_node('/', 3)) self.assertEqual(True, self.repos.has_node('/', 4)) # We can't make this work anymore because of #5213. # self.assertEqual(4, self.repos.oldest_rev) self.assertEqual(1, self.repos.oldest_rev) # should really be 4... self.assertEqual(None, self.repos.previous_rev(4)) class NonSelfContainedScopedTests(object): def test_mixed_changeset(self): chgset = self.repos.get_changeset(7) self.assertEqual(7, chgset.rev) changes = chgset.get_changes() self.assertEqual(('/', Node.DIRECTORY, Changeset.ADD, None, -1), changes.next()) self.assertRaises(TracError, lambda: self.repos.get_node(None, 6)) class AnotherNonSelfContainedScopedTests(object): def test_mixed_changeset_with_edit(self): chgset = self.repos.get_changeset(9) self.assertEqual(9, chgset.rev) changes = chgset.get_changes() self.assertEqual(('v1x/README.txt', Node.FILE, Changeset.EDIT, 'v1x/README.txt', 8), changes.next()) # -- Test cases for SubversionRepository class SubversionRepositoryTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() repositories = self.env.config['repositories'] DbRepositoryProvider(self.env).add_repository(REPOS_NAME, self.path, 'direct-svnfs') self.repos = self.env.get_repository(REPOS_NAME) def tearDown(self): self.env.reset_db() # needed to avoid issue with 'WindowsError: The process cannot access # the file ... being used by another process: ...\rep-cache.db' self.env.shutdown(get_thread_id()) # -- Test cases for SvnCachedRepository class SvnCachedRepositoryTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() DbRepositoryProvider(self.env).add_repository(REPOS_NAME, self.path, 'svn') self.repos = self.env.get_repository(REPOS_NAME) self.repos.sync() def tearDown(self): self.env.reset_db() self.repos.close() self.repos = None def suite(): suite = unittest.TestSuite() if has_svn: tests = [(NormalTests, ''), (ScopedTests, u'/tête'), (RecentPathScopedTests, u'/tête/dir1'), (NonSelfContainedScopedTests, '/tags/v1'), (AnotherNonSelfContainedScopedTests, '/branches'), ] skipped = { 'SvnCachedRepositoryNormalTests': [ 'test_changeset_repos_creation', ], 'SvnCachedRepositoryScopedTests': [ 'test_changeset_repos_creation', 'test_rev_navigation', ], } for test, scope in tests: tc = new.classobj('SubversionRepository' + test.__name__, (SubversionRepositoryTestCase, test), {'path': REPOS_PATH + scope}) suite.addTest(unittest.makeSuite( tc, 'test', suiteClass=SubversionRepositoryTestSetup)) tc = new.classobj('SvnCachedRepository' + test.__name__, (SvnCachedRepositoryTestCase, test), {'path': REPOS_PATH + scope}) for skip in skipped.get(tc.__name__, []): setattr(tc, skip, lambda self: None) # no skip, so we cheat... suite.addTest(unittest.makeSuite( tc, 'test', suiteClass=SubversionRepositoryTestSetup)) else: print("SKIP: tracopt/versioncontrol/svn/tests/svn_fs.py (no svn " "bindings)") return suite if __name__ == '__main__': runner = unittest.TextTestRunner() runner.run(suite()) Trac-1.0.1/tracopt/versioncontrol/svn/tests/svnrepos.dump0000644000175200017520000002366112102610255022664 0ustar cbooscboosSVN-fs-dump-format-version: 2 UUID: 92ea810a-adf3-0310-b540-bef912dcf5ba Revision-number: 0 Prop-content-length: 56 Content-length: 56 K 8 svn:date V 27 2005-04-01T09:57:41.312767Z PROPS-END Revision-number: 1 Prop-content-length: 124 Content-length: 124 K 7 svn:log V 25 Initial directory layout. K 10 svn:author V 4 john K 8 svn:date V 27 2005-04-01T10:00:52.353248Z PROPS-END Node-path: branches Node-kind: dir Node-action: add Prop-content-length: 10 Content-length: 10 PROPS-END Node-path: tags Node-kind: dir Node-action: add Prop-content-length: 10 Content-length: 10 PROPS-END Node-path: tête Node-kind: dir Node-action: add Prop-content-length: 10 Content-length: 10 PROPS-END Revision-number: 2 Prop-content-length: 112 Content-length: 112 K 7 svn:log V 13 Added README. K 10 svn:author V 4 john K 8 svn:date V 27 2005-04-01T13:12:18.216267Z PROPS-END Node-path: tête/README.txt Node-kind: file Node-action: add Prop-content-length: 10 Text-content-length: 8 Text-content-md5: a0691c0f61f52683bcb05da98fe028c8 Text-content-sha1: 59ccaf31a075b2a2fe64f83ca05e2694d692d4fc Content-length: 18 PROPS-END A text. Revision-number: 3 Prop-content-length: 113 Content-length: 113 K 7 svn:log V 14 Fixed README. K 10 svn:author V 4 kate K 8 svn:date V 27 2005-04-01T13:24:58.234643Z PROPS-END Node-path: tête/README.txt Node-kind: file Node-action: change Prop-content-length: 75 Text-content-length: 8 Text-content-md5: eaf1c95c78c9f848636d357788bd4a4c Text-content-sha1: 6aade8dde7d1b86b451b5d5f044a8ddcbab6b448 Content-length: 83 K 13 svn:mime-type V 10 text/plain K 13 svn:eol-style V 6 native PROPS-END A test. Revision-number: 4 Prop-content-length: 116 Content-length: 116 K 7 svn:log V 17 More directories. K 10 svn:author V 4 john K 8 svn:date V 27 2005-04-01T15:42:35.450595Z PROPS-END Node-path: tête/dir1 Node-kind: dir Node-action: add Prop-content-length: 10 Content-length: 10 PROPS-END Node-path: tête/dir2 Node-kind: dir Node-action: add Prop-content-length: 10 Content-length: 10 PROPS-END Node-path: tête/dir3 Node-kind: dir Node-action: add Prop-content-length: 10 Content-length: 10 PROPS-END Revision-number: 5 Prop-content-length: 117 Content-length: 117 K 7 svn:log V 18 Moved directories. K 10 svn:author V 4 kate K 8 svn:date V 27 2005-04-01T16:25:39.658099Z PROPS-END Node-path: tête/dir1/dir2 Node-kind: dir Node-action: add Node-copyfrom-rev: 4 Node-copyfrom-path: tête/dir2 Node-path: tête/dir1/dir3 Node-kind: dir Node-action: add Node-copyfrom-rev: 4 Node-copyfrom-path: tête/dir3 Node-path: tête/dir2 Node-action: delete Node-path: tête/dir3 Node-action: delete Revision-number: 6 Prop-content-length: 118 Content-length: 118 K 7 svn:log V 19 More things to read K 10 svn:author V 4 john K 8 svn:date V 27 2005-04-01T18:56:46.985846Z PROPS-END Node-path: tête/README2.txt Node-kind: file Node-action: add Node-copyfrom-rev: 3 Node-copyfrom-path: tête/README.txt Text-copy-source-md5: eaf1c95c78c9f848636d357788bd4a4c Text-copy-source-sha1: 6aade8dde7d1b86b451b5d5f044a8ddcbab6b448 Revision-number: 7 Prop-content-length: 151 Content-length: 151 K 7 svn:log V 42 test the tag operation (copy of directory) K 10 svn:author V 13 Administrator K 8 svn:date V 27 2005-04-14T15:06:20.717616Z PROPS-END Node-path: tags/v1 Node-kind: dir Node-action: add Node-copyfrom-rev: 6 Node-copyfrom-path: tête Revision-number: 8 Prop-content-length: 124 Content-length: 124 K 7 svn:log V 15 Fix stuff in v1 K 10 svn:author V 13 Administrator K 8 svn:date V 27 2005-04-22T08:57:33.499643Z PROPS-END Node-path: branches/v1x Node-kind: dir Node-action: add Node-copyfrom-rev: 7 Node-copyfrom-path: tags/v1 Revision-number: 9 Prop-content-length: 127 Content-length: 127 K 7 svn:log V 18 Now that's the fix K 10 svn:author V 13 Administrator K 8 svn:date V 27 2005-04-22T08:59:24.308979Z PROPS-END Node-path: branches/v1x/README.txt Node-kind: file Node-action: change Text-content-length: 16 Text-content-md5: 02bcabffffd16fe0fc250f08cad95e0c Text-content-sha1: 0828324174b10cc867b7255a84a8155cf89e1b8b Content-length: 16 This is a test. Revision-number: 10 Prop-content-length: 141 Content-length: 141 K 7 svn:log V 32 Tagging v1.1 from the fix branch K 10 svn:author V 13 Administrator K 8 svn:date V 27 2005-04-22T09:00:34.549980Z PROPS-END Node-path: tags/v1.1 Node-kind: dir Node-action: add Node-copyfrom-rev: 9 Node-copyfrom-path: branches/v1x Revision-number: 11 Prop-content-length: 191 Content-length: 191 K 7 svn:log V 82 ''(a few months later)'' We don't need the fix branch anymore, 1.1 is super-stable K 10 svn:author V 13 Administrator K 8 svn:date V 27 2005-04-22T09:01:38.361737Z PROPS-END Node-path: branches/v1x Node-action: delete Revision-number: 12 Prop-content-length: 166 Content-length: 166 K 7 svn:log V 57 ''(a few years later)'' Argh... v1.1 was buggy, after all K 10 svn:author V 13 Administrator K 8 svn:date V 27 2005-04-22T09:06:37.011174Z PROPS-END Node-path: branches/v1x Node-kind: dir Node-action: add Node-copyfrom-rev: 11 Node-copyfrom-path: tags/v1.1 Revision-number: 13 Prop-content-length: 143 Content-length: 143 K 7 svn:log V 43 Setting property on the repository_dir root K 10 svn:author V 5 cboos K 8 svn:date V 27 2005-11-17T15:13:16.197772Z PROPS-END Node-path: Node-kind: dir Node-action: change Prop-content-length: 37 Content-length: 37 K 10 svn:ignore V 6 *.pyc PROPS-END Node-path: tête Node-kind: dir Node-action: change Prop-content-length: 37 Content-length: 37 K 10 svn:ignore V 6 *.pyc PROPS-END Revision-number: 14 Prop-content-length: 119 Content-length: 119 K 7 svn:log V 19 Testing rename+edit K 10 svn:author V 5 cboos K 8 svn:date V 27 2005-11-30T08:47:03.467814Z PROPS-END Node-path: tête/README3.txt Node-kind: file Node-action: add Node-copyfrom-rev: 13 Node-copyfrom-path: tête/README2.txt Text-copy-source-md5: eaf1c95c78c9f848636d357788bd4a4c Text-copy-source-sha1: 6aade8dde7d1b86b451b5d5f044a8ddcbab6b448 Text-content-length: 42 Text-content-md5: 211b820b566541dd49a1283d6476d89f Text-content-sha1: 7e9f46800519d6ae305f44cc07bd7568be3c9d5c Content-length: 42 A test of svn move, followed by a change. Node-path: tête/README2.txt Node-action: delete Revision-number: 15 Prop-content-length: 162 Content-length: 162 K 7 svn:log V 62 Removing original file, just before committing the wc->wc copy K 10 svn:author V 5 cboos K 8 svn:date V 27 2005-12-06T12:47:36.271020Z PROPS-END Node-path: tags/v1.1/README2.txt Node-action: delete Revision-number: 16 Prop-content-length: 138 Content-length: 138 K 7 svn:log V 38 Committing wc->wc copy + local changes K 10 svn:author V 5 cboos K 8 svn:date V 27 2005-12-06T12:47:51.122376Z PROPS-END Node-path: branches/v2 Node-kind: dir Node-action: add Node-copyfrom-rev: 14 Node-copyfrom-path: tags/v1.1 Node-path: branches/v2/README2.txt Node-kind: file Node-action: change Text-content-length: 47 Text-content-md5: 9b7bad978c6ad159f939c3db2038cbb1 Text-content-sha1: 4a475c10d832c28f9a9798477885c48146c0bc18 Content-length: 47 A test + local modifications after wc->wc copy Revision-number: 17 Prop-content-length: 139 Content-length: 139 K 7 svn:log V 39 Test des caractères accentués (cp437) K 10 svn:author V 5 cboos K 8 svn:date V 27 2006-03-31T12:30:25.421875Z PROPS-END Node-path: tête/Résumé.txt Node-kind: file Node-action: add Prop-content-length: 10 Text-content-length: 13 Text-content-md5: 858e52306ecdfcb6f6eadb50d6f9086b Text-content-sha1: f4802e1b42e6256148438ce62d9b95ba671c23f8 Content-length: 23 PROPS-END En rsum ... Revision-number: 18 Prop-content-length: 124 Content-length: 124 K 7 svn:log V 24 Prepare for fancy rename K 10 svn:author V 5 cboos K 8 svn:date V 27 2006-09-27T10:40:38.671875Z PROPS-END Node-path: tête/Xprimary_proc Node-kind: dir Node-action: add Prop-content-length: 10 Content-length: 10 PROPS-END Node-path: tête/Xprimary_proc/Xprimary_pkg.vhd Node-kind: file Node-action: add Prop-content-length: 10 Text-content-length: 5 Text-content-md5: 2debfdcf79f03e4a65a667d21ef9de14 Text-content-sha1: 4c49b08e9b258e0e5867d76ce583c159597b6857 Content-length: 15 PROPS-END TEST Revision-number: 19 Prop-content-length: 145 Content-length: 145 K 7 svn:log V 45 Fancy copy+rename resulting in double delete. K 10 svn:author V 5 cboos K 8 svn:date V 27 2006-09-27T10:41:27.484375Z PROPS-END Node-path: tête/mpp_proc Node-kind: dir Node-action: add Node-copyfrom-rev: 18 Node-copyfrom-path: tête/Xprimary_proc Node-path: tête/mpp_proc/Xprimary_proc Node-kind: dir Node-action: add Node-copyfrom-rev: 18 Node-copyfrom-path: tête/Xprimary_proc Node-path: tête/mpp_proc/Xprimary_proc/Xprimary_pkg.vhd Node-action: delete Node-path: tête/mpp_proc/Xprimary_pkg.vhd Node-action: delete Node-path: tête/Xprimary_proc Node-action: delete Revision-number: 20 Prop-content-length: 132 Content-length: 132 K 7 svn:log V 20 Chez moi ça marche K 10 svn:author V 16 Jonas Borgström K 8 svn:date V 27 2006-12-04T10:47:24.015625Z PROPS-END Node-path: tête/Résumé.txt Node-kind: file Node-action: change Text-content-length: 25 Text-content-md5: c3744e0035c756a17fdf6dbbdd5719f0 Text-content-sha1: f7453fe6bac18f7dc717295a30331297ff05ae14 Content-length: 25 En rsum ... a marche. Revision-number: 21 Prop-content-length: 139 Content-length: 139 K 7 svn:log V 39 copy from outside of the scope + delete K 10 svn:author V 5 cboos K 8 svn:date V 27 2007-04-30T17:45:26.234375Z PROPS-END Node-path: tête/v2 Node-kind: dir Node-action: add Node-copyfrom-rev: 20 Node-copyfrom-path: branches/v2 Node-path: tête/v2/dir1 Node-action: delete Node-path: tête/v2/README2.txt Node-action: delete Revision-number: 22 Prop-content-length: 130 Content-length: 130 K 7 svn:log V 30 test delete after copy (#4900) K 10 svn:author V 5 cboos K 8 svn:date V 27 2010-02-26T14:48:19.377000Z PROPS-END Node-path: branches/v3 Node-kind: dir Node-action: add Node-copyfrom-rev: 21 Node-copyfrom-path: tête Node-path: branches/v3/v2 Node-action: delete Node-path: branches/v3/dir1 Node-action: delete Node-path: branches/v3/mpp_proc Node-action: delete Trac-1.0.1/tracopt/versioncontrol/svn/tests/__init__.py0000644000175200017520000000035212102610255022217 0ustar cbooscboosimport unittest from tracopt.versioncontrol.svn.tests import svn_fs def suite(): suite = unittest.TestSuite() suite.addTest(svn_fs.suite()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/tracopt/versioncontrol/svn/__init__.py0000644000175200017520000000000012102610255021043 0ustar cbooscboosTrac-1.0.1/tracopt/versioncontrol/git/0000755000175200017520000000000012102610436016722 5ustar cbooscboosTrac-1.0.1/tracopt/versioncontrol/git/PyGIT.py0000644000175200017520000010273012102610255020232 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2012 Edgewall Software # Copyright (C) 2006-2011, Herbert Valerio Riedel # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. from __future__ import with_statement import os import codecs from collections import deque from contextlib import contextmanager import cStringIO from functools import partial from operator import itemgetter import re from subprocess import Popen, PIPE import sys from threading import Lock import time import weakref __all__ = ['GitError', 'GitErrorSha', 'Storage', 'StorageFactory'] def terminate(process): """Python 2.5 compatibility method. os.kill is not available on Windows before Python 2.7. In Python 2.6 subprocess.Popen has a terminate method. (It also seems to have some issues on Windows though.) """ def terminate_win(process): import ctypes PROCESS_TERMINATE = 1 handle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, False, process.pid) ctypes.windll.kernel32.TerminateProcess(handle, -1) ctypes.windll.kernel32.CloseHandle(handle) def terminate_nix(process): import os import signal return os.kill(process.pid, signal.SIGTERM) if sys.platform == 'win32': return terminate_win(process) return terminate_nix(process) class GitError(Exception): pass class GitErrorSha(GitError): pass # Helper functions def parse_commit(raw): """Parse the raw content of a commit (as given by `git cat-file -p `). Return the commit message and a dict of properties. """ if not raw: raise GitErrorSha lines = raw.splitlines() if not lines: raise GitErrorSha line = lines.pop(0) props = {} multiline = multiline_key = None while line: if line[0] == ' ': if not multiline: multiline_key = key multiline = [props[multiline_key][-1]] multiline.append(line[1:]) else: key, value = line.split(None, 1) props.setdefault(key, []).append(value.strip()) line = lines.pop(0) if multiline and (not line or key != multiline_key): props[multiline_key][-1] = '\n'.join(multiline) multiline = None return '\n'.join(lines), props class GitCore(object): """Low-level wrapper around git executable""" def __init__(self, git_dir=None, git_bin='git'): self.__git_bin = git_bin self.__git_dir = git_dir def __repr__(self): return '' % (self.__git_bin, self.__git_dir) def __build_git_cmd(self, gitcmd, *args): """construct command tuple for git call suitable for Popen()""" cmd = [self.__git_bin] if self.__git_dir: cmd.append('--git-dir=%s' % self.__git_dir) cmd.append(gitcmd) cmd.extend(args) return cmd def __pipe(self, git_cmd, *cmd_args, **kw): if sys.platform == 'win32': return Popen(self.__build_git_cmd(git_cmd, *cmd_args), **kw) else: return Popen(self.__build_git_cmd(git_cmd, *cmd_args), close_fds=True, **kw) def __execute(self, git_cmd, *cmd_args): """execute git command and return file-like object of stdout""" #print >>sys.stderr, "DEBUG:", git_cmd, cmd_args p = self.__pipe(git_cmd, stdout=PIPE, stderr=PIPE, *cmd_args) stdout_data, stderr_data = p.communicate() #TODO, do something with p.returncode, e.g. raise exception return stdout_data def cat_file_batch(self): return self.__pipe('cat-file', '--batch', stdin=PIPE, stdout=PIPE) def log_pipe(self, *cmd_args): return self.__pipe('log', stdout=PIPE, *cmd_args) def __getattr__(self, name): if name[0] == '_' or name in ['cat_file_batch', 'log_pipe']: raise AttributeError, name return partial(self.__execute, name.replace('_','-')) __is_sha_pat = re.compile(r'[0-9A-Fa-f]*$') @classmethod def is_sha(cls, sha): """returns whether sha is a potential sha id (i.e. proper hexstring between 4 and 40 characters) """ # quick test before starting up regexp matcher if not (4 <= len(sha) <= 40): return False return bool(cls.__is_sha_pat.match(sha)) class SizedDict(dict): """Size-bounded dictionary with FIFO replacement strategy""" def __init__(self, max_size=0): dict.__init__(self) self.__max_size = max_size self.__key_fifo = deque() self.__lock = Lock() def __setitem__(self, name, value): with self.__lock: assert len(self) == len(self.__key_fifo) # invariant if not self.__contains__(name): self.__key_fifo.append(name) rc = dict.__setitem__(self, name, value) while len(self.__key_fifo) > self.__max_size: self.__delitem__(self.__key_fifo.popleft()) assert len(self) == len(self.__key_fifo) # invariant return rc def setdefault(self, *_): raise NotImplemented("SizedDict has no setdefault() method") class StorageFactory(object): __dict = weakref.WeakValueDictionary() __dict_nonweak = dict() __dict_lock = Lock() def __init__(self, repo, log, weak=True, git_bin='git', git_fs_encoding=None): self.logger = log with StorageFactory.__dict_lock: try: i = StorageFactory.__dict[repo] except KeyError: i = Storage(repo, log, git_bin, git_fs_encoding) StorageFactory.__dict[repo] = i # create or remove additional reference depending on 'weak' # argument if weak: try: del StorageFactory.__dict_nonweak[repo] except KeyError: pass else: StorageFactory.__dict_nonweak[repo] = i self.__inst = i self.__repo = repo def getInstance(self): is_weak = self.__repo not in StorageFactory.__dict_nonweak self.logger.debug("requested %sPyGIT.Storage instance %d for '%s'" % (("","weak ")[is_weak], id(self.__inst), self.__repo)) return self.__inst class Storage(object): """High-level wrapper around GitCore with in-memory caching""" __SREV_MIN = 4 # minimum short-rev length class RevCache(tuple): """RevCache(youngest_rev, oldest_rev, rev_dict, tag_set, srev_dict, branch_dict) In Python 2.7 this class could be defined by: from collections import namedtuple RevCache = namedtuple('RevCache', 'youngest_rev oldest_rev ' 'rev_dict tag_set srev_dict ' 'branch_dict') This implementation is what that code generator would produce. """ __slots__ = () _fields = ('youngest_rev', 'oldest_rev', 'rev_dict', 'tag_set', 'srev_dict', 'branch_dict') def __new__(cls, youngest_rev, oldest_rev, rev_dict, tag_set, srev_dict, branch_dict): return tuple.__new__(cls, (youngest_rev, oldest_rev, rev_dict, tag_set, srev_dict, branch_dict)) @classmethod def _make(cls, iterable, new=tuple.__new__, len=len): """Make a new RevCache object from a sequence or iterable""" result = new(cls, iterable) if len(result) != 6: raise TypeError('Expected 6 arguments, got %d' % len(result)) return result def __repr__(self): return 'RevCache(youngest_rev=%r, oldest_rev=%r, rev_dict=%r, ' \ 'tag_set=%r, srev_dict=%r, branch_dict=%r)' % self def _asdict(t): """Return a new dict which maps field names to their values""" return {'youngest_rev': t[0], 'oldest_rev': t[1], 'rev_dict': t[2], 'tag_set': t[3], 'srev_dict': t[4], 'branch_dict': t[5]} def _replace(self, **kwds): """Return a new RevCache object replacing specified fields with new values """ result = self._make(map(kwds.pop, ('youngest_rev', 'oldest_rev', 'rev_dict', 'tag_set', 'srev_dict', 'branch_dict'), self)) if kwds: raise ValueError("Got unexpected field names: %r" % kwds.keys()) return result def __getnewargs__(self): return tuple(self) youngest_rev = property(itemgetter(0)) oldest_rev = property(itemgetter(1)) rev_dict = property(itemgetter(2)) tag_set = property(itemgetter(3)) srev_dict = property(itemgetter(4)) branch_dict = property(itemgetter(5)) @staticmethod def __rev_key(rev): assert len(rev) >= 4 #assert GitCore.is_sha(rev) srev_key = int(rev[:4], 16) assert srev_key >= 0 and srev_key <= 0xffff return srev_key @staticmethod def git_version(git_bin='git'): GIT_VERSION_MIN_REQUIRED = (1, 5, 6) try: g = GitCore(git_bin=git_bin) [v] = g.version().splitlines() version = v.strip().split()[2] # 'version' has usually at least 3 numeric version # components, e.g.:: # 1.5.4.2 # 1.5.4.3.230.g2db511 # 1.5.4.GIT def try_int(s): try: return int(s) except ValueError: return s split_version = tuple(map(try_int, version.split('.'))) result = {} result['v_str'] = version result['v_tuple'] = split_version result['v_min_tuple'] = GIT_VERSION_MIN_REQUIRED result['v_min_str'] = ".".join(map(str, GIT_VERSION_MIN_REQUIRED)) result['v_compatible'] = split_version >= GIT_VERSION_MIN_REQUIRED return result except Exception, e: raise GitError("Could not retrieve GIT version (tried to " "execute/parse '%s --version' but got %s)" % (git_bin, repr(e))) def __init__(self, git_dir, log, git_bin='git', git_fs_encoding=None): """Initialize PyGit.Storage instance `git_dir`: path to .git folder; this setting is not affected by the `git_fs_encoding` setting `log`: logger instance `git_bin`: path to executable this setting is not affected by the `git_fs_encoding` setting `git_fs_encoding`: encoding used for paths stored in git repository; if `None`, no implicit decoding/encoding to/from unicode objects is performed, and bytestrings are returned instead """ self.logger = log self.commit_encoding = None # caches self.__rev_cache = None self.__rev_cache_lock = Lock() # cache the last 200 commit messages self.__commit_msg_cache = SizedDict(200) self.__commit_msg_lock = Lock() self.__cat_file_pipe = None self.__cat_file_pipe_lock = Lock() if git_fs_encoding is not None: # validate encoding name codecs.lookup(git_fs_encoding) # setup conversion functions self._fs_to_unicode = lambda s: s.decode(git_fs_encoding) self._fs_from_unicode = lambda s: s.encode(git_fs_encoding) else: # pass bytestrings as-is w/o any conversion self._fs_to_unicode = self._fs_from_unicode = lambda s: s # simple sanity checking __git_file_path = partial(os.path.join, git_dir) if not all(map(os.path.exists, map(__git_file_path, ['HEAD','objects','refs']))): self.logger.error("GIT control files missing in '%s'" % git_dir) if os.path.exists(__git_file_path('.git')): self.logger.error("entry '.git' found in '%s'" " -- maybe use that folder instead..." % git_dir) raise GitError("GIT control files not found, maybe wrong " "directory?") self.repo = GitCore(git_dir, git_bin=git_bin) self.logger.debug("PyGIT.Storage instance %d constructed" % id(self)) def __del__(self): with self.__cat_file_pipe_lock: if self.__cat_file_pipe is not None: self.__cat_file_pipe.stdin.close() terminate(self.__cat_file_pipe) self.__cat_file_pipe.wait() # # cache handling # # called by Storage.sync() def __rev_cache_sync(self, youngest_rev=None): """invalidates revision db cache if necessary""" with self.__rev_cache_lock: need_update = False if self.__rev_cache: last_youngest_rev = self.__rev_cache.youngest_rev if last_youngest_rev != youngest_rev: self.logger.debug("invalidated caches (%s != %s)" % (last_youngest_rev, youngest_rev)) need_update = True else: need_update = True # almost NOOP if need_update: self.__rev_cache = None return need_update def get_rev_cache(self): """Retrieve revision cache may rebuild cache on the fly if required returns RevCache tuple """ with self.__rev_cache_lock: if self.__rev_cache is None: # can be cleared by Storage.__rev_cache_sync() self.logger.debug("triggered rebuild of commit tree db " "for %d" % id(self)) ts0 = time.time() youngest = None oldest = None new_db = {} # db new_sdb = {} # short_rev db # helper for reusing strings __rev_seen = {} def __rev_reuse(rev): rev = str(rev) return __rev_seen.setdefault(rev, rev) new_tags = set(__rev_reuse(rev.strip()) for rev in self.repo.rev_parse('--tags') .splitlines()) new_branches = [(k, __rev_reuse(v)) for k, v in self._get_branches()] head_revs = set(v for _, v in new_branches) rev = ord_rev = 0 for ord_rev, revs in enumerate( self.repo.rev_list('--parents', '--topo-order', '--all') .splitlines()): revs = map(__rev_reuse, revs.strip().split()) rev = revs[0] # first rev seen is assumed to be the youngest one if not ord_rev: youngest = rev # shortrev "hash" map srev_key = self.__rev_key(rev) new_sdb.setdefault(srev_key, []).append(rev) # parents parents = tuple(revs[1:]) # new_db[rev] = (children(rev), parents(rev), # ordinal_id(rev), rheads(rev)) if rev in new_db: # (incomplete) entry was already created by children _children, _parents, _ord_rev, _rheads = new_db[rev] assert _children assert not _parents assert _ord_rev == 0 if rev in head_revs and rev not in _rheads: _rheads.append(rev) else: # new entry _children = [] _rheads = [rev] if rev in head_revs else [] # create/update entry # transform lists into tuples since entry will be final new_db[rev] = tuple(_children), tuple(parents), \ ord_rev + 1, tuple(_rheads) # update parents(rev)s for parent in parents: # by default, a dummy ordinal_id is used # for the mean-time _children, _parents, _ord_rev, _rheads2 = \ new_db.setdefault(parent, ([], [], 0, [])) # update parent(rev)'s children if rev not in _children: _children.append(rev) # update parent(rev)'s rheads for rev in _rheads: if rev not in _rheads2: _rheads2.append(rev) # last rev seen is assumed to be the oldest # one (with highest ord_rev) oldest = rev __rev_seen = None # convert sdb either to dict or array depending on size tmp = [()]*(max(new_sdb.keys())+1) \ if len(new_sdb) > 5000 else {} try: while True: k, v = new_sdb.popitem() tmp[k] = tuple(v) except KeyError: pass assert len(new_sdb) == 0 new_sdb = tmp # atomically update self.__rev_cache self.__rev_cache = Storage.RevCache(youngest, oldest, new_db, new_tags, new_sdb, new_branches) ts1 = time.time() self.logger.debug("rebuilt commit tree db for %d with %d " "entries (took %.1f ms)" % (id(self), len(new_db), 1000*(ts1-ts0))) assert all(e is not None for e in self.__rev_cache) \ or not any(self.__rev_cache) return self.__rev_cache # with self.__rev_cache_lock # see RevCache namedtuple rev_cache = property(get_rev_cache) def _get_branches(self): """returns list of (local) branches, with active (= HEAD) one being the first item """ result = [] for e in self.repo.branch('-v', '--no-abbrev').splitlines(): bname, bsha = e[1:].strip().split()[:2] if e.startswith('*'): result.insert(0, (bname, bsha)) else: result.append((bname, bsha)) return result def get_branches(self): """returns list of (local) branches, with active (= HEAD) one being the first item """ return ((self._fs_to_unicode(name), sha) for name, sha in self.rev_cache.branch_dict) def get_commits(self): return self.rev_cache.rev_dict def oldest_rev(self): return self.rev_cache.oldest_rev def youngest_rev(self): return self.rev_cache.youngest_rev def get_branch_contains(self, sha, resolve=False): """return list of reachable head sha ids or (names, sha) pairs if resolve is true see also get_branches() """ _rev_cache = self.rev_cache try: rheads = _rev_cache.rev_dict[sha][3] except KeyError: return [] if resolve: return ((self._fs_to_unicode(k), v) for k, v in _rev_cache.branch_dict if v in rheads) return rheads def history_relative_rev(self, sha, rel_pos): db = self.get_commits() if sha not in db: raise GitErrorSha() if rel_pos == 0: return sha lin_rev = db[sha][2] + rel_pos if lin_rev < 1 or lin_rev > len(db): return None for k, v in db.iteritems(): if v[2] == lin_rev: return k # should never be reached if db is consistent raise GitError("internal inconsistency detected") def hist_next_revision(self, sha): return self.history_relative_rev(sha, -1) def hist_prev_revision(self, sha): return self.history_relative_rev(sha, +1) def get_commit_encoding(self): if self.commit_encoding is None: self.commit_encoding = \ self.repo.repo_config("--get", "i18n.commitEncoding") \ .strip() or 'utf-8' return self.commit_encoding def head(self): """get current HEAD commit id""" return self.verifyrev('HEAD') def cat_file(self, kind, sha): with self.__cat_file_pipe_lock: if self.__cat_file_pipe is None: self.__cat_file_pipe = self.repo.cat_file_batch() try: self.__cat_file_pipe.stdin.write(sha + '\n') self.__cat_file_pipe.stdin.flush() split_stdout_line = self.__cat_file_pipe.stdout.readline() \ .split() if len(split_stdout_line) != 3: raise GitError("internal error (could not split line " "'%s')" % (split_stdout_line,)) _sha, _type, _size = split_stdout_line if _type != kind: raise GitError("internal error (got unexpected object " "kind '%s', expected '%s')" % (_type, kind)) size = int(_size) return self.__cat_file_pipe.stdout.read(size + 1)[:size] except: # There was an error, we should close the pipe to get to a # consistent state (Otherwise it happens that next time we # call cat_file we get payload from previous call) self.logger.debug("closing cat_file pipe") self.__cat_file_pipe.stdin.close() terminate(self.__cat_file_pipe) self.__cat_file_pipe.wait() self.__cat_file_pipe = None def verifyrev(self, rev): """verify/lookup given revision object and return a sha id or None if lookup failed """ rev = self._fs_from_unicode(rev) _rev_cache = self.rev_cache if GitCore.is_sha(rev): # maybe it's a short or full rev fullrev = self.fullrev(rev) if fullrev: return fullrev # fall back to external git calls rc = self.repo.rev_parse('--verify', rev).strip() if not rc: return None if rc in _rev_cache.rev_dict: return rc if rc in _rev_cache.tag_set: sha = self.cat_file('tag', rc).split(None, 2)[:2] if sha[0] != 'object': self.logger.debug("unexpected result from 'git-cat-file tag " "%s'" % rc) return None return sha[1] return None def shortrev(self, rev, min_len=7): """try to shorten sha id""" #try to emulate the following: #return self.repo.rev_parse("--short", str(rev)).strip() rev = str(rev) if min_len < self.__SREV_MIN: min_len = self.__SREV_MIN _rev_cache = self.rev_cache if rev not in _rev_cache.rev_dict: return None srev = rev[:min_len] srevs = set(_rev_cache.srev_dict[self.__rev_key(rev)]) if len(srevs) == 1: return srev # we already got a unique id # find a shortened id for which rev doesn't conflict with # the other ones from srevs crevs = srevs - set([rev]) for l in range(min_len+1, 40): srev = rev[:l] if srev not in [ r[:l] for r in crevs ]: return srev return rev # worst-case, all except the last character match def fullrev(self, srev): """try to reverse shortrev()""" srev = str(srev) _rev_cache = self.rev_cache # short-cut if len(srev) == 40 and srev in _rev_cache.rev_dict: return srev if not GitCore.is_sha(srev): return None try: srevs = _rev_cache.srev_dict[self.__rev_key(srev)] except KeyError: return None srevs = filter(lambda s: s.startswith(srev), srevs) if len(srevs) == 1: return srevs[0] return None def get_tags(self): return (self._fs_to_unicode(e.strip()) for e in self.repo.tag('-l').splitlines()) def ls_tree(self, rev, path=''): rev = rev and str(rev) or 'HEAD' # paranoia path = self._fs_from_unicode(path) if path.startswith('/'): path = path[1:] tree = self.repo.ls_tree('-z', '-l', rev, '--', path).split('\0') def split_ls_tree_line(l): """split according to ' \t'""" meta, fname = l.split('\t', 1) _mode, _type, _sha, _size = meta.split() if _size == '-': _size = None else: _size = int(_size) return _mode, _type, _sha, _size, self._fs_to_unicode(fname) return [ split_ls_tree_line(e) for e in tree if e ] def read_commit(self, commit_id): if not commit_id: raise GitError("read_commit called with empty commit_id") commit_id, commit_id_orig = self.fullrev(commit_id), commit_id db = self.get_commits() if commit_id not in db: self.logger.info("read_commit failed for '%s' ('%s')" % (commit_id, commit_id_orig)) raise GitErrorSha with self.__commit_msg_lock: if self.__commit_msg_cache.has_key(commit_id): # cache hit result = self.__commit_msg_cache[commit_id] return result[0], dict(result[1]) # cache miss raw = self.cat_file('commit', commit_id) raw = unicode(raw, self.get_commit_encoding(), 'replace') result = parse_commit(raw) self.__commit_msg_cache[commit_id] = result return result[0], dict(result[1]) def get_file(self, sha): return cStringIO.StringIO(self.cat_file('blob', str(sha))) def get_obj_size(self, sha): sha = str(sha) try: obj_size = int(self.repo.cat_file('-s', sha).strip()) except ValueError: raise GitErrorSha("object '%s' not found" % sha) return obj_size def children(self, sha): db = self.get_commits() try: return list(db[sha][0]) except KeyError: return [] def children_recursive(self, sha, rev_dict=None): """Recursively traverse children in breadth-first order""" if rev_dict is None: rev_dict = self.get_commits() work_list = deque() seen = set() seen.update(rev_dict[sha][0]) work_list.extend(rev_dict[sha][0]) while work_list: p = work_list.popleft() yield p _children = set(rev_dict[p][0]) - seen seen.update(_children) work_list.extend(_children) assert len(work_list) == 0 def parents(self, sha): db = self.get_commits() try: return list(db[sha][1]) except KeyError: return [] def all_revs(self): return self.get_commits().iterkeys() def sync(self): rev = self.repo.rev_list('--max-count=1', '--topo-order', '--all') \ .strip() return self.__rev_cache_sync(rev) @contextmanager def get_historian(self, sha, base_path): p = [] change = {} next_path = [] def name_status_gen(): p[:] = [self.repo.log_pipe('--pretty=format:%n%H', '--name-status', sha, '--', base_path)] f = p[0].stdout for l in f: if l == '\n': continue old_sha = l.rstrip('\n') for l in f: if l == '\n': break _, path = l.rstrip('\n').split('\t', 1) while path not in change: change[path] = old_sha if next_path == [path]: yield old_sha try: path, _ = path.rsplit('/', 1) except ValueError: break f.close() terminate(p[0]) p[0].wait() p[:] = [] while True: yield None gen = name_status_gen() def historian(path): try: return change[path] except KeyError: next_path[:] = [path] return gen.next() yield historian if p: p[0].stdout.close() terminate(p[0]) p[0].wait() def last_change(self, sha, path, historian=None): if historian is not None: return historian(path) return self.repo.rev_list('--max-count=1', sha, '--', self._fs_from_unicode(path)).strip() or None def history(self, sha, path, limit=None): if limit is None: limit = -1 tmp = self.repo.rev_list('--max-count=%d' % limit, str(sha), '--', self._fs_from_unicode(path)) return [ rev.strip() for rev in tmp.splitlines() ] def history_timerange(self, start, stop): return [ rev.strip() for rev in \ self.repo.rev_list('--reverse', '--max-age=%d' % start, '--min-age=%d' % stop, '--all').splitlines() ] def rev_is_anchestor_of(self, rev1, rev2): """return True if rev2 is successor of rev1""" rev1 = rev1.strip() rev2 = rev2.strip() rev_dict = self.get_commits() return (rev2 in rev_dict and rev2 in self.children_recursive(rev1, rev_dict)) def blame(self, commit_sha, path): in_metadata = False path = self._fs_from_unicode(path) for line in self.repo.blame('-p', '--', path, str(commit_sha)) \ .splitlines(): assert line if in_metadata: in_metadata = not line.startswith('\t') else: split_line = line.split() if len(split_line) == 4: (sha, orig_lineno, lineno, group_size) = split_line else: (sha, orig_lineno, lineno) = split_line assert len(sha) == 40 yield (sha, lineno) in_metadata = True assert not in_metadata def diff_tree(self, tree1, tree2, path='', find_renames=False): """calls `git diff-tree` and returns tuples of the kind (mode1,mode2,obj1,obj2,action,path1,path2)""" # diff-tree returns records with the following structure: # : NUL NUL [ NUL ] path = self._fs_from_unicode(path).strip('/') diff_tree_args = ['-z', '-r'] if find_renames: diff_tree_args.append('-M') diff_tree_args.extend([str(tree1) if tree1 else '--root', str(tree2), '--', path]) lines = self.repo.diff_tree(*diff_tree_args).split('\0') assert lines[-1] == '' del lines[-1] if tree1 is None and lines: # if only one tree-sha is given on commandline, # the first line is just the redundant tree-sha itself... assert not lines[0].startswith(':') del lines[0] # FIXME: the following code is ugly, needs rewrite chg = None def __chg_tuple(): if len(chg) == 6: chg.append(None) else: chg[6] = self._fs_to_unicode(chg[6]) chg[5] = self._fs_to_unicode(chg[5]) assert len(chg) == 7 return tuple(chg) for line in lines: if line.startswith(':'): if chg: yield __chg_tuple() chg = line[1:].split() assert len(chg) == 5 else: chg.append(line) # handle left-over chg entry if chg: yield __chg_tuple() Trac-1.0.1/tracopt/versioncontrol/git/tests/0000755000175200017520000000000012102610436020064 5ustar cbooscboosTrac-1.0.1/tracopt/versioncontrol/git/tests/PyGIT.py0000644000175200017520000003474712102610255021410 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2012 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. import os import shutil import tempfile import unittest from subprocess import Popen, PIPE from trac.test import locate, EnvironmentStub from trac.util import create_file from trac.util.compat import close_fds from tracopt.versioncontrol.git.PyGIT import GitCore, Storage, parse_commit class GitTestCase(unittest.TestCase): def test_is_sha(self): self.assertTrue(not GitCore.is_sha('123')) self.assertTrue(GitCore.is_sha('1a3f')) self.assertTrue(GitCore.is_sha('f' * 40)) self.assertTrue(not GitCore.is_sha('x' + 'f' * 39)) self.assertTrue(not GitCore.is_sha('f' * 41)) def test_git_version(self): v = Storage.git_version() self.assertTrue(v) self.assertTrue(v['v_compatible']) class TestParseCommit(unittest.TestCase): # The ''' ''' lines are intended to keep lines with trailing whitespace commit2240a7b = '''\ tree b19535236cfb6c64b798745dd3917dafc27bcd0a parent 30aaca4582eac20a52ac7b2ec35bdb908133e5b1 parent 5a0dc7365c240795bf190766eba7a27600be3b3e author Linus Torvalds 1323915958 -0800 committer Linus Torvalds 1323915958 -0800 mergetag object 5a0dc7365c240795bf190766eba7a27600be3b3e type commit tag tytso-for-linus-20111214A tagger Theodore Ts'o 1323890113 -0500 ''' ''' tytso-for-linus-20111214 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (GNU/Linux) ''' ''' iQIcBAABCAAGBQJO6PXBAAoJENNvdpvBGATwpuEP/2RCxmdWYZ8/6Z6pmTh3hHN5 fx6HckTdvLQOvbQs72wzVW0JKyc25QmW2mQc5z3MjSymjf/RbEKihPUITRNbHrTD T2sP/lWu09AKLioEg4ucAKn/A7Do3UDIkXTszvVVP/t2psVPzLeJ1njQKra14Nyz o0+gSlnwuGx9WaxfR+7MYNs2ikdSkXIeYsiFAOY4YOxwwC99J/lZ0YaNkbI7UBtC yu2XLIvPboa5JZXANq2G3VhVIETMmOyRTCC76OAXjqkdp9nLFWDG0ydqQh0vVZwL xQGOmAj+l3BNTE0QmMni1w7A0SBU3N6xBA5HN6Y49RlbsMYG27aN54Fy5K2R41I3 QXVhBL53VD6b0KaITcoz7jIGIy6qk9Wx+2WcCYtQBSIjL2YwlaJq0PL07+vRamex sqHGDejcNY87i6AV0DP6SNuCFCi9xFYoAoMi9Wu5E9+T+Vck0okFzW/luk/FvsSP YA5Dh+vISyBeCnWQvcnBmsUQyf8d9MaNnejZ48ath+GiiMfY8USAZ29RAG4VuRtS 9DAyTTIBA73dKpnvEV9u4i8Lwd8hRVMOnPyOO785NwEXk3Ng08pPSSbMklW6UfCY 4nr5UNB13ZPbXx4uoAvATMpCpYxMaLEdxmeMvgXpkekl0hHBzpVDey1Vu9fb/a5n dQpo6WWG9HIJ23hOGAGR =n3Lm -----END PGP SIGNATURE----- Merge tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4 * tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4: ext4: handle EOF correctly in ext4_bio_write_page() ext4: remove a wrong BUG_ON in ext4_ext_convert_to_initialized ext4: correctly handle pages w/o buffers in ext4_discard_partial_buffers() ext4: avoid potential hang in mpage_submit_io() when blocksize < pagesize ext4: avoid hangs in ext4_da_should_update_i_disksize() ext4: display the correct mount option in /proc/mounts for [no]init_itable ext4: Fix crash due to getting bogus eh_depth value on big-endian systems ext4: fix ext4_end_io_dio() racing against fsync() .. using the new signed tag merge of git that now verifies the gpg signature automatically. Yay. The branchname was just 'dev', which is prettier. I'll tell Ted to use nicer tag names for future cases. ''' def test_parse(self): msg, props = parse_commit(self.commit2240a7b) self.assertTrue(msg) self.assertTrue(props) self.assertEquals( ['30aaca4582eac20a52ac7b2ec35bdb908133e5b1', '5a0dc7365c240795bf190766eba7a27600be3b3e'], props['parent']) self.assertEquals( ['Linus Torvalds 1323915958 -0800'], props['author']) self.assertEquals(props['author'], props['committer']) # Merge tag self.assertEquals(['''\ object 5a0dc7365c240795bf190766eba7a27600be3b3e type commit tag tytso-for-linus-20111214A tagger Theodore Ts\'o 1323890113 -0500 tytso-for-linus-20111214 -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (GNU/Linux) iQIcBAABCAAGBQJO6PXBAAoJENNvdpvBGATwpuEP/2RCxmdWYZ8/6Z6pmTh3hHN5 fx6HckTdvLQOvbQs72wzVW0JKyc25QmW2mQc5z3MjSymjf/RbEKihPUITRNbHrTD T2sP/lWu09AKLioEg4ucAKn/A7Do3UDIkXTszvVVP/t2psVPzLeJ1njQKra14Nyz o0+gSlnwuGx9WaxfR+7MYNs2ikdSkXIeYsiFAOY4YOxwwC99J/lZ0YaNkbI7UBtC yu2XLIvPboa5JZXANq2G3VhVIETMmOyRTCC76OAXjqkdp9nLFWDG0ydqQh0vVZwL xQGOmAj+l3BNTE0QmMni1w7A0SBU3N6xBA5HN6Y49RlbsMYG27aN54Fy5K2R41I3 QXVhBL53VD6b0KaITcoz7jIGIy6qk9Wx+2WcCYtQBSIjL2YwlaJq0PL07+vRamex sqHGDejcNY87i6AV0DP6SNuCFCi9xFYoAoMi9Wu5E9+T+Vck0okFzW/luk/FvsSP YA5Dh+vISyBeCnWQvcnBmsUQyf8d9MaNnejZ48ath+GiiMfY8USAZ29RAG4VuRtS 9DAyTTIBA73dKpnvEV9u4i8Lwd8hRVMOnPyOO785NwEXk3Ng08pPSSbMklW6UfCY 4nr5UNB13ZPbXx4uoAvATMpCpYxMaLEdxmeMvgXpkekl0hHBzpVDey1Vu9fb/a5n dQpo6WWG9HIJ23hOGAGR =n3Lm -----END PGP SIGNATURE-----'''], props['mergetag']) # Message self.assertEquals("""Merge tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4 * tag 'tytso-for-linus-20111214' of git://git.kernel.org/pub/scm/linux/kernel/git/tytso/ext4: ext4: handle EOF correctly in ext4_bio_write_page() ext4: remove a wrong BUG_ON in ext4_ext_convert_to_initialized ext4: correctly handle pages w/o buffers in ext4_discard_partial_buffers() ext4: avoid potential hang in mpage_submit_io() when blocksize < pagesize ext4: avoid hangs in ext4_da_should_update_i_disksize() ext4: display the correct mount option in /proc/mounts for [no]init_itable ext4: Fix crash due to getting bogus eh_depth value on big-endian systems ext4: fix ext4_end_io_dio() racing against fsync() .. using the new signed tag merge of git that now verifies the gpg signature automatically. Yay. The branchname was just 'dev', which is prettier. I'll tell Ted to use nicer tag names for future cases.""", msg) class UnicodeNameTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub() self.repos_path = tempfile.mkdtemp(prefix='trac-gitrepos') self.git_bin = locate('git') # create git repository and master branch self._git('init', self.repos_path) create_file(os.path.join(self.repos_path, '.gitignore')) self._git('add', '.gitignore') self._git('commit', '-a', '-m', 'test') def tearDown(self): if os.path.isdir(self.repos_path): shutil.rmtree(self.repos_path) def _git(self, *args): args = [self.git_bin] + list(args) proc = Popen(args, stdout=PIPE, stderr=PIPE, close_fds=close_fds, cwd=self.repos_path) proc.wait() assert proc.returncode == 0 return proc def _storage(self): path = os.path.join(self.repos_path, '.git') return Storage(path, self.env.log, self.git_bin, 'utf-8') def test_unicode_verifyrev(self): storage = self._storage() self.assertNotEqual(None, storage.verifyrev(u'master')) self.assertEquals(None, storage.verifyrev(u'tété')) def test_unicode_filename(self): create_file(os.path.join(self.repos_path, 'tickét.txt')) self._git('add', 'tickét.txt') self._git('commit', '-m', 'unicode-filename') storage = self._storage() filenames = sorted(fname for mode, type, sha, size, fname in storage.ls_tree('HEAD')) self.assertEquals(unicode, type(filenames[0])) self.assertEquals(unicode, type(filenames[1])) self.assertEquals(u'.gitignore', filenames[0]) self.assertEquals(u'tickét.txt', filenames[1]) def test_unicode_branches(self): self._git('checkout', '-b', 'tickét10980', 'master') storage = self._storage() branches = sorted(storage.get_branches()) self.assertEquals(unicode, type(branches[0][0])) self.assertEquals(unicode, type(branches[1][0])) self.assertEquals(u'master', branches[0][0]) self.assertEquals(u'tickét10980', branches[1][0]) contains = sorted(storage.get_branch_contains(branches[1][1], resolve=True)) self.assertEquals(unicode, type(contains[0][0])) self.assertEquals(unicode, type(contains[1][0])) self.assertEquals(u'master', contains[0][0]) self.assertEquals(u'tickét10980', contains[1][0]) def test_unicode_tags(self): self._git('tag', 'täg-t10980', 'master') storage = self._storage() tags = tuple(storage.get_tags()) self.assertEquals(unicode, type(tags[0])) self.assertEquals(u'täg-t10980', tags[0]) self.assertNotEqual(None, storage.verifyrev(u'täg-t10980')) #class GitPerformanceTestCase(unittest.TestCase): # """Performance test. Not really a unit test. # Not self-contained: Needs a git repository and prints performance result # instead of testing anything. # TODO: Move to a profiling script?""" # # def test_performance(self): # import logging # import timeit # # g = Storage(path_to_repo, logging) # Need a git repository path here # revs = g.get_commits().keys() # # def shortrev_test(): # for i in revs: # i = str(i) # s = g.shortrev(i, min_len=4) # self.assertTrue(i.startswith(s)) # self.assertEquals(g.fullrev(s), i) # # iters = 1 # t = timeit.Timer("shortrev_test()", # "from __main__ import shortrev_test") # usec_per_rev = (1000000 * t.timeit(number=iters)/len(revs)) # print "%.2f usec/rev" % usec_per_rev # Print instead of testing #class GitMemoryUsageTestCase(unittest.TestCase): # """Memory test. Not really a unit test. # Not self-contained: Needs a git repository and prints memory usage # instead of testing anything. # TODO: Move to a profiling script?""" # # def test_memory_usage(self): # import logging # import sys # # # custom linux hack reading `/proc//statm` # if sys.platform == 'linux2': # __pagesize = os.sysconf('SC_PAGESIZE') # # def proc_statm(pid = os.getpid()): # __proc_statm = '/proc/%d/statm' % pid # try: # t = open(__proc_statm) # result = t.read().split() # t.close() # assert len(result) == 7 # return tuple([ __pagesize*int(p) for p in result ]) # except: # raise RuntimeError("failed to get memory stats") # # else: # not linux2 # print "WARNING - meminfo.proc_statm() not available" # def proc_statm(): # return (0,)*7 # # print "statm =", proc_statm() # __data_size = proc_statm()[5] # __data_size_last = [__data_size] # # def print_data_usage(): # __tmp = proc_statm()[5] # print "DATA: %6d %+6d" % (__tmp - __data_size, # __tmp - __data_size_last[0]) # __data_size_last[0] = __tmp # # print_data_usage() # # g = Storage(path_to_repo, logging) # Need a git repository path here # # print_data_usage() # # print "[%s]" % g.head() # print g.ls_tree(g.head()) # print "--------------" # print_data_usage() # print g.read_commit(g.head()) # print "--------------" # print_data_usage() # p = g.parents(g.head()) # print list(p) # print "--------------" # print list(g.children(list(p)[0])) # print list(g.children(list(p)[0])) # print "--------------" # print g.get_commit_encoding() # print "--------------" # print g.get_branches() # print "--------------" # print g.hist_prev_revision(g.oldest_rev()), g.oldest_rev(), \ # g.hist_next_revision(g.oldest_rev()) # print_data_usage() # print "--------------" # p = g.youngest_rev() # print g.hist_prev_revision(p), p, g.hist_next_revision(p) # print "--------------" # # p = g.head() # for i in range(-5, 5): # print i, g.history_relative_rev(p, i) # # # check for loops # def check4loops(head): # print "check4loops", head # seen = set([head]) # for _sha in g.children_recursive(head): # if _sha in seen: # print "dupe detected :-/", _sha, len(seen) # seen.add(_sha) # return seen # # print len(check4loops(g.parents(g.head())[0])) # # #p = g.head() # #revs = [ g.history_relative_rev(p, i) for i in range(0,10) ] # print_data_usage() # revs = g.get_commits().keys() # print_data_usage() # # #print len(check4loops(g.oldest_rev())) # #print len(list(g.children_recursive(g.oldest_rev()))) # # print_data_usage() # # # perform typical trac operations: # # if 1: # print "--------------" # rev = g.head() # for mode, _type, sha, _size, name in g.ls_tree(rev): # [last_rev] = g.history(rev, name, limit=1) # s = g.get_obj_size(sha) if _type == 'blob' else 0 # msg = g.read_commit(last_rev) # # print "%s %s %10d [%s]" % (_type, last_rev, s, name) # # print "allocating 2nd instance" # print_data_usage() # g2 = Storage(path_to_repo, logging) # Need a git repository path here # g2.head() # print_data_usage() # # print "allocating 3rd instance" # g3 = Storage(path_to_repo, logging) # Need a git repository path here # g3.head() # print_data_usage() def suite(): suite = unittest.TestSuite() git = locate("git") if git: suite.addTest(unittest.makeSuite(GitTestCase, 'test')) suite.addTest(unittest.makeSuite(TestParseCommit, 'test')) if os.name != 'nt': # Popen doesn't accept unicode path and arguments on Windows suite.addTest(unittest.makeSuite(UnicodeNameTestCase, 'test')) else: print("SKIP: tracopt/versioncontrol/git/tests/PyGIT.py (git cli " "binary, 'git', not found)") return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/tracopt/versioncontrol/git/tests/__init__.py0000644000175200017520000000132612102610255022176 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2012 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. import unittest from tracopt.versioncontrol.git.tests import PyGIT def suite(): suite = unittest.TestSuite() suite.addTest(PyGIT.suite()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/tracopt/versioncontrol/git/__init__.py0000644000175200017520000000000012102610255021020 0ustar cbooscboosTrac-1.0.1/tracopt/versioncontrol/git/git_fs.py0000644000175200017520000006362312102610255020560 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2012 Edgewall Software # Copyright (C) 2006-2011, Herbert Valerio Riedel # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. from __future__ import with_statement from datetime import datetime import os import sys from genshi.builder import tag from trac.config import BoolOption, IntOption, PathOption, Option from trac.core import * from trac.util import TracError, shorten_line from trac.util.datefmt import FixedOffset, to_timestamp, format_datetime from trac.util.text import to_unicode from trac.versioncontrol.api import Changeset, Node, Repository, \ IRepositoryConnector, NoSuchChangeset, \ NoSuchNode, IRepositoryProvider from trac.versioncontrol.cache import CachedRepository, CachedChangeset from trac.versioncontrol.web_ui import IPropertyRenderer from trac.web.chrome import Chrome from trac.wiki import IWikiSyntaxProvider from tracopt.versioncontrol.git import PyGIT class GitCachedRepository(CachedRepository): """Git-specific cached repository. Passes through {display,short,normalize}_rev """ def display_rev(self, rev): return self.short_rev(rev) def short_rev(self, path): return self.repos.short_rev(path) def normalize_rev(self, rev): if not rev: return self.repos.get_youngest_rev() normrev = self.repos.git.verifyrev(rev) if normrev is None: raise NoSuchChangeset(rev) return normrev def get_changeset(self, rev): return GitCachedChangeset(self, self.normalize_rev(rev), self.env) class GitCachedChangeset(CachedChangeset): """Git-specific cached changeset. Handles get_branches() """ def get_branches(self): _rev = self.rev return [(k, v == _rev) for k, v in self.repos.repos.git.get_branch_contains(_rev, resolve=True)] def _last_iterable(iterable): """helper for detecting last iteration in for-loop""" i = iter(iterable) v = i.next() for nextv in i: yield False, v v = nextv yield True, v def intersperse(sep, iterable): """The 'intersperse' generator takes an element and an iterable and intersperses that element between the elements of the iterable. inspired by Haskell's ``Data.List.intersperse`` """ for i, item in enumerate(iterable): if i: yield sep yield item # helper def _parse_user_time(s): """Parse author or committer attribute lines and return corresponding ``(user, timestamp)`` pair. """ user, time, tz_str = s.rsplit(None, 2) tz = FixedOffset((int(tz_str) * 6) / 10, tz_str) time = datetime.fromtimestamp(float(time), tz) return user, time class GitConnector(Component): implements(IRepositoryConnector, IWikiSyntaxProvider) def __init__(self): self._version = None try: self._version = PyGIT.Storage.git_version(git_bin=self.git_bin) except PyGIT.GitError, e: self.log.error("GitError: " + str(e)) if self._version: self.log.info("detected GIT version %s" % self._version['v_str']) self.env.systeminfo.append(('GIT', self._version['v_str'])) if not self._version['v_compatible']: self.log.error("GIT version %s installed not compatible" "(need >= %s)" % (self._version['v_str'], self._version['v_min_str'])) # IWikiSyntaxProvider methods def _format_sha_link(self, formatter, sha, label): # FIXME: this function needs serious rethinking... reponame = '' context = formatter.context while context: if context.resource.realm in ('source', 'changeset'): reponame = context.resource.parent.id break context = context.parent try: repos = self.env.get_repository(reponame) if not repos: raise Exception("Repository '%s' not found" % reponame) sha = repos.normalize_rev(sha) # in case it was abbreviated changeset = repos.get_changeset(sha) return tag.a(label, class_='changeset', title=shorten_line(changeset.message), href=formatter.href.changeset(sha, repos.reponame)) except Exception, e: return tag.a(label, class_='missing changeset', title=to_unicode(e), rel='nofollow') def get_wiki_syntax(self): yield (r'(?:\b|!)r?[0-9a-fA-F]{%d,40}\b' % self.wiki_shortrev_len, lambda fmt, sha, match: self._format_sha_link(fmt, sha.startswith('r') and sha[1:] or sha, sha)) def get_link_resolvers(self): yield ('sha', lambda fmt, _, sha, label, match=None: self._format_sha_link(fmt, sha, label)) # IRepositoryConnector methods persistent_cache = BoolOption('git', 'persistent_cache', 'false', """Enable persistent caching of commit tree.""") cached_repository = BoolOption('git', 'cached_repository', 'false', """Wrap `GitRepository` in `CachedRepository`.""") shortrev_len = IntOption('git', 'shortrev_len', 7, """The length at which a sha1 should be abbreviated to (must be >= 4 and <= 40). """) wiki_shortrev_len = IntOption('git', 'wikishortrev_len', 40, """The minimum length of an hex-string for which auto-detection as sha1 is performed (must be >= 4 and <= 40). """) trac_user_rlookup = BoolOption('git', 'trac_user_rlookup', 'false', """Enable reverse mapping of git email addresses to trac user ids (costly if you have many users).""") use_committer_id = BoolOption('git', 'use_committer_id', 'true', """Use git-committer id instead of git-author id for the changeset ''Author'' field. """) use_committer_time = BoolOption('git', 'use_committer_time', 'true', """Use git-committer timestamp instead of git-author timestamp for the changeset ''Timestamp'' field. """) git_fs_encoding = Option('git', 'git_fs_encoding', 'utf-8', """Define charset encoding of paths within git repositories.""") git_bin = Option('git', 'git_bin', 'git', """Path to the git executable.""") def get_supported_types(self): yield ('git', 8) def get_repository(self, type, dir, params): """GitRepository factory method""" assert type == 'git' if not (4 <= self.shortrev_len <= 40): raise TracError("[git] shortrev_len setting must be within [4..40]") if not (4 <= self.wiki_shortrev_len <= 40): raise TracError("[git] wikishortrev_len must be within [4..40]") if not self._version: raise TracError("GIT backend not available") elif not self._version['v_compatible']: raise TracError("GIT version %s installed not compatible" "(need >= %s)" % (self._version['v_str'], self._version['v_min_str'])) if self.trac_user_rlookup: def rlookup_uid(email): """Reverse map 'real name ' addresses to trac user ids. :return: `None` if lookup failed """ try: _, email = email.rsplit('<', 1) email, _ = email.split('>', 1) email = email.lower() except Exception: return None for _uid, _name, _email in self.env.get_known_users(): try: if email == _email.lower(): return _uid except Exception: continue else: def rlookup_uid(_): return None repos = GitRepository(dir, params, self.log, persistent_cache=self.persistent_cache, git_bin=self.git_bin, git_fs_encoding=self.git_fs_encoding, shortrev_len=self.shortrev_len, rlookup_uid=rlookup_uid, use_committer_id=self.use_committer_id, use_committer_time=self.use_committer_time, ) if self.cached_repository: repos = GitCachedRepository(self.env, repos, self.log) self.log.debug("enabled CachedRepository for '%s'" % dir) else: self.log.debug("disabled CachedRepository for '%s'" % dir) return repos class CsetPropertyRenderer(Component): implements(IPropertyRenderer) # relied upon by GitChangeset def match_property(self, name, mode): # default renderer has priority 1 return (name in ('Parents', 'Children', 'Branches', 'git-committer', 'git-author', ) and mode == 'revprop') and 4 or 0 def render_property(self, name, mode, context, props): def sha_link(sha, label=None): # sha is assumed to be a non-abbreviated 40-chars sha id try: reponame = context.resource.parent.id repos = self.env.get_repository(reponame) cset = repos.get_changeset(sha) if label is None: label = repos.display_rev(sha) return tag.a(label, class_='changeset', title=shorten_line(cset.message), href=context.href.changeset(sha, repos.reponame)) except Exception, e: return tag.a(sha, class_='missing changeset', title=to_unicode(e), rel='nofollow') if name == 'Branches': branches = props[name] # simple non-merge commit return tag(*intersperse(', ', (sha_link(rev, label) for label, rev in branches))) elif name in ('Parents', 'Children'): revs = props[name] # list of commit ids if name == 'Parents' and len(revs) > 1: # we got a merge... current_sha = context.resource.id reponame = context.resource.parent.id parent_links = intersperse(', ', \ ((sha_link(rev), ' (', tag.a('diff', title="Diff against this parent (show the " \ "changes merged from the other parents)", href=context.href.changeset(current_sha, reponame, old=rev)), ')') for rev in revs)) return tag(list(parent_links), tag.br(), tag.span(tag("Note: this is a ", tag.strong("merge"), " changeset, " "the changes displayed below " "correspond to the merge itself."), class_='hint'), tag.br(), tag.span(tag("Use the ", tag.tt("(diff)"), " links above to see all the changes " "relative to each parent."), class_='hint')) # simple non-merge commit return tag(*intersperse(', ', map(sha_link, revs))) elif name in ('git-committer', 'git-author'): user_, time_ = props[name] _str = "%s (%s)" % ( Chrome(self.env).format_author(context.req, user_), format_datetime(time_, tzinfo=context.req.tz)) return unicode(_str) raise TracError("Internal error") class GitRepository(Repository): """Git repository""" def __init__(self, path, params, log, persistent_cache=False, git_bin='git', git_fs_encoding='utf-8', shortrev_len=7, rlookup_uid=lambda _: None, use_committer_id=False, use_committer_time=False, ): self.logger = log self.gitrepo = path self.params = params self.shortrev_len = max(4, min(shortrev_len, 40)) self.rlookup_uid = rlookup_uid self.use_committer_time = use_committer_time self.use_committer_id = use_committer_id try: self.git = PyGIT.StorageFactory(path, log, not persistent_cache, git_bin=git_bin, git_fs_encoding=git_fs_encoding) \ .getInstance() except PyGIT.GitError, e: raise TracError("%s does not appear to be a Git " "repository." % path) Repository.__init__(self, 'git:'+path, self.params, log) def close(self): self.git = None def get_youngest_rev(self): return self.git.youngest_rev() def get_oldest_rev(self): return self.git.oldest_rev() def normalize_path(self, path): return path and path.strip('/') or '/' def normalize_rev(self, rev): if not rev: return self.get_youngest_rev() normrev = self.git.verifyrev(rev) if normrev is None: raise NoSuchChangeset(rev) return normrev def display_rev(self, rev): return self.short_rev(rev) def short_rev(self, rev): return self.git.shortrev(self.normalize_rev(rev), min_len=self.shortrev_len) def get_node(self, path, rev=None, historian=None): return GitNode(self, path, rev, self.log, None, historian) def get_quickjump_entries(self, rev): for bname, bsha in self.git.get_branches(): yield 'branches', bname, '/', bsha for t in self.git.get_tags(): yield 'tags', t, '/', t def get_path_url(self, path, rev): return self.params.get('url') def get_changesets(self, start, stop): for rev in self.git.history_timerange(to_timestamp(start), to_timestamp(stop)): yield self.get_changeset(rev) def get_changeset(self, rev): """GitChangeset factory method""" return GitChangeset(self, rev) def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=0): # TODO: handle renames/copies, ignore_ancestry if old_path != new_path: raise TracError("not supported in git_fs") with self.git.get_historian(old_rev, old_path.strip('/')) as old_historian: with self.git.get_historian(new_rev, new_path.strip('/')) as new_historian: for chg in self.git.diff_tree(old_rev, new_rev, self.normalize_path(new_path)): mode1, mode2, obj1, obj2, action, path, path2 = chg kind = Node.FILE if mode2.startswith('04') or mode1.startswith('04'): kind = Node.DIRECTORY change = GitChangeset.action_map[action] old_node = None new_node = None if change != Changeset.ADD: old_node = self.get_node(path, old_rev, old_historian) if change != Changeset.DELETE: new_node = self.get_node(path, new_rev, new_historian) yield old_node, new_node, kind, change def next_rev(self, rev, path=''): return self.git.hist_next_revision(rev) def previous_rev(self, rev, path=''): return self.git.hist_prev_revision(rev) def parent_revs(self, rev): return self.git.parents(rev) def child_revs(self, rev): return self.git.children(rev) def rev_older_than(self, rev1, rev2): rc = self.git.rev_is_anchestor_of(rev1, rev2) return rc # def clear(self, youngest_rev=None): # self.youngest = None # if youngest_rev is not None: # self.youngest = self.normalize_rev(youngest_rev) # self.oldest = None def clear(self, youngest_rev=None): self.sync() def sync(self, rev_callback=None, clean=None): if rev_callback: revs = set(self.git.all_revs()) if not self.git.sync(): return None # nothing expected to change if rev_callback: revs = set(self.git.all_revs()) - revs for rev in revs: rev_callback(rev) class GitNode(Node): def __init__(self, repos, path, rev, log, ls_tree_info=None, historian=None): self.log = log self.repos = repos self.fs_sha = None # points to either tree or blobs self.fs_perm = None self.fs_size = None rev = rev and str(rev) or 'HEAD' kind = Node.DIRECTORY p = path.strip('/') if p: # ie. not the root-tree if not ls_tree_info: ls_tree_info = repos.git.ls_tree(rev, p) or None if ls_tree_info: [ls_tree_info] = ls_tree_info if not ls_tree_info: raise NoSuchNode(path, rev) self.fs_perm, k, self.fs_sha, self.fs_size, _ = ls_tree_info # fix-up to the last commit-rev that touched this node rev = repos.git.last_change(rev, p, historian) if k == 'tree': pass elif k == 'commit': # FIXME: this is a workaround for missing git submodule # support in the plugin pass elif k == 'blob': kind = Node.FILE else: raise TracError("Internal error (got unexpected object " \ "kind '%s')" % k) self.created_path = path self.created_rev = rev Node.__init__(self, repos, path, rev, kind) def __git_path(self): """return path as expected by PyGIT""" p = self.path.strip('/') if self.isfile: assert p return p if self.isdir: return p and (p + '/') raise TracError("internal error") def get_content(self): if not self.isfile: return None return self.repos.git.get_file(self.fs_sha) def get_properties(self): return self.fs_perm and {'mode': self.fs_perm } or {} def get_annotations(self): if not self.isfile: return return [rev for rev, lineno in \ self.repos.git.blame(self.rev,self.__git_path())] def get_entries(self): if not self.isdir: return with self.repos.git.get_historian(self.rev, self.path.strip('/')) as historian: for ent in self.repos.git.ls_tree(self.rev, self.__git_path()): yield GitNode(self.repos, ent[-1], self.rev, self.log, ent, historian) def get_content_type(self): if self.isdir: return None return '' def get_content_length(self): if not self.isfile: return None if self.fs_size is None: self.fs_size = self.repos.git.get_obj_size(self.fs_sha) return self.fs_size def get_history(self, limit=None): # TODO: find a way to follow renames/copies for is_last, rev in _last_iterable(self.repos.git.history(self.rev, self.__git_path(), limit)): yield (self.path, rev, Changeset.EDIT if not is_last else Changeset.ADD) def get_last_modified(self): if not self.isfile: return None try: msg, props = self.repos.git.read_commit(self.rev) user, ts = _parse_user_time(props['committer'][0]) except: self.log.error("internal error (could not get timestamp from " "commit '%s')" % self.rev) return None return ts class GitChangeset(Changeset): """A Git changeset in the Git repository. Corresponds to a Git commit blob. """ action_map = { # see also git-diff-tree(1) --diff-filter 'A': Changeset.ADD, 'M': Changeset.EDIT, # modified 'T': Changeset.EDIT, # file type (mode) change 'D': Changeset.DELETE, 'R': Changeset.MOVE, # renamed 'C': Changeset.COPY } # TODO: U, X, B def __init__(self, repos, sha): if sha is None: raise NoSuchChangeset(sha) try: msg, props = repos.git.read_commit(sha) except PyGIT.GitErrorSha: raise NoSuchChangeset(sha) self.props = props assert 'children' not in props _children = list(repos.git.children(sha)) if _children: props['children'] = _children committer, author = self._get_committer_and_author() # use 1st author/committer as changeset owner/timestamp c_user = a_user = c_time = a_time = None if committer: c_user, c_time = _parse_user_time(committer) if author: a_user, a_time = _parse_user_time(author) if repos.use_committer_time: time = c_time or a_time else: time = a_time or c_time if repos.use_committer_id: user = c_user or a_user else: user = a_user or c_user # try to resolve email address to trac uid user = repos.rlookup_uid(user) or user Changeset.__init__(self, repos, rev=sha, message=msg, author=user, date=time) def _get_committer_and_author(self): committer = author = None if 'committer' in self.props: committer = self.props['committer'][0] if 'author' in self.props: author = self.props['author'][0] return committer, author def get_properties(self): properties = {} if 'parent' in self.props: properties['Parents'] = self.props['parent'] if 'children' in self.props: properties['Children'] = self.props['children'] committer, author = self._get_committer_and_author() if author != committer: properties['git-committer'] = _parse_user_time(committer) properties['git-author'] = _parse_user_time(author) branches = list(self.repos.git.get_branch_contains(self.rev, resolve=True)) if branches: properties['Branches'] = branches return properties def get_changes(self): paths_seen = set() for parent in self.props.get('parent', [None]): for mode1, mode2, obj1, obj2, action, path1, path2 in \ self.repos.git.diff_tree(parent, self.rev, find_renames=True): path = path2 or path1 p_path, p_rev = path1, parent kind = Node.FILE if mode2.startswith('04') or mode1.startswith('04'): kind = Node.DIRECTORY action = GitChangeset.action_map[action[0]] if action == Changeset.ADD: p_path = '' p_rev = None # CachedRepository expects unique (rev, path, change_type) key # this is only an issue in case of merges where files required # editing if path in paths_seen: continue paths_seen.add(path) yield path, kind, action, p_path, p_rev def get_branches(self): _rev = self.rev return [(k, v == _rev) for k, v in self.repos.git.get_branch_contains(_rev, resolve=True)] class GitwebProjectsRepositoryProvider(Component): implements(IRepositoryProvider) projects_list = PathOption('git', 'projects_list', doc= """Path to a gitweb-formatted projects.list""") projects_base = PathOption('git', 'projects_base', doc= """Path to the base of your git projects""") projects_url = Option('git', 'projects_url', doc= """Template for project URLs. %s will be replaced with the repo name""") def get_repositories(self): if not self.projects_list: return for line in open(self.projects_list): line = line.strip() name = line if name.endswith('.git'): name = name[:-4] repo = { 'dir': os.path.join(self.projects_base, line), 'type': 'git', } description_path = os.path.join(repo['dir'], 'description') if os.path.exists(description_path): repo['description'] = open(description_path).read().strip() if self.projects_url: repo['url'] = self.projects_url % name yield name, repo Trac-1.0.1/tracopt/versioncontrol/__init__.py0000644000175200017520000000000012102610255020235 0ustar cbooscboosTrac-1.0.1/tracopt/mimeview/0000755000175200017520000000000012102610436014673 5ustar cbooscboosTrac-1.0.1/tracopt/mimeview/silvercity.py0000644000175200017520000001401112102610255017436 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2004-2009 Edgewall Software # Copyright (C) 2004 Daniel Lundin # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Daniel Lundin """Syntax highlighting module, based on the SilverCity module. Get it at: http://silvercity.sourceforge.net/ """ import re from StringIO import StringIO from genshi.core import Markup from trac.core import * from trac.config import ListOption from trac.env import ISystemInfoProvider from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview from trac.util import get_pkginfo try: import SilverCity have_silvercity = True except ImportError: have_silvercity = False __all__ = ['SilverCityRenderer'] types = { 'text/css': ('CSS', 3), 'text/html': ('HyperText', 3, {'asp.default.language':1}), 'application/xml': ('XML', 3), 'application/xhtml+xml': ('HyperText', 3, {'asp.default.language':1}), 'application/rss+xml': ('HyperText', 3, {'asp.default.language':1}), 'application/x-yaml': ('YAML', 3), 'text/x-yaml': ('YAML', 3), 'application/x-javascript': ('CPP', 3), # Kludgy. 'text/x-asp': ('HyperText', 3, {'asp.default.language':2}), 'text/x-c++hdr': ('CPP', 3), 'text/x-c++src': ('CPP', 3), 'text/x-chdr': ('CPP', 3), 'text/x-csrc': ('CPP', 3), 'text/x-perl': ('Perl', 3), 'text/x-php': ('HyperText', 3, {'asp.default.language': 4}), 'application/x-httpd-php': ('HyperText', 3, {'asp.default.language': 4}), 'application/x-httpd-php4': ('HyperText', 3, {'asp.default.language': 4}), 'application/x-httpd-php3': ('HyperText', 3, {'asp.default.language': 4}), 'text/x-java': ('Java', 3), 'text/x-javascript': ('CPP', 3), # Kludgy. 'text/x-psp': ('HyperText', 3, {'asp.default.language': 3}), 'text/x-python': ('Python', 3), 'text/x-ruby': ('Ruby', 3), 'text/x-sql': ('SQL', 3), 'text/x-verilog': ('Verilog', 3), 'text/xml': ('XML', 3), 'text/xslt': ('XSLT', 3), 'image/svg+xml': ('XML', 3) } CRLF_RE = re.compile('\r$', re.MULTILINE) class SilverCityRenderer(Component): """Syntax highlighting based on SilverCity.""" implements(ISystemInfoProvider, IHTMLPreviewRenderer) silvercity_modes = ListOption('mimeviewer', 'silvercity_modes', '', doc= """List of additional MIME types known by SilverCity. For each, a tuple `mimetype:mode:quality` has to be specified, where `mimetype` is the MIME type, `mode` is the corresponding SilverCity mode to be used for the conversion and `quality` is the quality ratio associated to this conversion. That can also be used to override the default quality ratio used by the SilverCity render, which is 3 (''since 0.10'').""") expand_tabs = True returns_source = True def __init__(self): self._types = None # ISystemInfoProvider methods def get_system_info(self): if have_silvercity: yield 'SilverCity', get_pkginfo(SilverCity).get('version', '?') # TODO: the above works only if setuptools was used to build # SilverCity, which is not yet the case by default for 0.9.7. # I've not been able to find an alternative way to get version. # IHTMLPreviewRenderer methods def get_quality_ratio(self, mimetype): # Extend default MIME type to mode mappings with configured ones if not have_silvercity: return 0 if not self._types: self._types = {} self._types.update(types) self._types.update( Mimeview(self.env).configured_modes_mapping('silvercity')) return self._types.get(mimetype, (None, 0))[1] def render(self, context, mimetype, content, filename=None, rev=None): try: mimetype = mimetype.split(';', 1)[0] typelang = self._types[mimetype] lang = typelang[0] module = getattr(SilverCity, lang) generator = getattr(module, lang + "HTMLGenerator") try: allprops = typelang[2] propset = SilverCity.PropertySet() for p in allprops.keys(): propset[p] = allprops[p] except IndexError: pass except (KeyError, AttributeError): err = "No SilverCity lexer found for mime-type '%s'." % mimetype raise Exception, err # SilverCity does not like unicode strings content = content.encode('utf-8') # SilverCity generates extra empty line against some types of # the line such as comment or #include with CRLF. So we # standardize to LF end-of-line style before call. content = CRLF_RE.sub('', content) buf = StringIO() generator().generate_html(buf, content) br_re = re.compile(r'$', re.MULTILINE) span_default_re = re.compile(r'(.*?)', re.DOTALL) html = span_default_re.sub(r'\1', br_re.sub('', buf.getvalue())) # Convert the output back to a unicode string html = html.decode('utf-8') # SilverCity generates _way_ too many non-breaking spaces... # We don't need them anyway, so replace them by normal spaces return [Markup(line) for line in html.replace(' ', ' ').splitlines()] Trac-1.0.1/tracopt/mimeview/enscript.py0000644000175200017520000001373312102610255017102 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2004-2009 Edgewall Software # Copyright (C) 2004 Daniel Lundin # Copyright (C) 2005 Christopher Lenz # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Daniel Lundin from genshi.core import Markup from trac.config import Option, ListOption from trac.core import * from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview from trac.util import NaivePopen from trac.util.html import Deuglifier __all__ = ['EnscriptRenderer'] types = { 'application/xhtml+xml': ('html', 2), 'application/postscript': ('postscript', 2), 'application/x-csh': ('csh', 2), 'application/x-javascript': ('javascript', 2), 'application/x-troff': ('nroff', 2), 'text/html': ('html', 2), 'text/x-ada': ('ada', 2), 'text/x-asm': ('asm', 2), 'text/x-awk': ('awk', 2), 'text/x-c++src': ('cpp', 2), 'text/x-c++hdr': ('cpp', 2), 'text/x-chdr': ('c', 2), 'text/x-csh': ('csh', 2), 'text/x-csrc': ('c', 2), 'text/x-diff': ('diffu', 2), # Assume unified diff (works otherwise) 'text/x-eiffel': ('eiffel', 2), 'text/x-elisp': ('elisp', 2), 'text/x-fortran': ('fortran', 2), 'text/x-haskell': ('haskell', 2), 'text/x-idl': ('idl', 2), 'text/x-inf': ('inf', 2), 'text/x-java': ('java', 2), 'text/x-javascript': ('javascript', 2), 'text/x-ksh': ('ksh', 2), 'text/x-lua': ('lua', 2), 'text/x-m4': ('m4', 2), 'text/x-makefile': ('makefile', 2), 'text/x-mail': ('mail', 2), 'text/x-matlab': ('matlab', 2), 'text/x-objc': ('objc', 2), 'text/x-pascal': ('pascal', 2), 'text/x-perl': ('perl', 2), 'text/x-pyrex': ('pyrex', 2), 'text/x-python': ('python', 2), 'text/x-rfc': ('rfc', 2), 'text/x-ruby': ('ruby', 2), 'text/x-sh': ('sh', 2), 'text/x-scheme': ('scheme', 2), 'text/x-sql': ('sql', 2), 'text/x-tcl': ('tcl', 2), 'text/x-tex': ('tex', 2), 'text/x-vba': ('vba', 2), 'text/x-verilog': ('verilog', 2), 'text/x-vhdl': ('vhdl', 2), 'model/vrml': ('vrml', 2), 'application/x-sh': ('sh', 2), 'text/x-zsh': ('zsh', 2), 'text/vnd.wap.wmlscript': ('wmlscript', 2), } class EnscriptDeuglifier(Deuglifier): @classmethod def rules(cls): return [ r'(?P)', r'(?P)', r'(?P)', r'(?P)', r'(?P)', r'(?P)', r'(?P)', r'(?P)', r'(?P)', r'(?P)' ] class EnscriptRenderer(Component): """Syntax highlighter using GNU Enscript.""" implements(IHTMLPreviewRenderer) expand_tabs = True returns_source = True path = Option('mimeviewer', 'enscript_path', 'enscript', """Path to the Enscript executable.""") enscript_modes = ListOption('mimeviewer', 'enscript_modes', 'text/x-dylan:dylan:4', doc= """List of additional MIME types known by Enscript. For each, a tuple `mimetype:mode:quality` has to be specified, where `mimetype` is the MIME type, `mode` is the corresponding Enscript mode to be used for the conversion and `quality` is the quality ratio associated to this conversion. That can also be used to override the default quality ratio used by the Enscript render, which is 2 (''since 0.10'').""") def __init__(self): self._types = None # IHTMLPreviewRenderer methods def get_quality_ratio(self, mimetype): # Extend default MIME type to mode mappings with configured ones if not self._types: self._types = {} self._types.update(types) self._types.update( Mimeview(self.env).configured_modes_mapping('enscript')) return self._types.get(mimetype, (None, 0))[1] def render(self, context, mimetype, content, filename=None, rev=None): cmdline = self.path mimetype = mimetype.split(';', 1)[0] # strip off charset mode = self._types[mimetype][0] cmdline += ' --color -h -q --language=html -p - -E%s' % mode self.log.debug("Enscript command line: %s" % cmdline) np = NaivePopen(cmdline, content.encode('utf-8'), capturestderr=1) if np.errorlevel or np.err: self.env.disable_component(self) err = "Running enscript failed with (%s, %s), disabling " \ "EnscriptRenderer (command: '%s')" \ % (np.errorlevel, np.err.strip(), cmdline) raise Exception(err) odata = np.out # Strip header and footer i = odata.find('
')
        beg = i > 0 and i + 6
        i = odata.rfind('
') end = i if i > 0 else len(odata) odata = EnscriptDeuglifier().format(odata[beg:end].decode('utf-8')) return [Markup(line) for line in odata.splitlines()] Trac-1.0.1/tracopt/mimeview/php.py0000644000175200017520000001011112102610255016025 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2005-2009 Edgewall Software # Copyright (C) 2005 Christian Boos # Copyright (C) 2005 Christopher Lenz # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Christian Boos # Christopher Lenz import os import re from genshi.core import Markup from trac.core import * from trac.config import Option from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode from trac.util import NaivePopen from trac.util.html import Deuglifier from trac.util.translation import _ __all__ = ['PHPRenderer'] php_types = ('text/x-php', 'application/x-httpd-php', 'application/x-httpd-php4', 'application/x-httpd-php1') class PhpDeuglifier(Deuglifier): def format(self, indata): # The PHP highlighter produces the end-span tags on the next line # instead of the line they actually apply to, which causes # Trac to produce lots of (useless) open-and-immediately-close # spans beginning each line. This tries to curtail by bubbling # the first span after a set of 1+ "
" to before them. r_fixeol = re.compile(r"((?:
)+)()") indata = r_fixeol.sub(lambda m: m.group(2) + m.group(1), indata) # Now call superclass implementation that handles the dirty work # of applying css classes. return Deuglifier.format(self, indata) @classmethod def rules(cls): colors = dict(comment='FF8000', lang='0000BB', keyword='007700', string='DD0000') # rules check for for PHP 4 or for PHP 5 return [r'(?P<%s><(?:font color="|span style="color: )#%s">)' % c for c in colors.items() ] + [r'(?P)', r'(?P)'] class PHPRenderer(Component): """Syntax highlighter using the PHP executable.""" implements(IHTMLPreviewRenderer) path = Option('mimeviewer', 'php_path', 'php', """Path to the PHP executable (''since 0.9'').""") returns_source = True # IHTMLPreviewRenderer methods def get_quality_ratio(self, mimetype): if mimetype in php_types: return 5 return 0 def render(self, context, mimetype, content, filename=None, rev=None): # -n to ignore php.ini so we're using default colors cmdline = '%s -sn' % self.path self.log.debug("PHP command line: %s" % cmdline) content = content_to_unicode(self.env, content, mimetype) content = content.encode('utf-8') np = NaivePopen(cmdline, content, capturestderr=1) if (os.name != 'nt' and np.errorlevel) or np.err: msg = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, np.err) raise Exception(msg) odata = ''.join(np.out.splitlines()[1:-1]) if odata.startswith('X-Powered-By:') or \ odata.startswith('Content-type:'): raise TracError(_('You appear to be using the PHP CGI ' 'binary. Trac requires the CLI version ' 'for syntax highlighting.')) epilogues = ["", ""] for e in epilogues: if odata.endswith(e): odata = odata[:-len(e)] break html = PhpDeuglifier().format(odata.decode('utf-8')) # PHP generates _way_ too many non-breaking spaces... # We don't need them anyway, so replace them by normal spaces return [Markup(line.replace(' ', ' ')) for line in html.split('
')] Trac-1.0.1/tracopt/mimeview/tests/0000755000175200017520000000000012102610436016035 5ustar cbooscboosTrac-1.0.1/tracopt/mimeview/tests/php.py0000644000175200017520000001362712102610255017206 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C)2006-2009 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. from StringIO import StringIO import unittest from trac.mimeview.api import Mimeview from trac.test import EnvironmentStub, locate from tracopt.mimeview.php import PhpDeuglifier, PHPRenderer class PhpDeuglifierTestCase(unittest.TestCase): def test_nomarkup(self): self.assertEqual('asd', PhpDeuglifier().format('asd')) def test_rewrite_span(self): self.assertEqual('asd', PhpDeuglifier().format('asd')) self.assertEqual('asd', PhpDeuglifier().format('asd')) self.assertEqual('asd', PhpDeuglifier().format('asd')) self.assertEqual('asd', PhpDeuglifier().format('asd')) def test_rewrite_font(self): self.assertEqual('asd', PhpDeuglifier().format('asd')) self.assertEqual('asd', PhpDeuglifier().format('asd')) self.assertEqual('asd', PhpDeuglifier().format('asd')) self.assertEqual('asd', PhpDeuglifier().format('asd')) def test_reorder_br(self): """ Regression test for #3326 point 2 (close tags after line break) """ self.assertEqual('
', PhpDeuglifier().format( '
')) self.assertEqual('

', PhpDeuglifier().format( '

')) class PhpRendererTestCase(unittest.TestCase): def _test_render(self, stuff, type="string"): env = EnvironmentStub(enable=[PHPRenderer]) m = Mimeview(env) r = m.renderers[0] if type == "string": s = stuff elif type == "file": s = StringIO(stuff) else: raise NotImplementedException( "Pass either type=file or type=string") result = list(r.render(None, None, s)) return result def test_boring_string(self): """ Simple regression test for #3624 (php chops off the last line) """ result = self._test_render('asd') self.assertEqual('asd', result[0]) self.assertEqual(1, len(result)) def test_boring_filelike(self): """ Regression test for #3261 (treats content as string) # FIXME see #3332 """ result = self._test_render('asd', 'file') self.assertEqual('asd', result[0]) self.assertEqual(1, len(result)) def test_simple_string(self): result = self._test_render('') self.assertEqual('<?php', result[0]) self.assertEqual('?>', result[1]) self.assertEqual(2, len(result)) def test_simple_unicode(self): result = self._test_render(u'') self.assertEqual(u'<?php ' u'echo ' u'"é"' u'; ' u'?>', result[0]) self.assertEqual(1, len(result)) def test_way_too_many_nbsp(self): """ Regression test for a tiny part of #1676 """ result = self._test_render('') self.assertEqual('<?php', result[0]) self.assertEqual(' ?>', result[1]) self.assertEqual(2, len(result)) def test_deuglified_reorder_br(self): """ If the reordering of
and the subsequent orphan
isn't working, the appears at the beginning of the third line instead of the end of the second. """ result = self._test_render('') self.assertEqual('<?php', result[0]) self.assertEqual('$x=' '"asd"' ';', result[1]) self.assertEqual('?>', result[2]) self.assertEqual(3, len(result)) def test_keeps_last_line(self): """ More complex regression test for #3624 (php chops off the last line) """ result = self._test_render('

\n

') self.assertEqual('<p />', result[0]) self.assertEqual('<p />', result[1]) self.assertEqual(2, len(result)) def suite(): suite = unittest.TestSuite() php = locate("php") if php: suite.addTest(unittest.makeSuite(PhpDeuglifierTestCase, 'test')) suite.addTest(unittest.makeSuite(PhpRendererTestCase, 'test')) else: print("SKIP: tracopt/mimeview/tests/php.py (php cli binary, 'php', " "not found)") return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/tracopt/mimeview/tests/__init__.py0000644000175200017520000000131012102610255020140 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2009 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. import unittest from tracopt.mimeview.tests import php def suite(): suite = unittest.TestSuite() suite.addTest(php.suite()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/tracopt/mimeview/__init__.py0000644000175200017520000000000012102610255016771 0ustar cbooscboosTrac-1.0.1/tracopt/perm/0000755000175200017520000000000012102610436014014 5ustar cbooscboosTrac-1.0.1/tracopt/perm/authz_policy.py0000644000175200017520000002176112102610255017106 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2007-2009 Edgewall Software # Copyright (C) 2007 Alec Thomas # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Alec Thomas from fnmatch import fnmatch from itertools import groupby import os from trac.core import * from trac.config import Option from trac.perm import PermissionSystem, IPermissionPolicy ConfigObj = None try: from configobj import ConfigObj except ImportError: pass class AuthzPolicy(Component): """Permission policy using an authz-like configuration file. Refer to SVN documentation for syntax of the authz file. Groups are supported. As the fine-grained permissions brought by this permission policy are often used in complement of the other pemission policies (like the `DefaultPermissionPolicy`), there's no need to redefine all the permissions here. Only additional rights or restrictions should be added. === Installation === Note that this plugin requires the `configobj` package: http://www.voidspace.org.uk/python/configobj.html You should be able to install it by doing a simple `easy_install configobj` Enabling this policy requires listing it in `trac.ini: {{{ [trac] permission_policies = AuthzPolicy, DefaultPermissionPolicy [authz_policy] authz_file = conf/authzpolicy.conf }}} This means that the `AuthzPolicy` permissions will be checked first, and only if no rule is found will the `DefaultPermissionPolicy` be used. === Configuration === The `authzpolicy.conf` file is a `.ini` style configuration file. - Each section of the config is a glob pattern used to match against a Trac resource descriptor. These descriptors are in the form: {{{ :@[/:@ ...] }}} Resources are ordered left to right, from parent to child. If any component is inapplicable, `*` is substituted. If the version pattern is not specified explicitely, all versions (`@*`) is added implicitly Example: Match the WikiStart page {{{ [wiki:*] [wiki:WikiStart*] [wiki:WikiStart@*] [wiki:WikiStart] }}} Example: Match the attachment `wiki:WikiStart@117/attachment/FOO.JPG@*` on WikiStart {{{ [wiki:*] [wiki:WikiStart*] [wiki:WikiStart@*] [wiki:WikiStart@*/attachment/*] [wiki:WikiStart@117/attachment/FOO.JPG] }}} - Sections are checked against the current Trac resource '''IN ORDER''' of appearance in the configuration file. '''ORDER IS CRITICAL'''. - Once a section matches, the current username is matched, '''IN ORDER''', against the keys of the section. If a key is prefixed with a `@`, it is treated as a group. If a key is prefixed with a `!`, the permission is denied rather than granted. The username will match any of 'anonymous', 'authenticated', or '*', using normal Trac permission rules. Example configuration: {{{ [groups] administrators = athomas [*/attachment:*] * = WIKI_VIEW, TICKET_VIEW [wiki:WikiStart@*] @administrators = WIKI_ADMIN anonymous = WIKI_VIEW * = WIKI_VIEW # Deny access to page templates [wiki:PageTemplates/*] * = # Match everything else [*] @administrators = TRAC_ADMIN anonymous = BROWSER_VIEW, CHANGESET_VIEW, FILE_VIEW, LOG_VIEW, MILESTONE_VIEW, POLL_VIEW, REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_VIEW, SEARCH_VIEW, TICKET_CREATE, TICKET_MODIFY, TICKET_VIEW, TIMELINE_VIEW, WIKI_CREATE, WIKI_MODIFY, WIKI_VIEW # Give authenticated users some extra permissions authenticated = REPO_SEARCH, XML_RPC }}} """ implements(IPermissionPolicy) authz_file = Option('authz_policy', 'authz_file', '', 'Location of authz policy configuration file.') authz = None authz_mtime = None # IPermissionPolicy methods def check_permission(self, action, username, resource, perm): if ConfigObj is None: self.log.error('configobj package not found') return None if self.authz_file and not self.authz_mtime or \ os.path.getmtime(self.get_authz_file()) > self.authz_mtime: self.parse_authz() resource_key = self.normalise_resource(resource) self.log.debug('Checking %s on %s', action, resource_key) permissions = self.authz_permissions(resource_key, username) if permissions is None: return None # no match, can't decide elif permissions == ['']: return False # all actions are denied # FIXME: expand all permissions once for all ps = PermissionSystem(self.env) for deny, perms in groupby(permissions, key=lambda p: p.startswith('!')): if deny and action in ps.expand_actions([p[1:] for p in perms]): return False # action is explicitly denied elif action in ps.expand_actions(perms): return True # action is explicitly granted return None # no match for action, can't decide # Internal methods def get_authz_file(self): f = self.authz_file return f if os.path.isabs(f) else os.path.join(self.env.path, f) def parse_authz(self): self.log.debug('Parsing authz security policy %s', self.get_authz_file()) self.authz = ConfigObj(self.get_authz_file(), encoding='utf8') groups = {} for group, users in self.authz.get('groups', {}).iteritems(): if isinstance(users, basestring): users = [users] groups[group] = users self.groups_by_user = {} def add_items(group, items): for item in items: if item.startswith('@'): add_items(group, groups[item[1:]]) else: self.groups_by_user.setdefault(item, set()).add(group) for group, users in groups.iteritems(): add_items('@' + group, users) self.authz_mtime = os.path.getmtime(self.get_authz_file()) def normalise_resource(self, resource): def flatten(resource): if not resource: return ['*:*@*'] if not (resource.realm or resource.id): return ['%s:%s@%s' % (resource.realm or '*', resource.id or '*', resource.version or '*')] # XXX Due to the mixed functionality in resource we can end up with # ticket, ticket:1, ticket:1@10. This code naively collapses all # subsets of the parent resource into one. eg. ticket:1@10 parent = resource.parent while parent and (resource.realm == parent.realm or (resource.realm == parent.realm and resource.id == parent.id)): parent = parent.parent if parent: parent = flatten(parent) else: parent = [] return parent + ['%s:%s@%s' % (resource.realm or '*', resource.id or '*', resource.version or '*')] return '/'.join(flatten(resource)) def authz_permissions(self, resource_key, username): # TODO: Handle permission negation in sections. eg. "if in this # ticket, remove TICKET_MODIFY" if username and username != 'anonymous': valid_users = ['*', 'authenticated', username] else: valid_users = ['*', 'anonymous'] for resource_section in [a for a in self.authz.sections if a != 'groups']: resource_glob = resource_section if '@' not in resource_glob: resource_glob += '@*' if fnmatch(resource_key, resource_glob): section = self.authz[resource_section] for who, permissions in section.iteritems(): if who in valid_users or \ who in self.groups_by_user.get(username, []): self.log.debug('%s matched section %s for user %s', resource_key, resource_glob, username) if isinstance(permissions, basestring): return [permissions] else: return permissions return None Trac-1.0.1/tracopt/perm/tests/0000755000175200017520000000000012102610436015156 5ustar cbooscboosTrac-1.0.1/tracopt/perm/tests/authz_policy.py0000644000175200017520000000526312102610255020247 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2012 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. import os import tempfile import unittest try: from configobj import ConfigObj except ImportError: ConfigObj = None from trac.resource import Resource from trac.test import EnvironmentStub from trac.util import create_file from tracopt.perm.authz_policy import AuthzPolicy class AuthzPolicyTestCase(unittest.TestCase): def setUp(self): tmpdir = os.path.realpath(tempfile.gettempdir()) self.authz_file = os.path.join(tmpdir, 'trac-authz-policy') create_file(self.authz_file, """\ # Unicode user names [groups] administrators = éat [wiki:WikiStart] änon = WIKI_VIEW @administrators = WIKI_VIEW * = # Unicode page names [wiki:résumé] änon = @administrators = WIKI_VIEW * = """) self.env = EnvironmentStub(enable=[AuthzPolicy]) self.env.config.set('authz_policy', 'authz_file', self.authz_file) self.authz_policy = AuthzPolicy(self.env) def tearDown(self): self.env.reset_db() os.remove(self.authz_file) def check_permission(self, action, user, resource, perm): return self.authz_policy.check_permission(action, user, resource, perm) def test_unicode_username(self): resource = Resource('wiki', 'WikiStart') self.assertEqual( False, self.check_permission('WIKI_VIEW', 'anonymous', resource, None)) self.assertEqual( True, self.check_permission('WIKI_VIEW', u'änon', resource, None)) def test_unicode_resource_name(self): resource = Resource('wiki', u'résumé') self.assertEqual( False, self.check_permission('WIKI_VIEW', 'anonymous', resource, None)) self.assertEqual( False, self.check_permission('WIKI_VIEW', u'änon', resource, None)) self.assertEqual( True, self.check_permission('WIKI_VIEW', u'éat', resource, None)) def suite(): suite = unittest.TestSuite() if ConfigObj: suite.addTest(unittest.makeSuite(AuthzPolicyTestCase, 'test')) else: print "SKIP: tracopt/perm/tests/authz_policy.py (no configobj " + \ "installed)" return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/tracopt/perm/tests/__init__.py0000644000175200017520000000132612102610255017270 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2012 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. import unittest from tracopt.perm.tests import authz_policy def suite(): suite = unittest.TestSuite() suite.addTest(authz_policy.suite()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/tracopt/perm/config_perm_provider.py0000644000175200017520000000427112102610255020573 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2009 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. from trac.core import * from trac.config import ConfigSection from trac.perm import IPermissionRequestor class ExtraPermissionsProvider(Component): """Extra permission provider.""" implements(IPermissionRequestor) extra_permissions_section = ConfigSection('extra-permissions', doc="""This section provides a way to add arbitrary permissions to a Trac environment. This can be useful for adding new permissions to use for workflow actions, for example. To add new permissions, create a new section `[extra-permissions]` in your `trac.ini`. Every entry in that section defines a meta-permission and a comma-separated list of permissions. For example: {{{ [extra-permissions] extra_admin = extra_view, extra_modify, extra_delete }}} This entry will define three new permissions `EXTRA_VIEW`, `EXTRA_MODIFY` and `EXTRA_DELETE`, as well as a meta-permissions `EXTRA_ADMIN` that grants all three permissions. If you don't want a meta-permission, start the meta-name with an underscore (`_`): {{{ [extra-permissions] _perms = extra_view, extra_modify }}} """) def get_permission_actions(self): permissions = {} for meta, perms in self.extra_permissions_section.options(): perms = [each.strip().upper() for each in perms.split(',')] for perm in perms: permissions.setdefault(perm, []) meta = meta.strip().upper() if meta and not meta.startswith('_'): permissions.setdefault(meta, []).extend(perms) return [(k, v) if v else k for k, v in permissions.iteritems()] Trac-1.0.1/tracopt/perm/__init__.py0000644000175200017520000000000012102610255016112 0ustar cbooscboosTrac-1.0.1/tracopt/__init__.py0000644000175200017520000000000012102610255015147 0ustar cbooscboosTrac-1.0.1/tracini.cfg0000644000175200017520000000031212102610255013502 0ustar cbooscboos# mapping file for extracting messages of `*Option` and `ConfigSection` from # python files into trac/locale/tracini.pot (see setup.cfg) [python: **.py] [extractors] python = trac.dist:extract_python Trac-1.0.1/.hgeol0000644000175200017520000000010012102610255012462 0ustar cbooscboos[patterns] contrib/trac-svn-hook = LF **.cmd = CRLF ** = native Trac-1.0.1/Makefile.cfg.sample0000644000175200017520000000305012102610255015050 0ustar cbooscboos# -*- Makefile -*- configuration file sample # # Adapt to your local setting and copy to Makefile.cfg # # ---------------------------------------------------------------------------- # Python Installations (select with `python=` on the `make` command line) python.23 = python.24 = python.25 = C:/Dev/Python254 python.ap25 = C:/Dev/ActivePython254 python.26 = C:/Dev/Python261 python.27 = # default Python version (if not defined, pick the one from the path) .python = # ---------------------------------------------------------------------------- # Database Backends (select with `db=` on the `make` command line) # db URIs sqlite.uri = sqlite:test.db mysql.uri = mysql://tracuser:tracpassword@localhost/trac postgres.uri = postgres://tracuser:tracpassword@localhost:5432/trac?schema=tractest # default db backend (if not defined, use in-memory sqlite) .uri = # default Python versions to use when `db` is specified mysql.python = 25 postgres.python = 26 # ---------------------------------------------------------------------------- # Settings for the test server env = ~/tracenvs auth = *,~/tracenvs/htdigest.realm,realm # ---------------------------------------------------------------------------- # Settings for the documentation dotpath = /usr/local/bin/dot # ---------------------------------------------------------------------------- # Custom rules .PHONY: bigtest bigtest: make python=24 test make db=postgres test make db=mysql test .PHONY: frup frcomp frup: stats-pot extraction update-fr stats-fr frcomp: check-fr compile-fr stats-fr stats-pot Trac-1.0.1/setup_wininst.bmp0000644000175200017520000011605612102610255015020 0ustar cbooscboosBM.6(  @@ "+B /$&$o*-+131&'c7:8./e>A?FHF<>c!!LNLGGiPRQUWV33\_]dgeHIknlknsutzvvx{y]\}~uu?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????=0????????????????????????????????????????????????-=?????????????????????????????????????????????????????????????????????????????????????????????755:8?>;+75??=5=?:??:;??=557:?:?>;?75::>?:???>55;????;48?754:??87?==?:????:27??84=?;4:?:??;4:?>7:????>54>?=5;?:??=:?>=?84=?=5=?=4;??????????????????????*;=>/:2;*7=/?+>,>,??(>??;+==4=,??->/= =*?*???+8=+???=*>70>>2:75;-=/?0>??=+>/5:2>(?+>':->?+?/=-=75???+=:*?+>-;+??7'?>/>->+?*>-=,>,??????????????????????2:???*(?2:?,>+>>>2=?*????'??:558?,?-?2::'5???-=?*???75??(?'?>-28>=?+?87??=:?:/75?2;+?:278?*???+>>=???7??(?*>>>4:?472?+=-???+>>>?>*??????????????????????:0==?7$>:2?2;&55+:4?/>???(???-=-?->+?;4?'/???:0>,=??;/??0:+>?*:'5,8+>>-?????(>>,?:4/=?,=-?,>??/-5'?????28?&45+;0?0>/>,?+???$55+?/=???????????????????????&77?+=+> ;,=45;/? 755>??0:??-?"8,>->?,=-57???0/,*???(??845+;+?*;0;4,;-????/:??+;5777?,?$5:/;-=+;+????:-??70;-?$5,?=/58087754;->-;/??????????????????????*???:?=>;8;??87??;:7';??:4??;?;8:?=>?;==?:???+?>4???*>?=->:7>?>5:?=:7>???=-?;;?5:?>;?;?;;>:7>?;5=????*??8?:5>?;;:??:>=?78??87??:5>??????????????????????-:==????????????????8????+??=;???????????????45:>???:->07????????????????>*=+;???????????????????????,;7/????????????8??????????????????????????????????=778?????????????????????:??>;????????????????:7=????;5:??????????????????=5;?????????????????????????78?????????????:?????????????????????????????????????????????????????????????????????????;8458=??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????>- '=?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????; >??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????> >?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????7;??5 $?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????/:????, ???????????????????????????????????????????????????????????????????????????????????????????????????/$ &:???????>-"->??******8????????????:>????;(  />???????5******=??????-*****,??????=* +7??4*****;:******>:******>??????????????+ 7????7 2=4???=& "5?>5??:"?????? '?????;8????&= ;0=0=????????????? 4??4"7??/"?-?=>???7 ?????("???-=0=0=????????????5(&=> 8?0-? + ??? 7???????  =0=0=????????????????/'>??+8=*& -57???;??????84?? ???>=0=0=????????????5????;-----7>????8'*???>-*?????4----->?0>??$??????5=0=0=???????????:(----------5 ?????08 ?????4------------=? /?? =?*/5::=0=0=???????????0*?????88 ?????=-7=7?5"+??$=0=0=???????????5&(?????78 ?????>->+?;7/???2=0=0=???????????=/?????-8?????;-& ?+?,& ?>8????>5'  =0=0=???????????? 7????;8=????8"=????*-0?????:;?>&??"?;;;;;;????>?0=0=???????????? ????"?5:?> 85=??7-=4???=-$???:??0? 0???-?0=0=????????????=  7??8?-?*"? *???4???>4  &?0=0=?????????????8 ???: 8?= -??$75;???? (????=??0=0=??????????????>* 4?????= 788??>,:*???:=??????$;????2=>$>??0=0=????????????????>2*" *2?????????5*'4>?88????7*$/=??------7?????:/'&-8???0-----7?????=------0??????-----;???2'  '2?????0=0=??????????????????????????????????????????88??????????????????????????????????????????????????????????????????????????????????????0=0=??????????????????????????????????????????88??????????????????????????????????????????????????????????????????????????????????????0=0=??????????????????????????????????????????88??????????????????????????????????????????????????????????????????????????????????????0=0=??????????????????????????????????????????88??????????????????????????????????????????????????????????????????????????????????????0=0=??????????????????????????????????????????88??????????????????????????????????????????????????????????????????????????????????????0=0=??????????????????????????????????????????::??????????????????????????????????????????????????????????????????????????????????????5>5>????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????=4?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????5 =??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????$4???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????;>????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????/ :?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????> +???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????7 =????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????( 5?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????;"???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????2 :???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????> -?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????8 =??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????+ 7???????????????????????????????????????????????????????????????????????????????????????????????????????????????????=&?????????????????????????????????????????????????????????????????????????????????????????????????????????????????5;??????????????????????????????????????????????????????????????????????????????????????????????????????????????$ /???????????????????????????????????????????????????????????????????????????????????????????????????????????;>????????????????????????????????????????????????????????????????????????????????????????????????????????/  7?????????????????????????????????????????????????????????????????????????????????????????????????????>'???????????????????????????????????????????????????????????????????????????????????????????????????7 ;????????????????????????????????????????????????????????????????????????????????????????????????* 2??????????????????????????????????????????????????????????????????????????????????????????????> ??????????????????????????????????????????????????????????????????????????????????????????????4 ;????????????????????????????????????????????????????????????????????????????????????????????? /????????????????????????????????????????????????????????????????????????????????????????????? ????????????????????????????????????????????????????????????????????????????????????????????= ????????????????????????????????????????????????????????????????????????????????????????????2 ;??????????????????????????????????????????????????????????????????????????????????????????? /??????????????????????????????????????????????????????????????????????????????????????????? ??????????????????????????????????????????????????????????????????????????????????????????= ??????????????????????????????????????????????????????????????????????????????????????????0 ;????????????????????????????????????????????????????????????????????????????????????????? -????????????????????????????????????????????????????????????????????????????????????????? ????????????????????????????????????????????????????????????????????????????????????????= ????????????????????????????????????????????????????????????????????????????????????????/ !4;???????????????????????????????????????????????????????????????????????????????????????(>???; -???????????????????????????????????????????????????????????????????????????????????????  ,????????=" ??????????????????????????????????????????????????????????????????????????????????????; 5?????????????*??????????????????????????????????????????????????????????????????????????????????????/  :?????????????????0:?????????????????????????????????????????????????????????????????????????????????????!;?????????????????????7 ,????????????????????????????????????????????????????????????????????????????????????? !>?????????????????????????; ????????????????????????????????????????????????????????????????????????????????????;,??????????????????????????????>$ ????????????????????????????????????????????????????????????????????????????????????-  2???????????????????????????????????+:??????????????????????????????????????????????????????????????????????????????????? 7???????????????????????????????????????4,???????????????????????????????????????????????????????????????????????????????????;???????????????????????????????????????????8 ??????????????????????????????????????????????????????????????????????????????????:!=??????????????????????=5???????????????????????; ??????????????????????????????????????????????????????????????????????????????????, (>??????????????????????4 8??????????????????????>$ :????????????????????????????????????????????????????????????????????????????????? /??????????????????????:$  *=??????????????????????++????????????????????????????????????????????????????????????????????????????????? 5?????????????????????=+ 7???20??????????????????????4????????????????????????????????????????????????????????????????????????????????:$/:??????????????????40????????=+ 8?????????????????>7, ????????????????????????????????????????????????????????????????????????????????> +7>?????????:$ *=?????????????:"*=?????????=5( &???????????????????????????????????????????????????????????????????????????????????>7,  (4=?>+ 8???????????????????42??;0& $/:????????????????????????????????????????????????????????????????????????????????????????????:0& 2????????????????????????=+ (4=??????????????????????????????????????????????????????????????????????????????????????????????????????=5* *=?????????????????????????????:$ ,7>?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????  8==4 ;=8&? +==: 7>; -==:&? ? ? ? ? 4=>8?:>4  -==:&?  7>;,==: > 7= ?" 2? ?$>?55 ;'? ? ?  ? ?8*'=?0 8055 ;'? ??$> :' ?? ? ->? ?"=? ? ?  ?  :? ?>?"=? ? -> ? ? ? $;?8? ? ? ? ? ? ? (5?====> ? ? ? ? ? ? $;?8 ? ? ? ?* ?  ? "= ? ? ? ?  ;'? ? ? > ? "= ? ? ?* ? ? ? ? &? ? 08 ;+ ?+ 0?- /= ?==?; 70 54 ?0 :/ 08 ;+ ?+ ? ? &? ? ? ? ;=;" =?== ,;=;* ?":>:&;>8 ? /7 4==4 ?$;>4 ,;=;* ?,=*=?== ;=;" 8+ ? ? ? ? ? > 7= 4 ? +; 2 7=>5      ?===8                +===5    ? 48    ? ? -=>: -=>: $;=8(? 4=>8 ? ? />;*? 8=;" ? += ? ? ? 8>7'? ?? &> ? &> ?& 4? 8+ *> ? ? ? /? ;, (>? ??? ? :4 :? ? 0> 0> ??? ? ? ?& ?? ?=* ?? ?? $?? ';?8 (;?8 ???====>? ?';>>?? ?85 ?? ?? ???,?-??? ??????48????$??? *?? *? ? ?8475??-? *>81*>?57??,2=:5:? ?&;=;'';=;' ?"?7>>5??/=-/=>;&8>=& ?87 ? ?(;>: 8>:-? ?? ?"? ?"?$?===>?==="?"= ?&?+ ??-=&?':=>72==;+??0>;,??';>52==;+ ?4>;,?8>= ?2==;+ ?"?-?4+?8 ??? ?'>$8=77=,???0??4:477=, ? ?0?? ?77=, ?"?87>74? ??=+ ?;-&?'?*= ???( ??'>?*= ??( ?? ??*= ? ?? => ;& ??87 ??$?"? ??*;>>? ?"? ?"? ?*;>>?? ? ?"? ?"?'>4/ >04 ? ?5:"?$??#*>"? ?"?#?(>?$*>$?"? ? ??"*>$?$?47&:-7&="?"?78#?&?5: =0$? ??&->$?5;57: =2&?0"?&->$?"?5: =2&?05>;-?8,? $?$?:8'? ?"4=>=0&?"?0>>=*&?':>8 4=>=1'?4=02>>=(>?>>&?4=>=0'?-;>;' "?$8-?":/&?;0 ?''?$?'?'? ->/>>0:'? *>*8= (?'? (? $7  77 $?7 (?*> (? *>" *:>>8& (? (?  (?  (>" """"""""""""""""""""" " " """"""""""" """"    """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""#"""#""""""""""""##"#"#"#"#""""""""""##""""""""""""#"##"""# ""#"""#"""#"""""""""#"""#""#""#"""""##""#""""""#""""""""""$$##$#$$$$#$$$$$$$$$$#$#$$#$$$$$$###$##$$$$$$$$$$$$#$$#$$$#$##  "##$##$#$$$$$$#$#"$#$"$$$$$$##$"$#$$#$$"$$"#$"$$$$#$##$$$$$$$$$#$$$$$$$$$##$$$$$$$$$$#$#$$$$$$$#$$$#$$$"####$$$$$$$$$$"" #"#$$$$$$$$$$$$$#$$$#$$$$$$$$$$$$$$$""$$$$$$$$$##$$#$$$$#$"$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"'8>>=0*;>>8$&?;8$? "$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&$$>5 :4*>'":= &? ??$? "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&*?"'?;/ *?+'?28:,"? $&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''&&$ /? ?$ """ '?>+-;"? $&&''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''&"'7?8'?  " '?'? ?"$? $&'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''((((''('('((''''''''(''''(''''''('''''''''(('''''''''''(&$(8??>4 (?  &?7582$?"#'''''''''''(''('''''(((''''''''''''''('''''''''''('''''''((((((((((((((((((((((((((((((((((((((((((((((((((((''''(((('''>=/& &?''??( -=&?"&'((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((*(((((((((((((((((((((((((((((((((((((((((((('''(((((',?&  "$;4 '?+(?0> "#?0? $'(((((((((((((((((((((((((((((((((((((((((((((((((((((((((*(*(**((*((*****(*(((***(****(*(*****((*(((*((*(*((('&&''(***((>5&#$5>&&->-"&:>#+?=5 "" 7;? &((*(**((((**((****(((**(**((*(****((*(**(*****(*****(*******************************************((((((((((((('&$$&'(****(+8>>>;-'''-;>>:*&,??*$" +?? "&'(***(((((((((((((((((***(((((****************************+++++*++*+*++*++++*+*++++*****((*****(('''''''''''''$" $&(****('''''''''''&&&&&&&&&&&$  $'(**(''''''''''''''''(((('''''(***+***+*++*****++++++*+**++++++++++++++++++++++++++++**('''****'&$$$$$$$$&&&&&=- $'***('&&&&''''&&$$$$$$$$$$$$4>>>8""$'(('&$""$$$$$&&&$$$$&''&$$$$&'(*+++++++++++++++++++++++++++++,+++++,+++++++++++++++++*(&&'(**(&$" "$$$2:$'***'&$""$&&&&$" """ ""(?+7: "&''&" # """" "$$$$" "&(*+++++++++++++++++++++++++,,,,,,,,,,,,,,,,,,,,,,,,,,++*'$$$(**'$"  ""$?$"&***'$  $&$"   (? $&&$   "" #  $(*,+,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,+(-?$$&((&0=>;0? '?'? *?4"&(**-? '? "$+? :>=$8>>;( ';>:1?&? "#&7>=0?:>= "8>>;( &:>;/?""'*+,,,,,,,,,,,,,,,,,,,,,,,,,-,,---,,--,------,--,,--,,,*-?" $''*?0"&8? '?'?  7;> &((*-? (? "+? '?";40>=7 ";?&? #,?*4?&?" ;40>;8 ";?#"(+,-------,,----,--,-,,,,,,---------------------------,*-?" "&&,?&$&,? '?'?"?*;/"&(**-? '? "*? '?&? &?' -?'? $*?/'?&?&? &?' -? $'+-------------------------////////////////-//-////-/--+-? "$&,?&&'-?$(?'?0=$4;&'(**-?"(? "*? (?*?>>>>?$+?$" *? (?""$&2=>>?'?*?>>>>?$+?$" *? $(+--//----///////-//////-/////////////////////////////-+/?" "$&-?((*/?&+? '? ;4'*?-(*++/?$*?$"$,?$*? '?($"-?&+?,$$/?$+?'$&''&$+?$*? '?($"-?&+?+$"/?"$(,-///////////////////////////////////////////////////-,/?>>>>-/?++,2?*,?"(?"*?+++:8+,,+/?&+?5'(8>',?$$';8**;:*+=:**=?(/?7(*-?0(5>',?$$';8**;:*+=:((;?$&*-/////////////////////////00000000000000000000000000//-0?&"$'(0?---4?,/?&+?&7:+--5?/--,0?'-?5=?=2+>?>>*-;>>:---0=>=7?-2?8>7,8>>>4+>?>>*-;>>:/--0=>=4?'(+-//00000000000000000000000101000000001110011000000001/-1?'&&'*+-/101/,1?(-?*(,-///011/-2?**,,-1//-4?-,-11////////////////////11//-4?-,,///11//////,0?*+-/1000000110000000000101000010010000001001000001001110014?+***+,/10001/4?,0?,,/10000000/4?-,/1000001;01/0100010000010000100010000002;0/110010100100/4?--/00011100010101000000110110222222222222222222222222222205?>>>>7/00222205?/4?0/00222222205?0/002222220002222222222222222222222222222200022222222222205?0/0022222222222222222222222222222222222222222222222222222220000000222222220000022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222244444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222220000010010000000000000020100001000000002000002000000000000002002000020000200101000000000200100102010010000000000022000000000020000001000010100000001000000000000000000100010000000000000010000000000000000000000000000000000000000000001000000000000000000000000100000010000000110000000000110010000010000000000//////////////1////11////1//1//1//////////////////////////////1///////////////////1/////////////////1//1//1///////////////////////////////////////////////////////1///1///////////////////////////////////////////////////////////1///////////////1///1/1//////////////////////////////////////////////////////1////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////--------------------------------------------------------------------------------------------------------------------------------------------------------??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????<.).>??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????6.>????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????%9???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????%.???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????<)????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????9))6???).?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????<6???%????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????3???<9??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????39???66?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????<.????9?????????????????????0' $/=?55555558????????????5* "-;????=5555557?????????:- $+27????????????????????????3)????))???????????????????>'-???????????2 0??(??????;&  ;?????????????????????%)????)%6????????????????????-??????????+=?????*$????????????????????.????.%9?????????????????????&-?????????8 ???????????????????????8& ??8"/70  ??????????????????))???????.)????6??????????????4:??????????????8??????8 ?? 8?????:4?????????????????3?????????.%????<3?????????????-5???????????????>???????( ?>0???????'"?????????????????<%)9??????????????????????????-=????????????????/ ?* ??????????????????????????????<)???93??????936?????)????????????-=???????:?????????=& ?/??????????????????????????????<???63?????)%????3????????????-=???????4????????????>;8740---- ?: ????????>5555555;??????????????%=======????????'?? 4???????,/??????????????>???<????9????????????-=???????55??????8-??$>??????:??????????????????).???>)????6????????? 4?5,>????=;??> 8???;???????????????????&'????/ 4???????????????????.????)6????>3.9???????????-?'&5?0:?????("????????????????????%>????)?????.6????????????->*? 5??-5???????0&?????????????????????))????<????3.???????????-;-??- 5???:& ;?????????; ";??????????????????????39????3????%???????????----+-----:=-------;????8,$:??????8*"  '0>?????????????>4&  $->?????????????????????????%?????3????)%???????????????-=??????????????????????????????????????????????????????????????????????????????????????????????69?????3????.6???????????????-=???????????????????????????????????????????????????????????????????????????????????????????????.9??????3)????9????????????????-=????????????????????????????????????????????????????????????????????????????????????????????????6%)?????????????????????????????????????????????????????????????????????????????????????????????.???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????36???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????6???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????9???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????)????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????.>????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????<.>?????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????Trac-1.0.1/INSTALL0000644000175200017520000001200012102610255012416 0ustar cbooscboosTrac Installation Guide ======================= Trac is a lightweight project management tool that is implemented as a web-based application. Trac is written in the Python programming language and can use SQLite, PostgreSQL or MySQL as database. For HTML rendering, Trac uses the Genshi templating system. **You should also read the trac/wiki/default-pages/TracInstall documentation file present in the source distribution.** If you're upgrading an already installed Trac environment, please also read trac/wiki/default-pages/TracUpgrade. Requirements ------------ To install Trac, the following software packages must be installed: * Python, version >= 2.5. * setuptools, version >= 0.6, or the "distribute" package * Genshi, version >= 0.6 * Optionally, Subversion, version >= 1.1.x and the Subversion SWIG Python bindings (not PySVN, that's something different). * One of the following Python bindings, depending on the database used: * pysqlite version 2.x for SQLite 3.x * psycopg2 version 2.0.x for the PostgreSQL database * MySQLdb, version 1.2.2 for the MySQL database * A web server capable of executing CGI/FastCGI scripts, or Apache HTTPD with mod_python or mod_wsgi. (Trac also comes with a standalone server, tracd) Any of the above python library can usually be installed using easy_install, which itself can be installed using the following bootstrap script:: $ wget http://peak.telecommunity.com/dist/ez_setup.py $ python ez_setup.py or, alternatively, install distribute: $ curl -O http://python-distribute.org/distribute_setup.py $ python distribute_setup.py After that, you can do for example: easy_install Genshi Your version of Python comes with its own version of pysqlite, however if want to use the latest pysqlite package, you can download from http://code.google.com/p/pysqlite/downloads/list the Windows installers or the tar.gz archive for building from source:: $ tar xvfz .tar.gz $ cd $ python setup.py build_static install That way, the latest SQLite version will be downloaded and built into the bindings. Installing Trac --------------- The command:: $ python ./setup.py install will byte-compile the python source code and install it in the site-packages directory of your python installation. The script will also install the trac-admin command-line tool, used to create and maintain project environments. Trac-admin is the command center of Trac. Note: you'll need root permissions or equivalent for this step. To install Trac in a different location, or use other advanced installation options, run:: $ python ./setup.py --help Installing Trac on Windows -------------------------- If you downloaded the Trac installer (the .exe file), installing is simply a matter of running the installer. After running the installer, configuration and installation is the same as for other platforms. Creating a Project Environment ------------------------------ A Trac environment is the backend storage format where Trac stores information like wiki pages, tickets, reports, settings, etc. A Trac environment consists of the environment configuration file (trac.ini), custom templates, log files, and more. A new Trac environment is created with trac-admin:: $ trac-admin /path/to/projectenv initenv Note: The user account under which the web server is run needs write permission to the environment directory and all the files inside. trac-admin will prompt you for the name of the project, where your subversion repository is located, what database you want to use, etc. Running the Standalone Server ----------------------------- After having created a Trac environment, you can easily try the web interface by running the standalone server ``tracd``:: $ tracd --port 8000 /path/to/projectenv Then, fire up a browser and visit http://localhost:8000/. You should get a simple listing of all environments that tracd knows about. Follow the link to the environment you just created, and you should see Trac in action. Running Trac on a Web Server ---------------------------- Trac provides three options for connecting to a "real" web server: CGI, FastCGI, mod_python and mod_wsgi. For decent performance, it is recommended that you use either FastCGI or mod_wsgi. Please refer to the TracInstall page for details on these setups. You can find it either in the wiki of the Trac project you just created, or on the main Trac site. Using Trac ---------- Once you have your Trac site up and running, you should be able to browse your subversion repository, create tickets, view the timeline, etc. Keep in mind that anonymous (not logged in) users can by default access most but not all of the features. You will need to configure authentication and grant additional permissions to authenticated users to see the full set of features. For further documentation, see the TracGuide wiki page. Enjoy! /The Trac Team Please also consider joining the mailing lists at Visit the Trac open source project at Trac-1.0.1/README0000644000175200017520000000164612102610255012263 0ustar cbooscboosAbout Trac ========== Trac is a minimalistic web-based software project management and bug/issue tracking system. It provides an interface to the Git and Subversion revision control systems, an integrated wiki, flexible issue tracking and convenient report facilities. Trac is distributed using the modified BSD License. * For installation instructions, please see the INSTALL for a short overview and the trac/wiki/default-pages/TracInstall page for complete, up-to-date instructions * If you are upgrading from a previous Trac version, please read UPGRADE for a short overview and the trac/wiki/default-pages/TracUpgrade for complete, up-to-date instructions You might also want to take a look at the RELEASE and ChangeLog files for more information about this release. Otherwise, the primary source of information is the main Trac web site: We hope you enjoy it, /The Trac Team Trac-1.0.1/.tx/0000755000175200017520000000000012102610436012106 5ustar cbooscboosTrac-1.0.1/.tx/config0000644000175200017520000000075412102610255013303 0ustar cbooscboos[main] host = https://www.transifex.com [trac.1_0-stable-messages-js-pot] file_filter = trac/locale//LC_MESSAGES/messages-js.po source_file = trac/locale/messages-js.pot source_lang = en [trac.1_0-stable-messages-pot] file_filter = trac/locale//LC_MESSAGES/messages.po source_file = trac/locale/messages.pot source_lang = en [trac.1_0-stable-tracini-pot] file_filter = trac/locale//LC_MESSAGES/tracini.po source_file = trac/locale/tracini.pot source_lang = en type = PO Trac-1.0.1/doc/0000755000175200017520000000000012102610436012142 5ustar cbooscboosTrac-1.0.1/doc/glossary.rst0000644000175200017520000000021112102610255014530 0ustar cbooscboos======== Glossary ======== .. glossary:: :sorted: VCS Version Control System, what you use for versioning your source code Trac-1.0.1/doc/api/0000755000175200017520000000000012102610436012713 5ustar cbooscboosTrac-1.0.1/doc/api/trac_db_util.rst0000644000175200017520000000524412102610255016104 0ustar cbooscboos:mod:`trac.db.utils` -- Trac DB utilities ========================================= .. module :: trac.db.util Utilities for the Trac DB abstraction layer. Classes ------- The following classes are not meant to be used directly. In particular, the `ConnectionWrapper` is what the `~trac.db.api.DbContextManager` context managers will return. For example:: >>> with env.db_query as db: ... for name, value in db.execute("SELECT name, value FROM system"): ... print "row: [{name}, {value}]".format(name=name, value=value) ... row: [database_version, 29] Here ``db`` is a `ConnectionWrapper`. .. autoclass :: ConnectionWrapper :members: All of the following methods but `execute`, `executemany` and `check_select` need to be implemented by the backend-specific subclass. In addition, the standard methods from :pep:`0249` *Connection Objects* are also available. .. method :: cast(self, column, type) Local SQL dialect for type casting. :param column: name of the column :param type: generic type (``int``, ``int64``, ``text``) .. method :: concat(self, *args): Local SQL dialect for string concatenation. :param args: values to be concatenated specified as multiple parameters. .. method :: like(self): Local SQL dialect for a case-insensitive LIKE clause. .. method :: like_escape(self, text): Local SQL dialect for searching for litteral text in a LIKE clause. .. method :: quote(self, identifier): Local SQL dialect for quoting an identifier. .. method :: get_last_id(self, cursor, table, column='id'): Local SQL dialect for retrieving the last value of an auto-increment column, immediately following an INSERT clause. :param cursor: the cursor in which the INSERT was executed :param table: the name of the table in which the insertion happened :param column: the name of the auto-increment column Some backends, like PostgreSQL, support that feature natively indirectly via sequences. .. method :: update_sequence(self, cursor, table, column='id'): Local SQL dialect for resetting a sequence. Same parameters as for `get_last_id`. This can be used whenever rows were created *with an explicit value for the auto-increment column*, as it could happen during a database upgrade and the recreation of a table. See :teo:`#8575` for details. Also, all the `ConnectionWrapper` subclasses (``SQLiteConnection``, ``PostgreSQLConnection`` and ``MySQLConnection``) have a reimplemented ``cursor()`` method which returns an `IterableCursor`. .. autoclass :: IterableCursor :members: Trac-1.0.1/doc/api/trac_attachment.rst0000644000175200017520000000253512102610255016612 0ustar cbooscboos:mod:`trac.attachment` -- Attachments for Trac resources ======================================================== .. module :: trac.attachment This module contains the `Attachment` model class and the `AttachmentModule` component which manages file attachments for any kind of Trac resources. Currently, the wiki pages, tickets and milestones all support file attachments. You can use the same utility methods from the `AttachmentModule` as they do for easily adding attachments to other kinds of resources. See also the :download:`attach_file_form.html <../../trac/templates/attach_file_form.html>` and :download:`attachment.html <../../trac/templates/attachment.html>` templates which can be used to display the attachments. Interfaces ---------- .. autoclass :: IAttachmentChangeListener :members: See also :extensionpoints:`trac.attachment.IAttachmentChangeListener` .. autoclass :: IAttachmentManipulator :members: See also :extensionpoints:`trac.attachment.IAttachmentManipulator` .. autoclass :: ILegacyAttachmentPolicyDelegate :members: See also :extensionpoints:`trac.attachment.ILegacyAttachmentPolicyDelegate` Classes ------- .. autoclass :: Attachment :members: .. autoclass :: InvalidAttachment :members: Components ---------- .. autoclass :: AttachmentModule :members: .. autoclass :: AttachmentAdmin :members: Trac-1.0.1/doc/api/trac_web_api.rst0000644000175200017520000000331412102610255016064 0ustar cbooscboos:mod:`trac.web.api` -- Trac Web Request Handling ================================================ .. module :: trac.web.api Primary interface for handling web requests. Interfaces ---------- The following interfaces allow components to interact at various stages of the web requests processing pipeline. .. autoclass :: IRequestHandler :members: See also :extensionpoints:`trac.web.api.IRequestHandler` .. autoclass :: IRequestFilter :members: See also :extensionpoints:`trac.web.api.IRequestFilter` For how the main content itself can be generated, see `trac.web.chrome`. .. autoclass :: ITemplateStreamFilter :members: See also :extensionpoints:`trac.web.api.ITemplateStreamFilter` .. autoclass :: IAuthenticator :members: See also :extensionpoints:`trac.web.api.IAuthenticator` Classes ------- .. autoclass :: Request :members: .. attribute :: Request.authname The name associated with the user after authentification or `'anonymous'` if no authentification took place. This corresponds to the `~Request.remote_user` when the request is targeted to an area requiring authentication, otherwise the authname is retrieved from the ``trac_auth`` cookie. .. attribute :: Request.href An `~trac.web.href.Href` instance for generating *relative* URLs pointing to resources within the current Trac environment. .. attribute :: Request.abs_href An `~trac.web.href.Href` instance for generating *absolute* URLs pointing to resources within the current Trac environment. .. autoclass :: trac.web.api.RequestDone :members: Helper Functions ---------------- .. autofunction :: arg_list_to_args .. autofunction :: parse_arg_list Trac-1.0.1/doc/api/trac_cache.rst0000644000175200017520000000316312102610255015523 0ustar cbooscboos:mod:`trac.cache` -- Control of cached data coherency ===================================================== Trac is a server application which may involve multiple concurrent processes. The coherency of the data presented to the clients is ensured by the underlying database and its transaction handling. However, a server process will not systematically retrieve data from the database, as various in-memory caches are used for performance reasons. We could ensure the integrity of those caches in a single process in presence of multiple threads by the appropriate use of locking and by updating the caches as needed, but we also need a mechanism for invalidating the caches in the *other* processes. The purpose of this module is to provide a `cached` decorator_ which can annotate a data *retriever* method of a class for turning it into an attribute working like a cache. This means that an access to this attribute will only call the underlying retriever method once on first access, or only once after the cache has been invalidated, even if this invalidation happened in another process. .. _decorator: http://docs.python.org/glossary.html#term-decorator .. module :: trac.cache Public API ---------- .. autofunction :: cached Internal API ------------ .. autoclass :: CacheManager :members: The following classes are the descriptors_ created by the `cached` decorator: .. _descriptors: http://docs.python.org/glossary.html#term-descriptor .. autoclass :: CachedSingletonProperty :members: .. autoclass :: CachedProperty :members: Both classes inherit from a common base: .. autoclass :: CachedPropertyBase :members: Trac-1.0.1/doc/api/trac_mimeview.rst0000644000175200017520000000263412102610255016304 0ustar cbooscboos:mod:`trac.mimeview.api` -- Trac content transformation APIs ============================================================ .. automodule :: trac.mimeview.api Interfaces ---------- .. autoclass :: trac.mimeview.api.IHTMLPreviewRenderer :members: See also :extensionpoints:`trac.mimeview.api.IHTMLPreviewRenderer` .. autoclass :: trac.mimeview.api.IHTMLPreviewAnnotator :members: See also :extensionpoints:`trac.mimeview.api.IHTMLPreviewAnnotator` .. autoclass :: trac.mimeview.api.IContentConverter :members: See also :extensionpoints:`trac.mimeview.api.IContentConverter` Components ---------- .. autoclass :: trac.mimeview.api.Mimeview :members: Helper classes -------------- .. autoclass :: trac.mimeview.api.RenderingContext :members: .. autoclass :: trac.mimeview.api.Context :members: .. autoclass :: trac.mimeview.api.Content :members: Functions --------- .. py:function :: get_mimetype(filename, content=None, mime_map=MIME_MAP) Guess the most probable MIME type of a file with the given name. :param filename: is either a filename (the lookup will then use the suffix) or some arbitrary keyword. :param content: is either a `str` or an `unicode` string. .. autofunction :: trac.mimeview.api.ct_mimetype .. autofunction :: trac.mimeview.api.is_binary .. autofunction :: trac.mimeview.api.detect_unicode .. autofunction :: trac.mimeview.api.content_to_unicode Trac-1.0.1/doc/api/trac_util_html.rst0000644000175200017520000000247712102610255016470 0ustar cbooscboos:mod:`trac.util.html` -- HTML transformations ============================================= .. module :: trac.util.html Building HTML programmatically ------------------------------ With the introduction of the Genshi_ template engine in Trac 0.11, most of the (X)HTML content is produced directly using Genshi facilities, like the builder_ or snippet templates. The old `html` tag building facility is now not much more than an alias to the `tag` ElementFactory_, and most of the code uses directly the latter. .. data :: html A `TransposingElementFactory` using `str.lower` transformation. .. autoclass :: TransposingElementFactory .. _Genshi: http://genshi.edgewall.org .. _builder: http://genshi.edgewall.org/wiki/ApiDocs/genshi.builder .. _ElementFactory: http://genshi.edgewall.org/wiki/ApiDocs/genshi.builder#genshi.builder:ElementFactory HTML clean-up and sanitization ------------------------------ .. autoclass :: TracHTMLSanitizer .. autoclass :: Deuglifier See some usage examples in `tracopt.mimeview.enscript.EnscriptDeuglifier` and `tracopt.mimeview.php.PhpDeuglifier`. .. autofunction :: escape .. autofunction :: unescape .. autoclass :: FormTokenInjector Misc. HTML processing --------------------- .. autofunction :: expand_markup .. autofunction :: find_element .. autofunction :: plaintext Trac-1.0.1/doc/api/trac_util_presentation.rst0000644000175200017520000000122312102610255020223 0ustar cbooscboos:mod:`trac.util.presentation` -- Utilities for dynamic content generation ========================================================================= .. module :: trac.util.presentation The following utilities are all available within Genshi templates. .. autofunction :: captioned_button .. autofunction :: classes .. autofunction :: first_last .. autofunction :: group .. autofunction :: istext .. autofunction :: paginate .. autofunction :: separated .. autofunction :: to_json Modules generating paginated output will be happy to use a rich pagination controller. See *Query*, *Report* and *Search* modules for example usage. .. autoclass :: Paginator Trac-1.0.1/doc/api/trac_web_main.rst0000644000175200017520000000157212102610255016243 0ustar cbooscboos:mod:`trac.web.main` -- Trac Web Entry Point ============================================ .. module :: trac.web.main Entry point for dispatching web requests. `trac.web.dispatch_request` --------------------------- The WSGI compliant callable. It adapts the ``environ`` information passed from the WSGI gateway and retrieve the appropriate `~trac.env.Environment` from it, creates a `~trac.web.api.Request` instance and let the `RequestDispatcher` component forward it to the component implementing a matching `~trac.web.api.IRequestHandler`. .. autofunction :: dispatch_request Components ---------- .. autoclass :: RequestDispatcher :members: Classes ------- .. autoclass :: RequestWithSession Helper Functions ---------------- .. autofunction :: get_environments .. autofunction :: get_tracignore_patterns Miscellaneous ------------- .. autodata :: default_tracker Trac-1.0.1/doc/api/trac_web_auth.rst0000644000175200017520000000123512102610255016254 0ustar cbooscboos:mod:`trac.web.auth` -- Trac Authentication =========================================== .. module :: trac.web.auth This module deals with web request authentication, and provides the default implementation for the `~trac.web.api.IAuthenticator` interface. Component --------- .. autoclass :: LoginModule :members: Support Classes --------------- A few classes are provided for directly computing the REMOTE_USER information from the HTTP headers for Basic or Digest authentication. This will be used by the `~trac.web.standalone.AuthenticationMiddleware`. .. autoclass :: BasicAuthentication :members: .. autoclass :: DigestAuthentication :members: Trac-1.0.1/doc/api/trac_util_text.rst0000644000175200017520000000554512102610255016507 0ustar cbooscboos.. -*- coding: utf-8 -*- :mod:`trac.util.text` -- Text manipulation ========================================== .. module :: trac.util.text The Unicode toolbox ------------------- Trac internals are almost exclusively dealing with Unicode text, represented by `unicode` instances. The main advantage of using `unicode` over UTF-8 encoded `str` (as this used to be the case before version 0.10), is that text transformation functions in the present module will operate in a safe way on individual characters, and won't risk to eventually cut a multi-byte sequence in the middle. Similar issues with Python string handling routines are avoided as well, like surprising results when splitting text in lines. For example, did you know that "Priorità" is encoded as ``'Priorit\xc3\x0a'`` in UTF-8? Calling `strip()` on this value in some locales can cut away the trailing ``\x0a`` and it's no longer valid UTF-8... The drawback is that most of the outside world, while eventually "Unicode", is definitely not `unicode`. This is why we need to convert back and forth between `str` and `unicode` at the boundaries of the system. And more often than not we even have to guess which encoding is used in the incoming `str` strings. Encoding `unicode` to `str` is usually directly performed by calling `encode()` on the `unicode` instance, while decoding is preferably left to the `to_unicode` helper function, which converts `str` to `unicode` in a robust and guaranteed successful way. .. autofunction :: to_unicode .. autofunction :: exception_to_unicode Web utilities ............. .. autofunction :: unicode_quote .. autofunction :: unicode_quote_plus .. autofunction :: unicode_unquote .. autofunction :: unicode_urlencode .. autofunction :: quote_query_string .. autofunction :: javascript_quote .. autofunction :: to_js_string Console and file system ....................... .. autofunction :: path_to_unicode .. autofunction :: stream_encoding .. autofunction :: console_print .. autofunction :: printout .. autofunction :: printerr .. autofunction :: raw_input Miscellaneous ............. .. data :: empty A special tag object evaluating to the empty string, used as marker for missing value (as opposed to a present but empty value). .. autoclass :: unicode_passwd .. autofunction :: levenshtein_distance Text formatting --------------- .. autofunction :: pretty_size .. autofunction :: breakable_path .. autofunction :: normalize_whitespace .. autofunction :: unquote_label .. autofunction :: fix_eol .. autofunction :: expandtabs .. autofunction :: obfuscate_email_address .. autofunction :: text_width .. autofunction :: print_table .. autofunction :: shorten_line .. autofunction :: stripws .. autofunction :: wrap Conversion utilities -------------------- .. autofunction :: unicode_to_base64 .. autofunction :: unicode_from_base64 .. autofunction :: to_utf8 Trac-1.0.1/doc/api/trac_ticket_roadmap.rst0000644000175200017520000000270112102610255017443 0ustar cbooscboos:mod:`trac.ticket.roadmap` -- The Roadmap and Milestone modules =============================================================== .. module :: trac.ticket.roadmap The `component` responsible for the *Roadmap* feature in Trac is the `RoadmapModule`. It provides an overview of the milestones and the progress in each of these milestones. The `component` responsible for interacting with each milestone is the `MilestoneModule`. A milestone also provides an overview of the progress in terms of tickets processed. The grouping of tickets in each progress bar is governed by the use of another component implementing the `ITicketGroupStatsProvider` interface. By default, this is the `DefaultTicketGroupStatsProvider` (for both the `RoadmapModule` and the `MilestoneModule`), which provides a configurable way to specify how tickets are grouped. Interfaces ---------- .. autoclass :: ITicketGroupStatsProvider :members: See also :extensionpoints:`trac.ticket.roadmap.ITicketGroupStatsProvider` .. autoclass :: TicketGroupStats :members: Components ---------- .. autoclass :: MilestoneModule :members: :exclude-members: resource_exists .. autoclass :: RoadmapModule :members: .. autoclass :: DefaultTicketGroupStatsProvider :members: :exclude-members: milestone_groups_section Helper Functions ---------------- .. autofunction :: apply_ticket_permissions .. autofunction :: get_tickets_for_milestone .. autofunction :: grouped_stats_data Trac-1.0.1/doc/api/trac_versioncontrol_diff.rst0000644000175200017520000000217412102610255020537 0ustar cbooscboos:mod:`trac.versioncontrol.diff` -- Utilities for generation of diffs ==================================================================== .. automodule :: trac.versioncontrol.diff Synopsis -------- `get_filtered_hunks`, `get_hunks` are low-level wrappers for Python's `difflib.SequenceMatcher`, and they generate groups of opcodes corresponding to diff "hunks". `get_change_extent` is a low-level utility used when marking intra-lines differences. `diff_blocks` is used at a higher-level to fill the template data needed by the "diff_div.html" template. `unified_diff` is also a higher-level function returning differences following the `unified diff`_ file format. Finally, `get_diff_options` is an utility for retrieving user diff preferences from a `~trac.web.api.Request`. .. _unified diff: http://www.gnu.org/software/hello/manual/diff/Detailed-Unified.html Function Reference ------------------ .. autofunction :: get_change_extent .. autofunction :: get_filtered_hunks .. autofunction :: get_hunks .. autofunction :: hdf_diff .. autofunction :: diff_blocks .. autofunction :: unified_diff .. autofunction :: get_diff_options Trac-1.0.1/doc/api/tracopt_mimeview.rst0000644000175200017520000000136412102610255017026 0ustar cbooscboos:mod:`tracopt.mimeview` -- Optional content generation modules ============================================================== .. module :: tracopt.mimeview Syntax Highlighters ------------------- .. module :: tracopt.mimeview.enscript .. autoclass :: EnscriptRenderer .. autoclass :: EnscriptDeuglifier .. literalinclude:: /../tracopt/mimeview/enscript.py :pyobject: EnscriptDeuglifier.rules See also `trac.util.html.Deuglifier`. .. module :: tracopt.mimeview.php .. autoclass :: PHPRenderer .. autoclass :: PhpDeuglifier .. literalinclude:: /../tracopt/mimeview/php.py :pyobject: PhpDeuglifier.rules See also `trac.util.html.Deuglifier`. .. module :: tracopt.mimeview.silvercity .. autoclass :: SilverCityRenderer Trac-1.0.1/doc/api/trac_wiki_api.rst0000644000175200017520000000353212102610255016254 0ustar cbooscboos:mod:`trac.wiki.api` -- The Wiki API ==================================== .. module :: trac.wiki.api Interfaces ---------- The wiki module presents several possibilities of extension, for interacting with the Wiki application and also for extending the Wiki syntax. First, components can be notified of the changes happening in the wiki. .. autoclass :: trac.wiki.api.IWikiChangeListener :members: See also :extensionpoints:`trac.wiki.api.IWikiChangeListener`. Components can also interfere with the changes, before or after they're made. .. autoclass :: trac.wiki.api.IWikiPageManipulator :members: See also :extensionpoints:`trac.wiki.api.IWikiPageManipulator`. Then, the Wiki syntax itself can be extended. The first and less intrusive way is to provide new Wiki macros or Wiki processors. Those are basically the same thing, as they're implemented using the following interface. The difference comes from the invocation syntax used in the Wiki markup, which manifests itself in the `args` parameter of :meth:`IWikiMacroProvider.expand_macro`. .. autoclass :: trac.wiki.api.IWikiMacroProvider :members: See also `~trac.wiki.macros.WikiMacroBase` and :teo:`wiki/WikiMacros#DevelopingCustomMacros` and :extensionpoints:`trac.wiki.api.IWikiMacroProvider`. The Wiki syntax can also be extended by introducing new markup. .. autoclass :: trac.wiki.api.IWikiSyntaxProvider :members: See also :teo:`wiki:TracDev/IWikiSyntaxProviderExample` and :extensionpoints:`trac.wiki.api.IWikiSyntaxProvider`. The Wiki System --------------- The wiki system provide an access to all the pages. .. autoclass :: trac.wiki.api.WikiSystem :members: :exclude-members: get_resource_description, resource_exists Other Functions --------------- .. autofunction :: trac.wiki.api.parse_args .. autofunction :: trac.wiki.api.validate_page_name Trac-1.0.1/doc/api/trac_env.rst0000644000175200017520000000123112102610255015242 0ustar cbooscboos:mod:`trac.env` -- Trac Environment model and APIs ================================================== .. module :: trac.env Interfaces ---------- .. autoclass :: trac.env.IEnvironmentSetupParticipant :members: See also :extensionpoints:`trac.env.IEnvironmentSetupParticipant` .. autoclass :: trac.env.ISystemInfoProvider :members: See also :extensionpoints:`trac.env.ISystemInfoProvider` Components ---------- The `Environment` is special in the sense it is not only a `Component`, but also a `trac.core.ComponentManager`. .. autoclass :: trac.env.Environment :members: Functions --------- .. autofunction :: trac.env.open_environment Trac-1.0.1/doc/api/trac_versioncontrol_api.rst0000644000175200017520000000271012102610255020374 0ustar cbooscboos:mod:`trac.versioncontrol.api` -- Trac Version Control APIs =========================================================== .. automodule :: trac.versioncontrol.api This module implements an abstraction layer over different kind of version control systems and the mechanism to access several heterogeneous repositories under a single "virtual" hierarchy. This abstraction was derived from the original model built around the Subversion system (versioned tree, changesets). It gradually became more general, now aiming at supporting distributed version control systems (DVCS). Interfaces ---------- .. autoclass :: IRepositoryConnector :members: See also :extensionpoints:`trac.versioncontrol.api.IRepositoryConnector` .. autoclass :: IRepositoryProvider :members: See also :extensionpoints:`trac.versioncontrol.api.IRepositoryProvider` .. autoclass :: IRepositoryChangeListener :members: See also :extensionpoints:`trac.versioncontrol.api.IRepositoryChangeListener` Components ---------- .. autoclass :: RepositoryManager :members: .. autoclass :: DbRepositoryProvider :members: Exceptions ---------- Subclasses of `ResourceNotFound`. .. autoclass :: NoSuchChangeset :members: .. autoclass :: NoSuchNode :members: Abstract classes ---------------- .. autoclass :: Repository :members: .. autoclass :: Node :members: .. autoclass :: Changeset :members: Helper Functions ---------------- .. autofunction :: is_default Trac-1.0.1/doc/api/trac_util_datefmt.rst0000644000175200017520000000424712102610255017145 0ustar cbooscboos:mod:`trac.util.datefmt` -- Date and Time manipulation ====================================================== .. module :: trac.util.datefmt Since version 0.10, Trac mainly uses `datetime.datetime` objects for handling date and time values. This enables us to properly deal with timezones so that time can be shown in the user's own local time. Conversion ---------- From "anything" to a `datetime`: .. autofunction :: to_datetime A `datetime` can be converted to milliseconds and microseconds timestamps. The latter is the preferred representation for dates and times values for storing them in the database, since Trac 0.12. .. autofunction :: to_timestamp .. autofunction :: to_utimestamp Besides `to_datetime`, there's a specialized conversion from microseconds timestamps to `datetime`: .. autofunction :: from_utimestamp Parsing ------- .. autofunction :: parse_date Formatting ---------- .. autofunction :: pretty_timedelta .. autofunction :: format_datetime Derivatives: .. autofunction :: format_date .. autofunction :: format_time Propose suggestion for date/time input format: .. autofunction :: get_date_format_hint .. autofunction :: get_datetime_format_hint .. autofunction :: http_date .. autofunction :: is_24_hours Formatting and parsing according to user preferences: .. autofunction :: user_time jQuery UI datepicker helpers ---------------------------- .. autofunction :: get_date_format_jquery_ui .. autofunction :: get_time_format_jquery_ui .. autofunction :: get_day_names_jquery_ui .. autofunction :: get_first_week_day_jquery_ui .. autofunction :: get_month_names_jquery_ui .. autofunction :: get_timezone_list_jquery_ui Timezone utilities ------------------ .. data :: trac.util.datefmt.localtz A global `LocalTimezone` instance. .. autoclass :: LocalTimezone .. data :: trac.util.datefmt.all_timezones List of all available timezones. If pytz_ is installed, this corresponds to a rich variety of "official" timezones, otherwise this corresponds to `FixedOffset` instances, ranging from GMT -12:00 to GMT +13:00. .. autofunction :: timezone .. autofunction :: get_timezone .. autoclass :: FixedOffset .. _pytz: http://pytz.sourceforge.net/ Trac-1.0.1/doc/api/trac_db_api.rst0000644000175200017520000000164612102610255015702 0ustar cbooscboos:mod:`trac.db.api` -- Trac DB abstraction layer =============================================== .. module :: trac.db.api Interfaces ---------- .. autoclass :: IDatabaseConnector :members: See also :extensionpoints:`trac.db.api.IDatabaseConnector`. Classes ------- The following classes are not meant to be used directly, but rather via the `~trac.env.Environment` methods `~trac.env.Environment.db_transaction` and `~trac.env.Environment.db_query`. .. autoclass :: QueryContextManager :show-inheritance: :members: .. autoclass :: TransactionContextManager :show-inheritance: :members: The above are both subclasses of `DbContextManager`: .. autoclass :: DbContextManager :members: Components ---------- .. autoclass :: DatabaseManager :members: Functions --------- .. autofunction :: get_column_names .. autofunction :: with_transaction See also -------- :teo:`wiki/TracDev/DatabaseApi` Trac-1.0.1/doc/api/trac_wiki_macros.rst0000644000175200017520000000110712102610255016763 0ustar cbooscboos:mod:`trac.wiki.macros` -- The standard set of Wiki macros ========================================================== .. module :: trac.wiki.macros The standard set of components corresponding to Wiki macros are not meant to be used directly from the API. You may study their implementation though, for getting inspiration. In particular, you'll see they all subclass the `WikiMacroBase` class, which provides a convenient way to implement a new `~trac.wiki.api.IWikiMacroProvider` interface. .. autoclass :: WikiMacroBase See also :teo:`wiki/WikiMacros#DevelopingCustomMacros`. Trac-1.0.1/doc/api/trac_core.rst0000644000175200017520000001011212102610255015400 0ustar cbooscboos:mod:`trac.core` -- the Trac "kernel" ===================================== Component model --------------- .. module :: trac.core The Trac component model is very simple, it is based on `Interface` classes that are used to document a particular set of methods and properties that must be defined by a `Component` subclass when it declares it *implements* that interface. .. autoclass :: trac.core.Interface :members: .. autoclass :: trac.core.Component :members: The static method `Component.implements` is never used as such, but rather via the global `implements` function. This globally registers that particular component subclass as an implementation of the listed interfaces. .. autofunction :: trac.core.implements For example:: class IStuffProvider(Interface): """All interfaces start by convention with an "I" and even if it's not a convention, in practice most interfaces are "Provider" of something ;-) """ def get_stuff(color=None): """We usually don't specify "self" here, but try to describe as precisely as possible how the method might be called and what is the expected return type.""" class ComponentA(Component): implements(IStuffProvider) # IStuffProvider methods def get_stuff(self, color=None): if not color or color == 'yellow': yield ('duck', "the regular waterproof plastic duck") The benefit of implementing an interface is to possibility to define an `ExtensionPoint` property for an `Interface`, in a `Component` subclass. Such a property provides a convenient way to retrieve *all* registered and enabled component instances for that interface. The enabling of components is the responsibility of the `ComponentManager`, see `~ComponentManager.is_component_enabled` below. .. autoclass :: trac.core.ExtensionPoint :members: Continuing the example:: class StuffModule(Component): stuff_providers = ExtensionPoint(IStuffProvider) def get_all_stuff(self, color=None): stuff = {} for provider in self.stuff_provider: for name, descr in provider.get_stuff(color) or []: stuff[name] = descr return stuff Note that besides going through an extension point, `Component` subclass instances can alternatively be retrieved directly by using the instantiation syntax. This is not an usual instantiation though, as this will always return the same instance in the given `ComponentManager` "scope" passed to the constructor:: >>> a1 = ComponentA(mgr) >>> a2 = ComponentA(mgr) >>> a1 is a2 True The same thing happens when retrieving components via an extension point, the retrieved instances belong to the same "scope" as the instance used to access the extension point:: >>> b = StuffModule(mgr) >>> any(a is a1 for a in b.stuff_providers) True .. autoclass :: trac.core.ComponentManager :members: In practice, there's only one kind of `ComponentManager` in the Trac application itself, the `trac.env.Environment`. More on components ------------------ We have seen above that one way to retrieve a `Component` instance is to call the constructor on a `ComponentManager` instance `mgr`:: a1 = ComponentA(mgr) This will eventually trigger the creation of a new `ComponentA` instance if there wasn't already one created for `mgr` [*]_. At this unique occasion, the constructor of the component subclass will be called *without arguments*, so if you define a constructor it must have the following signature:: def __init__(self): self.all_colors = set() Note that one should try to do as little as possible in a `Component` constructor. The most complex operation could be for example the allocation of a lock to control the concurrent access to some data members and guarantee thread-safe initialization of more costly resources on first use. Never do such costly initializations in the constructor itself. Miscellaneous ------------- .. autoclass :: trac.core.TracError :members: .. [*] Ok, it *might* happen that more than one component instance get created due to a race condition. This is usually harmless, see :teo:`#9418`. Trac-1.0.1/doc/api/trac_web_href.rst0000644000175200017520000000032112102610255016232 0ustar cbooscboos:mod:`trac.web.href` -- Creation of URLs ======================================== .. module :: trac.web.href This module mainly proposes the following class: .. autoclass :: trac.web.href.Href :members: Trac-1.0.1/doc/api/trac_web_chrome.rst0000644000175200017520000000254612102610255016576 0ustar cbooscboos:mod:`trac.web.chrome` -- Trac content generation for the Web ============================================================= .. automodule :: trac.web.chrome Interfaces ---------- .. autoclass :: trac.web.chrome.INavigationContributor :members: See also :extensionpoints:`trac.web.chrome.INavigationContributor` .. autoclass :: trac.web.chrome.ITemplateProvider :members: See also :extensionpoints:`trac.web.chrome.ITemplateProvider` Components ---------- .. autoclass :: trac.web.chrome.Chrome :members: Functions --------- Most of the helper functions are related to content generation, and in particular, (X)HTML content generation, in one way or another. .. autofunction :: trac.web.chrome.web_context .. autofunction :: trac.web.chrome.add_meta Web resources ~~~~~~~~~~~~~ .. autofunction :: trac.web.chrome.add_stylesheet .. autofunction :: trac.web.chrome.add_javascript .. autofunction :: trac.web.chrome.add_script .. autofunction :: trac.web.chrome.add_script_data Page admonitions ~~~~~~~~~~~~~~~~ .. autofunction :: trac.web.chrome.add_warning .. autofunction :: trac.web.chrome.add_notice Contextual Navigation ~~~~~~~~~~~~~~~~~~~~~ .. autofunction :: trac.web.chrome.add_link .. autofunction :: trac.web.chrome.add_ctxtnav .. autofunction :: trac.web.chrome.prevnext_nav Miscellaneous ~~~~~~~~~~~~~ .. autofunction :: auth_link Trac-1.0.1/doc/api/index.rst0000644000175200017520000000015212102610255014551 0ustar cbooscboos============= API Reference ============= .. toctree:: :maxdepth: 1 :glob: trac_* tracopt_* Trac-1.0.1/doc/api/trac_util.rst0000644000175200017520000000435112102610255015435 0ustar cbooscboos:mod:`trac.util` -- General purpose utilities ============================================= .. module :: trac.util The `trac.util` package is a hodgepodge of various categories of utilities. If a category contains enough code in itself, it earns a sub-module on its own, like the following ones: .. toctree:: :maxdepth: 1 :glob: trac_util_* Otherwise, the functions are direct members of the `trac.util` package (i.e. placed in the "__init__.py" file). Web related utilities --------------------- .. autofunction :: trac.util.get_reporter_id .. autofunction :: trac.util.content_disposition OS related utilies ------------------ .. autofunction :: copytree .. autofunction :: create_file .. autofunction :: create_unique_file .. autofunction :: getuser .. autofunction :: is_path_below .. autofunction :: makedirs .. autofunction :: read_file .. autofunction :: rename .. autoclass :: AtomicFile :members: .. autoclass :: NaivePopen :members: .. autoclass :: WindowsError Also defined on non-Windows systems (by a dummy OSError subclass). .. autoclass :: file_or_std .. data :: urandom The standard `os.urandom` when available, otherwise a reasonable replacement. Python "system" utilities ------------------------- Complements the `inspect`, `traceback` and `sys` modules. .. autofunction :: fq_class_name .. autofunction :: arity .. autofunction :: get_last_traceback .. autofunction :: get_lines_from_file .. autofunction :: get_frame_info .. autofunction :: import_namespace .. autofunction :: safe__import__ .. autofunction :: safe_repr .. autofunction :: get_doc Setuptools utilities -------------------- .. autofunction :: get_module_path .. autofunction :: get_sources .. autofunction :: get_pkginfo Cryptographic related utilities ------------------------------- .. autofunction :: hex_entropy .. autofunction :: md5crypt Data structures which don't fit anywhere else --------------------------------------------- .. autoclass :: Ranges :members: .. autofunction :: to_ranges .. autoclass :: lazy Algorithmic utilities --------------------- .. autofunction :: embedded_numbers .. autofunction :: pairwise .. autofunction :: partition .. autofunction :: as_int .. autofunction :: as_bool .. autofunction :: pathjoin Trac-1.0.1/doc/api/trac_versioncontrol_svn_fs.rst0000644000175200017520000000220012102610255021113 0ustar cbooscboos:mod:`trac.versioncontrol.svn_fs` -- Subversion backend for Trac ================================================================ This module can be considered to be private. However, it can serve as an example implementation of a version control backend. Speaking of Subversion, we use its ``svn.fs`` layer mainly, which means we need direct (read) access to the repository content. Though there's no documentation for the Python API per se, the doxygen documentation for the `C libraries`_ are usually enough. Another possible source of inspiration are the `examples`_ and the helper classes in the `bindings`_ themselves. .. _C libraries: http://svn.collab.net/svn-doxygen/files.html .. _examples: http://svn.apache.org/viewvc/subversion/trunk/tools/examples/ .. _bindings: http://svn.apache.org/viewvc/subversion/trunk/subversion/bindings/swig/python/svn/ .. automodule :: trac.versioncontrol.svn_fs Components ---------- .. autoclass :: SubversionConnector Concrete classes ---------------- .. autoclass :: SubversionRepository :members: .. autoclass :: SubversionNode :members: .. autoclass :: SubversionChangeset :members: Trac-1.0.1/doc/conf.py0000644000175200017520000002367412102610255013454 0ustar cbooscboos# -*- coding: utf-8 -*- # # Trac documentation build configuration file, created by # sphinx-quickstart on Wed May 14 09:05:13 2008. # # This file is execfile()d with the current directory set to its # containing dir. # # The contents of this file are pickled, so don't put values in the # namespace that aren't pickleable (module imports are okay, they're # removed automatically). # # All configuration values have a default value; values that are # commented out serve to show the default value. import sys, os # General substitutions. project = 'Trac' copyright = '2012, Edgewall Software' url = 'http://trac.edgewall.org' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0.1' # The full version, including alpha/beta/rc tags. release = '1.0.1' # Devel or Release mode for the documentation (if devel, include TODOs, # can also be used in conditionals: .. ifconfig :: devel) devel = True if devel: release += 'dev' # If your extensions are in another directory, add it here. If the # directory is relative to the documentation root, use os.path.abspath # to make it absolute, like shown here. # sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. # They can be extensions coming with Sphinx (named 'sphinx.ext.*') # or your custom ones. extensions = [] # -- Autodoc extensions.append('sphinx.ext.autodoc') autoclass_content = 'both' autodoc_member_order = 'bysource' # -- Conditional content (see setup() below) extensions.append('sphinx.ext.ifconfig') # -- Link to other Sphinx documentations extensions.append('sphinx.ext.intersphinx') intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None)} # -- Keep track of :todo: items extensions.append('sphinx.ext.todo') todo_include_todos = devel # -- PDF support via http://code.google.com/p/rst2pdf/ try: import rst2pdf extensions.append('rst2pdf.pdfbuilder') except ImportError: pass # Add any paths that contain templates here, relative to this directory. #templates_path = ['utils/templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. exclude_patterns = [ ] # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'trac' # The default role is a reference to some Python object default_role = 'py:obj' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'tracsphinx.css' html_theme = 'sphinxdoc' html_theme_options = { # 'linkcolor': '#B00', # 'visitedlinkcolor': '#B00', } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. html_logo = 'images/trac_logo.png' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['utils/'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_use_modindex = True # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Tracdoc' modindex_common_prefix = ['trac.', 'tracopt.'] # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'Trac.tex', 'Trac API Documentation', 'The Trac Team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # Options for PDF output # ---------------------- # (initially copied from # http://rst2pdf.googlecode.com/svn/tags/0.16/doc/manual.txt) # Grouping the document tree into PDF files. List of tuples # (source start file, target name, title, author, options). # # If there is more than one author, separate them with \\. # For example: r'Guido van Rossum\\Fred L. Drake, Jr., editor' # # The options element is a dictionary that lets you override # this config per-document. # For example, # ('index', u'MyProject', u'My Project', u'Author Name', # dict(pdf_compressed = True)) # would mean that specific document would be compressed # regardless of the global pdf_compressed setting. pdf_documents = [ ('index', 'trac_dev', project, u'The Trac Team'), ] # A comma-separated list of custom stylesheets (latest has higher precedence) pdf_stylesheets = [ 'sphinx', 'a4', 'trac', os.path.join(os.path.dirname(__file__), 'utils', 'trac_dev_pdf.style') ] # Create a compressed PDF # Use True/False or 1/0 # Example: compressed=True pdf_compressed = True # A colon-separated list of folders to search for fonts. Example: # pdf_font_path = ['/usr/share/fonts', '/usr/share/texmf-dist/fonts/'] # Language to be used for hyphenation support pdf_language = "en_US" # Mode for literal blocks wider than the frame. Can be # overflow, shrink or truncate pdf_fit_mode = "shrink" # Section level that forces a break page. # For example: 1 means top-level sections start in a new page # 0 means disabled pdf_break_level = 1 # When a section starts in a new page, force it to be 'even', 'odd', # or just use 'any' #pdf_breakside = 'any' # Insert footnotes where they are defined instead of # at the end. #pdf_inline_footnotes = True # verbosity level. 0 1 or 2 #pdf_verbosity = 0 # If false, no index is generated. pdf_use_index = True # If false, no modindex is generated. pdf_use_modindex = True # If false, no coverpage is generated. #pdf_use_coverpage = True # Name of the cover page template to use #pdf_cover_template = 'sphinxcover.tmpl' # Documents to append as an appendix to all manuals. #pdf_appendices = [] # Enable experimental feature to split table cells. Use it # if you get "DelayedTable too big" errors #pdf_splittables = False # Set the default DPI for images #pdf_default_dpi = 72 # Enable rst2pdf extension modules (default is only vectorpdf) # you need vectorpdf if you want to use sphinx's graphviz support #pdf_extensions = ['vectorpdf'] # Page template name for "regular" pages #pdf_page_template = 'cutePage' # Show Table Of Contents at the beginning? #pdf_use_toc = True # How many levels deep should the table of contents be? pdf_toc_depth = 9999 # Add section number to section references pdf_use_numbered_links = False # Background images fitting mode pdf_fit_background_mode = 'scale' def setup(app): # adding role for linking to InterTrac targets on t.e.o from docutils import nodes from docutils.parsers.rst import roles def teo_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # special case ticket references if text[0] == '#': ref = url + '/ticket/' + text[1:] else: ref = url + '/intertrac/' + text roles.set_classes(options) node = nodes.reference(rawtext, text, refuri=ref, **options) return [node], [] roles.register_canonical_role('teo', teo_role) def extensionpoints_role(name, rawtext, text, lineno, inliner, options={}, content=[]): ref = url + '/wiki/TracDev/PluginDevelopment/ExtensionPoints/' + text roles.set_classes(options) node = nodes.reference(rawtext, text + " extension point", refuri=ref, **options) return [node], [] roles.register_canonical_role('extensionpoints', extensionpoints_role) # ifconfig variables app.add_config_value('devel', '', True) Trac-1.0.1/doc/README0000644000175200017520000000004712102610255013022 0ustar cbooscboosSee the toplevel Makefile (make help). Trac-1.0.1/doc/dev/0000755000175200017520000000000012102610436012720 5ustar cbooscboosTrac-1.0.1/doc/dev/testing-plugins.rst0000644000175200017520000000463712102610255016617 0ustar cbooscboosWriting Tests for Plugins ========================= Testing a VCS backend --------------------- You'll need to make several subclasses to get this working in the current test infrastructure. But first, we start with some imports. These are pretty much required for all plugin tests:: from trac.tests.functional import (FunctionalTestSuite, FunctionalTestCaseSetup, FunctionalTwillTestCaseSetup, tc) from trac.tests.functional import testenv Now subclass :class:`~trac.tests.functional.testenv.FunctionalTestEnvironment`. This allows you to override methods that you need to set up your repo instead of the default Subversion one:: class GitFunctionalTestEnvironment(testenv.FunctionalTestEnvironment): repotype = 'git' def create_repo(self): os.mkdir(self.repodir) self.call_in_repo(["git", "init"]) self.call_in_repo(["git", "config", "user.name", "Test User"]) self.call_in_repo(["git", "config", "user.email", "test@example.com"]) def get_enabled_components(self): return ['tracext.git.*'] def get_repourl(self): return self.repodir + '/.git' repourl = property(get_repourl) Now you need a bit of glue that sets up a test suite specifically for your plugin's repo type. Any testcases within this test suite will use the same environment. No other changes are generally necessary on the test suite:: class GitFunctionalTestSuite(FunctionalTestSuite): env_class = GitFunctionalTestEnvironment Your test cases can call functions on either the :ref:`tester ` or :ref:`twill commands ` to do their job. Here's one that just verifies we were able to sync the repo without issue:: class EmptyRepoTestCase(FunctionalTwillTestCaseSetup): def runTest(self): self._tester.go_to_timeline() tc.notfind('Unsupported version control system') Lastly, there's some boilerplate needed for the end of your test file, so it can be run from the command line:: def suite(): # Here you need to create an instance of your subclass suite = GitFunctionalTestSuite() suite.addTest(EmptyRepoTestCase()) # ... suite.addTest(AnotherRepoTestCase()) return suite if __name__ == '__main__': unittest.main(defaultTest='suite') Trac-1.0.1/doc/dev/testing-database.rst0000644000175200017520000000711212102610255016671 0ustar cbooscboos.. _testing-database: Using an alternate database backend =================================== The unit tests don't really touch the db. The functional tests will, however, but if you're not using sqlite you need to setup the database yourself. Once it's set up, just set :envvar:`TRAC_TEST_DB_URI` to the connection string you would use for an :command:`trac-admin inittenv` and run the tests. .. index:: pair: Postgres; testing on pair: PostgreSQL; testing on Postgres -------- Testing against Postgres requires you to setup a postgres database and user for testing, then setting an environment variable. The test scripts will create a schema within the database, and on consecutive runs remove the schema. .. warning:: Do not run this against a live Trac db schema, the schema *will* be removed if it exists. On OS X and Linux, you can run the following to create the test database:: $ sudo -u postgres createuser -S -D -r -P -e tracuser $ sudo -u postgres createdb -O tracuser trac Windows:: > createuser -U postgres -S -D -r -P -e tracuser > createdb -U postgres -O tracuser trac Prior to running the tests, set the :envvar:`TRAC_TEST_DB_URI` variable. If you do not include a schema in the URI, the schema ``tractest`` will be used. OS X and Linux:: $ export TRAC_TEST_DB_URI=postgres://tracuser:password@localhost:5432/trac?schema=tractest $ make test Windows:: set TRAC_TEST_DB_URI=postgres://tracuser:password@localhost:5432/trac?schema=tractest Finally, run the tests as usual. Note that if you have already a test environment set up from a previous run, thesettings in testenv/trac/conf/trac.ini will be used. In particular, they will take precedence over the TRAC_TEST_DB_URI variable. Simply edit that trac.ini file or even remove the whole testenv folder if this gets in the way. If in some cases the tests go wrong and you can't run the tests again because the schema is already there, you can drop the schema manually like this: OS X and Linux:: > echo 'drop schema "tractest" cascade' | psql trac tracuser Windows:: > echo drop schema "tractest" cascade | psql trac tracuser If you later want to remove the test user and database, use the following: On OS X and Linux, you can run the following to create the test database:: $ sudo -u postgres dropdb tractest $ sudo -u postgres dropuser tractest Windows:: > dropdb -U postgres trac > dropuser -U postgres tracuser .. index:: pair: MySQL; testing on MySQL ----- Create the database and user as you normally would. See the MySqlDb_ page for more information. .. _MySqlDb: http://trac.edgewall.org/wiki/MySqlDb Example:: $ mysql -u root CREATE DATABASE trac DEFAULT CHARACTER SET utf8 COLLATE utf8_bin; CREATE USER tracuser IDENTIFIED BY 'password'; GRANT ALL ON trac.* TO tracuser; FLUSH PRIVILEGES; ^D $ export TRAC_TEST_DB_URI=mysql://tracuser:password@localhost/trac $ make test ... $ mysql -u root DROP DATABASE trac DROP USER tracuser ^D If you have better ideas on automating this, please contact us. Troubleshooting --------------- If you hit the following error message:: trac.core.TracError: The Trac Environment needs to be upgraded. This is because the test environment clean-up stopped half-way: the testenv/trac environment is still there, but the testenv/trac/conf/trac.ini file has already been removed. The default ticket workflow then requests an environment upgrade. Simply remove manually the whole testenv folder and, when using Postgres, remove the tractest schema manually as explained above. Trac-1.0.1/doc/dev/testing-intro.rst0000644000175200017520000000667512102610255016275 0ustar cbooscboos.. index:: pair: tests; prerequisites Running the tests ================= Prerequisites ------------- Beyond the standard installation prereqs, you also need: * `Pygments `_ (0.8+) * `Twill `_ (0.9+) * `Coverage `_ (for coverage) * `Figleaf `_ (0.6.1+, alternative for coverage) Additionally, if you're on Windows, you need to get fcrypt. See `Prerequisites on Windows`_ below for more information. .. index:: pair: tests; running Invoking the tests ------------------ Just run :command:`make test` in the Trac tree once you have everything installed. This will run the unit tests first, then the functional tests (if you have the dependencies) against SQLite. On a reasonably fast machine, the former takes 10 seconds and the latter a couple of minutes. A few environment variables will influence the way tests are executed: .. envvar:: TRAC_TEST_DB_URI Use another database backend than the default in-memory SQLite database. See :ref:`Using an alternate database backend ` for more. .. envvar:: TRAC_TEST_TRACD_OPTIONS Provide additional options to the standalone :command:`tracd` server used for the functional tests. The :file:`Makefile` is actually written in a way that allow you to get more control, if you want. Other possible usages:: make test=trac/tests/allwiki.py # run all the Wiki formatter tests make unit-test db=postgres # run only the unit tests with PostgreSQL make functional-test db=mysql # run only the functional tests with MySQL make test python=24 # run all the tests using Python 2.4 If you're running the tests on Windows and don't have cygwin, you'll need to manually run the tests using :command:`python trac\\test.py`, but this will run all the tests interleaved. Understanding failures ---------------------- Functional test failures can happen a few different ways. :Running trac-admin fails every time: Make sure the prereqs are met. In particular, that new enough Genshi is available and has :command:`python setup.py egg_info` run. :Repo creation fails: Subversion is required for the tests; they are not designed to run without it. :Repo creation works, other repo access fails: Probably a mismatch in svn bindings versus the :command:`svn` binary. :Twill errors which save to HTML: Check the html and see if there's a traceback contained in it. Chances are it has an obvious traceback with an error -- these are triggered on the server, not the tester, so they're difficult for us to show in the failure itself. If you can't decipher what the problem is from viewing the HTML, run the server manually and see what state that particular page is in. :Random weird platform issues: Please report them. :Can't remove files on Windows: Ugh. Please report them. :Reload tests fail: Chances are, you're on a Windows VM that has an unstable clock and FAT32 filesystem (which has a granularity of several seconds). If that's not the case, report it. :Coverage doesn't work with functional tests: Know issue, patches welcome... Prerequisites on Windows ------------------------ * You have to install fcrypt_ * You may install pywin32_ (optional, improve `subprocess` performance) .. _pywin32: http://sourceforge.net/projects/pywin32/ .. _fcrypt: http://carey.geek.nz/code/python-fcrypt/ Trac-1.0.1/doc/dev/testing-environment.rst0000644000175200017520000000044212102610255017470 0ustar cbooscboosTest Environment Helpers ======================== Functional Test Environment --------------------------- .. automodule :: trac.tests.functional.testenv :members: .. _functional-tester: Functional Tester ----------------- .. automodule :: trac.tests.functional.tester :members: Trac-1.0.1/doc/dev/testing.rst0000644000175200017520000000047412102610255015133 0ustar cbooscboosTesting in Trac =============== So, you'd like to make sure Trac works in your configuration. Excellent. Here's what you need to know. .. toctree:: :maxdepth: 2 testing-intro testing-core testing-environment testing-database testing-plugins .. todo :: write more, with testing-specific steps. Trac-1.0.1/doc/dev/testing-core.rst0000644000175200017520000000412312102610255016054 0ustar cbooscboosWriting Tests for Core ====================== Where tests belong ------------------ If it's a regression, it belongs in :file:`trac/tests/functional/testcases.py` for now. Module-specific tests generally already have a :file:`trac/$MOD/tests/functional.py` which you can add to. The environment is brought up as few times as possible, for speed, and your test order is guaranteed to be run in the order it's added to the suite, at the end of the file. .. _using-twill: Using Twill ----------- The definitive guide for Twill commands is the `Command Reference `_, but 90% of what you need is contained by convenience methods in :class:`~trac.tests.functional.tester.FunctionalTester`\ .\ :meth:`go_to_*`\ and the following few commands: :tc.find: Looks for a regex on the current page. If it isn't found, raise an exception. Example:: tc.find("\bPreferences\b") :tc.notfind: Like find, but raises an exception if it *is* there. Example:: tc.find(r"\bPreferences\b") :tc.follow: Find a link matching the regex, and simulate clicking it. Example:: tc.follow("Login") :tc.fv: Short for `formvalue`, fill in a field. Example:: tc.fv("searchform", "q", "ponies") :tc.submit: Submit the active form. Example:: tc.submit() Example ------- This is how you might construct a test that verifies that admin users can see detailed version information. Start with the navigation. You shouldn't rely on the browser being in any specific state, so begin with :meth:`FunctionalTester.go_to_*`. :: def test_about_page(self): self._tester.logout() # Begin by logging out. self._tester.go_to_front() # The homepage has a link we want tc.follow("About") # Follow the link with "About" in it tc.find("Trac is a web-based software") tc.notfind("Version Info") self._tester.login("admin") self._tester.go_to_front() tc.follow("About") tc.find("Trac is a web-based software") tc.find("Version Info") Trac-1.0.1/doc/utils/0000755000175200017520000000000012102610436013302 5ustar cbooscboosTrac-1.0.1/doc/utils/runepydoc.py0000644000175200017520000000337712102610255015675 0ustar cbooscboos# Simple wrapper script needed to run epydoc import sys try: from epydoc.cli import cli except ImportError: print>>sys.stderr, "No epydoc installed (see http://epydoc.sourceforge.net)" sys.exit(2) # Epydoc 3.0.1 has some trouble running with recent Docutils (>= 0.6), # so we work around this bug, following the lines of the fix in # https://bugs.gentoo.org/attachment.cgi?id=210118 # (see http://bugs.gentoo.org/287546) try: from docutils.nodes import Text if not hasattr(Text, 'data'): setattr(Text, 'data', property(lambda self: self.astext())) except ImportError: print>>sys.stderr, "docutils is needed for running epydoc " \ "(see http://docutils.sourceforge.net)" sys.exit(2) # Epydoc doesn't allow much control over the generated graphs. This is # bad especially for the class graph for Component which has a lot of # subclasses, so we need to force Left-to-Right mode. # from epydoc.docwriter.html import HTMLWriter # HTMLWriter_render_graph = HTMLWriter.render_graph # def render_graph_LR(self, graph): # if graph: # graph.body += 'rankdir=LR\n' # return HTMLWriter_render_graph(self, graph) # HTMLWriter.render_graph = render_graph_LR # Well, LR mode doesn't really look better... # the ASCII-art version seems better in most cases. # Workaround "visiting unknown node type" error due to `.. note ::` # This was due to the lack of Admonitions transforms. Add it. from epydoc.markup.restructuredtext import _DocumentPseudoWriter from docutils.transforms import writer_aux orig_get_transforms = _DocumentPseudoWriter.get_transforms def pseudo_get_transforms(self): return orig_get_transforms(self) + [writer_aux.Admonitions] _DocumentPseudoWriter.get_transforms = pseudo_get_transforms # Run epydoc cli() Trac-1.0.1/doc/utils/epydoc.css0000644000175200017520000001303212102610255015275 0ustar cbooscbooshtml { background: #4b4d4d url(images/bkgnd_pattern.png); margin: 0; padding: 1em 1em 3em; } body { background: #fff url(images/vertbars.png) repeat-x; border: 1px solid #000; color: #000; margin: 1em 0; padding: 0 1em 1em; } body, th, td { font: normal small Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; } h1, h2, h3, h4 { font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; font-weight: bold; letter-spacing: -0.018em; } h1 { font-size: 19px; margin: 2em 0 .5em; } h2 { font-size: 16px; margin: 1.5em 0 .5em; } h3 { font-size: 14px; margin: 1.2em 0 .5em; } hr { border: none; border-top: 1px solid #ccb; margin: 2em 0; } p { margin: 0 0 1em; } :link, :visited { text-decoration: none; border-bottom: 1px dotted #bbb; color: #b00; } :link:hover, :visited:hover { background-color: #eee; color: #555; } table { border: none; border-collapse: collapse; } table.navbar { background: #000; color: #fff; margin: 2em 0 .33em; } table.navbar th { border: 1px solid #000; font-weight: bold; padding: 1px; } table.navbar :link, table.navbar :visited { border: none; color: #fff; } table.navbar :link:hover, table.navbar :visited:hover { background: none; text-decoration: underline overline; } table.navbar th.navbar-select { background: #fff; color: #000; } span.breadcrumbs { color: #666; font-size: 95%; } h1.epydoc { border: none; color: #666; font-size: x-large; margin: 1em 0 0; padding: 0; } pre.base-tree { color: #666; margin: 0; padding: 0; } pre.base-tree :link, pre.base-tree :visited { border: none; } pre.py-doctest, pre.variable, pre.rst-literal-block { background: #eee; border: 1px solid #e6e6e6; color: #000; margin: 1em; padding: .25em; overflow: auto; } pre.variable { margin: 0; } /* Summary tables */ table.summary { margin: .5em 0; } table.summary tr.table-header { background: #f7f7f0; } table.summary td.table-header { color: #666; font-weight: bold; } table.summary th.group-header { background: #f7f7f0; color: #666; font-size: 90%; font-weight: bold; text-align: left; } table.summary th, table.summary td { border: 1px solid #d7d7d7; } table.summary th th, table.summary td td { border: none; } table.summary td.summary table td { color: #666; font-size: 90%; } table.summary td.summary table br { display: none; } table.summary td.summary span.summary-type { font-family: monospace; font-size: 90%; } table.summary td.summary span.summary-type code { font-size: 110%; } p.indent-wrapped-lines { color: #999; font-size: 85%; margin: 0; padding: 0 0 0 7em; text-indent: -7em; } p.indent-wrapped-lines code { color: #999; font-size: 115%; } p.indent-wrapped-lines :link, p.indent-wrapped-lines :visited { border: none; } .summary-sig { display: block; font-family: monospace; font-size: 120%; margin-bottom: .5em; } .summary-sig-name { font-weight: bold; } .summary-sig-arg { color: #333; } .summary-sig :link, .summary-sig :visited { border: none; } .summary-name { font-family: monospace; font-weight: bold; } /* Details tables */ table.details { margin: 2em 0 0; } div table.details { margin-top: 0; } table.details tr.table-header { background: transparent; } table.details td.table-header { border-bottom: 1px solid #ccc; padding: 2em 0 0; } table.details span.table-header { font: bold 140% Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; letter-spacing: -0.018em; } table.details th, table.details td { border: none; } table.details th th, table.details td td { border: none; } table.details td { padding-left: 2em; } table.details td td { padding-left: 0; } table.details h3.epydoc { margin-left: -2em; } table.details h3.epydoc .sig { color: #999; font-family: monospace; } table.details h3.epydoc .sig-name { color: #000; } table.details h3.epydoc .sig-arg { color: #666; } table.details h3.epydoc .sig-default { font-size: 95%; font-weight: normal; } table.details h3.epydoc .sig-default code { font-weight: normal; } table.details h3.epydoc .fname { color: #999; font-size: 90%; font-style: italic; font-weight: normal; line-height: 1.6em; } dl dt { color: #666; margin-top: 1em; } dl dd { margin: 0; padding-left: 2em; } dl.fields { margin: 1em 0; padding: 0; } dl.fields dt { color: #666; margin-top: 1em; } dl.fields dd ul { margin: 0; padding: 0; } div.fields { font-size: 90%; margin: 0 0 2em 2em; } div.fields p { margin-bottom: 0.5em; } table td.footer { color: #999; font-size: 85%; margin-top: 3em; padding: 0 3em 1em; position: absolute; width: 80%; } table td.footer :link, table td.footer :visited { border: none; color: #999; } table td.footer :link:hover, table td.footer :visited:hover { background: transparent; text-decoration: underline; } /* Syntax highlighting */ .py-prompt, .py-more, .variable-ellipsis, .variable-op { color: #999; } .variable-group { color: #666; font-weight: bold; } .py-string, .variable-string, .variable-quote { color: #093; } .py-comment { color: #06f; font-style: italic; } .py-keyword { color: #00f; } .py-output { background: #f6f6f0; color: #666; font-weight: bold; } /* Index */ table.link-index { background: #f6f6f0; border: none; margin-top: 1em; } table.link-index td.link-index { border: none; font-family: monospace; font-weight: bold; padding: .5em 1em; } table.link-index td table, table.link-index td td { border: none; } table.link-index .index-where { color: #999; font-family: Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; font-size: 90%; font-weight: normal; line-height: 1.6em; } table.link-index .index-where :link, table.link-index .index-where :visited { border: none; color: #666; } h2.epydoc { color: #999; font-size: 200%; line-height: 10px; } Trac-1.0.1/doc/utils/checkapidoc.py0000644000175200017520000001412712102610255016115 0ustar cbooscboos# -*- coding: utf-8 -*- """Trac API doc checker Verify that all symbols belonging to modules already documented in the doc/api Sphinx sources are referenced. See http://trac.edgewall.org/wiki/TracDev/ApiDocs """ import fnmatch import os import re import sys excluded_docs = ['index.rst'] api_doc = 'doc/api' def usage(cmd): print "Usage: %s [FILE...]" % (cmd,) print print "FILE is a %s file and can be a glob pattern." % (api_doc,) print "If no files are given, check all." exit(0) def main(argv): api_files = rst_files = [rst for rst in os.listdir('doc/api') if fnmatch.fnmatch(rst, '*.rst') and rst not in excluded_docs] cmd = argv.pop(0) def has(*options): for opt in options: if opt in argv: return argv.pop(argv.index(opt)) if has('-h', '--help'): usage(cmd) verbose = has('-v', '--verbose') only_documented = not has('-a', '--all') if argv: given_files = [] for arg in argv: arg = arg.replace('\\', '/').replace(api_doc + '/', '') arg = arg.replace('.rst', '') + '.rst' if '*' in arg: # glob pattern given_files += [rst for rst in api_files if fnmatch.fnmatch(rst, arg)] elif arg in api_files: given_files.append(arg) api_files = given_files rst_basenames = sorted(f[:-4] for f in rst_files) for rst in api_files: basename = rst.replace('.rst', '') if verbose or len(api_files) > 1: print "== Checking %s ... " % (rst,) check_api_doc(basename, verbose, only_documented, any(f.startswith(basename) and f != basename for f in rst_basenames)) def check_api_doc(basename, verbose, only_documented, has_submodules): module_name = basename.replace('_', '.') try: module = __import__(module_name, globals(), {}, ['__all__']) except ImportError, e: print "Skipping %s (%s)" % (basename, e) return all = getattr(module, '__all__', None) if not all: print "Warning: %s doesn't define __all__, using exported symbols." % ( module_name,) all = get_default_symbols(module, only_documented, has_submodules) no_apidoc = getattr(module, '__no_apidoc__', None) if no_apidoc: if isinstance(no_apidoc, basestring): no_apidoc = [s.strip() for s in no_apidoc.split()] all = list(set(all) - set(no_apidoc)) symbols, keywords = get_sphinx_documented_symbols(basename + '.rst') for symbol in sorted(all): if symbol in symbols: if verbose: print " - OK %14s :: %s" % ( keywords[symbols.index(symbol)], symbol) else: value = getattr(module, symbol) cls = getattr(value, '__class__', None) keyword = 'data' if cls.__name__ in ('function', 'instancemethod'): keyword = 'function' elif cls.__name__ == 'module': keyword = 'module' else: keyword = 'class' print " * .. %14s :: %s" % ('auto' + keyword, symbol) sphinx_doc_re = re.compile(r''' ^.. \s+ ((?:py:|auto)(?:module|class|function|attribute)|data) # keyword \s* :: \s* ([\w\.]+) # symbol ''', re.MULTILINE | re.VERBOSE) def get_sphinx_documented_symbols(rst): doc = file(os.path.join(api_doc, rst)).read() symbols, keywords = [], [] for k, s in sphinx_doc_re.findall(doc): symbols.append(s.split('.')[-1]) keywords.append(k) return symbols, keywords def get_default_symbols(module, only_documented, has_submodules): public = get_public_symbols(module) - get_imported_symbols(module, has_submodules) # eliminate modules all = [] for symbol in public: try: __import__(symbol) except ImportError: all.append(symbol) # only keep symbols having a docstring if only_documented: documented = [] for symbol in all: value = getattr(module, symbol) if value.__doc__ and (not getattr(value, '__class__', None) or value.__doc__ != value.__class__.__doc__): documented.append(symbol) all = documented return all def get_public_symbols(m): return set(symbol for symbol in dir(m) if not symbol.startswith('_')) import_from_re = re.compile(r''' ^ \s* from \s+ ([\w\.]+) \s+ import \s+ # module ( \* # all symbols | %s (?: [\s\\]* , [\s\\]* %s)* # list of symbols | \( \s* %s (?: \s* , \s* %s)* \s* \) # list of symbols in parenthesis ) ''' % ((r'(?:\w+|\w+\s+as\s+\w+)',) * 4), re.MULTILINE | re.VERBOSE) remove_original_re = re.compile(r'\w+\s+as', re.MULTILINE) def get_imported_symbols(module, has_submodules): src_filename = module.__file__.replace('\\', '/').replace('.pyc', '.py') if src_filename.endswith('/__init__.py') and not has_submodules: return set() src = file(src_filename).read() imported = set() for mod, symbol_list in import_from_re.findall(src): symbol_list = symbol_list.strip() if symbol_list == '*': try: imported_module = __import__(mod, globals(), {}, ['__all__']) symbols = set(getattr(imported_module, '__all__', None) or get_public_symbols(imported_module)) except ImportError: print "Warning: 'from %s import *' couldn't be resolved" % ( mod,) continue else: if symbol_list and symbol_list[0] == '(' and symbol_list[-1] == ')': symbol_list = symbol_list[1:-1] symbols = set(remove_original_re.sub('', symbol_list) .replace('\\', '').replace(',', ' ').split()) imported |= symbols return imported if __name__ == '__main__': main(sys.argv) Trac-1.0.1/doc/utils/trac_dev_pdf.style0000644000175200017520000000274612102610255017014 0ustar cbooscboos# This rst2pdf stylesheet, written in RSON format (http://code.google.com/p/rson/) # # Some guidelines: # - the base styles are defined in conf.py in pdf_stylesheets, the main one # being the 'sphinx' stylesheet, look at it to see what can be adapted (see # http://rst2pdf.googlecode.com/svn/tags/0.16/rst2pdf/styles/sphinx.style) # - the numerical color specifications need to have 6 hex digits # (i.e. #A00 won't work) fontsAlias: stdFont: Helvetica stdBold: Helvetica-Bold stdItalic: Helvetica-Oblique stdBoldItalic: Helvetica-BoldOblique stdSans: Helvetica stdSansBold: Helvetica-Bold stdSansItalic: Helvetica-Oblique stdSansBoldItalic: Helvetica-BoldOblique stdMono: Courier stdMonoItalic: Courier-Oblique stdMonoBold: Courier-Bold stdMonoBoldItalic: Courier-BoldOblique stdSerif: Times-Roman # Note: not using a predefined font results in extremely slow runtime on Windows # so please stick with Helvetica, Courier and the like for now linkColor: #B00000 # The "standard" Trac color for links styles: admonition-heading: {fontSize: 80%, textColor: #A00000} code: backColor: #F5F5DC borderColor: #D5D5CC borderPadding: [6, 0, 6, 4] spaceBefore: 2 heading: backColor: #F2F2F2 borderColor: #CCCCCC borderPadding: [6, 0, 2, 10] borderWidth: 0.3 fontName: stdSansBold fontSize: 10 leading: 10 leftIndent: 0 spaceBefore: 16 textColor: #303050 # Some styles missing in sphinx.style: file: fontName: stdMono function: fontName: stdMonoItalic Trac-1.0.1/doc/utils/tracsphinx.css0000644000175200017520000000530512102610255016201 0ustar cbooscboos/* Trac specific styling */ @import url("sphinxdoc.css"); /* Structure */ div.footer { background-color: #4b4d4d; text-align: center; } div.bodywrapper { border-right: none; } /* Sidebar */ div.sphinxsidebarwrapper { -moz-box-shadow: 2px 2px 7px 0 grey; -webkit-box-shadow: 2px 2px 7px 0 grey; box-shadow: 2px 2px 7px 0 grey; padding: 0 0 1px .4em; } div.sphinxsidebar h3 a, div.sphinxsidebar h4 a { color: #b00; } div.sphinxsidebar h3, div.sphinxsidebar h4 { padding: 0; color: black; } div.sphinxsidebar h3, div.sphinxsidebar h4 { background: none; border: none; border-bottom: 1px solid #ddd; } div.sphinxsidebar input { border: 1px solid #d7d7d7; } p.searchtip { font-size: 90%; color: #999; } /* Navigation */ div.related ul li a { color: #b00 } div.related ul li a:hover { color: #b00; } /* Content */ body { font: normal 13px Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; background-color: #4b4d4d; border: none; border-top: 1px solid #aaa; } h1, h2, h3, h4 { font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif; font-weight: bold; letter-spacing: -0.018em; page-break-after: avoid; } h1 { color: #555 } h2 { border-bottom: 1px solid #ddd } div.body a { text-decoration: none } a, a tt { color: #b00 } a:visited, a:visited tt { color: #800 } :link:hover, :visited:hover, a:link:hover tt, a:visited:hover tt { background-color: #eee; color: #555; } a.headerlink, a.headerlink:hover { color: #d7d7d7 !important; font-size: .8em; font-weight: normal; vertical-align: text-top; margin: 0; padding: .5em; } a.headerlink:hover { background: none; } div.body h1 a, div.body h2 a, div.body h3 a, div.body h4 a, div.body h5 a, div.body h6 a { color: #d7d7d7 !important; } dl.class { -moz-box-shadow: 1px 1px 6px 0 #888; -webkit-box-shadow: 1px 1px 6px 0 #888; box-shadow: 1px 1px 6px 0 #888; padding: .5em; } dl.function { margin-bottom: 24px; } dl.class > dt, dl.function > dt { border-bottom: 1px solid #ddd; } th.field-name { white-space: nowrap; font-size: 90%; color: #555; } td.field-body > ul { list-style-type: square; } td.field-body > ul > li > strong { font-weight: normal; font-style: italic; } /* Admonitions */ div.admonition p.admonition-title, div.warning p.admonition-title { background: none; color: #555; border: none; } div.admonition { background: none; border: none; border-left: 2px solid #acc; } div.warning { background: none; border: none; border-left: 3px solid #c33; } /* Search */ dl:target, dt:target, .highlighted { background-color: #ffa } Trac-1.0.1/doc/utils/epydoc.conf0000644000175200017520000001101312102610255015427 0ustar cbooscboos# from http://epydoc.sourceforge.net/manual-reference.html#command-line-usage [epydoc] # Epydoc section marker (required by ConfigParser) # The list of objects to document. Objects can be named using # dotted names, module filenames, or package directory names. # Alases for this option include "objects" and "values". modules: trac/, tracopt/ # The type of output that should be generated. Should be one # of: html, text, latex, dvi, ps, pdf. output: html # The path to the output directory. May be relative or absolute. target: build/doc/epydoc # An integer indicating how verbose epydoc should be. The default # value is 0; negative values will supress warnings and errors; # positive values will give more verbose output. verbosity: 1 # A boolean value indicating that Epydoc should show a tracaback # in case of unexpected error. By default don't show tracebacks debug: 1 # If True, don't try to use colors or cursor control when doing # textual output. The default False assumes a rich text prompt simple-term: 0 ### Generation options # The default markup language for docstrings, for modules that do # not define __docformat__. Defaults to epytext. docformat: reStructuredText # Whether or not parsing should be used to examine objects. parse: yes # Whether or not introspection should be used to examine objects. introspect: yes # Don't examine in any way the modules whose dotted name match this # regular expression pattern. #exclude # Don't perform introspection on the modules whose dotted name match this # regular expression pattern. #exclude-introspect # Don't perform parsing on the modules whose dotted name match this # regular expression pattern. #exclude-parse # The format for showing inheritance objects. # It should be one of: 'grouped', 'listed', 'included'. inheritance: listed # Whether or not to inclue private variables. (Even if included, # private variables will be hidden by default.) private: no # Whether or not to list each module's imports. imports: yes # Whether or not to include syntax highlighted source code in # the output (HTML only). sourcecode: yes # Whether or not to includea a page with Epydoc log, containing # effective option at the time of generation and the reported logs. include-log: yes ### Output options # The documented project's name. name: Trac # The CSS stylesheet for HTML output. Can be the name of a builtin # stylesheet, or the name of a file. css: doc/utils/epydoc.css # The documented project's URL. url: http://trac.edgewall.org # HTML code for the project link in the navigation bar. If left # unspecified, the project link will be generated based on the # project's name and URL. #link: My Cool Project # The "top" page for the documentation. Can be a URL, the name # of a module or class, or one of the special names "trees.html", # "indices.html", or "help.html" top: index.html # An alternative help file. The named file should contain the # body of an HTML file; navigation bars will be added to it. #help: my_helpfile.html # Whether or not to include a frames-based table of contents. frames: no # Whether each class should be listed in its own section when # generating LaTeX or PDF output. separate-classes: no ### API linking options # Define a new API document. A new interpreted text role # will be created #external-api: epydoc # Use the records in this file to resolve objects in the API named NAME. #external-api-file: epydoc:api-objects.txt # Use this URL prefix to configure the string returned for external API. #external-api-root: epydoc:http://epydoc.sourceforge.net/api ### Graph options # The list of graph types that should be automatically included # in the output. Graphs are generated using the Graphviz "dot" # executable. Graph types include: "classtree", "callgraph", # "umlclass". Use "all" to include all graph types #graph: umlclasstree ## prefer the ASCII-art version for now, see comments in doc/runepydoc.py # The path to the Graphviz "dot" executable, used to generate # graphs. ## dotpath: /usr/local/bin/dot ## see Makefile # The name of one or more pstat files (generated by the profile # or hotshot module). These are used to generate call graphs. ## pstat: profile.out # Specify the font used to generate Graphviz graphs. # (e.g., helvetica or times). graph-font: Helvetica # Specify the font size used to generate Graphviz graphs. graph-font-size: 10 ### Return value options # The condition upon which Epydoc should exit with a non-zero # exit status. Possible values are error, warning, docstring_warning #fail-on: error Trac-1.0.1/doc/images/0000755000175200017520000000000012102610436013407 5ustar cbooscboosTrac-1.0.1/doc/images/trac_logo.png0000644000175200017520000001423612102610255016073 0ustar cbooscboosPNG  IHDR9utRNSԑ pHYs  >IDATxy\RKDJMܲrԢ,--Vm/X}S˭vDEEA_~"Ri?8ggx33à<0fuPxLЯ$,(qPDs +[bԝ3E< [hUZDf"RUC r) sƍV RDuCoV #j_elii߾Do|J$*ix{KSSS_&}# xêám]"mMRv#G~#xêڵVzBc2yL90c,.AU۶KdVmݞ~y!OR05huMIit]iM sGZV]K+w< fjPfj֬svR63yooop8q2߿J2 n &&&޼qJVTTkh={E %%$D"1\.177?~ Ӄ7H! t:it;v>D_u7@=Wِ0ٹ[ _^ޞ`e0T#oŊ_EPj|yG xML~juP})+})zWUa4 bz/?Q15t6T Wu +ܒ!p[攧^MtT ֭ᯡwm?Ϟ͘1bh9A/_Is||B||ŠO>-A`ؾŘN)5s8sssc Uu[sۧ&dMq*Vl+|-RQ>Nʋ66;=_U)?}G\n綶vgΜު#{Q5zT` =*,8DT i+*6'C&'Oo`fVZ!* :ѵ>MuㆷZz &Lqc~po77nV!0C& iWRrRJd_)by)<{pb:*>c91xê9rL\ڮ=B)3,au?$:h,T˿ysԷ]xTۤݻwihjpE5F8[pv{yy:;㍌ t''nܧJ5p䶜\ _03_`!z6q[ĶUWA33ae.u`  U`68ԩ˖-pB#LLFxzz._LVj|CUQ(}[[ե`AȢ_233<~loom0^Q)LG[& ȯ9H$1ҸqN6M4]IQ۲VVU#̦̚&KKK(L.8ˢŢY...1ci7Џz6ť9` C!K.SЍa@VAMsEcDb@ $ \ri~ȗ)5 մjqb...W._G*++'$JYo'a3#džw7?EחqKxP/x˯\J$"q8Q߰ )CV%DzWNŒR& u8uY# Qj"O N4`)Ҟqh%2G5YN臅I#ym^_`_oݺ]J)ҪtzeEEņ Tտ1,݄vX/ou.Qwv4XYWG[HbU[C6VjܠyV!Sz>;/^`nOAMMrI!-QsTPS3wm͑c"ÑZuV_F[[[@tTNݺQ\a/Y>V[.[^2 tf$~ pz¾'+kWNBE4zġ?վ؎b` VT$A@}dMqA*+*Eĵ/Q8Y ۺ |q7k|c|CqlsSʌ@O֣gsUrc ͮ4ꔒ^|("+q7H`02GZ1-**+:j~~~?m}RD躴>4%<.'+*GK b:;;Y_PX(H6l(YgP D;; 0 &.|୸>! Or))n܃/,!XOӵ1VV&N>l8---qYϞ>;v쫀 ==$A?WW*R+aBN~sg߷PDZzo]rј755MO۷նm$MKSq3#hzQ&N/.R 94uzѶ766YWm'"mml%vCIhU4Z|TNУ-Qf*Zur -]"E2{l{̼yriUn ٰ,--/_ Rx}3Shf1KF"]2ϟ&0s Zvӥ6l7Зφ)6RHؗ.ЂqnORyaQ]{&':D[SܖK[P`LfJNP*Õ)dk@EEH[ttt _]]rtСCh~}SӚ;[nxLKꍷn&f\2Oe1c`8FPrưiOamK{ usC{\?3E4uPuJ؛5{S;- O$h-[ԝ&5%%!^8ԉ?LIIqnJzzʴJNqLx#%FzVf !. %1'hv9l?N< SQaJ(ŭwUP⎝ۣΜNH=fTGF"ty+gY[Op{yĎ_dO-{w}le5gkٳwVme5gkoMݿ%βf<̨Ze͝tڕW9ٓ'O*.)_TLڽgׅ 玟8zYAΞ~8`ڴbw|U+c.C( 0D"U?pX{wE BC]#99@ׇl]jjj&fq867.T :^FEFBfBWwv+՚AU\PVV̔ Xޮ͆~ml6jpH$𴶶F"x<̬DEB[X,EEE/o13b!,ꢍF47^*55a`FK Ҳ2~:D kirx<777t`\.pswKJJp===c<̟QgϝpVV<@۷ ssU ainnljkyTO x$7@o*u<̽78񱱱qqq^fM`ĬyJPp ٗb.-_鄛7oP+*|||4Կ28ֱ͛_-8ǧTF 6cx{Y >&l:U*0pweEE@@@'SQIi欙rt‘C1\\]ܼܿ$lnF}mƌh3g qD6-_lnppzf&%--->>>BعkgDDD`Tvb0YBN:w}mLCWx#*}2 o i>ro^qV`0"ODVh  ˤ{QJj\ 66{߾}n;R.(,looVvzߚo)5 5 4bGIENDB`Trac-1.0.1/doc/images/bkgnd_pattern.png0000644000175200017520000000016012102610255016733 0ustar cbooscboosPNG  IHDR,XPLTEKMMUWWcg%IDATcq`X(BwZGA cҿj<IENDB`Trac-1.0.1/doc/images/vertbars.png0000644000175200017520000000041612102610255015745 0ustar cbooscboosPNG  IHDR o pHYs  tIME ((IDAT8˕I  cT*U RuT0 T89 im2ag C˲6ZRrlF[gHuNk 9 C{&CpI[gHuh- ~9buNh .n48PIENDB`Trac-1.0.1/doc/index.rst0000644000175200017520000000124312102610255014002 0ustar cbooscboos================================ Trac |version| API Documentation ================================ :Release: |release| :Date: |today| *This is work in progress. The API is not yet fully covered, but what you'll find here should be accurate, otherwise it's a bug and you're welcome to open a ticket for reporting the problem.* Content ======= .. toctree:: :maxdepth: 1 api/index dev/testing .. toctree:: :hidden: glossary .. ifconfig:: devel .. toctree:: :hidden: todo Indices and tables ================== * :ref:`General Index ` * :ref:`modindex` * :ref:`search` * :doc:`glossary` .. ifconfig:: devel * :doc:`todo` Trac-1.0.1/doc/todo.rst0000644000175200017520000000006512102610255013641 0ustar cbooscboosDocumentation TODO ================== .. todolist:: Trac-1.0.1/trac/0000755000175200017520000000000012102610436012326 5ustar cbooscboosTrac-1.0.1/trac/test.py0000755000175200017520000003773612102610255013701 0ustar cbooscboos#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2003-2009 Edgewall Software # Copyright (C) 2003-2005 Jonas Borgström # Copyright (C) 2005 Christopher Lenz # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. # # Author: Jonas Borgström # Christopher Lenz from __future__ import with_statement import doctest import os import unittest import sys try: from babel import Locale locale_en = Locale.parse('en_US') except ImportError: locale_en = None from trac.config import Configuration from trac.core import Component, ComponentManager from trac.env import Environment from trac.db.api import _parse_db_str, DatabaseManager from trac.db.sqlite_backend import SQLiteConnection from trac.db.util import ConnectionWrapper import trac.db.postgres_backend import trac.db.mysql_backend from trac.ticket.default_workflow import load_workflow_config_snippet from trac.util import translation def Mock(bases=(), *initargs, **kw): """ Simple factory for dummy classes that can be used as replacement for the real implementation in tests. Base classes for the mock can be specified using the first parameter, which must be either a tuple of class objects or a single class object. If the bases parameter is omitted, the base class of the mock will be object. So to create a mock that is derived from the builtin dict type, you can do: >>> mock = Mock(dict) >>> mock['foo'] = 'bar' >>> mock['foo'] 'bar' Attributes of the class are provided by any additional keyword parameters. >>> mock = Mock(foo='bar') >>> mock.foo 'bar' Objects produces by this function have the special feature of not requiring the 'self' parameter on methods, because you should keep data at the scope of the test function. So you can just do: >>> mock = Mock(add=lambda x,y: x+y) >>> mock.add(1, 1) 2 To access attributes from the mock object from inside a lambda function, just access the mock itself: >>> mock = Mock(dict, do=lambda x: 'going to the %s' % mock[x]) >>> mock['foo'] = 'bar' >>> mock.do('foo') 'going to the bar' Because assignments or other types of statements don't work in lambda functions, assigning to a local variable from a mock function requires some extra work: >>> myvar = [None] >>> mock = Mock(set=lambda x: myvar.__setitem__(0, x)) >>> mock.set(1) >>> myvar[0] 1 """ if not isinstance(bases, tuple): bases = (bases,) cls = type('Mock', bases, {}) mock = cls(*initargs) for k, v in kw.items(): setattr(mock, k, v) return mock class MockPerm(object): """Fake permission class. Necessary as Mock can not be used with operator overloading.""" username = '' def has_permission(self, action, realm_or_resource=None, id=False, version=False): return True __contains__ = has_permission def __call__(self, realm_or_resource, id=False, version=False): return self def require(self, action, realm_or_resource=None, id=False, version=False): pass assert_permission = require class TestSetup(unittest.TestSuite): """ Test suite decorator that allows a fixture to be setup for a complete suite of test cases. """ def setUp(self): """Sets up the fixture, and sets self.fixture if needed""" pass def tearDown(self): """Tears down the fixture""" pass def run(self, result): """Setup the fixture (self.setUp), call .setFixture on all the tests, and tear down the fixture (self.tearDown).""" self.setUp() if hasattr(self, 'fixture'): for test in self._tests: if hasattr(test, 'setFixture'): test.setFixture(self.fixture) unittest.TestSuite.run(self, result) self.tearDown() return result def _wrapped_run(self, *args, **kwargs): "Python 2.7 / unittest2 compatibility - there must be a better way..." self.setUp() if hasattr(self, 'fixture'): for test in self._tests: if hasattr(test, 'setFixture'): test.setFixture(self.fixture) unittest.TestSuite._wrapped_run(self, *args, **kwargs) self.tearDown() class TestCaseSetup(unittest.TestCase): def setFixture(self, fixture): self.fixture = fixture # -- Database utilities def get_dburi(): dburi = os.environ.get('TRAC_TEST_DB_URI') if dburi: scheme, db_prop = _parse_db_str(dburi) # Assume the schema 'tractest' for Postgres if scheme == 'postgres' and \ not db_prop.get('params', {}).get('schema'): if '?' in dburi: dburi += "&schema=tractest" else: dburi += "?schema=tractest" return dburi return 'sqlite::memory:' def reset_sqlite_db(env, db_prop): dbname = os.path.basename(db_prop['path']) with env.db_transaction as db: tables = db("SELECT name FROM sqlite_master WHERE type='table'") for table in tables: db("DELETE FROM %s" % table) return tables def reset_postgres_db(env, db_prop): with env.db_transaction as db: dbname = db.schema if dbname: # reset sequences # information_schema.sequences view is available in PostgreSQL 8.2+ # however Trac supports PostgreSQL 8.0+, uses # pg_get_serial_sequence() for seq in db(""" SELECT sequence_name FROM ( SELECT pg_get_serial_sequence(%s||table_name, column_name) AS sequence_name FROM information_schema.columns WHERE table_schema=%s) AS tab WHERE sequence_name IS NOT NULL""", (dbname + '.', dbname)): db("ALTER SEQUENCE %s RESTART WITH 1" % seq) # clear tables tables = db("""SELECT table_name FROM information_schema.tables WHERE table_schema=%s""", (dbname,)) for table in tables: db("DELETE FROM %s" % table) # PostgreSQL supports TRUNCATE TABLE as well # (see http://www.postgresql.org/docs/8.1/static/sql-truncate.html) # but on the small tables used here, DELETE is actually much faster return tables def reset_mysql_db(env, db_prop): dbname = os.path.basename(db_prop['path']) if dbname: with env.db_transaction as db: tables = db("""SELECT table_name FROM information_schema.tables WHERE table_schema=%s""", (dbname,)) for table in tables: # TRUNCATE TABLE is prefered to DELETE FROM, as we need to reset # the auto_increment in MySQL. db("TRUNCATE TABLE %s" % table) return tables # -- Environment stub class EnvironmentStub(Environment): """A stub of the trac.env.Environment object for testing.""" href = abs_href = None global_databasemanager = None def __init__(self, default_data=False, enable=None, disable=None, path=None, destroying=False): """Construct a new Environment stub object. :param default_data: If True, populate the database with some defaults. :param enable: A list of component classes or name globs to activate in the stub environment. """ ComponentManager.__init__(self) Component.__init__(self) self.systeminfo = [] import trac self.path = path if self.path is None: self.path = os.path.dirname(trac.__file__) if not os.path.isabs(self.path): self.path = os.path.join(os.getcwd(), self.path) # -- configuration self.config = Configuration(None) # We have to have a ticket-workflow config for ''lots'' of things to # work. So insert the basic-workflow config here. There may be a # better solution than this. load_workflow_config_snippet(self.config, 'basic-workflow.ini') self.config.set('logging', 'log_level', 'DEBUG') self.config.set('logging', 'log_type', 'stderr') if enable is not None: self.config.set('components', 'trac.*', 'disabled') else: self.config.set('components', 'tracopt.versioncontrol.svn.*', 'enabled') for name_or_class in enable or (): config_key = self._component_name(name_or_class) self.config.set('components', config_key, 'enabled') for name_or_class in disable or (): config_key = self._component_name(name_or_class) self.config.set('components', config_key, 'disabled') # -- logging from trac.log import logger_handler_factory self.log, self._log_handler = logger_handler_factory('test') # -- database self.config.set('components', 'trac.db.*', 'enabled') self.dburi = get_dburi() init_global = False if self.global_databasemanager: self.components[DatabaseManager] = global_databasemanager else: self.config.set('trac', 'database', self.dburi) self.global_databasemanager = DatabaseManager(self) self.config.set('trac', 'debug_sql', True) self.config.set('logging', 'log_type', 'stderr') self.config.set('logging', 'log_level', 'DEBUG') init_global = not destroying if default_data or init_global: self.reset_db(default_data) from trac.web.href import Href self.href = Href('/trac.cgi') self.abs_href = Href('http://example.org/trac.cgi') self.known_users = [] translation.activate(locale_en) def reset_db(self, default_data=None): """Remove all data from Trac tables, keeping the tables themselves. :param default_data: after clean-up, initialize with default data :return: True upon success """ from trac import db_default scheme, db_prop = _parse_db_str(self.dburi) tables = [] remove_sqlite_db = False try: with self.db_transaction as db: db.rollback() # make sure there's no transaction in progress # check the database version database_version = db( "SELECT value FROM system WHERE name='database_version'") if database_version: database_version = int(database_version[0][0]) if database_version == db_default.db_version: # same version, simply clear the tables (faster) m = sys.modules[__name__] reset_fn = 'reset_%s_db' % scheme if hasattr(m, reset_fn): tables = getattr(m, reset_fn)(self, db_prop) else: # different version or version unknown, drop the tables remove_sqlite_db = True self.destroy_db(scheme, db_prop) except Exception, e: # "Database not found ...", # "OperationalError: no such table: system" or the like pass db = None # as we might shutdown the pool FIXME no longer needed! if scheme == 'sqlite' and remove_sqlite_db: path = db_prop['path'] if path != ':memory:': if not os.path.isabs(path): path = os.path.join(self.path, path) self.global_databasemanager.shutdown() os.remove(path) if not tables: self.global_databasemanager.init_db() # we need to make sure the next get_db_cnx() will re-create # a new connection aware of the new data model - see #8518. if self.dburi != 'sqlite::memory:': self.global_databasemanager.shutdown() with self.db_transaction as db: if default_data: for table, cols, vals in db_default.get_data(db): db.executemany("INSERT INTO %s (%s) VALUES (%s)" % (table, ','.join(cols), ','.join(['%s' for c in cols])), vals) else: db("INSERT INTO system (name, value) VALUES (%s, %s)", ('database_version', str(db_default.db_version))) def destroy_db(self, scheme=None, db_prop=None): if not (scheme and db_prop): scheme, db_prop = _parse_db_str(self.dburi) try: with self.db_transaction as db: if scheme == 'postgres' and db.schema: db('DROP SCHEMA "%s" CASCADE' % db.schema) elif scheme == 'mysql': dbname = os.path.basename(db_prop['path']) for table in db(""" SELECT table_name FROM information_schema.tables WHERE table_schema=%s""", (dbname,)): db("DROP TABLE IF EXISTS `%s`" % table) except Exception: # "TracError: Database not found...", # psycopg2.ProgrammingError: schema "tractest" does not exist pass return False # overriden def is_component_enabled(self, cls): if self._component_name(cls).startswith('__main__.'): return True return Environment.is_component_enabled(self, cls) def get_known_users(self, cnx=None): return self.known_users def locate(fn): """Locates a binary on the path. Returns the fully-qualified path, or None. """ exec_suffix = '.exe' if os.name == 'nt' else '' for p in ["."] + os.environ['PATH'].split(os.pathsep): f = os.path.join(p, fn + exec_suffix) if os.path.exists(f): return f return None INCLUDE_FUNCTIONAL_TESTS = True def suite(): import trac.tests import trac.admin.tests import trac.db.tests import trac.mimeview.tests import trac.ticket.tests import trac.util.tests import trac.versioncontrol.tests import trac.versioncontrol.web_ui.tests import trac.web.tests import trac.wiki.tests import tracopt.mimeview.tests import tracopt.perm.tests import tracopt.versioncontrol.git.tests import tracopt.versioncontrol.svn.tests suite = unittest.TestSuite() suite.addTest(trac.tests.basicSuite()) if INCLUDE_FUNCTIONAL_TESTS: suite.addTest(trac.tests.functionalSuite()) suite.addTest(trac.admin.tests.suite()) suite.addTest(trac.db.tests.suite()) suite.addTest(trac.mimeview.tests.suite()) suite.addTest(trac.ticket.tests.suite()) suite.addTest(trac.util.tests.suite()) suite.addTest(trac.versioncontrol.tests.suite()) suite.addTest(trac.versioncontrol.web_ui.tests.suite()) suite.addTest(trac.web.tests.suite()) suite.addTest(trac.wiki.tests.suite()) suite.addTest(tracopt.mimeview.tests.suite()) suite.addTest(tracopt.perm.tests.suite()) suite.addTest(tracopt.versioncontrol.git.tests.suite()) suite.addTest(tracopt.versioncontrol.svn.tests.suite()) suite.addTest(doctest.DocTestSuite(sys.modules[__name__])) return suite if __name__ == '__main__': #FIXME: this is a bit inelegant if '--skip-functional-tests' in sys.argv: sys.argv.remove('--skip-functional-tests') INCLUDE_FUNCTIONAL_TESTS = False unittest.main(defaultTest='suite') Trac-1.0.1/trac/dist.py0000644000175200017520000005210312102610255013643 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2011 Edgewall Software # 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://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/log/. """Extra commands for setup.py. In addition to providing a few extra command classes in `l10n_cmdclass`, we also modify the standard `distutils.command.build` and `setuptools.command.install_lib` classes so that the relevant l10n commands for compiling catalogs are issued upon install. """ from __future__ import with_statement from StringIO import StringIO from itertools import izip import os import re from tokenize import generate_tokens, COMMENT, NAME, OP, STRING from distutils import log from distutils.cmd import Command from distutils.command.build import build as _build from distutils.errors import DistutilsOptionError from setuptools.command.install_lib import install_lib as _install_lib try: from babel.messages.catalog import TranslationError from babel.messages.extract import extract_javascript from babel.messages.frontend import extract_messages, init_catalog, \ compile_catalog, update_catalog from babel.messages.pofile import read_po from babel.support import Translations from babel.util import parse_encoding _GENSHI_MARKUP_SEARCH = re.compile(r'\[[0-9]+:').search _DEFAULT_KWARGS_MAPS = { 'Option': {'doc': 4}, 'BoolOption': {'doc': 4}, 'IntOption': {'doc': 4}, 'FloatOption': {'doc': 4}, 'ListOption': {'doc': 6}, 'ChoiceOption': {'doc': 4}, 'PathOption': {'doc': 4}, 'ExtensionOption': {'doc': 5}, 'OrderedExtensionsOption': {'doc': 6}, } _DEFAULT_CLEANDOC_KEYWORDS = ( 'ConfigSection', 'Option', 'BoolOption', 'IntOption', 'FloatOption', 'ListOption', 'ChoiceOption', 'PathOption', 'ExtensionOption', 'OrderedExtensionsOption', 'cleandoc_', ) def extract_python(fileobj, keywords, comment_tags, options): """Extract messages from Python source code, This is patched extract_python from Babel to support keyword argument mapping. `kwargs_maps` option: names of keyword arguments will be mapping to index of messages array. `cleandoc_keywords` option: a list of keywords to clean up the extracted messages with `cleandoc`. """ from trac.util.compat import cleandoc funcname = lineno = message_lineno = None kwargs_maps = func_kwargs_map = None call_stack = -1 buf = [] messages = [] messages_kwargs = {} translator_comments = [] in_def = in_translator_comments = False comment_tag = None encoding = parse_encoding(fileobj) \ or options.get('encoding', 'iso-8859-1') kwargs_maps = _DEFAULT_KWARGS_MAPS.copy() if 'kwargs_maps' in options: kwargs_maps.update(options['kwargs_maps']) cleandoc_keywords = set(_DEFAULT_CLEANDOC_KEYWORDS) if 'cleandoc_keywords' in options: cleandoc_keywords.update(options['cleandoc_keywords']) tokens = generate_tokens(fileobj.readline) tok = value = None for _ in tokens: prev_tok, prev_value = tok, value tok, value, (lineno, _), _, _ = _ if call_stack == -1 and tok == NAME and value in ('def', 'class'): in_def = True elif tok == OP and value == '(': if in_def: # Avoid false positives for declarations such as: # def gettext(arg='message'): in_def = False continue if funcname: message_lineno = lineno call_stack += 1 kwarg_name = None elif in_def and tok == OP and value == ':': # End of a class definition without parens in_def = False continue elif call_stack == -1 and tok == COMMENT: # Strip the comment token from the line value = value.decode(encoding)[1:].strip() if in_translator_comments and \ translator_comments[-1][0] == lineno - 1: # We're already inside a translator comment, continue # appending translator_comments.append((lineno, value)) continue # If execution reaches this point, let's see if comment line # starts with one of the comment tags for comment_tag in comment_tags: if value.startswith(comment_tag): in_translator_comments = True translator_comments.append((lineno, value)) break elif funcname and call_stack == 0: if tok == OP and value == ')': if buf: message = ''.join(buf) if kwarg_name in func_kwargs_map: messages_kwargs[kwarg_name] = message else: messages.append(message) del buf[:] else: messages.append(None) for name, message in messages_kwargs.iteritems(): if name not in func_kwargs_map: continue index = func_kwargs_map[name] while index >= len(messages): messages.append(None) messages[index - 1] = message if funcname in cleandoc_keywords: messages = [m and cleandoc(m) for m in messages] if len(messages) > 1: messages = tuple(messages) else: messages = messages[0] # Comments don't apply unless they immediately preceed the # message if translator_comments and \ translator_comments[-1][0] < message_lineno - 1: translator_comments = [] yield (message_lineno, funcname, messages, [comment[1] for comment in translator_comments]) funcname = lineno = message_lineno = None kwarg_name = func_kwargs_map = None call_stack = -1 messages = [] messages_kwargs = {} translator_comments = [] in_translator_comments = False elif tok == STRING: # Unwrap quotes in a safe manner, maintaining the string's # encoding # https://sourceforge.net/tracker/?func=detail&atid=355470& # aid=617979&group_id=5470 value = eval('# coding=%s\n%s' % (encoding, value), {'__builtins__':{}}, {}) if isinstance(value, str): value = value.decode(encoding) buf.append(value) elif tok == OP and value == '=' and prev_tok == NAME: kwarg_name = prev_value elif tok == OP and value == ',': if buf: message = ''.join(buf) if kwarg_name in func_kwargs_map: messages_kwargs[kwarg_name] = message else: messages.append(message) del buf[:] else: messages.append(None) kwarg_name = None if translator_comments: # We have translator comments, and since we're on a # comma(,) user is allowed to break into a new line # Let's increase the last comment's lineno in order # for the comment to still be a valid one old_lineno, old_comment = translator_comments.pop() translator_comments.append((old_lineno+1, old_comment)) elif call_stack > 0 and tok == OP and value == ')': call_stack -= 1 elif funcname and call_stack == -1: funcname = func_kwargs_map = kwarg_name = None elif tok == NAME and value in keywords: funcname = value func_kwargs_map = kwargs_maps.get(funcname, {}) kwarg_name = None def extract_javascript_script(fileobj, keywords, comment_tags, options): """Extract messages from Javascript embedding in $prefix$cnum

Modify

Create New Ticket (${ticket.type})

Change History (${len(changes)})

Add Comment

This ticket has been modified since you started editing. You should review the other modifications which have been appended above, and any conflicts shown in the preview below. You can nevertheless proceed and submit your changes if you wish so.

Modify Ticket

Change Properties Properties
${field.cc_entry}
Action
$controls $hint
Author Reporter


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.
Trac-1.0.1/trac/ticket/templates/report_view.html0000644000175200017520000002242612102610254021046 0ustar cbooscboos $title

$title (${ngettext('%(num)s match', '%(num)s matches', numrows)})

Arguments
${wiki_to_html(context, description)}

Results (${paginator.displayed_items()})

$header.title

$value_for_group (${ngettext('%(num)s match', '%(num)s matches', cnt) if cnt else _('No matches found.')})

${group_heading(*row_groups[0]) if row_groups else None} ${column_headers()} ${column_headers()}
${group_heading(value_for_group or _('(empty)'), row_group)}
{$cell.value}
${shortname_of(row.resource)}
$cell.value
${format_time(from_utimestamp(long(cell.value))) if cell.value != '' else '--'}
${format_date(from_utimestamp(long(cell.value))) if cell.value != '' else '--'}
${format_datetime(from_utimestamp(long(cell.value))) if cell.value != '' else '--'}
${wiki_to_html(context.child(row.resource), cell.value)}
$cell.value
$cell.value
$message
No matches found.
Note: See TracReports for help on using and creating reports.
Trac-1.0.1/trac/ticket/templates/ticket_notify_email.txt0000644000175200017520000000135112102610254022370 0ustar cbooscboos$ticket_body_hdr $ticket_props {% choose ticket.new %}\ {% when True %}\ $ticket.description {% end %}\ {% otherwise %}\ {% if changes_body %}\ ${_('Changes (by %(author)s):', author=change.author)} $changes_body {% end %}\ {% if changes_descr %}\ {% if not changes_body and not change.comment and change.author %}\ ${_('Description changed by %(author)s:', author=change.author)} {% end %}\ $changes_descr -- {% end %}\ {% if change.comment %}\ ${_('Comment:') if changes_body else _('Comment (by %(author)s):', author=change.author)} $change.comment {% end %}\ {% end %}\ {% end %}\ -- ${_('Ticket URL: <%(link)s>', link=ticket.link)} $project.name <${project.url or abs_href()}> $project.descr Trac-1.0.1/trac/ticket/templates/batch_modify.html0000644000175200017520000000454312102610254021131 0ustar cbooscboos
Batch Modify ${group_heading(*groups[0]) if groups else None}
Error: $error

Note: See TracReports for help on using and creating reports.
Trac-1.0.1/trac/ticket/templates/roadmap.html0000644000175200017520000000633612102610254020126 0ustar cbooscboos Roadmap

Roadmap

Milestone: ${milestone.name}

Completed ${dateinfo(milestone.completed)} ago (${format_datetime(milestone.completed)})

${dateinfo(milestone.due)} late (${format_datetime(milestone.due)})

Due in ${dateinfo(milestone.due)} (${format_datetime(milestone.due)})

No date set

${wiki_to_html(context.child(milestone.resource), milestone.description)}
Note: See TracRoadmap for help on using the roadmap.
Trac-1.0.1/trac/ticket/templates/report.rss0000644000175200017520000000377412102610254017664 0ustar cbooscboos $project.name: $title ${abs_href.report(report.id)} Trac Report - $description en-us $project.name $chrome.logo.src_abs ${abs_href.report(report.id if report.id != -1 else '')} Trac v${trac.version} ${http_date(from_utimestamp(long(cell.value)))} #$row.id: $cell.value ${unicode(wiki_to_html(context.child(row.resource), cell.value))} ${abs_url_of(row.resource)} ${abs_url_of(row.resource)} Report Trac-1.0.1/trac/ticket/templates/query.html0000644000175200017520000003244712102610254017652 0ustar cbooscboos $title

$title (${ngettext('%(num)s match', '%(num)s matches', query.num_items)})

${wiki_to_html(context.child(report_resource), description)}
Filters
 
Or
 
  
 
Columns

Show under each result:


Note: See TracQuery for help on using queries.
Trac-1.0.1/trac/ticket/templates/ticket.rss0000644000175200017520000000403212102610254017620 0ustar cbooscboos ${project.name}: Ticket #${ticket.id}: ${ticket.summary} ${abs_href.ticket(ticket.id)} ${unicode(wiki_to_html(context, ticket.description))} en-us $project.name $chrome.logo.src_abs ${abs_href.ticket(ticket.id)} Trac $trac.version ${http_date(change.date)} $change.title ${abs_href.ticket(ticket.id)}#comment:$change.cnum ${abs_href.ticket(ticket.id)}#comment:$change.cnum <ul> <li><strong>$field</strong> ${unicode(value.rendered)} set to <em>$value.new</em> changed from <em>$value.old</em> to <em>$value.new</em> <em>$value.old</em> deleted </li> </ul> ${unicode(wiki_to_html(context, change.comment))} Ticket Trac-1.0.1/trac/ticket/templates/milestone_view.html0000644000175200017520000001044112102610254021524 0ustar cbooscboos Milestone ${milestone.name}

Milestone ${milestone.name}

Completed ${dateinfo(milestone.completed)} ago (${format_datetime(milestone.completed)})

${dateinfo(milestone.due)} late (${format_datetime(milestone.due)})

Due in ${dateinfo(milestone.due)} (${format_datetime(milestone.due)})

No date set

${wiki_to_html(context, milestone.description)}
Note: See TracRoadmap for help on using the roadmap.
Trac-1.0.1/trac/ticket/templates/ticket_preview.html0000644000175200017520000000220612102610254021517 0ustar cbooscboos
Trac-1.0.1/trac/ticket/templates/query.rss0000644000175200017520000000234512102610254017507 0ustar cbooscboos $project.name: Ticket Query $query_href $project.descr en-US $project.name $chrome.logo.src_abs $query_href Trac $trac.version $href $href #$result.id: ${result.summary} ${http_date(result.time)} ${unicode(wiki_to_html(context.child('ticket', result.id), result.description))} Results $href#changelog Trac-1.0.1/trac/ticket/templates/milestone_delete.html0000644000175200017520000000367112102610254022023 0ustar cbooscboos Delete Milestone ${milestone.name}

Delete Milestone ${milestone.name}

Are you sure you want to delete this milestone?

Note: See TracRoadmap for help on using the roadmap.
Trac-1.0.1/trac/ticket/templates/ticket_change.html0000644000175200017520000002021712102610254021265 0ustar cbooscboos $prefix$cnum

${commentref('comment:', cnum)} in reply to: ${commentref('↑ ', change.replyto)} ; follow-up: follow-ups: ${commentref('↓ ', reply, 'follow-up')} Changed ${pretty_dateinfo(change.date)} by ${authorinfo(change.author)} Changed by ${authorinfo(change.author)}

  • ${field.label} ${field.new } added ${field.rendered} changed from ${field.old} to ${field.new} set to ${field.new} ${field.old} deleted ()
${wiki_to_html(context, text, escape_newlines=preserve_newlines)}
${wiki_to_html(context, change.comment_history[int(cversion)].comment, escape_newlines=preserve_newlines)}
${wiki_to_html(context, change.comment, escape_newlines=preserve_newlines)}
Version ${comment_version}, edited ${pretty_dateinfo(change.comment_history[comment_version].date)} by ${authorinfo(change.comment_history[comment_version].author)} Last edited ${pretty_dateinfo(change.comment_history[comment_version].date)} by ${authorinfo(change.comment_history[comment_version].author)} (previous) (next) (diff)
Trac-1.0.1/trac/ticket/templates/report_list.rss0000644000175200017520000000154712102610254020713 0ustar cbooscboos $project.name: Available Reports ${abs_href.report()} List of available reports en-us $project.name $chrome.logo.src_abs ${abs_href.report()} Trac v${trac.version} {$report}: $title ${abs_href.report(report)} ${abs_href.report(report)} Report Trac-1.0.1/trac/ticket/templates/batch_ticket_notify_email.txt0000644000175200017520000000055112102610254023532 0ustar cbooscboos${_('Batch modification to %(tickets)s by %(author)s:', tickets=tickets_descr, author=author)} $changes_descr {%if action %}\ ${_('Action: %(action)s', action=action)} {% end %}\ {%if comment %}\ ${_('Comment:')} $comment {% end %}\ -- ${_('Tickets URL: <%(link)s>', link=ticket_query_link)} $project.name <${project.url or abs_href()}> $project.descr Trac-1.0.1/trac/ticket/templates/report_list.html0000644000175200017520000001142112102610254021040 0ustar cbooscboos Available Reports

Available Reports

Return to Last Query

Continue browsing through the current list of results, from the last selected report or custom query.

Custom Query

Compose a new ticket query by selecting filters and columns to display.

SQL reports and saved custom queries Sort by: Identifier Title

$title {$id} {$id} $title

${wiki_to_html(context, description)}

No reports available.

Note: See TracReports for help on using and creating reports.
Trac-1.0.1/trac/ticket/templates/report_delete.html0000644000175200017520000000214712102610254021334 0ustar cbooscboos $title

$report.title

Are you sure you want to delete this report?

Note: See TracReports for help on using and creating reports.
Trac-1.0.1/trac/ticket/templates/ticket_box.html0000644000175200017520000001213512102610254020630 0ustar cbooscboos

Opened ${pretty_dateinfo(ticket.time)}

Closed ${pretty_dateinfo(closetime)}

Last modified ${pretty_dateinfo(ticket.changetime)}

(ticket not yet created)

#${ticket.id} ${'status' in fields_map and fields[fields_map['status']].rendered or ticket.status} ${'type' in fields_map and fields[fields_map['type']].rendered or ticket.type} (${'resolution' in fields_map and fields[fields_map['resolution']].rendered or ticket.resolution})

$ticket.summary at Initial Version at Version $version

Reported by: $v_reporter Owned by: $v_owner
${field.label or field.name}: ${field.rendered} ${ticket[field.name]}

Description (last modified by ${authorinfo(description_change.author)})

${wiki_to_html(context, ticket.description, escape_newlines=preserve_newlines)}

Trac-1.0.1/trac/ticket/templates/milestone_edit.html0000644000175200017520000001174312102610254021505 0ustar cbooscboos Edit Milestone ${milestone.name} New Milestone

Edit Milestone ${milestone.name}

New Milestone

Schedule

Note: See TracRoadmap for help on using the roadmap.
Trac-1.0.1/trac/ticket/templates/query_results.html0000644000175200017520000001307112102610254021423 0ustar cbooscboos

Results (${paginator.displayed_items()})

${grouplabel}: ${groupname} (${count})

${header.label}
${column_headers()} ${column_headers()}
${group_heading(groupname, results)}
No tickets found
#$result.id $value ${pretty_dateinfo(value, dateonly=True)} ${authorinfo(value)} ${format_emails(ticket_context, value)} ${authorinfo(value)} ${value} ${wiki_to_oneliner(ticket_context, value)} $value

(this ticket)

${fields.get(r, {'label': r or 'none'}).label} ${wiki_to_html(ticket_context, result[r])}
(more results for this group on next page)
Trac-1.0.1/trac/ticket/admin.py0000644000175200017520000010073512102610254015257 0ustar cbooscboos# -*- coding: utf-8 -*- # # Copyright (C) 2005-2009 Edgewall Software # 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://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://trac.edgewall.org/. from __future__ import with_statement from datetime import datetime from trac.admin import * from trac.core import * from trac.perm import PermissionSystem from trac.resource import ResourceNotFound from trac.ticket import model from trac.util import getuser from trac.util.datefmt import utc, parse_date, format_date, format_datetime, \ get_datetime_format_hint, user_time from trac.util.text import print_table, printout, exception_to_unicode from trac.util.translation import _, N_, gettext from trac.web.chrome import Chrome, add_notice, add_warning class TicketAdminPanel(Component): implements(IAdminPanelProvider, IAdminCommandProvider) abstract = True _label = (N_('(Undefined)'), N_('(Undefined)')) # i18n note: use gettext() whenever refering to the above as text labels, # and don't use it whenever using them as field names (after # a call to `.lower()`) # IAdminPanelProvider methods def get_admin_panels(self, req): if 'TICKET_ADMIN' in req.perm: yield ('ticket', _('Ticket System'), self._type, gettext(self._label[1])) def render_admin_panel(self, req, cat, page, version): req.perm.require('TICKET_ADMIN') # Trap AssertionErrors and convert them to TracErrors try: return self._render_admin_panel(req, cat, page, version) except AssertionError, e: raise TracError(e) def _save_config(config, req, log): """Try to save the config, and display either a success notice or a failure warning. """ try: config.save() add_notice(req, _('Your changes have been saved.')) except Exception, e: log.error('Error writing to trac.ini: %s', exception_to_unicode(e)) add_warning(req, _('Error writing to trac.ini, make sure it is ' 'writable by the web server. Your changes have not ' 'been saved.')) class ComponentAdminPanel(TicketAdminPanel): _type = 'components' _label = (N_('Component'), N_('Components')) # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, component): # Detail view? if component: comp = model.Component(self.env, component) if req.method == 'POST': if req.args.get('save'): comp.name = name = req.args.get('name') comp.owner = req.args.get('owner') comp.description = req.args.get('description') try: comp.update() except self.env.db_exc.IntegrityError: raise TracError(_('The component "%(name)s" already ' 'exists.', name=name)) add_notice(req, _('Your changes have been saved.')) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'component': comp} else: default = self.config.get('ticket', 'default_component') if req.method == 'POST': # Add Component if req.args.get('add') and req.args.get('name'): name = req.args.get('name') try: comp = model.Component(self.env, name=name) except ResourceNotFound: comp = model.Component(self.env) comp.name = name if req.args.get('owner'): comp.owner = req.args.get('owner') comp.insert() add_notice(req, _('The component "%(name)s" has been ' 'added.', name=name)) req.redirect(req.href.admin(cat, page)) else: if comp.name is None: raise TracError(_("Invalid component name.")) raise TracError(_("Component %(name)s already exists.", name=name)) # Remove components elif req.args.get('remove'): sel = req.args.get('sel') if not sel: raise TracError(_('No component selected')) if not isinstance(sel, list): sel = [sel] with self.env.db_transaction: for name in sel: model.Component(self.env, name).delete() add_notice(req, _("The selected components have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default component elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default component to %s", name) self.config.set('ticket', 'default_component', name) _save_config(self.config, req, self.log) req.redirect(req.href.admin(cat, page)) data = {'view': 'list', 'components': model.Component.select(self.env), 'default': default} if self.config.getbool('ticket', 'restrict_owner'): perm = PermissionSystem(self.env) def valid_owner(username): return perm.get_user_permissions(username).get('TICKET_MODIFY') data['owners'] = [username for username, name, email in self.env.get_known_users() if valid_owner(username)] data['owners'].insert(0, '') data['owners'].sort() else: data['owners'] = None return 'admin_components.html', data # IAdminCommandProvider methods def get_admin_commands(self): yield ('component list', '', 'Show available components', None, self._do_list) yield ('component add', ' ', 'Add a new component', self._complete_add, self._do_add) yield ('component rename', ' ', 'Rename a component', self._complete_remove_rename, self._do_rename) yield ('component remove', '', 'Remove/uninstall a component', self._complete_remove_rename, self._do_remove) yield ('component chown', ' ', 'Change component ownership', self._complete_chown, self._do_chown) def get_component_list(self): return [c.name for c in model.Component.select(self.env)] def get_user_list(self): return [username for username, in self.env.db_query("SELECT DISTINCT username FROM permission")] def _complete_add(self, args): if len(args) == 2: return self.get_user_list() def _complete_remove_rename(self, args): if len(args) == 1: return self.get_component_list() def _complete_chown(self, args): if len(args) == 1: return self.get_component_list() elif len(args) == 2: return self.get_user_list() def _do_list(self): print_table([(c.name, c.owner) for c in model.Component.select(self.env)], [_('Name'), _('Owner')]) def _do_add(self, name, owner): component = model.Component(self.env) component.name = name component.owner = owner component.insert() def _do_rename(self, name, newname): component = model.Component(self.env, name) component.name = newname component.update() def _do_remove(self, name): model.Component(self.env, name).delete() def _do_chown(self, name, owner): component = model.Component(self.env, name) component.owner = owner component.update() class MilestoneAdminPanel(TicketAdminPanel): _type = 'milestones' _label = (N_('Milestone'), N_('Milestones')) # IAdminPanelProvider methods def get_admin_panels(self, req): if 'MILESTONE_VIEW' in req.perm: return TicketAdminPanel.get_admin_panels(self, req) return iter([]) # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, milestone): req.perm.require('MILESTONE_VIEW') # Detail view? if milestone: mil = model.Milestone(self.env, milestone) if req.method == 'POST': if req.args.get('save'): req.perm.require('MILESTONE_MODIFY') mil.name = name = req.args.get('name') mil.due = mil.completed = None due = req.args.get('duedate', '') if due: mil.due = user_time(req, parse_date, due, hint='datetime') if req.args.get('completed', False): completed = req.args.get('completeddate', '') mil.completed = user_time(req, parse_date, completed, hint='datetime') if mil.completed > datetime.now(utc): raise TracError(_('Completion date may not be in ' 'the future'), _('Invalid Completion Date')) mil.description = req.args.get('description', '') try: mil.update() except self.env.db_exc.IntegrityError: raise TracError(_('The milestone "%(name)s" already ' 'exists.', name=name)) add_notice(req, _('Your changes have been saved.')) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'milestone': mil} else: default = self.config.get('ticket', 'default_milestone') if req.method == 'POST': # Add Milestone if req.args.get('add') and req.args.get('name'): req.perm.require('MILESTONE_CREATE') name = req.args.get('name') try: mil = model.Milestone(self.env, name=name) except ResourceNotFound: mil = model.Milestone(self.env) mil.name = name if req.args.get('duedate'): mil.due = user_time(req, parse_date, req.args.get('duedate'), hint='datetime') mil.insert() add_notice(req, _('The milestone "%(name)s" has been ' 'added.', name=name)) req.redirect(req.href.admin(cat, page)) else: if mil.name is None: raise TracError(_('Invalid milestone name.')) raise TracError(_("Milestone %(name)s already exists.", name=name)) # Remove milestone elif req.args.get('remove'): req.perm.require('MILESTONE_DELETE') sel = req.args.get('sel') if not sel: raise TracError(_('No milestone selected')) if not isinstance(sel, list): sel = [sel] with self.env.db_transaction: for name in sel: mil = model.Milestone(self.env, name) mil.delete(author=req.authname) add_notice(req, _("The selected milestones have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default milestone elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default milestone to %s", name) self.config.set('ticket', 'default_milestone', name) _save_config(self.config, req, self.log) req.redirect(req.href.admin(cat, page)) # Get ticket count milestones = [ (milestone, self.env.db_query(""" SELECT COUNT(*) FROM ticket WHERE milestone=%s """, (milestone.name,))[0][0]) for milestone in model.Milestone.select(self.env)] data = {'view': 'list', 'milestones': milestones, 'default': default} Chrome(self.env).add_jquery_ui(req) data.update({ 'datetime_hint': get_datetime_format_hint(req.lc_time), }) return 'admin_milestones.html', data # IAdminCommandProvider methods def get_admin_commands(self): yield ('milestone list', '', "Show milestones", None, self._do_list) yield ('milestone add', ' [due]', "Add milestone", None, self._do_add) yield ('milestone rename', ' ', "Rename milestone", self._complete_name, self._do_rename) yield ('milestone due', ' ', """Set milestone due date The date must be specified in the "%s" format. Alternatively, "now" can be used to set the due date to the current time. To remove the due date from a milestone, specify an empty string (""). """ % console_date_format_hint, self._complete_name, self._do_due) yield ('milestone completed', ' ', """Set milestone complete date The date must be specified in the "%s" format. Alternatively, "now" can be used to set the completion date to the current time. To remove the completion date from a milestone, specify an empty string (""). """ % console_date_format_hint, self._complete_name, self._do_completed) yield ('milestone remove', '', "Remove milestone", self._complete_name, self._do_remove) def get_milestone_list(self): return [m.name for m in model.Milestone.select(self.env)] def _complete_name(self, args): if len(args) == 1: return self.get_milestone_list() def _do_list(self): print_table([(m.name, m.due and format_date(m.due, console_date_format), m.completed and format_datetime(m.completed, console_datetime_format)) for m in model.Milestone.select(self.env)], [_("Name"), _("Due"), _("Completed")]) def _do_add(self, name, due=None): milestone = model.Milestone(self.env) milestone.name = name if due is not None: milestone.due = parse_date(due, hint='datetime') milestone.insert() def _do_rename(self, name, newname): milestone = model.Milestone(self.env, name) milestone.name = newname milestone.update() def _do_due(self, name, due): milestone = model.Milestone(self.env, name) milestone.due = due and parse_date(due, hint='datetime') milestone.update() def _do_completed(self, name, completed): milestone = model.Milestone(self.env, name) milestone.completed = completed and parse_date(completed, hint='datetime') milestone.update() def _do_remove(self, name): model.Milestone(self.env, name).delete(author=getuser()) class VersionAdminPanel(TicketAdminPanel): _type = 'versions' _label = (N_('Version'), N_('Versions')) # TicketAdminPanel methods def _render_admin_panel(self, req, cat, page, version): # Detail view? if version: ver = model.Version(self.env, version) if req.method == 'POST': if req.args.get('save'): ver.name = name = req.args.get('name') if req.args.get('time'): ver.time = user_time(req, parse_date, req.args.get('time'), hint='datetime') else: ver.time = None # unset ver.description = req.args.get('description') try: ver.update() except self.env.db_exc.IntegrityError: raise TracError(_('The version "%(name)s" already ' 'exists.', name=name)) add_notice(req, _('Your changes have been saved.')) req.redirect(req.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(req.href.admin(cat, page)) Chrome(self.env).add_wiki_toolbars(req) data = {'view': 'detail', 'version': ver} else: default = self.config.get('ticket', 'default_version') if req.method == 'POST': # Add Version if req.args.get('add') and req.args.get('name'): name = req.args.get('name') try: ver = model.Version(self.env, name=name) except ResourceNotFound: ver = model.Version(self.env) ver.name = name if req.args.get('time'): ver.time = user_time(req, parse_date, req.args.get('time'), hint='datetime') ver.insert() add_notice(req, _('The version "%(name)s" has been ' 'added.', name=name)) req.redirect(req.href.admin(cat, page)) else: if ver.name is None: raise TracError(_("Invalid version name.")) raise TracError(_("Version %(name)s already exists.", name=name)) # Remove versions elif req.args.get('remove'): sel = req.args.get('sel') if not sel: raise TracError(_("No version selected")) if not isinstance(sel, list): sel = [sel] with self.env.db_transaction: for name in sel: ver = model.Version(self.env, name) ver.delete() add_notice(req, _("The selected versions have been " "removed.")) req.redirect(req.href.admin(cat, page)) # Set default version elif req.args.get('apply'): name = req.args.get('default') if name and name != default: self.log.info("Setting default version to %s", name) self.config.set('ticket', 'default_version', name) _save_config(self.config, req, self.log) req.redirect(req.href.admin(cat, page)) data = {'view': 'list', 'versions': model.Version.select(self.env), 'default': default} Chrome(self.env).add_jquery_ui(req) data.update({ 'datetime_hint': get_datetime_format_hint(req.lc_time), }) return 'admin_versions.html', data # IAdminCommandProvider methods def get_admin_commands(self): yield ('version list', '', "Show versions", None, self._do_list) yield ('version add', ' [time]', "Add version", None, self._do_add) yield ('version rename', ' ', "Rename version", self._complete_name, self._do_rename) yield ('version time', '