Commits

Anonymous committed 465033c

0.12dev: merged [source:sandbox/multirepos@9124] branch on trunk.

Closes #2086.

Comments (0)

Files changed (69)

- = Testing Readme =
-
-So, you want to see what's broken?  Eeeexcellent.
-
-If you are running python < 2.4.4, please see the troubleshooting section.
-
- == Quick Start ==
-First thing to do is run the tests.
-If you have genshi and twill installed on your system, you should be able to run the tests like this:
-{{{
-PYTHONPATH=. ./trac/test.py
-}}}
-
- == Slow Start ==
-If you want to test against specific versions of genshi, twill, pygments, etc., you can set those up like this:
-{{{
-myworktree/trac
-          /pygments-0.8
-          /twill-0.9
-          /genshi-0.4.4
-}}}
-
-Run `python setup egg_info` in those subdirectories as needed.
-Then you can run:
-{{{
-PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/test.py
-}}}
-
-If you want to run just the functional tests, you can do that by running
-{{{
-PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/tests/functional/testcases.py
-}}}
-
-And to run everything except the functional tests,
-{{{
-PYTHONPATH=. ./trac/test.py --skip-functional-tests
-}}}
-
-
-NOTE: Unlike most unittests, the functional tests share a test fixture across tests.  This means that you can't(*) run just one of the tests by itself.
-But you can run a sub-set of the functional tests:
-{{{
-PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/tests/functional/__init__.py
-PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/ticket/tests/functional.py
-PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/versioncontrol/tests/functional.py
-}}}
-Note that that there is a set of functional tests that are run regardless of what subset you choose; these tests setup the shared fixture.
-
-
-(*) Unless you modify the code to comment out the other functional tests.  The test fixture is setup and torn down by `FunctionalTestSuite`, and it runs the tests added to it with `_tester` and `_testenv` set in the testcase objects.
-
-The functional tests require subversion, and use a random local port 8000-8999 for the test web server.
-
- == Testing output and byproducts ==
-There is some logging done:
- - testing.log
-    output from trac environment creation, tracd, and some svn commands
- - functional-testing.log
-    output from twill
-
-The test fixture is left behind in 'testenv' so you can inspect it when debugging a problem.
-{{{
-testenv/htpasswd   -- the password/authentication for the test fixture.  password = username
-       /repo       -- the Subversion repository
-       /trac       -- the Trac environment
-}}}
-(Note that running the tests again will automatically delete this test environment and create a new one.  If you want to save a test environment, you will need to rename this directory before running the tests again.)
-
-The command to serve the test environment is:
-{{{
-PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 \
-./trac/web/standalone.py --basic-auth=trac,testenv/htpasswd, -s \
-    --port=8888 testenv/trac
-}}}
-This is particularly useful when a test fails and you want to explore the environment to debug the problem.
-
- == Test Coverage ==
-You can now determine statement coverage of unittests and functional tests.  But you'll have to run them separately for now.
-
-`figleaf` will need to be on your `PATH`.  Set the `FIGLEAF` environment variable to the figleaf command.
-
-Create a figleaf-exclude file with filename patterns to exclude.  For example:
-{{{
-/usr/lib/python.*/.*.py
-.*.html
-}}}
-
-Then run the tests something like this:
-{{{
-export FIGLEAF=figleaf
-figleaf ./trac/test.py -v --skip-functional-tests
-mv .figleaf .figleaf.unittests
-python trac/tests/functional/testcases.py -v
-mv .figleaf .figleaf.functional
-figleaf2html --exclude-patterns=../figleaf-exclude .figleaf.functional .figleaf.unittests
-}}}
-Also, this is very slow; on a decent machine, 10 minutes for the functional tests is normal.
-
-To run without figleaf, be sure to unset `FIGLEAF`.
-
---------------------------------------------------------------------------------
-== TROUBLESHOOTING: ==
-
- 1. trac-admin is failing on initenv with this exception:
-    {{{
-    raise Exception('Failed with exitcode %s running trac-admin with %r' % (retval, args))
-Exception: Failed with exitcode 1 running trac-admin with ('initenv', 'testenv', 'sqlite:db/trac.db', 'svn', '..../testenv/repo')
-    }}}
-    This can be caused by not having run `python setup.py egg_info` in the genshi tree.
-
- 2. Windows needs an implementation of crypt or fcrypt.  Carey Evans' pure
-    python version works, but prints warnings on Python 2.3 (they can be
-    ignored).  See http://carey.geek.nz/code/python-fcrypt/
-
- 3. Python versions compatibility notes
-   * If using Python >=2.4.0, <2.4.4, you need to backport `unittest` from
-     2.4.4.  If you do not do this, you will get an `AttributeError` regarding
-     `_fixture` being unset on every test.
-   * If using Python >=2.3.0, <2.3.5, you need to backport a few modules for
-     `setuptools` to work properly (`httplib2`, `cookielib`, `_*CookieJar`) and
-     remove the tuple imports to replace with explicit line continuations.
-   * If using Python >=2.3.0, <2.4.0, you need `subprocess`, `unittest`, and
-     `traceback` from 2.4.4.  On Windows you also need to modify `subprocess.py`
-     to use `pywin32` instead of `_subprocess`.  Twill includes a `subprocess`
-     module that will not work in this situation because it is not modified.
-
+ = Testing Readme =
+
+So, you want to see what's broken?  Eeeexcellent.
+
+If you are running python < 2.4.4, please see the troubleshooting section.
+
+ == Quick Start ==
+First thing to do is run the tests.
+If you have genshi and twill installed on your system, you should be able to run the tests like this:
+{{{
+PYTHONPATH=. ./trac/test.py
+}}}
+
+ == Slow Start ==
+If you want to test against specific versions of genshi, twill, pygments, etc., you can set those up like this:
+{{{
+myworktree/trac
+          /pygments-0.8
+          /twill-0.9
+          /genshi-0.4.4
+}}}
+
+Run `python setup egg_info` in those subdirectories as needed.
+Then you can run:
+{{{
+PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/test.py
+}}}
+
+If you want to run just the functional tests, you can do that by running
+{{{
+PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/tests/functional/testcases.py
+}}}
+
+And to run everything except the functional tests,
+{{{
+PYTHONPATH=. ./trac/test.py --skip-functional-tests
+}}}
+
+
+NOTE: Unlike most unittests, the functional tests share a test fixture across tests.  This means that you can't(*) run just one of the tests by itself.
+But you can run a sub-set of the functional tests:
+{{{
+PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/tests/functional/__init__.py
+PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/ticket/tests/functional.py
+PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 ./trac/versioncontrol/tests/functional.py
+}}}
+Note that that there is a set of functional tests that are run regardless of what subset you choose; these tests setup the shared fixture.
+
+
+(*) Unless you modify the code to comment out the other functional tests.  The test fixture is setup and torn down by `FunctionalTestSuite`, and it runs the tests added to it with `_tester` and `_testenv` set in the testcase objects.
+
+The functional tests require subversion, and use a random local port 8000-8999 for the test web server.
+
+ == Testing output and byproducts ==
+There is some logging done:
+ - testing.log
+    output from trac environment creation, tracd, and some svn commands
+ - functional-testing.log
+    output from twill
+
+The test fixture is left behind in 'testenv' so you can inspect it when debugging a problem.
+{{{
+testenv/htpasswd   -- the password/authentication for the test fixture.  password = username
+       /repo       -- the Subversion repository
+       /trac       -- the Trac environment
+}}}
+(Note that running the tests again will automatically delete this test environment and create a new one.  If you want to save a test environment, you will need to rename this directory before running the tests again.)
+
+The command to serve the test environment is:
+{{{
+PYTHONPATH=.:../twill-0.9:../genshi-0.4.4:../pygments-0.8 \
+./trac/web/standalone.py --basic-auth=trac,testenv/htpasswd, -s \
+    --port=8888 testenv/trac
+}}}
+This is particularly useful when a test fails and you want to explore the environment to debug the problem.
+
+ == Test Coverage ==
+You can now determine statement coverage of unittests and functional tests.  But you'll have to run them separately for now.
+
+`figleaf` will need to be on your `PATH`.  Set the `FIGLEAF` environment variable to the figleaf command.
+
+Create a figleaf-exclude file with filename patterns to exclude.  For example:
+{{{
+/usr/lib/python.*/.*.py
+.*.html
+}}}
+
+Then run the tests something like this:
+{{{
+export FIGLEAF=figleaf
+figleaf ./trac/test.py -v --skip-functional-tests
+mv .figleaf .figleaf.unittests
+python trac/tests/functional/testcases.py -v
+mv .figleaf .figleaf.functional
+figleaf2html --exclude-patterns=../figleaf-exclude .figleaf.functional .figleaf.unittests
+}}}
+Also, this is very slow; on a decent machine, 10 minutes for the functional tests is normal.
+
+To run without figleaf, be sure to unset `FIGLEAF`.
+
+--------------------------------------------------------------------------------
+== TROUBLESHOOTING: ==
+
+ 1. trac-admin is failing on initenv with this exception:
+    {{{
+    raise Exception('Failed with exitcode %s running trac-admin with %r' % (retval, args))
+Exception: Failed with exitcode 1 running trac-admin with ('initenv', 'testenv', 'sqlite:db/trac.db', 'svn', '..../testenv/repo')
+    }}}
+    This can be caused by not having run `python setup.py egg_info` in the genshi tree.
+
+ 2. Windows needs an implementation of crypt or fcrypt.  Carey Evans' pure
+    python version works, but prints warnings on Python 2.3 (they can be
+    ignored).  See http://carey.geek.nz/code/python-fcrypt/
+
+ 3. Python versions compatibility notes
+   * If using Python >=2.4.0, <2.4.4, you need to backport `unittest` from
+     2.4.4.  If you do not do this, you will get an `AttributeError` regarding
+     `_fixture` being unset on every test.
+   * If using Python >=2.3.0, <2.3.5, you need to backport a few modules for
+     `setuptools` to work properly (`httplib2`, `cookielib`, `_*CookieJar`) and
+     remove the tuple imports to replace with explicit line continuations.
+   * If using Python >=2.3.0, <2.4.0, you need `subprocess`, `unittest`, and
+     `traceback` from 2.4.4.  On Windows you also need to modify `subprocess.py`
+     to use `pywin32` instead of `_subprocess`.  Twill includes a `subprocess`
+     module that will not work in this situation because it is not modified.
+

contrib/bugzilla2trac.py

 Other enhancements, Florent Guillaume <fg@nuxeo.com>
 Reworked, Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>
 
-$Id$
+$Id: bugzilla2trac.py 8743 2009-10-30 21:49:24Z rblank $
 """
 
 import re

contrib/checkwiki.py

  "TracPlugins",
  "TracQuery",
  "TracReports",
+ "TracRepositoryAdmin",
  "TracRevisionLog",
  "TracRoadmap",
  "TracRss",

contrib/trac-post-commit-hook

-#!/usr/bin/env python
-
-# trac-post-commit-hook
-# ----------------------------------------------------------------------------
-# 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.
-# ----------------------------------------------------------------------------
-
-# This Subversion post-commit hook script is meant to interface to the
-# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc 
-# system.
-# 
-# It should be called from the 'post-commit' script in Subversion, such as
-# via:
-#
-# REPOS="$1"
-# REV="$2"
-# TRAC_ENV="/path/to/tracenv"
-#
-# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
-#  -p "$TRAC_ENV" -r "$REV"
-#
-# (all the other arguments are now deprecated and not needed anymore)
-#
-# It 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 issue numbers are closed with the contents of this
-#     commit message being added to it. 
-#   references, refs, addresses, re, see 
-#     The specified issue numbers are left in their current status, but 
-#     the contents of this commit message are added to their notes. 
-#
-# 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.
-
-import re
-import os
-import sys
-from datetime import datetime 
-from optparse import OptionParser
-
-parser = OptionParser()
-depr = '(not used anymore)'
-parser.add_option('-e', '--require-envelope', dest='envelope', default='',
-                  help="""
-Require commands to be enclosed in an envelope.
-If -e[], then commands must be in the form of [closes #4].
-Must be two characters.""")
-parser.add_option('-p', '--project', dest='project',
-                  help='Path to the Trac project.')
-parser.add_option('-r', '--revision', dest='rev',
-                  help='Repository revision number.')
-parser.add_option('-u', '--user', dest='user',
-                  help='The user who is responsible for this action '+depr)
-parser.add_option('-m', '--msg', dest='msg',
-                  help='The log message to search '+depr)
-parser.add_option('-c', '--encoding', dest='encoding',
-                  help='The encoding used by the log message '+depr)
-parser.add_option('-s', '--siteurl', dest='url',
-                  help=depr+' the base_url from trac.ini will always be used.')
-
-(options, args) = parser.parse_args(sys.argv[1:])
-
-if not 'PYTHON_EGG_CACHE' in os.environ:
-    os.environ['PYTHON_EGG_CACHE'] = os.path.join(options.project, '.egg-cache')
-
-from trac.env import open_environment
-from trac.ticket.notification import TicketNotifyEmail
-from trac.ticket import Ticket
-from trac.ticket.web_ui import TicketModule
-# TODO: move grouped_changelog_entries to model.py
-from trac.util.text import to_unicode
-from trac.util.datefmt import utc
-from trac.versioncontrol.api import NoSuchChangeset
-
-ticket_prefix = '(?:#|(?:ticket|issue|bug)[: ]?)'
-ticket_reference = ticket_prefix + '[0-9]+'
-ticket_command =  (r'(?P<action>[A-Za-z]*).?'
-                   '(?P<ticket>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
-                   (ticket_reference, ticket_reference))
-
-if options.envelope:
-    ticket_command = r'\%s%s\%s' % (options.envelope[0], ticket_command,
-                                    options.envelope[1])
-    
-command_re = re.compile(ticket_command)
-ticket_re = re.compile(ticket_prefix + '([0-9]+)')
-
-class CommitHook:
-    _supported_cmds = {'close':      '_cmdClose',
-                       'closed':     '_cmdClose',
-                       'closes':     '_cmdClose',
-                       'fix':        '_cmdClose',
-                       'fixed':      '_cmdClose',
-                       'fixes':      '_cmdClose',
-                       'addresses':  '_cmdRefs',
-                       're':         '_cmdRefs',
-                       'references': '_cmdRefs',
-                       'refs':       '_cmdRefs',
-                       'see':        '_cmdRefs'}
-
-    def __init__(self, project=options.project, author=options.user,
-                 rev=options.rev, url=options.url):
-        self.env = open_environment(project)
-        repos = self.env.get_repository()
-        repos.sync()
-        
-        # Instead of bothering with the encoding, we'll use unicode data
-        # as provided by the Trac versioncontrol API (#1310).
-        try:
-            chgset = repos.get_changeset(rev)
-        except NoSuchChangeset:
-            return # out of scope changesets are not cached
-        self.author = chgset.author
-        self.rev = rev
-        self.msg = "(In [%s]) %s" % (rev, chgset.message)
-        self.now = datetime.now(utc)
-
-        cmd_groups = command_re.findall(self.msg)
-
-        tickets = {}
-        for cmd, tkts in cmd_groups:
-            funcname = CommitHook._supported_cmds.get(cmd.lower(), '')
-            if funcname:
-                for tkt_id in ticket_re.findall(tkts):
-                    func = getattr(self, funcname)
-                    tickets.setdefault(tkt_id, []).append(func)
-
-        for tkt_id, cmds in tickets.iteritems():
-            try:
-                db = self.env.get_db_cnx()
-                
-                ticket = Ticket(self.env, int(tkt_id), db)
-                for cmd in cmds:
-                    cmd(ticket)
-
-                # determine sequence number... 
-                cnum = 0
-                tm = TicketModule(self.env)
-                for change in tm.grouped_changelog_entries(ticket, db):
-                    if change['permanent']:
-                        cnum += 1
-                
-                ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
-                db.commit()
-                
-                tn = TicketNotifyEmail(self.env)
-                tn.notify(ticket, newticket=0, modtime=self.now)
-            except Exception, e:
-                # import traceback
-                # traceback.print_exc(file=sys.stderr)
-                print>>sys.stderr, 'Unexpected error while processing ticket ' \
-                                   'ID %s: %s' % (tkt_id, e)
-            
-
-    def _cmdClose(self, ticket):
-        ticket['status'] = 'closed'
-        ticket['resolution'] = 'fixed'
-
-    def _cmdRefs(self, ticket):
-        pass
-
-
-if __name__ == "__main__":
-    if len(sys.argv) < 5:
-        print "For usage: %s --help" % (sys.argv[0])
-        print
-        print "Note that the deprecated options will be removed in Trac 0.12."
-    else:
-        CommitHook()

contrib/trac-post-commit-hook.cmd

-@ECHO OFF
-::
-:: Trac post-commit-hook script for Windows
-::
-:: Contributed by markus, modified by cboos.
-
-:: Usage:
-::
-:: 1) Insert the following line in your post-commit.bat script
-::
-:: call %~dp0\trac-post-commit-hook.cmd %1 %2
-::
-:: 2) Check the 'Modify paths' section below, be sure to set at least TRAC_ENV
-
-
-:: ----------------------------------------------------------
-:: Modify paths here:
-
-:: -- this one *must* be set
-SET TRAC_ENV=
-
-:: -- set if Python is not in the system path
-SET PYTHON_PATH=
-
-:: -- set to the folder containing trac/ if installed in a non-standard location
-SET TRAC_PATH=
-:: ----------------------------------------------------------
-
-:: Do not execute hook if trac environment does not exist
-IF NOT EXIST %TRAC_ENV% GOTO :EOF
-
-set PATH=%PYTHON_PATH%;%PATH%
-set PYTHONPATH=%TRAC_PATH%;%PYTHONPATH%
-
-SET REV=%2
-
-Python "%~dp0\trac-post-commit-hook" -p "%TRAC_ENV%" -r "%REV%" 
-

contrib/trac-svn-hook

+#!/bin/sh
+#
+# = trac-svn-hook =
+# 
+#  Purpose:: this script is meant to be called from the Subversion hooks 
+#            for notifying Trac when changesets are added or modified.
+#
+#  Scope:: The http://trac.edgewall.org/wiki/0.12/TracRepositoryAdmin page
+#          describes how to directly call the relevant trac-admin commands
+#          from the Subversion hooks. In most cases this should be enough,
+#          however this script should make troubleshooting easier and 
+#          has support for notifying multiple Trac environments.
+#
+#  Usage:: copy this script to some central place, for example in your
+#          TRAC_ENV or TRAC_PARENT_ENV folder
+#          **Be sure to read the Configuration Notes section below first**
+#          then fill in the variables listed below the Configuration section.
+#
+# For each Subversion repository $REPOS that has to be monitored by 
+# your Trac environment(s), you need to modify the hooks in order to
+# call the present script:
+#
+# Add this to your `$REPOS/hooks/post-commit` script:
+#
+#     /path/to/trac-svn-hook $REPOS $REV
+#
+# If you allow revision property editing in `$REPOS/hooks/pre-revprop-change`,
+# then you can let Trac know about modified changesets by adding the following
+# lines to the `$REPOS/hooks/post-revprop-change` script:
+#
+#     if [ "$PROPNAME" = "svn:log" -o "$PROPNAME" = "svn:author" ]; then
+#         /path/to/trac-svn-hook $REPOS $REV $USER $PROPNAME
+#     fi
+#
+# See also http://svnbook.red-bean.com/en/1.5/svn.reposadmin.create.html#svn.reposadmin.create.hooks
+#
+#  Platform:: Unix or Cygwin.
+# 
+# On Windows, if you have Cygwin installed, you can also use this
+# script instead of the `trac-svn-hook.cmd`.
+# In your `post-commit.bat` and `post-revprop-change.bat` hooks, call
+# this script using:
+#
+#     bash /path/to/trac-svn-hook "%1" "%2" "%3" "%4"
+#
+# -----------------------------------------------------------------------------
+#
+# == Configuration
+#
+# Uncomment and adapt to your local setup:
+#
+# export TRAC_ENV=/path/to/trac-env:/path/to/another/trac-env
+# export PATH=/path/to/python/bin:$PATH
+# export LD_LIBRARY_PATH=/path/to/python/lib:$LD_LIBRARY_PATH
+#
+# -----------------------------------------------------------------------------
+#
+# == Configuration Notes
+#
+# As a preliminary remark, you should be aware that Subversion usually
+# run the hooks in a very minimal environment.
+# This is why we have to be very explicit about where to find things.
+# 
+# According to http://subversion.apache.org/faq.html#hook-debugging,
+# one useful method for getting the post-commit hook to work is to call
+# the hook manually from a shell, as the user(s) which will end up running 
+# the hook (e.g. wwwrun, www-data, nobody). For example:
+#
+#     env - $REPOS/hooks/post-commit $REPOS 1234
+#
+# or:
+#
+#     env - $REPOS/hooks/post-revprop-change $REPOS 1234 nobody svn:log
+#
+# 
+# The environment variables that have to be set in this script are
+# TRAC_ENV, PATH and eventually LD_LIBRARY_PATH.
+#
+#  TRAC_ENV:: the path(s) to the Trac environment(s)
+#
+# In case you need to maintain more than one environment in sync with
+# the repository (using a different scope or not), simply specify more
+# than one path, using the ":" path separator (or ";" if the script is
+# used on Windows with Cygwin's bash - in this case also don't forget to 
+# enclose the list of paths in quotes, e.g. TRAC_ENV="path1;path2").
+#
+#  PATH:: the folder containing the trac-admin script
+#
+# This folder is typically the same as your Python installation bin/ folder.
+# If this is /usr/bin, then you probably don't need to put it in the PATH. 
+#
+# Note that if you're using a python program installed in a non-default 
+# location (such as /usr/local or a virtual environment), then you need 
+# to add it to the PATH as well.
+#
+#  LD_LIBRARY_PATH:: folder(s) containing additional required libraries
+#
+# You may also need to setup the LD_LIBRARY_PATH accordingly. 
+# The same goes for any custom dependency, such as SQLite libraries or
+# SVN libraries: make sure everything is reachable.
+# For example, if you get errors like "global name 'sqlite' is not defined"
+# or similar, then make sure the LD_LIBRARY_PATH contains the path to all
+# the required libraries (libsqlite3.so in the above example).
+#
+#
+# -----------------------------------------------------------------------------
+#
+# == Examples
+#
+# === Minimal setup example ===
+#
+# Python is installed in /usr/bin, Trac was easy_install'ed.
+#
+# {{{
+# export TRAC_ENV=/srv/trac/the_trac_env
+# }}}
+#
+#
+# === Virtualenv setup example ===
+#
+# Here we're using a Trac installation set up using virtualenv
+# (http://pypi.python.org/pypi/virtualenv).
+#
+# In this example, the virtualenv is located in
+# /packages/trac/branches/trac-multirepos
+# and is based off a custom Python installation (/opt/python-2.4.4). 
+# We're also using a custom SQLite build (/opt/sqlite-3.3.8). 
+#
+# Note that virtualenv's activate script doesn't seem to care
+# about LD_LIBRARY_PATH and the only other thing it does and that
+# we need here is to set the PATH, we can as well do that ourselves:
+#
+# We also want to notify two Trac instances:
+#
+# {{{
+# export TRAC_ENV=/srv/trac/the_trac_env:/srv/trac/trac_other_trac_env
+# export PATH=/packages/trac/branches/trac-multirepos/bin:$PATH
+# export LD_LIBRARY_PATH=/opt/python-2.4.4/lib:/opt/sqlite-3.3.8/lib:$LD_LIBRARY_PATH
+# }}}
+#
+#
+# === Cygwin setup example ===
+#
+# {{{
+# export TRAC_ENV=C:/Workspace/local/trac/devel
+# export PYTHONPATH=C:/Workspace/src/trac/repos/multirepos
+# export PATH=/C/Dev/Python261/Scripts:$PATH
+# }}}
+#
+# -----------------------------------------------------------------------------
+#
+# This is the script itself, you shouldn't need to modify this part.
+
+# -- Command line arguments (cf. usage)
+
+REPOS="$1"
+REV="$2"
+USER="$3"
+PROPNAME="$4"
+
+# -- Foolproofing
+
+if [ -z "$REPOS" -o -z "$REV" ]; then
+    echo "Usage: $0 REPOS REV"
+    exit 2
+fi
+
+if ! python -V 2>/dev/null; then
+    echo "python is not in the PATH ($PATH), check PATH and LD_LIBRARY_PATH."
+    exit 2
+fi
+
+if [ -z "$TRAC_ENV" ]; then
+    echo "TRAC_ENV is not set."
+    exit 2
+fi
+
+# -- Feedback
+
+echo "----"
+
+if [ -z "$USER" -a -z "$PROPNAME" ]; then
+    EVENT="added"
+    echo "Changeset $REV was added in $REPOS"
+else
+    EVENT="modified"
+    echo "Changeset $REV was modified by $USER in $REPOS"
+fi
+
+# -- Call "trac-admin ... changeset ... $REPOS $REV" for each Trac environment
+
+ifs=$IFS
+IFS=:
+if [ -n $BASH_VERSION ]; then # we can use Bash syntax
+    if [[ ${BASH_VERSINFO[5]} = *cygwin ]]; then
+        IFS=";"
+    fi
+fi
+for env in $TRAC_ENV; do
+    if [ -e "$env/VERSION" ]; then
+        trac-admin $env changeset $EVENT $REPOS $REV && \
+        echo "$env has been successfully notified" || \
+        echo "ERROR: $env has not been notified"
+    else
+        echo "$env doesn't seem to be a Trac environment, skipping..."
+    fi
+done
+IFS=$ifs
+

contrib/trac-svn-post-commit-hook.cmd

+@ECHO OFF
+::
+:: Trac post-commit-hook script for Windows
+::
+:: Contributed by markus, modified by cboos.
+:: Modified for the multirepos branch to use the `changeset` command.
+
+:: Usage:
+::
+:: 1. Insert the following line in your REPOS/hooks/post-commit.bat script:
+::
+::      call %~dp0\trac-post-commit-hook.cmd %1 %2
+::
+:: 2. Check the 'Modify paths' section below, be sure to set at least TRAC_ENV
+::
+:: 3. Verify that the hook is working:
+::
+::      - enable DEBUG level logging to a file and to the console 
+::        (see TracLogging)
+::
+::      - call the trac-post-commit-hook.cmd from a cmd.exe shell:
+::
+::          trac-post-commit-hook.cmd <REPOS> 123
+::
+::      - call the post-commit.bat hook from a cmd.exe shell (check that
+::        no unwanted side-effects could be triggered when doing this...):
+::
+::          post-commit.bat <REPOS> 123
+::
+::      - in each case, verify that you actually see the logging from Trac
+::        and in particular that you see something like (near the end):
+::
+::          DEBUG: Event changeset_added on <REPOS> for revision 123
+::
+
+
+:: ----------------------------------------------------------
+:: Modify paths here:
+
+:: -- this one *must* be set
+set TRAC_ENV=
+
+:: -- set if Python is not in the system path
+set PYTHON_PATH=
+
+:: -- set to the folder containing trac/ if installed in a non-standard location
+set TRAC_PATH=
+:: ----------------------------------------------------------
+
+:: -- Do not execute hook if trac environment does not exist
+if not exist %TRAC_ENV% goto :EOF
+
+:: -- Determine trac-admin
+
+:: By default assume it's reachable from the PATH
+set TRAC_ADMIN=trac-admin.exe
+
+:: ... or take it from the Scripts folder of the specified Python installation
+if not %PYTHON_PATH%.==. set TRAC_ADMIN="%PYTHON_PATH%/Scripts/trac-admin.exe"
+
+:: ... or take it from the specified Trac source checkout
+if not %TRAC_PATH%.==. set TRAC_ADMIN=python.exe "%TRAC_PATH%/trac/admin/console.py"
+
+:: -- Setup the environment
+set PATH=%PYTHON_PATH%;%PATH%
+set PYTHONPATH=%TRAC_PATH%;%PYTHONPATH%
+
+:: -- Retrieve the information that Subversion gave to the hook
+set REPOS=%1
+set REV=%2
+
+:: Now we're about to call trac-admin's changeset added command.
+:: We have to call it like that:
+::
+::   repository changeset added <repos> <rev>
+::
+:: where <repos> can be the repository symbolic name or directly
+:: the repository directory, which we happen to have in %REPOS%.
+
+%TRAC_ADMIN% "%TRAC_ENV%" changeset added "%REPOS%" "%REV%"
+
+:: Based on either the symbolic name or the %REPOS% information, 
+:: Trac will figure out which repository (or which scoped repositories)
+:: it has to synchronize.

sample-plugins/revision_links.py

 
     def get_wiki_syntax(self):
         def revlink(f, match, fullmatch):
-            rev = match.split(' ', 1)[1] # ignore keyword
-            return self._format_revision_link(f, 'revision', rev, rev,
+            elts = match.split()
+            rev = elts[1] # ignore keyword
+            reponame = ''
+            if len(elts) > 2: # reponame specified
+                reponame = elts[-1]
+            return self._format_revision_link(f, 'revision', reponame, rev, rev,
                                               fullmatch)
 
-        yield (r"!?(?:%s)\s+%s" % ("|".join(self.KEYWORDS),
-                                   ChangesetModule.CHANGESET_ID),
-               revlink)
+        yield (r"!?(?:%s)\s+%s(?:\s+in\s+\w+)?" % 
+               ("|".join(self.KEYWORDS), ChangesetModule.CHANGESET_ID), revlink)
 
     def get_link_resolvers(self):
-        yield ('revision', self._format_revision_link)
+        def resolverev(f, ns, rev, label, fullmatch):
+            return self._format_revision_link(f, ns, '', rev, label, fullmatch)
+        yield ('revision', resolverev)
 
-    def _format_revision_link(self, formatter, ns, rev, label, fullmatch=None):
+    def _format_revision_link(self, formatter, ns, reponame, rev, label, 
+                              fullmatch=None):
         rev, params, fragment = formatter.split_link(rev)
         try:
-            changeset = self.env.get_repository().get_changeset(rev)
-            return tag.a(label, class_="changeset",
-                         title=shorten_line(changeset.message),
-                         href=(formatter.href.changeset(rev) +
-                               params + fragment))
+            repos = self.env.get_repository(reponame)
+            if repos:
+                changeset = repos.get_changeset(rev)
+                return tag.a(label, class_="changeset",
+                             title=shorten_line(changeset.message),
+                             href=(formatter.href.changeset(rev) +
+                                   params + fragment))
         except NoSuchChangeset:
-            return tag.a(label, class_="missing changeset",
-                         href=formatter.href.changeset(rev),
-                         rel="nofollow")
+            pass
+        return tag.a(label, class_="missing changeset", rel="nofollow",
+                     href=formatter.href.changeset(rev))
+        
 [egg_info]
-tag_build = dev
+tag_build = multirepos
 tag_svn_revision = true
 
 [bdist_wininst]
         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.svn_fs = trac.versioncontrol.svn_fs
         trac.versioncontrol.svn_prop = trac.versioncontrol.svn_prop
         trac.versioncontrol.web_ui = trac.versioncontrol.web_ui
         tracopt.mimeview.php = tracopt.mimeview.php
         tracopt.perm.authz_policy = tracopt.perm.authz_policy
         tracopt.perm.config_perm_provider = tracopt.perm.config_perm_provider
+        tracopt.ticket.commit_updater = tracopt.ticket.commit_updater
     """,
 
     **extra

trac/admin/console.py

         try:
             import readline
             delims = readline.get_completer_delims()
-            for c in '-/:':
+            for c in '-/:()':
                 delims = delims.replace(c, '')
             readline.set_completer_delims(delims)
             
 
     ## Initenv
     _help_initenv = [
-        ('initenv', '[<projectname> <db> <repostype> <repospath>]',
+        ('initenv', '[<projectname> <db> [<repostype> <repospath>]]',
          """Create and initialize a new environment
          
          If no arguments are given, then the required parameters are requested
         ddb = 'sqlite:db/trac.db'
         prompt = _("Database connection string [%(default)s]> ", default=ddb)
         returnvals.append(raw_input(prompt).strip() or ddb)
-        printout(_(""" 
- Please specify the type of version control system,
- By default, it will be svn.
-
- If you don't want to use Trac with version control integration,
- choose the default here and don\'t specify a repository directory.
- in the next question.
-"""))
-        drpt = 'svn'
-        prompt = _("Repository type [%(default)s]> ", default=drpt)
-        returnvals.append(raw_input(prompt).strip() or drpt)
-        printout(_("""
- Please specify the absolute path to the version control
- repository, or leave it blank to use Trac without a repository.
- You can also set the repository location later.
-"""))
-        prompt = _("Path to repository [/path/to/repos]> ")
-        returnvals.append(raw_input(prompt).strip())
         print
         return returnvals
 
         arg = arg or [''] # Reset to usual empty in case we popped the only one
         project_name = None
         db_str = None
+        repository_type = None
         repository_dir = None
         if len(arg) == 1 and not arg[0]:
-            returnvals = self.get_initenv_args()
-            project_name, db_str, repository_type, repository_dir = returnvals
-        elif len(arg) != 4:
+            project_name, db_str = self.get_initenv_args()
+        elif len(arg) == 2:
+            project_name, db_str = arg
+        elif len(arg) == 4:
+            project_name, db_str, repository_type, repository_dir = arg
+        else:
             initenv_error('Wrong number of arguments: %d' % len(arg))
             return 2
-        else:
-            project_name, db_str, repository_type, repository_dir = arg[:4]
 
         try:
             printout(_("Creating and Initializing Project"))
             options = [
+                ('project', 'name', project_name),
                 ('trac', 'database', db_str),
-                ('trac', 'repository_type', repository_type),
-                ('trac', 'repository_dir', repository_dir),
-                ('project', 'name', project_name),
             ]
+            if repository_dir:
+                options.extend([
+                    ('trac', 'repository_type', repository_type),
+                    ('trac', 'repository_dir', repository_dir),
+                ])
             if inherit_paths:
                 options.append(('inherit', 'file',
                                 ",\n      ".join(inherit_paths)))
                 try:
                     repos = self.__env.get_repository()
                     if repos:
-                        printout(_(" Indexing repository"))
+                        printout(_(" Indexing default repository"))
                         repos.sync(self._resync_feedback)
                 except TracError, e:
                     printerr(_("""
 ---------------------------------------------------------------------
-Warning: couldn't index the repository.
+Warning: couldn't index the default repository.
 
 This can happen for a variety of reasons: wrong repository type, 
 no appropriate third party library for this repository type,
 
 You can nevertheless start using your Trac environment, but 
 you'll need to check again your trac.ini file and the [trac] 
-repository_type and repository_path settings in order to enable
-the Trac repository browser.
+repository_type and repository_path settings.
 """))
         except Exception, e:
             initenv_error(to_unicode(e))

trac/admin/tests/console-tests.txt

 attachment export    Export an attachment from a resource to a file or stdout
 attachment list      List attachments of a resource
 attachment remove    Remove an attachment from a resource
+changeset added      Notify trac about changesets added to a repository
+changeset modified   Notify trac about changesets modified in a repository
 component add        Add a new component
 component chown      Change component ownership
 component list       Show available components
 priority list        Show possible ticket priorities
 priority order       Move a priority value up or down in the list
 priority remove      Remove a priority value
+repository add       Add a source repository
+repository alias     Create an alias for a repository
+repository list      List source repositories
+repository remove    Remove a source repository
+repository resync    Re-synchronize trac with repositories
+repository set       Set an attribute of a repository
+repository sync      Resume synchronization of repositories
 resolution add       Add a resolution value option
 resolution change    Change a resolution value
 resolution list      Show possible ticket resolutions
 resolution order     Move a resolution value up or down in the list
 resolution remove    Remove a resolution value
-resync               Re-synchronize trac with the repository
 severity add         Add a severity value option
 severity change      Change a severity value
 severity list        Show possible ticket severities

trac/db_default.py

 from trac.db import Table, Column, Index
 
 # Database version identifier. Used for automatic upgrades.
-db_version = 22
+db_version = 24
 
 def __mkreports(reports):
     """Utility function used to create report data in same syntax as the
         Index(['time'])],
 
     # Version control cache
-    Table('revision', key='rev')[
+    Table('repository', key=('id', 'name'))[
+        Column('id', type='int'),
+        Column('name'),
+        Column('value')],
+    Table('revision', key=('repos', 'rev'))[
+        Column('repos', type='int'),
         Column('rev'),
         Column('time', type='int'),
         Column('author'),
         Column('message'),
-        Index(['time'])],
-    Table('node_change', key=('rev', 'path', 'change_type'))[
+        Index(['repos', 'time'])],
+    Table('node_change', key=('repos', 'rev', 'path', 'change_type'))[
+        Column('repos', type='int'),
         Column('rev'),
         Column('path'),
         Column('node_type', size=1),
         Column('change_type', size=1),
         Column('base_path'),
         Column('base_rev'),
-        Index(['rev'])],
+        Index(['repos', 'rev'])],
 
     # Ticket system
     Table('ticket', key='id')[
             ('system',
               ('name', 'value'),
                 (('database_version', str(db_version)),
-                 ('initial_database_version', str(db_version)),
-                 ('youngest_rev', ''))),
+                 ('initial_database_version', str(db_version)))),
             ('report',
               ('author', 'title', 'query', 'description'),
                 __mkreports(get_reports(db))))
                 break
             cname = cname[:idx]
 
-        # versioncontrol components are enabled if the repository is configured
-        # FIXME: this shouldn't be hardcoded like this
-        if component_name.startswith('trac.versioncontrol.'):
-            return self.config.get('trac', 'repository_dir') != ''
-
         # By default, all components in the trac package are enabled
         return component_name.startswith('trac.') or None
 
             hdlr.close()
             del self.log._trac_handler
 
-    def get_repository(self, authname=None):
+    def get_repository(self, reponame=None, authname=None):
         """Return the version control repository configured for this
         environment.
         
         @param authname: user name for authorization
         """
-        return RepositoryManager(self).get_repository(authname)
+        return RepositoryManager(self).get_repository(reponame)
 
     def create(self, options=[]):
         """Create the basic directory structure of the environment, initialize
     def upgrade(self, backup=False, backup_dest=None):
         """Upgrade database.
         
-        Each db version should have its own upgrade module, names
+        Each db version should have its own upgrade module, named
         upgrades/dbN.py, where 'N' is the version number (int).
 
         @param backup: whether or not to backup before upgrading

trac/htdocs/css/browser.css

 
 /* Styles for the directory entries table
    (extends the styles for "table.listing") */
-#dirlist { margin-top: 0 }
-#dirlist td.rev, #dirlist td.age, #dirlist td.change {
+table.dirlist { margin-top: 0 }
+table.dirlist td.rev, table.dirlist td.age, table.dirlist td.change {
  color: #888;
  white-space: nowrap;
- vertical-align: baseline;
+ vertical-align: middle;
 }
-#dirlist td.rev {
+table.dirlist td.rev {
  font-family: monospace;
  letter-spacing: -0.08em;
  font-size: 90%;
  text-align: right;
 }
-#dirlist td.size {  
+table.dirlist td.size {  
  color: #888;
  white-space: nowrap;
  text-align: right;
  vertical-align: middle;
  font-size: 70%;
 }
-#dirlist td.age {
+table.dirlist td.age {
  border-width: 0 2px 0 0;
  border-style: solid;
  font-size: 85%;
 }
-#dirlist td.name { width: 100% }
-#dirlist td.name a, #dirlist td.name span {
+table.dirlist td.name { width: 100% }
+table.dirlist td.name a, table.dirlist td.name span {
  background-position: 0% 50%;
  background-repeat: no-repeat;
  padding-left: 20px;
 }
-#dirlist td.name a.parent { background-image: url(../parent.png) }
-#dirlist td.name div { white-space: pre }
-#dirlist tr span.expander { 
+table.dirlist td.name a.parent { background-image: url(../parent.png) }
+table.dirlist td.name div { white-space: pre }
+table.dirlist tr span.expander { 
   background-image: url(../expander_normal.png); 
   cursor: pointer; 
   padding-left: 8px; 
   margin-left: 4px; 
 }
-#dirlist tr span.expander:hover { 
+table.dirlist tr span.expander:hover { 
   background-image: url(../expander_normal_hover.png); 
 }
-#dirlist tr.expanded span.expander { 
+table.dirlist tr.expanded span.expander { 
   background-image: url(../expander_open.png); 
   padding-left: 12px; 
   margin-left: 0; 
 }
-#dirlist tr.expanded span.expander:hover { 
+table.dirlist tr.expanded span.expander:hover { 
   background-image: url(../expander_open_hover.png); 
 }
-#dirlist td.name a.dir { background-image: url(../folder.png) }
-#dirlist td.name a.file { background-image: url(../file.png); display: block }
-#dirlist td.name a, #dirlist td.rev a { border-bottom: none }
-#dirlist td.rev { text-align: right }
-#dirlist td.change { 
-  font-size: 85%; 
-  vertical-align: middle; 
-  white-space: nowrap 
+table.dirlist td.name a.dir { background-image: url(../folder.png) }
+table.dirlist td.name a.file { background-image: url(../file.png); display: block }
+table.dirlist td.name a, table.dirlist td.rev a { border-bottom: none }
+table.dirlist td.change { font-size: 85% }
+table.dirlist td.rev a.chgset { 
+  background-repeat: no-repeat;
+  background-image: url(../changeset.png); 
+  background-position: 100% 50%;
+  padding: 0 0 0 5px; 
+  margin: 0 5px 0 0; 
 }
+table.dirlist td.description { padding-left: 2em }
+table.dirlist td.description > :first-child { margin-top: 0 }
+table.dirlist td.description > :last-child { margin-bottom: 0 }
 
-#dirlist td span.loading { 
+table.dirlist td span.loading { 
   background-image: url(../loading.gif); 
   font-style: italic 
 }
 
+#content.browser div.description { padding: 0 0.5em }
+
 /* Style for the ''View Changes'' button and the diff preparation form */
 #anydiff { margin: 0 0 1em; float: left }
 #anydiff form, #anydiff div, #anydiff h2 { display: inline }
 
 /* Styles for the revision log table (extends the styles for "table.listing") */
 table.chglist { margin-top: 0 }
+.chglist td.diff, .chglist td.rev, .chglist td.age, 
+.chglist td.author, .chglist td.change {
+ white-space: nowrap;
+ vertical-align: middle;
+}
 .chglist td.change span { 
  border: 1px solid #999;
  display: block;
  margin: .2em .5em 0 0;
  width: .8em; height: .8em;
 }
-.chglist td.diff { white-space: nowrap }
+.chglist td.diff { padding: 1px }
 .chglist td.change .comment { display: none }
-.chglist td.old_path { font-style: italic }
-.chglist td.date {
+.chglist td.age {
  font-size: 85%;
- vertical-align: top;
  padding-top: 0.55em;
- white-space: nowrap;
 }
-.chglist td.author { font-size: 85%; vertical-align: top; padding-top: 0.55em }
-.chglist td.rev, .chglist td.chgset { 
+.chglist td.author { font-size: 85%; }
+.chglist td.rev { 
  font-family: monospace;  
  letter-spacing: -0.08em;
  font-size: 90%;
  text-align: right; 
 }
-.chglist td.rev a, .chglist td.chgset a { border-bottom: none }
-.chglist td.summary { 
+.chglist td.rev a { border-bottom: none }
+.chglist td.rev a.chgset {
+  background-repeat: no-repeat;
+  background-image: url(../changeset.png); 
+  background-position: 100% 50%;
+  padding: 0 0 0 5px; 
+  margin: 0 5px 0 0; 
+}
+
+.chglist td.summary, .chglist td.log { 
  width: 100%; 
  font-size: 85%; 
  vertical-align: middle; 
- white-space: nowrap;
 }
-.chglist tr.verbose td.summary {
+.chglist td.summary *, .chglist td.log * { margin-top: 0 }
+/* verbose mode */
+.chglist tr.verbose { border-top: none }
+.chglist tr.verbose td.filler, .chglist tr.verbose td.log {
  border: none;
+ border-bottom: 1px solid #ddd;
  color: #333;
- padding: .5em 1em 1em 2em;
- white-space: normal;
 }
-
-.chglist td.summary * { margin-top: 0 }
+.chglist tr.verbose td { border: none; }
+.chglist tr.verbose td.diff, .chglist tr.verbose td.filler {
+ border-left: 1px solid #ddd; 
+}
+.chglist tr.verbose td.summary, .chglist tr.verbose td.log {
+ border-right: 1px solid #ddd; 
+}
 
 #paging { margin: 1em 0 }
 

trac/htdocs/css/trac.css

 }
 option { border-bottom: 1px dotted #d7d7d7 }
 fieldset { border: 1px solid #d7d7d7; padding: .5em; margin: 1em 0 }
-form p.hint, form span.hint { color: #666; font-size: 85%; font-style: italic; margin: .5em 0;
+p.hint, span.hint { color: #666; font-size: 85%; font-style: italic; margin: .5em 0;
   padding-left: 1em;
 }
 fieldset.iefix {

trac/htdocs/js/blame.js

 
 (function($){
 
-  window.enableBlame = function(url, original_path) {
+  window.enableBlame = function(url, reponame, original_path) {
     var message = null;
     var message_rev = null;
   
       if ( href ) {
         a.removeAttr("href");
         href = href.slice(href.indexOf("changeset/") + 10);
+        if (reponame)
+            href = href.substr(reponame.length);
         var sep = href.indexOf("/");
         if ( sep > 0 )
           path = href.slice(sep+1);
           message_rev = rev;
           highlight_rev = message_rev;
   
-          $.get(url + rev.substr(1), {annotate: annotate_path}, function(data) {
+          $.get(url + [rev.substr(1), reponame].join("/"), 
+                {annotate: annotate_path}, function(data) {
             // remove former message panel if any
             if (message)
               message.remove();

trac/htdocs/js/timeline_multirepos.js

+jQuery(document).ready(function($){
+  csetfilter = $("input[name=changeset]");
+  function toggleRepositories() {
+    $("input[name^=repo-]").parent().toggle();
+  }
+  csetfilter.click(toggleRepositories);
+  if (csetfilter.checked()) 
+    toggleRepositories();
+});

trac/mimeview/api.py

 
     @classmethod
     def from_request(cls, req, resource=None, id=False, version=False,
-                     absurls=False):
+                     parent=False, absurls=False):
         """Create a rendering context from a request.
 
         The `perm` and `href` properties of the context will be initialized
         else:
             href = None
             perm = None
-        self = cls(Resource(resource, id=id, version=version), href=href,
-                   perm=perm)
+        self = cls(Resource(resource, id=id, version=version, parent=parent),
+                   href=href, perm=perm)
         self.req = req
         return self
 
             context = context.parent
         return '<%s %s>' % (type(self).__name__, ' - '.join(reversed(path)))
 
-    def __call__(self, resource=None, id=False, version=False):
+    def __call__(self, resource=None, id=False, version=False, parent=False):
         """Create a nested rendering context.
 
         `self` will be the parent for the new nested context.
         True
         """
         if resource:
-            resource = Resource(resource, id=id, version=version)
+            resource = Resource(resource, id=id, version=version,
+                                parent=parent)
         else:
             resource = self.resource
         context = Context(resource, href=self.href, perm=self.perm)
         resource.parent = parent
         return resource
 
-
     def __call__(self, realm=False, id=False, version=False, parent=False):
         """Create a new Resource using the current resource as a template.
 
         >>> repr(Resource(None).child('attachment', 'file.txt'))
         "<Resource u', attachment:file.txt'>"
         """
-        return self.__call__(realm, id, version, self)
-    
+        return Resource(realm, id, version, self)
 
 
 class ResourceSystem(Component):

trac/templates/diff_options.html

+<!--! Add diff option fields (to be used inside a form)
+ 
+     `diff` the datastructure which contains diff options
+-->
+<div xmlns="http://www.w3.org/1999/xhtml"
+    xmlns:py="http://genshi.edgewall.org/"
+    xmlns:xi="http://www.w3.org/2001/XInclude"
+    py:strip="">
+  <label for="style">View differences</label>
+  <select id="style" name="style">
+    <option selected="${diff.style == 'inline' or None}"
+            value="inline">inline</option>
+    <option selected="${diff.style == 'sidebyside' or None}"
+            value="sidebyside">side by side</option>
+  </select>
+  <div class="field">
+    Show <input type="text" name="contextlines" id="contextlines" size="2"
+          maxlength="3" value="${diff.options.contextlines &lt; 0 and 
+                                 'all' or diff.options.contextlines}" />
+    <label for="contextlines">lines around each change</label>
+  </div>
+  <fieldset id="ignore" py:with="options = diff.options">
+    <legend>Ignore:</legend>
+    <div class="field">
+      <input type="checkbox" id="ignoreblanklines" name="ignoreblanklines"
+             checked="${options.ignoreblanklines or None}" />
+      <label for="ignoreblanklines">Blank lines</label>
+    </div>
+    <div class="field">
+      <input type="checkbox" id="ignorecase" name="ignorecase"
+             checked="${options.ignorecase or None}" />
+      <label for="ignorecase">Case changes</label>
+    </div>
+    <div class="field">
+      <input type="checkbox" id="ignorewhitespace" name="ignorewhitespace"
+             checked="${options.ignorewhitespace or None}" />
+      <label for="ignorewhitespace">White space changes</label>
+    </div>
+  </fieldset>
+  <div class="buttons">
+    <input type="submit" name="update" value="${_('Update')}" />
+  </div>
+</div>

trac/templates/diff_view.html

           <input py:for="k, v in diff_args or []" type="hidden" name="$k" value="$v"/>
           <input type="hidden" name="version" value="$new_version" />
           <input type="hidden" name="old_version" value="$old_version" />
-          ${diff_options_fields(diff)}
+          <xi:include href="diff_options.html" />
         </div>
       </form>
       <dl id="overview" py:with="multi = num_changes &gt; 1">

trac/templates/error.html

 
 ==== Python Traceback ====
 {{{
-${traceback}
+${to_unicode(traceback)}
 }}}</textarea>
     <span class="inlinebuttons">
       <input type="submit" name="create" value="${_('Create')}" />

trac/templates/macros.html

       pretty_size(size)
   }</span></py:def>
 
-  <!--!  Display author information, eventually obfuscating the e-mail address
-  -
-  -      We take care to not insert any extra space.
-  -->
-  <py:def function="authorinfo(author, email_map=None)"><py:choose><py:when test="author"><py:with
-    vars="author = show_email_addresses and email_map and '@' not in author and email_map[author] or author">${
-      author and format_author(author) or 'anonymous'
-  }</py:with></py:when><py:otherwise>anonymous</py:otherwise></py:choose></py:def>
-
-  <!--!  Display a sequence of path components.
-  -
-  -      Each component is a link to the corresponding location in the browser.
-  -->
-  <py:def function="browser_path_links(path_links,rev=None)">
-    <py:for each="idx, part in enumerate(path_links)"><py:with
-        vars="first = idx == 0; last = idx == len(path_links) - 1"><a
-          class="${classes('pathentry', first=first)}"
-          title="${first and _('Go to root directory') or _('View %(folder)s', folder=part.name)}"
-          href="$part.href">$part.name</a><py:if
-        test="not last"><span class="pathentry sep">/</span></py:if></py:with></py:for>
-    <py:if test="rev"><span class="pathentry sep">@</span>
-      <a class="pathentry" href="${href.changeset(rev)}" title="View changeset $rev">$rev</a>
-    </py:if>
-    <br style="clear: both" />
-  </py:def>
-
   <!--! Add Previous/Up/Next navigation links
   -
   -     `label` the label to use after the Previous/Next words
     </li>
   </ul>
 
-  <!--! Add diff option fields (to be used inside a form)
-  -
-  -     `diff` the datastructure which contains diff options
-  -
-  -->
-  <py:def function="diff_options_fields(diff)">
-    <label for="style">View differences</label>
-    <select id="style" name="style">
-      <option selected="${diff.style == 'inline' or None}"
-              value="inline">inline</option>
-      <option selected="${diff.style == 'sidebyside' or None}"
-              value="sidebyside">side by side</option>
-    </select>
-    <div class="field">
-      Show <input type="text" name="contextlines" id="contextlines" size="2"
-                  maxlength="3" value="${diff.options.contextlines &lt; 0 and 'all' or diff.options.contextlines}" />
-      <label for="contextlines">lines around each change</label>
-    </div>
-    <fieldset id="ignore" py:with="options = diff.options">
-      <legend>Ignore:</legend>
-      <div class="field">
-        <input type="checkbox" id="ignoreblanklines" name="ignoreblanklines"
-               checked="${options.ignoreblanklines or None}" />
-        <label for="ignoreblanklines">Blank lines</label>
-      </div>
-      <div class="field">
-        <input type="checkbox" id="ignorecase" name="ignorecase"
-               checked="${options.ignorecase or None}" />
-        <label for="ignorecase">Case changes</label>
-      </div>
-      <div class="field">
-        <input type="checkbox" id="ignorewhitespace" name="ignorewhitespace"
-               checked="${options.ignorewhitespace or None}" />
-        <label for="ignorewhitespace">White space changes</label>
-      </div>
-    </fieldset>
-    <div class="buttons">
-      <input type="submit" name="update" value="${_('Update')}" />
-    </div>
-  </py:def>
 
   <!--! Display a div for visualizing a preview of a file content
   -
 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

trac/ticket/admin.py

                               format_datetime
 from trac.util.text import print_table, printout, exception_to_unicode
 from trac.util.translation import _, N_, gettext
-from trac.web.chrome import add_notice, add_script, add_warning, Chrome
+from trac.web.chrome import Chrome, add_notice, add_warning
 
 
 class TicketAdminPanel(Component):

trac/upgrades/db23.py

+from trac.db import Table, Column, Index, DatabaseManager
+
+def do_upgrade(env, ver, cursor):
+    # Make changeset cache multi-repository aware
+    cursor.execute("CREATE TEMPORARY TABLE rev_old "
+                   "AS SELECT * FROM revision")
+    cursor.execute("DROP TABLE revision")
+    cursor.execute("CREATE TEMPORARY TABLE nc_old "
+                   "AS SELECT * FROM node_change")
+    cursor.execute("DROP TABLE node_change")
+    
+    tables = [Table('repository', key=('id', 'name'))[
+                Column('id'),
+                Column('name'),
+                Column('value')],
+              Table('revision', key=('repos', 'rev'))[
+                Column('repos'),
+                Column('rev'),
+                Column('time', type='int'),
+                Column('author'),
+                Column('message'),
+                Index(['repos', 'time'])],
+              Table('node_change', key=('repos', 'rev', 'path', 'change_type'))[
+                Column('repos'),
+                Column('rev'),
+                Column('path'),
+                Column('node_type', size=1),
+                Column('change_type', size=1),
+                Column('base_path'),
+                Column('base_rev'),
+                Index(['repos', 'rev'])]]
+    
+    db_connector, _ = DatabaseManager(env)._get_connector()
+    for table in tables:
+        for stmt in db_connector.to_sql(table):
+            cursor.execute(stmt)
+    
+    cursor.execute("INSERT INTO revision (repos,rev,time,author,message) "
+                   "SELECT '',rev,time,author,message FROM rev_old")
+    cursor.execute("DROP TABLE rev_old")
+    cursor.execute("INSERT INTO node_change (repos,rev,path,node_type,"
+                   "change_type,base_path,base_rev) "
+                   "SELECT '',rev,path,node_type,change_type,base_path,"
+                   "base_rev FROM nc_old")
+    cursor.execute("DROP TABLE nc_old")
+    
+    cursor.execute("INSERT INTO repository (id,name,value) "
+                   "SELECT '',name,value FROM system "
+                   "WHERE name IN ('repository_dir', 'youngest_rev')")
+    cursor.execute("DELETE FROM system "
+                   "WHERE name IN ('repository_dir', 'youngest_rev')")

trac/upgrades/db24.py

+from trac.db import Table, Column, Index, DatabaseManager
+
+def do_upgrade(env, ver, cursor):
+    # Change repository key from reponame to a surrogate id
+    cursor.execute("SELECT id FROM repository "
+                   "UNION SELECT repos AS id FROM revision "
+                   "UNION SELECT repos AS id FROM node_change "
+                   "ORDER BY id")
+    id_name_list = [(i + 1, name) for i, (name,) in enumerate(cursor)]
+    
+    cursor.execute("CREATE TEMPORARY TABLE repo_old "
+                   "AS SELECT * FROM repository")
+    cursor.execute("DROP TABLE repository")
+    cursor.execute("CREATE TEMPORARY TABLE rev_old "
+                   "AS SELECT * FROM revision")
+    cursor.execute("DROP TABLE revision")
+    cursor.execute("CREATE TEMPORARY TABLE nc_old "
+                   "AS SELECT * FROM node_change")
+    cursor.execute("DROP TABLE node_change")
+    
+    tables = [Table('repository', key=('id', 'name'))[
+                  Column('id', type='int'),
+                  Column('name'),
+                  Column('value')],
+              Table('revision', key=('repos', 'rev'))[
+                  Column('repos', type='int'),
+                  Column('rev'),
+                  Column('time', type='int'),
+                  Column('author'),
+                  Column('message'),
+                  Index(['repos', 'time'])],
+              Table('node_change', key=('repos', 'rev', 'path', 'change_type'))[
+                  Column('repos', type='int'),
+                  Column('rev'),
+                  Column('path'),
+                  Column('node_type', size=1),
+                  Column('change_type', size=1),
+                  Column('base_path'),
+                  Column('base_rev'),
+                  Index(['repos', 'rev'])]]
+    
+    db_connector, _ = DatabaseManager(env)._get_connector()
+    for table in tables:
+        for stmt in db_connector.to_sql(table):
+            cursor.execute(stmt)
+    
+    cursor.executemany("INSERT INTO repository (id,name,value) "
+                       "VALUES (%s,'name',%s)", id_name_list)
+    cursor.executemany("INSERT INTO repository (id,name,value) "
+                       "SELECT %s,name,value FROM repo_old WHERE id=%s",
+                       id_name_list)
+    cursor.execute("DROP TABLE repo_old")
+    cursor.executemany("INSERT INTO revision (repos,rev,time,author,message) "
+                       "SELECT %s,rev,time,author,message FROM rev_old "
+                       "WHERE repos=%s", id_name_list)
+    cursor.execute("DROP TABLE rev_old")
+    cursor.executemany("INSERT INTO node_change (repos,rev,path,node_type,"
+                       "  change_type,base_path,base_rev) "
+                       "SELECT %s,rev,path,node_type,change_type,base_path,"
+                       "  base_rev FROM nc_old WHERE repos=%s", id_name_list)
+    cursor.execute("DROP TABLE nc_old")

trac/util/__init__.py

     if max is not None and value > max:
         value = max
     return value
+
+def pathjoin(*args):
+    """Strip `/` from the arguments and join them with a single `/`."""
+    return '/'.join(filter(None, (each.strip('/') for each in args if each)))

trac/util/text.py

                    (address[-1] == '>' and '>' or '')
     return address
 
+def breakable_path(path):
+    """Make a path breakable after path separators, and conversely, avoid
+    breaking at spaces.
+    """
+    if not path:
+        return path
+    prefix = ''
+    if path.startswith('/'):    # Avoid breaking after a leading /
+        prefix = '/'
+        path = path[1:]
+    return prefix + path.replace('/', u'/\u200b').replace('\\', u'\\\u200b') \
+                        .replace(' ', u'\u00a0')
+
+def normalize_whitespace(text, to_space=u'\u00a0', remove=u'\u200b'):
+    """Normalize whitespace in a string, by replacing special spaces by normal
+    spaces and removing zero-width spaces."""
+    if not text:
+        return text
+    return text.replace(u'\u00a0', ' ').replace(u'\u200b', '')
+
 # -- Conversion
 
 def pretty_size(size, format='%.1f'):

trac/versioncontrol/admin.py

 
 import sys
 
-from trac.admin import IAdminCommandProvider
+from trac.admin import IAdminCommandProvider, IAdminPanelProvider
+from trac.config import _TRUE_VALUES
 from trac.core import *
-from trac.util.text import printout
+from trac.util.text import breakable_path, normalize_whitespace, print_table, \
+                           printout
 from trac.util.translation import _, ngettext
+from trac.versioncontrol import DbRepositoryProvider, RepositoryManager, \
+                                is_default
+from trac.web.chrome import Chrome, add_notice, add_warning
 
 
 class VersionControlAdmin(Component):
     """trac-admin command provider for version control administration."""
 
-    implements(IAdminCommandProvider)
+    implements(IAdminCommandProvider, IAdminPanelProvider)
 
     # IAdminCommandProvider methods
     
     def get_admin_commands(self):
-        yield ('resync', '[rev]',
-               """Re-synchronize trac with the repository
+        yield ('changeset added', '<repos> <rev> [rev] [...]',
+               """Notify trac about changesets added to a repository
+               
+               This command should be called from a post-commit hook. It will
+               trigger a cache update and notify components about the addition.
+               """,
+               self._complete_repos, self._do_changeset_added)
+        yield ('changeset modified', '<repos> <rev> [rev] [...]',
+               """Notify trac about changesets modified in a repository
+               
+               This command should be called from a post-revprop hook after
+               revision properties like the commit message, author or date
+               have been changed. It will trigger a cache update for the given
+               revisions and notify components about the change.
+               """,
+               self._complete_repos, self._do_changeset_modified)
+        yield ('repository list', '',
+               'List source repositories',
+               None, self._do_list)
+        yield ('repository resync', '<repos> [rev]',
+               """Re-synchronize trac with repositories
                
                When [rev] is specified, only that revision is synchronized.
                Otherwise, the complete revision history is synchronized. Note
                that this operation can take a long time to complete.
+               If synchronization gets interrupted, it can be resumed later
+               using the `sync` command.
+               
+               To synchronize all repositories, specify "*" as the repository.
                """,
-               None, self._do_resync)
+               self._complete_repos, self._do_resync)
+        yield ('repository sync', '<repos> [rev]',
+               """Resume synchronization of repositories
+               
+               Similar to `resync`, but doesn't clear the already synchronized
+               changesets. Useful for resuming an interrupted `resync`.
+               
+               To synchronize all repositories, specify "*" as the repository.
+               """,
+               self._complete_repos, self._do_sync)
     
-    def _do_resync(self, rev=None):
-        if rev:
-            self.env.get_repository().sync_changeset(rev)
-            printout(_('%(rev)s resynced.', rev=rev))
-            return
-        from trac.versioncontrol.cache import CACHE_METADATA_KEYS
-        printout(_('Resyncing repository history... '))
+    def get_reponames(self):
+        rm = RepositoryManager(self.env)
+        return [reponame or _('(default)') for reponame
+                in rm.get_all_repositories()]
+    
+    def _complete_repos(self, args):
+        if len(args) == 1:
+            return self.get_reponames()
+    
+    def _do_changeset_added(self, reponame, *revs):
+        if is_default(reponame):
+            reponame = ''
+        rm = RepositoryManager(self.env)
+        rm.notify('changeset_added', reponame, revs)
+    
+    def _do_changeset_modified(self, reponame, *revs):
+        if is_default(reponame):
+            reponame = ''
+        rm = RepositoryManager(self.env)
+        rm.notify('changeset_modified', reponame, revs)
+    
+    def _do_list(self):
+        rm = RepositoryManager(self.env)
+        values = []
+        for (reponame, info) in sorted(rm.get_all_repositories().iteritems()):
+            alias = ''
+            if 'alias' in info:
+                alias = info['alias'] or _('(default)')
+            values.append((reponame or _('(default)'), info.get('type', ''),
+                           alias, info.get('dir', '')))
+        print_table(values, [_('Name'), _('Type'), _('Alias'), _('Directory')])
+    
+    def _sync(self, reponame, rev, clean):
+        rm = RepositoryManager(self.env)
+        if reponame == '*':
+            if rev is not None:
+                raise TracError(_('Cannot synchronize a single revision '
+                                  'on multiple repositories'))
+            repositories = rm.get_real_repositories()
+        else:
+            if is_default(reponame):
+                reponame = ''
+            repos = rm.get_repository(reponame)
+            if repos is None:
+                raise TracError(_("Unknown repository '%(reponame)s'",
+                                  reponame=reponame or _('(default)')))
+            if rev is not None:
+                repos.sync_changeset(rev)
+                printout(_('%(rev)s resynced on %(reponame)s.', rev=rev,
+                           reponame=repos.reponame or _('(default)')))
+                return
+            repositories = [repos]
+        
         db = self.env.get_db_cnx()
         cursor = db.cursor()
-        cursor.execute("DELETE FROM revision")
-        cursor.execute("DELETE FROM node_change")
-        cursor.executemany("DELETE FROM system WHERE name=%s",
-                           [(k,) for k in CACHE_METADATA_KEYS])
-        cursor.executemany("INSERT INTO system (name, value) VALUES (%s, %s)",
-                           [(k, '') for k in CACHE_METADATA_KEYS])
-        db.commit()
-        self.env.get_repository().sync(self._resync_feedback)
-        cursor.execute("SELECT count(rev) FROM revision")
-        for cnt, in cursor:
-            printout(ngettext('%(num)s revision cached.',
-                              '%(num)s revisions cached.', num=cnt))
+        for repos in sorted(repositories, key=lambda r: r.reponame):
+            printout(_('Resyncing repository history for %(reponame)s... ',
+                       reponame=repos.reponame or _('(default)')))
+            repos.sync(self._sync_feedback, clean=clean)
+            cursor.execute("SELECT count(rev) FROM revision WHERE repos=%s",
+                           (repos.id,))
+            for cnt, in cursor:
+                printout(ngettext('%(num)s revision cached.',
+                                  '%(num)s revisions cached.', num=cnt))
         printout(_('Done.'))
 
-    def _resync_feedback(self, rev):
+    def _sync_feedback(self, rev):
         sys.stdout.write(' [%s]\r' % rev)
         sys.stdout.flush()
+
+    def _do_resync(self, reponame, rev=None):
+        self._sync(reponame, rev, clean=True)
+
+    def _do_sync(self, reponame, rev=None):
+        self._sync(reponame, rev, clean=False)
+
+    # IAdminPanelProvider methods
+
+    def get_admin_panels(self, req):
+        if 'TICKET_ADMIN' in req.perm:
+            yield ('versioncontrol', 'Version Control', 'repository',
+                   _('Repositories'))
+    
+    def render_admin_panel(self, req, category, page, path_info):
+        req.perm.require('TICKET_ADMIN')
+        
+        # Retrieve info for all repositories
+        rm = RepositoryManager(self.env)
+        all_repos = rm.get_all_repositories()
+        db_provider = self.env[DbRepositoryProvider]
+        
+        if path_info:
+            # Detail view
+            reponame = not is_default(path_info) and path_info or ''
+            info = all_repos.get(reponame)
+            if info is None:
+                raise TracError(_('Repository %(name)s does not exist.',
+                                  name=path_info))
+            if req.method == 'POST':
+                if req.args.get('cancel'):
+                    req.redirect(req.href.admin(category, page))
+                
+                elif db_provider and req.args.get('save'):
+                    # Modify repository
+                    changes = {}
+                    for field in db_provider.repository_attrs:
+                        value = normalize_whitespace(req.args.get(field))
+                        if (value is not None or field == 'hidden') \
+                                and value != info.get(field):
+                            changes[field] = value
+                    if changes:
+                        db_provider.modify_repository(reponame, changes)
+                        add_notice(req, _('Your changes have been saved.'))
+                    name = req.args.get('name')
+                    if 'dir' in changes:
+                        msg = _('You should now run "trac-admin $ENV '
+                                'repository resync %(name)s" to synchronize '
+                                'Trac with the repository.', name=name)
+                        add_notice(req, msg)
+                    elif 'type' in changes:
+                        msg = _('You may have to run "trac-admin $ENV '
+                                'repository resync %(name)s" to synchronize '
+                                'Trac with the repository.', name=name)
+                        add_notice(req, msg)
+                    if name and name != path_info and not 'alias' in info:
+                        msg = _('You will need to update your post-commit '
+                                'hook to call "trac-admin $ENV changeset '
+                                'added" with the new repository name.')
+                        add_notice(req, msg)
+                    req.redirect(req.href.admin(category, page))
+            
+            Chrome(self.env).add_wiki_toolbars(req)
+            data = {'view': 'detail', 'reponame': reponame}
+        
+        else:
+            # List view
+            if req.method == 'POST':
+                # Add a repository
+                if db_provider and req.args.get('add_repos'):
+                    name = req.args.get('name')
+                    type_ = req.args.get('type')
+                    dir = req.args.get('dir')
+                    if name is not None and type_ is not None and dir:
+                        # Avoid errors when copy/pasting paths
+                        dir = normalize_whitespace(dir)
+                        db_provider.add_repository(name, dir, type_)
+                        add_notice(req, _('The repository "%(name)s" has been '
+                                          'added.', name=name))
+                        msg = _('You should now run "trac-admin $ENV '
+                                'repository resync %(name)s" to synchronize '
+                                'Trac with the repository.',
+                                name=name or _('(default)'))
+                        add_notice(req, msg)
+                        msg = _('You should also set up a post-commit hook '
+                                'on the repository to call "trac-admin $ENV '
+                                'changeset added %(name)s $REV" for each '
+                                'committed changeset.', name=name)
+                        add_notice(req, msg)
+                        req.redirect(req.href.admin(category, page))
+                    add_warning(req, _('Missing arguments to add a '
+                                       'repository.'))
+                
+                # Add a repository alias
+                elif db_provider and req.args.get('add_alias'):
+                    name = req.args.get('name')
+                    alias = req.args.get('alias')
+                    if name is not None and alias is not None:
+                        db_provider.add_alias(name, alias)</