Merged Trac 1.0.2 and resolved merge conflicts. Many tests are failing.


git-svn-id: https://svn.apache.org/repos/asf/bloodhound/branches/trac-1.0.2-integration@1639823 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/trac/.gitignore b/trac/.gitignore
index d9ee2e9..da0eea1 100644
--- a/trac/.gitignore
+++ b/trac/.gitignore
@@ -19,6 +19,7 @@
 doc/.build
 *.mo
 trac/htdocs/js/messages/*.js
+.idea
 .project
 .pydevproject
 .settings
diff --git a/trac/.hgignore b/trac/.hgignore
index cd2289c..5bccd01 100644
--- a/trac/.hgignore
+++ b/trac/.hgignore
@@ -20,3 +20,7 @@
 doc/.build
 *.mo
 trac/htdocs/js/messages/*.js
+.idea
+.project
+.pydevproject
+.settings
diff --git a/trac/.travis.yml b/trac/.travis.yml
new file mode 100644
index 0000000..d4dc2d6
--- /dev/null
+++ b/trac/.travis.yml
@@ -0,0 +1,26 @@
+language: python
+python:
+  - "2.6"
+  - "2.7_with_system_site_packages"
+env:
+  - "TRAC_TEST_DB_URI="
+  - "TRAC_TEST_DB_URI=sqlite:test.db"
+  - "TRAC_TEST_DB_URI=postgres://tracuser:password@localhost/trac?schema=tractest"
+  - "TRAC_TEST_DB_URI=mysql://tracuser:password@localhost/trac"
+before_install:
+  - sudo apt-get update -qq
+  - sudo apt-get install -qq python-subversion
+  - psql -U postgres -c "CREATE USER tracuser NOSUPERUSER NOCREATEDB CREATEROLE PASSWORD 'password';"
+  - psql -U postgres -c "CREATE DATABASE trac OWNER tracuser;"
+  - mysql -u root -e "CREATE DATABASE trac DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;"
+  - mysql -u root -e "CREATE USER tracuser@localhost IDENTIFIED BY 'password';"
+  - mysql -u root -e "GRANT ALL ON trac.* TO tracuser@localhost; FLUSH PRIVILEGES;"
+install:
+  - pip install -q Genshi==0.7 Babel configobj Pygments docutils lxml pytz twill==0.9.1 psycopg2 MySQL-python
+  - echo ".uri = $TRAC_TEST_DB_URI" >Makefile.cfg
+script:
+  - make Trac.egg-info unit-test functional-test
+notifications:
+  email:
+    recipients:
+      - trac-builds@googlegroups.com
diff --git a/trac/AUTHORS b/trac/AUTHORS
index 1dbaf42..88de3ec 100644
--- a/trac/AUTHORS
+++ b/trac/AUTHORS
@@ -16,5 +16,6 @@
  * Remy Blank <remy.blank@pobox.com>
  * Jun Omae <jun66j5@gmail.com>
  * Peter Suter <petsuter@gmail.com>
+ * Ryan Ollos <ryan.j.ollos@gmail.com>
 
 See also THANKS for people who have contributed to the project.
diff --git a/trac/ChangeLog b/trac/ChangeLog
index 35aba61..865f5bb 100644
--- a/trac/ChangeLog
+++ b/trac/ChangeLog
@@ -1,3 +1,34 @@
+Trac 1.0.2 (TBD)
+
+Trac 1.0.2 is a maintenance release containing numerous fixes and minor
+enhancements. The following are a few of the highlights:
+
+ - Subversion keywords are expanded and EOL substitutions made when viewing
+   a file in the repository browser and when downloading a file (#717). 
+ - Notification email is sent to the old owner when a ticket is reassigned
+   (#2311).
+ - Ticket change history is updated when renaming and deleting a milestone,
+   and when retargeting tickets to another milestone (#4582, #5658).
+ - Numerous fixes for the Authz permissions policy in the browser/repository
+   (#10961, #11646), wiki (#8976, #11067), admin (#11069) and report (#11176)
+   realms.
+ - Multiple forms submits are disallowed (#10138).
+ - `ConfigurationError` is raised if any of the `permission_policies` can't
+   be loaded, preventing possible information leakage due to internal and
+   installation errors (#10285).
+ - Wiki toolbars can be disabled through a configuration setting (#10837)
+ - The number of entries in a table is shown next to heading on applicable
+   admin pages (#11027).
+ - //Cancel// buttons are consistently located on all pages (#11076).
+ - Focus is placed on a text element when an edit page is loaded (#11084).
+ - The //Edit conflict// and //Merge// warning messages are always visible
+   in side-by-side edit mode (#11102).
+ - Improvements to the layout of the Report (#11106, #11664) and Ticket pages
+   (#11471).
+ - Genshi 0.7 compatibility (#11218).
+ - Numerous minor fixes for Git repository support.
+ - … and more than a hundred more fixes!
+
 Trac 1.0.1 (February 1, 2013)
 http://svn.edgewall.org/repos/trac/tags/trac-1.0.1
 
@@ -57,6 +88,27 @@
 
 -----------------------------------------------------------------------------
 
+Trac 0.12.6 (TBD)
+
+Trac 0.12.6 is a maintenance release that contains fixes for a few issues:
+ - Subversion blame would fail for a path with URL-encoded characters (#10386),
+   a lower-case drive letter on Windows (#10514), or a non-ascii filename with
+   Subversion 1.7 (#11167).
+ - Improved performance rendering `svn:mergeinfo` properties in browser view
+   (#8459) and changeset view (#11219).
+ - Query with many custom fields would fail (#11140).
+ - Zip archive had a timestamp with no timezone information (#11162).
+ - Failure or incorrect ranges rendering log TracLinks (#11308, #11346).
+ - Textareas in ticket view did not wrap correctly in IE 11 (#11376).
+ - Emails were not being obfuscated in owner field on CSV export from ticket
+   and query pages (#11594).
+ - Locale data was not being included in egg in Distribute 0.6.29 and later
+   (#11640).
+ - Deleting a milestone would not delete its attachments (#11672).
+ - Added support for Babel 1.0 and later (#11258, #11345).
+ - Added support for `ConfigObj` 5.0 and later (#11498).
+ - … and dozens more fixes!
+
 Trac 0.12.5 (January 15, 2013)
 http://svn.edgewall.org/repos/trac/tags/trac-0.12.5
 
diff --git a/trac/Makefile b/trac/Makefile
index 65cda40..d49e3fa 100644
--- a/trac/Makefile
+++ b/trac/Makefile
@@ -42,7 +42,7 @@
 
  ---------------- Standalone test server
 
-  server              start tracd
+  [start-]server      start tracd
 
   [port=...]          variable for selecting the port
   [auth=...]          variable for specifying authentication
@@ -95,10 +95,17 @@
   [epydocopts=...]    variable containing extra options for Epydoc
   [dotpath=/.../dot]  path to Graphviz dot program (not used yet)
 
+ ---------------- Miscellaneous
+
+  start-admin         start trac-admin (on `env')
+  start-python        start the Python interpreter
+
+  [adminopts=...]     variable containing extra options for trac-admin
+
 endef
 export HELP
 
-# ` (keep emacs font-lock happy)
+# ' (keep emacs font-lock happy)
 
 define HELP_CFG
  It looks like you don't have a Makefile.cfg file yet.
@@ -154,6 +161,11 @@
 
 # ----------------------------------------------------------------------------
 #
+# Copy Makefile.cfg.sample to Makefile.cfg and adapt to your local
+# environment, no customizations to the present Makefile should be
+# necessary.
+#
+#
 -include Makefile.cfg
 #
 # ----------------------------------------------------------------------------
@@ -220,7 +232,7 @@
 else
 compile:
 	python setup.py $(foreach catalog,$(catalogs), \
-	    compile_catalog$(_catalog))
+	    compile_catalog$(_catalog)) generate_messages_js
 endif
 
 
@@ -450,9 +462,11 @@
  $(if $(wildcard $(env)/VERSION),$(env),-e $(env))
 endef
 
-.PHONY: server
+.PHONY: server start-server tracd start-tracd
 
-server: Trac.egg-info
+server tracd start-tracd: start-server
+
+start-server: Trac.egg-info
 ifdef env
 	python trac/web/standalone.py $(server-options)
 else
@@ -460,6 +474,26 @@
 endif
 
 
+.PHONY: trac-admin start-admin
+
+trac-admin: start-admin
+
+start-admin:
+ifneq "$(wildcard $(env)/VERSION)" ""
+	@python trac/admin/console.py $(env) $(adminopts)
+else
+	@echo "\`env' variable was not specified or doesn't point to one env."
+endif
+
+
+.PHONY: start-python
+
+start-python:
+	@python
+# (this doesn't seem to be much, but we're taking benefit of the
+# environment setup we're doing below)
+
+
 # ----------------------------------------------------------------------------
 #
 # Documentation related tasks
diff --git a/trac/Makefile.cfg.sample b/trac/Makefile.cfg.sample
index 96b5e74..a7f0a17 100644
--- a/trac/Makefile.cfg.sample
+++ b/trac/Makefile.cfg.sample
@@ -1,10 +1,13 @@
 # -*- Makefile -*- configuration file sample
 #
-# Adapt to your local setting and copy to Makefile.cfg
+# Copy to Makefile.cfg and adapt to your local environment.
 #
 # ----------------------------------------------------------------------------
-# Python Installations (select with `python=` on the `make` command line)
+# Switching between different Python installations 
+#
+# (one of them can be selected with `make python=<key>`)
 
+# python.<key> = <path to Python installation>
 python.23 =
 python.24 =
 python.25 = C:/Dev/Python254
@@ -12,23 +15,48 @@
 python.26 = C:/Dev/Python261
 python.27 =
 
-# default Python version (if not defined, pick the one from the path)
-.python =
+# And also:
+
+# pythonpath.<key> = <extension to the PYTHONPATH for that installation>
+# path.<key> = <extension to the PATH for that installation>
+
+# (both very convenient for specifying non-default Subversion bindings,
+# for example)
+
 
 # ----------------------------------------------------------------------------
-# Database Backends (select with `db=` on the `make` command line)
+# Switching between different database backends
+#
+#  (one of them can be selected with `make db=<backend>`)
 
 # db URIs
+# <backend>.uri = <db:params>
 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)
+# db backend to use if when `db=<backend>` parameter was given to `make`
 .uri =
+# (if left undefined, use in-memory sqlite)
 
-# default Python versions to use when `db` is specified
+# Python installation to use when `db=<backend>` is specified but `python=<key>`
+# is not.
+#
+# <backend>.python = <key>  where <key> corresponds to the python.<key> vars
 mysql.python = 25
 postgres.python = 26
+.python =
+# (if db is left empty, .python will get used to select the Python
+# installation; if left undefined, the 'python' command will be used
+# instead of a fully qualified pathname)
+
+# For example, if you only have the MySqlDB Python bindings available
+# for your Python 2.7.4 installation, specify something like:
+# mysql.python = 27
+#
+# given that you also have:
+# python.27 = <path to my 2.7.4 install containing the MySqlDB bindings...>
+
 
 # ----------------------------------------------------------------------------
 # Settings for the test server
@@ -42,7 +70,7 @@
 dotpath = /usr/local/bin/dot
 
 # ----------------------------------------------------------------------------
-# Custom rules
+# Custom rules - let your imagination go wild ;-)
 
 .PHONY: bigtest
 
diff --git a/trac/THANKS b/trac/THANKS
index 97391df..655034e 100644
--- a/trac/THANKS
+++ b/trac/THANKS
@@ -88,7 +88,7 @@
  * Jennifer Murtell               jen@jmurtell.com
  * Jacob Norda                    jacobnorda@gmail.com
  * Dirkjan Ochtman                dirkjan@ochtman.nl
- * Ryan J Ollos                   ryano@physiosonics.com
+ * Ryan J Ollos                   ryan.j.ollos@gmail.com
  * Jun Omae                       jun66j5@gmail.com
  * Itamar Ostricher               itamarost@gmail.com
  * Bas van Oostveen               v.oostveen@gmail.com
diff --git a/trac/contrib/bugzilla2trac.py b/trac/contrib/bugzilla2trac.py
index 3993f75..ce625df 100644
--- a/trac/contrib/bugzilla2trac.py
+++ b/trac/contrib/bugzilla2trac.py
@@ -1,4 +1,23 @@
 #!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2013 Edgewall Software
+# Copyright (C) 2004 Dmitry Yusupov <dmitry_yus@yahoo.com>
+# Copyright (C) 2004 Mark Rowe <mrowe@bluewire.net.nz>
+# Copyright (C) 2005 Bill Soudan <bill@soudan.net>
+# Copyright (C) 2005 Florent Guillaume <fg@nuxeo.com>
+# Copyright (C) 2005 Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>
+# Copyright (C) 2010 Jeff Moreland <hou5e@hotmail.com>
+#
+# 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/.
 
 """
 Import a Bugzilla items into a Trac database.
@@ -9,17 +28,7 @@
            or PostGreSQL 8.4 from http://www.postgresql.org/
            or SQLite 3 from http://www.sqlite.org/
 
-Thanks:    Mark Rowe <mrowe@bluewire.net.nz>
-            for original TracDatabase class
-
-Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com>
-
-Many enhancements, Bill Soudan <bill@soudan.net>
-Other enhancements, Florent Guillaume <fg@nuxeo.com>
-Reworked, Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>
-Jeff Moreland <hou5e@hotmail.com>
-
-$Id: bugzilla2trac.py 11490 2013-01-13 15:18:06Z rblank $
+$Id: bugzilla2trac.py 12500 2014-02-12 20:54:59Z rjollos $
 """
 
 from __future__ import with_statement
@@ -230,7 +239,7 @@
 # mapping, just return string, otherwise return value
 class FieldTranslator(dict):
     def __getitem__(self, item):
-        if not dict.has_key(self, item):
+        if item not in self:
             return item
 
         return dict.__getitem__(self, item)
@@ -243,8 +252,8 @@
         self.loginNameCache = {}
         self.fieldNameCache = {}
         from trac.db.api import DatabaseManager
-	self.using_postgres = \
-                DatabaseManager(self.env).connection_uri.startswith("postgres:")
+        self.using_postgres = \
+            DatabaseManager(self.env).connection_uri.startswith("postgres:")
 
     def hasTickets(self):
         return int(self.env.db_query("SELECT count(*) FROM ticket")[0][0] > 0)
@@ -335,7 +344,7 @@
             if BUG_NO_RE.search(desc):
                 desc = re.sub(BUG_NO_RE, BUG_NO_REPL, desc)
 
-        if PRIORITIES_MAP.has_key(priority):
+        if priority in PRIORITIES_MAP:
             priority = PRIORITIES_MAP[priority]
 
         print "  inserting ticket %s -- %s" % (id, summary)
@@ -377,7 +386,7 @@
         comment = value
 
         if PREFORMAT_COMMENTS:
-          comment = '{{{\n%s\n}}}' % comment
+            comment = '{{{\n%s\n}}}' % comment
 
         if REPLACE_BUG_NO:
             if BUG_NO_RE.search(comment):
@@ -393,15 +402,15 @@
     def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
 
         if field == "owner":
-            if LOGIN_MAP.has_key(oldvalue):
+            if oldvalue in LOGIN_MAP:
                 oldvalue = LOGIN_MAP[oldvalue]
-            if LOGIN_MAP.has_key(newvalue):
+            if newvalue in LOGIN_MAP:
                 newvalue = LOGIN_MAP[newvalue]
 
         if field == "priority":
-            if PRIORITIES_MAP.has_key(oldvalue.lower()):
+            if oldvalue.lower() in PRIORITIES_MAP:
                 oldvalue = PRIORITIES_MAP[oldvalue.lower()]
-            if PRIORITIES_MAP.has_key(newvalue.lower()):
+            if newvalue.lower() in PRIORITIES_MAP:
                 newvalue = PRIORITIES_MAP[newvalue.lower()]
 
         # Doesn't make sense if we go from highest -> highest, for example.
@@ -720,7 +729,7 @@
                     ignore = True
 
             if ignore:
-                    continue
+                continue
 
             trac.addTicketComment(ticket=bugid,
                 time = desc['bug_when'],
@@ -824,19 +833,19 @@
 
             # Bugzilla splits large summary changes into two records.
             for oldChange in ticketChanges:
-              if (field_name == "summary"
-                  and oldChange['field'] == ticketChange['field']
-                  and oldChange['time'] == ticketChange['time']
-                  and oldChange['author'] == ticketChange['author']):
-                  oldChange['oldvalue'] += " " + ticketChange['oldvalue']
-                  oldChange['newvalue'] += " " + ticketChange['newvalue']
-                  break
-              # cc and attachments.isobsolete sometime appear
-              # in different activities with same time
-              if ((field_name == "cc" or field_name == "attachments.isobsolete") \
-                  and oldChange['time'] == ticketChange['time']):
-                  oldChange['newvalue'] += ", " + ticketChange['newvalue']
-                  break
+                if (field_name == "summary"
+                    and oldChange['field'] == ticketChange['field']
+                    and oldChange['time'] == ticketChange['time']
+                    and oldChange['author'] == ticketChange['author']):
+                    oldChange['oldvalue'] += " " + ticketChange['oldvalue']
+                    oldChange['newvalue'] += " " + ticketChange['newvalue']
+                    break
+                # cc and attachments.isobsolete sometime appear
+                # in different activities with same time
+                if ((field_name == "cc" or field_name == "attachments.isobsolete") \
+                    and oldChange['time'] == ticketChange['time']):
+                    oldChange['newvalue'] += ", " + ticketChange['newvalue']
+                    break
             else:
                 ticketChanges.append (ticketChange)
 
@@ -895,7 +904,7 @@
         users = ()
     htpasswd = file("htpasswd", 'w')
     for user in users:
-        if LOGIN_MAP.has_key(user['login_name']):
+        if user['login_name'] in LOGIN_MAP:
             login = LOGIN_MAP[user['login_name']]
         else:
             login = user['login_name']
@@ -939,36 +948,36 @@
     global BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN
     global SEVERITIES, PRIORITIES, PRIORITIES_MAP
     if len (sys.argv) > 1:
-    	if sys.argv[1] in ['--help','help'] or len(sys.argv) < 4:
-    	    usage()
-    	iter = 1
-    	while iter < len(sys.argv):
-    	    if sys.argv[iter] in ['--db'] and iter+1 < len(sys.argv):
-    	        BZ_DB = sys.argv[iter+1]
-    	        iter = iter + 1
-    	    elif sys.argv[iter] in ['-h', '--host'] and iter+1 < len(sys.argv):
-    	        BZ_HOST = sys.argv[iter+1]
-    	        iter = iter + 1
-    	    elif sys.argv[iter] in ['-u', '--user'] and iter+1 < len(sys.argv):
-    	        BZ_USER = sys.argv[iter+1]
-    	        iter = iter + 1
-    	    elif sys.argv[iter] in ['-p', '--passwd'] and iter+1 < len(sys.argv):
-    	        BZ_PASSWORD = sys.argv[iter+1]
-    	        iter = iter + 1
-    	    elif sys.argv[iter] in ['--tracenv'] and iter+1 < len(sys.argv):
-    	        TRAC_ENV = sys.argv[iter+1]
-    	        iter = iter + 1
-    	    elif sys.argv[iter] in ['-c', '--clean']:
-    	        TRAC_CLEAN = 1
+        if sys.argv[1] in ['--help','help'] or len(sys.argv) < 4:
+            usage()
+        iter = 1
+        while iter < len(sys.argv):
+            if sys.argv[iter] in ['--db'] and iter+1 < len(sys.argv):
+                BZ_DB = sys.argv[iter+1]
+                iter = iter + 1
+            elif sys.argv[iter] in ['-h', '--host'] and iter+1 < len(sys.argv):
+                BZ_HOST = sys.argv[iter+1]
+                iter = iter + 1
+            elif sys.argv[iter] in ['-u', '--user'] and iter+1 < len(sys.argv):
+                BZ_USER = sys.argv[iter+1]
+                iter = iter + 1
+            elif sys.argv[iter] in ['-p', '--passwd'] and iter+1 < len(sys.argv):
+                BZ_PASSWORD = sys.argv[iter+1]
+                iter = iter + 1
+            elif sys.argv[iter] in ['--tracenv'] and iter+1 < len(sys.argv):
+                TRAC_ENV = sys.argv[iter+1]
+                iter = iter + 1
+            elif sys.argv[iter] in ['-c', '--clean']:
+                TRAC_CLEAN = 1
             elif sys.argv[iter] in ['-n', '--noseverities']:
                 # treat Bugzilla severites as Trac priorities
                 PRIORITIES = SEVERITIES
                 SEVERITIES = []
                 PRIORITIES_MAP = {}
-    	    else:
-    	        print "Error: unknown parameter: " + sys.argv[iter]
-    	        sys.exit(0)
-    	    iter = iter + 1
+            else:
+                print "Error: unknown parameter: " + sys.argv[iter]
+                sys.exit(0)
+            iter = iter + 1
 
     convert(BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN)
 
diff --git a/trac/contrib/cgi-bin/trac.cgi b/trac/contrib/cgi-bin/trac.cgi
index 6b27b43..44e3ab4 100755
--- a/trac/contrib/cgi-bin/trac.cgi
+++ b/trac/contrib/cgi-bin/trac.cgi
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2003-2009 Edgewall Software
+# Copyright (C) 2003-2013 Edgewall Software
 # Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
 # All rights reserved.
 #
diff --git a/trac/contrib/cgi-bin/trac.fcgi b/trac/contrib/cgi-bin/trac.fcgi
index 23c28e2..bdd81d9 100755
--- a/trac/contrib/cgi-bin/trac.fcgi
+++ b/trac/contrib/cgi-bin/trac.fcgi
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2003-2009 Edgewall Software
+# Copyright (C) 2003-2013 Edgewall Software
 # Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
 # All rights reserved.
 #
diff --git a/trac/contrib/checkwiki.py b/trac/contrib/checkwiki.py
index 0b8f94b..626d471 100755
--- a/trac/contrib/checkwiki.py
+++ b/trac/contrib/checkwiki.py
@@ -1,4 +1,17 @@
 #!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2013 Edgewall Software
+# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
+# 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/.
 #
 # Check/update default wiki pages from the Trac project website.
 #
@@ -145,4 +158,3 @@
         data[p] = get_page(prefix, p)
     if check:
         check_links(data)
-
diff --git a/trac/contrib/emailfilter.py b/trac/contrib/emailfilter.py
index 1f05d2c..0e3e4e2 100644
--- a/trac/contrib/emailfilter.py
+++ b/trac/contrib/emailfilter.py
@@ -1,4 +1,18 @@
 #!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2013 Edgewall Software
+# Copyright (C) 2005 Daniel Lundin <daniel@edgewall.com>
+# 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/.
+
 """
 emailfilter.py -- Email tickets to Trac.
 
diff --git a/trac/contrib/htdigest.py b/trac/contrib/htdigest.py
index 0fbc7dc..9fe36ce 100755
--- a/trac/contrib/htdigest.py
+++ b/trac/contrib/htdigest.py
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2006-2009 Edgewall Software
+# Copyright (C) 2006-2013 Edgewall Software
 # Copyright (C) 2006 Matthew Good <matt@matt-good.net>
 # All rights reserved.
 #
diff --git a/trac/contrib/htpasswd.py b/trac/contrib/htpasswd.py
index 3853f2c..852902a 100755
--- a/trac/contrib/htpasswd.py
+++ b/trac/contrib/htpasswd.py
@@ -1,12 +1,27 @@
 #!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-2013 Edgewall Software
+# Copyright (C) 2008 Eli Carter
+# 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/.
+
 """Replacement for htpasswd"""
-# Original author: Eli Carter
 
 import os
 import sys
 import random
 from optparse import OptionParser
 
+from trac.util.compat import wait_for_file_mtime_change
+
 # We need a crypt module, but Windows doesn't have one by default.  Try to find
 # one, and tell the user if we can't.
 try:
@@ -51,6 +66,7 @@
 
     def save(self):
         """Write the htpasswd file to disk"""
+        wait_for_file_mtime_change(self.filename)
         open(self.filename, 'w').writelines(["%s:%s\n" % (entry[0], entry[1])
                                              for entry in self.entries])
 
@@ -71,8 +87,9 @@
 
 
 def main():
-    """%prog [-c] -b filename username password
-    Create or update an htpasswd file"""
+    """
+        %prog -b[c] filename username password
+        %prog -D filename username"""
     # For now, we only care about the use cases that affect tests/functional.py
     parser = OptionParser(usage=main.__doc__)
     parser.add_option('-b', action='store_true', dest='batch', default=False,
@@ -93,8 +110,8 @@
         sys.stderr.write(parser.get_usage())
         sys.exit(1)
 
-    if not options.batch:
-        syntax_error("Only batch mode is supported\n")
+    if not (options.batch or options.delete_user):
+        syntax_error("Only batch and delete modes are supported\n")
 
     # Non-option arguments
     if len(args) < 2:
diff --git a/trac/contrib/l10n_diff_index.py b/trac/contrib/l10n_diff_index.py
index c653e76..75507c8 100644
--- a/trac/contrib/l10n_diff_index.py
+++ b/trac/contrib/l10n_diff_index.py
@@ -1,5 +1,16 @@
+# -*- coding: utf-8 -*-
+#
 # Copyright (C) 2013 Edgewall Software
-# This file is distributed under the same license as the Trac project.
+# Copyright (C) 2013 Christian Boos <cboos@edgewall.org>
+# 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/.
 
 """
 
diff --git a/trac/contrib/l10n_reset_en_GB.py b/trac/contrib/l10n_reset_en_GB.py
index 8c51538..00ff166 100644
--- a/trac/contrib/l10n_reset_en_GB.py
+++ b/trac/contrib/l10n_reset_en_GB.py
@@ -1,5 +1,16 @@
+# -*- coding: utf-8 -*-
+#
 # Copyright (C) 2013 Edgewall Software
-# This file is distributed under the same license as the Trac project.
+# Copyright (C) 2013 Christian Boos <cboos@edgewall.org>
+# 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/.
 
 """
 
diff --git a/trac/contrib/l10n_revert_lineno_conflicts.py b/trac/contrib/l10n_revert_lineno_conflicts.py
index 6250339..cdec753 100644
--- a/trac/contrib/l10n_revert_lineno_conflicts.py
+++ b/trac/contrib/l10n_revert_lineno_conflicts.py
@@ -1,5 +1,16 @@
+# -*- coding: utf-8 -*-
+#
 # Copyright (C) 2013 Edgewall Software
-# This file is distributed under the same license as the Trac project.
+# Copyright (C) 2013 Christian Boos <cboos@edgewall.org>
+# 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/.
 
 """
 
@@ -17,15 +28,32 @@
 
 ignore_lineno_re = re.compile(r'''
           <<<< .* \n
-    ( (?: [^=] .* \n )+)   # \1 == "working copy"
+    ( (?: [^=] .* \n )+ )   # \1 == "working copy"
           ==== .* \n
-    ( (?: \#   .* \n )+)   # \2 == comment only for "theirs"
+    ( (?: \#   .* \n )+ )   # \2 == comment only for "theirs"
           >>>> .* \n
     ''', re.MULTILINE | re.VERBOSE)
 
+HEADERS = '''
+Project-Id-Version Report-Msgid-Bugs-To POT-Creation-Date PO-Revision-Date
+Last-Translator Language-Team Plural-Forms MIME-Version Content-Type
+Content-Transfer-Encoding Generated-By
+'''.split()
+
+po_headers_re = re.compile(r'''
+          <<<< .* \n
+    ( (?: "(?:%(header)s): \s [^"]+" \n )+ )  # \1 == "working copy"
+          ==== .* \n
+    ( (?: "(?:%(header)s): \s [^"]+" \n )+ )  # \2 == another date for "theirs"
+          >>>> .* \n
+    ''' % dict(header='|'.join(HEADERS)), re. MULTILINE | re.VERBOSE)
+
+
 def sanitize_file(path):
-    with file(path, 'rb+') as f:
+    with file(path, 'r+') as f:
         sanitized, nsub = ignore_lineno_re.subn(r'\1', f.read())
+        sanitized, nsub2 = po_headers_re.subn(r'\1', sanitized)
+        nsub += nsub2
         if nsub:
             print("reverted %d ignorable changes in %s" % (nsub, path))
             f.seek(0)
diff --git a/trac/contrib/migrateticketmodel.py b/trac/contrib/migrateticketmodel.py
index e44bc6e..e5e653d 100644
--- a/trac/contrib/migrateticketmodel.py
+++ b/trac/contrib/migrateticketmodel.py
@@ -1,5 +1,18 @@
 #!/usr/bin/env python
+# -*- coding: utf-8 -*-
 #
+# Copyright (C) 2005-2013 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# 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/.
+
 # This script completely migrates a <= 0.8.x Trac environment to use the new
 # default ticket model introduced in Trac 0.9.
 #
diff --git a/trac/contrib/sourceforge2trac.py b/trac/contrib/sourceforge2trac.py
index e3e1566..ec0f184 100644
--- a/trac/contrib/sourceforge2trac.py
+++ b/trac/contrib/sourceforge2trac.py
@@ -1,3 +1,19 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2013 Edgewall Software
+# Copyright (C) 2004 Dmitry Yusupov <dmitry_yus@yahoo.com>
+# Copyright (C) 2004 Mark Rowe <mrowe@bluewire.net.nz>
+# Copyright (C) 2010 Anatoly Techtonik <techtonik@php.net>
+# 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/.
+
 """
 Import a Sourceforge project's tracker items into a Trac database.
 
@@ -11,15 +27,6 @@
 of the project admin section. Substitute XXXXX with project id:
 https://sourceforge.net/export/xml_export2.php?group_id=XXXXX
 
-
-Initial version for Trac 0.7 and old artiface SF export format is
-Copyright 2004, Mark Rowe <mrowe@bluewire.net.nz>
-
-Version for Trac 0.11 and SF XML2 export format, completely rewritten
-except TracDatabase class is
-Copyright 2010, anatoly techtonik <techtonik@php.net>
-HGID: 92fd15e8398c
-
 $Id$
 
 
@@ -123,9 +130,9 @@
         for c in el:
             if len(c.getchildren()) == 0:
                 if c.text != None and len(c.text.strip()) != 0:
-                   self.__setattr__(c.tag, c.text)
+                    self.__setattr__(c.tag, c.text)
                 else:
-                   self.__setattr__(c.tag, [])
+                    self.__setattr__(c.tag, [])
             else: #if c.getchildren()[0].tag == c.tag[:-1]:
                 # c is a set of elements
                 self.__setattr__(c.tag, [FlatXML(x) for x in c.getchildren()])
@@ -552,137 +559,137 @@
     trac.setMilestoneList([])
 
     for tracker in project.trackers:
-      # id 100 means no component selected
-      component_lookup = dict(project.get_categories(noowner=True) +
-                              [("100", None)])
-      for t in tracker.tracker_items:
-        i = trac.addTicket(type=tracker.name,
-                           time=int(t.submit_date),
-                           changetime=int(t.submit_date),
-                           component=component_lookup[t.category_id],
-                           priority=t.priority,
-                           owner=t.assignee \
-                                   if t.assignee not in user_map \
-                                   else user_map[t.assignee],
-                           reporter=t.submitter \
-                                   if t.submitter not in user_map \
-                                   else user_map[t.submitter],
-                           cc=None,
-                           # 100 means no group selected
-                           version=dict(project.groups +
-                                        [("100", None)])[t.group_id],
-                           milestone=None,
-                           status=dict(project.statuses)[t.status_id],
-                           resolution=dict(resolutions)[t.resolution_id] \
-                                   if hasattr(t, "resolution_id") else None,
-                           summary=t.summary,
-                           description=t.details,
-                           keywords='sf' + t.id)
+        # id 100 means no component selected
+        component_lookup = dict(project.get_categories(noowner=True) +
+                                [("100", None)])
+        for t in tracker.tracker_items:
+            i = trac.addTicket(type=tracker.name,
+                               time=int(t.submit_date),
+                               changetime=int(t.submit_date),
+                               component=component_lookup[t.category_id],
+                               priority=t.priority,
+                               owner=t.assignee \
+                                       if t.assignee not in user_map \
+                                       else user_map[t.assignee],
+                               reporter=t.submitter \
+                                       if t.submitter not in user_map \
+                                       else user_map[t.submitter],
+                               cc=None,
+                               # 100 means no group selected
+                               version=dict(project.groups +
+                                            [("100", None)])[t.group_id],
+                               milestone=None,
+                               status=dict(project.statuses)[t.status_id],
+                               resolution=dict(resolutions)[t.resolution_id] \
+                                       if hasattr(t, "resolution_id") else None,
+                               summary=t.summary,
+                               description=t.details,
+                               keywords='sf' + t.id)
 
-        print 'Imported %s as #%d' % (t.id, i)
+            print 'Imported %s as #%d' % (t.id, i)
 
-        if len(t.attachments):
-            attmsg = "SourceForge attachments:\n"
-            for a in t.attachments:
-                attmsg = attmsg + " * [%s %s] (%s) - added by '%s' %s [[BR]] "\
-                         % (a.url+t.id, a.filename, a.filesize+" bytes",
-                            user_map.get(a.submitter, a.submitter),
-                            time.strftime("%Y-%m-%d %H:%M:%S",
-                                          time.localtime(int(a.date))))
-                attmsg = attmsg + "''%s ''\n" % (a.description or '')
-                # empty description is as empty list
-            trac.addTicketComment(ticket=i,
-                                  time=time.strftime("%Y-%m-%d %H:%M:%S",
-                                          time.localtime(int(t.submit_date))),
-                                  author=None, value=attmsg)
-            print '    added information about %d attachments for #%d' % \
-                    (len(t.attachments), i)
+            if len(t.attachments):
+                attmsg = "SourceForge attachments:\n"
+                for a in t.attachments:
+                    attmsg = attmsg + " * [%s %s] (%s) - added by '%s' %s [[BR]] "\
+                             % (a.url+t.id, a.filename, a.filesize+" bytes",
+                                user_map.get(a.submitter, a.submitter),
+                                time.strftime("%Y-%m-%d %H:%M:%S",
+                                              time.localtime(int(a.date))))
+                    attmsg = attmsg + "''%s ''\n" % (a.description or '')
+                    # empty description is as empty list
+                trac.addTicketComment(ticket=i,
+                                      time=time.strftime("%Y-%m-%d %H:%M:%S",
+                                              time.localtime(int(t.submit_date))),
+                                      author=None, value=attmsg)
+                print '    added information about %d attachments for #%d' % \
+                        (len(t.attachments), i)
 
-        for msg in t.followups:
+            for msg in t.followups:
+                """
+                <followup>
+                <id>3280792</id>
+                <submitter>goblinhack</submitter>
+                <date>1231087739</date>
+                <details>done</details>
+                </followup>
+                """
+                trac.addTicketComment(ticket=i,
+                                      time=msg.date,
+                                      author=msg.submitter,
+                                      value=msg.details)
+            if t.followups:
+                print '    imported %d messages for #%d' % (len(t.followups), i)
+
+            # Import history
             """
-            <followup>
-            <id>3280792</id>
-            <submitter>goblinhack</submitter>
-            <date>1231087739</date>
-            <details>done</details>
-            </followup>
+            <history_entry>
+            <id>4452195</id>
+            <field_name>resolution_id</field_name>
+            <old_value>100</old_value>
+            <date>1176043865</date>
+            <updator>goblinhack</updator>
+            </history_entry>
             """
-            trac.addTicketComment(ticket=i,
-                                  time=msg.date,
-                                  author=msg.submitter,
-                                  value=msg.details)
-        if t.followups:
-            print '    imported %d messages for #%d' % (len(t.followups), i)
+            revision = t.__dict__.copy()
 
-        # Import history
-        """
-        <history_entry>
-        <id>4452195</id>
-        <field_name>resolution_id</field_name>
-        <old_value>100</old_value>
-        <date>1176043865</date>
-        <updator>goblinhack</updator>
-        </history_entry>
-        """
-        revision = t.__dict__.copy()
+            # iterate the history in reverse order and update ticket revision from
+            # current (last) to initial
+            changes = 0
+            for h in sorted(t.history_entries, reverse=True):
+                """
+                 Processed fields (field - notes):
+                IP         - no target field, just skip
+                summary
+                priority
+                close_date
+                assigned_to
 
-        # iterate the history in reverse order and update ticket revision from
-        # current (last) to initial
-        changes = 0
-        for h in sorted(t.history_entries, reverse=True):
-            """
-             Processed fields (field - notes):
-            IP         - no target field, just skip
-            summary
-            priority
-            close_date
-            assigned_to
+                 Fields not processed (field: explanation):
+                File Added - TODO
+                resolution_id - need to update used_resolutions
+                status_id
+                artifact_group_id
+                category_id
+                group_id
+                """
+                f = None
+                if h.field_name in ("IP",):
+                    changes += 1
+                    continue
+                elif h.field_name in ("summary", "priority"):
+                    f = h.field_name
+                    oldvalue = h.old_value
+                    newvalue = revision.get(h.field_name, None)
+                elif h.field_name == 'assigned_to':
+                    f = "owner"
+                    newvalue = revision['assignee']
+                    if h.old_value == '100': # was not assigned
+                        revision['assignee'] = None
+                        oldvalue = None
+                    else:
+                        username = project.users[h.old_value]
+                        if username in user_map: username = user_map[username]
+                        revision['assignee'] = oldvalue = username
+                elif h.field_name == 'close_date' and revision['close_date'] != 0:
+                    f = 'status'
+                    oldvalue = 'assigned'
+                    newvalue = 'closed'
 
-             Fields not processed (field: explanation):
-            File Added - TODO
-            resolution_id - need to update used_resolutions
-            status_id
-            artifact_group_id
-            category_id
-            group_id
-            """
-            f = None
-            if h.field_name in ("IP",):
-                changes += 1
-                continue
-            elif h.field_name in ("summary", "priority"):
-                f = h.field_name
-                oldvalue = h.old_value
-                newvalue = revision.get(h.field_name, None)
-            elif h.field_name == 'assigned_to':
-                f = "owner"
-                newvalue = revision['assignee']
-                if h.old_value == '100': # was not assigned
-                    revision['assignee'] = None
-                    oldvalue = None
-                else:
-                    username = project.users[h.old_value]
-                    if username in user_map: username = user_map[username]
-                    revision['assignee'] = oldvalue = username
-            elif h.field_name == 'close_date' and revision['close_date'] != 0:
-                f = 'status'
-                oldvalue = 'assigned'
-                newvalue = 'closed'
+                if f:
+                    changes += 1
+                    trac.addTicketChange(ticket=i,
+                                         time=h.date,
+                                         author=h.updator,
+                                         field=f,
+                                         oldvalue=oldvalue,
+                                         newvalue=newvalue)
 
-            if f:
-                changes += 1
-                trac.addTicketChange(ticket=i,
-                                     time=h.date,
-                                     author=h.updator,
-                                     field=f,
-                                     oldvalue=oldvalue,
-                                     newvalue=newvalue)
-
-            if h.field_name != 'assigned_to':
-                revision[h.field_name] = h.old_value
-        if changes:
-            print '    processed %d out of %d history items for #%d' % \
-                    (changes, len(t.history_entries), i)
+                if h.field_name != 'assigned_to':
+                    revision[h.field_name] = h.old_value
+            if changes:
+                print '    processed %d out of %d history items for #%d' % \
+                        (changes, len(t.history_entries), i)
 
 
 def main():
diff --git a/trac/contrib/trac-pre-commit-hook b/trac/contrib/trac-pre-commit-hook
index c254723..fe34a3e 100644
--- a/trac/contrib/trac-pre-commit-hook
+++ b/trac/contrib/trac-pre-commit-hook
@@ -1,7 +1,17 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 #
-# Author: Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2013 Edgewall Software
+# Copyright (C) 2004 Jonas Borgström <jonas@edgewall.com>
+# 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/.
 #
 # This script will enforce the following policy:
 #
diff --git a/trac/contrib/trac-svn-hook b/trac/contrib/trac-svn-hook
index 0f92de0..7a14160 100755
--- a/trac/contrib/trac-svn-hook
+++ b/trac/contrib/trac-svn-hook
@@ -1,4 +1,17 @@
 #!/bin/sh
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2013 Edgewall Software
+# Copyright (C) 2009 Christian Boos <cboos@edgewall.org>
+# 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/.
 #
 # = trac-svn-hook =
 #
diff --git a/trac/contrib/trac-svn-post-commit-hook.cmd b/trac/contrib/trac-svn-post-commit-hook.cmd
index 6b1cfb7..5547a34 100644
--- a/trac/contrib/trac-svn-post-commit-hook.cmd
+++ b/trac/contrib/trac-svn-post-commit-hook.cmd
@@ -1,8 +1,19 @@
 @ECHO OFF
 ::
+:: Copyright (C) 2007-2013 Edgewall Software
+:: Copyright (C) 2007 Markus Tacker <m@tacker.org>
+:: Copyright (C) 2007 Christian Boos <cboos@edgewall.org>
+:: 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/.
 :: Trac post-commit-hook script for Windows
 ::
-:: Contributed by markus, modified by cboos.
 :: Modified for the multirepos branch to use the `changeset` command.
 
 :: Usage:
diff --git a/trac/contrib/workflow/migrate_original_to_basic.py b/trac/contrib/workflow/migrate_original_to_basic.py
index 1c704e9..b35a5dd 100755
--- a/trac/contrib/workflow/migrate_original_to_basic.py
+++ b/trac/contrib/workflow/migrate_original_to_basic.py
@@ -1,9 +1,24 @@
 #!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Eli Carter <retracile@gmail.com>
+# 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/.
+
 import sys
 
 import trac.env
 from trac.ticket.default_workflow import load_workflow_config_snippet
 
+
 def main():
     """Rewrite the ticket-workflow section of the config; and change all
     'assigned' tickets to 'accepted'.
diff --git a/trac/contrib/workflow/showworkflow b/trac/contrib/workflow/showworkflow
index 4bafc5f..6cfe6cd 100755
--- a/trac/contrib/workflow/showworkflow
+++ b/trac/contrib/workflow/showworkflow
@@ -1,4 +1,17 @@
 #!/bin/bash -x
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Eli Carter <retracile@gmail.com>
+# 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/.
 
 basedir=`dirname $0`
 options=""
diff --git a/trac/contrib/workflow/workflow_parser.py b/trac/contrib/workflow/workflow_parser.py
index 573e84b..1abd61c 100755
--- a/trac/contrib/workflow/workflow_parser.py
+++ b/trac/contrib/workflow/workflow_parser.py
@@ -1,4 +1,17 @@
 #!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Eli Carter <retracile@gmail.com>
+# 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/.
 
 import sys
 import getopt
diff --git a/trac/doc/conf.py b/trac/doc/conf.py
index d3ab0a7..a645237 100644
--- a/trac/doc/conf.py
+++ b/trac/doc/conf.py
@@ -1,5 +1,17 @@
 # -*- coding: utf-8 -*-
 #
+# Copyright (C) 2008-2013 Edgewall Software
+# Copyright (C) 2008 Noah Kantrowitz <noah@coderanger.net>
+# 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/.
+
 # Trac documentation build configuration file, created by
 # sphinx-quickstart on Wed May 14 09:05:13 2008.
 #
@@ -24,9 +36,9 @@
 # other places throughout the built documents.
 #
 # The short X.Y version.
-version = '1.0.1'
+version = '1.0.2'
 # The full version, including alpha/beta/rc tags.
-release = '1.0.1'
+release = '1.0.2'
 
 # Devel or Release mode for the documentation (if devel, include TODOs,
 # can also be used in conditionals: .. ifconfig :: devel)
diff --git a/trac/doc/dev/testing-intro.rst b/trac/doc/dev/testing-intro.rst
index 190bb9c..c5c7c91 100644
--- a/trac/doc/dev/testing-intro.rst
+++ b/trac/doc/dev/testing-intro.rst
@@ -42,6 +42,14 @@
 Provide additional options to the standalone
 :command:`tracd` server used for the functional tests.
 
+.. envvar:: TRAC_TEST_ENV_PATH
+
+Use the specified path for the test environment directory.
+
+.. envvar:: TRAC_TEST_PORT
+
+Use the specified port for running the standalone :command:`tracd` server.
+
 The :file:`Makefile` is actually written in a way that allow you to
 get more control, if you want.
 
diff --git a/trac/doc/utils/checkapidoc.py b/trac/doc/utils/checkapidoc.py
index 980ce3e..8458048 100644
--- a/trac/doc/utils/checkapidoc.py
+++ b/trac/doc/utils/checkapidoc.py
@@ -1,4 +1,16 @@
-# -*- coding:  utf-8 -*-
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012-2013 Edgewall Software
+# Copyright (C) 2012 Christian Boos <cboos@edgewall.org>
+# 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/.
 
 """Trac API doc checker
 
diff --git a/trac/doc/utils/runepydoc.py b/trac/doc/utils/runepydoc.py
index 1a35184..21bd00d 100644
--- a/trac/doc/utils/runepydoc.py
+++ b/trac/doc/utils/runepydoc.py
@@ -1,3 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010-2013 Edgewall Software
+# Copyright (C) 2010 Christian Boos <cboos@edgewall.org>
+# 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/.
+
 # Simple wrapper script needed to run epydoc
 
 import sys
@@ -52,4 +66,3 @@
 
 # Run epydoc
 cli()
-
diff --git a/trac/sample-plugins/HelloWorld.py b/trac/sample-plugins/HelloWorld.py
index 0e3ae2b..8812d99 100644
--- a/trac/sample-plugins/HelloWorld.py
+++ b/trac/sample-plugins/HelloWorld.py
@@ -1,7 +1,21 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Christian Boos <cboos@edgewall.org>
+# 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/.
+
 """Example macro."""
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/HelloWorld.py $"
+revision = "$Rev: 12412 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/HelloWorld.py $"
 
 #
 # The following shows the code for macro, old-style.
@@ -12,6 +26,7 @@
 # ---- (ignore in your own macro) ----
 # --
 from trac.util import escape
+from trac.util.translation import cleandoc_
 
 def execute(hdf, txt, env):
     # Currently hdf is set only when the macro is called
@@ -49,16 +64,16 @@
     the !MacroList macro (usually used in the TracWikiMacros page).
     """)
 
-    def expand_macro(self, formatter, name, args):
+    def expand_macro(self, formatter, name, content):
         """Return some output that will be displayed in the Wiki content.
 
         `name` is the actual name of the macro (no surprise, here it'll be
         `'HelloWorld'`),
-        `args` is the text enclosed in parenthesis at the call of the macro.
-          Note that if there are ''no'' parenthesis (like in, e.g.
-          [[HelloWorld]]), then `args` is `None`.
+        `content` is the text enclosed in parenthesis at the call of the
+          macro. Note that if there are ''no'' parenthesis (like in, e.g.
+          [[HelloWorld]]), then `content` is `None`.
         """
-        return 'Hello World, args = ' + unicode(args)
+        return 'Hello World, content = ' + unicode(content)
 
     # Note that there's no need to HTML escape the returned data,
     # as the template engine (Genshi) will do it for us.
diff --git a/trac/sample-plugins/Timestamp.py b/trac/sample-plugins/Timestamp.py
index 98cb6a3..a8f271a 100644
--- a/trac/sample-plugins/Timestamp.py
+++ b/trac/sample-plugins/Timestamp.py
@@ -1,7 +1,21 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Christian Boos <cboos@edgewall.org>
+# 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/.
+
 """Inserts the current time (in seconds) into the wiki page."""
 
-revision = "$Rev: 10617 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/Timestamp.py $"
+revision = "$Rev: 12743 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/Timestamp.py $"
 
 #
 # The following shows the code for macro, old-style.
@@ -36,8 +50,8 @@
 class TimestampMacro(WikiMacroBase):
     _description = "Inserts the current time (in seconds) into the wiki page."
 
-    def expand_macro(self, formatter, name, args):
+    def expand_macro(self, formatter, name, content, args=None):
         t = datetime.now(utc)
-        return tag.b(format_datetime(t, '%c'))
+        return tag.strong(format_datetime(t, '%c'))
 # --
 # ---- (reuse for your own macro) ----
diff --git a/trac/sample-plugins/milestone_to_version.py b/trac/sample-plugins/milestone_to_version.py
index c36f11b..d6d8d89 100644
--- a/trac/sample-plugins/milestone_to_version.py
+++ b/trac/sample-plugins/milestone_to_version.py
@@ -1,3 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2013 Edgewall Software
+# Copyright (C) 2009 Remy Blank <remy.blank@pobox.com>
+# 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/.
+
 import re
 
 from trac.config import Option
@@ -6,8 +20,8 @@
 from trac.ticket.api import IMilestoneChangeListener
 from trac.ticket.model import Version
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/milestone_to_version.py $"
+revision = "$Rev: 12164 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/milestone_to_version.py $"
 
 
 class MilestoneToVersion(Component):
diff --git a/trac/sample-plugins/permissions/debug_perm.py b/trac/sample-plugins/permissions/debug_perm.py
index 394e826..aa3de88 100644
--- a/trac/sample-plugins/permissions/debug_perm.py
+++ b/trac/sample-plugins/permissions/debug_perm.py
@@ -1,3 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Christian Boos <cboos@edgewall.org>
+# 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 trac.core import *
 from trac.perm import IPermissionPolicy, PermissionCache
 from trac.resource import Resource
diff --git a/trac/sample-plugins/permissions/public_wiki_policy.py b/trac/sample-plugins/permissions/public_wiki_policy.py
index 7c0a7ce..95635c7 100644
--- a/trac/sample-plugins/permissions/public_wiki_policy.py
+++ b/trac/sample-plugins/permissions/public_wiki_policy.py
@@ -1,11 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Christian Boos <cboos@edgewall.org>
+# 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 fnmatch import fnmatchcase
 
 from trac.config import Option
 from trac.core import *
 from trac.perm import IPermissionPolicy
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/permissions/public_wiki_policy.py $"
+revision = "$Rev: 12500 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/permissions/public_wiki_policy.py $"
 
 class PublicWikiPolicy(Component):
     """Allow public access to some wiki pages.
@@ -60,4 +74,3 @@
             if action.startswith('WIKI_'):
                 return True
                 # this policy ''may'' grant permissions on some wiki pages
-
diff --git a/trac/sample-plugins/permissions/vulnerability_tickets.py b/trac/sample-plugins/permissions/vulnerability_tickets.py
index 15686a3..484012f 100644
--- a/trac/sample-plugins/permissions/vulnerability_tickets.py
+++ b/trac/sample-plugins/permissions/vulnerability_tickets.py
@@ -1,8 +1,22 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Alec Thomas <alec@swapoff.org>
+# 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 trac.core import *
 from trac.perm import IPermissionPolicy, IPermissionRequestor
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/permissions/vulnerability_tickets.py $"
+revision = "$Rev: 12164 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/permissions/vulnerability_tickets.py $"
 
 class SecurityTicketsPolicy(Component):
     """Prevent public access to security sensitive tickets.
diff --git a/trac/sample-plugins/revision_links.py b/trac/sample-plugins/revision_links.py
index 22b2011..58aa625 100644
--- a/trac/sample-plugins/revision_links.py
+++ b/trac/sample-plugins/revision_links.py
@@ -1,3 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Christian Boos <cboos@edgewall.org>
+# 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/.
+
 """Sample Wiki syntax extension plugin."""
 
 from genshi.builder import tag
@@ -8,8 +22,8 @@
 from trac.versioncontrol.web_ui import ChangesetModule
 from trac.wiki.api import IWikiSyntaxProvider
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/revision_links.py $"
+revision = "$Rev: 12500 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/revision_links.py $"
 
 class RevisionLinks(Component):
     """Adds a few more ways to refer to changesets."""
@@ -53,4 +67,3 @@
             pass
         return tag.a(label, class_="missing changeset", rel="nofollow",
                      href=formatter.href.changeset(rev))
-
diff --git a/trac/sample-plugins/workflow/CodeReview.py b/trac/sample-plugins/workflow/CodeReview.py
index 896e7f6..6f839a7 100644
--- a/trac/sample-plugins/workflow/CodeReview.py
+++ b/trac/sample-plugins/workflow/CodeReview.py
@@ -1,13 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Eli Carter <retracile@gmail.com>
+# 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 genshi.builder import tag
 
-from trac.core import implements,Component
+from trac.core import Component, implements
+from trac.perm import IPermissionRequestor
 from trac.ticket.api import ITicketActionController
 from trac.ticket.default_workflow import ConfigurableTicketWorkflow
-from trac.perm import IPermissionRequestor
-from trac.config import Option, ListOption
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/workflow/CodeReview.py $"
+revision = "$Rev: 12731 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/workflow/CodeReview.py $"
 
 class CodeReviewActionController(Component):
     """Support for simple code reviews.
diff --git a/trac/sample-plugins/workflow/DeleteTicket.py b/trac/sample-plugins/workflow/DeleteTicket.py
index 4bcbba4..af72e64 100644
--- a/trac/sample-plugins/workflow/DeleteTicket.py
+++ b/trac/sample-plugins/workflow/DeleteTicket.py
@@ -1,11 +1,23 @@
-from genshi.builder import tag
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Eli Carter <retracile@gmail.com>
+# 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 trac.core import implements,Component
 from trac.ticket.api import ITicketActionController
 from trac.perm import IPermissionRequestor
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/workflow/DeleteTicket.py $"
+revision = "$Rev: 12731 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/workflow/DeleteTicket.py $"
 
 class DeleteTicketActionController(Component):
     """Provides the admin with a way to delete a ticket.
diff --git a/trac/sample-plugins/workflow/MilestoneOperation.py b/trac/sample-plugins/workflow/MilestoneOperation.py
index 1163fac..8d821b1 100644
--- a/trac/sample-plugins/workflow/MilestoneOperation.py
+++ b/trac/sample-plugins/workflow/MilestoneOperation.py
@@ -1,14 +1,16 @@
 # -*- coding: utf-8 -*-
 #
+# Copyright (C) 2002-2013 Edgewall Software
 # Copyright (C) 2012 Franz Mayer <franz.mayer@gefasoft.de>
+# All rights reserved.
 #
-# "THE BEER-WARE LICENSE" (Revision 42):
-# <franz.mayer@gefasoft.de> wrote this file.  As long as you retain this
-# notice you can do whatever you want with this stuff. If we meet some day,
-# and you think this stuff is worth it, you can buy me a beer in return.
-# Franz Mayer
+# 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.
 #
-# Author: Franz Mayer <franz.mayer@gefasoft.de>
+# 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 genshi.builder import tag
 
diff --git a/trac/sample-plugins/workflow/StatusFixer.py b/trac/sample-plugins/workflow/StatusFixer.py
index dd60715..d7740f7 100644
--- a/trac/sample-plugins/workflow/StatusFixer.py
+++ b/trac/sample-plugins/workflow/StatusFixer.py
@@ -1,11 +1,25 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Eli Carter <retracile@gmail.com>
+# 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 genshi.builder import tag
 
 from trac.core import Component, implements
 from trac.ticket.api import ITicketActionController, TicketSystem
 from trac.perm import IPermissionRequestor
 
-revision = "$Rev: 11075 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/workflow/StatusFixer.py $"
+revision = "$Rev: 12164 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/workflow/StatusFixer.py $"
 
 class StatusFixerActionController(Component):
     """Provides the admin with a way to correct a ticket's status.
diff --git a/trac/sample-plugins/workflow/VoteOperation.py b/trac/sample-plugins/workflow/VoteOperation.py
index a9fc94e..fde7759 100644
--- a/trac/sample-plugins/workflow/VoteOperation.py
+++ b/trac/sample-plugins/workflow/VoteOperation.py
@@ -1,3 +1,17 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007 Eli Carter <retracile@gmail.com>
+# 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 genshi.builder import tag
 
 from trac.core import implements,Component
@@ -6,8 +20,8 @@
 from trac.ticket.model import Priority, Ticket
 #from trac.perm import IPermissionRequestor # (TODO)
 
-revision = "$Rev: 11490 $"
-url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.1/sample-plugins/workflow/VoteOperation.py $"
+revision = "$Rev: 12164 $"
+url = "$URL: http://svn.edgewall.org/repos/trac/tags/trac-1.0.2/sample-plugins/workflow/VoteOperation.py $"
 
 class VoteOperation(Component):
     """Provides a simplistic vote feature.
diff --git a/trac/setup.cfg b/trac/setup.cfg
index 2c56dec..b365660 100644
--- a/trac/setup.cfg
+++ b/trac/setup.cfg
@@ -1,4 +1,4 @@
-[egg_info]
+#[egg_info]
 #tag_build = dev
 #tag_svn_revision = true
 
diff --git a/trac/setup.py b/trac/setup.py
index d1ebe23..b7b5fe6 100755
--- a/trac/setup.py
+++ b/trac/setup.py
@@ -18,10 +18,10 @@
 
 min_python = (2, 5)
 if sys.version_info < min_python:
-    print "Trac requires Python %d.%d or later" % 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)"
+    print("Trac doesn't support Python 3 (yet)")
     sys.exit(1)
 
 extra = {}
@@ -49,13 +49,13 @@
 try:
     import genshi
 except ImportError:
-    print "Genshi is needed by Trac setup, pre-installing"
+    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',
+    version = '1.0.2',
     description = 'Integrated SCM, wiki, issue tracker and project environment',
     long_description = """
 Trac is a minimalistic web-based software project management and bug/issue
@@ -105,8 +105,9 @@
     ],
     extras_require = {
         'Babel': ['Babel>=0.9.5'],
+        'ConfigObj': ['ConfigObj'],
         'Pygments': ['Pygments>=0.6'],
-        'reST': ['docutils>=0.3'],
+        'reST': ['docutils>=0.3.9'],
         'SilverCity': ['SilverCity>=0.9.4'],
         'Textile': ['textile>=2.0'],
     },
@@ -141,6 +142,7 @@
         trac.versioncontrol.svn_authz = trac.versioncontrol.svn_authz
         trac.versioncontrol.web_ui = trac.versioncontrol.web_ui
         trac.web.auth = trac.web.auth
+        trac.web.main = trac.web.main
         trac.web.session = trac.web.session
         trac.wiki.admin = trac.wiki.admin
         trac.wiki.interwiki = trac.wiki.interwiki
@@ -150,7 +152,7 @@
         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.authz_policy = tracopt.perm.authz_policy[ConfigObj]
         tracopt.perm.config_perm_provider = tracopt.perm.config_perm_provider
         tracopt.ticket.clone = tracopt.ticket.clone
         tracopt.ticket.commit_updater = tracopt.ticket.commit_updater
diff --git a/trac/trac/TRAC_VERSION b/trac/trac/TRAC_VERSION
index 7dea76e..6d7de6e 100644
--- a/trac/trac/TRAC_VERSION
+++ b/trac/trac/TRAC_VERSION
@@ -1 +1 @@
-1.0.1
+1.0.2
diff --git a/trac/trac/__init__.py b/trac/trac/__init__.py
index d3840dd..d857c35 100644
--- a/trac/trac/__init__.py
+++ b/trac/trac/__init__.py
@@ -16,7 +16,7 @@
 try:
     __version__ = get_distribution('Trac').version
 except DistributionNotFound:
-    __version__ = '1.0.1'
+    __version__ = '1.0.2'
 
 try:
     from hooks import install_global_hooks
diff --git a/trac/trac/about.py b/trac/trac/about.py
index a8a625c..9c9ba0e 100644
--- a/trac/trac/about.py
+++ b/trac/trac/about.py
@@ -26,7 +26,7 @@
 from trac.perm import IPermissionRequestor
 from trac.util.translation import _
 from trac.web import IRequestHandler
-from trac.web.chrome import INavigationContributor
+from trac.web.chrome import Chrome, INavigationContributor
 
 
 class AboutModule(Component):
@@ -62,6 +62,7 @@
         if 'CONFIG_VIEW' in req.perm('config', 'systeminfo'):
             # Collect system information
             data['systeminfo'] = self.env.get_systeminfo()
+            Chrome(self.env).add_jquery_ui(req)
 
         if 'CONFIG_VIEW' in req.perm('config', 'plugins'):
             # Collect plugin information
diff --git a/trac/trac/admin/api.py b/trac/trac/admin/api.py
index a5ae562..bae8ee7 100644
--- a/trac/trac/admin/api.py
+++ b/trac/trac/admin/api.py
@@ -11,15 +11,18 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://trac.edgewall.org/log/.
 
+import os
 import os.path
 import sys
 import traceback
 
 from trac.core import *
 from trac.util.text import levenshtein_distance
-from trac.util.translation import _
+from trac.util.translation import _, get_negotiated_locale, has_babel
 
 
+LANG = os.environ.get('LANG')
+
 console_date_format = '%Y-%m-%d'
 console_datetime_format = '%Y-%m-%d %H:%M:%S'
 console_date_format_hint = 'YYYY-MM-DD'
@@ -192,3 +195,21 @@
         except OSError:
             pass
     return result
+
+
+def get_console_locale(env=None, lang=LANG):
+    """Return negotiated locale for console by LANG environment and
+    [trac] default_language."""
+    if has_babel:
+        from babel.core import Locale, UnknownLocaleError, parse_locale
+        try:
+            lang = '_'.join(filter(None, parse_locale(lang)))
+        except:
+            lang = None
+        default = env.config.get('trac', 'default_language', '') \
+                  if env else None
+        try:
+            return get_negotiated_locale([lang, default]) or Locale.default()
+        except UnknownLocaleError:
+            pass
+    return None
diff --git a/trac/trac/admin/console.py b/trac/trac/admin/console.py
index 559aed2..d1077d2 100755
--- a/trac/trac/admin/console.py
+++ b/trac/trac/admin/console.py
@@ -15,7 +15,6 @@
 from __future__ import with_statement
 
 import cmd
-import locale
 import os.path
 import pkg_resources
 from shlex import shlex
@@ -24,24 +23,25 @@
 import traceback
 
 from trac import __version__ as VERSION
-from trac.admin import AdminCommandError, AdminCommandManager
+from trac.admin.api import AdminCommandError, AdminCommandManager, \
+                           get_console_locale
 from trac.core import TracError
 from trac.env import Environment
 from trac.ticket.model import *
-from trac.util import translation
+from trac.util import translation, warn_setuptools_issue
 from trac.util.html import html
 from trac.util.text import console_print, exception_to_unicode, printout, \
                            printerr, raw_input, to_unicode, \
                            getpreferredencoding
-from trac.util.translation import _, ngettext, get_negotiated_locale, \
-                                  has_babel, cleandoc_
+from trac.util.translation import _, ngettext, has_babel, cleandoc_
 from trac.versioncontrol.api import RepositoryManager
 from trac.wiki.admin import WikiAdmin
 from trac.wiki.macros import WikiMacroBase
 
+
 TRAC_VERSION = pkg_resources.get_distribution('Trac').version
 rl_completion_suppress_append = None
-LANG = os.environ.get('LANG')
+
 
 def find_readline_lib():
     """Return the name (and possibly the full path) of the readline library
@@ -67,6 +67,7 @@
     envname = None
     __env = None
     needs_upgrade = None
+    cmd_mgr = None
 
     def __init__(self, envdir=None):
         cmd.Cmd.__init__(self)
@@ -146,6 +147,7 @@
         self.prompt = "Trac [%s]> " % self.envname
         if env is not None:
             self.__env = env
+            self.cmd_mgr = AdminCommandManager(env)
 
     def env_check(self):
         if not self.__env:
@@ -168,12 +170,13 @@
 
     def _init_env(self):
         self.__env = env = Environment(self.envname)
+        negotiated = None
         # fixup language according to env settings
         if has_babel:
-            default = env.config.get('trac', 'default_language', '')
-            negotiated = get_negotiated_locale([LANG, default])
+            negotiated = get_console_locale(env)
             if negotiated:
                 translation.activate(negotiated)
+        self.cmd_mgr = AdminCommandManager(env)
 
     ##
     ## Utility methods
@@ -240,9 +243,8 @@
         if line and line[-1] == ' ':    # Space starts new argument
             args.append('')
         if self.env_check():
-            cmd_mgr = AdminCommandManager(self.env)
             try:
-                comp = cmd_mgr.complete_command(args, cmd_only)
+                comp = self.cmd_mgr.complete_command(args, cmd_only)
             except Exception, e:
                 printerr()
                 printerr(_('Completion error: %(err)s',
@@ -281,8 +283,7 @@
             raise TracError(_('The Trac Environment needs to be upgraded.\n\n'
                               'Run "trac-admin %(path)s upgrade"',
                               path=self.envname))
-        cmd_mgr = AdminCommandManager(self.env)
-        return cmd_mgr.execute_command(*args)
+        return self.cmd_mgr.execute_command(*args)
 
     ##
     ## Available Commands
@@ -304,9 +305,10 @@
     def do_help(self, line=None):
         arg = self.arg_tokenize(line)
         if arg[0]:
+            cmd_mgr = None
             doc = getattr(self, "_help_" + arg[0], None)
             if doc is None and self.env_check():
-                cmd_mgr = AdminCommandManager(self.env)
+                cmd_mgr = self.cmd_mgr
                 doc = cmd_mgr.get_command_help(arg)
             if doc:
                 self.print_doc(doc)
@@ -314,7 +316,9 @@
                 printerr(_("No documentation found for '%(cmd)s'."
                            " Use 'help' to see the list of commands.",
                            cmd=' '.join(arg)))
-                cmds = cmd_mgr.get_similar_commands(arg[0])
+                cmds = None
+                if cmd_mgr:
+                    cmds = cmd_mgr.get_similar_commands(arg[0])
                 if cmds:
                     printout('')
                     printout(ngettext("Did you mean this?",
@@ -555,21 +559,16 @@
             doc = TracAdmin.all_docs(self.env)
         buf = StringIO.StringIO()
         TracAdmin.print_doc(doc, buf, long=True)
-        return html.PRE(buf.getvalue(), class_='wiki')
+        return html.PRE(buf.getvalue().decode('utf-8'), class_='wiki')
 
 
 def run(args=None):
     """Main entry point."""
     if args is None:
         args = sys.argv[1:]
-    locale = None
     if has_babel:
-        import babel
-        try:
-            locale = get_negotiated_locale([LANG]) or babel.Locale.default()
-        except babel.UnknownLocaleError:
-            pass
-        translation.activate(locale)
+        translation.activate(get_console_locale())
+    warn_setuptools_issue()
     admin = TracAdmin()
     if len(args) > 0:
         if args[0] in ('-h', '--help', 'help'):
diff --git a/trac/trac/admin/templates/admin.html b/trac/trac/admin/templates/admin.html
index 4de1d12..8019400 100644
--- a/trac/trac/admin/templates/admin.html
+++ b/trac/trac/admin/templates/admin.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/admin/templates/admin_basics.html b/trac/trac/admin/templates/admin_basics.html
index e778372..0297d0f 100644
--- a/trac/trac/admin/templates/admin_basics.html
+++ b/trac/trac/admin/templates/admin_basics.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -39,15 +49,25 @@
                       selected="${tzname == default_timezone or None}">${tzname}</option>
             </select>
           </label>
+          <span py:if="not has_pytz" class="hint">
+            Install pytz for a complete list of timezones.
+          </span>
         </div>
         <div class="field">
           <label>Default language:<br />
-            <select name="default_language">
+            <select name="default_language" disabled="${not languages or None}"
+                    title="${_('Translations are currently unavailable') if not languages else None}">
               <option value="">Browser's language</option>
               <option py:for="locale, language in languages" value="${locale}"
                       selected="${locale == default_language or None}">${language}</option>
             </select>
           </label>
+          <span py:if="not has_babel" class="hint">
+            Install Babel for extended language support.
+          </span>
+          <span py:if="has_babel and not languages" class="hint">
+            Message catalogs have not been compiled.
+          </span>
         </div>
         <div class="field">
           <label>Default date format:<br />
@@ -57,6 +77,9 @@
                       selected="${default_date_format == 'iso8601' or None}">ISO 8601 format</option>
             </select>
           </label>
+          <span py:if="not has_babel" class="hint">
+            Install Babel for localized date formats.
+          </span>
         </div>
       </fieldset>
       <div class="buttons">
diff --git a/trac/trac/admin/templates/admin_legacy.html b/trac/trac/admin/templates/admin_legacy.html
index 0a74f71..a61d906 100644
--- a/trac/trac/admin/templates/admin_legacy.html
+++ b/trac/trac/admin/templates/admin_legacy.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2007-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/admin/templates/admin_logging.html b/trac/trac/admin/templates/admin_logging.html
index f6ede35..e606499 100644
--- a/trac/trac/admin/templates/admin_logging.html
+++ b/trac/trac/admin/templates/admin_logging.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/admin/templates/admin_perms.html b/trac/trac/admin/templates/admin_perms.html
index 21b1529..213ab4d 100644
--- a/trac/trac/admin/templates/admin_perms.html
+++ b/trac/trac/admin/templates/admin_perms.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -13,7 +23,7 @@
   <body>
     <h2>Manage Permissions and Groups</h2>
 
-    <py:if test="'PERMISSION_GRANT' in perm">
+    <py:if test="'PERMISSION_GRANT' in perm('admin', 'general/perm')">
       <form id="addperm" class="addnew" method="post" action="">
         <fieldset>
           <legend>Grant Permission:</legend>
@@ -22,13 +32,14 @@
           </div>
           <div class="field">
             <label>Action:
-              <select id="action" name="action">
-                <option py:for="action in sorted(actions)">$action</option>
+              <select id="action" name="action"
+                      py:with="allowed_actions = [a for a in actions if a in perm]">
+                <option py:for="action in sorted(allowed_actions)">$action</option>
               </select>
             </label>
           </div>
           <div class="buttons">
-            <input type="submit" name="add" value="${_('Add')}" />
+            <input type="submit" name="add" class="trac-disable-on-submit" value="${_('Add')}" />
           </div>
           <p class="help">
             Grant permission for an action to a subject, which can be either a user
@@ -47,7 +58,7 @@
             <label>Group: <input id="sg_group" type="text" name="group" /></label>
           </div>
           <div class="buttons">
-            <input type="submit" name="add" value="${_('Add')}"/>
+            <input type="submit" name="add" class="trac-disable-on-submit" value="${_('Add')}"/>
           </div>
           <p class="help">
             Add a user or group to an existing permission group.
@@ -56,7 +67,7 @@
       </form>
     </py:if>
 
-    <form id="revokeform" method="post" action="" py:with="can_revoke = 'PERMISSION_REVOKE' in perm">
+    <form id="revokeform" method="post" action="" py:with="can_revoke = 'PERMISSION_REVOKE' in perm('admin', 'general/perm')">
       <h3>Permissions</h3>
       <table class="listing" id="permlist">
         <thead>
@@ -67,14 +78,18 @@
               class="${'odd' if idx % 2 else 'even'}">
             <td>$subject</td>
             <td>
-              <label py:for="subject, action in perm_group">
+              <label py:for="subject, action in perm_group"
+                     py:with="invalid = action not in actions; has_perm = action in perm">
                 <!--! base64 makes it safe to use ':' as separator when passing
                       both subject and action as one query parameter -->
                 <input py:if="can_revoke" type="checkbox" name="sel"
+                       title="${_('You don\'t have permission to revoke this action')
+                                if not has_perm else None}"
                        value="${'%s:%s' % (unicode_to_base64(subject),
-                                           unicode_to_base64(action))}"/>
-                <span py:strip="action in actions" class="missing"
-                      title="Action is no longer defined">${action}</span>
+                                           unicode_to_base64(action))}"
+                       disabled="${'disabled' if not has_perm else None}" />
+                <span class="${classes(missing=invalid)}"
+                      title="${_('%(action)s is no longer defined', action=action) if invalid else action}">${action}</span>
               </label>
             </td>
           </tr>
diff --git a/trac/trac/admin/templates/admin_plugins.html b/trac/trac/admin/templates/admin_plugins.html
index ad04d22..7359816 100644
--- a/trac/trac/admin/templates/admin_plugins.html
+++ b/trac/trac/admin/templates/admin_plugins.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -54,7 +64,7 @@
   </head>
 
   <body>
-    <h2>Manage Plugins</h2>
+    <h2>Manage Plugins <span class="trac-count">(${len(plugins)})</span></h2>
 
     <form id="addplug" class="addnew" method="post" enctype="multipart/form-data" action="">
       <fieldset>
@@ -65,8 +75,8 @@
           </label>
         </div>
         <div class="buttons">
-          <input type="submit" name="install" value="${_('Install')}"
-                 disabled="${readonly or None}" />
+          <input type="submit" name="install" class="trac-disable-on-submit"
+                 value="${_('Install')}" disabled="${readonly or None}" />
         </div>
         <p class="help" py:choose="readonly">
           <py:when test="True">
@@ -80,7 +90,7 @@
       </fieldset>
     </form>
 
-    <form py:for="idx, plugin in enumerate(plugins)" method="post" action="">
+    <form py:for="idx, plugin in enumerate(plugins)" id="edit-plugin-${plugin.name.lower()}" method="post" action="">
       <div class="plugin" id="trac-plugin-${plugin.name}">
         <h3 class="foldable">${plugin.name} ${plugin.version}</h3>
         <!--! FIXME: Plugin uninstall disabled as it is unreliable (#3545)
@@ -148,7 +158,7 @@
                 </p>
                 <div py:if="module.description" xml:space="preserve">${safe_wiki_to_html(context, module.description)}</div>
               </td>
-              <td class="sel"></td>
+              <td class="sel trac-module"></td>
             </tr>
             <tr py:for="component_name, component in sorted(module.components.iteritems())">
               <td py:with="show_doc = show == plugin.name or show == component.full_name" id="trac-comp-${component.full_name}"
@@ -164,7 +174,7 @@
                 </p>
                 <div py:if="component.description" xml:space="preserve">${safe_wiki_to_html(context, component.description)}</div>
               </td>
-              <td class="sel">
+              <td class="sel trac-component">
                 <input py:if="not component.required" type="hidden" name="component"
                        value="${module_name}.${component_name}" />
                 <input type="checkbox" name="enable"
diff --git a/trac/trac/admin/tests/__init__.py b/trac/trac/admin/tests/__init__.py
index 56dbce4..57e2c51 100644
--- a/trac/trac/admin/tests/__init__.py
+++ b/trac/trac/admin/tests/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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 unittest
 
 from trac.admin.tests import console
diff --git a/trac/trac/admin/tests/console-tests.txt b/trac/trac/admin/tests/console-tests.txt
index 005c40c..c49cbd3 100644
--- a/trac/trac/admin/tests/console-tests.txt
+++ b/trac/trac/admin/tests/console-tests.txt
@@ -54,7 +54,7 @@
 session add          Create a session for the given sid
 session delete       Delete the session of the specified sid
 session list         List the name and email for the given sids
-session purge        Purge all anonymous sessions older than the given age
+session purge        Purge anonymous sessions older than the given age or date
 session set          Set the name or email attribute of the given sid
 severity add         Add a severity value option
 severity change      Change a severity value
@@ -87,6 +87,8 @@
 Name  Size  Author  Date  Description
 -------------------------------------
 
+===== test_attachment_add_nonexistent_resource =====
+ResourceNotFound: NonExistentPage doesn't exist, can't create attachment
 ===== test_config_get =====
 Test project
 ===== test_config_set =====
@@ -357,9 +359,11 @@
  WIKI_MODIFY, WIKI_RENAME, WIKI_VIEW
 
 ===== test_permission_remove_unknown_user =====
-Error: Cannot remove permission TICKET_VIEW for user joe.
+Error: Cannot remove permission TICKET_VIEW for user joe. The user has not been granted the permission.
 ===== test_permission_remove_action_not_granted =====
-Error: Cannot remove permission TICKET_CREATE for user anonymous.
+Error: Cannot remove permission TICKET_CREATE for user anonymous. The user has not been granted the permission.
+===== test_permission_remove_action_granted_through_meta_permission =====
+Error: Cannot remove permission WIKI_VIEW for user joe. The permission is granted through a meta-permission or group.
 ===== test_permission_export_ok =====
 anonymous,BROWSER_VIEW,CHANGESET_VIEW,FILE_VIEW,LOG_VIEW,MILESTONE_VIEW,REPORT_SQL_VIEW,REPORT_VIEW,ROADMAP_VIEW,SEARCH_VIEW,TICKET_VIEW,TIMELINE_VIEW,WIKI_VIEW
 authenticated,TICKET_CREATE,TICKET_MODIFY,WIKI_CREATE,WIKI_MODIFY
@@ -412,6 +416,14 @@
 -----------------------
 component1     somebody
 component2     somebody
+new_component
+
+===== test_component_add_optional_owner_ok =====
+
+Name           Owner
+-----------------------
+component1     somebody
+component2     somebody
 new_component  new_user
 
 ===== test_component_add_error_already_exists =====
@@ -720,6 +732,8 @@
 
 ===== test_milestone_add_error_already_exists =====
 IntegrityError: [...]
+===== test_milestone_add_invalid_date =====
+TracError: "<add>" is an invalid date, or the date format is not known. Try "%(hint)s" or "%(isohint)s" instead.
 ===== test_milestone_rename_ok =====
 
 Name               Due  Completed
@@ -751,6 +765,8 @@
 
 ===== test_milestone_due_error_bad_milestone =====
 ResourceNotFound: Milestone bad_milestone does not exist.
+===== test_milestone_due_invalid_date =====
+TracError: "<due>" is an invalid date, or the date format is not known. Try "%(hint)s" or "%(isohint)s" instead.
 ===== test_milestone_completed_ok =====
 
 Name        Due  Completed
@@ -762,6 +778,8 @@
 
 ===== test_milestone_completed_error_bad_milestone =====
 ResourceNotFound: Milestone bad_milestone does not exist.
+===== test_milestone_completed_invalid_date =====
+TracError: "<com>" is an invalid date, or the date format is not known. Try "%(hint)s" or "%(isohint)s" instead.
 ===== test_milestone_remove_ok =====
 
 Name        Due  Completed
@@ -1013,3 +1031,5 @@
 name18  0     2010-01-19  val18  val18
 name19  0     2010-01-20  val19  val19
 
+===== test_session_purge_invalid_date =====
+TracError: "<purge>" is an invalid date, or the date format is not known. Try "%(hint)s" or "%(isohint)s" instead.
diff --git a/trac/trac/admin/tests/console.py b/trac/trac/admin/tests/console.py
index cf7cbd3..3201421 100644
--- a/trac/trac/admin/tests/console.py
+++ b/trac/trac/admin/tests/console.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2004-2009 Edgewall Software
+# Copyright (C) 2004-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -14,6 +14,7 @@
 # Author: Tim Moloney <t.moloney@verizon.net>
 
 import difflib
+import inspect
 import os
 import re
 import sys
@@ -42,9 +43,14 @@
 import trac.timeline.web_ui
 import trac.wiki.web_ui
 
-from trac.admin import console, console_date_format
+from trac.admin.api import AdminCommandManager, IAdminCommandProvider, \
+                           console_date_format, get_console_locale
+from trac.admin.console import TracAdmin, TracAdminHelpMacro
+from trac.core import Component, implements
 from trac.test import EnvironmentStub
-from trac.util.datefmt import format_date, get_date_format_hint
+from trac.util.datefmt import format_date, get_date_format_hint, \
+                              get_datetime_format_hint
+from trac.util.translation import get_available_locales, has_babel
 from trac.web.tests.session import _prep_session_table
 
 STRIP_TRAILING_SPACE = re.compile(r'( +)$', re.MULTILINE)
@@ -71,20 +77,50 @@
     return expected
 
 
+def execute_cmd(tracadmin, cmd, strip_trailing_space=True, input=None):
+    _in = sys.stdin
+    _err = sys.stderr
+    _out = sys.stdout
+    try:
+        if input:
+            sys.stdin = StringIO(input.encode('utf-8'))
+            sys.stdin.encoding = 'utf-8' # fake input encoding
+        sys.stderr = sys.stdout = out = StringIO()
+        out.encoding = 'utf-8' # fake output encoding
+        retval = None
+        try:
+            retval = tracadmin.onecmd(cmd)
+        except SystemExit:
+            pass
+        value = out.getvalue()
+        if isinstance(value, str): # reverse what print_listing did
+            value = value.decode('utf-8')
+        if strip_trailing_space:
+            return retval, STRIP_TRAILING_SPACE.sub('', value)
+        else:
+            return retval, value
+    finally:
+        sys.stdin = _in
+        sys.stderr = _err
+        sys.stdout = _out
+
+
 class TracadminTestCase(unittest.TestCase):
     """
     Tests the output of trac-admin and is meant to be used with
     .../trac/tests.py.
     """
 
-    expected_results = load_expected_results(
-            os.path.join(os.path.split(__file__)[0], 'console-tests.txt'),
-            '===== (test_[^ ]+) =====')
+    expected_results_file = os.path.join(os.path.dirname(__file__),
+                                         'console-tests.txt')
+
+    expected_results = load_expected_results(expected_results_file,
+                                             '===== (test_[^ ]+) =====')
 
     def setUp(self):
         self.env = EnvironmentStub(default_data=True, enable=('trac.*',),
                                    disable=('trac.tests.*',))
-        self._admin = console.TracAdmin()
+        self._admin = TracAdmin()
         self._admin.env_set('', self.env)
 
         # Set test date to 11th Jan 2004
@@ -94,39 +130,32 @@
         self.env = None
 
     def _execute(self, cmd, strip_trailing_space=True, input=None):
-        _in = sys.stdin
-        _err = sys.stderr
-        _out = sys.stdout
-        try:
-            if input:
-                sys.stdin = StringIO(input.encode('utf-8'))
-                sys.stdin.encoding = 'utf-8' # fake input encoding
-            sys.stderr = sys.stdout = out = StringIO()
-            out.encoding = 'utf-8' # fake output encoding
-            retval = None
-            try:
-                retval = self._admin.onecmd(cmd)
-            except SystemExit:
-                pass
-            value = out.getvalue()
-            if isinstance(value, str): # reverse what print_listing did
-                value = value.decode('utf-8')
-            # DEBUG: uncomment in case of `AssertionError: 0 != 2` in tests
-            #if retval != 0:
-            #    print>>_err, value
-            if strip_trailing_space:
-                return retval, STRIP_TRAILING_SPACE.sub('', value)
-            else:
-                return retval, value
-        finally:
-            sys.stdin = _in
-            sys.stderr = _err
-            sys.stdout = _out
+        return execute_cmd(self._admin, cmd,
+                           strip_trailing_space=strip_trailing_space,
+                           input=input)
 
-    def assertEqual(self, expected_results, output):
-        if not (isinstance(expected_results, basestring) and \
+    @property
+    def _datetime_format_hint(self):
+        return get_datetime_format_hint(get_console_locale(self.env))
+
+    def _get_command_help(self, *args):
+        docs = AdminCommandManager(self.env).get_command_help(list(args))
+        self.assertEqual(1, len(docs))
+        return docs[0][2]
+
+    def assertExpectedResult(self, output, args=None):
+        test_name = inspect.stack()[1][3]
+        expected_result = self.expected_results[test_name]
+        if args is not None:
+            expected_result %= args
+        self.assertEqual(expected_result, output)
+
+    def assertEqual(self, expected_results, output, msg=None):
+        """:deprecated: since 1.0.2, use `assertExpectedResult` instead."""
+        if not (isinstance(expected_results, basestring) and
                 isinstance(output, basestring)):
-            return unittest.TestCase.assertEqual(self, expected_results, output)
+            return unittest.TestCase.assertEqual(self, expected_results,
+                                                 output, msg)
         def diff():
             # Create a useful delta between the output and the expected output
             output_lines = ['%s\n' % x for x in output.split('\n')]
@@ -153,13 +182,69 @@
         """
         from trac import __version__
 
-        test_name = sys._getframe().f_code.co_name
-        d = {'version': __version__,
-             'date_format_hint': get_date_format_hint()}
-        expected_results = self.expected_results[test_name] % d
         rv, output = self._execute('help')
-        self.assertEqual(0, rv)
-        self.assertEqual(expected_results, output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output, {
+            'version': __version__,
+            'date_format_hint': get_date_format_hint()
+        })
+        self.assertTrue(all(len(line) < 80 for line in output.split('\n')),
+                        "Lines should be less than 80 characters in length.")
+
+    # Locale test
+
+    def _test_get_console_locale_with_babel(self):
+        from babel.core import Locale, UnknownLocaleError
+        locales = get_available_locales()
+        en_US = Locale.parse('en_US')
+        de = Locale.parse('de')
+        de_DE = Locale.parse('de_DE')
+        try:
+            default = Locale.default()
+        except UnknownLocaleError:
+            default = None
+
+        language = self.env.config.get('trac', 'default_language')
+        try:
+            self.assertEqual(default, get_console_locale(None, None))
+            self.env.config.set('trac', 'default_language', '')
+            if 'de' in locales:
+                self.assertEqual(de, get_console_locale(None, 'de_DE.UTF8'))
+                self.env.config.set('trac', 'default_language', 'de')
+                self.assertEqual(de, get_console_locale(self.env, None))
+                self.assertEqual(de, get_console_locale(self.env, 'C'))
+                self.env.config.set('trac', 'default_language', 'en_US')
+                self.assertEqual(en_US, get_console_locale(self.env, None))
+                self.assertEqual(en_US, get_console_locale(self.env, 'C'))
+                self.assertEqual(de, get_console_locale(self.env,
+                                                        'de_DE.UTF8'))
+            if not locales:  # compiled catalog is missing
+                self.assertEqual(default, get_console_locale(None,
+                                                             'de_DE.UTF8'))
+                self.env.config.set('trac', 'default_language', 'de')
+                self.assertEqual(default, get_console_locale(self.env, None))
+                self.assertEqual(default, get_console_locale(self.env, 'C'))
+                self.env.config.set('trac', 'default_language', 'en_US')
+                self.assertEqual(en_US, get_console_locale(self.env, None))
+                self.assertEqual(en_US, get_console_locale(self.env, 'C'))
+                self.assertEqual(en_US, get_console_locale(self.env,
+                                                           'de_DE.UTF8'))
+        finally:
+            self.env.config.set('trac', 'default_language', language)
+
+    def _test_get_console_locale_without_babel(self):
+        self.assertEqual(None, get_console_locale(None, 'en_US.UTF8'))
+        language = self.env.config.get('trac', 'default_language')
+        try:
+            self.env.config.set('trac', 'default_language', 'en_US')
+            self.assertEqual(None, get_console_locale(self.env, 'en_US.UTF8'))
+        finally:
+            self.env.config.set('trac', 'default_language', language)
+
+    if has_babel:
+        test_get_console_locale = _test_get_console_locale_with_babel
+    else:
+        test_get_console_locale = _test_get_console_locale_without_babel
 
     # Attachment tests
 
@@ -172,10 +257,17 @@
         #        commands. This requires being able to control the current
         #        time, which in turn would require centralizing the time
         #        provider, for example in the environment object.
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('attachment list wiki:WikiStart')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
+
+    def test_attachment_add_nonexistent_resource(self):
+        """Tests the 'attachment add' command in trac-admin, on a non-existent
+        resource."""
+        rv, output = self._execute('attachment add wiki:NonExistentPage %s'
+                                   % __file__)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     # Config tests
 
@@ -184,21 +276,19 @@
         Tests the 'config get' command in trac-admin.  This particular
         test gets the project name from the config.
         """
-        test_name = sys._getframe().f_code.co_name
         self.env.config.set('project', 'name', 'Test project')
         rv, output = self._execute('config get project name')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_config_set(self):
         """
         Tests the 'config set' command in trac-admin.  This particular
         test sets the project name using an option value containing a space.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('config set project name "Test project"')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
         self.assertEqual('Test project',
                          self.env.config.get('project', 'name'))
 
@@ -208,11 +298,10 @@
         test removes the project name from the config, therefore reverting
         the option to the default value.
         """
-        test_name = sys._getframe().f_code.co_name
         self.env.config.set('project', 'name', 'Test project')
         rv, output = self._execute('config remove project name')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
         self.assertEqual('My Project', self.env.config.get('project', 'name'))
 
     # Permission tests
@@ -223,10 +312,9 @@
         has no command arguments, it is hard to call it incorrectly.  As
         a result, there is only this one test.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_add_one_action_ok(self):
         """
@@ -234,11 +322,10 @@
         test passes valid arguments to add one permission and checks for
         success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('permission add test_user WIKI_VIEW')
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_add_multiple_actions_ok(self):
         """
@@ -246,11 +333,10 @@
         test passes valid arguments to add multiple permissions and checks for
         success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('permission add test_user LOG_VIEW FILE_VIEW')
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_add_already_exists(self):
         """
@@ -258,13 +344,12 @@
         test passes a permission that already exists and checks for the
         message. Other permissions passed are added.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('permission add anonymous WIKI_CREATE '
                                    'WIKI_VIEW WIKI_MODIFY')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output2 = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output + output2)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output + output2)
 
     def test_permission_remove_one_action_ok(self):
         """
@@ -272,11 +357,10 @@
         test passes valid arguments to remove one permission and checks for
         success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('permission remove anonymous TICKET_MODIFY')
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_remove_multiple_actions_ok(self):
         """
@@ -284,44 +368,40 @@
         test passes valid arguments to remove multiple permission and checks
         for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('permission remove anonymous WIKI_CREATE WIKI_MODIFY')
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_remove_all_actions_for_user(self):
         """
         Tests the 'permission remove' command in trac-admin.  This particular
         test removes all permissions for anonymous.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('permission remove anonymous *')
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_remove_action_for_all_users(self):
         """
         Tests the 'permission remove' command in trac-admin.  This particular
         test removes the TICKET_CREATE permission from all users.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('permission add anonymous TICKET_CREATE')
         self._execute('permission remove * TICKET_CREATE')
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_remove_unknown_user(self):
         """
         Tests the 'permission remove' command in trac-admin.  This particular
         test tries removing a permission from an unknown user.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('permission remove joe TICKET_VIEW')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_remove_action_not_granted(self):
         """
@@ -329,38 +409,45 @@
         test tries removing TICKET_CREATE from user anonymous, who doesn't
         have that permission.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('permission remove anonymous TICKET_CREATE')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
+
+    def test_permission_remove_action_granted_through_meta_permission(self):
+        """
+        Tests the 'permission remove' command in trac-admin.  This particular
+        test tries removing WIKI_VIEW from a user. WIKI_VIEW has been granted
+        through user anonymous."""
+        self._execute('permission add joe TICKET_VIEW')
+        rv, output = self._execute('permission remove joe WIKI_VIEW')
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_export_ok(self):
         """
         Tests the 'permission export' command in trac-admin.  This particular
         test exports the default permissions to stdout.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('permission export')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_permission_import_ok(self):
         """
         Tests the 'permission import' command in trac-admin.  This particular
         test exports additional permissions, removes them and imports them back.
         """
-        test_name = sys._getframe().f_code.co_name
         user = u'test_user\u0250'
         self._execute('permission add ' + user + ' WIKI_VIEW')
         self._execute('permission add ' + user + ' TICKET_VIEW')
         rv, output = self._execute('permission export')
         self._execute('permission remove ' + user + ' *')
         rv, output = self._execute('permission import', input=output)
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         self.assertEqual('', output)
         rv, output = self._execute('permission list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     # Component tests
 
@@ -370,21 +457,30 @@
         has no command arguments, it is hard to call it incorrectly.  As
         a result, there is only this one test.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('component list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_add_ok(self):
         """
         Tests the 'component add' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
+        self._execute('component add new_component')
+        rv, output = self._execute('component list')
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
+
+    def test_component_add_optional_owner_ok(self):
+        """
+        Tests the 'component add' command in trac-admin with the optional
+        'owner' argument.  This particular test passes valid arguments and
+        checks for success.
+        """
         self._execute('component add new_component new_user')
         rv, output = self._execute('component list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_add_error_already_exists(self):
         """
@@ -392,52 +488,47 @@
         test passes a component name that already exists and checks for an
         error message.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('component add component1 new_user')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_rename_ok(self):
         """
         Tests the 'component rename' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('component rename component1 changed_name')
         rv, output = self._execute('component list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_rename_error_bad_component(self):
         """
         Tests the 'component rename' command in trac-admin.  This particular
         test tries to rename a component that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('component rename bad_component changed_name')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_rename_error_bad_new_name(self):
         """
         Tests the 'component rename' command in trac-admin.  This particular
         test tries to rename a component to a name that already exists.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('component rename component1 component2')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_chown_ok(self):
         """
         Tests the 'component chown' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('component chown component2 changed_owner')
         rv, output = self._execute('component list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_chown_error_bad_component(self):
         """
@@ -445,34 +536,28 @@
         test tries to change the owner of a component that does not
         exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('component chown bad_component changed_owner')
-        self.assertEqual(2, rv)
-        # We currently trigger a deprecation warning with py26 so we
-        # can currrently only verify that the end of the output string is
-        # correct
-        self.assertEqual(output.endswith(self.expected_results[test_name]), True)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_remove_ok(self):
         """
         Tests the 'component remove' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('component remove component1')
         rv, output = self._execute('component list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_component_remove_error_bad_component(self):
         """
         Tests the 'component remove' command in trac-admin.  This particular
         test tries to remove a component that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('component remove bad_component')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     # Ticket-type tests
 
@@ -482,21 +567,19 @@
         has no command arguments, it is hard to call it incorrectly.  As
         a result, there is only this one test.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('ticket_type list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_add_ok(self):
         """
         Tests the 'ticket_type add' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('ticket_type add new_type')
         rv, output = self._execute('ticket_type list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_add_error_already_exists(self):
         """
@@ -504,94 +587,85 @@
         test passes a ticket type that already exists and checks for an error
         message.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('ticket_type add defect')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_change_ok(self):
         """
         Tests the 'ticket_type change' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('ticket_type change defect bug')
         rv, output = self._execute('ticket_type list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_change_error_bad_type(self):
         """
         Tests the 'ticket_type change' command in trac-admin.  This particular
         test tries to change a priority that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('ticket_type change bad_type changed_type')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_change_error_bad_new_name(self):
         """
         Tests the 'ticket_type change' command in trac-admin.  This particular
         test tries to change a ticket type to another type that already exists.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('ticket_type change defect task')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_remove_ok(self):
         """
         Tests the 'ticket_type remove' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('ticket_type remove task')
         rv, output = self._execute('ticket_type list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_remove_error_bad_type(self):
         """
         Tests the 'ticket_type remove' command in trac-admin.  This particular
         test tries to remove a ticket type that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('ticket_type remove bad_type')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_order_down_ok(self):
         """
         Tests the 'ticket_type order' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('ticket_type order defect down')
         rv, output = self._execute('ticket_type list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_order_up_ok(self):
         """
         Tests the 'ticket_type order' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('ticket_type order enhancement up')
         rv, output = self._execute('ticket_type list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_ticket_type_order_error_bad_type(self):
         """
         Tests the 'priority order' command in trac-admin.  This particular
         test tries to reorder a priority that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('ticket_type order bad_type up')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     # Priority tests
 
@@ -601,33 +675,30 @@
         has no command arguments, it is hard to call it incorrectly.  As
         a result, there is only this one test.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('priority list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_add_ok(self):
         """
         Tests the 'priority add' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('priority add new_priority')
         rv, output = self._execute('priority list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_add_many_ok(self):
         """
         Tests adding more than 10 priority values.  This makes sure that
         ordering is preserved when adding more than 10 values.
         """
-        test_name = sys._getframe().f_code.co_name
         for i in xrange(11):
             self._execute('priority add p%s' % i)
         rv, output = self._execute('priority list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_add_error_already_exists(self):
         """
@@ -635,94 +706,85 @@
         test passes a priority name that already exists and checks for an
         error message.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('priority add blocker')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_change_ok(self):
         """
         Tests the 'priority change' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('priority change major normal')
         rv, output = self._execute('priority list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_change_error_bad_priority(self):
         """
         Tests the 'priority change' command in trac-admin.  This particular
         test tries to change a priority that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('priority change bad_priority changed_name')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_change_error_bad_new_name(self):
         """
         Tests the 'priority change' command in trac-admin.  This particular
         test tries to change a priority to a name that already exists.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('priority change major minor')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_remove_ok(self):
         """
         Tests the 'priority remove' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('priority remove major')
         rv, output = self._execute('priority list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_remove_error_bad_priority(self):
         """
         Tests the 'priority remove' command in trac-admin.  This particular
         test tries to remove a priority that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('priority remove bad_priority')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_order_down_ok(self):
         """
         Tests the 'priority order' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('priority order blocker down')
         rv, output = self._execute('priority list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_order_up_ok(self):
         """
         Tests the 'priority order' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('priority order critical up')
         rv, output = self._execute('priority list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_priority_order_error_bad_priority(self):
         """
         Tests the 'priority order' command in trac-admin.  This particular
         test tries to reorder a priority that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('priority remove bad_priority')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     # Severity tests
 
@@ -732,21 +794,19 @@
         has no command arguments, it is hard to call it incorrectly.  As
         a result, there is only this one test.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('severity list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_add_ok(self):
         """
         Tests the 'severity add' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('severity add new_severity')
         rv, output = self._execute('severity list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_add_error_already_exists(self):
         """
@@ -754,102 +814,93 @@
         test passes a severity name that already exists and checks for an
         error message.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('severity add blocker')
         rv, output = self._execute('severity add blocker')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_change_ok(self):
         """
         Tests the 'severity add' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('severity add critical')
         self._execute('severity change critical "end-of-the-world"')
         rv, output = self._execute('severity list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_change_error_bad_severity(self):
         """
         Tests the 'severity change' command in trac-admin.  This particular
         test tries to change a severity that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('severity change bad_severity changed_name')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_change_error_bad_new_name(self):
         """
         Tests the 'severity change' command in trac-admin.  This particular
         test tries to change a severity to a name that already exists.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('severity add major')
         self._execute('severity add critical')
         rv, output = self._execute('severity change critical major')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_remove_ok(self):
         """
         Tests the 'severity add' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('severity remove trivial')
         rv, output = self._execute('severity list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_remove_error_bad_severity(self):
         """
         Tests the 'severity remove' command in trac-admin.  This particular
         test tries to remove a severity that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('severity remove bad_severity')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_order_down_ok(self):
         """
         Tests the 'severity order' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('severity add foo')
         self._execute('severity add bar')
         self._execute('severity order foo down')
         rv, output = self._execute('severity list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_order_up_ok(self):
         """
         Tests the 'severity order' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('severity add foo')
         self._execute('severity add bar')
         self._execute('severity order bar up')
         rv, output = self._execute('severity list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_severity_order_error_bad_severity(self):
         """
         Tests the 'severity order' command in trac-admin.  This particular
         test tries to reorder a priority that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('severity remove bad_severity')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     # Version tests
 
@@ -859,21 +910,19 @@
         has no command arguments, it is hard to call it incorrectly.  As
         a result, there is only this one test.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('version list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_add_ok(self):
         """
         Tests the 'version add' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('version add 9.9 "%s"' % self._test_date)
         rv, output = self._execute('version list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_add_error_already_exists(self):
         """
@@ -881,86 +930,78 @@
         test passes a version name that already exists and checks for an
         error message.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('version add 1.0 "%s"' % self._test_date)
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_rename_ok(self):
         """
         Tests the 'version rename' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('version rename 1.0 9.9')
         rv, output = self._execute('version list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_rename_error_bad_version(self):
         """
         Tests the 'version rename' command in trac-admin.  This particular
         test tries to rename a version that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('version rename bad_version changed_name')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_time_ok(self):
         """
         Tests the 'version time' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('version time 2.0 "%s"' % self._test_date)
         rv, output = self._execute('version list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_time_unset_ok(self):
         """
         Tests the 'version time' command in trac-admin.  This particular
         test passes valid arguments for unsetting the date.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('version time 2.0 "%s"' % self._test_date)
         self._execute('version time 2.0 ""')
         rv, output = self._execute('version list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_time_error_bad_version(self):
         """
         Tests the 'version time' command in trac-admin.  This particular
         test tries to change the time on a version that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('version time bad_version "%s"'
                                    % self._test_date)
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_remove_ok(self):
         """
         Tests the 'version remove' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('version remove 1.0')
         rv, output = self._execute('version list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_version_remove_error_bad_version(self):
         """
         Tests the 'version remove' command in trac-admin.  This particular
         test tries to remove a version that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('version remove bad_version')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     # Milestone tests
 
@@ -970,33 +1011,30 @@
         has no command arguments, it is hard to call it incorrectly.  As
         a result, there is only this one test.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_add_ok(self):
         """
         Tests the 'milestone add' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('milestone add new_milestone "%s"' % self._test_date)
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_add_utf8_ok(self):
         """
         Tests the 'milestone add' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute(u'milestone add \xa9tat_final "%s"'  #\xc2\xa9
-                              % self._test_date)
+                      % self._test_date)
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_add_error_already_exists(self):
         """
@@ -1004,78 +1042,86 @@
         test passes a milestone name that already exists and checks for an
         error message.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('milestone add milestone1 "%s"'
                                    % self._test_date)
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
+
+    def test_milestone_add_invalid_date(self):
+        rv, output = self._execute('milestone add new_milestone <add>')
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output, {
+            'hint': self._datetime_format_hint,
+            'isohint': get_datetime_format_hint('iso8601')
+        })
 
     def test_milestone_rename_ok(self):
         """
         Tests the 'milestone rename' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('milestone rename milestone1 changed_milestone')
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_rename_error_bad_milestone(self):
         """
         Tests the 'milestone rename' command in trac-admin.  This particular
         test tries to rename a milestone that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('milestone rename bad_milestone changed_name')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_due_ok(self):
         """
         Tests the 'milestone due' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('milestone due milestone2 "%s"' % self._test_date)
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_due_unset_ok(self):
         """
         Tests the 'milestone due' command in trac-admin.  This particular
         test passes valid arguments for unsetting the due date.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('milestone due milestone2 "%s"' % self._test_date)
         self._execute('milestone due milestone2 ""')
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_due_error_bad_milestone(self):
         """
         Tests the 'milestone due' command in trac-admin.  This particular
         test tries to change the due date on a milestone that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('milestone due bad_milestone "%s"'
                                    % self._test_date)
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
+
+    def test_milestone_due_invalid_date(self):
+        rv, output = self._execute('milestone due milestone1 <due>')
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output, {
+            'hint': self._datetime_format_hint,
+            'isohint': get_datetime_format_hint('iso8601')
+        })
 
     def test_milestone_completed_ok(self):
         """
         Tests the 'milestone completed' command in trac-admin.  This particular
         test passes valid arguments and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
-
         self._execute('milestone completed milestone2 "%s"' % self._test_date)
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_completed_error_bad_milestone(self):
         """
@@ -1083,216 +1129,283 @@
         test tries to change the completed date on a milestone that does not
         exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('milestone completed bad_milestone "%s"'
                                    % self._test_date)
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
+
+    def test_milestone_completed_invalid_date(self):
+        rv, output = self._execute('milestone completed milestone1 <com>')
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output, {
+            'hint': self._datetime_format_hint,
+            'isohint': get_datetime_format_hint('iso8601')
+        })
 
     def test_milestone_remove_ok(self):
         """
         Tests the 'milestone remove' command in trac-admin.  This particular
         test passes a valid argument and checks for success.
         """
-        test_name = sys._getframe().f_code.co_name
         self._execute('milestone remove milestone3')
         rv, output = self._execute('milestone list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_milestone_remove_error_bad_milestone(self):
         """
         Tests the 'milestone remove' command in trac-admin.  This particular
         test tries to remove a milestone that does not exist.
         """
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('milestone remove bad_milestone')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
     def test_backslash_use_ok(self):
-        test_name = sys._getframe().f_code.co_name
         if self._admin.interactive:
             self._execute('version add \\')
         else:
             self._execute(r"version add '\'")
         rv, output = self._execute('version list')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_session_list_no_sessions(self):
-        test_name = sys._getframe().f_code.co_name
         rv, output = self._execute('session list authenticated')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_session_list_authenticated(self):
-        test_name = sys._getframe().f_code.co_name
         _prep_session_table(self.env)
         rv, output = self._execute('session list authenticated')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_session_list_anonymous(self):
-        test_name = sys._getframe().f_code.co_name
         _prep_session_table(self.env)
         rv, output = self._execute('session list anonymous')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_session_list_all(self):
-        test_name = sys._getframe().f_code.co_name
         _prep_session_table(self.env)
         if self._admin.interactive:
             rv, output = self._execute("session list *")
         else:
             rv, output = self._execute("session list '*'")
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_session_list_authenticated_sid(self):
-        test_name = sys._getframe().f_code.co_name
         _prep_session_table(self.env)
         rv, output = self._execute('session list name00')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_session_list_anonymous_sid(self):
-        test_name = sys._getframe().f_code.co_name
         _prep_session_table(self.env)
         rv, output = self._execute('session list name10:0')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
     def test_session_list_missing_sid(self):
-        test_name = sys._getframe().f_code.co_name
         _prep_session_table(self.env)
         rv, output = self._execute('session list thisdoesntexist')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_add_missing_sid(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_add_missing_sid(self):
         rv, output = self._execute('session add')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_add_duplicate_sid(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_add_duplicate_sid(self):
         _prep_session_table(self.env)
         rv, output = self._execute('session add name00')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_add_sid_all(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_add_sid_all(self):
         rv, output = self._execute('session add john John john@example.org')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list john')
-        self.assertEqual(self.expected_results[test_name]
-                         % {'today': format_date(None, console_date_format)},
-                         output)
+        self.assertExpectedResult(output, {
+            'today': format_date(None, console_date_format)
+        })
 
-    def  test_session_add_sid(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_add_sid(self):
         rv, output = self._execute('session add john')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list john')
-        self.assertEqual(self.expected_results[test_name]
-                         % {'today': format_date(None, console_date_format)},
-                         output)
+        self.assertExpectedResult(output, {
+            'today': format_date(None, console_date_format)
+        })
 
-    def  test_session_add_sid_name(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_add_sid_name(self):
         rv, output = self._execute('session add john John')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list john')
-        self.assertEqual(self.expected_results[test_name]
-                         % {'today': format_date(None, console_date_format)},
-                         output)
+        self.assertExpectedResult(output,  {
+            'today': format_date(None, console_date_format)
+        })
 
-    def  test_session_set_attr_name(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_set_attr_name(self):
         _prep_session_table(self.env)
         rv, output = self._execute('session set name name00 JOHN')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list name00')
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertExpectedResult(output)
 
-    def  test_session_set_attr_email(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_set_attr_email(self):
         _prep_session_table(self.env)
         rv, output = self._execute('session set email name00 JOHN@EXAMPLE.ORG')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list name00')
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertExpectedResult(output)
 
-    def  test_session_set_attr_missing_attr(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_set_attr_missing_attr(self):
         rv, output = self._execute('session set')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_set_attr_missing_value(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_set_attr_missing_value(self):
         rv, output = self._execute('session set name john')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_set_attr_missing_sid(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_set_attr_missing_sid(self):
         rv, output = self._execute('session set name')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_set_attr_nonexistent_sid(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_set_attr_nonexistent_sid(self):
         rv, output = self._execute('session set name john foo')
-        self.assertEqual(2, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_delete_sid(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_delete_sid(self):
         _prep_session_table(self.env)
         rv, output = self._execute('session delete name00')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list nam00')
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertExpectedResult(output)
 
-    def  test_session_delete_missing_params(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_delete_missing_params(self):
         rv, output = self._execute('session delete')
-        self.assertEqual(0, rv)
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertEqual(0, rv, output)
+        self.assertExpectedResult(output)
 
-    def  test_session_delete_anonymous(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_delete_anonymous(self):
         _prep_session_table(self.env)
         rv, output = self._execute('session delete anonymous')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list *')
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertExpectedResult(output)
 
     def test_session_delete_multiple_sids(self):
-        test_name = sys._getframe().f_code.co_name
         _prep_session_table(self.env)
         rv, output = self._execute('session delete name00 name01 name02 '
                                    'name03')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list *')
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertExpectedResult(output)
 
-    def  test_session_purge_age(self):
-        test_name = sys._getframe().f_code.co_name
+    def test_session_purge_age(self):
         _prep_session_table(self.env, spread_visits=True)
         rv, output = self._execute('session purge 20100112')
-        self.assertEqual(0, rv)
+        self.assertEqual(0, rv, output)
         rv, output = self._execute('session list *')
-        self.assertEqual(self.expected_results[test_name], output)
+        self.assertExpectedResult(output)
+
+    def test_session_purge_invalid_date(self):
+        rv, output = self._execute('session purge <purge>')
+        self.assertEqual(2, rv, output)
+        self.assertExpectedResult(output, {
+            'hint': self._datetime_format_hint,
+            'isohint': get_datetime_format_hint('iso8601')
+        })
+
+    def test_help_milestone_due(self):
+        doc = self._get_command_help('milestone', 'due')
+        self.assertIn(self._datetime_format_hint, doc)
+        self.assertIn(u'"YYYY-MM-DDThh:mm:ss±hh:mm"', doc)
+
+    def test_help_milestone_completed(self):
+        doc = self._get_command_help('milestone', 'completed')
+        self.assertIn(self._datetime_format_hint, doc)
+        self.assertIn(u'"YYYY-MM-DDThh:mm:ss±hh:mm"', doc)
+
+    def test_help_version_time(self):
+        doc = self._get_command_help('version', 'time')
+        self.assertIn(self._datetime_format_hint, doc)
+        self.assertIn(u'"YYYY-MM-DDThh:mm:ss±hh:mm"', doc)
+
+    def test_help_session_purge(self):
+        doc = self._get_command_help('session', 'purge')
+        self.assertIn(u'"YYYY-MM-DDThh:mm:ss±hh:mm"', doc)
+
+
+class TracadminNoEnvTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self._admin = TracAdmin()
+
+    def tearDown(self):
+        self._admin = None
+
+    def _execute(self, cmd, strip_trailing_space=True, input=None):
+        return execute_cmd(self._admin, cmd,
+                           strip_trailing_space=strip_trailing_space,
+                           input=input)
+
+    def test_help(self):
+        rv, output = self._execute('help')
+        output = output.splitlines()
+        self.assertEqual('', output[-3])
+        self.assertEqual('help     Show documentation', output[-2])
+        self.assertEqual('initenv  Create and initialize a new environment',
+                         output[-1])
+
+    def test_help_with_nocmd(self):
+        rv, output = self._execute('help nocmd')
+        output = output.splitlines()
+        self.assertEqual(["No documentation found for 'nocmd'. Use 'help' to "
+                          "see the list of commands."],
+                          output)
+
+
+class TracAdminHelpMacroTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(enable=['%s.UnicodeHelpCommand' %
+                                           self.__module__])
+
+    def tearDown(self):
+        self.env.reset_db()
+
+    def test_unicode_help(self):
+        unicode_help = u'Hélp text with unicöde charàcters'
+
+        class UnicodeHelpCommand(Component):
+            implements(IAdminCommandProvider)
+            def get_admin_commands(self):
+                yield ('unicode-help', '', unicode_help,
+                       None, self._cmd)
+            def _cmd(self):
+                pass
+
+        macro = TracAdminHelpMacro(self.env)
+        help = unicode(macro.expand_macro(None, None, 'unicode-help'))
+        self.assertTrue(unicode_help in help)
 
 
 def suite():
-    return unittest.makeSuite(TracadminTestCase, 'test')
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TracadminTestCase))
+    suite.addTest(unittest.makeSuite(TracadminNoEnvTestCase))
+    suite.addTest(unittest.makeSuite(TracAdminHelpMacroTestCase))
+    return suite
+
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/admin/tests/functional.py b/trac/trac/admin/tests/functional.py
index cff73b4..ec47957 100755
--- a/trac/trac/admin/tests/functional.py
+++ b/trac/trac/admin/tests/functional.py
@@ -1,6 +1,67 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-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/.
+
 from trac.tests.functional import *
-from trac.util.text import unicode_to_base64, unicode_from_base64
+from trac.util.text import unicode_to_base64
+
+
+class AuthorizationTestCaseSetup(FunctionalTwillTestCaseSetup):
+    def test_authorization(self, href, perms, h2_text):
+        """Check permissions required to access an administration panel. A
+        fine-grained permissions test will also be executed if ConfigObj is
+        installed.
+
+        :param href: the relative href of the administration panel
+        :param perms: list or tuple of permissions required to access
+                      the administration panel
+        :param h2_text: the body of the h2 heading on the administration
+                        panel"""
+        self._tester.go_to_front()
+        self._tester.logout()
+        self._tester.login('user')
+        if isinstance(perms, basestring):
+            perms = (perms, )
+
+        h2 = r'<h2>[ \t\n]*%s[ \t\n]*' \
+             r'( <span class="trac-count">\(\d+\)</span>)?[ \t\n]*</h2>'
+        try:
+            for perm in perms:
+                try:
+                    tc.go(href)
+                    tc.find("No administration panels available")
+                    self._testenv.grant_perm('user', perm)
+                    tc.go(href)
+                    tc.find(h2 % h2_text)
+                finally:
+                    self._testenv.revoke_perm('user', perm)
+                try:
+                    tc.go(href)
+                    tc.find("No administration panels available")
+                    self._testenv.enable_authz_permpolicy({
+                        href.strip('/').replace('/', ':', 1): {'user': perm},
+                    })
+                    tc.go(href)
+                    tc.find(h2 % h2_text)
+                except ImportError:
+                    pass
+                finally:
+                    self._testenv.disable_authz_permpolicy()
+        finally:
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('admin')
+
 
 class TestBasicSettings(FunctionalTwillTestCaseSetup):
     def runTest(self):
@@ -11,24 +72,36 @@
         tc.find('https://my.example.com/something')
 
 
+class TestBasicSettingsAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access Basic Settings panel."""
+        self.test_authorization('/admin/general/basics', 'TRAC_ADMIN',
+                                "Basic Settings")
+
+
 class TestLoggingNone(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Turn off logging."""
         # For now, we just check that it shows up.
-        self._tester.go_to_admin()
-        tc.follow('Logging')
+        self._tester.go_to_admin("Logging")
         tc.find('trac.log')
         tc.formvalue('modlog', 'log_type', 'none')
         tc.submit()
         tc.find('selected="selected">None</option')
 
 
+class TestLoggingAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access Logging panel."""
+        self.test_authorization('/admin/general/logging', 'TRAC_ADMIN',
+                                "Logging")
+
+
 class TestLoggingToFile(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Turn logging back on."""
         # For now, we just check that it shows up.
-        self._tester.go_to_admin()
-        tc.follow('Logging')
+        self._tester.go_to_admin("Logging")
         tc.find('trac.log')
         tc.formvalue('modlog', 'log_type', 'file')
         tc.formvalue('modlog', 'log_file', 'trac.log2')
@@ -43,8 +116,7 @@
     def runTest(self):
         """Setting logging back to normal."""
         # For now, we just check that it shows up.
-        self._tester.go_to_admin()
-        tc.follow('Logging')
+        self._tester.go_to_admin("Logging")
         tc.find('trac.log')
         tc.formvalue('modlog', 'log_file', 'trac.log')
         tc.formvalue('modlog', 'log_level', 'DEBUG')
@@ -54,11 +126,18 @@
         tc.find('selected="selected">DEBUG</option>')
 
 
+class TestPermissionsAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access Permissions panel."""
+        self.test_authorization('/admin/general/perm',
+                                ('PERMISSION_GRANT', 'PERMISSION_REVOKE'),
+                                "Manage Permissions and Groups")
+
+
 class TestCreatePermissionGroup(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Create a permissions group"""
-        self._tester.go_to_admin()
-        tc.follow('Permissions')
+        self._tester.go_to_admin("Permissions")
         tc.find('Manage Permissions')
         tc.formvalue('addperm', 'gp_subject', 'somegroup')
         tc.formvalue('addperm', 'action', 'REPORT_CREATE')
@@ -71,8 +150,7 @@
 class TestAddUserToGroup(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Add a user to a permissions group"""
-        self._tester.go_to_admin()
-        tc.follow('Permissions')
+        self._tester.go_to_admin("Permissions")
         tc.find('Manage Permissions')
         tc.formvalue('addsubj', 'sg_subject', 'authenticated')
         tc.formvalue('addsubj', 'sg_group', 'somegroup')
@@ -81,12 +159,35 @@
         somegroup = unicode_to_base64('somegroup')
         tc.find('%s:%s' % (authenticated, somegroup))
 
+        revoke_checkbox = '%s:%s' % (unicode_to_base64('anonymous'),
+                                     unicode_to_base64('PERMISSION_GRANT'))
+        tc.formvalue('addperm', 'gp_subject', 'anonymous')
+        tc.formvalue('addperm', 'action', 'PERMISSION_GRANT')
+        tc.submit()
+        tc.find(revoke_checkbox)
+        self._testenv.get_trac_environment().config.touch()
+        self._tester.logout()
+        self._tester.go_to_admin("Permissions")
+        try:
+            tc.formvalue('addsubj', 'sg_subject', 'someuser')
+            tc.formvalue('addsubj', 'sg_group', 'authenticated')
+            tc.submit()
+            tc.find("The subject someuser was not added to the "
+                    "group authenticated because the group has "
+                    "TICKET_CHGPROP permission and users cannot "
+                    "grant permissions they don't possess.")
+        finally:
+            self._tester.login('admin')
+            self._tester.go_to_admin("Permissions")
+            tc.formvalue('revokeform', 'sel', revoke_checkbox)
+            tc.submit()
+            tc.notfind(revoke_checkbox)
+
 
 class TestRemoveUserFromGroup(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Remove a user from a permissions group"""
-        self._tester.go_to_admin()
-        tc.follow('Permissions')
+        self._tester.go_to_admin("Permissions")
         tc.find('Manage Permissions')
         authenticated = unicode_to_base64('authenticated')
         somegroup = unicode_to_base64('somegroup')
@@ -99,8 +200,7 @@
 class TestRemovePermissionGroup(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Remove a permissions group"""
-        self._tester.go_to_admin()
-        tc.follow('Permissions')
+        self._tester.go_to_admin("Permissions")
         tc.find('Manage Permissions')
         somegroup = unicode_to_base64('somegroup')
         REPORT_CREATE = unicode_to_base64('REPORT_CREATE')
@@ -114,25 +214,147 @@
 class TestPluginSettings(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Check plugin settings."""
-        self._tester.go_to_admin()
-        tc.follow('Plugins')
+        self._tester.go_to_admin("Plugins")
         tc.find('Manage Plugins')
         tc.find('Install Plugin')
 
 
+class TestPluginsAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access Logging panel."""
+        self.test_authorization('/admin/general/plugin', 'TRAC_ADMIN',
+                                "Manage Plugins")
+
+
+class RegressionTestTicket10752(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/10752
+        Permissions on the web admin page should be greyed out when they
+        are no longer defined.
+        """
+        env = self._testenv.get_trac_environment()
+        try:
+            env.db_transaction("INSERT INTO permission VALUES (%s,%s)",
+                               ('anonymous', 'NOTDEFINED_PERMISSION'))
+        except env.db_exc.IntegrityError:
+            pass
+        env.config.touch()
+
+        self._tester.go_to_admin("Permissions")
+        tc.find('<span class="missing" '
+                'title="NOTDEFINED_PERMISSION is no longer defined">'
+                'NOTDEFINED_PERMISSION</span>')
+
+
+class RegressionTestTicket11069(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11069
+        The permissions list should only be populated with permissions that
+        the user can grant."""
+        self._tester.go_to_front()
+        self._tester.logout()
+        self._tester.login('user')
+        self._testenv.grant_perm('user', 'PERMISSION_GRANT')
+        env = self._testenv.get_trac_environment()
+        from trac.perm import PermissionSystem
+        user_perms = PermissionSystem(env).get_user_permissions('user')
+        all_actions = PermissionSystem(env).get_actions()
+        try:
+            self._tester.go_to_admin("Permissions")
+            for action in all_actions:
+                option = r"<option>%s</option>" % action
+                if action in user_perms and user_perms[action] is True:
+                    tc.find(option)
+                else:
+                    tc.notfind(option)
+        finally:
+            self._testenv.revoke_perm('user', 'PERMISSION_GRANT')
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('admin')
+
+
+class RegressionTestTicket11095(FunctionalTwillTestCaseSetup):
+    """Test for regression of http://trac.edgewall.org/ticket/11095
+    The permission is truncated if it overflows the available space (CSS)
+    and the full permission name is shown in the title on hover.
+    """
+    def runTest(self):
+        self._tester.go_to_admin("Permissions")
+        tc.find('<span title="MILESTONE_VIEW">MILESTONE_VIEW</span>')
+        tc.find('<span title="WIKI_VIEW">WIKI_VIEW</span>')
+
+
+class RegressionTestTicket11117(FunctionalTwillTestCaseSetup):
+    """Test for regression of http://trac.edgewall.org/ticket/11117
+    Hint should be shown on the Basic Settings admin panel when pytz is not
+    installed.
+    """
+    def runTest(self):
+        self._tester.go_to_admin("Basic Settings")
+        pytz_hint = "Install pytz for a complete list of timezones."
+        from trac.util.datefmt import pytz
+        if pytz is None:
+            tc.find(pytz_hint)
+        else:
+            tc.notfind(pytz_hint)
+
+
+class RegressionTestTicket11257(FunctionalTwillTestCaseSetup):
+    """Test for regression of http://trac.edgewall.org/ticket/11257
+    Hints should be shown on the Basic Settings admin panel when Babel is not
+    installed.
+    """
+    def runTest(self):
+        from trac.util.translation import get_available_locales, has_babel
+
+        babel_hint_lang = "Install Babel for extended language support."
+        babel_hint_date = "Install Babel for localized date formats."
+        catalog_hint = "Message catalogs have not been compiled."
+        language_select = '<select name="default_language">'
+        disabled_language_select = \
+            '<select name="default_language" disabled="disabled" ' \
+            'title="Translations are currently unavailable">'
+
+        self._tester.go_to_admin("Basic Settings")
+        if has_babel:
+            tc.notfind(babel_hint_lang)
+            tc.notfind(babel_hint_date)
+            if get_available_locales():
+                tc.find(language_select)
+                tc.notfind(catalog_hint)
+            else:
+                tc.find(disabled_language_select)
+                tc.find(catalog_hint)
+        else:
+            tc.find(disabled_language_select)
+            tc.find(babel_hint_lang)
+            tc.find(babel_hint_date)
+            tc.notfind(catalog_hint)
+
+
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
     suite.addTest(TestBasicSettings())
+    suite.addTest(TestBasicSettingsAuthorization())
     suite.addTest(TestLoggingNone())
+    suite.addTest(TestLoggingAuthorization())
     suite.addTest(TestLoggingToFile())
     suite.addTest(TestLoggingToFileNormal())
+    suite.addTest(TestPermissionsAuthorization())
     suite.addTest(TestCreatePermissionGroup())
     suite.addTest(TestAddUserToGroup())
     suite.addTest(TestRemoveUserFromGroup())
     suite.addTest(TestRemovePermissionGroup())
     suite.addTest(TestPluginSettings())
+    suite.addTest(TestPluginsAuthorization())
+    suite.addTest(RegressionTestTicket10752())
+    suite.addTest(RegressionTestTicket11069())
+    suite.addTest(RegressionTestTicket11095())
+    suite.addTest(RegressionTestTicket11117())
+    suite.addTest(RegressionTestTicket11257())
     return suite
 
 
diff --git a/trac/trac/admin/web_ui.py b/trac/trac/admin/web_ui.py
index 0cfb1d6..c8fb4df 100644
--- a/trac/trac/admin/web_ui.py
+++ b/trac/trac/admin/web_ui.py
@@ -22,11 +22,6 @@
 import re
 import shutil
 
-try:
-    from babel.core import Locale
-except ImportError:
-    Locale = None
-
 from genshi import HTML
 from genshi.builder import tag
 
@@ -34,10 +29,10 @@
 from trac.core import *
 from trac.loader import get_plugin_info, get_plugins_dir
 from trac.perm import PermissionSystem, IPermissionRequestor
-from trac.util.datefmt import all_timezones
+from trac.util.datefmt import all_timezones, pytz
 from trac.util.text import exception_to_unicode, \
-                            unicode_to_base64, unicode_from_base64
-from trac.util.translation import _, get_available_locales, ngettext
+                           unicode_to_base64, unicode_from_base64
+from trac.util.translation import _, Locale, get_available_locales, ngettext
 from trac.web import HTTPNotFound, IRequestHandler
 from trac.web.chrome import add_notice, add_stylesheet, \
                             add_warning, Chrome, INavigationContributor, \
@@ -76,8 +71,8 @@
         # admin panel is available
         panels, providers = self._get_panels(req)
         if panels:
-            yield 'mainnav', 'admin', tag.a(_('Admin'), href=req.href.admin(),
-                                            title=_('Administration'))
+            yield 'mainnav', 'admin', tag.a(_("Admin"), href=req.href.admin(),
+                                            title=_("Administration"))
 
     # IRequestHandler methods
 
@@ -93,7 +88,7 @@
     def process_request(self, req):
         panels, providers = self._get_panels(req)
         if not panels:
-            raise HTTPNotFound(_('No administration panels available'))
+            raise HTTPNotFound(_("No administration panels available"))
 
         def _panel_order(p1, p2):
             if p1[::2] == ('general', 'basics'):
@@ -116,14 +111,14 @@
         path_info = req.args.get('path_info')
         if not panel_id:
             try:
-                panel_id = filter(
-                            lambda panel: panel[0] == cat_id, panels)[0][2]
+                panel_id = \
+                    filter(lambda panel: panel[0] == cat_id, panels)[0][2]
             except IndexError:
-                raise HTTPNotFound(_('Unknown administration panel'))
+                raise HTTPNotFound(_("Unknown administration panel"))
 
         provider = providers.get((cat_id, panel_id), None)
         if not provider:
-            raise HTTPNotFound(_('Unknown administration panel'))
+            raise HTTPNotFound(_("Unknown administration panel"))
 
         if hasattr(provider, 'render_admin_panel'):
             template, data = provider.render_admin_panel(req, cat_id, panel_id,
@@ -201,14 +196,14 @@
     try:
         config.save()
         if notices is None:
-            notices = [_('Your changes have been saved.')]
+            notices = [_("Your changes have been saved.")]
         for notice in notices:
             add_notice(req, notice)
     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.'))
+        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 BasicsAdminPanel(Component):
@@ -218,19 +213,19 @@
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'TRAC_ADMIN' in req.perm:
-            yield ('general', _('General'), 'basics', _('Basic Settings'))
+        if 'TRAC_ADMIN' in req.perm('admin', 'general/basics'):
+            yield ('general', _("General"), 'basics', _("Basic Settings"))
 
     def render_admin_panel(self, req, cat, page, path_info):
-        req.perm.require('TRAC_ADMIN')
-
         if Locale:
-            locales = [Locale.parse(locale)
-                       for locale in  get_available_locales()]
-            languages = sorted((str(locale), locale.display_name)
-                               for locale in locales)
+            locale_ids = get_available_locales()
+            locales = [Locale.parse(locale) for locale in locale_ids]
+            # don't use str(locale) to prevent storing expanded locale
+            # identifier, see #11258
+            languages = sorted((id, locale.display_name)
+                               for id, locale in zip(locale_ids, locales))
         else:
-            locales, languages = [], []
+            locale_ids, locales, languages = [], [], []
 
         if req.method == 'POST':
             for option in ('name', 'url', 'descr'):
@@ -242,7 +237,7 @@
             self.config.set('trac', 'default_timezone', default_timezone)
 
             default_language = req.args.get('default_language')
-            if default_language not in locales:
+            if default_language not in locale_ids:
                 default_language = ''
             self.config.set('trac', 'default_language', default_language)
 
@@ -261,9 +256,11 @@
         data = {
             'default_timezone': default_timezone,
             'timezones': all_timezones,
+            'has_pytz': pytz is not None,
             'default_language': default_language.replace('-', '_'),
             'languages': languages,
             'default_date_format': default_date_format,
+            'has_babel': Locale is not None,
         }
         Chrome(self.env).add_textarea_grips(req)
         return 'admin_basics.html', data
@@ -276,7 +273,8 @@
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None):
+        if 'TRAC_ADMIN' in req.perm('admin', 'general/logging') and \
+                not getattr(self.env, 'parent', None):
             yield ('general', _('General'), 'logging', _('Logging'))
 
     def render_admin_panel(self, req, cat, page, path_info):
@@ -288,16 +286,18 @@
         log_dir = os.path.join(self.env.path, 'log')
 
         log_types = [
-            dict(name='none', label=_('None'), selected=log_type == 'none', disabled=False),
-            dict(name='stderr', label=_('Console'),
+            dict(name='none', label=_("None"),
+                 selected=log_type == 'none', disabled=False),
+            dict(name='stderr', label=_("Console"),
                  selected=log_type == 'stderr', disabled=False),
-            dict(name='file', label=_('File'), selected=log_type == 'file',
-                 disabled=False),
-            dict(name='syslog', label=_('Syslog'), disabled=os.name != 'posix',
-                 selected=log_type in ('unix', 'syslog')),
-            dict(name='eventlog', label=_('Windows event log'),
-                 disabled=os.name != 'nt',
-                 selected=log_type in ('winlog', 'eventlog', 'nteventlog')),
+            dict(name='file', label=_("File"),
+                 selected=log_type == 'file', disabled=False),
+            dict(name='syslog', label=_("Syslog"),
+                 selected=log_type in ('unix', 'syslog'),
+                 disabled=os.name != 'posix'),
+            dict(name='eventlog', label=_("Windows event log"),
+                 selected=log_type in ('winlog', 'eventlog', 'nteventlog'),
+                 disabled=os.name != 'nt'),
         ]
 
         log_levels = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG']
@@ -308,8 +308,8 @@
             new_type = req.args.get('log_type')
             if new_type not in [t['name'] for t in log_types]:
                 raise TracError(
-                    _('Unknown log type %(type)s', type=new_type),
-                    _('Invalid log type')
+                    _("Unknown log type %(type)s", type=new_type),
+                    _("Invalid log type")
                 )
             if new_type != log_type:
                 self.config.set('logging', 'log_type', new_type)
@@ -323,8 +323,8 @@
                 new_level = req.args.get('log_level')
                 if new_level not in log_levels:
                     raise TracError(
-                        _('Unknown log level %(level)s', level=new_level),
-                        _('Invalid log level'))
+                        _("Unknown log level %(level)s", level=new_level),
+                        _("Invalid log level"))
                 if new_level != log_level:
                     self.config.set('logging', 'log_level', new_level)
                     changed = True
@@ -337,8 +337,8 @@
                     changed = True
                     log_file = new_file
                 if not log_file:
-                    raise TracError(_('You must specify a log file'),
-                                    _('Missing field'))
+                    raise TracError(_("You must specify a log file"),
+                                    _("Missing field"))
             else:
                 self.config.remove('logging', 'log_file')
                 changed = True
@@ -366,8 +366,9 @@
 
     # IAdminPanelProvider methods
     def get_admin_panels(self, req):
-        if 'PERMISSION_GRANT' in req.perm or 'PERMISSION_REVOKE' in req.perm:
-            yield ('general', _('General'), 'perm', _('Permissions'))
+        perm = req.perm('admin', 'general/perm')
+        if 'PERMISSION_GRANT' in perm or 'PERMISSION_REVOKE' in perm:
+            yield ('general', _("General"), 'perm', _("Permissions"))
 
     def render_admin_panel(self, req, cat, page, path_info):
         perm = PermissionSystem(self.env)
@@ -380,51 +381,57 @@
             group = req.args.get('group', '').strip()
 
             if subject and subject.isupper() or \
-                   group and group.isupper():
-                raise TracError(_('All upper-cased tokens are reserved for '
-                                  'permission names'))
+                    group and group.isupper():
+                raise TracError(_("All upper-cased tokens are reserved for "
+                                  "permission names"))
 
             # Grant permission to subject
             if req.args.get('add') and subject and action:
-                req.perm.require('PERMISSION_GRANT')
+                req.perm('admin', 'general/perm').require('PERMISSION_GRANT')
                 if action not in all_actions:
-                    raise TracError(_('Unknown action'))
+                    raise TracError(_("Unknown action"))
                 req.perm.require(action)
                 if (subject, action) not in all_permissions:
                     perm.grant_permission(subject, action)
-                    add_notice(req, _('The subject %(subject)s has been '
-                                      'granted the permission %(action)s.',
+                    add_notice(req, _("The subject %(subject)s has been "
+                                      "granted the permission %(action)s.",
                                       subject=subject, action=action))
                     req.redirect(req.href.admin(cat, page))
                 else:
-                    add_warning(req, _('The permission %(action)s was already '
-                                       'granted to %(subject)s.',
+                    add_warning(req, _("The permission %(action)s was already "
+                                       "granted to %(subject)s.",
                                        action=action, subject=subject))
 
             # Add subject to group
             elif req.args.get('add') and subject and group:
-                req.perm.require('PERMISSION_GRANT')
+                req.perm('admin', 'general/perm').require('PERMISSION_GRANT')
                 for action in perm.get_user_permissions(group):
                     if not action in all_actions: # plugin disabled?
-                        self.env.log.warn("Adding %s to group %s: " \
-                            "Permission %s unavailable, skipping perm check." \
-                            % (subject, group, action))
+                        self.env.log.warn("Adding %s to group %s: "
+                            "Permission %s unavailable, skipping perm check.",
+                            subject, group, action)
                     else:
-                        req.perm.require(action)
+                        req.perm.require(action,
+                            message=_("The subject %(subject)s was not added "
+                                      "to the group %(group)s because the "
+                                      "group has %(perm)s permission and "
+                                      "users cannot grant permissions they "
+                                      "don't possess.", subject=subject,
+                                      group=group, perm=action))
                 if (subject, group) not in all_permissions:
                     perm.grant_permission(subject, group)
-                    add_notice(req, _('The subject %(subject)s has been added '
-                                      'to the group %(group)s.',
+                    add_notice(req, _("The subject %(subject)s has been added "
+                                      "to the group %(group)s.",
                                       subject=subject, group=group))
                     req.redirect(req.href.admin(cat, page))
                 else:
-                    add_warning(req, _('The subject %(subject)s was already '
-                                       'added to the group %(group)s.',
+                    add_warning(req, _("The subject %(subject)s was already "
+                                       "added to the group %(group)s.",
                                        subject=subject, group=group))
 
             # Remove permissions action
             elif req.args.get('remove') and req.args.get('sel'):
-                req.perm.require('PERMISSION_REVOKE')
+                req.perm('admin', 'general/perm').require('PERMISSION_REVOKE')
                 sel = req.args.get('sel')
                 sel = sel if isinstance(sel, list) else [sel]
                 for key in sel:
@@ -433,8 +440,8 @@
                     action = unicode_from_base64(action)
                     if (subject, action) in perm.get_all_permissions():
                         perm.revoke_permission(subject, action)
-                add_notice(req, _('The selected permissions have been '
-                                  'revoked.'))
+                add_notice(req, _("The selected permissions have been "
+                                  "revoked."))
                 req.redirect(req.href.admin(cat, page))
 
         perms = [perm for perm in all_permissions if perm[1].isupper()]
@@ -453,13 +460,14 @@
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'TRAC_ADMIN' in req.perm and not getattr(self.env, 'parent', None):
+        if 'TRAC_ADMIN' in req.perm('admin', 'general/plugin') and \
+                not getattr(self.env, 'parent', None):
             yield ('general', _('General'), 'plugin', _('Plugins'))
 
     def render_admin_panel(self, req, cat, page, path_info):
         if getattr(self.env, 'parent', None):
             raise PermissionError()
-        req.perm.require('TRAC_ADMIN')
+        req.perm('admin', 'general/plugin').require('TRAC_ADMIN')
 
         if req.method == 'POST':
             if 'install' in req.args:
@@ -469,7 +477,7 @@
             else:
                 self._do_update(req)
             anchor = ''
-            if req.args.has_key('plugin'):
+            if 'plugin' in req.args:
                 anchor = '#no%d' % (int(req.args.get('plugin')) + 1)
             req.redirect(req.href.admin(cat, page) + anchor)
 
@@ -479,26 +487,26 @@
 
     def _do_install(self, req):
         """Install a plugin."""
-        if not req.args.has_key('plugin_file'):
-            raise TracError(_('No file uploaded'))
+        if 'plugin_file' not in req.args:
+            raise TracError(_("No file uploaded"))
         upload = req.args['plugin_file']
         if isinstance(upload, unicode) or not upload.filename:
-            raise TracError(_('No file uploaded'))
+            raise TracError(_("No file uploaded"))
         plugin_filename = upload.filename.replace('\\', '/').replace(':', '/')
         plugin_filename = os.path.basename(plugin_filename)
         if not plugin_filename:
-            raise TracError(_('No file uploaded'))
+            raise TracError(_("No file uploaded"))
         if not plugin_filename.endswith('.egg') and \
                 not plugin_filename.endswith('.py'):
-            raise TracError(_('Uploaded file is not a Python source file or '
-                              'egg'))
+            raise TracError(_("Uploaded file is not a Python source file or "
+                              "egg"))
 
         target_path = os.path.join(self.env.path, 'plugins', plugin_filename)
         if os.path.isfile(target_path):
-            raise TracError(_('Plugin %(name)s already installed',
+            raise TracError(_("Plugin %(name)s already installed",
                               name=plugin_filename))
 
-        self.log.info('Installing plugin %s', plugin_filename)
+        self.log.info("Installing plugin %s", plugin_filename)
         flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL
         try:
             flags += os.O_BINARY
@@ -507,7 +515,7 @@
             pass
         with os.fdopen(os.open(target_path, flags, 0666), 'w') as target_file:
             shutil.copyfileobj(upload.file, target_file)
-            self.log.info('Plugin %s installed to %s', plugin_filename,
+            self.log.info("Plugin %s installed to %s", plugin_filename,
                           target_path)
         # TODO: Validate that the uploaded file is actually a valid Trac plugin
 
@@ -522,7 +530,7 @@
         plugin_path = os.path.join(self.env.path, 'plugins', plugin_filename)
         if not os.path.isfile(plugin_path):
             return
-        self.log.info('Uninstalling plugin %s', plugin_filename)
+        self.log.info("Uninstalling plugin %s", plugin_filename)
         os.remove(plugin_path)
 
         # Make the environment reset itself on the next request
@@ -543,8 +551,8 @@
             if is_enabled != must_enable:
                 self.config.set('components', component,
                                 'disabled' if is_enabled else 'enabled')
-                self.log.info('%sabling component %s',
-                              'Dis' if is_enabled else 'En', component)
+                self.log.info("%sabling component %s",
+                              "Dis" if is_enabled else "En", component)
                 if must_enable:
                     added.append(component)
                 else:
@@ -562,13 +570,13 @@
             removed.sort()
             notices = []
             if removed:
-                msg = ngettext('The following component has been disabled:',
-                               'The following components have been disabled:',
+                msg = ngettext("The following component has been disabled:",
+                               "The following components have been disabled:",
                                len(removed))
                 notices.append(tag(msg, make_list(removed)))
             if added:
-                msg = ngettext('The following component has been enabled:',
-                               'The following components have been enabled:',
+                msg = ngettext("The following component has been enabled:",
+                               "The following components have been enabled:",
                                len(added))
                 notices.append(tag(msg, make_list(added)))
 
@@ -581,7 +589,7 @@
             try:
                 return format_to_html(self.env, context, text)
             except Exception, e:
-                self.log.error('Unable to render component documentation: %s',
+                self.log.error("Unable to render component documentation: %s",
                                exception_to_unicode(e, traceback=True))
                 return tag.pre(text)
 
diff --git a/trac/trac/attachment.py b/trac/trac/attachment.py
index a0bf5ea..f4523d5 100644
--- a/trac/trac/attachment.py
+++ b/trac/trac/attachment.py
@@ -38,7 +38,7 @@
 from trac.perm import PermissionError, IPermissionPolicy
 from trac.resource import *
 from trac.search import search_to_sql, shorten_result
-from trac.util import content_disposition, get_reporter_id
+from trac.util import content_disposition, create_zipinfo, get_reporter_id
 from trac.util.compat import sha1
 from trac.util.datefmt import format_datetime, from_utimestamp, \
                               to_datetime, to_utimestamp, utc
@@ -93,8 +93,9 @@
         attachment. Therefore, a return value of ``[]`` means
         everything is OK."""
 
+
 class ILegacyAttachmentPolicyDelegate(Interface):
-    """Interface that can be used by plugins to seemlessly participate
+    """Interface that can be used by plugins to seamlessly participate
        to the legacy way of checking for attachment permissions.
 
        This should no longer be necessary once it becomes easier to
@@ -310,6 +311,12 @@
             t = to_datetime(t, utc)
         self.date = t
 
+        parent_resource = self.resource.parent
+        if not resource_exists(self.env, parent_resource):
+            raise ResourceNotFound(
+                _("%(parent)s doesn't exist, can't create attachment",
+                  parent=get_resource_name(self.env, parent_resource)))
+
         # Make sure the path to the attachment is inside the environment
         # attachments directory
         attachments_dir = os.path.join(os.path.normpath(self.env.path),
@@ -341,7 +348,6 @@
             listener.attachment_added(self)
         ResourceSystem(self.env).resource_created(self)
 
-
     @classmethod
     def select(cls, env, parent_realm, parent_id, db=None):
         """Iterator yielding all `Attachment` instances attached to
@@ -377,7 +383,8 @@
                 os.rmdir(attachment_dir)
             except OSError, e:
                 env.log.error("Can't delete attachment directory %s: %s",
-                    attachment_dir, exception_to_unicode(e, traceback=True))
+                              attachment_dir,
+                              exception_to_unicode(e, traceback=True))
 
     @classmethod
     def reparent_all(cls, env, parent_realm, parent_id, new_realm, new_id):
@@ -393,7 +400,8 @@
                 os.rmdir(attachment_dir)
             except OSError, e:
                 env.log.error("Can't delete attachment directory %s: %s",
-                    attachment_dir, exception_to_unicode(e, traceback=True))
+                              attachment_dir,
+                              exception_to_unicode(e, traceback=True))
 
     def open(self):
         path = self.path
@@ -436,8 +444,7 @@
     CHUNK_SIZE = 4096
 
     max_size = IntOption('attachment', 'max_size', 262144,
-        """Maximum allowed file size (in bytes) for ticket and wiki
-        attachments.""")
+        """Maximum allowed file size (in bytes) for attachments.""")
 
     max_zip_size = IntOption('attachment', 'max_zip_size', 2097152,
         """Maximum allowed total size (in bytes) for an attachment list to be
@@ -499,6 +506,10 @@
                 parent_id, filename = path[:last_slash], path[last_slash + 1:]
 
         parent = parent_realm(id=parent_id)
+        if not resource_exists(self.env, parent):
+            raise ResourceNotFound(
+                _("Parent resource %(parent)s doesn't exist",
+                  parent=get_resource_name(self.env, parent)))
 
         # Link the attachment page to parent resource
         parent_name = get_resource_name(self.env, parent)
@@ -695,10 +706,6 @@
     def _do_save(self, req, attachment):
         req.perm(attachment.resource).require('ATTACHMENT_CREATE')
         parent_resource = attachment.resource.parent
-        if not resource_exists(self.env, parent_resource):
-            raise ResourceNotFound(
-                _("%(parent)s doesn't exist, can't create attachment",
-                  parent=get_resource_name(self.env, parent_resource)))
 
         if 'cancel' in req.args:
             req.redirect(get_resource_url(self.env, parent_resource, req.href))
@@ -764,7 +771,7 @@
             try:
                 old_attachment = Attachment(self.env,
                                             attachment.resource(id=filename))
-                if not (req.authname and req.authname != 'anonymous' \
+                if not (req.authname and req.authname != 'anonymous'
                         and old_attachment.author == req.authname) \
                    and 'ATTACHMENT_DELETE' \
                                         not in req.perm(attachment.resource):
@@ -774,7 +781,7 @@
                         "attachments requires ATTACHMENT_DELETE permission.",
                         name=filename))
                 if (not attachment.description.strip() and
-                    old_attachment.description):
+                        old_attachment.description):
                     attachment.description = old_attachment.description
                 old_attachment.delete()
             except TracError:
@@ -806,7 +813,7 @@
     def _render_form(self, req, attachment):
         req.perm(attachment.resource).require('ATTACHMENT_CREATE')
         return {'mode': 'new', 'author': get_reporter_id(req),
-            'attachment': attachment, 'max_size': self.max_size}
+                'attachment': attachment, 'max_size': self.max_size}
 
     def _download_as_zip(self, req, parent, attachments=None):
         if attachments is None:
@@ -823,19 +830,14 @@
         req.send_header('Content-Disposition',
                         content_disposition('inline', filename))
 
-        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
+        from zipfile import ZipFile, ZIP_DEFLATED
 
         buf = StringIO()
         zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
         for attachment in attachments:
-            zipinfo = ZipInfo()
-            zipinfo.filename = attachment.filename.encode('utf-8')
-            zipinfo.flag_bits |= 0x800 # filename is encoded with utf-8
-            zipinfo.date_time = attachment.date.utctimetuple()[:6]
-            zipinfo.compress_type = ZIP_DEFLATED
-            if attachment.description:
-                zipinfo.comment = attachment.description.encode('utf-8')
-            zipinfo.external_attr = 0644 << 16L # needed since Python 2.5
+            zipinfo = create_zipinfo(attachment.filename,
+                                     mtime=attachment.date,
+                                     comment=attachment.description)
             try:
                 with attachment.open() as fd:
                     zipfile.writestr(zipinfo, fd.read())
@@ -999,7 +1001,7 @@
         else:
             for d in self.delegates:
                 decision = d.check_attachment_permission(action, username,
-                        resource, perm)
+                                                         resource, perm)
                 if decision is not None:
                     return decision
 
@@ -1113,4 +1115,3 @@
             finally:
                 if destination is not None:
                     output.close()
-
diff --git a/trac/trac/cache.py b/trac/trac/cache.py
index 711e2b8..8bba398 100644
--- a/trac/trac/cache.py
+++ b/trac/trac/cache.py
@@ -13,6 +13,8 @@
 
 from __future__ import with_statement
 
+import functools
+
 from .core import Component
 from .util import arity
 from .util.concurrency import ThreadLocal, threading
@@ -34,12 +36,15 @@
     return result
 
 
-class CachedPropertyBase(object):
-    """Base class for cached property descriptors"""
+class CachedPropertyBase(property):
+    """Base class for cached property descriptors.
+
+    :since 1.0.2: inherits from `property`.
+    """
 
     def __init__(self, retriever):
         self.retriever = retriever
-        self.__doc__ = retriever.__doc__
+        functools.update_wrapper(self, retriever)
 
     def make_key(self, cls):
         attr = self.retriever.__name__
diff --git a/trac/trac/config.py b/trac/trac/config.py
index 873ac64..7b233d5 100644
--- a/trac/trac/config.py
+++ b/trac/trac/config.py
@@ -18,12 +18,13 @@
 from copy import deepcopy
 import os.path
 
+from genshi.builder import tag
 from trac.admin import AdminCommandError, IAdminCommandProvider
 from trac.core import *
 from trac.util import AtomicFile, as_bool
-from trac.util.compat import cleandoc
+from trac.util.compat import cleandoc, wait_for_file_mtime_change
 from trac.util.text import printout, to_unicode, CRLF
-from trac.util.translation import _, N_
+from trac.util.translation import _, N_, tag_
 
 __all__ = ['Configuration', 'ConfigSection', 'Option', 'BoolOption',
            'IntOption', 'FloatOption', 'ListOption', 'ChoiceOption',
@@ -43,6 +44,12 @@
     """Exception raised when a value in the configuration file is not valid."""
     title = N_('Configuration Error')
 
+    def __init__(self, message=None, title=None, show_traceback=False):
+        if message is None:
+            message = _("Look in the Trac log for more information.")
+        super(ConfigurationError, self).__init__(message, title,
+                                                 show_traceback)
+
 
 class Configuration(object):
     """Thin layer over `ConfigParser` from the Python standard library.
@@ -234,10 +241,12 @@
 
         # At this point, all the strings in `sections` are UTF-8 encoded `str`
         try:
+            wait_for_file_mtime_change(self.filename)
             with AtomicFile(self.filename, 'w') as fileobj:
                 fileobj.write('# -*- coding: utf-8 -*-\n\n')
-                for section, options in sections:
-                    fileobj.write('[%s]\n' % section)
+                for section_str, options in sections:
+                    fileobj.write('[%s]\n' % section_str)
+                    section = to_unicode(section_str)
                     for key_str, val_str in options:
                         if to_unicode(key_str) in self[section].overridden:
                             fileobj.write('# %s = <inherited>\n' % key_str)
@@ -287,7 +296,8 @@
 
     def touch(self):
         if self.filename and os.path.isfile(self.filename) \
-           and os.access(self.filename, os.W_OK):
+                and os.access(self.filename, os.W_OK):
+            wait_for_file_mtime_change(self.filename)
             os.utime(self.filename, None)
 
     def set_defaults(self, compmgr=None):
@@ -296,14 +306,15 @@
 
         Values already set in the configuration are not overridden.
         """
-        for section, default_options in self.defaults(compmgr).items():
-            for name, value in default_options.items():
-                if not self.parser.has_option(_to_utf8(section),
-                                              _to_utf8(name)):
-                    if any(parent[section].contains(name, defaults=False)
-                           for parent in self.parents):
-                        value = None
-                    self.set(section, name, value)
+        for (section, name), option in Option.get_registry(compmgr).items():
+            if not self.parser.has_option(_to_utf8(section), _to_utf8(name)):
+                value = option.default
+                if any(parent[section].contains(name, defaults=False)
+                       for parent in self.parents):
+                    value = None
+                if value is not None:
+                    value = option.dumps(value)
+                self.set(section, name, value)
 
 
 class Section(object):
@@ -325,7 +336,7 @@
         for parent in self.config.parents:
             if parent[self.name].contains(key, defaults=False):
                 return True
-        return defaults and Option.registry.has_key((self.name, key))
+        return defaults and (self.name, key) in Option.registry
 
     __contains__ = contains
 
@@ -608,27 +619,42 @@
             return value
 
     def __set__(self, instance, value):
-        raise AttributeError, 'can\'t set attribute'
+        raise AttributeError(_("Setting attribute is not allowed."))
 
     def __repr__(self):
         return '<%s [%s] "%s">' % (self.__class__.__name__, self.section,
                                    self.name)
 
+    def dumps(self, value):
+        """Return the value as a string to write to a trac.ini file"""
+        if value is None:
+            return ''
+        if value is True:
+            return 'enabled'
+        if value is False:
+            return 'disabled'
+        if isinstance(value, unicode):
+            return value
+        return to_unicode(value)
+
 
 class BoolOption(Option):
     """Descriptor for boolean configuration options."""
+
     def accessor(self, section, name, default):
         return section.getbool(name, default)
 
 
 class IntOption(Option):
     """Descriptor for integer configuration options."""
+
     def accessor(self, section, name, default):
         return section.getint(name, default)
 
 
 class FloatOption(Option):
     """Descriptor for float configuration options."""
+
     def accessor(self, section, name, default):
         return section.getfloat(name, default)
 
@@ -647,6 +673,11 @@
     def accessor(self, section, name, default):
         return section.getlist(name, default, self.sep, self.keep_empty)
 
+    def dumps(self, value):
+        if isinstance(value, (list, tuple)):
+            return self.sep.join(Option.dumps(self, v) or '' for v in value)
+        return Option.dumps(self, value)
+
 
 class ChoiceOption(Option):
     """Descriptor for configuration options providing a choice among a list
@@ -678,11 +709,15 @@
     Relative paths are resolved to absolute paths using the directory
     containing the configuration file as the reference.
     """
+
     def accessor(self, section, name, default):
         return section.getpath(name, default)
 
 
 class ExtensionOption(Option):
+    """Name of a component implementing `interface`. Raises a
+    `ConfigurationError` if the component cannot be found in the list of
+    active components implementing the interface."""
 
     def __init__(self, section, name, interface, default=None, doc='',
                  doc_domain='tracini'):
@@ -696,11 +731,14 @@
         for impl in self.xtnpt.extensions(instance):
             if impl.__class__.__name__ == value:
                 return impl
-        raise AttributeError('Cannot find an implementation of the "%s" '
-                             'interface named "%s".  Please update the option '
-                             '%s.%s in trac.ini.'
-                             % (self.xtnpt.interface.__name__, value,
-                                self.section, self.name))
+        raise ConfigurationError(
+            tag_("Cannot find an implementation of the %(interface)s "
+                 "interface named %(implementation)s. Please check "
+                 "that the Component is enabled or update the option "
+                 "%(option)s in trac.ini.",
+                 interface=tag.tt(self.xtnpt.interface.__name__),
+                 implementation=tag.tt(value),
+                 option=tag.tt("[%s] %s" % (self.section, self.name))))
 
 
 class OrderedExtensionsOption(ListOption):
@@ -722,9 +760,23 @@
             return self
         order = ListOption.__get__(self, instance, owner)
         components = []
+        implementing_classes = []
         for impl in self.xtnpt.extensions(instance):
+            implementing_classes.append(impl.__class__.__name__)
             if self.include_missing or impl.__class__.__name__ in order:
                 components.append(impl)
+        not_found = sorted(set(order) - set(implementing_classes))
+        if not_found:
+            raise ConfigurationError(
+                tag_("Cannot find implementation(s) of the %(interface)s "
+                     "interface named %(implementation)s. Please check "
+                     "that the Component is enabled or update the option "
+                     "%(option)s in trac.ini.",
+                     interface=tag.tt(self.xtnpt.interface.__name__),
+                     implementation=tag(
+                         (', ' if idx != 0 else None, tag.tt(impl))
+                         for idx, impl in enumerate(not_found)),
+                     option=tag.tt("[%s] %s" % (self.section, self.name))))
 
         def compare(x, y):
             x, y = x.__class__.__name__, y.__class__.__name__
diff --git a/trac/trac/core.py b/trac/trac/core.py
index 6ad0f9b..809f146 100644
--- a/trac/trac/core.py
+++ b/trac/trac/core.py
@@ -141,6 +141,8 @@
             return self
 
         # The normal case where the component is not also the component manager
+        assert len(args) >= 1 and isinstance(args[0], ComponentManager), \
+               "First argument must be a ComponentManager instance"
         compmgr = args[0]
         self = compmgr.components.get(cls)
         # Note that this check is racy, we intentionally don't use a
@@ -204,14 +206,14 @@
         """Activate the component instance for the given class, or
         return the existing instance if the component has already been
         activated.
+
+        Note that `ComponentManager` components can't be activated
+        that way.
         """
         if not self.is_enabled(cls):
             return None
         component = self.components.get(cls)
-
-        # Leave other manager components out of extension point lists
-        # see bh:comment:5:ticket:438 and ticket:11121
-        if not component and not issubclass(cls, ComponentManager) :
+        if not component and not issubclass(cls, ComponentManager):
             if cls not in ComponentMeta._components:
                 raise TracError('Component "%s" not registered' % cls.__name__)
             try:
diff --git a/trac/trac/db/__init__.py b/trac/trac/db/__init__.py
index 8b01242..54cef1d 100644
--- a/trac/trac/db/__init__.py
+++ b/trac/trac/db/__init__.py
@@ -1,2 +1,15 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
+
 from trac.db.api import *
 from trac.db.schema import *
diff --git a/trac/trac/db/api.py b/trac/trac/db/api.py
index 7a8c9c0..66da58e 100644
--- a/trac/trac/db/api.py
+++ b/trac/trac/db/api.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2005-2009 Edgewall Software
+# Copyright (C)2005-2014 Edgewall Software
 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
 #
@@ -22,6 +22,7 @@
 
 from trac.config import BoolOption, IntOption, Option
 from trac.core import *
+from trac.db.schema import Table
 from trac.util.concurrency import ThreadLocal
 from trac.util.text import unicode_passwd
 from trac.util.translation import _
@@ -61,9 +62,9 @@
 
     :deprecated: This decorator is in turn deprecated in favor of
                  context managers now that python 2.4 support has been
-                 dropped. Use instead the new context manager,
-                 `QueryContextManager` and
-                 `TransactionContextManager`, which makes for much
+                 dropped. It will be removed in Trac 1.3.1. Use instead
+                 the new context managers, `QueryContextManager` and
+                 `TransactionContextManager`, which make for much
                  simpler to write code:
 
     >>> def api_method(p1, p2):
@@ -250,10 +251,35 @@
         args['schema'] = schema
         connector.init_db(**args)
 
+    def create_tables(self, schema):
+        """Create the specified tables.
+
+        :param schema: an iterable of table objects.
+
+        :since: version 1.0.2
+        """
+        connector = self.get_connector()[0]
+        with self.env.db_transaction as db:
+            for table in schema:
+                for sql in connector.to_sql(table):
+                    db(sql)
+
+    def drop_tables(self, schema):
+        """Drop the specified tables.
+
+        :param schema: an iterable of `Table` objects or table names.
+
+        :since: version 1.0.2
+        """
+        with self.env.db_transaction as db:
+            for table in schema:
+                table_name = table.name if isinstance(table, Table) else table
+                db.drop_table(table_name)
+
     def get_connection(self, readonly=False):
         """Get a database connection from the pool.
 
-        If `readonly` is `True`, the returned connection will purposedly
+        If `readonly` is `True`, the returned connection will purposely
         lack the `rollback` and `commit` methods.
         """
         if not self._cnx_pool:
@@ -287,7 +313,7 @@
                 backup_dir = os.path.join(self.env.path, backup_dir)
             db_str = self.config.get('trac', 'database')
             db_name, db_path = db_str.split(":", 1)
-            dest_name = '%s.%i.%d.bak' % (db_name, self.env.get_version(),
+            dest_name = '%s.%i.%d.bak' % (db_name, self.env.database_version,
                                           int(time.time()))
             dest = os.path.join(backup_dir, dest_name)
         else:
diff --git a/trac/trac/db/mysql_backend.py b/trac/trac/db/mysql_backend.py
index ca93b11..367e3ec 100644
--- a/trac/trac/db/mysql_backend.py
+++ b/trac/trac/db/mysql_backend.py
@@ -14,14 +14,19 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://trac.edgewall.org/log/.
 
-import os, re, types
+import os
+import re
+import sys
+import types
 
 from genshi.core import Markup
 
 from trac.core import *
 from trac.config import Option
-from trac.db.api import IDatabaseConnector, _parse_db_str
+from trac.db.api import DatabaseManager, IDatabaseConnector, _parse_db_str, \
+                        get_column_names
 from trac.db.util import ConnectionWrapper, IterableCursor
+from trac.env import IEnvironmentSetupParticipant
 from trac.util import as_int, get_pkginfo
 from trac.util.compat import close_fds
 from trac.util.text import exception_to_unicode, to_unicode
@@ -73,7 +78,7 @@
      * `read_default_group`: Configuration group to use from the default file
      * `unix_socket`: Use a Unix socket at the given path to connect
     """
-    implements(IDatabaseConnector)
+    implements(IDatabaseConnector, IEnvironmentSetupParticipant)
 
     mysqldump_path = Option('trac', 'mysqldump_path', 'mysqldump',
         """Location of mysqldump for MySQL database backups""")
@@ -109,17 +114,29 @@
                 host=None, port=None, params={}):
         cnx = self.get_connection(path, log, user, password, host, port,
                                   params)
+        self._verify_variables(cnx)
+        utf8_size = self._utf8_size(cnx)
         cursor = cnx.cursor()
-        utf8_size = {'utf8': 3, 'utf8mb4': 4}.get(cnx.charset)
         if schema is None:
             from trac.db_default import schema
         for table in schema:
             for stmt in self.to_sql(table, utf8_size=utf8_size):
                 self.log.debug(stmt)
                 cursor.execute(stmt)
+        self._verify_table_status(cnx)
         cnx.commit()
 
-    def _collist(self, table, columns, utf8_size=3):
+    def _utf8_size(self, cnx):
+        if cnx is None:
+            connector, args = DatabaseManager(self.env).get_connector()
+            cnx = connector.get_connection(**args)
+            charset = cnx.charset
+            cnx.close()
+        else:
+            charset = cnx.charset
+        return 4 if charset == 'utf8mb4' else 3
+
+    def _collist(self, table, columns, utf8_size):
         """Take a list of columns and impose limits on each so that indexing
         works properly.
 
@@ -148,7 +165,9 @@
             cols.append(name)
         return ','.join(cols)
 
-    def to_sql(self, table, utf8_size=3):
+    def to_sql(self, table, utf8_size=None):
+        if utf8_size is None:
+            utf8_size = self._utf8_size(None)
         sql = ['CREATE TABLE %s (' % table.name]
         coldefs = []
         for column in table.columns:
@@ -235,6 +254,83 @@
             raise TracError(_("No destination file created"))
         return dest_file
 
+    # IEnvironmentSetupParticipant methods
+
+    def environment_created(self):
+        pass
+
+    def environment_needs_upgrade(self, db):
+        if getattr(self, 'required', False):
+            self._verify_table_status(db)
+            self._verify_variables(db)
+        return False
+
+    def upgrade_environment(self, db):
+        pass
+
+    UNSUPPORTED_ENGINES = ('MyISAM', 'EXAMPLE', 'ARCHIVE', 'CSV', 'ISAM')
+
+    def _verify_table_status(self, db):
+        from trac.db_default import schema
+        tables = [t.name for t in schema]
+        cursor = db.cursor()
+        cursor.execute("SHOW TABLE STATUS WHERE name IN (%s)" %
+                       ','.join(('%s',) * len(tables)),
+                       tables)
+        cols = get_column_names(cursor)
+        rows = [dict(zip(cols, row)) for row in cursor]
+
+        engines = [row['Name'] for row in rows
+                               if row['Engine'] in self.UNSUPPORTED_ENGINES]
+        if engines:
+            raise TracError(_(
+                "All tables must be created as InnoDB or NDB storage engine "
+                "to support transactions. The following tables have been "
+                "created as storage engine which doesn't support "
+                "transactions: %(tables)s", tables=', '.join(engines)))
+
+        non_utf8bin = [row['Name'] for row in rows
+                       if row['Collation'] not in ('utf8_bin', 'utf8mb4_bin',
+                                                   None)]
+        if non_utf8bin:
+            raise TracError(_("All tables must be created with utf8_bin or "
+                              "utf8mb4_bin as collation. The following tables "
+                              "don't have the collations: %(tables)s",
+                              tables=', '.join(non_utf8bin)))
+
+    SUPPORTED_COLLATIONS = (('utf8', 'utf8_bin'), ('utf8mb4', 'utf8mb4_bin'))
+
+    def _verify_variables(self, db):
+        cursor = db.cursor()
+        cursor.execute("SHOW VARIABLES WHERE variable_name IN ("
+                       "'default_storage_engine','storage_engine',"
+                       "'default_tmp_storage_engine',"
+                       "'character_set_database','collation_database')")
+        vars = dict((row[0].lower(), row[1]) for row in cursor)
+
+        engine = vars.get('default_storage_engine') or \
+                 vars.get('storage_engine')
+        if engine in self.UNSUPPORTED_ENGINES:
+            raise TracError(_("The current storage engine is %(engine)s. "
+                              "It must be InnoDB or NDB storage engine to "
+                              "support transactions.", engine=engine))
+
+        tmp_engine = vars.get('default_tmp_storage_engine')
+        if tmp_engine in self.UNSUPPORTED_ENGINES:
+            raise TracError(_("The current storage engine for TEMPORARY "
+                              "tables is %(engine)s. It must be InnoDB or NDB "
+                              "storage engine to support transactions.",
+                              engine=tmp_engine))
+
+        charset = vars['character_set_database']
+        collation = vars['collation_database']
+        if (charset, collation) not in self.SUPPORTED_COLLATIONS:
+            raise TracError(_(
+                "The charset and collation of database are '%(charset)s' and "
+                "'%(collation)s'. The database must be created with one of "
+                "%(supported)s.", charset=charset, collation=collation,
+                supported=repr(self.SUPPORTED_COLLATIONS)))
+
 
 class MySQLConnection(ConnectionWrapper):
     """Connection wrapper for MySQL."""
@@ -251,16 +347,21 @@
             port = 3306
         opts = {}
         for name, value in params.iteritems():
-            if name in ('init_command', 'read_default_file',
-                        'read_default_group', 'unix_socket'):
-                opts[name] = value
+            key = name.encode('utf-8')
+            if name == 'read_default_group':
+                opts[key] = value
+            elif name == 'init_command':
+                opts[key] = value.encode('utf-8')
+            elif name in ('read_default_file', 'unix_socket'):
+                opts[key] = value.encode(sys.getfilesystemencoding())
             elif name in ('compress', 'named_pipe'):
-                opts[name] = as_int(value, 0)
+                opts[key] = as_int(value, 0)
             else:
                 self.log.warning("Invalid connection string parameter '%s'",
                                  name)
         cnx = MySQLdb.connect(db=path, user=user, passwd=password, host=host,
                               port=port, charset='utf8', **opts)
+        self.schema = path
         if hasattr(cnx, 'encoders'):
             # 'encoders' undocumented but present since 1.2.1 (r422)
             cnx.encoders[Markup] = cnx.encoders[types.UnicodeType]
@@ -274,33 +375,8 @@
         ConnectionWrapper.__init__(self, cnx, log)
         self._is_closed = False
 
-    def cast(self, column, type):
-        if type == 'int' or type == 'int64':
-            type = 'signed'
-        elif type == 'text':
-            type = 'char'
-        return 'CAST(%s AS %s)' % (column, type)
-
-    def concat(self, *args):
-        return 'concat(%s)' % ', '.join(args)
-
-    def like(self):
-        """Return a case-insensitive LIKE clause."""
-        return "LIKE %%s COLLATE %s_general_ci ESCAPE '/'" % self.charset
-
-    def like_escape(self, text):
-        return _like_escape_re.sub(r'/\1', text)
-
-    def quote(self, identifier):
-        """Return the quoted identifier."""
-        return "`%s`" % identifier.replace('`', '``')
-
-    def get_last_id(self, cursor, table, column='id'):
-        return cursor.lastrowid
-
-    def update_sequence(self, cursor, table, column='id'):
-        # MySQL handles sequence updates automagically
-        pass
+    def cursor(self):
+        return IterableCursor(MySQLUnicodeCursor(self.cnx), self.log)
 
     def rollback(self):
         self.cnx.ping()
@@ -317,5 +393,56 @@
                 pass # this error would mean it's already closed.  So, ignore
             self._is_closed = True
 
-    def cursor(self):
-        return IterableCursor(MySQLUnicodeCursor(self.cnx), self.log)
+    def cast(self, column, type):
+        if type == 'int' or type == 'int64':
+            type = 'signed'
+        elif type == 'text':
+            type = 'char'
+        return 'CAST(%s AS %s)' % (column, type)
+
+    def concat(self, *args):
+        return 'concat(%s)' % ', '.join(args)
+
+    def drop_table(self, table):
+        cursor = MySQLdb.cursors.Cursor(self.cnx)
+        cursor._defer_warnings = True  # ignore "Warning: Unknown table ..."
+        cursor.execute("DROP TABLE IF EXISTS " + self.quote(table))
+
+    def get_column_names(self, table):
+        rows = self.execute("""
+            SELECT column_name FROM information_schema.columns
+            WHERE table_schema=%s AND table_name=%s
+            """, (self.schema, table))
+        return [row[0] for row in rows]
+
+    def get_last_id(self, cursor, table, column='id'):
+        return cursor.lastrowid
+
+    def get_table_names(self):
+        rows = self.execute("""
+            SELECT table_name FROM information_schema.tables
+            WHERE table_schema=%s""", (self.schema,))
+        return [row[0] for row in rows]
+
+    def like(self):
+        """Return a case-insensitive LIKE clause."""
+        return "LIKE %%s COLLATE %s_general_ci ESCAPE '/'" % self.charset
+
+    def like_escape(self, text):
+        return _like_escape_re.sub(r'/\1', text)
+
+    def prefix_match(self):
+        """Return a case sensitive prefix-matching operator."""
+        return "LIKE %s ESCAPE '/'"
+
+    def prefix_match_value(self, prefix):
+        """Return a value for case sensitive prefix-matching operator."""
+        return self.like_escape(prefix) + '%'
+
+    def quote(self, identifier):
+        """Return the quoted identifier."""
+        return "`%s`" % identifier.replace('`', '``')
+
+    def update_sequence(self, cursor, table, column='id'):
+        # MySQL handles sequence updates automagically
+        pass
diff --git a/trac/trac/db/pool.py b/trac/trac/db/pool.py
index 39962fe..60c5488 100644
--- a/trac/trac/db/pool.py
+++ b/trac/trac/db/pool.py
@@ -26,7 +26,7 @@
 from trac.util.translation import _
 
 
-class TimeoutError(Exception):
+class TimeoutError(TracError):
     """Exception raised by the connection pool when no connection has become
     available after a given timeout."""
 
@@ -93,7 +93,7 @@
         deferred = num == 1 and isinstance(cnx, tuple)
         err = None
         if deferred:
-            # Potentially lenghty operations must be done without lock held
+            # Potentially lengthy operations must be done without lock held
             op, cnx = cnx
             try:
                 if op == 'ping':
@@ -214,4 +214,3 @@
 
     def shutdown(self, tid=None):
         _backend.shutdown(tid)
-
diff --git a/trac/trac/db/postgres_backend.py b/trac/trac/db/postgres_backend.py
index 9f36448..6e2dd12 100644
--- a/trac/trac/db/postgres_backend.py
+++ b/trac/trac/db/postgres_backend.py
@@ -22,7 +22,7 @@
 from trac.config import Option
 from trac.db.api import IDatabaseConnector, _parse_db_str
 from trac.db.util import ConnectionWrapper, IterableCursor
-from trac.util import get_pkginfo
+from trac.util import get_pkginfo, lazy
 from trac.util.compat import close_fds
 from trac.util.text import empty, exception_to_unicode, to_unicode
 from trac.util.translation import _
@@ -231,6 +231,9 @@
             cnx.rollback()
         ConnectionWrapper.__init__(self, cnx, log)
 
+    def cursor(self):
+        return IterableCursor(self.cnx.cursor(), self.log)
+
     def cast(self, column, type):
         # Temporary hack needed for the union of selects in the search module
         return 'CAST(%s AS %s)' % (column, _type_map.get(type, type))
@@ -238,6 +241,37 @@
     def concat(self, *args):
         return '||'.join(args)
 
+    def drop_table(self, table):
+        if (self._version or '').startswith(('8.0.', '8.1.')):
+            cursor = self.cursor()
+            cursor.execute("""SELECT table_name FROM information_schema.tables
+                              WHERE table_schema=current_schema()
+                              AND table_name=%s""", (table,))
+            for row in cursor:
+                if row[0] == table:
+                    self.execute("DROP TABLE " + self.quote(table))
+                    break
+        else:
+            self.execute("DROP TABLE IF EXISTS " + self.quote(table))
+
+    def get_column_names(self, table):
+        rows = self.execute("""
+            SELECT column_name FROM information_schema.columns
+            WHERE table_schema=%s AND table_name=%s
+            """, (self.schema, table))
+        return [row[0] for row in rows]
+
+    def get_last_id(self, cursor, table, column='id'):
+        cursor.execute("SELECT CURRVAL(%s)",
+                       (self.quote(self._sequence_name(table, column)),))
+        return cursor.fetchone()[0]
+
+    def get_table_names(self):
+        rows = self.execute("""
+            SELECT table_name FROM information_schema.tables
+            WHERE table_schema=%s""", (self.schema,))
+        return [row[0] for row in rows]
+
     def like(self):
         """Return a case-insensitive LIKE clause."""
         return "ILIKE %s ESCAPE '/'"
@@ -245,19 +279,31 @@
     def like_escape(self, text):
         return _like_escape_re.sub(r'/\1', text)
 
+    def prefix_match(self):
+        """Return a case sensitive prefix-matching operator."""
+        return "LIKE %s ESCAPE '/'"
+
+    def prefix_match_value(self, prefix):
+        """Return a value for case sensitive prefix-matching operator."""
+        return self.like_escape(prefix) + '%'
+
     def quote(self, identifier):
         """Return the quoted identifier."""
         return '"%s"' % identifier.replace('"', '""')
 
-    def get_last_id(self, cursor, table, column='id'):
-        cursor.execute("""SELECT CURRVAL('"%s_%s_seq"')""" % (table, column))
-        return cursor.fetchone()[0]
-
     def update_sequence(self, cursor, table, column='id'):
-        cursor.execute("""
-            SELECT setval('"%s_%s_seq"', (SELECT MAX(%s) FROM %s))
-            """ % (table, column, column, table))
+        cursor.execute("SELECT SETVAL(%%s, (SELECT MAX(%s) FROM %s))"
+                       % (self.quote(column), self.quote(table)),
+                       (self.quote(self._sequence_name(table, column)),))
 
-    def cursor(self):
-        return IterableCursor(self.cnx.cursor(), self.log)
+    def _sequence_name(self, table, column):
+        return '%s_%s_seq' % (table, column)
 
+    @lazy
+    def _version(self):
+        cursor = self.cursor()
+        cursor.execute('SELECT version()')
+        for version, in cursor:
+            # retrieve "8.1.23" from "PostgreSQL 8.1.23 on ...."
+            if version.startswith('PostgreSQL '):
+                return version.split(' ', 2)[1]
diff --git a/trac/trac/db/sqlite_backend.py b/trac/trac/db/sqlite_backend.py
index 2cd23eb..7b9b139 100644
--- a/trac/trac/db/sqlite_backend.py
+++ b/trac/trac/db/sqlite_backend.py
@@ -27,6 +27,8 @@
 
 _like_escape_re = re.compile(r'([/_%])')
 
+_glob_escape_re = re.compile(r'[*?\[]')
+
 try:
     import pysqlite2.dbapi2 as sqlite
     have_pysqlite = 2
@@ -255,7 +257,8 @@
                              and sqlite.version_info >= (2, 5, 0)
 
     def __init__(self, path, log=None, params={}):
-        assert have_pysqlite > 0
+        if have_pysqlite == 0:
+            raise TracError(_("Cannot load Python bindings for SQLite"))
         self.cnx = None
         if path != ':memory:':
             if not os.access(path, os.F_OK):
@@ -312,6 +315,24 @@
     def concat(self, *args):
         return '||'.join(args)
 
+    def drop_table(self, table):
+        cursor = self.cursor()
+        cursor.execute("DROP TABLE IF EXISTS " + self.quote(table))
+
+    def get_column_names(self, table):
+        cursor = self.cnx.cursor()
+        rows = cursor.execute("PRAGMA table_info(%s)"
+                              % self.quote(table))
+        return [row[1] for row in rows]
+
+    def get_last_id(self, cursor, table, column='id'):
+        return cursor.lastrowid
+
+    def get_table_names(self):
+        rows = self.execute("""
+            SELECT name FROM sqlite_master WHERE type='table'""")
+        return [row[0] for row in rows]
+
     def like(self):
         """Return a case-insensitive LIKE clause."""
         if sqlite_version >= (3, 1, 0):
@@ -325,13 +346,18 @@
         else:
             return text
 
+    def prefix_match(self):
+        """Return a case sensitive prefix-matching operator."""
+        return 'GLOB %s'
+
+    def prefix_match_value(self, prefix):
+        """Return a value for case sensitive prefix-matching operator."""
+        return _glob_escape_re.sub(lambda m: '[%s]' % m.group(0), prefix) + '*'
+
     def quote(self, identifier):
         """Return the quoted identifier."""
         return "`%s`" % identifier.replace('`', '``')
 
-    def get_last_id(self, cursor, table, column='id'):
-        return cursor.lastrowid
-
     def update_sequence(self, cursor, table, column='id'):
         # SQLite handles sequence updates automagically
         # http://www.sqlite.org/autoinc.html
diff --git a/trac/trac/db/tests/__init__.py b/trac/trac/db/tests/__init__.py
index e533bf9..bab8589 100644
--- a/trac/trac/db/tests/__init__.py
+++ b/trac/trac/db/tests/__init__.py
@@ -1,11 +1,23 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2014 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 trac.db.tests import api, mysql_test, postgres_test, util
-
 from trac.db.tests.functional import functionalSuite
 
-def suite():
 
+def suite():
     suite = unittest.TestSuite()
     suite.addTest(api.suite())
     suite.addTest(mysql_test.suite())
@@ -15,4 +27,3 @@
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
-
diff --git a/trac/trac/db/tests/api.py b/trac/trac/db/tests/api.py
index 85bf17c..42ac6ea 100644
--- a/trac/trac/db/tests/api.py
+++ b/trac/trac/db/tests/api.py
@@ -1,12 +1,26 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
 
 from __future__ import with_statement
 
 import os
 import unittest
 
+import trac.tests.compat
 from trac.db.api import DatabaseManager, _parse_db_str, get_column_names, \
                         with_transaction
+from trac.db_default import schema as default_schema
+from trac.db.schema import Column, Table
 from trac.test import EnvironmentStub, Mock
 from trac.util.concurrency import ThreadLocal
 
@@ -28,7 +42,8 @@
 
 
 def make_env(get_cnx):
-    return Mock(components={DatabaseManager:
+    from trac.core import ComponentManager
+    return Mock(ComponentManager, components={DatabaseManager:
              Mock(get_connection=get_cnx,
                   _transaction_local=ThreadLocal(wdb=None, rdb=None))})
 
@@ -275,6 +290,9 @@
     def setUp(self):
         self.env = EnvironmentStub()
 
+    def tearDown(self):
+        self.env.reset_db()
+
     def test_insert_unicode(self):
         self.env.db_transaction(
                 "INSERT INTO system (name,value) VALUES (%s,%s)",
@@ -306,48 +324,179 @@
         self.assertEqual(r'alpha\`\"\'\\beta``gamma""delta',
                          get_column_names(cursor)[0])
 
+    def test_quoted_id_with_percent(self):
+        db = self.env.get_read_db()
+        name = """%?`%s"%'%%"""
+
+        def test(db, logging=False):
+            cursor = db.cursor()
+            if logging:
+                cursor.log = self.env.log
+
+            cursor.execute('SELECT 1 AS ' + db.quote(name))
+            self.assertEqual(name, get_column_names(cursor)[0])
+            cursor.execute('SELECT %s AS ' + db.quote(name), (42,))
+            self.assertEqual(name, get_column_names(cursor)[0])
+            cursor.executemany("UPDATE system SET value=%s WHERE "
+                               "1=(SELECT 0 AS " + db.quote(name) + ")",
+                               [])
+            cursor.executemany("UPDATE system SET value=%s WHERE "
+                               "1=(SELECT 0 AS " + db.quote(name) + ")",
+                               [('42',), ('43',)])
+
+        test(db)
+        test(db, logging=True)
+
+    def test_prefix_match_case_sensitive(self):
+        @self.env.with_transaction()
+        def do_insert(db):
+            cursor = db.cursor()
+            cursor.executemany("INSERT INTO system (name,value) VALUES (%s,1)",
+                               [('blahblah',), ('BlahBlah',), ('BLAHBLAH',),
+                                (u'BlähBlah',), (u'BlahBläh',)])
+
+        db = self.env.get_read_db()
+        cursor = db.cursor()
+        cursor.execute("SELECT name FROM system WHERE name %s" %
+                       db.prefix_match(),
+                       (db.prefix_match_value('Blah'),))
+        names = sorted(name for name, in cursor)
+        self.assertEqual('BlahBlah', names[0])
+        self.assertEqual(u'BlahBläh', names[1])
+        self.assertEqual(2, len(names))
+
+    def test_prefix_match_metachars(self):
+        def do_query(prefix):
+            db = self.env.get_read_db()
+            cursor = db.cursor()
+            cursor.execute("SELECT name FROM system WHERE name %s "
+                           "ORDER BY name" % db.prefix_match(),
+                           (db.prefix_match_value(prefix),))
+            return [name for name, in cursor]
+
+        @self.env.with_transaction()
+        def do_insert(db):
+            values = ['foo*bar', 'foo*bar!', 'foo?bar', 'foo?bar!',
+                      'foo[bar', 'foo[bar!', 'foo]bar', 'foo]bar!',
+                      'foo%bar', 'foo%bar!', 'foo_bar', 'foo_bar!',
+                      'foo/bar', 'foo/bar!', 'fo*ob?ar[fo]ob%ar_fo/obar']
+            cursor = db.cursor()
+            cursor.executemany("INSERT INTO system (name,value) VALUES (%s,1)",
+                               [(value,) for value in values])
+
+        self.assertEqual(['foo*bar', 'foo*bar!'], do_query('foo*'))
+        self.assertEqual(['foo?bar', 'foo?bar!'], do_query('foo?'))
+        self.assertEqual(['foo[bar', 'foo[bar!'], do_query('foo['))
+        self.assertEqual(['foo]bar', 'foo]bar!'], do_query('foo]'))
+        self.assertEqual(['foo%bar', 'foo%bar!'], do_query('foo%'))
+        self.assertEqual(['foo_bar', 'foo_bar!'], do_query('foo_'))
+        self.assertEqual(['foo/bar', 'foo/bar!'], do_query('foo/'))
+        self.assertEqual(['fo*ob?ar[fo]ob%ar_fo/obar'], do_query('fo*'))
+        self.assertEqual(['fo*ob?ar[fo]ob%ar_fo/obar'],
+                         do_query('fo*ob?ar[fo]ob%ar_fo/obar'))
+
 
 class ConnectionTestCase(unittest.TestCase):
     def setUp(self):
         self.env = EnvironmentStub()
+        self.schema = [
+            Table('HOURS', key='ID')[
+                Column('ID', auto_increment=True),
+                Column('AUTHOR')],
+            Table('blog', key='bid')[
+                Column('bid', auto_increment=True),
+                Column('author')
+            ]
+        ]
+        self.env.global_databasemanager.drop_tables(self.schema)
+        self.env.global_databasemanager.create_tables(self.schema)
 
     def tearDown(self):
+        self.env.global_databasemanager.drop_tables(self.schema)
         self.env.reset_db()
 
     def test_get_last_id(self):
-        id1 = id2 = None
         q = "INSERT INTO report (author) VALUES ('anonymous')"
         with self.env.db_transaction as db:
             cursor = db.cursor()
             cursor.execute(q)
             # Row ID correct before...
             id1 = db.get_last_id(cursor, 'report')
-            self.assertNotEqual(0, id1)
             db.commit()
             cursor.execute(q)
             # ... and after commit()
             db.commit()
             id2 = db.get_last_id(cursor, 'report')
-            self.assertEqual(id1 + 1, id2)
 
-    def test_update_sequence(self):
-        self.env.db_transaction(
-            "INSERT INTO report (id, author) VALUES (42, 'anonymous')")
+        self.assertNotEqual(0, id1)
+        self.assertEqual(id1 + 1, id2)
+
+    def test_update_sequence_default_column(self):
         with self.env.db_transaction as db:
+            db("INSERT INTO report (id, author) VALUES (42, 'anonymous')")
             cursor = db.cursor()
             db.update_sequence(cursor, 'report', 'id')
+
         self.env.db_transaction(
             "INSERT INTO report (author) VALUES ('next-id')")
+
         self.assertEqual(43, self.env.db_query(
                 "SELECT id FROM report WHERE author='next-id'")[0][0])
 
+    def test_update_sequence_nondefault_column(self):
+        with self.env.db_transaction as db:
+            cursor = db.cursor()
+            cursor.execute(
+                "INSERT INTO blog (bid, author) VALUES (42, 'anonymous')")
+            db.update_sequence(cursor, 'blog', 'bid')
+
+        self.env.db_transaction(
+            "INSERT INTO blog (author) VALUES ('next-id')")
+
+        self.assertEqual(43, self.env.db_query(
+            "SELECT bid FROM blog WHERE author='next-id'")[0][0])
+
+    def test_identifiers_need_quoting(self):
+        """Test for regression described in comment:4:ticket:11512."""
+        with self.env.db_transaction as db:
+            db("INSERT INTO %s (%s, %s) VALUES (42, 'anonymous')"
+               % (db.quote('HOURS'), db.quote('ID'), db.quote('AUTHOR')))
+            cursor = db.cursor()
+            db.update_sequence(cursor, 'HOURS', 'ID')
+
+        with self.env.db_transaction as db:
+            cursor = db.cursor()
+            cursor.execute(
+                "INSERT INTO %s (%s) VALUES ('next-id')"
+                % (db.quote('HOURS'), db.quote('AUTHOR')))
+            last_id = db.get_last_id(cursor, 'HOURS', 'ID')
+
+        self.assertEqual(43, last_id)
+
+    def test_table_names(self):
+        schema = default_schema + self.schema
+        with self.env.db_query as db:
+            db_tables = db.get_table_names()
+            self.assertEqual(len(schema), len(db_tables))
+            for table in schema:
+                self.assertIn(table.name, db_tables)
+
+    def test_get_column_names(self):
+        schema = default_schema + self.schema
+        with self.env.db_transaction as db:
+            for table in schema:
+                db_columns = db.get_column_names(table.name)
+                self.assertEqual(len(table.columns), len(db_columns))
+                for column in table.columns:
+                    self.assertIn(column.name, db_columns)
+
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(ParseConnectionStringTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(StringsTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ConnectionTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(WithTransactionTest, 'test'))
+    suite.addTest(unittest.makeSuite(ParseConnectionStringTestCase))
+    suite.addTest(unittest.makeSuite(StringsTestCase))
+    suite.addTest(unittest.makeSuite(ConnectionTestCase))
+    suite.addTest(unittest.makeSuite(WithTransactionTest))
     return suite
 
 
diff --git a/trac/trac/db/tests/functional.py b/trac/trac/db/tests/functional.py
index 07fd3fb..e101fac 100644
--- a/trac/trac/db/tests/functional.py
+++ b/trac/trac/db/tests/functional.py
@@ -1,4 +1,16 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-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 os
 from trac.tests.functional import *
@@ -18,12 +30,11 @@
 
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
     suite.addTest(DatabaseBackupTestCase())
     return suite
 
 
 if __name__ == '__main__':
     unittest.main(defaultTest='functionalSuite')
-
diff --git a/trac/trac/db/tests/mysql_test.py b/trac/trac/db/tests/mysql_test.py
index 2f8d238..e407a76 100644
--- a/trac/trac/db/tests/mysql_test.py
+++ b/trac/trac/db/tests/mysql_test.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2009 Edgewall Software
+# Copyright (C) 2010-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -13,8 +13,10 @@
 
 import unittest
 
+import trac.tests.compat
 from trac.db.mysql_backend import MySQLConnector
-from trac.test import EnvironmentStub
+from trac.db.schema import Table, Column, Index
+from trac.test import EnvironmentStub, Mock
 
 
 class MySQLTableAlterationSQLTest(unittest.TestCase):
@@ -50,10 +52,31 @@
                                            {'due': ('int', 'int')})
         self.assertEqual([], list(sql))
 
+    def test_utf8_size(self):
+        connector = MySQLConnector(self.env)
+        self.assertEqual(3, connector._utf8_size(Mock(charset='utf8')))
+        self.assertEqual(4, connector._utf8_size(Mock(charset='utf8mb4')))
+
+    def test_to_sql(self):
+        connector = MySQLConnector(self.env)
+        tab = Table('blah', key=('col1', 'col2'))[Column('col1'),
+                                                  Column('col2'),
+                                                  Index(['col2'])]
+
+        sql = list(connector.to_sql(tab, utf8_size=3))
+        self.assertEqual(2, len(sql))
+        self.assertIn(' PRIMARY KEY (`col1`(166),`col2`(166))', sql[0])
+        self.assertIn(' blah_col2_idx ON blah (`col2`(255))', sql[1])
+
+        sql = list(connector.to_sql(tab, utf8_size=4))
+        self.assertEqual(2, len(sql))
+        self.assertIn(' PRIMARY KEY (`col1`(125),`col2`(125))', sql[0])
+        self.assertIn(' blah_col2_idx ON blah (`col2`(191))', sql[1])
+
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(MySQLTableAlterationSQLTest, 'test'))
+    suite.addTest(unittest.makeSuite(MySQLTableAlterationSQLTest))
     return suite
 
 
diff --git a/trac/trac/db/tests/postgres_test.py b/trac/trac/db/tests/postgres_test.py
index 026d1d0..2b655f4 100644
--- a/trac/trac/db/tests/postgres_test.py
+++ b/trac/trac/db/tests/postgres_test.py
@@ -1,4 +1,15 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-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 re
 import unittest
@@ -149,8 +160,8 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(PostgresTableCreationSQLTest, 'test'))
-    suite.addTest(unittest.makeSuite(PostgresTableAlterationSQLTest, 'test'))
+    suite.addTest(unittest.makeSuite(PostgresTableCreationSQLTest))
+    suite.addTest(unittest.makeSuite(PostgresTableAlterationSQLTest))
     return suite
 
 
diff --git a/trac/trac/db/tests/util.py b/trac/trac/db/tests/util.py
index 51c8580..dae1349 100644
--- a/trac/trac/db/tests/util.py
+++ b/trac/trac/db/tests/util.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2010-2014 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -31,10 +31,41 @@
         self.assertEqual("'%% %%'", sql_escape_percent("'% %'"))
         self.assertEqual("'%%s %%i'", sql_escape_percent("'%s %i'"))
 
+        self.assertEqual("%", sql_escape_percent("%"))
+        self.assertEqual("`%%`", sql_escape_percent("`%`"))
+        self.assertEqual("``%``", sql_escape_percent("``%``"))
+        self.assertEqual("```%%```", sql_escape_percent("```%```"))
+        self.assertEqual("```%%`", sql_escape_percent("```%`"))
+        self.assertEqual("%s", sql_escape_percent("%s"))
+        self.assertEqual("% %", sql_escape_percent("% %"))
+        self.assertEqual("%s %i", sql_escape_percent("%s %i"))
+        self.assertEqual("`%%s`", sql_escape_percent("`%s`"))
+        self.assertEqual("`%% %%`", sql_escape_percent("`% %`"))
+        self.assertEqual("`%%s %%i`", sql_escape_percent("`%s %i`"))
+
+        self.assertEqual('%', sql_escape_percent('%'))
+        self.assertEqual('"%%"', sql_escape_percent('"%"'))
+        self.assertEqual('""%""', sql_escape_percent('""%""'))
+        self.assertEqual('"""%%"""', sql_escape_percent('"""%"""'))
+        self.assertEqual('"""%%"', sql_escape_percent('"""%"'))
+        self.assertEqual('%s', sql_escape_percent('%s'))
+        self.assertEqual('% %', sql_escape_percent('% %'))
+        self.assertEqual('%s %i', sql_escape_percent('%s %i'))
+        self.assertEqual('"%%s"', sql_escape_percent('"%s"'))
+        self.assertEqual('"%% %%"', sql_escape_percent('"% %"'))
+        self.assertEqual('"%%s %%i"', sql_escape_percent('"%s %i"'))
+
+        self.assertEqual("""'%%?''"%%s`%%i`%%%%"%%S'""",
+                         sql_escape_percent("""'%?''"%s`%i`%%"%S'"""))
+        self.assertEqual("""`%%?``'%%s"%%i"%%%%'%%S`""",
+                         sql_escape_percent("""`%?``'%s"%i"%%'%S`"""))
+        self.assertEqual('''"%%?""`%%s'%%i'%%%%`%%S"''',
+                         sql_escape_percent('''"%?""`%s'%i'%%`%S"'''))
+
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(SQLEscapeTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(SQLEscapeTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/db/util.py b/trac/trac/db/util.py
index 178b4fb..bf000bc 100644
--- a/trac/trac/db/util.py
+++ b/trac/trac/db/util.py
@@ -15,11 +15,18 @@
 #
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
+import re
+
+_sql_escape_percent_re = re.compile("""
+    '(?:[^']+|'')*' |
+    `(?:[^`]+|``)*` |
+    "(?:[^"]+|"")*" """, re.VERBOSE)
+
 
 def sql_escape_percent(sql):
-    import re
-    return re.sub("'((?:[^']|(?:''))*)'",
-                  lambda m: m.group(0).replace('%', '%%'), sql)
+    def repl(match):
+        return match.group(0).replace('%', '%%')
+    return _sql_escape_percent_re.sub(repl, sql)
 
 
 class IterableCursor(object):
@@ -118,7 +125,7 @@
         """
         dql = self.check_select(query)
         cursor = self.cnx.cursor()
-        cursor.execute(query, params)
+        cursor.execute(query, params if params is not None else [])
         rows = cursor.fetchall() if dql else None
         cursor.close()
         return rows
diff --git a/trac/trac/db_default.py b/trac/trac/db_default.py
index 9d1b357..e6e420e 100644
--- a/trac/trac/db_default.py
+++ b/trac/trac/db_default.py
@@ -300,34 +300,27 @@
 logged in user when executed.
 """,
 """\
-SELECT  __color__, __group,
+SELECT p.value AS __color__,
        (CASE
-         WHEN __group = 1 THEN 'Accepted'
-         WHEN __group = 2 THEN 'Owned'
-         WHEN __group = 3 THEN 'Reported'
+         WHEN owner = $USER AND status = 'accepted' THEN 'Accepted'
+         WHEN owner = $USER THEN 'Owned'
+         WHEN reporter = $USER THEN 'Reported'
          ELSE 'Commented' END) AS __group__,
-       ticket, summary, component, version, milestone,
-       type, priority, created, _changetime, _description,
-       _reporter
-FROM (
- SELECT DISTINCT """ + db.cast('p.value', 'int') + """ AS __color__,
-      (CASE
-         WHEN owner = $USER AND status = 'accepted' THEN 1
-         WHEN owner = $USER THEN 2
-         WHEN reporter = $USER THEN 3
-         ELSE 4 END) AS __group,
        t.id AS ticket, summary, component, version, milestone,
        t.type AS type, priority, t.time AS created,
        t.changetime AS _changetime, description AS _description,
        reporter AS _reporter
   FROM ticket t
   LEFT JOIN enum p ON p.name = t.priority AND p.type = 'priority'
-  LEFT JOIN ticket_change tc ON tc.ticket = t.id AND tc.author = $USER
-                                AND tc.field = 'comment'
-  WHERE t.status <> 'closed'
-        AND (owner = $USER OR reporter = $USER OR author = $USER)
-) AS sub
-ORDER BY __group, __color__, milestone, type, created
+  WHERE t.status <> 'closed' AND
+        (owner = $USER OR reporter = $USER OR
+         EXISTS (SELECT * FROM ticket_change tc
+                 WHERE tc.ticket = t.id AND tc.author = $USER AND
+                       tc.field = 'comment'))
+  ORDER BY (COALESCE(owner, '') = $USER AND status = 'accepted') DESC,
+           COALESCE(owner, '') = $USER DESC,
+           COALESCE(reporter, '') = $USER DESC,
+           """ + db.cast('p.value', 'int') + """, milestone, t.type, t.time
 """),
 #----------------------------------------------------------------------------
 ('Active Tickets, Mine first',
diff --git a/trac/trac/dist.py b/trac/trac/dist.py
index 779da5c..ddb586e 100644
--- a/trac/trac/dist.py
+++ b/trac/trac/dist.py
@@ -85,8 +85,8 @@
         in_def = in_translator_comments = False
         comment_tag = None
 
-        encoding = parse_encoding(fileobj) \
-                   or options.get('encoding', 'iso-8859-1')
+        encoding = str(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'])
@@ -466,6 +466,17 @@
                 self.run_command('compile_catalog')
             def run(self):
                 self.l10n_run()
+                # When bdist_egg is called on distribute 0.6.29 and later, the
+                # egg file includes no *.mo and *.js files which are generated
+                # in l10n_run() method.
+                # We remove build_py.data_files property to re-compute in order
+                # to avoid the issue (#11640).
+                build_py = self.get_finalized_command('build_py')
+                if 'data_files' in build_py.__dict__ and \
+                   not any(any(name.endswith('.mo') for name in filenames)
+                           for pkg, src_dir, build_dir, filenames
+                           in build_py.data_files):
+                    del build_py.__dict__['data_files']
                 _install_lib.run(self)
         return build, install_lib
 
diff --git a/trac/trac/env.py b/trac/trac/env.py
index 266bd87..4cb6c91 100644
--- a/trac/trac/env.py
+++ b/trac/trac/env.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2003-2011 Edgewall Software
+# Copyright (C) 2003-2014 Edgewall Software
 # Copyright (C) 2003-2007 Jonas Borgström <jonas@edgewall.com>
 # All rights reserved.
 #
@@ -27,7 +27,8 @@
 from trac import db_default
 from trac.admin import AdminCommandError, IAdminCommandProvider
 from trac.cache import CacheManager
-from trac.config import *
+from trac.config import BoolOption, ConfigSection, Configuration, Option, \
+                        PathOption
 from trac.core import Component, ComponentManager, implements, Interface, \
                       ExtensionPoint, TracError
 from trac.db.api import (DatabaseManager, QueryContextManager,
@@ -276,7 +277,6 @@
 
         self.path = path
         self.systeminfo = []
-        self._href = self._abs_href = None
 
         if create:
             self.create(options)
@@ -328,17 +328,14 @@
             name = name_or_class.__module__ + '.' + name_or_class.__name__
         return name.lower()
 
-    @property
+    @lazy
     def _component_rules(self):
-        try:
-            return self._rules
-        except AttributeError:
-            self._rules = {}
-            for name, value in self.components_section.options():
-                if name.endswith('.*'):
-                    name = name[:-2]
-                self._rules[name.lower()] = value.lower() in ('enabled', 'on')
-            return self._rules
+        _rules = {}
+        for name, value in self.components_section.options():
+            if name.endswith('.*'):
+                name = name[:-2]
+            _rules[name.lower()] = value.lower() in ('enabled', 'on')
+        return _rules
 
     def is_component_enabled(self, cls):
         """Implemented to only allow activation of components that are
@@ -375,8 +372,10 @@
                 break
             cname = cname[:idx]
 
-        # By default, all components in the trac package are enabled
-        return component_name.startswith('trac.') or None
+        # By default, all components in the trac package except
+        # trac.test are enabled
+        return component_name.startswith('trac.') and \
+               not component_name.startswith('trac.test.') or None
 
     def enable_component(self, cls):
         """Enable a component or module."""
@@ -396,7 +395,8 @@
     def get_db_cnx(self):
         """Return a database connection from the connection pool
 
-        :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead
+        :deprecated: Use :meth:`db_transaction` or :meth:`db_query` instead.
+                     Removed in Trac 1.1.2.
 
         `db_transaction` for obtaining the `db` database connection
         which can be used for performing any query
@@ -437,13 +437,21 @@
         return DatabaseManager(self).get_exceptions()
 
     def with_transaction(self, db=None):
-        """Decorator for transaction functions :deprecated:"""
+        """Decorator for transaction functions.
+
+        :deprecated: Use the query and transaction context managers instead.
+                     Will be removed in Trac 1.3.1.
+        """
         return with_transaction(self, db)
 
     def get_read_db(self):
-        """Return a database connection for read purposes :deprecated:
+        """Return a database connection for read purposes.
 
-        See `trac.db.api.get_read_db` for detailed documentation."""
+        See `trac.db.api.get_read_db` for detailed documentation.
+
+        :deprecated: Use :meth:`db_query` instead.
+                     Will be removed in Trac 1.3.1.
+        """
         return DatabaseManager(self).get_connection(readonly=True)
 
     @property
@@ -585,6 +593,25 @@
         # Create the database
         DatabaseManager(self).init_db()
 
+    @lazy
+    def database_version(self):
+        """Returns the current version of the database.
+
+        :since 1.0.2:
+        """
+        return self.get_version()
+
+    @lazy
+    def database_initial_version(self):
+        """Returns the version of the database at the time of creation.
+
+        In practice, for database created before 0.11, this will
+        return `False` which is "older" than any db version number.
+
+        :since 1.0.2:
+        """
+        return self.get_version(initial=True)
+
     def get_version(self, db=None, initial=False):
         """Return the current version of the database.  If the
         optional argument `initial` is set to `True`, the version of
@@ -597,11 +624,16 @@
 
         :since 1.0: deprecation warning: the `db` parameter is no
                     longer used and will be removed in version 1.1.1
+
+        :since 1.0.2: The lazily-evaluated attributes `database_version` and
+                      `database_initial_version` should be used instead. This
+                      method will be renamed to a private method in
+                      release 1.3.1.
         """
         rows = self.db_query("""
                 SELECT value FROM system WHERE name='%sdatabase_version'
                 """ % ('initial_' if initial else ''))
-        return rows and int(rows[0][0])
+        return int(rows[0][0]) if rows else False
 
     def setup_config(self):
         """Load the configuration file."""
@@ -715,26 +747,24 @@
                 participant.upgrade_environment(db)
             # Database schema may have changed, so close all connections
             DatabaseManager(self).shutdown()
+        del self.database_version
         return True
 
-    @property
+    @lazy
     def href(self):
         """The application root path"""
-        if not self._href:
-            self._href = Href(urlsplit(self.abs_href.base)[2])
-        return self._href
+        return Href(urlsplit(self.abs_href.base).path)
 
-    @property
+    @lazy
     def abs_href(self):
         """The application URL"""
-        if not self._abs_href:
-            if not self.base_url:
-                self.log.warn("base_url option not set in configuration, "
-                              "generated links may be incorrect")
-                self._abs_href = Href('')
-            else:
-                self._abs_href = Href(self.base_url)
-        return self._abs_href
+        if not self.base_url:
+            self.log.warn("base_url option not set in configuration, "
+                          "generated links may be incorrect")
+            _abs_href = Href('')
+        else:
+            _abs_href = Href(self.base_url)
+        return _abs_href
 
 
 class EnvironmentSetup(Component):
@@ -756,7 +786,7 @@
         self._update_sample_config()
 
     def environment_needs_upgrade(self, db):
-        dbver = self.env.get_version(db)
+        dbver = self.env.database_version
         if dbver == db_default.db_version:
             return False
         elif dbver > db_default.db_version:
@@ -770,7 +800,7 @@
         upgrades/dbN.py, where 'N' is the version number (int).
         """
         cursor = db.cursor()
-        dbver = self.env.get_version()
+        dbver = self.env.database_version
         for i in range(dbver + 1, db_default.db_version + 1):
             name  = 'db%i' % i
             try:
@@ -794,9 +824,8 @@
         if not os.path.isfile(filename):
             return
         config = Configuration(filename)
-        for section, default_options in config.defaults().iteritems():
-            for name, value in default_options.iteritems():
-                config.set(section, name, value)
+        for (section, name), option in Option.get_registry().iteritems():
+            config.set(section, name, option.dumps(option.default))
         try:
             config.save()
             self.log.info("Wrote sample configuration file with the new "
diff --git a/trac/trac/htdocs/css/admin.css b/trac/trac/htdocs/css/admin.css
index 685f8be..05c71a8 100644
--- a/trac/trac/htdocs/css/admin.css
+++ b/trac/trac/htdocs/css/admin.css
@@ -57,14 +57,14 @@
 }
 form.addnew div.field,
 form.addnew div.buttons {
- padding: 0.2em 0.5em 0.2em 0;
+ padding: 0.2em 0;
  white-space: nowrap;
 }
-form.addnew div.buttons input { margin: 0 0.5em 0 0; }
+form.addnew div.field { padding-right: 1em; }
+form.addnew div.buttons input { margin: 0 1em 0 0; }
 form.addnew p.hint,
 form.addnew span.hint {
- padding-left: 0.5em;
- padding-right: 0.5em;
+ padding-left: 0.25em;
 }
 form.addnew p.help { margin-top: 0.5em; }
 form.addnew br { display: none; }
@@ -103,6 +103,7 @@
 .plugin .info dd { padding: 0; margin: 0; }
 .plugin .listing { width: 100%; }
 .plugin .listing th.sel input { margin-right: 0.5em; vertical-align: bottom; }
+.plugin .listing td.trac-module { background: #fcfcfc; }
 .plugin .listing td { background: #fff; }
 .trac-heading { margin: 0; }
 .trac-name { font-family: monospace; }
@@ -125,9 +126,9 @@
 #permlist { margin-bottom: 2em; }
 #permlist label, #grouplist label {
  float: left;
- min-width: 13em;
- max-width: 33%;
- padding: 0 2em 0 0;
+ width: 13em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding: 0;
  white-space: nowrap;
 }
-fieldset tr.field th { text-align: right; }
diff --git a/trac/trac/htdocs/css/browser.css b/trac/trac/htdocs/css/browser.css
index cfe883d..c8a7bfc 100644
--- a/trac/trac/htdocs/css/browser.css
+++ b/trac/trac/htdocs/css/browser.css
@@ -146,7 +146,7 @@
  vertical-align: middle;
 }
 .chglist td.author { color: #888 }
-.chglist td.change span.edit {
+.chglist td.change span {
  border: 1px solid #999;
  float: left;
  margin: .2em .5em 0 0;
diff --git a/trac/trac/htdocs/css/changeset.css b/trac/trac/htdocs/css/changeset.css
index 2f15970..664f18f 100644
--- a/trac/trac/htdocs/css/changeset.css
+++ b/trac/trac/htdocs/css/changeset.css
@@ -1,5 +1,5 @@
 /* Changeset overview */
-#overview .files { padding-top: 1em }
+#overview .files { padding: 1px 0 }
 #overview .files ul { margin: 0; padding: 0 }
 #overview .files li { list-style-type: none }
 #overview .files li .comment { display: none }
@@ -22,7 +22,6 @@
  margin-bottom: 0;
  margin-top: 0;
 }
-#overview .files { padding: 1px 0 }
 
 .diff ul.props {
  font-size: 90%;
diff --git a/trac/trac/htdocs/css/code.css b/trac/trac/htdocs/css/code.css
index 054de7d..ac0291a 100644
--- a/trac/trac/htdocs/css/code.css
+++ b/trac/trac/htdocs/css/code.css
@@ -66,6 +66,9 @@
 table.code tbody th :link:hover, table.code tbody th :visited:hover {
  color: #000;
 }
+table.code tbody tr:hover td {
+ background: #eed;
+}
 table.code td {
  font: normal 11px monospace;
  overflow: hidden;
diff --git a/trac/trac/htdocs/css/diff.css b/trac/trac/htdocs/css/diff.css
index bc081cc..eaa2756 100644
--- a/trac/trac/htdocs/css/diff.css
+++ b/trac/trac/htdocs/css/diff.css
@@ -2,16 +2,15 @@
 #prefs fieldset { margin: 1em .5em .5em; padding: .5em 1em 0 }
 
 /* Diff/change overview */
-#overview { line-height: 130%; margin-top: 1em; padding: .5em }
+#overview { line-height: 130%; margin-top: 1em; padding: .5em .5em .5em 0 }
 #overview dt.property {
+ clear: left;
+ float: left;
  font-weight: bold;
- padding-right: .25em;
- position: absolute; /* relies on #content { position: relative } */
- left: 0;
  text-align: right;
  width: 7.75em;
 }
-#overview dd { margin-left: 8em }
+#overview dd { margin-left: 8.5em }
 
 #overview .message { padding: 1em 0 1px }
 #overview dd.message p, #overview dd.message ul, #overview dd.message ol,
@@ -132,6 +131,18 @@
  padding: 1px 2px;
  vertical-align: top;
 }
+.diff table.trac-diff tbody tr:hover td {
+ background: #eed;
+}
+.diff table.trac-diff tbody.mod tr:hover td,
+.diff table.trac-diff tbody.add tr:hover td,
+.diff table.trac-diff tbody.rem tr:hover td {
+ background: #ddc;
+}
+.diff table.trac-diff tbody.mod tr:hover td del,
+.diff table.trac-diff tbody.mod tr:hover td ins {
+ background: #bb9;
+}
 .diff table.trac-diff tbody.skipped td, .diff table.trac-diff thead td {
  background: #f7f7f7;
  border: 1px solid #d7d7d7;
diff --git a/trac/trac/htdocs/css/jquery-ui/jquery-ui.css b/trac/trac/htdocs/css/jquery-ui/jquery-ui.css
index 4f1a9c5..751290e 100644
--- a/trac/trac/htdocs/css/jquery-ui/jquery-ui.css
+++ b/trac/trac/htdocs/css/jquery-ui/jquery-ui.css
@@ -47,15 +47,15 @@
  *
  * http://docs.jquery.com/UI/Theming/API
  *
- * To view and modify this theme, visit http://jqueryui.com/themeroller/?ctl=themeroller&ffDefault=Verdana,Arial,\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'Bitstream%20Vera%20Sans\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\',Helvetica,sans-serif&fwDefault=normal&fsDefault=13px&cornerRadius=.3em&bgColorHeader=ffffdd&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=80&borderColorHeader=bbbbbb&fcHeader=000000&iconColorHeader=707070&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=00&borderColorContent=bbbbbb&fcContent=000000&iconColorContent=222222&bgColorDefault=ffffff&bgTextureDefault=01_flat.png&bgImgOpacityDefault=0&borderColorDefault=bbbbbb&fcDefault=b00000&iconColorDefault=b00000&bgColorHover=ffffdd&bgTextureHover=01_flat.png&bgImgOpacityHover=0&borderColorHover=505050&fcHover=505050&iconColorHover=505050&bgColorActive=303030&bgTextureActive=03_highlight_soft.png&bgImgOpacityActive=30&borderColorActive=bbbbbb&fcActive=eeeeee&iconColorActive=d7d7d7&bgColorHighlight=c0f0c0&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=c0f0c0&fcHighlight=363636&iconColorHighlight=4b954f&bgColorError=ffddcc&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=9b081d&fcError=500000&iconColorError=9b081d&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px
+ * To view and modify this theme, visit http://jqueryui.com/themeroller/?ctl=themeroller&ffDefault=Verdana,Arial,'Bitstream%20Vera%20Sans',Helvetica,sans-serif&fwDefault=normal&fsDefault=13px&cornerRadius=.3em&bgColorHeader=ffffdd&bgTextureHeader=03_highlight_soft.png&bgImgOpacityHeader=80&borderColorHeader=bbbbbb&fcHeader=000000&iconColorHeader=707070&bgColorContent=ffffff&bgTextureContent=01_flat.png&bgImgOpacityContent=00&borderColorContent=bbbbbb&fcContent=000000&iconColorContent=222222&bgColorDefault=ffffff&bgTextureDefault=01_flat.png&bgImgOpacityDefault=0&borderColorDefault=bbbbbb&fcDefault=b00000&iconColorDefault=b00000&bgColorHover=ffffdd&bgTextureHover=01_flat.png&bgImgOpacityHover=0&borderColorHover=505050&fcHover=505050&iconColorHover=505050&bgColorActive=303030&bgTextureActive=03_highlight_soft.png&bgImgOpacityActive=30&borderColorActive=bbbbbb&fcActive=eeeeee&iconColorActive=d7d7d7&bgColorHighlight=c0f0c0&bgTextureHighlight=03_highlight_soft.png&bgImgOpacityHighlight=75&borderColorHighlight=c0f0c0&fcHighlight=363636&iconColorHighlight=4b954f&bgColorError=ffddcc&bgTextureError=08_diagonals_thick.png&bgImgOpacityError=18&borderColorError=9b081d&fcError=500000&iconColorError=9b081d&bgColorOverlay=666666&bgTextureOverlay=08_diagonals_thick.png&bgImgOpacityOverlay=20&opacityOverlay=50&bgColorShadow=000000&bgTextureShadow=01_flat.png&bgImgOpacityShadow=10&opacityShadow=20&thicknessShadow=5px&offsetTopShadow=-5px&offsetLeftShadow=-5px&cornerRadiusShadow=5px
  */
 
 
 /* Component containers
 ----------------------------------*/
-.ui-widget { font-family: Verdana,Arial,\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'Bitstream Vera Sans\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\',Helvetica,sans-serif; font-size: 13px; }
+.ui-widget { font-family: Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; font-size: 13px; }
 .ui-widget .ui-widget { font-size: 1em; }
-.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\'Bitstream Vera Sans\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\',Helvetica,sans-serif; font-size: 1em; }
+.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,'Bitstream Vera Sans',Helvetica,sans-serif; font-size: 1em; }
 .ui-widget-content { border: 1px solid #bbbbbb; background: #ffffff url(images/ui-bg_flat_00_ffffff_40x100.png) 50% 50% repeat-x; color: #000000; }
 .ui-widget-content a { color: #000000; }
 .ui-widget-header { border: 1px solid #bbbbbb; background: #ffffdd url(images/ui-bg_highlight-soft_80_ffffdd_1x100.png) 50% 50% repeat-x; color: #000000; font-weight: bold; }
diff --git a/trac/trac/htdocs/css/report.css b/trac/trac/htdocs/css/report.css
index a96366d..549d52d 100644
--- a/trac/trac/htdocs/css/report.css
+++ b/trac/trac/htdocs/css/report.css
@@ -25,7 +25,6 @@
 
 .report div.reports { clear: both }
 
-
 .report div.reports h2 {
  /* taken from .wikipage h2 */
  border-bottom: 1px solid #ddd;
@@ -81,8 +80,13 @@
 }
 #report-descr { margin: 0 2em; font-size: 90% }
 #report-notfound { margin: 2em; font-size: 110% }
+
+/* Report edit form */
+#content.report.edit form {
+ max-width: 54em;
+}
 #content.report .field { margin: 1em 0; }
-#content.report .field label { padding-bottom: .3em; }
+#content.report .field label { display: block; padding-bottom: .3em; }
 
 #query { clear: right }
 #query fieldset, #query fieldset input, #query fieldset select {
@@ -179,9 +183,9 @@
 .tickets tr.color2-even { background: #ffd; border-color: #dd8; color: #880 }
 .tickets tr.color3-odd  { background: #fbfbfb; border-color: #ddd; color: #444 }
 .tickets tr.color3-even { background: #f6f6f6; border-color: #ccc; color: #333 }
-.tickets tr.color4-odd { background: #e7ffff; border-color: #cee; color: #099 }
+.tickets tr.color4-odd  { background: #e7ffff; border-color: #cee; color: #099 }
 .tickets tr.color4-even { background: #dff; border-color: #bee; color: #099 }
-.tickets tr.color5-odd { background: #e7eeff; border-color: #cde; color: #469 }
+.tickets tr.color5-odd  { background: #e7eeff; border-color: #cde; color: #469 }
 .tickets tr.color5-even { background: #dde7ff; border-color: #cde; color: #469 }
 .tickets tr.color6-odd  { background: #f0f0f0; border-color: #ddd; color: #888 }
 .tickets tr.color6-even { background: #f7f7f7; border-color: #ddd; color: #888 }
@@ -207,7 +211,7 @@
 table.tickets tbody tr.even.prio1 { background: #fed; border-color: #e99 }
 table.tickets tbody tr.prio2 { background: #ffb; border-color: #eea }
 table.tickets tbody tr.even.prio2 { background: #ffd; border-color: #dd8 }
-table.tickets tbody tr.prio3  { background: #fbfbfb; border-color: #ddd }
+table.tickets tbody tr.prio3 { background: #fbfbfb; border-color: #ddd }
 table.tickets tbody tr.even.prio3 { background: #f6f6f6; border-color: #ccc }
 table.tickets tbody tr.prio4 { background: #e7ffff; border-color: #cee }
 table.tickets tbody tr.even.prio4 { background: #dff; border-color: #bee }
@@ -216,16 +220,18 @@
 table.tickets tbody tr.prio6 { background: #f0f0f0; border-color: #ddd }
 table.tickets tbody tr.even.prio6 { background: #f7f7f7 }
 table.tickets tbody tr.fullrow th {
-  border: none;
-  vertical-align: middle;
-  text-align: center;
-  font-size: 85%;
+ border: none;
+ vertical-align: middle;
+ text-align: center;
+ font-size: 85%;
 }
+table.tickets tbody tr p:first-child { margin-top: 0 }
+table.tickets tbody tr p:last-child { margin-bottom: 0 }
 
 /* Batchmod Form */
 
 #batchmod_form { display: none; }
-#batchmod_form fieldset input#batchmod_submit { font-size: 14px; }
+#batchmod_form fieldset input#batchmod_submit { font-size: 100% }
 #batchmod_form fieldset input[type="button"]{ padding: 0.1em 0.5em; }
 #batchmod_form > fieldset { margin-top: 1.5em }
 #batchmod_form fieldset.collapsed {
@@ -236,10 +242,8 @@
 .batchmod_property { width: 100%; }
 .batchmod_required:before { content: " * "; }
 #batchmod_form fieldset input,
-#batchmod_form fieldset select,
-.batchmod_property,
-.batchmod_label {
-    font-size: 11px;
+#batchmod_form fieldset select, .batchmod_property, .batchmod_label {
+ font-size: 11px;
 }
 #batchmod_action { margin-top: 0; line-height: 2em; }
 #batchmod_form th {
diff --git a/trac/trac/htdocs/css/roadmap.css b/trac/trac/htdocs/css/roadmap.css
index b119766..e5faedd 100644
--- a/trac/trac/htdocs/css/roadmap.css
+++ b/trac/trac/htdocs/css/roadmap.css
@@ -55,11 +55,8 @@
  font-style: italic;
  margin: 0 0 1em 0;
 }
-.milestone .description { margin-left: 1em }
-
-/* Styles for the milestone view */
-.milestone .date { color: #888; font-style: italic; margin: 0 }
 .milestone .description { margin: 1em 0 2em }
+
 #stats {
  float: right;
  margin: 0 1em 2em 2em;
@@ -98,6 +95,3 @@
 #edit .field { margin: 0.5em 0 }
 #edit label { padding-left: .2em }
 #edit fieldset { margin-left: 1px; margin-right: 1px }
-#edit textarea#description { margin-left: -1px; margin-right: -1px; padding: 0; width: 100% }
-#edit .wikitoolbar { margin-left: -1px }
-#edit div.trac-resizable { width: 100% }
diff --git a/trac/trac/htdocs/css/ticket.css b/trac/trac/htdocs/css/ticket.css
index c482dee..ac3ae0e 100644
--- a/trac/trac/htdocs/css/ticket.css
+++ b/trac/trac/htdocs/css/ticket.css
@@ -8,7 +8,6 @@
 }
 
 #field-description-help { float: right }
-#properties div.trac-resizable, #field-description { width: 100% }
 
 #content.ticket .trac-topnav {
  float: none;
@@ -132,8 +131,9 @@
 ul.children {
  margin-top: 1.5em;
  padding-left: 2em;
- list-style-image: url(../inreply.png);
 }
+ul.children, ul.children ul.children { list-style-image: url(../inreply.png) }
+ul.children ul, ul.children ol { list-style-image: none }
 ul.children > li.child {
  padding-left: .5em;
  margin-bottom: 1.5em;
@@ -155,22 +155,9 @@
  #changelog h3, #ticketchange h3 { box-shadow: none }
 }
 
-div.comment ul { list-style: disc }
-div.comment ul ul, div.comment ol ul { list-style: circle }
-div.comment ul ul ul, div.comment ol ul ul { list-style: square }
-div.comment ul ol ul, div.comment ol ol ul { list-style: square }
-div.comment ol { list-style: decimal }
-
 /* Comment editor */
 #trac-comment-editor { margin-left: 2em; margin-bottom: 1em }
-#trac-comment-editor div.trac-resizable { width: 100% }
-#trac-comment-editor textarea {
- background: #ffffe0;
- margin-left: -1px;
- margin-right: -1px;
- width: 100%;
-}
-#trac-comment-editor .wikitoolbar { margin-left: -1px }
+#trac-comment-editor textarea { background: #ffffe0; }
 .trac-new { border-left: 0.31em solid #c0f0c0; padding-left: 0.31em; }
 
 .trac-loading {
@@ -213,22 +200,39 @@
 form .field { margin-top: .75em; width: 100% }
 form .field fieldset { margin-left: 1px; margin-right: 1px }
 label[for=comment] { float: right }
-#comment { margin-left: -1px; margin-right: -1px; padding: 0; width: 100% }
-form .field .wikitoolbar { margin-left: -1px }
-form .field div.trac-resizable { width: 100% }
 
 #propertyform { margin-bottom: 2em; }
 #properties { white-space: nowrap; line-height: 160%; padding: .5em }
-#properties table { border-spacing: 0; width: 100%; padding: 0 .5em }
-#properties table th {
+#properties table {
+ border-spacing: 0;
+ padding: 0 .5em;
+ table-layout: fixed;
+ width: 100%;
+}
+#properties table th,
+#properties table col.th {
  padding: .4em;
  text-align: right;
- width: 20%;
  vertical-align: top;
+ white-space: normal;
+ width: 17%;
 }
 #properties table th.col2 { border-left: 1px dotted #d7d7d7 }
-#properties table td { vertical-align: middle; width: 30% }
-#properties table td.fullrow { vertical-align: middle; width: 80% }
+#properties table td,
+#properties table col.td {
+ padding-right: 0.6em;
+ vertical-align: middle;
+ width: 33%
+}
+#properties table td.fullrow { width: 83% }
+#properties table td input[type="text"] {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ width: 100%;
+}
+#properties table td.col1 label,
+#properties table td.col2 label { float: left; margin-right: 0.5em; }
+#properties table td select { max-width: 100% }
 
 #action { line-height: 2em }
 
@@ -242,4 +246,4 @@
 }
 fieldset.radio label { padding-right: 1em }
 
-#content.ticket .trac-nav a { margin-left: 1em; }
\ No newline at end of file
+#content.ticket .trac-nav a { margin-left: 1em; }
diff --git a/trac/trac/htdocs/css/trac.css b/trac/trac/htdocs/css/trac.css
index c5ced1a..274761f 100644
--- a/trac/trac/htdocs/css/trac.css
+++ b/trac/trac/htdocs/css/trac.css
@@ -82,6 +82,13 @@
 
 /* Forms */
 input, textarea, select { margin: 2px }
+/* Avoid respect to system font settings for controls on Firefox, #11607 */
+input, select, button {
+ font-family: Arial,Verdana,'Bitstream Vera Sans',Helvetica,sans-serif;
+ font-size: 100%;
+}
+/* Avoid to inherit white-space of its parent element for IE 11, #11376 */
+textarea { white-space: pre-wrap }
 input, select { vertical-align: middle }
 input[type=button], input[type=submit], input[type=reset], button {
  *overflow: visible; /* Workaround too much margin on button in IE7 */
@@ -176,6 +183,14 @@
 input[type=submit].trac-delete:hover {
  color: #e31313;
 }
+textarea.trac-fullwidth, input.trac-fullwidth {
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ margin-left: 0;
+ margin-right: 0;
+ width: 100%;
+}
+textarea.trac-fullwidth { padding: 2px; }
 
 /* Header */
 #header hr { display: none }
@@ -225,6 +240,26 @@
 #metanav {
  padding-top: .3em;
 }
+#metanav form.trac-logout {
+ display: inline;
+ margin: 0;
+ padding: 0;
+}
+#metanav form.trac-logout button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ outline: 0;
+ background: transparent;
+ font-family: inherit;
+ font-size: 100%;
+ color: #b00;
+ border-bottom: 1px dotted #bbb;
+ cursor: pointer;
+}
+#metanav form.trac-logout button::-moz-focus-inner { border: 0; padding: 0 }
+#metanav form.trac-logout button:hover { background-color: #eee; color: #555 }
+#metanav form.trac-logout button:active { position: static }
 
 /* Main navigation bar */
 #mainnav {
@@ -311,7 +346,7 @@
  text-align: center;
 }
 #altlinks h3 { font-size: 12px; letter-spacing: normal; margin: 0 }
-#altlinks ul { list-style: none; margin: 0; }
+#altlinks ul { list-style: none; margin: 0; padding: 0 }
 #altlinks li {
  border-right: 1px solid #d7d7d7;
  display: inline;
@@ -473,6 +508,11 @@
 div.compact > p:first-child { margin-top: 0 }
 div.compact > p:last-child { margin-bottom: 0 }
 
+/* Styles related to RTL support */
+.rtl { direction: rtl; }
+.rtl div.wiki-toc { float: left; }
+.rtl .wiki-toc ul ul, .wiki-toc ol ol { padding-right: 1.2em }
+
 a.missing:link, a.missing:visited, a.missing, span.missing,
 a.forbidden, span.forbidden { color: #998 }
 a.missing:hover { color: #000 }
@@ -532,10 +572,11 @@
  padding: .1em .25em;
  background-color: #f7f7f7;
 }
+table.wiki tbody tr.even { background-color: #fcfcfc }
+table.wiki tbody tr.odd { background-color: #f7f7f7 }
 
 .wikitoolbar {
  margin-top: 0.3em;
- margin-left: 2px;
  border: solid #d7d7d7;
  border-width: 1px 1px 1px 0;
  height: 18px;
@@ -566,7 +607,7 @@
 .wikitoolbar a#img { background-position: 0 -128px }
 
 /* Textarea resizer */
-div.trac-resizable { display: table; width: 1px }
+div.trac-resizable { display: table; width: 100% }
 div.trac-resizable > div { display: table-cell }
 div.trac-resizable textarea { display: block; margin-bottom: 0 }
 div.trac-grip {
@@ -649,7 +690,7 @@
 table.listing tbody tr { border-top: 1px solid #ddd }
 table.listing tbody tr.even { background-color: #fcfcfc }
 table.listing tbody tr.odd { background-color: #f7f7f7 }
-table.listing tbody tr:hover { background: #eed !important }
+table.listing tbody tr:hover td { background: #eed !important }
 table.listing tbody tr.focus { background: #ddf !important }
 
 table.listing pre { white-space: pre-wrap }
diff --git a/trac/trac/htdocs/css/wiki.css b/trac/trac/htdocs/css/wiki.css
index 2b4300c..b31a1ec 100644
--- a/trac/trac/htdocs/css/wiki.css
+++ b/trac/trac/htdocs/css/wiki.css
@@ -50,15 +50,11 @@
 #edit fieldset { margin-left: 1px; margin-right: 1px }
 #edit #text {
  clear: both;
- margin-left: -1px;
- margin-right: -1px;
- padding: 0;
- width: 100%;
  min-height: 10em;
  resize: vertical;
 }
-#edit .wikitoolbar { float: left; margin-left: -1px }
-#edit div.trac-resizable { clear: both; width: 100% }
+#edit .wikitoolbar { float: left }
+#edit div.trac-resizable { clear: both }
 #edit + #info { margin-top: 1em }
 #edit + #attachments { margin-top: 1.5em }
 #changeinfo { padding: .5em }
@@ -118,11 +114,6 @@
  div.trac-modifiedby span.trac-print { display: block; }
 }
 
-/* Styles related to RTL support */
-.rtl { direction: rtl; }
-.rtl div.wiki-toc { float: left; }
-.rtl .wiki-toc ul ul, .wiki-toc ol ol { padding-right: 1.2em }
-
 /* TracIni default value */
 div.tracini td.default { font-size: 90% }
 div.tracini td.nodefault {
diff --git a/trac/trac/htdocs/js/auto_preview.js b/trac/trac/htdocs/js/auto_preview.js
index c0c790a..a1dfd1b 100644
--- a/trac/trac/htdocs/js/auto_preview.js
+++ b/trac/trac/htdocs/js/auto_preview.js
@@ -92,9 +92,9 @@
     }
 
     var values = form.serializeArray();
-    return inputs.each(function() {
-      $(this).keydown(trigger).keypress(trigger).change(trigger).blur(trigger);
-    });
+    // See #11510
+    return inputs.bind('input cut paste keydown keypress change blur',
+                       trigger);
   };
 
   // Enable automatic previewing to <textarea> elements.
@@ -150,7 +150,10 @@
         return true;
       }
 
-      $(this).keydown(trigger).keypress(trigger).blur(trigger);
+      // "input" event to detect editing using IMEs on Firefox,
+      // "cut" and "paste" events to detect editing using context
+      // menu on Internet Explorer (#11510)
+      $(this).bind('input cut paste keydown keypress blur', trigger);
     });
   };
 })(jQuery);
diff --git a/trac/trac/htdocs/js/jquery-ui-i18n.js b/trac/trac/htdocs/js/jquery-ui-i18n.js
index 5868269..c4d35dc 100644
--- a/trac/trac/htdocs/js/jquery-ui-i18n.js
+++ b/trac/trac/htdocs/js/jquery-ui-i18n.js
@@ -22,6 +22,7 @@
     dateFormat: jquery_ui.date_format,
     firstDay: jquery_ui.first_week_day,
     isRTL: false,
+    showButtonPanel: true,
     showMonthAfterYear: formatMonth.indexOf('$month') >
                         formatMonth.indexOf('$year')
   });
diff --git a/trac/trac/htdocs/js/query.js b/trac/trac/htdocs/js/query.js
index aaff1d2..83a8d06 100644
--- a/trac/trac/htdocs/js/query.js
+++ b/trac/trac/htdocs/js/query.js
@@ -380,13 +380,15 @@
 
     // Add a checkbox at the top of the column
     // to select every ticket in the group.
-    $("table.listing tr th.id").each(function() {
-      $(this).before(
-        $('<th class="batchmod_selector sel">').append(
-          $('<input type="checkbox" name="batchmod_toggleGroup" />').attr({
-            title: _("Toggle selection of all tickets shown in this group")
-          })));
-    });
+    if ($("table.listing tr td.id").length) {
+        $("table.listing tr th.id").each(function() {
+          $(this).before(
+            $('<th class="batchmod_selector sel">').append(
+              $('<input type="checkbox" name="batchmod_toggleGroup" />').attr({
+                title: _("Toggle selection of all tickets shown in this group")
+              })));
+        });
+    }
 
     // Add the click behavior for the group toggle.
     $("input[name='batchmod_toggleGroup']").click(function() {
@@ -443,6 +445,13 @@
         }
       });
 
+      // Remove handler and class that prevent multi-submit
+      if (!valid) {
+        var form = $(this);
+        form.removeClass("trac-submit-is-disabled");
+        form.unbind("submit.prevent-submit");
+      }
+
       return valid;
     });
 
diff --git a/trac/trac/htdocs/js/threaded_comments.js b/trac/trac/htdocs/js/threaded_comments.js
index 0be448b..bb11638 100644
--- a/trac/trac/htdocs/js/threaded_comments.js
+++ b/trac/trac/htdocs/js/threaded_comments.js
@@ -83,7 +83,7 @@
     $.ajax({ url: form.attr('action'), type: 'POST', data: {
       save_prefs: true,
       ticket_comments_order: order,
-      __FORM_TOKEN: form_token,
+      __FORM_TOKEN: form_token
     }, dataType: 'text' });
   });
 
@@ -94,7 +94,7 @@
     $.ajax({ url: form.attr('action'), type: 'POST', data: {
       save_prefs: true,
       ticket_comments_only: !!commentsOnly.attr('checked'),
-      __FORM_TOKEN: form_token,
+      __FORM_TOKEN: form_token
     }, dataType: 'text' });
   });
 });
diff --git a/trac/trac/htdocs/js/trac.js b/trac/trac/htdocs/js/trac.js
index e5f1a4e..9c800c8 100644
--- a/trac/trac/htdocs/js/trac.js
+++ b/trac/trac/htdocs/js/trac.js
@@ -59,6 +59,29 @@
     });
   }
 
+  // Disable the form's submit action after the submit button is pressed by
+  // replacing it with a handler that cancels the action. The handler is
+  // removed when navigating away from the page so that the action will
+  // be enabled when using the back button to return to the page.
+  $.fn.disableOnSubmit = function() {
+    this.click(function() {
+      var form = $(this).closest("form");
+      if (form.hasClass("trac-submit-is-disabled")) {
+        form.bind("submit.prevent-submit", function() {
+          return false;
+        });
+        $(window).on("unload", function() {
+          form.unbind("submit.prevent-submit");
+        });
+      } else {
+        form.addClass("trac-submit-is-disabled");
+        $(window).on("unload", function() {
+          form.removeClass("trac-submit-is-disabled");
+        })
+      }
+    });
+  }
+
   $.loadStyleSheet = function(href, type) {
     type = type || "text/css";
     $(document).ready(function() {
diff --git a/trac/trac/htdocs/js/wikitoolbar.js b/trac/trac/htdocs/js/wikitoolbar.js
index b5def19..3d7b928 100644
--- a/trac/trac/htdocs/js/wikitoolbar.js
+++ b/trac/trac/htdocs/js/wikitoolbar.js
@@ -1,8 +1,5 @@
-
-
 (function($){
 
-
   window.addWikiFormattingToolbar = function(textarea) {
     if ((document.selection == undefined)
      && (textarea.setSelectionRange == undefined)) {
@@ -17,7 +14,13 @@
       a.href = "#";
       a.id = id;
       a.title = title;
-      a.onclick = function() { try { fn() } catch (e) { } return false };
+      a.onclick = function() {
+        if ($(textarea).prop("disabled") === false &&
+            $(textarea).prop("readonly") === false) {
+          try { fn() } catch (e) { }
+        }
+        return false;
+      };
       a.tabIndex = 400;
       toolbar.appendChild(a);
     }
@@ -84,10 +87,9 @@
     $(textarea).before(toolbar);
   }
 
-})(jQuery);
+  // Add toolbar to all <textarea> elements on the page with the class 'wikitext'.
+  $(document).ready(function() {
+    $("textarea.wikitext").each(function() { addWikiFormattingToolbar(this) });
+  });
 
-// Add the toolbar to all <textarea> elements on the page with the class
-// 'wikitext'.
-jQuery(document).ready(function($) {
-  $("textarea.wikitext").each(function() { addWikiFormattingToolbar(this) });
-});
+})(jQuery);
diff --git a/trac/trac/loader.py b/trac/trac/loader.py
index c208ae0..815bc41 100644
--- a/trac/trac/loader.py
+++ b/trac/trac/loader.py
@@ -182,7 +182,10 @@
                 version = (getattr(module, 'version', '') or
                            getattr(module, 'revision', ''))
                 # special handling for "$Rev$" strings
-                version = version.replace('$', '').replace('Rev: ', 'r')
+                if version != '$Rev$':
+                    version = version.replace('$', '').replace('Rev: ', 'r')
+                else:  # keyword hasn't been expanded
+                    version = ''
             plugins[dist.project_name] = {
                 'name': dist.project_name, 'version': version,
                 'path': dist.location, 'plugin_filename': plugin_filename,
@@ -239,7 +242,7 @@
                 pass    # Metadata not found
 
     for plugin in plugins:
-        base, ext = os.path.splitext(plugin['path'])
+        base, ext = os.path.splitext(plugin['path'].replace('\\', '/'))
         if ext == '.egg' and egg_frames:
             find_egg_frame_index(plugin)
         else:
diff --git a/trac/trac/locale/ca/LC_MESSAGES/messages-js.po b/trac/trac/locale/ca/LC_MESSAGES/messages-js.po
index 853c4c5..e75cb57 100644
--- a/trac/trac/locale/ca/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/ca/LC_MESSAGES/messages-js.po
@@ -1,21 +1,22 @@
 # Catalan translation of Trac-js.
-# Copyright © 2010 Edgewall Software
+# Copyright © 2010, 2013 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# Jordi Mallach <jordi@sindominio.net>, 2010.
+# Jordi Mallach <jordi@sindominio.net>, 2010, 2013.
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: Trac 0.12\n"
+"Project-Id-Version: Trac 1.0-dev\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
-"PO-Revision-Date: 2010-06-18 13:55+0200\n"
+"POT-Creation-Date: 2013-03-21 22:54+0100\n"
+"PO-Revision-Date: 2013-04-25 17:42+0200\n"
 "Last-Translator: Jordi Mallach <jordi@sindominio.net>\n"
 "Language-Team: Catalan <ca@dodds.net>\n"
-"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"Language: ca\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
+"Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
@@ -72,67 +73,65 @@
 #. and showMonthAfterYear
 #: trac/htdocs/js/jquery-ui-i18n.js:4
 msgid "$month$year"
-msgstr ""
+msgstr "$month$year"
 
 #. TRANSLATOR: Link that closes the datepicker
 #. TRANSLATOR: Link that closes the timepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:7 trac/htdocs/js/jquery-ui-i18n.js:39
 msgid "Done"
-msgstr ""
+msgstr "Fet"
 
 #. TRANSLATOR: Link to the previous month in the datepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:9
 msgid "Prev"
-msgstr ""
+msgstr "Ant"
 
 #. TRANSLATOR: Link to the next month in the datepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:11
 msgid "Next"
-msgstr ""
+msgstr "Seg"
 
 #. TRANSLATOR: Link to the current day in the datepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:13
 msgid "Today"
-msgstr ""
+msgstr "Avui"
 
 #. TRANSLATOR: Heading for the week-of-the-year column in the datepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:20
 msgid "Wk"
-msgstr ""
+msgstr "St"
 
 #. TRANSLATOR: Heading of the standalone timepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:30
 msgid "Choose Time"
-msgstr ""
+msgstr "Selecciona l'hora"
 
 #. TRANSLATOR: Time selector label
 #: trac/htdocs/js/jquery-ui-i18n.js:32
 msgid "Time"
-msgstr ""
+msgstr "Hora"
 
 #. TRANSLATOR: Time labels in the timepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:34
-#, fuzzy
 msgid "Hour"
-msgstr "o"
+msgstr "Hora"
 
 #: trac/htdocs/js/jquery-ui-i18n.js:34
 msgid "Minute"
-msgstr ""
+msgstr "Minut"
 
 #: trac/htdocs/js/jquery-ui-i18n.js:34
 msgid "Second"
-msgstr ""
+msgstr "Segon"
 
 #: trac/htdocs/js/jquery-ui-i18n.js:35
 msgid "Time Zone"
-msgstr ""
+msgstr "Fus horari"
 
 #. TRANSLATOR: Link to pick the current time in the timepicker
 #: trac/htdocs/js/jquery-ui-i18n.js:37
-#, fuzzy
 msgid "Now"
-msgstr "no"
+msgstr "Ara"
 
 #: trac/htdocs/js/query.js:132
 msgid "A filter already exists for that property"
@@ -160,20 +159,20 @@
 
 #: trac/htdocs/js/query.js:337
 msgid " remove:"
-msgstr ""
+msgstr " suprimeix:"
 
 #: trac/htdocs/js/query.js:347
 msgid " add:"
-msgstr ""
+msgstr " afegeix:"
 
 #: trac/htdocs/js/query.js:376
 #, python-format
 msgid "Select ticket %(id)s for modification"
-msgstr ""
+msgstr "Seleccioneu els %(id)s de tiquets a modificars"
 
 #: trac/htdocs/js/query.js:387
 msgid "Toggle selection of all tickets shown in this group"
-msgstr ""
+msgstr "Commuta la selecció de tots els tiquets mostrats en aquest grup"
 
 #: trac/htdocs/js/trac.js:7
 msgid "Link here"
@@ -227,4 +226,3 @@
 #, python-format
 msgid "Link to #%(id)s"
 msgstr "Enllaç a #%(id)s"
-
diff --git a/trac/trac/locale/ca/LC_MESSAGES/messages.po b/trac/trac/locale/ca/LC_MESSAGES/messages.po
index 944d910..02b01da 100644
--- a/trac/trac/locale/ca/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/ca/LC_MESSAGES/messages.po
@@ -1,35 +1,36 @@
 # Catalan translation of Trac.
-# Copyright © 2004, 2009, 2010, 2011 Edgewall Software.
+# Copyright © 2004, 2009, 2010, 2011, 2013 Edgewall Software.
 # This file is distributed under the same license as the trac package.
 # Núria Montesinos, 2004.
-# Jordi Mallach <jordi@sindominio.net>, 2004, 2009, 2010, 2011.
+# Jordi Mallach <jordi@sindominio.net>, 2004, 2009, 2010, 2011, 2013.
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: trac 0.12.3-dev\n"
+"Project-Id-Version: trac 1.0-dev\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
-"PO-Revision-Date: 2011-11-17 11:12+0100\n"
+"POT-Creation-Date: 2013-03-21 22:54+0100\n"
+"PO-Revision-Date: 2013-04-25 17:37+0200\n"
 "Last-Translator: Jordi Mallach <jordi@sindominio.net>\n"
 "Language-Team: Catalan <ca@dodds.net>\n"
-"Plural-Forms: nplurals=2; plural=n!=1\n"
+"Language: ca\n"
 "MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
+"Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Plural-Forms: nplurals=2; plural=n!=1\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
-"You appear to be using the PHP CGI binary. Trac requires the CLI version "
-"for syntax highlighting."
+"You appear to be using the PHP CGI binary. Trac requires the CLI version for "
+"syntax highlighting."
 msgstr ""
-"Sembla que esteu emprant el binari de PHP CGI. Trac requereix la versió "
-"CLI per al ressaltat de sintaxi."
+"Sembla que esteu emprant el binari de PHP CGI. Trac requereix la versió CLI "
+"per al ressaltat de sintaxi."
 
 #: tracopt/ticket/clone.py:49
 #, python-format
 msgid "%(summary)s (cloned)"
-msgstr ""
+msgstr "%(summary)s (clonat)"
 
 #: tracopt/ticket/clone.py:53
 #, python-format
@@ -38,15 +39,17 @@
 "----\n"
 "%(description)s"
 msgstr ""
+"Clonat des de #%(id)s:\n"
+"----\n"
+"%(description)s"
 
 #: tracopt/ticket/clone.py:60
-#, fuzzy
 msgid "Clone"
-msgstr "tancat"
+msgstr "Clona"
 
 #: tracopt/ticket/clone.py:61
 msgid "Create a copy of this ticket"
-msgstr ""
+msgstr "Crea una còpia d'aquest tiquet"
 
 #: tracopt/ticket/commit_updater.py:275
 msgid ""
@@ -172,12 +175,12 @@
 #: tracopt/versioncontrol/svn/svn_fs.py:664
 #, python-format
 msgid ""
-"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)."
+"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)."
 msgstr ""
-"Inconsistència al diff: La base és un %(oldnode)s (%(oldpath)s a la "
-"revisió %(oldrev)s) i la destinació és %(newnode)s (%(newpath)s a la "
-"revisió %(newrev)s)."
+"Inconsistència al diff: La base és un %(oldnode)s (%(oldpath)s a la revisió "
+"%(oldrev)s) i la destinació és %(newnode)s (%(newpath)s a la revisió "
+"%(newrev)s)."
 
 #: tracopt/versioncontrol/svn/svn_fs.py:823
 #, python-format
@@ -287,14 +290,14 @@
 #, python-format
 msgid "Cannot reparent attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
 msgstr ""
-"No es pot assignar un pare a l'adjunt «%(att)s» perquè %(realm)s:%(id)s "
-"és invàlid"
+"No es pot assignar un pare a l'adjunt «%(att)s» perquè %(realm)s:%(id)s és "
+"invàlid"
 
 #: trac/attachment.py:258
 #, python-format
 msgid ""
-"Cannot reparent attachment \"%(att)s\" as it already exists in "
-"%(realm)s:%(id)s"
+"Cannot reparent attachment \"%(att)s\" as it already exists in %(realm)s:"
+"%(id)s"
 msgstr ""
 "No es pot assignar un pare a l'adjunt «%(att)s» perquè ja existeix a "
 "%(realm)s:%(id)s"
@@ -377,20 +380,19 @@
 msgstr "L'adjunt és invàlid: %(message)s"
 
 #: trac/attachment.py:745
-#, fuzzy
 msgid "Note: File must be selected again."
-msgstr "No s'ha seleccionat cap fita"
+msgstr "Nota: S'ha de tornar a seleccionar el fitxer."
 
 #: trac/attachment.py:758
 #, python-format
 msgid ""
-"You don't have permission to replace the attachment %(name)s. You can "
-"only replace your own attachments. Replacing other's attachments requires"
-" ATTACHMENT_DELETE permission."
+"You don't have permission to replace the attachment %(name)s. You can only "
+"replace your own attachments. Replacing other's attachments requires "
+"ATTACHMENT_DELETE permission."
 msgstr ""
-"No teniu permís per a reemplaçar l'adjunt %(name)s. Només podeu "
-"reemplaçar els adjunts propis. Reemplaçar els adjunts d'altres requereix "
-"permisos «ATTACHMENT_DELETE»."
+"No teniu permís per a reemplaçar l'adjunt %(name)s. Només podeu reemplaçar "
+"els adjunts propis. Reemplaçar els adjunts d'altres requereix permisos "
+"«ATTACHMENT_DELETE»."
 
 #: trac/attachment.py:789
 #, python-format
@@ -398,14 +400,13 @@
 msgstr "%(attachment)s (suprimeix)"
 
 #: trac/attachment.py:803
-#, fuzzy, python-format
+#, python-format
 msgid "Maximum total attachment size: %(num)s bytes"
-msgstr "Mida màxima dels adjunts: %(num)s octets"
+msgstr "Mida màxima total dels adjunts: %(num)s octets"
 
 #: trac/attachment.py:804
-#, fuzzy
 msgid "Download failed"
-msgstr "Ha fallat la pujada"
+msgstr "Ha fallat la baixada"
 
 #: trac/attachment.py:892 trac/versioncontrol/web_ui/browser.py:669
 #: trac/wiki/web_ui.py:73
@@ -473,6 +474,7 @@
 #, python-format
 msgid "Error reading '%(file)s', make sure it is readable."
 msgstr ""
+"S'ha produït un error en llegir «%(file)s», assegureu-vos que és llegible."
 
 #: trac/config.py:420
 #, python-format
@@ -488,8 +490,7 @@
 #, python-format
 msgid "[%(section)s] %(entry)s: expected one of (%(choices)s), got %(value)s"
 msgstr ""
-"[%(section)s] %(entry)s: s'esperava un de (%(choices)s), s'ha rebut "
-"%(value)s"
+"[%(section)s] %(entry)s: s'esperava un de (%(choices)s), s'ha rebut %(value)s"
 
 #: trac/config.py:761 trac/config.py:774
 #, python-format
@@ -502,11 +503,11 @@
 
 #: trac/env.py:218
 msgid ""
-"Visit the Trac open source project at<br /><a "
-"href=\"http://trac.edgewall.org/\">http://trac.edgewall.org/</a>"
+"Visit the Trac open source project at<br /><a href=\"http://trac.edgewall."
+"org/\">http://trac.edgewall.org/</a>"
 msgstr ""
-"Visiteu el projecte de programari lliure del Trac a<br /><a "
-"href=\"http://trac.edgewall.org/\">http://trac.edgewall.org/</a>"
+"Visiteu el projecte de programari lliure del Trac a<br /><a href=\"http://"
+"trac.edgewall.org/\">http://trac.edgewall.org/</a>"
 
 #: trac/env.py:761
 msgid "Database newer than Trac version"
@@ -516,13 +517,12 @@
 #, python-format
 msgid "No upgrade module for version %(num)i (%(version)s.py)"
 msgstr ""
-"No hi ha cap mòdul d'actualització per a la versió %(num)i "
-"(%(version)s.py)"
+"No hi ha cap mòdul d'actualització per a la versió %(num)i (%(version)s.py)"
 
 #: trac/env.py:825
 msgid ""
-"Missing environment variable \"TRAC_ENV\". Trac requires this variable to"
-" point to a valid Trac environment."
+"Missing environment variable \"TRAC_ENV\". Trac requires this variable to "
+"point to a valid Trac environment."
 msgstr ""
 "Manca la variable d'entorn «TRAC_ENV». El Trac requereix que aquesta "
 "variable apunte a un entorn de Trac vàlid."
@@ -547,9 +547,9 @@
 msgstr "S'estan creant els scripts."
 
 #: trac/env.py:923
-#, fuzzy, python-format
+#, python-format
 msgid "Invalid argument '%(arg)s'"
-msgstr "Els arguments no són vàlids"
+msgstr "L'argument «%(arg)s no és vàlid"
 
 #: trac/env.py:928
 #, python-format
@@ -567,7 +567,7 @@
 
 #: trac/env.py:965
 msgid "Backing up database ..."
-msgstr ""
+msgstr "S'està fent una còpia de seguretat de la base de dades…"
 
 #: trac/env.py:970
 msgid "Hotcopy done."
@@ -582,17 +582,16 @@
 msgstr "La base de dades està al dia, no cal una actualització."
 
 #: trac/env.py:984
-#, fuzzy
 msgid ""
 "The pre-upgrade backup failed.\n"
 "Use '--no-backup' to upgrade without doing a backup.\n"
 msgstr ""
-"Ha fallat la còpia de seguretat: %(msg)s.\n"
-"Empreu «--no-backup» per a actualitzar sense fer una còpia de seguretat."
+"Ha fallat la còpia de seguretat prèvia a l'actualització.\n"
+"Empreu «--no-backup» per a actualitzar sense fer una còpia de seguretat.\n"
 
 #: trac/env.py:988
 msgid "The upgrade failed. Please fix the issue and try again.\n"
-msgstr ""
+msgstr "L'actualització ha fallat. Corregiu el problema i torneu-ho a provar.\n"
 
 #: trac/env.py:1000
 msgid ""
@@ -607,8 +606,7 @@
 #, python-format
 msgid ""
 "Error while removing wiki-macros: %(err)s\n"
-"Trac doesn't load plugins from wiki-macros anymore. Please remove it by "
-"hand."
+"Trac doesn't load plugins from wiki-macros anymore. Please remove it by hand."
 msgstr ""
 "S'ha produït un error en suprimir wiki-macros: %(err)s\n"
 "El trac ja no carrega connectors des de wiki-macros. Suprimiu-lo a mà."
@@ -624,8 +622,7 @@
 msgstr ""
 "S'ha completat l'actualització.\n"
 "\n"
-"És possible que ara vulgueu actualitzar la documentació del Trac "
-"executant:\n"
+"És possible que ara vulgueu actualitzar la documentació del Trac executant:\n"
 "\n"
 "  trac-admin %(path)s wiki upgrade"
 
@@ -637,8 +634,8 @@
 #, python-format
 msgid "Invalid email encoding setting: %(pref)s"
 msgstr ""
-"El valor establert per a la codificació del correu electrònic és invàlid:"
-" %(pref)s"
+"El valor establert per a la codificació del correu electrònic és invàlid: "
+"%(pref)s"
 
 #: trac/notification.py:337
 msgid "Unable to send email due to identity crisis."
@@ -660,8 +657,8 @@
 #: trac/perm.py:56
 #, fuzzy, python-format
 msgid ""
-"%(perm)s privileges are required to perform this operation on "
-"%(resource)s. You don't have the required permissions."
+"%(perm)s privileges are required to perform this operation on %(resource)s. "
+"You don't have the required permissions."
 msgstr ""
 "Es requereixen privilegis de %(perm)s per a realitzar aquesta operació a "
 "%(resource)s"
@@ -669,9 +666,11 @@
 #: trac/perm.py:58
 #, fuzzy, python-format
 msgid ""
-"%(perm)s privileges are required to perform this operation. You don't "
-"have the required permissions."
+"%(perm)s privileges are required to perform this operation. You don't have "
+"the required permissions."
 msgstr ""
+"Es requereixen privilegis de %(perm)s per a realitzar aquesta operació a "
+"%(resource)s"
 
 #: trac/perm.py:64
 msgid "Insufficient privileges to perform this operation."
@@ -703,12 +702,12 @@
 #: trac/perm.py:675
 #, fuzzy, python-format
 msgid "The user %(user)s already has permission %(action)s."
-msgstr "S'ha concedit el permís %(action)s al subjecte %(user)s."
+msgstr "S'ha concedit el permís %(action)s al subjecte %(subject)s."
 
 #: trac/perm.py:689
-#, python-format
+#, fuzzy, python-format
 msgid "Cannot remove permission %(action)s for user %(user)s."
-msgstr ""
+msgstr "Ja s'havia concedit el permís %(action)s a %(subject)s."
 
 #: trac/perm.py:706
 #, fuzzy, python-format
@@ -723,8 +722,8 @@
 #: trac/perm.py:727
 #, fuzzy, python-format
 msgid ""
-"Invalid user %(user)s on line %(line)d: All upper-cased tokens are "
-"reserved for permission names."
+"Invalid user %(user)s on line %(line)d: All upper-cased tokens are reserved "
+"for permission names."
 msgstr "Els testimonis en majúscules són reservats per a noms de permisos"
 
 #: trac/perm.py:736
@@ -735,7 +734,7 @@
 #: trac/perm.py:741
 #, fuzzy, python-format
 msgid "Cannot import from %(filename)s: %(error)s"
-msgstr ""
+msgstr "  %(page)s importada des de %(filename)s"
 
 #: trac/resource.py:336
 #, python-format
@@ -752,7 +751,7 @@
 msgstr "Error: %(msg)s"
 
 #: trac/admin/console.py:132
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "Welcome to trac-admin %(version)s\n"
 "Interactive Trac administration console.\n"
@@ -763,9 +762,9 @@
 msgstr ""
 "Benvingut/da al trac-admin %(version)s\n"
 "Consola d'administració del Trac interactiva.\n"
-"Copyright © 2003-2011 Edgewall Software\n"
+"Copyright © 2003-2013 Edgewall Software\n"
 "\n"
-"Teclegeu: «?» o «help» per a obtenir ajuda sobre les ordres.\n"
+"Teclegeu: «?» o «help» per obtenir ajuda sobre les ordres.\n"
 "        "
 
 #: trac/admin/console.py:166
@@ -779,17 +778,18 @@
 msgstr "Error de la compleció: %(err)s"
 
 #: trac/admin/console.py:316
-#, fuzzy, python-format
+#, python-format
 msgid ""
-"No documentation found for '%(cmd)s'. Use 'help' to see the list of "
-"commands."
-msgstr "No s'ha trobat documentació per a «%(cmd)s»"
+"No documentation found for '%(cmd)s'. Use 'help' to see the list of commands."
+msgstr ""
+"No s'ha trobat documentació per a «%(cmd)s». Empreu «help» per veure la "
+"llista d'ordres."
 
 #: trac/admin/console.py:322
 msgid "Did you mean this?"
 msgid_plural "Did you mean one of these?"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "Volíeu dir això?"
+msgstr[1] "Volíeu dir una d'aquestes?"
 
 #: trac/admin/console.py:326
 #, python-format
@@ -797,8 +797,10 @@
 msgstr "trac-admin - La consola d'administració del Trac %(version)s"
 
 #: trac/admin/console.py:330
-msgid "Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]\n"
-msgstr "Forma d'ús: trac-admin </camí/al/entornproj> [ordre [subordre] opció …]]\n"
+msgid ""
+"Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]\n"
+msgstr ""
+"Forma d'ús: trac-admin </camí/al/entornproj> [ordre [subordre] opció …]]\n"
 
 #: trac/admin/console.py:333
 msgid "Invoking trac-admin without command starts interactive mode.\n"
@@ -867,11 +869,12 @@
 msgstr "El directori existeix i no és buit."
 
 #: trac/admin/console.py:413
-#, fuzzy, python-format
+#, python-format
 msgid ""
-"Base directory '%(env)s' does not exist. Please create it manually and "
-"retry."
+"Base directory '%(env)s' does not exist. Please create it manually and retry."
 msgstr ""
+"El directori base «%(env)s» no existeix. Creeu-lo manualment i torneu-ho a "
+"provar."
 
 #: trac/admin/console.py:441
 msgid "Creating and Initializing Project"
@@ -980,6 +983,15 @@
 "[[TracAdminHelp(upgrade)]]      # the upgrade command\n"
 "}}}"
 msgstr ""
+"Mostra l'ajuda per a les ordres de trac-admin.\n"
+"\n"
+"Exemples:\n"
+"{{{\n"
+"[[TracAdminHelp]]               # totes les ordres\n"
+"[[TracAdminHelp(wiki)]]         # totes les ordres de wiki\n"
+"[[TracAdminHelp(wiki export)]]  # l'ordre «wiki export»\n"
+"[[TracAdminHelp(upgrade)]]      # l'ordre upgrade\n"
+"}}}"
 
 #: trac/admin/console.py:580
 #, python-format
@@ -1003,9 +1015,8 @@
 msgstr "El quadre d'administració és desconegut"
 
 #: trac/admin/web_ui.py:133
-#, fuzzy
 msgid "Untitled"
-msgstr "Títol"
+msgstr "Sense títol"
 
 #: trac/admin/web_ui.py:192 trac/ticket/admin.py:66 trac/ticket/admin.py:95
 #: trac/ticket/admin.py:275 trac/ticket/admin.py:455 trac/ticket/admin.py:607
@@ -1016,8 +1027,8 @@
 
 #: trac/admin/web_ui.py:197 trac/ticket/admin.py:69
 msgid ""
-"Error writing to trac.ini, make sure it is writable by the web server. "
-"Your changes have not been saved."
+"Error writing to trac.ini, make sure it is writable by the web server. Your "
+"changes have not been saved."
 msgstr ""
 "S'ha produït un error en escriure al trac.ini, assegureu-vos que és "
 "escrivible pel servidor web. No es desaran els canvis."
@@ -1180,33 +1191,30 @@
 msgstr "Descripció:"
 
 #: trac/admin/templates/admin_basics.html:35
-#, fuzzy
 msgid "Default timezone:"
-msgstr "Fus horari per defecte"
+msgstr "Fus horari per defecte:"
 
 #: trac/admin/templates/admin_basics.html:37
-#, fuzzy
 msgid "Server's local time zone"
-msgstr "Fus horari per defecte"
+msgstr "Fus horari local del servidor"
 
 #: trac/admin/templates/admin_basics.html:44
-#, fuzzy
 msgid "Default language:"
-msgstr "Llengua per defecte"
+msgstr "Llengua per defecte:"
 
 #: trac/admin/templates/admin_basics.html:46
 #: trac/admin/templates/admin_basics.html:55
 msgid "Browser's language"
-msgstr ""
+msgstr "Llengua del navegador"
 
 #: trac/admin/templates/admin_basics.html:53
 msgid "Default date format:"
-msgstr ""
+msgstr "Format de data per defecte:"
 
 #: trac/admin/templates/admin_basics.html:57
 #: trac/prefs/templates/prefs_datetime.html:65
 msgid "ISO 8601 format"
-msgstr ""
+msgstr "Format ISO 8601"
 
 #: trac/admin/templates/admin_basics.html:63
 #: trac/admin/templates/admin_components.html:99
@@ -1282,7 +1290,7 @@
 #: trac/admin/templates/admin_components.html:98
 #: trac/admin/templates/admin_enums.html:69
 #: trac/admin/templates/admin_milestones.html:131
-#: trac/admin/templates/admin_perms.html:109
+#: trac/admin/templates/admin_perms.html:111
 #: trac/admin/templates/admin_versions.html:99
 #: trac/versioncontrol/templates/admin_repositories.html:145
 msgid "Remove selected items"
@@ -1335,8 +1343,7 @@
 "[1:Note:] The order of priorities determines the\n"
 "              coloring of entries in the ticket queries and reports."
 msgstr ""
-"[1:Note:] L'ordre de les prioritats determina la coloració de les "
-"entrades\n"
+"L'ordre de les prioritats determina la coloració de les entrades\n"
 "              a les consultes de tiquets i informes."
 
 #: trac/admin/templates/admin_logging.html:26 trac/templates/about.html:85
@@ -1436,9 +1443,8 @@
 msgstr "Tiquets"
 
 #: trac/admin/templates/admin_perms.html:14
-#, fuzzy
 msgid "Manage Permissions and Groups"
-msgstr "Gestiona els permisos"
+msgstr "Gestiona els permisos i grups"
 
 #: trac/admin/templates/admin_perms.html:19
 msgid "Grant Permission:"
@@ -1459,8 +1465,8 @@
 "Grant permission for an action to a subject, which can be either a user\n"
 "            or a group."
 msgstr ""
-"Atorga permisos per a una acció a un subjecte, que pot ser un usuari o un"
-" grup"
+"Atorga permisos per a una acció a un subjecte, que pot ser un usuari o un "
+"grup"
 
 #: trac/admin/templates/admin_perms.html:42
 msgid "Add Subject to Group:"
@@ -1475,33 +1481,32 @@
 msgstr "Afegeix un usuari o grup a un grup de permisos existent."
 
 #: trac/admin/templates/admin_perms.html:63
-#: trac/admin/templates/admin_perms.html:88
+#: trac/admin/templates/admin_perms.html:90
 msgid "Subject"
 msgstr "Subjecte"
 
 #: trac/admin/templates/admin_perms.html:76
-msgid "Action is no longer defined"
-msgstr ""
+#, python-format
+msgid "%(action)s is no longer defined"
+msgstr "%(action)s ja no està definit"
 
-#: trac/admin/templates/admin_perms.html:81
-#, fuzzy
+#: trac/admin/templates/admin_perms.html:83
 msgid "No permissions"
-msgstr "Permisos"
+msgstr "Cap permís"
 
-#: trac/admin/templates/admin_perms.html:85
+#: trac/admin/templates/admin_perms.html:87
 msgid "Group Membership"
-msgstr ""
+msgstr "Pertinença a grups"
 
-#: trac/admin/templates/admin_perms.html:88
-#, fuzzy
+#: trac/admin/templates/admin_perms.html:90
 msgid "Group"
-msgstr "Grup:"
+msgstr "Grup"
 
-#: trac/admin/templates/admin_perms.html:105
+#: trac/admin/templates/admin_perms.html:107
 msgid "No group memberships"
-msgstr ""
+msgstr "Cap pertinença a grups"
 
-#: trac/admin/templates/admin_perms.html:113
+#: trac/admin/templates/admin_perms.html:115
 msgid ""
 "Note that [1:Subject] or [2:Group] names can't be all upper-case,\n"
 "      as that is reserved for permission names."
@@ -1537,7 +1542,8 @@
 msgid "Upload a plugin packaged as Python egg."
 msgstr "Puja un connector empaquetat com a ou de Python."
 
-#: trac/admin/templates/admin_plugins.html:100 trac/templates/diff_view.html:51
+#: trac/admin/templates/admin_plugins.html:100
+#: trac/templates/diff_view.html:51
 #: trac/versioncontrol/templates/changeset.html:142
 msgid "Author:"
 msgstr "Autor:"
@@ -1580,17 +1586,14 @@
 msgstr "Modifica la versió:"
 
 #: trac/admin/templates/admin_versions.html:31
-msgid "Date:"
-msgstr "Data:"
+#: trac/admin/templates/admin_versions.html:64
+msgid "Released:"
+msgstr "Llançada:"
 
 #: trac/admin/templates/admin_versions.html:59
 msgid "Add Version:"
 msgstr "Afegeix una versió:"
 
-#: trac/admin/templates/admin_versions.html:64
-msgid "Released:"
-msgstr "Llançada:"
-
 #: trac/admin/templates/admin_versions.html:83
 msgid "Released"
 msgstr "Llançada"
@@ -1603,8 +1606,8 @@
 #: trac/db/api.py:347
 #, python-format
 msgid ""
-"Unknown scheme \"%(scheme)s\"; database connection string must start with"
-" {scheme}:/"
+"Unknown scheme \"%(scheme)s\"; database connection string must start with "
+"{scheme}:/"
 msgstr ""
 "L'esquema «%(scheme)s» és desconegut. La cadena de connexió a la base de "
 "dades ha de començar amb {scheme}:/"
@@ -1632,7 +1635,8 @@
 #: trac/db/pool.py:130
 #, python-format
 msgid "Unable to get database connection within %(time)d seconds."
-msgstr "No s'ha pogut obtenir la connexió a la base de dades en %(time)d segons."
+msgstr ""
+"No s'ha pogut obtenir la connexió a la base de dades en %(time)d segons."
 
 #: trac/db/postgres_backend.py:81
 msgid "Cannot load Python bindings for PostgreSQL"
@@ -1669,11 +1673,11 @@
 #: trac/db/sqlite_backend.py:271
 #, python-format
 msgid ""
-"The user %(user)s requires read _and_ write permissions to the database "
-"file %(path)s and the directory it is located in."
+"The user %(user)s requires read _and_ write permissions to the database file "
+"%(path)s and the directory it is located in."
 msgstr ""
-"L'usuari %(user)s requereix permisos de lectura _i_ escriptura al fitxer "
-"de la base de dades %(path)s i al directori on està ubicat."
+"L'usuari %(user)s requereix permisos de lectura _i_ escriptura al fitxer de "
+"la base de dades %(path)s i al directori on està ubicat."
 
 #: trac/mimeview/api.py:685 trac/mimeview/api.py:695
 #, python-format
@@ -1743,7 +1747,7 @@
 
 #: trac/prefs/web_ui.py:97 trac/prefs/templates/prefs_userinterface.html:10
 msgid "User Interface"
-msgstr ""
+msgstr "Interfície d'usuari"
 
 #: trac/prefs/web_ui.py:99 trac/prefs/templates/prefs_language.html:10
 msgid "Language"
@@ -1766,15 +1770,14 @@
 "This page lets you customize your personal settings for this site.\n"
 "      These settings are stored on the server and are identified by a "
 "session\n"
-"      key stored in a browser cookie. That cookie allows your settings to"
-" be\n"
+"      key stored in a browser cookie. That cookie allows your settings to "
+"be\n"
 "      restored on subsequent visits."
 msgstr ""
-"Aquesta pàgina us permet configurar les vostres preferències personals "
-"per a\n"
+"Aquesta pàgina us permet configurar les vostres preferències personals per "
+"a\n"
 "      aquest lloc. Aquestes preferències s'emmagatzemen al servidor i\n"
-"      s'identifiquen per una clau de sessió emmagatzemada en una galeta "
-"del\n"
+"      s'identifiquen per una clau de sessió emmagatzemada en una galeta del\n"
 "      navegador. Aquesta galeta us permet restaurar les vostres "
 "preferències\n"
 "      en visites posteriors."
@@ -1817,8 +1820,7 @@
 #: trac/prefs/templates/prefs_advanced.html:30
 msgid ""
 "You may load a previously created session by entering the\n"
-"      corresponding session key below. This lets you share settings "
-"between\n"
+"      corresponding session key below. This lets you share settings between\n"
 "      multiple computers and web browsers."
 msgstr ""
 "Podeu carregar una sessió creada anteriorment introduint la clau de la\n"
@@ -1858,44 +1860,39 @@
 "            [1:%(formatted)s]."
 
 #: trac/prefs/templates/prefs_datetime.html:45
-#, fuzzy, python-format
+#, python-format
 msgid "In the default time zone, this would be displayed as [1:%(formatted)s]."
-msgstr ""
-"Al fus horari per defecte, això es mostraria com\n"
-"            [1:%(formatted)s]."
+msgstr "Al fus horari per defecte, això es mostraria com [1:%(formatted)s]."
 
 #: trac/prefs/templates/prefs_datetime.html:51
 msgid ""
-"Note: Universal Co-ordinated Time (UTC) is also known as Greenwich Mean "
-"Time (GMT).[1:]\n"
+"Note: Universal Co-ordinated Time (UTC) is also known as Greenwich Mean Time "
+"(GMT).[1:]\n"
 "        A positive offset is used to indicate a timezone at the east of "
 "Greenwich, i.e. ahead of Universal Time."
 msgstr ""
-"Nota: L'Hora Universal Coordinada (UTC) també és coneguda com a Greenwich"
-" Mean Time (GMT).[1:]\n"
+"Nota: L'Hora Universal Coordinada (UTC) també és coneguda com a Greenwich "
+"Mean Time (GMT).[1:]\n"
 "        Un òfset positiu indica un fus horari a l'est de Greenwich, és a "
 "dir, per davant de l'Hora Universal."
 
 #: trac/prefs/templates/prefs_datetime.html:59
-#, fuzzy
 msgid "Date format:"
-msgstr "Informació del sistema:"
+msgstr "Format de la data:"
 
 #: trac/prefs/templates/prefs_datetime.html:61
-#, fuzzy
 msgid "Default date format"
-msgstr "Fus horari per defecte"
+msgstr "Format de la data per defecte"
 
 #: trac/prefs/templates/prefs_datetime.html:63
 msgid "Your language setting"
-msgstr ""
+msgstr "La vostra preferència de llengua"
 
 #: trac/prefs/templates/prefs_datetime.html:69
 #, fuzzy
 msgid ""
 "Configuring your date format will result in formatting\n"
-"      and parsing datetime displayed on this site to use your date format"
-"\n"
+"      and parsing datetime displayed on this site to use your date format\n"
 "      instead of that of the server."
 msgstr ""
 "Si configureu el vostre fus horari, les dates i hores\n"
@@ -1904,33 +1901,30 @@
 
 #: trac/prefs/templates/prefs_datetime.html:75
 msgid "Date relative/absolute format:"
-msgstr ""
+msgstr "Format de la data relativa/absoluta:"
 
 #: trac/prefs/templates/prefs_datetime.html:77
-#, fuzzy
 msgid "Default format"
-msgstr "(per defecte)"
+msgstr "Format per defecte"
 
 #: trac/prefs/templates/prefs_datetime.html:79
 msgid "Relative format"
-msgstr ""
+msgstr "Format relatiu"
 
 #: trac/prefs/templates/prefs_datetime.html:81
-#, fuzzy
 msgid "Absolute format"
-msgstr "Informació del sistema"
+msgstr "Format absolut"
 
 #: trac/prefs/templates/prefs_datetime.html:85
-#, fuzzy
 msgid ""
 "Configuring your relative/absolute format will result in\n"
-"      formatting datetime displayed on this site to use your format "
-"instead of\n"
+"      formatting datetime displayed on this site to use your format instead "
+"of\n"
 "      that of the server."
 msgstr ""
-"Si configureu el vostre fus horari, les dates i hores\n"
-"      mostrades a aquest lloc empraran el vostre fus horari\n"
-"      en lloc del del servidor."
+"Si configureu el vostre format relatiu/absolut, les dates i hores\n"
+"      mostrades a aquest lloc empraran el vostre format en lloc\n"
+"      del del servidor."
 
 #: trac/prefs/templates/prefs_general.html:15
 msgid "Full name:"
@@ -1965,8 +1959,8 @@
 #: trac/prefs/templates/prefs_keybindings.html:21
 msgid ""
 "This site provides keyboard shortcuts for\n"
-"      faster access to certain functions of this site. As these shortcuts"
-" can\n"
+"      faster access to certain functions of this site. As these shortcuts "
+"can\n"
 "      cause conflicts with shortcuts provided by the desktop system or\n"
 "      web browser, they are disabled by default. See\n"
 "      [1:TracAccessibility]\n"
@@ -2005,7 +1999,7 @@
 "The [1:Default language] option uses the browser's\n"
 "        language negotiation feature to select the appropriate language."
 msgstr ""
-"L'opció [1:Llengua per defecte] empra la funcionalitat\n"
+"L'opció «Llengua per defecte» empra la funcionalitat\n"
 "        de negociació de llengua del navegador per a seleccionar la\n"
 "        llengua apropiada."
 
@@ -2027,23 +2021,27 @@
 
 #: trac/prefs/templates/prefs_userinterface.html:18
 msgid "Use only symbols for buttons."
-msgstr ""
+msgstr "Empra només símbols per als botons."
 
 #: trac/prefs/templates/prefs_userinterface.html:21
 msgid ""
 "Display only the icon or symbol for\n"
 "      short inline buttons, and hide the text caption."
 msgstr ""
+"Mostra només la icona i símbol per\n"
+"       botons curts en línea, i amaga els subtítols de text."
 
 #: trac/prefs/templates/prefs_userinterface.html:29
 msgid "Hide help links."
-msgstr ""
+msgstr "Amaga els enllaços de l'ajuda."
 
 #: trac/prefs/templates/prefs_userinterface.html:32
 msgid ""
 "Don't show the various help links.\n"
 "      This reduces the verbosity of the pages."
 msgstr ""
+"No mostres els diferents enllaços d'ajuda.\n"
+"      Això redueix la verbositat de les pàgines."
 
 #: trac/search/web_ui.py:72 trac/search/templates/search.html:12
 #: trac/search/templates/search.html:26 trac/search/templates/search.html:31
@@ -2060,21 +2058,23 @@
 #, python-format
 msgid "Search query too short. Query must be at least %(num)s characters long."
 msgstr ""
-"La consulta de cerca és massa curta. La consulta ha de tenir almenys "
-"%(num)s caràcters."
+"La consulta de cerca és massa curta. La consulta ha de tenir almenys %(num)s "
+"caràcters."
 
-#: trac/search/web_ui.py:245 trac/ticket/query.py:785 trac/ticket/report.py:459
+#: trac/search/web_ui.py:245 trac/ticket/query.py:785
+#: trac/ticket/report.py:459
 msgid "Next Page"
 msgstr "Pàgina següent"
 
-#: trac/search/web_ui.py:251 trac/ticket/query.py:790 trac/ticket/report.py:462
+#: trac/search/web_ui.py:251 trac/ticket/query.py:790
+#: trac/ticket/report.py:462
 msgid "Previous Page"
 msgstr "Pàgina anterior"
 
 #: trac/search/templates/search.html:11
 #, fuzzy
 msgid "Search Results"
-msgstr "Cap resultat"
+msgstr "Resultats"
 
 #: trac/search/templates/search.html:43
 #: trac/ticket/templates/query_results.html:20
@@ -2115,15 +2115,12 @@
 "Trac is a web-based software project management and bug/issue\n"
 "        tracking system emphasizing ease of use and low ceremony.\n"
 "        It provides an integrated Wiki, an interface to version control\n"
-"        systems, and a number of convenient ways to stay on top of events"
-"\n"
+"        systems, and a number of convenient ways to stay on top of events\n"
 "        and changes within a project."
 msgstr ""
-"El Trac és un gestor de projectes de programari i gestor "
-"d'errors/problemes\n"
+"El Trac és un gestor de projectes de programari i gestor d'errors/problemes\n"
 "        basat en web, que enfatitza la facilitat d'ús i la simplicitat.\n"
-"        Proveeix un Wiki integrat, una interfície per a sistemes de "
-"control\n"
+"        Proveeix un Wiki integrat, una interfície per a sistemes de control\n"
 "        de versions i un nombre de maneres convenients d'estar informat "
 "dels\n"
 "        esdeveniments i canvis que tenen lloc a un projecte."
@@ -2153,12 +2150,11 @@
 "        [1:http://trac.edgewall.org/]"
 
 #: trac/templates/about.html:46
-#, fuzzy
 msgid ""
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2011\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
@@ -2166,9 +2162,8 @@
 msgstr "Informació del sistema"
 
 #: trac/templates/about.html:56
-#, fuzzy
 msgid "Package"
-msgstr "canvi"
+msgstr "Paquet"
 
 #: trac/templates/about.html:56 trac/templates/about.html:69
 #: trac/templates/history_view.html:28 trac/ticket/admin.py:431
@@ -2258,9 +2253,9 @@
 msgid "Attach another file"
 msgstr "Adjunta un altre fitxer"
 
-#: trac/templates/attachment.html:98 trac/templates/list_of_attachments.html:21
-#: trac/templates/macros.html:19 trac/util/text.py:621
-#: trac/versioncontrol/templates/browser.html:189
+#: trac/templates/attachment.html:98
+#: trac/templates/list_of_attachments.html:21 trac/templates/macros.html:19
+#: trac/util/text.py:621 trac/versioncontrol/templates/browser.html:189
 #: trac/versioncontrol/templates/dir_entries.html:17
 #, python-format
 msgid "%(size)s bytes"
@@ -2533,8 +2528,7 @@
 "                [1:[2:searching]\n"
 "                for similar issues], as it is quite likely that this "
 "problem\n"
-"                has been reported before. For questions about "
-"installation\n"
+"                has been reported before. For questions about installation\n"
 "                and configuration of Trac or its plugins, please try the\n"
 "                [3:mailing list]\n"
 "                instead of creating a ticket."
@@ -2661,10 +2655,9 @@
 "       ([3:%(size)s]) -\n"
 "      added by [4:%(author)s] %(date)s."
 msgstr ""
-"[1:%(file)s]\n"
-"      [2:[3:]]\n"
-"       ([4:%(size)s])\n"
-"      afegit per [5:%(author)s], fa %(date)s)."
+"[1:%(file)s][2: ]\n"
+"       ([3:%(size)s])\n"
+"      afegit per [4:%(author)s], fa %(date)s)."
 
 #: trac/templates/list_of_attachments.html:28
 #: trac/templates/list_of_attachments.html:44
@@ -2676,12 +2669,12 @@
 #: trac/templates/list_of_attachments.html:54
 #, fuzzy
 msgid "Download all attachments as:"
-msgstr "Afegeix un adjunt"
+msgstr "Baixa en altres formats:"
 
 #: trac/templates/list_of_attachments.html:39
 #: trac/templates/list_of_attachments.html:55
 msgid ".zip"
-msgstr ""
+msgstr ".zip"
 
 #: trac/templates/macros.html:37 trac/templates/macros.html:38
 msgid "Previous"
@@ -2698,17 +2691,17 @@
 #: trac/templates/preview_file.html:19
 #, python-format
 msgid ""
-"[1:HTML preview not available], since the file size exceeds %(size)s "
-"bytes."
+"[1:HTML preview not available], since the file size exceeds %(size)s bytes."
 msgstr ""
-"[1:La previsualització HTML no és disponible], ja que el fitxer excedeix "
-"els %(size)s octets."
+"[1:La previsualització HTML no és disponible], ja que el fitxer excedeix els "
+"%(size)s octets."
 
 #: trac/templates/preview_file.html:22
-msgid "[1:HTML preview not available], since no preview renderer could handle it."
+msgid ""
+"[1:HTML preview not available], since no preview renderer could handle it."
 msgstr ""
-"[1:La previsualització HTML no és disponible], ja que cap renderitzador "
-"de previsualització ho pot gestionar."
+"[1:La previsualització HTML no és disponible], ja que cap renderitzador de "
+"previsualització ho pot gestionar."
 
 #: trac/templates/preview_file.html:26
 msgid "Try [1:downloading] the file instead."
@@ -2727,7 +2720,7 @@
 #: trac/templates/progress_bar.html:41
 #, fuzzy, python-format
 msgid "%(title)s: %(count)s"
-msgstr ""
+msgstr "%(title)s: %(revs)s"
 
 #: trac/templates/progress_bar_grouped.html:17
 msgid "(none)"
@@ -2826,7 +2819,8 @@
 msgid "The milestone \"%(name)s\" has been added."
 msgstr "S'ha afegit la fita «%(name)s."
 
-#: trac/ticket/admin.py:305 trac/ticket/model.py:1038 trac/ticket/model.py:1060
+#: trac/ticket/admin.py:305 trac/ticket/model.py:1038
+#: trac/ticket/model.py:1060
 msgid "Invalid milestone name."
 msgstr "El nom de la fita és invàlid."
 
@@ -2853,7 +2847,8 @@
 msgid "The version \"%(name)s\" has been added."
 msgstr "S'ha afegit la versió «%(name)s."
 
-#: trac/ticket/admin.py:484 trac/ticket/model.py:1165 trac/ticket/model.py:1183
+#: trac/ticket/admin.py:484 trac/ticket/model.py:1165
+#: trac/ticket/model.py:1183
 msgid "Invalid version name."
 msgstr "El nom de la versió és invàlid."
 
@@ -2901,11 +2896,11 @@
 
 #: trac/ticket/admin.py:668
 msgid ""
-"Error writing to trac.ini, make sure it is writable by the web server. "
-"The default value has not been saved."
+"Error writing to trac.ini, make sure it is writable by the web server. The "
+"default value has not been saved."
 msgstr ""
-"S'ha produït un error en escriure al trac.ini. Assegureu-vos que el "
-"servidor web ho pot escriure. No s'ha desat el valor per defecte."
+"S'ha produït un error en escriure al trac.ini. Assegureu-vos que el servidor "
+"web ho pot escriure. No s'ha desat el valor per defecte."
 
 #: trac/ticket/admin.py:680
 msgid "Order numbers must be unique"
@@ -2997,7 +2992,7 @@
 #: trac/ticket/api.py:480
 #, fuzzy, python-format
 msgid "Tickets %(ranges)s"
-msgstr ""
+msgstr "Tiquet #%(shortname)s"
 
 #: trac/ticket/api.py:504
 #, python-format
@@ -3010,31 +3005,27 @@
 msgstr "Tiquet #%(shortname)s"
 
 #: trac/ticket/batch.py:95
-#, fuzzy
 msgid "add"
-msgstr "afegit"
+msgstr "afegeix"
 
 #: trac/ticket/batch.py:96
-#, fuzzy
 msgid "remove"
-msgstr "suprimit"
+msgstr "suprimeix"
 
 #: trac/ticket/batch.py:97
-#, fuzzy
 msgid "add / remove"
-msgstr "suprimit"
+msgstr "afegeix/suprimeix"
 
 #: trac/ticket/batch.py:98
-#, fuzzy
 msgid "set to"
-msgstr "establert"
+msgstr "establit a"
 
 #: trac/ticket/batch.py:180
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "The changes have been saved, but an error occurred while sending "
 "notifications: %(message)s"
-msgstr ""
+msgstr "S'han desat el canvis, però s'ha produït un error en enviar les notificacions: %(message)s"
 
 #: trac/ticket/default_workflow.py:241
 msgid "Current state no longer exists"
@@ -3065,9 +3056,9 @@
 msgstr "El propietari canviarà de %(current_owner)s a %(selected_owner)s"
 
 #: trac/ticket/default_workflow.py:283
-#, fuzzy, python-format
+#, python-format
 msgid "The owner will be changed from %(current_owner)s to the selected user"
-msgstr ""
+msgstr "El propietari canviarà de %(current_owner)s a l'usuari seleccionat"
 
 #: trac/ticket/default_workflow.py:288
 #, python-format
@@ -3111,7 +3102,7 @@
 msgid "Next status will be '%(name)s'"
 msgstr "L'estat següent serà «%(name)s»"
 
-#: trac/ticket/default_workflow.py:418
+#: trac/ticket/default_workflow.py:419
 msgid ""
 "Render a workflow graph.\n"
 "\n"
@@ -3153,9 +3144,9 @@
 "}}}"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:493
+#: trac/ticket/default_workflow.py:494
 msgid "Enable JavaScript to display the workflow graph."
-msgstr ""
+msgstr "Habiliteu el JavaScript per mostrar la gràfica de flux de treball."
 
 #: trac/ticket/model.py:120
 #, python-format
@@ -3228,8 +3219,8 @@
 #: trac/ticket/query.py:167
 msgid "Query filter requires field and constraints separated by a \"=\""
 msgstr ""
-"El filtre de consulta requereix que els camps i restriccions siguen "
-"separats per un «=»"
+"El filtre de consulta requereix que els camps i restriccions siguen separats "
+"per un «=»"
 
 #: trac/ticket/query.py:180
 msgid "Query filter requires field name"
@@ -3278,19 +3269,19 @@
 msgid "Page %(num)d"
 msgstr "Pàgina %(num)d"
 
-#: trac/ticket/query.py:847 trac/ticket/report.py:328 trac/ticket/report.py:627
-#: trac/ticket/web_ui.py:140 trac/timeline/web_ui.py:235
-#: trac/versioncontrol/web_ui/log.py:319
+#: trac/ticket/query.py:847 trac/ticket/report.py:328
+#: trac/ticket/report.py:627 trac/ticket/web_ui.py:140
+#: trac/timeline/web_ui.py:235 trac/versioncontrol/web_ui/log.py:319
 msgid "RSS Feed"
 msgstr "Font RSS"
 
-#: trac/ticket/query.py:849 trac/ticket/report.py:330 trac/ticket/report.py:629
-#: trac/ticket/web_ui.py:136
+#: trac/ticket/query.py:849 trac/ticket/report.py:330
+#: trac/ticket/report.py:629 trac/ticket/web_ui.py:136
 msgid "Comma-delimited Text"
 msgstr "Text delimitat per comes"
 
-#: trac/ticket/query.py:851 trac/ticket/report.py:332 trac/ticket/report.py:631
-#: trac/ticket/web_ui.py:138
+#: trac/ticket/query.py:851 trac/ticket/report.py:332
+#: trac/ticket/report.py:631 trac/ticket/web_ui.py:138
 msgid "Tab-delimited Text"
 msgstr "Text delimitat per tabuladors"
 
@@ -3326,11 +3317,10 @@
 "of a filter specifier as defined in TracQuery#QueryLanguage.\n"
 "Note that this is ''not'' the same as the simplified URL syntax\n"
 "used for `query:` links starting with a `?` character. Commas (`,`)\n"
-"can be included in field values by escaping them with a backslash (`\\`)."
-"\n"
+"can be included in field values by escaping them with a backslash (`\\`).\n"
 "\n"
 "Groups of field constraints to be OR-ed together can be separated by a\n"
-"litteral `or` argument.\n"
+"literal `or` argument.\n"
 "\n"
 "In addition to filters, several other named parameters can be used\n"
 "to control how the results are presented. All of them are optional.\n"
@@ -3438,9 +3428,9 @@
 msgstr "Quan s'especifica, el número d'informe hauria de ser «%(num)s»."
 
 #: trac/ticket/report.py:444
-#, fuzzy, python-format
+#, python-format
 msgid "Report execution failed: %(error)s %(sql)s"
-msgstr "Ha fallat l'execució de l'informe: %(error)s"
+msgstr "Ha fallat l'execució de l'informe: %(error)s %(sql)s"
 
 #: trac/ticket/report.py:635
 msgid "SQL Query"
@@ -3465,9 +3455,9 @@
 #, python-format
 msgid ""
 "Hint: if the report failed due to automatic modification of the ORDER BY "
-"clause or the addition of LIMIT/OFFSET, please look up %(sort_column)s "
-"and %(limit_offset)s in TracReports to see how to gain complete control "
-"over report rewriting."
+"clause or the addition of LIMIT/OFFSET, please look up %(sort_column)s and "
+"%(limit_offset)s in TracReports to see how to gain complete control over "
+"report rewriting."
 msgstr ""
 
 #: trac/ticket/roadmap.py:243
@@ -3484,17 +3474,17 @@
 "'%(group1)s' and '%(group2)s' milestone groups both are declared to be "
 "\"catch-all\" groups. Please check your configuration."
 msgstr ""
-"Els grups de fites «%(group1)s i «%(group2)s» estan declarats com grups "
-"de tipus «catch-all». Reviseu la configuració."
+"Els grups de fites «%(group1)s i «%(group2)s» estan declarats com grups de "
+"tipus «catch-all». Reviseu la configuració."
 
 #: trac/ticket/roadmap.py:269
 #, python-format
 msgid ""
-"'%(groupname)s' milestone group reused status '%(status)s' already taken "
-"by other groups. Please check your configuration."
+"'%(groupname)s' milestone group reused status '%(status)s' already taken by "
+"other groups. Please check your configuration."
 msgstr ""
-"L'estat %(status)s del grup de fites «%(groupname)s» ja s'està emprant en"
-" altres grups. Reviseu la configuració."
+"L'estat %(status)s del grup de fites «%(groupname)s» ja s'està emprant en "
+"altres grups. Reviseu la configuració."
 
 #: trac/ticket/roadmap.py:403 trac/ticket/roadmap.py:527
 #: trac/ticket/roadmap.py:661 trac/ticket/templates/roadmap.html:10
@@ -3542,23 +3532,21 @@
 msgstr "Heu de donar un nom per a la fita."
 
 #: trac/ticket/roadmap.py:877
-#, fuzzy, python-format
+#, python-format
 msgid "Milestone \"%(name)s\""
-msgstr "Fita %(name)s"
+msgstr "Fita «%(name)s»"
 
 #: trac/ticket/roadmap.py:891
-#, fuzzy
 msgid "Previous Milestone"
-msgstr "Versió anterior"
+msgstr "Fita anterior"
 
 #: trac/ticket/roadmap.py:891
-#, fuzzy
 msgid "Next Milestone"
-msgstr "Suprimeix la fita"
+msgstr "Fita següent"
 
 #: trac/ticket/roadmap.py:892
 msgid "Back to Roadmap"
-msgstr ""
+msgstr "Torna a la planificació"
 
 #: trac/ticket/web_ui.py:65
 msgid "Invalid Ticket"
@@ -3594,34 +3582,34 @@
 msgstr[1] "%(labels)s canviats"
 
 #: trac/ticket/web_ui.py:392
-#, fuzzy, python-format
+#, python-format
 msgid "Ticket %(ticketref)s (%(summary)s) created"
-msgstr "Tiquet %(ticketref)s (%(summary)s) created"
+msgstr "Tiquet %(ticketref)s (%(summary)s) creat"
 
 #: trac/ticket/web_ui.py:393
-#, fuzzy, python-format
+#, python-format
 msgid "Ticket %(ticketref)s (%(summary)s) reopened"
-msgstr "Tiquet %(ticketref)s (%(summary)s) reopened"
+msgstr "Tiquet %(ticketref)s (%(summary)s) reobert"
 
 #: trac/ticket/web_ui.py:394
-#, fuzzy, python-format
+#, python-format
 msgid "Ticket %(ticketref)s (%(summary)s) closed"
-msgstr "Tiquet %(ticketref)s (%(summary)s) closed"
+msgstr "Tiquet %(ticketref)s (%(summary)s) tancat"
 
 #: trac/ticket/web_ui.py:395
-#, fuzzy, python-format
+#, python-format
 msgid "Ticket %(ticketref)s (%(summary)s) updated"
-msgstr "Tiquet %(ticketref)s (%(summary)s) updated"
+msgstr "Tiquet %(ticketref)s (%(summary)s) actualitzat"
 
 #: trac/ticket/web_ui.py:424
-#, fuzzy, python-format
+#, python-format
 msgid "Tickets %(ticketids)s"
-msgstr "Tiquet %(ticketids)s"
+msgstr "Tiquets #%(ticketids)s"
 
 #: trac/ticket/web_ui.py:426
 #, python-format
 msgid "Tickets %(ticketlist)s batch updated"
-msgstr ""
+msgstr "Tickets %(ticketlist)s actualitzats per lots"
 
 #: trac/ticket/web_ui.py:576
 #, python-format
@@ -3631,8 +3619,8 @@
 #: trac/ticket/web_ui.py:602
 #, python-format
 msgid ""
-"Please review your configuration, probably starting with %(section)s in "
-"your %(tracini)s."
+"Please review your configuration, probably starting with %(section)s in your "
+"%(tracini)s."
 msgstr ""
 "Comproveu la configuració, començant per la secció %(section)s al "
 "%(tracini)s."
@@ -3671,8 +3659,8 @@
 #: trac/ticket/web_ui.py:901 trac/ticket/web_ui.py:958
 #: trac/ticket/web_ui.py:966 trac/ticket/web_ui.py:1037
 #: trac/ticket/web_ui.py:1082 trac/ticket/web_ui.py:1089
-#: trac/wiki/web_ui.py:449 trac/wiki/web_ui.py:455 trac/wiki/web_ui.py:653
-#: trac/wiki/web_ui.py:667
+#: trac/wiki/web_ui.py:458 trac/wiki/web_ui.py:464 trac/wiki/web_ui.py:662
+#: trac/wiki/web_ui.py:676
 #, python-format
 msgid "Version %(num)s"
 msgstr "Versió %(num)s"
@@ -3691,12 +3679,12 @@
 msgstr "Propietat %(label)s %(rendered)s"
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:477
 msgid "Previous Change"
 msgstr "Canvi anterior"
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:477
 msgid "Next Change"
 msgstr "Canvi següent"
 
@@ -3723,8 +3711,8 @@
 #, python-format
 msgid "No version %(version)d for comment %(cnum)d on ticket #%(ticket)s"
 msgstr ""
-"No hi ha una versió %(version)d per al comentari %(cnum)d al tiquet "
-"#%(ticket)s"
+"No hi ha una versió %(version)d per al comentari %(cnum)d al tiquet #"
+"%(ticket)s"
 
 #: trac/ticket/web_ui.py:1097
 msgid "Ticket Comment Diff"
@@ -3766,11 +3754,11 @@
 
 #: trac/ticket/web_ui.py:1223
 msgid ""
-"Sorry, can not save your changes. This ticket has been modified by "
-"someone else since you started"
+"Sorry, can not save your changes. This ticket has been modified by someone "
+"else since you started"
 msgstr ""
-"Lamentablement, no podeu desar els vostres canvis. Algú ha modificat "
-"aquest tiquet des de que vau començar"
+"Lamentablement, no podeu desar els vostres canvis. Algú ha modificat aquest "
+"tiquet des de que vau començar"
 
 #: trac/ticket/web_ui.py:1230
 msgid "Tickets must contain a summary."
@@ -3785,8 +3773,8 @@
 #, python-format
 msgid "Ticket description is too long (must be less than %(num)s characters)"
 msgstr ""
-"La descripció del tiquet és massa llarga (ha de ser més curta que %(num)s"
-" caràcters)"
+"La descripció del tiquet és massa llarga (ha de ser més curta que %(num)s "
+"caràcters)"
 
 #: trac/ticket/web_ui.py:1261
 #, python-format
@@ -3816,18 +3804,18 @@
 #: trac/ticket/web_ui.py:1305
 #, python-format
 msgid ""
-"The ticket %(ticketref)s has been created. You can now attach the desired"
-" files."
+"The ticket %(ticketref)s has been created. You can now attach the desired "
+"files."
 msgstr ""
-"S'ha creat el tiquet %(ticketref)s. Ara podeu adjuntar els fitxers "
-"desitjats."
+"S'ha creat el tiquet %(ticketref)s. Ara podeu adjuntar els fitxers desitjats."
 
 #: trac/ticket/web_ui.py:1311
 #, python-format
 msgid ""
-"The ticket %(ticketref)s has been created, but you don't have permission "
-"to view it."
-msgstr "S'ha creat el tiquet %(ticketref)s, però no teniu permís per a mostrar-ho."
+"The ticket %(ticketref)s has been created, but you don't have permission to "
+"view it."
+msgstr ""
+"S'ha creat el tiquet %(ticketref)s, però no teniu permís per a mostrar-ho."
 
 #. TRANSLATOR: The 'change' has been saved... (link)
 #: trac/ticket/web_ui.py:1338
@@ -3882,7 +3870,7 @@
 
 #. TRANSLATOR: modified ('diff') (link)
 #: trac/ticket/web_ui.py:1732 trac/ticket/templates/ticket_change.html:155
-#: trac/wiki/web_ui.py:747
+#: trac/wiki/web_ui.py:756
 msgid "diff"
 msgstr "diff"
 
@@ -3922,49 +3910,44 @@
 
 #: trac/ticket/templates/batch_modify.html:8
 msgid "Batch Modify"
-msgstr ""
+msgstr "Modifica per lots"
 
 #: trac/ticket/templates/batch_modify.html:9
 msgid "Batch modification fields"
-msgstr ""
+msgstr "Camps de modificació per lots"
 
 #: trac/ticket/templates/batch_modify.html:21
-#, fuzzy
 msgid "Add Field:"
-msgstr "afegit"
+msgstr "Afegeix un camp:"
 
 #: trac/ticket/templates/batch_modify.html:50
-#, fuzzy
 msgid "[1:Note:] See [2:TracBatchModify] for help on using batch modify."
-msgstr ""
-"[1:Nota:] Vegeu [2:TracQuery] per a\n"
-"        obtenir ajuda sobre l'ús de consultes."
+msgstr "[1:Nota:] Vegeu [2:TracBatchModify] per a obtenir ajuda sobre l'ús de la modificació per lots."
 
 #: trac/ticket/templates/batch_modify.html:57
-#, fuzzy
 msgid "Change tickets"
-msgstr "tiquets"
+msgstr "Canvia els tiquets"
 
 #: trac/ticket/templates/batch_ticket_notify_email.txt:1
-#, fuzzy, python-format
+#, python-format
 msgid "Batch modification to %(tickets)s by %(author)s:"
-msgstr "(modificat per darrera vegada per %(author)s)"
+msgstr "Modificació per lots de %(tickets)s per %(author)s):"
 
 #: trac/ticket/templates/batch_ticket_notify_email.txt:5
-#, fuzzy, python-format
+#, python-format
 msgid "Action: %(action)s"
-msgstr ""
+msgstr "Acció: %(action)s"
 
 #: trac/ticket/templates/batch_ticket_notify_email.txt:14
-#, fuzzy, python-format
+#, python-format
 msgid "Tickets URL: <%(link)s>"
-msgstr "URL del tiquet: <%(link)s>"
+msgstr "URL dels tiquets: <%(link)s>"
 
 #: trac/ticket/templates/milestone_delete.html:10
 #: trac/ticket/templates/milestone_delete.html:22
-#, fuzzy, python-format
+#, python-format
 msgid "Delete Milestone %(name)s"
-msgstr "Fita %(name)s"
+msgstr "Suprimeix la fita %(name)s"
 
 #: trac/ticket/templates/milestone_delete.html:27
 msgid "Are you sure you want to delete this milestone?"
@@ -3991,9 +3974,9 @@
 
 #: trac/ticket/templates/milestone_edit.html:11
 #: trac/ticket/templates/milestone_edit.html:45
-#, fuzzy, python-format
+#, python-format
 msgid "Edit Milestone %(name)s"
-msgstr "Fita %(name)s"
+msgstr "Edita la fita %(name)s"
 
 #: trac/ticket/templates/milestone_edit.html:12
 #: trac/ticket/templates/milestone_edit.html:46
@@ -4174,9 +4157,9 @@
 "        obtenir ajuda sobre l'ús de consultes."
 
 #: trac/ticket/templates/query_results.html:25
-#, fuzzy, python-format
+#, python-format
 msgid "%(grouplabel)s: %(groupname)s [1:(%(count)s)]"
-msgstr ""
+msgstr "%(grouplabel)s: %(groupname)s [1:(%(count)s)]"
 
 #: trac/ticket/templates/query_results.html:37
 msgid "(ascending)"
@@ -4265,21 +4248,20 @@
 msgstr "Desa l'informe"
 
 #: trac/ticket/templates/report_list.html:33
-#, fuzzy
 msgid "Show Descriptions"
-msgstr "Mostra totes les descripcions"
+msgstr "Mostra les descripcions"
 
 #: trac/ticket/templates/report_list.html:45
 msgid "Clear"
-msgstr ""
+msgstr "Neteja"
 
 #: trac/ticket/templates/report_list.html:45
 msgid "Forget last query"
-msgstr ""
+msgstr "Oblida la darrera consulta"
 
 #: trac/ticket/templates/report_list.html:48
 msgid "Return to Last Query"
-msgstr ""
+msgstr "Torna a la darrera consulta"
 
 #: trac/ticket/templates/report_list.html:51
 msgid ""
@@ -4296,13 +4278,12 @@
 msgstr ""
 
 #: trac/ticket/templates/report_list.html:67
-#, fuzzy
 msgid "Sort by:"
-msgstr "Notificat per:"
+msgstr "Ordena per:"
 
 #: trac/ticket/templates/report_list.html:70
 msgid "Identifier"
-msgstr ""
+msgstr "Identificador"
 
 #: trac/ticket/templates/report_list.html:73 trac/wiki/admin.py:197
 msgid "Title"
@@ -4371,14 +4352,12 @@
 msgstr "Afegeix una fita nova"
 
 #: trac/ticket/templates/ticket.html:132
-#, fuzzy
 msgid "Go to the ticket editor"
-msgstr "Vés a l'editor"
+msgstr "Vés a l'editor de  tiquets"
 
 #: trac/ticket/templates/ticket.html:132
-#, fuzzy
 msgid "Modify"
-msgstr "modificat"
+msgstr "Modifica"
 
 #: trac/ticket/templates/ticket.html:134
 msgid "Create New Ticket"
@@ -4393,44 +4372,40 @@
 msgstr ""
 
 #: trac/ticket/templates/ticket.html:157
-#, fuzzy
 msgid "Threaded"
-msgstr "creat"
+msgstr ""
 
 #: trac/ticket/templates/ticket.html:162
-#, fuzzy
 msgid "Comments only"
-msgstr "comentari:"
+msgstr "Només comentaris"
 
 #: trac/ticket/templates/ticket.html:167
 msgid "Change History"
 msgstr "Historial de canvis"
 
 #: trac/ticket/templates/ticket.html:186
-#, fuzzy
 msgid "Add Comment"
 msgstr "Afegeix un comentari"
 
 #: trac/ticket/templates/ticket.html:188
 msgid ""
-"This ticket has been modified since you started editing. You should "
-"review the\n"
+"This ticket has been modified since you started editing. You should review "
+"the\n"
 "              [1:other modifications] which have been appended above,\n"
 "              and any [2:conflicts] shown in the preview below.\n"
-"              You can nevertheless proceed and submit your changes if you"
-" wish so."
+"              You can nevertheless proceed and submit your changes if you "
+"wish so."
 msgstr ""
 
 #: trac/ticket/templates/ticket.html:198
-#, fuzzy
 msgid ""
 "You may use\n"
 "                [1:WikiFormatting]\n"
 "                here."
 msgstr ""
 "Podeu emprar\n"
-"              [1:WikiFormatting]\n"
-"              aquí):"
+"                [1:WikiFormatting]\n"
+"                aquí."
 
 #: trac/ticket/templates/ticket.html:209
 msgid "Modify Ticket"
@@ -4469,8 +4444,8 @@
 #: trac/ticket/templates/ticket.html:295
 msgid "This checkbox allows you to add or remove yourself from the CC list."
 msgstr ""
-"Aquesta casella de selecció us permet afegir-vos o treure-vos de la "
-"llista de CC."
+"Aquesta casella de selecció us permet afegir-vos o treure-vos de la llista "
+"de CC."
 
 #: trac/ticket/templates/ticket.html:301
 msgid "Space or comma delimited email addresses and usernames are accepted."
@@ -4480,8 +4455,7 @@
 #: trac/wiki/templates/wiki_edit_form.html:46
 msgid "E-mail address and user name can be saved in the [1:Preferences]."
 msgstr ""
-"Es poden desar l'adreça de correu i el nom d'usuari a les "
-"[1:preferències]."
+"Es poden desar l'adreça de correu i el nom d'usuari a les [1:preferències]."
 
 #: trac/ticket/templates/ticket.html:372
 msgid "I have files to attach to this ticket"
@@ -4492,13 +4466,13 @@
 msgstr "Vés a la llista d'adjunts"
 
 #: trac/ticket/templates/ticket.html:379
-#, fuzzy
 msgid "View the ticket description"
-msgstr "No teniu permís per a editar la descripció del tiquet."
+msgstr "Mostra la descripció del tiquet"
 
 #: trac/ticket/templates/ticket.html:387
 #: trac/ticket/templates/ticket_change.html:114
-#: trac/wiki/templates/wiki_edit.html:95 trac/wiki/templates/wiki_edit.html:153
+#: trac/wiki/templates/wiki_edit.html:95
+#: trac/wiki/templates/wiki_edit.html:153
 #: trac/wiki/templates/wiki_edit_form.html:64
 msgid "Preview"
 msgstr "Previsualitza"
@@ -4518,19 +4492,19 @@
 "        sobre l'ús dels tiquets."
 
 #: trac/ticket/templates/ticket_box.html:22
-#, fuzzy, python-format
+#, python-format
 msgid "Opened %(created)s"
-msgstr "Obert fa %(created)s"
+msgstr "Obert %(created)s"
 
 #: trac/ticket/templates/ticket_box.html:23
 #, python-format
 msgid "Closed %(closed)s"
-msgstr ""
+msgstr "Tancat %(closed)s"
 
 #: trac/ticket/templates/ticket_box.html:24
-#, fuzzy, python-format
+#, python-format
 msgid "Last modified %(modified)s"
-msgstr "Modificat per darrera vegada fa %(modified)s"
+msgstr "Modificat per darrera vegada %(modified)s"
 
 #: trac/ticket/templates/ticket_box.html:26
 msgid "(ticket not yet created)"
@@ -4580,7 +4554,7 @@
 #: trac/ticket/templates/ticket_change.html:53
 #, fuzzy, python-format
 msgid "Changed %(date)s by %(author)s"
-msgstr "Canviat fa %(date)s per %(author)s"
+msgstr "Canviat %(date)s per %(author)s"
 
 #: trac/ticket/templates/ticket_change.html:56
 #, python-format
@@ -4598,13 +4572,12 @@
 msgstr "Respon al comentari %(cnum)s"
 
 #: trac/ticket/templates/ticket_change.html:81
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "[1:[2:%(name)s]][3:​]\n"
 "          added"
 msgstr ""
-"[1:[2:%(name)s]]\n"
-"          [3:[4:]]\n"
+"[1:[2:%(name)s]][3: ]\n"
 "          afegit"
 
 #: trac/ticket/templates/ticket_change.html:88
@@ -4623,14 +4596,12 @@
 msgstr "[1:%(value)s] suprimit"
 
 #: trac/ticket/templates/ticket_change.html:98
-#, fuzzy
 msgid "Revert this change"
-msgstr "Crea aquesta pàgina"
+msgstr "Reverteix aquest canvi"
 
 #: trac/ticket/templates/ticket_change.html:100
-#, fuzzy
 msgid "revert"
-msgstr "Gravetat"
+msgstr "reverteix"
 
 #: trac/ticket/templates/ticket_change.html:114
 #, python-format
@@ -4647,22 +4618,22 @@
 msgstr "Canceŀla l'edició del comentari"
 
 #: trac/ticket/templates/ticket_change.html:137
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "Version %(version)s, edited %(date)s\n"
 "        by %(author)s"
 msgstr ""
-"Versió %(version)s, editat fa %(date)s\n"
-"                      per %(author)s"
+"Versió %(version)s, editat %(date)s\n"
+"        per %(author)s"
 
 #: trac/ticket/templates/ticket_change.html:141
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "Last edited %(date)s\n"
 "        by %(author)s"
 msgstr ""
-"Editat per darrera vegada fa %(date)s\n"
-"                      per %(author)s"
+"Editat per darrera vegada %(date)s\n"
+"        per %(author)s"
 
 #: trac/ticket/templates/ticket_change.html:147
 #: trac/versioncontrol/templates/changeset.html:129
@@ -4709,27 +4680,27 @@
 #: trac/timeline/web_ui.py:290
 #, python-format
 msgid "at %(iso8601)s"
-msgstr ""
+msgstr "el %(iso8601)s"
 
 #: trac/timeline/web_ui.py:294
-#, fuzzy, python-format
+#, python-format
 msgid "on %(date)s at %(time)s"
-msgstr ""
+msgstr "el %(date)s a les %(time)s"
 
 #: trac/timeline/web_ui.py:295
 #, fuzzy, python-format
 msgid "See timeline %(relativetime)s ago"
-msgstr ""
+msgstr "Obert fa %(created)s"
 
 #: trac/timeline/web_ui.py:298 trac/web/chrome.py:865 trac/web/chrome.py:867
 #, fuzzy, python-format
 msgid "%(relativetime)s ago"
-msgstr ""
+msgstr "Obert fa %(created)s"
 
 #: trac/timeline/web_ui.py:300 trac/timeline/web_ui.py:344
 #, python-format
 msgid "See timeline at %(absolutetime)s"
-msgstr ""
+msgstr "Vegeu la línia de temps a les %(absolutetime)s"
 
 #. TRANSLATOR: ...want to see the 'other kinds of events' from... (link)
 #: trac/timeline/web_ui.py:391
@@ -4740,19 +4711,18 @@
 #, python-format
 msgid "Event provider %(name)s failed for filters %(kinds)s: "
 msgstr ""
-"El proveïdor d'esdeveniments %(name)s ha fallat per als filtres "
-"%(kinds)s: "
+"El proveïdor d'esdeveniments %(name)s ha fallat per als filtres %(kinds)s: "
 
 #: trac/timeline/web_ui.py:399
 #, python-format
 msgid ""
-"You may want to see the %(other_events)s from the Timeline or notify your"
-" Trac administrator about the error (detailed information was written to "
-"the log)."
+"You may want to see the %(other_events)s from the Timeline or notify your "
+"Trac administrator about the error (detailed information was written to the "
+"log)."
 msgstr ""
 "Podeu veure els %(other_events)s des de l'Historial o notificar "
-"l'Administrador del Trac sobre l'error (s'ha escrit informació detallada "
-"al fitxer de registre)."
+"l'Administrador del Trac sobre l'error (s'ha escrit informació detallada al "
+"fitxer de registre)."
 
 #: trac/timeline/templates/timeline.html:24
 msgid ""
@@ -4819,56 +4789,56 @@
 "  %(new_path)s\n"
 msgstr ""
 
-#: trac/util/datefmt.py:118
+#: trac/util/datefmt.py:122
 #, python-format
 msgid "%(num)d year"
 msgid_plural "%(num)d years"
 msgstr[0] "%(num)d any"
 msgstr[1] "%(num)d anys"
 
-#: trac/util/datefmt.py:119
+#: trac/util/datefmt.py:123
 #, python-format
 msgid "%(num)d month"
 msgid_plural "%(num)d months"
 msgstr[0] "%(num)d mes"
 msgstr[1] "%(num)d mesos"
 
-#: trac/util/datefmt.py:120
+#: trac/util/datefmt.py:124
 #, python-format
 msgid "%(num)d week"
 msgid_plural "%(num)d weeks"
 msgstr[0] "%(num)d setmana"
 msgstr[1] "%(num)d setmanes"
 
-#: trac/util/datefmt.py:121
+#: trac/util/datefmt.py:125
 #, python-format
 msgid "%(num)d day"
 msgid_plural "%(num)d days"
 msgstr[0] "%(num)d dia"
 msgstr[1] "%(num)d dies"
 
-#: trac/util/datefmt.py:122
+#: trac/util/datefmt.py:126
 #, python-format
 msgid "%(num)d hour"
 msgid_plural "%(num)d hours"
 msgstr[0] "%(num)d hora"
 msgstr[1] "%(num)d hores"
 
-#: trac/util/datefmt.py:123
+#: trac/util/datefmt.py:127
 #, python-format
 msgid "%(num)d minute"
 msgid_plural "%(num)d minutes"
 msgstr[0] "%(num)d minut"
 msgstr[1] "%(num)d minuts"
 
-#: trac/util/datefmt.py:142
+#: trac/util/datefmt.py:146
 #, python-format
 msgid "%(num)i second"
 msgid_plural "%(num)i seconds"
 msgstr[0] "%(num)i segon"
 msgstr[1] "%(num)i segons"
 
-#: trac/util/datefmt.py:464
+#: trac/util/datefmt.py:468
 #, python-format
 msgid ""
 "\"%(date)s\" is an invalid date, or the date format is not known. Try "
@@ -4877,18 +4847,18 @@
 "«%(date)s» és una data invàlida, o el format de la data no és conegut. "
 "Proveu amb «%(hint)s»."
 
-#: trac/util/datefmt.py:466 trac/util/datefmt.py:474
+#: trac/util/datefmt.py:470 trac/util/datefmt.py:478
 msgid "Invalid Date"
 msgstr "La data és invàlida"
 
-#: trac/util/datefmt.py:472
+#: trac/util/datefmt.py:476
 #, python-format
 msgid ""
-"The date \"%(date)s\" is outside valid range. Try a date closer to "
-"present time."
+"The date \"%(date)s\" is outside valid range. Try a date closer to present "
+"time."
 msgstr ""
-"La data «%(date)s» és fora de l'abast vàlid. Proveu amb una data més "
-"propera al present."
+"La data «%(date)s» és fora de l'abast vàlid. Proveu amb una data més propera "
+"al present."
 
 #: trac/util/presentation.py:265
 #, python-format
@@ -4956,24 +4926,23 @@
 #, python-format
 msgid "You should now run %(resync)s to synchronize Trac with the repository."
 msgstr ""
-"Ara hauríeu d'executar %(resync)s per a sincronitzar el Trac amb el "
-"dipòsit."
+"Ara hauríeu d'executar %(resync)s per a sincronitzar el Trac amb el dipòsit."
 
 #: trac/versioncontrol/admin.py:225
 #, python-format
 msgid "You may have to run %(resync)s to synchronize Trac with the repository."
 msgstr ""
-"És possible que hagueu d'executar %(resync)s per a sincronitzar el Trac "
-"amb el dipòsit."
+"És possible que hagueu d'executar %(resync)s per a sincronitzar el Trac amb "
+"el dipòsit."
 
 #: trac/versioncontrol/admin.py:233
 #, python-format
 msgid ""
-"You will need to update your post-commit hook to call %(cset_added)s with"
-" the new repository name."
+"You will need to update your post-commit hook to call %(cset_added)s with "
+"the new repository name."
 msgstr ""
-"Haureu d'actualitzar el vostre gallet «post-commit» per a fer una crida a"
-" %(cset_added)s amb el nom de dipòsit nou."
+"Haureu d'actualitzar el vostre gallet «post-commit» per a fer una crida a "
+"%(cset_added)s amb el nom de dipòsit nou."
 
 #: trac/versioncontrol/admin.py:253
 msgid "Missing arguments to add a repository."
@@ -4990,8 +4959,8 @@
 "You should also set up a post-commit hook on the repository to call "
 "%(cset_added)s for each committed changeset."
 msgstr ""
-"Hauríeu de configurar un gallet «post-commit» al dipòsit per a fer una "
-"crida a «%(cset_added)s per a cada conjunt de canvis validat."
+"Hauríeu de configurar un gallet «post-commit» al dipòsit per a fer una crida "
+"a «%(cset_added)s per a cada conjunt de canvis validat."
 
 #: trac/versioncontrol/admin.py:281
 #, python-format
@@ -5020,8 +4989,8 @@
 "The repository directory must be located below one of the following "
 "directories: %(dirs)s"
 msgstr ""
-"El directori del dipòsit ha d'estar ubicat sota un dels directoris "
-"següents: %(dirs)s"
+"El directori del dipòsit ha d'estar ubicat sota un dels directoris següents: "
+"%(dirs)s"
 
 #: trac/versioncontrol/api.py:34
 #: trac/versioncontrol/templates/admin_repositories.html:20
@@ -5061,8 +5030,8 @@
 #: trac/versioncontrol/api.py:356
 #, python-format
 msgid ""
-"Can't synchronize with repository \"%(name)s\" (%(error)s). Look in the "
-"Trac log for more information."
+"Can't synchronize with repository \"%(name)s\" (%(error)s). Look in the Trac "
+"log for more information."
 msgstr ""
 "No es pot sincronitzar amb el dipòsit «%(name)s» (%(error)s). Vegeu el "
 "registre del Trac per a obtenir més informació."
@@ -5118,12 +5087,12 @@
 #: trac/versioncontrol/api.py:714
 #, python-format
 msgid ""
-"Unsupported version control system \"%(name)s\": Can't find an "
-"appropriate component, maybe the corresponding plugin was not enabled? "
+"Unsupported version control system \"%(name)s\": Can't find an appropriate "
+"component, maybe the corresponding plugin was not enabled? "
 msgstr ""
 "El sistema de control de versions «%(name)s» no és suportat: no es pot "
-"trobar un component apropiat. És possible que el connector corresponent "
-"no estiga habilitat?"
+"trobar un component apropiat. És possible que el connector corresponent no "
+"estiga habilitat?"
 
 #: trac/versioncontrol/api.py:722
 #, python-format
@@ -5241,9 +5210,8 @@
 msgstr ""
 
 #: trac/versioncontrol/templates/browser.html:63
-#, fuzzy
 msgid "View diff against:"
-msgstr "mostra els diffs"
+msgstr "Mostra les diferències contra:"
 
 #: trac/versioncontrol/templates/browser.html:76
 msgid "Hint: clear the field to view latest revision"
@@ -5278,7 +5246,7 @@
 #: trac/versioncontrol/templates/browser.html:103
 #: trac/versioncontrol/templates/revisionlog.html:182
 msgid "Tag"
-msgstr ""
+msgstr "Etiqueta"
 
 #: trac/versioncontrol/templates/browser.html:114
 msgid "Parent Directory"
@@ -5289,7 +5257,8 @@
 msgstr "No s'ha trobat cap fitxer"
 
 #: trac/versioncontrol/templates/browser.html:128
-#: trac/wiki/templates/wiki_edit.html:137 trac/wiki/templates/wiki_view.html:34
+#: trac/wiki/templates/wiki_edit.html:137
+#: trac/wiki/templates/wiki_view.html:34
 msgid "Revision info"
 msgstr "Info de la revisió"
 
@@ -5301,22 +5270,28 @@
 msgstr "Mostra el conjunt de canvis %(rev)s"
 
 #: trac/versioncontrol/templates/browser.html:137
-#, python-format
+#, fuzzy, python-format
 msgid ""
 "[1:Last change]\n"
 "                  on this file since %(stickyrev)s was\n"
 "                  [2:%(rev)s],\n"
 "                  checked in by %(author)s, %(age)s"
 msgstr ""
+"Fitxer %(file)s,\n"
+"                  [1:%(size)s]\n"
+"                  (afegit per %(author)s, fa %(date)s)"
 
 #: trac/versioncontrol/templates/browser.html:145
-#, python-format
+#, fuzzy, python-format
 msgid ""
 "[1:Last change]\n"
 "                  on this file was\n"
 "                  [2:%(rev)s],\n"
 "                  checked in by %(author)s, %(age)s"
 msgstr ""
+"Fitxer %(file)s,\n"
+"                  [1:%(size)s]\n"
+"                  (afegit per %(author)s, fa %(date)s)"
 
 #: trac/versioncontrol/templates/browser.html:180
 #, python-format
@@ -5620,8 +5595,7 @@
 #: trac/versioncontrol/templates/diff_form.html:50
 msgid ""
 "For either path, you can start typing the path and will be\n"
-"              presented a list of existing directories and files to "
-"select\n"
+"              presented a list of existing directories and files to select\n"
 "              from. Select an entry by clicking on it, or by using the\n"
 "              up/down cursor keys and hitting tab."
 msgstr ""
@@ -5653,7 +5627,7 @@
 #: trac/versioncontrol/templates/repository_index.html:22
 #: trac/versioncontrol/web_ui/browser.py:823
 msgid "Download as Zip archive"
-msgstr ""
+msgstr "Baixa-ho com a arxiu Zip"
 
 #: trac/versioncontrol/templates/dir_entries.html:22
 #: trac/versioncontrol/templates/repository_index.html:26
@@ -5758,11 +5732,12 @@
 #: trac/versioncontrol/templates/revisionlog.html:101
 #: trac/versioncontrol/templates/revisionlog.html:204
 msgid "Diff from Old Revision to New Revision (as selected in the Diff column)"
-msgstr "Diff de la revisió antiga a revisió nova (seleccionades a la columna Diff)"
+msgstr ""
+"Diff de la revisió antiga a revisió nova (seleccionades a la columna Diff)"
 
 #: trac/versioncontrol/templates/revisionlog.html:107
 msgid "Graph"
-msgstr ""
+msgstr "Gràfica"
 
 #: trac/versioncontrol/templates/revisionlog.html:108
 msgid "Old / New"
@@ -5829,12 +5804,12 @@
 #: trac/versioncontrol/templates/revisionlog.txt:2
 #, fuzzy, python-format
 msgid "ChangeLog for %(path)s in %(repo)s"
-msgstr "Conjunt de canvis per a %(path)s %(repo)s"
+msgstr "Conjunt de canvis per a %(path)s%(in_repo)s"
 
 #: trac/versioncontrol/templates/revisionlog.txt:2
 #, fuzzy, python-format
 msgid "ChangeLog for %(path)s"
-msgstr "Conjunt de canvis per a %(path)s"
+msgstr "Conjunt de canvis per a %(path)s%(in_repo)s"
 
 #: trac/versioncontrol/templates/revisionlog.txt:4
 #, python-format
@@ -5893,7 +5868,8 @@
 msgid ""
 "Annotate each line with the last changed revision (this can be time "
 "consuming...)"
-msgstr "Anota cada línia amb la darrera revisió amb canvis (això pot trigar molt…)"
+msgstr ""
+"Anota cada línia amb la darrera revisió amb canvis (això pot trigar molt…)"
 
 #: trac/versioncontrol/web_ui/browser.py:477
 msgid "Revision Log"
@@ -5949,8 +5925,7 @@
 #: trac/versioncontrol/web_ui/changeset.py:250
 msgid "No repository specified and no default repository configured."
 msgstr ""
-"No s'ha especificat cap dipòsit i no hi ha un dipòsit per defecte "
-"configurat."
+"No s'ha especificat cap dipòsit i no hi ha un dipòsit per defecte configurat."
 
 #: trac/versioncontrol/web_ui/changeset.py:262
 msgid "Invalid Changeset Number"
@@ -6052,11 +6027,11 @@
 #: trac/versioncontrol/web_ui/log.py:209
 #, python-format
 msgid ""
-"The file or directory '%(path)s' doesn't exist at revision %(rev)s or at "
-"any previous revision."
+"The file or directory '%(path)s' doesn't exist at revision %(rev)s or at any "
+"previous revision."
 msgstr ""
-"El fitxer o directori «%(path)s no existeix a la revisió %(rev)s o cap "
-"altra revisió anterior."
+"El fitxer o directori «%(path)s no existeix a la revisió %(rev)s o cap altra "
+"revisió anterior."
 
 #: trac/versioncontrol/web_ui/log.py:209
 msgid "Nonexistent path"
@@ -6091,8 +6066,8 @@
 #: trac/versioncontrol/web_ui/util.py:78
 #, python-format
 msgid ""
-"You can %(search)s in the repository history to see if that path existed "
-"but was later removed"
+"You can %(search)s in the repository history to see if that path existed but "
+"was later removed"
 msgstr ""
 "Podeu %(search)s a l'historial del dipòsit per a veure si aquest camí "
 "existia però va ser suprimit"
@@ -6132,11 +6107,9 @@
 #: trac/web/auth.py:151
 #, python-format
 msgid ""
-"Authentication information not available. Please refer to the "
-"%(inst_doc)s."
+"Authentication information not available. Please refer to the %(inst_doc)s."
 msgstr ""
-"La informació de l'autenticació no és disponible. Consulteu la "
-"%(inst_doc)s."
+"La informació de l'autenticació no és disponible. Consulteu la %(inst_doc)s."
 
 #: trac/web/auth.py:159
 #, python-format
@@ -6234,8 +6207,8 @@
 msgstr ""
 "==== Com reproduir-ho ====\n"
 "\n"
-"Mentre feia una operació %(method)s en `%(path_info)s, el Trac ha emès un"
-" error intern.\n"
+"Mentre feia una operació %(method)s en `%(path_info)s, el Trac ha emès un "
+"error intern.\n"
 "\n"
 "''(afegiu detalls addicionals aquí)''\n"
 "\n"
@@ -6300,7 +6273,7 @@
 msgstr "No s'ha trobat la pàgina «%(page)s»"
 
 #: trac/wiki/admin.py:118 trac/wiki/model.py:127 trac/wiki/model.py:174
-#: trac/wiki/web_ui.py:119
+#: trac/wiki/web_ui.py:120
 #, python-format
 msgid "Invalid Wiki page name '%(name)s'"
 msgstr "El nom de la pàgina del Wiki «%(name)s» és invàlid"
@@ -6329,7 +6302,7 @@
 msgid "Edits"
 msgstr "Edicions"
 
-#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:300
+#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:301
 msgid "A new name is mandatory for a rename."
 msgstr "Els canvis de nom requereixen un nom nou."
 
@@ -6337,7 +6310,7 @@
 msgid "The new name is invalid."
 msgstr "El nom nou és invàlid."
 
-#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:307
+#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:308
 #, python-format
 msgid "The page %(name)s already exists."
 msgstr "La pàgina %(name)s ja existeix."
@@ -6361,7 +6334,8 @@
 msgstr "S'ha produït un error en analitzar l'HTML: %(message)s"
 
 #: trac/wiki/formatter.py:226
-msgid "Error: Forbidden character sequence \"--\" in htmlcomment wiki code block"
+msgid ""
+"Error: Forbidden character sequence \"--\" in htmlcomment wiki code block"
 msgstr ""
 "Error: Seqüència de caràcters prohibida «--» al bloc de codi del wiki "
 "htmlcomment"
@@ -6375,8 +6349,8 @@
 #, python-format
 msgid "!#%(name)s must contain at least one table cell (and table cells only)"
 msgstr ""
-"!#%(name)s ha de contenir, com a mínim una ceŀla de taula (i només ceŀles"
-" de taula)"
+"!#%(name)s ha de contenir, com a mínim una ceŀla de taula (i només ceŀles de "
+"taula)"
 
 #: trac/wiki/formatter.py:684 trac/wiki/interwiki.py:104
 #, python-format
@@ -6423,11 +6397,9 @@
 "   children pages will be shown, etc. If not set, or set to -1,\n"
 "   all pages in the hierarchy will be shown.\n"
 " - `include=page1:page*2`: include only pages that match an item in the\n"
-"   colon-separated list of pages. If the list is empty, or if no "
-"`include`\n"
+"   colon-separated list of pages. If the list is empty, or if no `include`\n"
 "   argument is given, include all pages.\n"
-" - `exclude=page1:page*2`: exclude pages that match an item in the colon-"
-"\n"
+" - `exclude=page1:page*2`: exclude pages that match an item in the colon-\n"
 "   separated list of pages.\n"
 "\n"
 "The `include` and `exclude` lists accept shell-style patterns."
@@ -6441,8 +6413,7 @@
 "This macro accepts two ordered arguments and a named argument. The named\n"
 "argument can be placed in any position within the argument list.\n"
 "\n"
-"The first parameter is a prefix string: if provided, only pages with "
-"names\n"
+"The first parameter is a prefix string: if provided, only pages with names\n"
 "that start with the prefix are included in the resulting list. If this\n"
 "parameter is omitted, all pages are included in the list.\n"
 "\n"
@@ -6466,28 +6437,21 @@
 "\n"
 "This macro accepts four optional parameters:\n"
 "\n"
-" * The first is a number or range that allows configuring the minimum and"
-"\n"
+" * The first is a number or range that allows configuring the minimum and\n"
 "   maximum level of headings that should be included in the outline. For\n"
 "   example, specifying \"1\" here will result in only the top-level "
 "headings\n"
-"   being included in the outline. Specifying \"2-3\" will make the "
-"outline\n"
-"   include all headings of level 2 and 3, as a nested list. The default "
-"is\n"
+"   being included in the outline. Specifying \"2-3\" will make the outline\n"
+"   include all headings of level 2 and 3, as a nested list. The default is\n"
 "   to include all heading levels.\n"
-" * The second parameter can be used to specify a custom title (the "
-"default\n"
+" * The second parameter can be used to specify a custom title (the default\n"
 "   is no title).\n"
 " * The third parameter selects the style of the outline. This can be\n"
 "   either `inline` or `pullout` (the latter being the default). The\n"
-"   `inline` style renders the outline as normal part of the content, "
-"while\n"
-"   `pullout` causes the outline to be rendered in a box that is by "
-"default\n"
+"   `inline` style renders the outline as normal part of the content, while\n"
+"   `pullout` causes the outline to be rendered in a box that is by default\n"
 "   floated to the right side of the other content.\n"
-" * The fourth parameter specifies whether the outline is numbered or not."
-"\n"
+" * The fourth parameter specifies whether the outline is numbered or not.\n"
 "   It can be either `numbered` or `unnumbered` (the former being the\n"
 "   default). This parameter only has an effect in `inline` style."
 msgstr ""
@@ -6498,38 +6462,30 @@
 "\n"
 "The first argument is the file specification. The file specification may\n"
 "reference attachments in three ways:\n"
-" * `module:id:file`, where module can be either '''wiki''' or "
-"'''ticket''',\n"
-"   to refer to the attachment named ''file'' of the specified wiki page "
-"or\n"
+" * `module:id:file`, where module can be either '''wiki''' or '''ticket''',\n"
+"   to refer to the attachment named ''file'' of the specified wiki page or\n"
 "   ticket.\n"
-" * `id:file`: same as above, but id is either a ticket shorthand or a "
-"Wiki\n"
+" * `id:file`: same as above, but id is either a ticket shorthand or a Wiki\n"
 "   page name.\n"
-" * `file` to refer to a local attachment named 'file'. This only works "
-"from\n"
+" * `file` to refer to a local attachment named 'file'. This only works from\n"
 "   within that wiki page or a ticket.\n"
 "\n"
 "Also, the file specification may refer to repository files, using the\n"
 "`source:file` syntax (`source:file@rev` works also).\n"
 "\n"
 "Files can also be accessed with a direct URLs; `/file` for a\n"
-"project-relative, `//file` for a server-relative, or `http://server/file`"
-"\n"
+"project-relative, `//file` for a server-relative, or `http://server/file`\n"
 "for absolute location of the file.\n"
 "\n"
-"The remaining arguments are optional and allow configuring the attributes"
-"\n"
+"The remaining arguments are optional and allow configuring the attributes\n"
 "and style of the rendered `<img>` element:\n"
 " * digits and unit are interpreted as the size (ex. 120, 25%)\n"
 "   for the image\n"
-" * `right`, `left`, `center`, `top`, `bottom` and `middle` are "
-"interpreted\n"
+" * `right`, `left`, `center`, `top`, `bottom` and `middle` are interpreted\n"
 "   as the alignment for the image (alternatively, the first three can be\n"
 "   specified using `align=...` and the last three using `valign=...`)\n"
 " * `link=some TracLinks...` replaces the link to the image source by the\n"
-"   one specified using a TracLinks. If no value is specified, the link is"
-"\n"
+"   one specified using a TracLinks. If no value is specified, the link is\n"
 "   simply removed.\n"
 " * `nolink` means without link to image source (deprecated, use `link=`)\n"
 " * `key=value` style are interpreted as HTML attributes or CSS style\n"
@@ -6542,13 +6498,10 @@
 "Examples:\n"
 "{{{\n"
 "    [[Image(photo.jpg)]]                           # simplest\n"
-"    [[Image(photo.jpg, 120px)]]                    # with image width "
-"size\n"
+"    [[Image(photo.jpg, 120px)]]                    # with image width size\n"
 "    [[Image(photo.jpg, right)]]                    # aligned by keyword\n"
-"    [[Image(photo.jpg, nolink)]]                   # without link to "
-"source\n"
-"    [[Image(photo.jpg, align=right)]]              # aligned by attribute"
-"\n"
+"    [[Image(photo.jpg, nolink)]]                   # without link to source\n"
+"    [[Image(photo.jpg, align=right)]]              # aligned by attribute\n"
 "}}}\n"
 "\n"
 "You can use image from other page, other ticket or other module.\n"
@@ -6575,8 +6528,7 @@
 "Display a list of all installed Wiki macros, including documentation if\n"
 "available.\n"
 "\n"
-"Optionally, the name of a specific macro can be provided as an argument. "
-"In\n"
+"Optionally, the name of a specific macro can be provided as an argument. In\n"
 "that case, only the documentation for that macro will be rendered.\n"
 "\n"
 "Note that this macro will not be able to display the documentation of\n"
@@ -6615,8 +6567,7 @@
 msgid ""
 "List all known mime-types which can be used as WikiProcessors.\n"
 "\n"
-"Can be given an optional argument which is interpreted as mime-type "
-"filter."
+"Can be given an optional argument which is interpreted as mime-type filter."
 msgstr ""
 
 #: trac/wiki/macros.py:818
@@ -6646,163 +6597,173 @@
 msgid "Can't rename to existing %(name)s page."
 msgstr "No es pot canviar el nom a la pàgina %(name)s ja existent."
 
-#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:754
+#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:763
 msgid "Wiki"
 msgstr "Wiki"
 
-#: trac/wiki/web_ui.py:89
+#: trac/wiki/web_ui.py:90
 msgid "Help/Guide"
 msgstr "Ajuda/Guia"
 
-#: trac/wiki/web_ui.py:130
+#: trac/wiki/web_ui.py:131
 #, python-format
 msgid "No version \"%(num)s\" for Wiki page \"%(name)s\""
 msgstr "La versió «%(num)s de la pàgina del Wiki «%(name)s» no existeix"
 
-#: trac/wiki/web_ui.py:195
+#: trac/wiki/web_ui.py:196
 #, python-format
 msgid "The wiki page is too long (must be less than %(num)s characters)"
 msgstr ""
-"La pàgina del wiki és massa llarga (ha de ser de menys de %(num)s "
-"caràcters)"
+"La pàgina del wiki és massa llarga (ha de ser de menys de %(num)s caràcters)"
 
-#: trac/wiki/web_ui.py:205
+#: trac/wiki/web_ui.py:206
 #, python-format
 msgid "The Wiki page field '%(field)s' is invalid: %(message)s"
 msgstr "El camp «%(field)s» de la pàgina del Wiki és invàlida: %(message)s"
 
-#: trac/wiki/web_ui.py:209
+#: trac/wiki/web_ui.py:210
 #, python-format
 msgid "Invalid Wiki page: %(message)s"
 msgstr "La pàgina del Wiki és invàlida: %(message)s"
 
 #. TRANSLATOR: wiki page
-#: trac/wiki/web_ui.py:236
+#: trac/wiki/web_ui.py:237
 msgid "currently edited"
 msgstr "actualment editada"
 
-#: trac/wiki/web_ui.py:269
+#: trac/wiki/web_ui.py:270
 #, python-format
 msgid "The page %(name)s has been deleted."
 msgstr "S'ha suprimit la pàgina %(name)s."
 
-#: trac/wiki/web_ui.py:274
+#: trac/wiki/web_ui.py:275
 #, python-format
-msgid "The versions %(from_)d to %(to)d of the page %(name)s have been deleted."
+msgid ""
+"The versions %(from_)d to %(to)d of the page %(name)s have been deleted."
 msgstr ""
 "S'han suprimit les versions des de la %(from_)d a la %(to)d de la pàgina "
 "%(name)s."
 
-#: trac/wiki/web_ui.py:278
+#: trac/wiki/web_ui.py:279
 #, python-format
 msgid "The version %(version)d of the page %(name)s has been deleted."
 msgstr "S'ha suprimit la versió %(version)d de la pàgina %(name)s."
 
-#: trac/wiki/web_ui.py:302
+#: trac/wiki/web_ui.py:303
 msgid ""
-"The new name is invalid (a name which is separated with slashes cannot be"
-" '.' or '..')."
+"The new name is invalid (a name which is separated with slashes cannot be "
+"'.' or '..')."
 msgstr ""
-"El nom nou és invàlid (un nom separat per barres «/» no pot ser «.» o "
-"«..»)."
+"El nom nou és invàlid (un nom separat per barres «/» no pot ser «.» o «..»)."
 
-#: trac/wiki/web_ui.py:305
+#: trac/wiki/web_ui.py:306
 msgid "The new name must be different from the old name."
 msgstr "El nom nou ha de ser diferent al nom vell."
 
-#: trac/wiki/web_ui.py:316
+#: trac/wiki/web_ui.py:317
 #, python-format
 msgid "See [wiki:\"%(name)s\"]."
 msgstr "Vegeu [wiki:\"%(name)s\"]."
 
-#: trac/wiki/web_ui.py:340
+#: trac/wiki/web_ui.py:323
+#, fuzzy, python-format
+msgid "The page %(old_name)s has been renamed to %(new_name)s."
+msgstr "S'ha suprimit la pàgina %(name)s."
+
+#: trac/wiki/web_ui.py:327
+#, fuzzy, python-format
+msgid ""
+"The page %(old_name)s has been recreated with a redirect to %(new_name)s."
+msgstr "S'ha suprimit la pàgina %(name)s."
+
+#: trac/wiki/web_ui.py:349
 #, python-format
 msgid "Your changes have been saved in version %(version)s."
 msgstr "S'han desat els canvis a la versió %(version)s."
 
-#: trac/wiki/web_ui.py:345
+#: trac/wiki/web_ui.py:354
 msgid "Page not modified, showing latest version."
 msgstr "No s'ha modificat la pàgina, es mostra la darrera versió."
 
-#: trac/wiki/web_ui.py:399
+#: trac/wiki/web_ui.py:408
 #, python-format
 msgid "Version %(num)s of page \"%(name)s\" does not exist"
 msgstr "La versió %(num)s de la pàgina «%(name)s no existeix"
 
-#: trac/wiki/web_ui.py:451
+#: trac/wiki/web_ui.py:460
 msgid "Page history"
 msgstr "Historial de la pàgina"
 
-#: trac/wiki/web_ui.py:469
+#: trac/wiki/web_ui.py:478
 msgid "Wiki History"
 msgstr "Historial del wiki"
 
-#: trac/wiki/web_ui.py:499
+#: trac/wiki/web_ui.py:508
 #, fuzzy, python-format
 msgid "Reverted to version %(version)s."
-msgstr "Suprimeix la versió %(version)s"
+msgstr "Suprimeix la versió %(version)d"
 
-#: trac/wiki/web_ui.py:562
+#: trac/wiki/web_ui.py:571
 #, python-format
 msgid "Page %(name)s does not exist"
 msgstr "La pàgina %(name)s no existeix."
 
-#: trac/wiki/web_ui.py:576
+#: trac/wiki/web_ui.py:585
 #, python-format
 msgid "Back to %(wikipage)s"
 msgstr "Torna a %(wikipage)s"
 
-#: trac/wiki/web_ui.py:604
+#: trac/wiki/web_ui.py:613
 #, python-format
 msgid "Page %(name)s not found"
 msgstr "No s'ha trobat la pàgina %(name)s"
 
-#: trac/wiki/web_ui.py:658
+#: trac/wiki/web_ui.py:667
 msgid "View latest version"
 msgstr "Mostra l'última versió"
 
-#: trac/wiki/web_ui.py:662
+#: trac/wiki/web_ui.py:671
 msgid "View parent page"
 msgstr "Mostra la pàgina pare"
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:680
 msgid "Previous Version"
 msgstr "Versió anterior"
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:680
 msgid "Next Version"
 msgstr "Versió següent"
 
-#: trac/wiki/web_ui.py:672
+#: trac/wiki/web_ui.py:681
 msgid "View Latest Version"
 msgstr "Mostra l'última versió"
 
-#: trac/wiki/web_ui.py:675
+#: trac/wiki/web_ui.py:684
 msgid "Up"
 msgstr "Amunt"
 
-#: trac/wiki/web_ui.py:700
+#: trac/wiki/web_ui.py:709
 msgid "Start Page"
 msgstr "Pàgina inicial"
 
-#: trac/wiki/web_ui.py:701
+#: trac/wiki/web_ui.py:710
 msgid "Index"
 msgstr "Índex"
 
-#: trac/wiki/web_ui.py:703
+#: trac/wiki/web_ui.py:712
 msgid "History"
 msgstr "Historial"
 
-#: trac/wiki/web_ui.py:710
+#: trac/wiki/web_ui.py:719
 msgid "Wiki changes"
 msgstr "Canvis al wiki"
 
-#: trac/wiki/web_ui.py:737
+#: trac/wiki/web_ui.py:746
 #, python-format
 msgid "%(page)s edited"
 msgstr "%(page)s editat"
 
-#: trac/wiki/web_ui.py:739
+#: trac/wiki/web_ui.py:748
 #, python-format
 msgid "%(page)s created"
 msgstr "%(page)s creat"
@@ -6849,6 +6810,8 @@
 "                    created %(created)s, so the page will be removed "
 "completely!"
 msgstr ""
+"Aquesta és la única versió de la pàgina, per la qual cosa aquesta pàgina es "
+"suprimirà completament!"
 
 #: trac/wiki/templates/wiki_delete.html:64
 #, fuzzy, python-format
@@ -6863,8 +6826,7 @@
 #, python-format
 msgid ""
 "Removing the one and only [1:\n"
-"                        version] of the page, which was created "
-"%(created)s."
+"                        version] of the page, which was created %(created)s."
 msgstr ""
 
 #: trac/wiki/templates/wiki_delete.html:83
@@ -6933,10 +6895,9 @@
 msgid ""
 "Please review all those changes and manually merge them with your\n"
 "        own changes. [1:]\n"
-"        If you're unsure about what you're doing, please press [2:Cancel]"
-"\n"
-"        (losing your changes) and start editing the latest version of the"
-" page\n"
+"        If you're unsure about what you're doing, please press [2:Cancel]\n"
+"        (losing your changes) and start editing the latest version of the "
+"page\n"
 "        again."
 msgstr ""
 "Reviseu aquests canvis i fusioneu-los manualment amb els vostres. [1:]\n"
@@ -6947,8 +6908,7 @@
 #: trac/wiki/templates/wiki_edit.html:139
 #, python-format
 msgid ""
-"Change information for future version %(version)s (modified by "
-"%(author)s):"
+"Change information for future version %(version)s (modified by %(author)s):"
 msgstr ""
 "Informació dels canvis per a la versió futura %(version)s (modificat per "
 "%(author)s):"
@@ -6987,10 +6947,11 @@
 msgstr "Ajusta l'alçada de l'àrea d'edició:"
 
 #: trac/wiki/templates/wiki_edit_form.html:24
-msgid "Selecting and pressing 'Preview' enters a two-column [edit|preview] mode"
+msgid ""
+"Selecting and pressing 'Preview' enters a two-column [edit|preview] mode"
 msgstr ""
-"Seleccionar i prèmer «Previsualitza» inicia un mode "
-"d'[edició|previsualització] en dos columnes"
+"Seleccionar i prèmer «Previsualitza» inicia un mode d'[edició|"
+"previsualització] en dos columnes"
 
 #: trac/wiki/templates/wiki_edit_form.html:24
 msgid "Edit side-by-side"
@@ -7044,7 +7005,8 @@
 msgstr "Canvia el nom de [1:%(name)s]"
 
 #: trac/wiki/templates/wiki_rename.html:19
-msgid "Renaming the page will rename all existing versions of the page in place."
+msgid ""
+"Renaming the page will rename all existing versions of the page in place."
 msgstr ""
 "Si canvieu el nom de la pàgina, també es canviarà el nom de totes les "
 "versions existents de la pàgina al mateix temps."
@@ -7131,8 +7093,95 @@
 msgstr "(pàgina en blanc)"
 
 #: trac/wiki/templates/wiki_view.html:135
-msgid "The following pages have a name similar to this page, and may be related:"
+msgid ""
+"The following pages have a name similar to this page, and may be related:"
 msgstr ""
-"Les pàgines següents tenen un nom similar a aquesta pàgina, i poden estar"
-" relacionades:"
+"Les pàgines següents tenen un nom similar a aquesta pàgina, i poden estar "
+"relacionades:"
 
+#~ msgid "comment:"
+#~ msgstr "comentari:"
+
+#~ msgid "%(perm)s privileges are required to perform this operation"
+#~ msgstr ""
+#~ "Es requereixen privilegis de %(perm)s per a realitzar aquesta operació"
+
+#~ msgid "Note:"
+#~ msgstr "Nota:"
+
+#~ msgid "Date:"
+#~ msgstr "Data:"
+
+#~ msgid ""
+#~ "[1:[2:%(title)s:]]\n"
+#~ "          [3:[4:%(count)s]]"
+#~ msgstr ""
+#~ "[1:[2:%(title)s:]]\n"
+#~ "          [3:[4:%(count)s]]"
+
+#~ msgid ""
+#~ "[1:[2:Total:]]\n"
+#~ "      [3:[4:%(count)s]]"
+#~ msgstr ""
+#~ "[1:[2:Total:]]\n"
+#~ "      [3:[4:%(count)s]]"
+
+#~ msgid "created"
+#~ msgstr "creat"
+
+#~ msgid "reopened"
+#~ msgstr "reobert"
+
+#~ msgid "updated"
+#~ msgstr "actualitzat"
+
+#~ msgid "Delete Milestone"
+#~ msgstr "Suprimeix la fita"
+
+#~ msgid "Edit Milestone"
+#~ msgstr "Edita la fita"
+
+#~ msgid "Report"
+#~ msgstr "Informe"
+
+#~ msgid "Ticket #"
+#~ msgstr "Tiquet #"
+
+#~ msgid "View"
+#~ msgstr "Visualitza"
+
+#~ msgid ""
+#~ "Warnings are shown at the [1:top of the page]. The ticket validation\n"
+#~ "            may have failed."
+#~ msgstr ""
+#~ "Es mostren els avisos a la [1:part superior de la pàgina]. La validació\n"
+#~ "            del tiquet pot haver fallat."
+
+#~ msgid "%(date)s in Timeline"
+#~ msgstr "%(date)s a l'Historial"
+
+#~ msgid ""
+#~ "Revision [1:%(rev)s],\n"
+#~ "            [2:%(size)s]\n"
+#~ "            checked in by %(author)s, %(date)s ago\n"
+#~ "            ([3:diff])"
+#~ msgstr ""
+#~ "Revisió [1:%(rev)s],\n"
+#~ "            [2:%(size)s]\n"
+#~ "            enregistrada per %(author)s, fa %(date)s\n"
+#~ "            ([3:diff])"
+
+#~ msgid ""
+#~ "Are you sure you want to delete versions %(from)s to %(to)s of this page?"
+#~ msgstr ""
+#~ "Esteu segur de voler suprimir les versions de %(from)s a %(to)s d'aquesta "
+#~ "pàgina?"
+
+#~ msgid "Editing"
+#~ msgstr "S'està editant"
+
+#~ msgid "See the start of the diffs"
+#~ msgstr "Vegeu l'inici dels diffs"
+
+#~ msgid "See the start of the preview"
+#~ msgstr "Vegeu l'inici de la previsualizació"
diff --git a/trac/trac/locale/cs/LC_MESSAGES/messages.po b/trac/trac/locale/cs/LC_MESSAGES/messages.po
index 7554891..f8df9fe 100644
--- a/trac/trac/locale/cs/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/cs/LC_MESSAGES/messages.po
@@ -5,7 +5,7 @@
 # Translators:
 # Radek Bartoň <xbarto33@stud.fit.vutbr.cz>, 2007.
 # Tomáš Čapek <soulcharmer@gmail.com>, 2010.
-# Zbyněk Schwarz <zbynek.schwarz@gmail.com>, 2012.
+# Zbyněk Schwarz <zbynek.schwarz@gmail.com>, 2012-2013.
 msgid ""
 msgstr ""
 "Project-Id-Version:  Trac\n"
@@ -19,7 +19,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -757,7 +757,7 @@
 msgstr ""
 "Vítejte v trac-admin %(version)s\n"
 "Interaktivní konzoli pro správu Trac.\n"
-"Autorská práva (C) 2003-2012 Edgewall Software\n"
+"Autorská práva (C) 2003-2013 Edgewall Software\n"
 "\n"
 "Zadejte:  '?' nebo 'help' pro nápovědu pro příkazy.\n"
 "        "
@@ -778,13 +778,15 @@
 "No documentation found for '%(cmd)s'. Use 'help' to see the list of "
 "commands."
 msgstr ""
+"Pro '%(cmd)s' nebyla nalezena žádná dokumentace, Použijte 'help' pro "
+"zobrazení seznamu příkazů."
 
 #: trac/admin/console.py:322
 msgid "Did you mean this?"
 msgid_plural "Did you mean one of these?"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Mysleli jste toto?"
+msgstr[1] "Mysleli jste tyto?"
+msgstr[2] "Mysleli jste tyto?"
 
 #: trac/admin/console.py:326
 #, python-format
@@ -2121,7 +2123,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Autorská práva © 2003-2012\n"
+"Autorská práva © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
diff --git a/trac/trac/locale/da/LC_MESSAGES/messages-js.po b/trac/trac/locale/da/LC_MESSAGES/messages-js.po
index 6e88610..3fc240e 100644
--- a/trac/trac/locale/da/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/da/LC_MESSAGES/messages-js.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/da/LC_MESSAGES/messages.po b/trac/trac/locale/da/LC_MESSAGES/messages.po
index 8c3baa2..cdf0cd0 100644
--- a/trac/trac/locale/da/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/da/LC_MESSAGES/messages.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -772,7 +772,7 @@
 msgstr ""
 "Velkommen til trac-admin %(version)s\n"
 "Interaktiv Trac administrationskonsol.\n"
-"Copyright (C) 2003-2012 Edgewall Software\n"
+"Copyright (C) 2003-2013 Edgewall Software\n"
 "\n"
 "Skriv:  '?' eller 'help' for hjælp til kommandoer.\n"
 "        "
diff --git a/trac/trac/locale/de/LC_MESSAGES/messages-js.po b/trac/trac/locale/de/LC_MESSAGES/messages-js.po
index 3f81ea7..85baeaf 100644
--- a/trac/trac/locale/de/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/de/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/de/LC_MESSAGES/messages.po b/trac/trac/locale/de/LC_MESSAGES/messages.po
index 4ca9207..af3e712 100644
--- a/trac/trac/locale/de/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/de/LC_MESSAGES/messages.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -776,7 +776,7 @@
 msgstr ""
 "Willkommen bei trac-admin %(version)s\n"
 "Interaktive Administrations-Konsole von Trac\n"
-"Copyright (c) 2003-2012 Edgewall Software\n"
+"Copyright (c) 2003-2013 Edgewall Software\n"
 "\n"
 "Geben Sie '?' oder 'help' für Hilfe zu den Kommandos ein.\n"
 "        "
@@ -2175,7 +2175,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003–2012\n"
+"Copyright © 2003–2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
@@ -4948,7 +4948,7 @@
 #, python-format
 msgid "You should now run %(resync)s to synchronize Trac with the repository."
 msgstr ""
-"Sie sollten jetzt %(resync)s ausführen, um Trac mit dem Reposiory zu "
+"Sie sollten jetzt %(resync)s ausführen, um Trac mit dem Repository zu "
 "synchronisieren."
 
 #: trac/versioncontrol/admin.py:225
diff --git a/trac/trac/locale/el/LC_MESSAGES/messages.po b/trac/trac/locale/el/LC_MESSAGES/messages.po
index 7a74e75..e415544 100644
--- a/trac/trac/locale/el/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/el/LC_MESSAGES/messages.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -7361,3 +7361,17 @@
 "Οι ακόλουθες σελίδες έχουν όνομα παρόμοιο με αυτήν και ίσως έχουν κάποια "
 "σχέση:"
 
+#~ msgid ""
+#~ "Welcome to trac-admin %(version)s\n"
+#~ "Interactive Trac administration console.\n"
+#~ "Copyright (C) 2003-2012 Edgewall Software\n"
+#~ "\n"
+#~ "Type:  '?' or 'help' for help on commands.\n"
+#~ "        "
+#~ msgstr ""
+
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/en_GB/LC_MESSAGES/messages-js.po b/trac/trac/locale/en_GB/LC_MESSAGES/messages-js.po
index b517a22..cf669ff 100644
--- a/trac/trac/locale/en_GB/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/en_GB/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/eo/LC_MESSAGES/messages-js.po b/trac/trac/locale/eo/LC_MESSAGES/messages-js.po
index 210b1d4..8ab3cca 100644
--- a/trac/trac/locale/eo/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/eo/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/eo/LC_MESSAGES/messages.po b/trac/trac/locale/eo/LC_MESSAGES/messages.po
index 9116bef..2efbe65 100644
--- a/trac/trac/locale/eo/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/eo/LC_MESSAGES/messages.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
diff --git a/trac/trac/locale/es/LC_MESSAGES/messages-js.po b/trac/trac/locale/es/LC_MESSAGES/messages-js.po
index 4f99979..06bbb22 100644
--- a/trac/trac/locale/es/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/es/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/es/LC_MESSAGES/messages.po b/trac/trac/locale/es/LC_MESSAGES/messages.po
index c9ec2ae..cd8c1c3 100644
--- a/trac/trac/locale/es/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/es/LC_MESSAGES/messages.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -765,7 +765,7 @@
 msgstr ""
 "Bienvenido a trac-admin %(version)s\n"
 "Administración por consola interactiva de Trac.\n"
-"Copyright (C) 2003-2012 Edgewall Software\n"
+"Copyright (C) 2003-2013 Edgewall Software\n"
 "\n"
 "Escriba:  '?' o 'help' para ayuda sobre órdenes.\n"
 "        "
@@ -2166,7 +2166,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2012\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
diff --git a/trac/trac/locale/es_AR/LC_MESSAGES/messages-js.po b/trac/trac/locale/es_AR/LC_MESSAGES/messages-js.po
index 6fd7beb..1cf2782 100644
--- a/trac/trac/locale/es_AR/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/es_AR/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/es_AR/LC_MESSAGES/messages.po b/trac/trac/locale/es_AR/LC_MESSAGES/messages.po
index ccb8023..96debb9 100644
--- a/trac/trac/locale/es_AR/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/es_AR/LC_MESSAGES/messages.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -771,7 +771,7 @@
 msgstr ""
 "Bienvenido a trac-admin %(version)s\n"
 "Consola interactive de administración de Trac.\n"
-"Copyright (c) 2003-2012 Edgewall Software\n"
+"Copyright (c) 2003-2013 Edgewall Software\n"
 "\n"
 "Escriba:  '?' o 'help' para obtener ayuda sobre órdenes.\n"
 "        "
@@ -2179,7 +2179,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2012\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
diff --git a/trac/trac/locale/es_MX/LC_MESSAGES/messages-js.po b/trac/trac/locale/es_MX/LC_MESSAGES/messages-js.po
index eb44ac7..b19285c 100644
--- a/trac/trac/locale/es_MX/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/es_MX/LC_MESSAGES/messages-js.po
@@ -20,7 +20,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/es_MX/LC_MESSAGES/messages.po b/trac/trac/locale/es_MX/LC_MESSAGES/messages.po
index 8f54ac3..484587f 100644
--- a/trac/trac/locale/es_MX/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/es_MX/LC_MESSAGES/messages.po
@@ -19,7 +19,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -767,7 +767,7 @@
 msgstr ""
 "Bienvenido a trac-admin %(version)s\n"
 "Administración interactiva por consola de Trac.\n"
-"Copyright (C) 2003-2012 Edgewall Software\n"
+"Copyright (C) 2003-2013 Edgewall Software\n"
 "\n"
 "Escriba:  '?' o 'help' para ayuda sobre comandos.\n"
 "        "
@@ -2175,7 +2175,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2012\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
diff --git a/trac/trac/locale/et/LC_MESSAGES/messages-js.po b/trac/trac/locale/et/LC_MESSAGES/messages-js.po
index e94f401..70956ad 100644
--- a/trac/trac/locale/et/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/et/LC_MESSAGES/messages-js.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/et/LC_MESSAGES/messages.po b/trac/trac/locale/et/LC_MESSAGES/messages.po
index 0ff71fb..bbc3556 100644
--- a/trac/trac/locale/et/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/et/LC_MESSAGES/messages.po
@@ -18,7 +18,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -7089,3 +7089,17 @@
 "Järgmistel lehekülgedel on sellele lehele sarnane nimi, ja nad võivad "
 "olla seotud:"
 
+#~ msgid ""
+#~ "Welcome to trac-admin %(version)s\n"
+#~ "Interactive Trac administration console.\n"
+#~ "Copyright (C) 2003-2012 Edgewall Software\n"
+#~ "\n"
+#~ "Type:  '?' or 'help' for help on commands.\n"
+#~ "        "
+#~ msgstr ""
+
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/fi/LC_MESSAGES/messages-js.po b/trac/trac/locale/fi/LC_MESSAGES/messages-js.po
index b97c426..de004e5 100644
--- a/trac/trac/locale/fi/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/fi/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/fr/LC_MESSAGES/messages-js.po b/trac/trac/locale/fr/LC_MESSAGES/messages-js.po
index aa9d18b..11c4582 100644
--- a/trac/trac/locale/fr/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/fr/LC_MESSAGES/messages-js.po
@@ -7,7 +7,7 @@
 msgstr ""
 "Project-Id-Version: Trac 1.0\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2013-03-21 22:54+0100\n"
 "PO-Revision-Date: 2011-01-25 20:28+0100\n"
 "Last-Translator: Christian Boos <cboos@edgewall.org>\n"
 "Language-Team: fr <trac-dev@googlegroups.com>\n"
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/fr/LC_MESSAGES/messages.po b/trac/trac/locale/fr/LC_MESSAGES/messages.po
index 05ce134..3ea6a3f 100644
--- a/trac/trac/locale/fr/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/fr/LC_MESSAGES/messages.po
@@ -10,7 +10,7 @@
 msgstr ""
 "Project-Id-Version: Trac 1.0\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2013-03-21 22:54+0100\n"
 "PO-Revision-Date: 2011-01-25 20:28+0100\n"
 "Last-Translator: Christian Boos <cboos@edgewall.org>\n"
 "Language-Team: fr_FR <trac-dev@googlegroups.com>\n"
@@ -18,7 +18,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -794,7 +794,7 @@
 msgstr ""
 "Bienvenue dans trac-admin %(version)s\n"
 "Console d'administration pour Trac.\n"
-"Copyright (c) 2003-2012 Edgewall Software\n"
+"Copyright (c) 2003-2013 Edgewall Software\n"
 "\n"
 "Saisissez : « ? » ou « help » pour obtenir une aide sur les commandes.\n"
 "        "
@@ -1339,7 +1339,7 @@
 #: trac/admin/templates/admin_components.html:98
 #: trac/admin/templates/admin_enums.html:69
 #: trac/admin/templates/admin_milestones.html:131
-#: trac/admin/templates/admin_perms.html:109
+#: trac/admin/templates/admin_perms.html:111
 #: trac/admin/templates/admin_versions.html:99
 #: trac/versioncontrol/templates/admin_repositories.html:145
 msgid "Remove selected items"
@@ -1528,31 +1528,32 @@
 msgstr "Ajoute un utilisateur ou un groupe à un groupe de permission existant."
 
 #: trac/admin/templates/admin_perms.html:63
-#: trac/admin/templates/admin_perms.html:88
+#: trac/admin/templates/admin_perms.html:90
 msgid "Subject"
 msgstr "Sujet"
 
 #: trac/admin/templates/admin_perms.html:76
-msgid "Action is no longer defined"
-msgstr "L'action n'est plus définie"
+#, python-format
+msgid "%(action)s is no longer defined"
+msgstr "L'action « %(action)s » n'est plus définie"
 
-#: trac/admin/templates/admin_perms.html:81
+#: trac/admin/templates/admin_perms.html:83
 msgid "No permissions"
 msgstr "Pas de permissions"
 
-#: trac/admin/templates/admin_perms.html:85
+#: trac/admin/templates/admin_perms.html:87
 msgid "Group Membership"
 msgstr "Appartenance aux groupes"
 
-#: trac/admin/templates/admin_perms.html:88
+#: trac/admin/templates/admin_perms.html:90
 msgid "Group"
 msgstr "Groupe"
 
-#: trac/admin/templates/admin_perms.html:105
+#: trac/admin/templates/admin_perms.html:107
 msgid "No group memberships"
 msgstr "Aucun groupe n'est défini"
 
-#: trac/admin/templates/admin_perms.html:113
+#: trac/admin/templates/admin_perms.html:115
 msgid ""
 "Note that [1:Subject] or [2:Group] names can't be all upper-case,\n"
 "      as that is reserved for permission names."
@@ -1633,17 +1634,14 @@
 msgstr "Modification d'une version :"
 
 #: trac/admin/templates/admin_versions.html:31
-msgid "Date:"
-msgstr "Date :"
+#: trac/admin/templates/admin_versions.html:64
+msgid "Released:"
+msgstr "Livrée :"
 
 #: trac/admin/templates/admin_versions.html:59
 msgid "Add Version:"
 msgstr "Ajout d'une version :"
 
-#: trac/admin/templates/admin_versions.html:64
-msgid "Released:"
-msgstr "Livrée :"
-
 #: trac/admin/templates/admin_versions.html:83
 msgid "Released"
 msgstr "Livrée"
@@ -2218,7 +2216,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2012\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
@@ -3170,7 +3168,7 @@
 msgid "Next status will be '%(name)s'"
 msgstr "Le prochain statut sera « %(name)s »"
 
-#: trac/ticket/default_workflow.py:418
+#: trac/ticket/default_workflow.py:419
 msgid ""
 "Render a workflow graph.\n"
 "\n"
@@ -3252,7 +3250,7 @@
 "    }}}\n"
 "}}}"
 
-#: trac/ticket/default_workflow.py:493
+#: trac/ticket/default_workflow.py:494
 msgid "Enable JavaScript to display the workflow graph."
 msgstr "Activer JavaScript pour visualiser le graphe du workflow"
 
@@ -3429,7 +3427,7 @@
 "\n"
 "\n"
 "Groups of field constraints to be OR-ed together can be separated by a\n"
-"litteral `or` argument.\n"
+"literal `or` argument.\n"
 "\n"
 "In addition to filters, several other named parameters can be used\n"
 "to control how the results are presented. All of them are optional.\n"
@@ -3775,8 +3773,8 @@
 #: trac/ticket/web_ui.py:901 trac/ticket/web_ui.py:958
 #: trac/ticket/web_ui.py:966 trac/ticket/web_ui.py:1037
 #: trac/ticket/web_ui.py:1082 trac/ticket/web_ui.py:1089
-#: trac/wiki/web_ui.py:449 trac/wiki/web_ui.py:455 trac/wiki/web_ui.py:653
-#: trac/wiki/web_ui.py:667
+#: trac/wiki/web_ui.py:458 trac/wiki/web_ui.py:464 trac/wiki/web_ui.py:662
+#: trac/wiki/web_ui.py:676
 #, python-format
 msgid "Version %(num)s"
 msgstr "Version %(num)s"
@@ -3795,12 +3793,12 @@
 msgstr "Propriété %(label)s modifiée (%(rendered)s)"
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:477
 msgid "Previous Change"
 msgstr "Modification précédente"
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:477
 msgid "Next Change"
 msgstr "Modification suivante"
 
@@ -3988,7 +3986,7 @@
 
 #. TRANSLATOR: modified ('diff') (link)
 #: trac/ticket/web_ui.py:1732 trac/ticket/templates/ticket_change.html:155
-#: trac/wiki/web_ui.py:747
+#: trac/wiki/web_ui.py:756
 msgid "diff"
 msgstr "modification"
 
@@ -4474,14 +4472,12 @@
 msgstr "Ajouter un nouveau jalon"
 
 #: trac/ticket/templates/ticket.html:132
-#, fuzzy
 msgid "Go to the ticket editor"
-msgstr "Éditer la page"
+msgstr "Aller éditer le ticket"
 
 #: trac/ticket/templates/ticket.html:132
-#, fuzzy
 msgid "Modify"
-msgstr "modifié"
+msgstr "Modifier"
 
 #: trac/ticket/templates/ticket.html:134
 msgid "Create New Ticket"
@@ -4601,9 +4597,8 @@
 msgstr "Allez à la liste des pièces jointes"
 
 #: trac/ticket/templates/ticket.html:379
-#, fuzzy
 msgid "View the ticket description"
-msgstr "Aucun droit pour modifier la description."
+msgstr "Voir la description du ticket."
 
 #: trac/ticket/templates/ticket.html:387
 #: trac/ticket/templates/ticket_change.html:114
@@ -4893,7 +4888,7 @@
 "        de l'aide relative aux activités."
 
 #: trac/upgrades/db28.py:72
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "The upgrade of attachments was successful, but the old attachments "
 "directory:\n"
@@ -4918,7 +4913,7 @@
 " était :\n"
 "\n"
 "  %(exception)s\n"
-"  \n"
+"\n"
 "Cette erreur peut être ignorée, mais pour conserver un environnement en "
 "bon ordre, il conviendrait de faire une sauvegarde des fichiers encore "
 "présent dans ce répertoire et de le supprimer manuellement ensuite.\n"
@@ -4942,56 +4937,56 @@
 "\n"
 "  %(new_path)s\n"
 
-#: trac/util/datefmt.py:118
+#: trac/util/datefmt.py:122
 #, python-format
 msgid "%(num)d year"
 msgid_plural "%(num)d years"
 msgstr[0] "un an"
 msgstr[1] "%(num)d ans"
 
-#: trac/util/datefmt.py:119
+#: trac/util/datefmt.py:123
 #, python-format
 msgid "%(num)d month"
 msgid_plural "%(num)d months"
 msgstr[0] "un mois"
 msgstr[1] "%(num)d mois"
 
-#: trac/util/datefmt.py:120
+#: trac/util/datefmt.py:124
 #, python-format
 msgid "%(num)d week"
 msgid_plural "%(num)d weeks"
 msgstr[0] "une semaine"
 msgstr[1] "%(num)d semaines"
 
-#: trac/util/datefmt.py:121
+#: trac/util/datefmt.py:125
 #, python-format
 msgid "%(num)d day"
 msgid_plural "%(num)d days"
 msgstr[0] "un jour"
 msgstr[1] "%(num)d jours"
 
-#: trac/util/datefmt.py:122
+#: trac/util/datefmt.py:126
 #, python-format
 msgid "%(num)d hour"
 msgid_plural "%(num)d hours"
 msgstr[0] "une heure"
 msgstr[1] "%(num)d heures"
 
-#: trac/util/datefmt.py:123
+#: trac/util/datefmt.py:127
 #, python-format
 msgid "%(num)d minute"
 msgid_plural "%(num)d minutes"
 msgstr[0] "une minute"
 msgstr[1] "%(num)d minutes"
 
-#: trac/util/datefmt.py:142
+#: trac/util/datefmt.py:146
 #, python-format
 msgid "%(num)i second"
 msgid_plural "%(num)i seconds"
 msgstr[0] "%(num)i seconde"
 msgstr[1] "%(num)i secondes"
 
-#: trac/util/datefmt.py:464
+#: trac/util/datefmt.py:468
 #, python-format
 msgid ""
 "\"%(date)s\" is an invalid date, or the date format is not known. Try "
@@ -5000,11 +4995,11 @@
 "« %(date)s » n'est pas une date valide, ou bien le format n'est pas "
 "reconnu. Essayez plutôt le format « %(hint)s »."
 
-#: trac/util/datefmt.py:466 trac/util/datefmt.py:474
+#: trac/util/datefmt.py:470 trac/util/datefmt.py:478
 msgid "Invalid Date"
 msgstr "Date erronée"
 
-#: trac/util/datefmt.py:472
+#: trac/util/datefmt.py:476
 #, python-format
 msgid ""
 "The date \"%(date)s\" is outside valid range. Try a date closer to "
@@ -5611,7 +5606,7 @@
 "                %(old_path)s])"
 msgstr ""
 "(%(kind)s à partir de [1:\n"
-"                %(old_path)s)"
+"                %(old_path)s])"
 
 #: trac/versioncontrol/templates/changeset.html:119
 #: trac/versioncontrol/templates/changeset.html:122
@@ -6440,7 +6435,7 @@
 msgstr "La page « %(page)s » est introuvable"
 
 #: trac/wiki/admin.py:118 trac/wiki/model.py:127 trac/wiki/model.py:174
-#: trac/wiki/web_ui.py:119
+#: trac/wiki/web_ui.py:120
 #, python-format
 msgid "Invalid Wiki page name '%(name)s'"
 msgstr "Nom de page Wiki non valide : '%(name)s'"
@@ -6469,7 +6464,7 @@
 msgid "Edits"
 msgstr "Versions"
 
-#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:300
+#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:301
 msgid "A new name is mandatory for a rename."
 msgstr "Un nouveau nom est obligatoire pour pouvoir renommer."
 
@@ -6477,7 +6472,7 @@
 msgid "The new name is invalid."
 msgstr "Le nouveau nom n'est pas valide."
 
-#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:307
+#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:308
 #, python-format
 msgid "The page %(name)s already exists."
 msgstr "La page %(name)s existe déjà."
@@ -6817,59 +6812,59 @@
 msgid "Can't rename to existing %(name)s page."
 msgstr "Impossible de renommer vers la page existante %(name)s"
 
-#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:754
+#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:763
 msgid "Wiki"
 msgstr "Wiki"
 
-#: trac/wiki/web_ui.py:89
+#: trac/wiki/web_ui.py:90
 msgid "Help/Guide"
 msgstr "Aide / Guide"
 
-#: trac/wiki/web_ui.py:130
+#: trac/wiki/web_ui.py:131
 #, python-format
 msgid "No version \"%(num)s\" for Wiki page \"%(name)s\""
 msgstr "Aucune version « %(num)s » pour la page Wiki « %(name)s »"
 
-#: trac/wiki/web_ui.py:195
+#: trac/wiki/web_ui.py:196
 #, python-format
 msgid "The wiki page is too long (must be less than %(num)s characters)"
 msgstr ""
 "La page wiki est trop longue (elle doit être inférieure\n"
 "à %(num)s caractères)"
 
-#: trac/wiki/web_ui.py:205
+#: trac/wiki/web_ui.py:206
 #, python-format
 msgid "The Wiki page field '%(field)s' is invalid: %(message)s"
 msgstr "Le champ « %(field)s » de la page Wiki est n'est pas valide : %(message)s"
 
-#: trac/wiki/web_ui.py:209
+#: trac/wiki/web_ui.py:210
 #, python-format
 msgid "Invalid Wiki page: %(message)s"
 msgstr "Page Wiki non valide : %(message)s"
 
 #. TRANSLATOR: wiki page
-#: trac/wiki/web_ui.py:236
+#: trac/wiki/web_ui.py:237
 msgid "currently edited"
 msgstr "actuellement modifiée"
 
-#: trac/wiki/web_ui.py:269
+#: trac/wiki/web_ui.py:270
 #, python-format
 msgid "The page %(name)s has been deleted."
 msgstr "Cette page %(name)s a été supprimée."
 
-#: trac/wiki/web_ui.py:274
+#: trac/wiki/web_ui.py:275
 #, python-format
 msgid "The versions %(from_)d to %(to)d of the page %(name)s have been deleted."
 msgstr ""
 "Les versions allant de %(from_)d à %(to)d de la page %(name)s ont été "
 "supprimées."
 
-#: trac/wiki/web_ui.py:278
+#: trac/wiki/web_ui.py:279
 #, python-format
 msgid "The version %(version)d of the page %(name)s has been deleted."
 msgstr "La version %(version)d de la page %(name)s a été supprimée."
 
-#: trac/wiki/web_ui.py:302
+#: trac/wiki/web_ui.py:303
 msgid ""
 "The new name is invalid (a name which is separated with slashes cannot be"
 " '.' or '..')."
@@ -6877,103 +6872,115 @@
 "Le nouveau nom n'est pas valide (aucun élément séparé par des '/' ne peut"
 " être '.' ou '..')."
 
-#: trac/wiki/web_ui.py:305
+#: trac/wiki/web_ui.py:306
 msgid "The new name must be different from the old name."
 msgstr "Le nouveau nom doit être différent de l'ancien nom."
 
-#: trac/wiki/web_ui.py:316
+#: trac/wiki/web_ui.py:317
 #, python-format
 msgid "See [wiki:\"%(name)s\"]."
 msgstr "Voir [wiki:\"%(name)s\"]."
 
-#: trac/wiki/web_ui.py:340
+#: trac/wiki/web_ui.py:323
+#, python-format
+msgid "The page %(old_name)s has been renamed to %(new_name)s."
+msgstr "La page « %(old_name)s » a été renommée « %(new_name)s »."
+
+#: trac/wiki/web_ui.py:327
+#, python-format
+msgid "The page %(old_name)s has been recreated with a redirect to %(new_name)s."
+msgstr ""
+"La page « %(old_name)s » a été recréée et redirige à présent vers \"\n"
+"\"« %(new_name)s »."
+
+#: trac/wiki/web_ui.py:349
 #, python-format
 msgid "Your changes have been saved in version %(version)s."
 msgstr "Vos modifications ont été enregistrées dans la version %(version)s."
 
-#: trac/wiki/web_ui.py:345
+#: trac/wiki/web_ui.py:354
 msgid "Page not modified, showing latest version."
 msgstr "Page inchangée, version précédente affichée."
 
-#: trac/wiki/web_ui.py:399
+#: trac/wiki/web_ui.py:408
 #, python-format
 msgid "Version %(num)s of page \"%(name)s\" does not exist"
 msgstr "La version %(num)s de la page « %(name)s » n'existe pas"
 
-#: trac/wiki/web_ui.py:451
+#: trac/wiki/web_ui.py:460
 msgid "Page history"
 msgstr "Historique de la page"
 
-#: trac/wiki/web_ui.py:469
+#: trac/wiki/web_ui.py:478
 msgid "Wiki History"
 msgstr "Historique de la page"
 
-#: trac/wiki/web_ui.py:499
+#: trac/wiki/web_ui.py:508
 #, python-format
 msgid "Reverted to version %(version)s."
 msgstr "Rétabli à la version %(version)s."
 
-#: trac/wiki/web_ui.py:562
+#: trac/wiki/web_ui.py:571
 #, python-format
 msgid "Page %(name)s does not exist"
 msgstr "La page %(name)s n'existe pas"
 
-#: trac/wiki/web_ui.py:576
+#: trac/wiki/web_ui.py:585
 #, python-format
 msgid "Back to %(wikipage)s"
 msgstr "Retour à la page %(wikipage)s"
 
-#: trac/wiki/web_ui.py:604
+#: trac/wiki/web_ui.py:613
 #, python-format
 msgid "Page %(name)s not found"
 msgstr "La page %(name)s est introuvable"
 
-#: trac/wiki/web_ui.py:658
+#: trac/wiki/web_ui.py:667
 msgid "View latest version"
 msgstr "Voir la dernière version"
 
-#: trac/wiki/web_ui.py:662
+#: trac/wiki/web_ui.py:671
 msgid "View parent page"
 msgstr "Voir la page parente"
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:680
 msgid "Previous Version"
 msgstr "Version précédente"
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:680
 msgid "Next Version"
 msgstr "Version suivante"
 
-#: trac/wiki/web_ui.py:672
+#: trac/wiki/web_ui.py:681
 msgid "View Latest Version"
 msgstr "Voir la dernière version"
 
-#: trac/wiki/web_ui.py:675
+#: trac/wiki/web_ui.py:684
 msgid "Up"
 msgstr "Remonter"
 
-#: trac/wiki/web_ui.py:700
+#: trac/wiki/web_ui.py:709
 msgid "Start Page"
 msgstr "Page d'accueil"
 
-#: trac/wiki/web_ui.py:701
+#: trac/wiki/web_ui.py:710
 msgid "Index"
 msgstr "Index"
 
-#: trac/wiki/web_ui.py:703
+#: trac/wiki/web_ui.py:712
 msgid "History"
 msgstr "Historique"
 
-#: trac/wiki/web_ui.py:710
+#: trac/wiki/web_ui.py:719
 msgid "Wiki changes"
 msgstr "Modifications des pages Wiki"
 
-#: trac/wiki/web_ui.py:737
+#: trac/wiki/web_ui.py:746
 #, python-format
 msgid "%(page)s edited"
 msgstr "%(page)s modifiée"
 
-#: trac/wiki/web_ui.py:739
+#: trac/wiki/web_ui.py:748
 #, python-format
 msgid "%(page)s created"
 msgstr "%(page)s créée"
@@ -6994,7 +7001,7 @@
 msgstr "Supprimer [1:%(name)s]"
 
 #: trac/wiki/templates/wiki_delete.html:38
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "[1:\n"
 "                  Are you sure you want to delete versions %(from)s to "
@@ -7012,7 +7019,7 @@
 "                ]\n"
 "                [2:]\n"
 "                  Supprimer [3:\n"
-"                    %(versions)s versions] de la page, \n"
+"                    %(versions)s versions] de la page,\n"
 "                  initiallement modifiée le %(first_modified)s et "
 "dernièrement modifiée le %(last_modified)s."
 
@@ -7056,7 +7063,7 @@
 "supprimée !"
 
 #: trac/wiki/templates/wiki_delete.html:83
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "Removing all [1:\n"
 "                        %(versions)s versions] of the page,\n"
@@ -7064,8 +7071,8 @@
 "%(modified)s."
 msgstr ""
 "Suppression de toutes les [1:\n"
-"                        %(versions)s versions] de la page, \n"
-"                    créée %(created)s et dernièrement modifiée \n"
+"                        %(versions)s versions] de la page,\n"
+"                    créée %(created)s et dernièrement modifiée "
 "%(modified)s."
 
 #: trac/wiki/templates/wiki_delete.html:99
diff --git a/trac/trac/locale/fr/LC_MESSAGES/tracini.po b/trac/trac/locale/fr/LC_MESSAGES/tracini.po
index 7398b9d..f91bc06 100644
--- a/trac/trac/locale/fr/LC_MESSAGES/tracini.po
+++ b/trac/trac/locale/fr/LC_MESSAGES/tracini.po
@@ -7,7 +7,7 @@
 msgstr ""
 "Project-Id-Version: Trac 0.13\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2013-03-21 22:54+0100\n"
 "PO-Revision-Date: 2011-02-23 22:27+0900\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: fr <LL@li.org>\n"
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/enscript.py:105
 msgid "Path to the Enscript executable."
@@ -159,15 +159,15 @@
 msgid "Path to the git executable."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:749
+#: tracopt/versioncontrol/git/git_fs.py:752
 msgid "Path to a gitweb-formatted projects.list"
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:752
+#: tracopt/versioncontrol/git/git_fs.py:755
 msgid "Path to the base of your git projects"
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:755
+#: tracopt/versioncontrol/git/git_fs.py:758
 #, python-format
 msgid ""
 "Template for project URLs. %s will be replaced with the repo\n"
diff --git a/trac/trac/locale/gl/LC_MESSAGES/messages.po b/trac/trac/locale/gl/LC_MESSAGES/messages.po
index 2681520..d75ed0a 100644
--- a/trac/trac/locale/gl/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/gl/LC_MESSAGES/messages.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -7084,3 +7084,8 @@
 "As páxinas seguintes teñen un nome similar a esta, e poden estar "
 "relacionadas:"
 
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/he/LC_MESSAGES/messages-js.po b/trac/trac/locale/he/LC_MESSAGES/messages-js.po
index b0e2a19..63bfa3b 100644
--- a/trac/trac/locale/he/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/he/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/he/LC_MESSAGES/messages.po b/trac/trac/locale/he/LC_MESSAGES/messages.po
index cfb1688..d372b29 100644
--- a/trac/trac/locale/he/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/he/LC_MESSAGES/messages.po
@@ -11,7 +11,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
diff --git a/trac/trac/locale/hu/LC_MESSAGES/messages-js.po b/trac/trac/locale/hu/LC_MESSAGES/messages-js.po
index 9944439..312028a 100644
--- a/trac/trac/locale/hu/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/hu/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/hu/LC_MESSAGES/messages.po b/trac/trac/locale/hu/LC_MESSAGES/messages.po
index 8af4673..0a95827 100644
--- a/trac/trac/locale/hu/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/hu/LC_MESSAGES/messages.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -773,6 +773,12 @@
 "Type:  '?' or 'help' for help on commands.\n"
 "        "
 msgstr ""
+"Üdvözöli Önt a trac-admin %(version)s\n"
+"Interaktív Trac Adminisztrációs Konzol.\n"
+"Copyright (C) 2003-2013 Edgewall Software\n"
+"\n"
+"Segítséget a '?' vagy 'help' beírásával kaphat.\n"
+"        "
 
 #: trac/admin/console.py:166
 #, python-format
@@ -2113,6 +2119,8 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
+"Copyright © 2003-2013\n"
+"        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
 msgid "System Information"
diff --git a/trac/trac/locale/hy/LC_MESSAGES/messages-js.po b/trac/trac/locale/hy/LC_MESSAGES/messages-js.po
index 8d12f26..33601b3 100644
--- a/trac/trac/locale/hy/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/hy/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/hy/LC_MESSAGES/messages.po b/trac/trac/locale/hy/LC_MESSAGES/messages.po
index bc3798b..ab75ecc 100644
--- a/trac/trac/locale/hy/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/hy/LC_MESSAGES/messages.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
diff --git a/trac/trac/locale/it/LC_MESSAGES/messages-js.po b/trac/trac/locale/it/LC_MESSAGES/messages-js.po
index 9c44154..8711311 100644
--- a/trac/trac/locale/it/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/it/LC_MESSAGES/messages-js.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/it/LC_MESSAGES/messages.po b/trac/trac/locale/it/LC_MESSAGES/messages.po
index 26785e1..b270d7d 100644
--- a/trac/trac/locale/it/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/it/LC_MESSAGES/messages.po
@@ -4,6 +4,7 @@
 #
 # Translators:
 # Giancarlo Gaifas <lallo@artiemestieri.tn.it>, 2007.
+# Roberto Longobardi <seccanj@gmail.com>, 2012-2013.
 msgid ""
 msgstr ""
 "Project-Id-Version:  Trac\n"
@@ -17,7 +18,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -776,6 +777,12 @@
 "Type:  '?' or 'help' for help on commands.\n"
 "        "
 msgstr ""
+"Benvenuto in trac-admin %(version)s\n"
+"Console di amministrazione interattiva di Trac.\n"
+"Copyright (c) 2003-2013 Edgewall Software\n"
+"\n"
+"Inserisci '?' oppure 'help' per un aiuto sui comandi.\n"
+"        "
 
 #: trac/admin/console.py:166
 #, python-format
@@ -793,8 +800,8 @@
 "No documentation found for '%(cmd)s'. Use 'help' to see the list of "
 "commands."
 msgstr ""
-"Non è stata trovata alcuna documentazione per \"%(cmd)s\".Usa 'help' per "
-"vedere la lista di comandi."
+"Non è stata trovata alcuna documentazione per \"%(cmd)s\". Usare 'help' "
+"per ottenere la lista di comandi disponibili."
 
 #: trac/admin/console.py:322
 msgid "Did you mean this?"
@@ -2132,6 +2139,8 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
+"Copyright © 2003-2013\n"
+"        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
 msgid "System Information"
diff --git a/trac/trac/locale/ja/LC_MESSAGES/messages-js.po b/trac/trac/locale/ja/LC_MESSAGES/messages-js.po
index 39e5e79..2d2b970 100644
--- a/trac/trac/locale/ja/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/ja/LC_MESSAGES/messages-js.po
@@ -1,13 +1,13 @@
 # Japanese translations for Trac.
-# Copyright (C) 2010-2013 Edgewall Software
+# Copyright (C) 2010-2014 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# Jun Omae <jun66j5@gmail.com>, 2010-2013.
+# Jun Omae <jun66j5@gmail.com>, 2010-2014.
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: Trac 1.0\n"
+"Project-Id-Version: Trac 1.0.2\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2014-09-01 12:43+0000\n"
 "PO-Revision-Date: 2010-05-25 18:45+0900\n"
 "Last-Translator: Jun Omae <jun66j5@gmail.com>\n"
 "Language-Team: ja <LL@li.org>\n"
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
@@ -76,7 +76,7 @@
 
 #. TRANSLATOR: Link that closes the datepicker
 #. TRANSLATOR: Link that closes the timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:7 trac/htdocs/js/jquery-ui-i18n.js:39
+#: trac/htdocs/js/jquery-ui-i18n.js:7 trac/htdocs/js/jquery-ui-i18n.js:40
 msgid "Done"
 msgstr "閉じる"
 
@@ -101,34 +101,34 @@
 msgstr "週"
 
 #. TRANSLATOR: Heading of the standalone timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:30
+#: trac/htdocs/js/jquery-ui-i18n.js:31
 msgid "Choose Time"
 msgstr "時間を選択"
 
 #. TRANSLATOR: Time selector label
-#: trac/htdocs/js/jquery-ui-i18n.js:32
+#: trac/htdocs/js/jquery-ui-i18n.js:33
 msgid "Time"
 msgstr "時間"
 
 #. TRANSLATOR: Time labels in the timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:34
+#: trac/htdocs/js/jquery-ui-i18n.js:35
 msgid "Hour"
 msgstr "時"
 
-#: trac/htdocs/js/jquery-ui-i18n.js:34
+#: trac/htdocs/js/jquery-ui-i18n.js:35
 msgid "Minute"
 msgstr "分"
 
-#: trac/htdocs/js/jquery-ui-i18n.js:34
+#: trac/htdocs/js/jquery-ui-i18n.js:35
 msgid "Second"
 msgstr "秒"
 
-#: trac/htdocs/js/jquery-ui-i18n.js:35
+#: trac/htdocs/js/jquery-ui-i18n.js:36
 msgid "Time Zone"
 msgstr "タイムゾーン"
 
 #. TRANSLATOR: Link to pick the current time in the timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:37
+#: trac/htdocs/js/jquery-ui-i18n.js:38
 msgid "Now"
 msgstr "現時刻"
 
@@ -169,7 +169,7 @@
 msgid "Select ticket %(id)s for modification"
 msgstr "更新対象としてチケット %(id)s を選択する"
 
-#: trac/htdocs/js/query.js:387
+#: trac/htdocs/js/query.js:388
 msgid "Toggle selection of all tickets shown in this group"
 msgstr "このグループに表示しているすべてのチケットに対し、選択を切り替える"
 
@@ -177,39 +177,39 @@
 msgid "Link here"
 msgstr "ここにリンクする"
 
-#: trac/htdocs/js/wikitoolbar.js:56
+#: trac/htdocs/js/wikitoolbar.js:59
 msgid "Bold text: '''Example'''"
 msgstr "太字: '''例'''"
 
-#: trac/htdocs/js/wikitoolbar.js:59
+#: trac/htdocs/js/wikitoolbar.js:62
 msgid "Italic text: ''Example''"
 msgstr "斜体: '''例'''"
 
-#: trac/htdocs/js/wikitoolbar.js:62
+#: trac/htdocs/js/wikitoolbar.js:65
 msgid "Heading: == Example =="
 msgstr "ヘッダ: == 例 =="
 
-#: trac/htdocs/js/wikitoolbar.js:65
+#: trac/htdocs/js/wikitoolbar.js:68
 msgid "Link: [http://www.example.com/ Example]"
 msgstr "リンク: [http://www.example.com/ 例]"
 
-#: trac/htdocs/js/wikitoolbar.js:68
+#: trac/htdocs/js/wikitoolbar.js:71
 msgid "Code block: {{{ example }}}"
 msgstr "コードブロック: {{{ 例 }}}"
 
-#: trac/htdocs/js/wikitoolbar.js:71
+#: trac/htdocs/js/wikitoolbar.js:74
 msgid "Horizontal rule: ----"
 msgstr "横罫線: ----"
 
-#: trac/htdocs/js/wikitoolbar.js:74
+#: trac/htdocs/js/wikitoolbar.js:77
 msgid "New paragraph"
 msgstr "段落"
 
-#: trac/htdocs/js/wikitoolbar.js:77
+#: trac/htdocs/js/wikitoolbar.js:80
 msgid "Line break: [[BR]]"
 msgstr "改行: [[BR]]"
 
-#: trac/htdocs/js/wikitoolbar.js:80
+#: trac/htdocs/js/wikitoolbar.js:83
 msgid "Image: [[Image()]]"
 msgstr "画像: [[Image()]]"
 
diff --git a/trac/trac/locale/ja/LC_MESSAGES/messages.po b/trac/trac/locale/ja/LC_MESSAGES/messages.po
index ababecb..62a0f2f 100644
--- a/trac/trac/locale/ja/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/ja/LC_MESSAGES/messages.po
@@ -1,18 +1,18 @@
 # Japanese (Japan) translations for Trac.
-# Copyright (C) 2007-2013 Edgewall Software
+# Copyright (C) 2007-2014 Edgewall Software
 # This file is distributed under the same license as the Trac project.
 # Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>, 2007.
 # Kyosuke Takayama <support@mc.neweb.ne.jp>, 2008,2009.
 # hirobe, 2008.
 # kabuchan, 2009.
 # IWAI, Masaharu <iwai@alib.jp>, 2009,2010.
-# Jun Omae <jun66j5@gmail.com>, 2010-2013.
+# Jun Omae <jun66j5@gmail.com>, 2010-2014.
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: Trac 1.0\n"
+"Project-Id-Version: Trac 1.0.2\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2014-09-01 12:43+0000\n"
 "PO-Revision-Date: 2011-07-14 21:38+0900\n"
 "Last-Translator: Jun Omae <jun66j5@gmail.com>\n"
 "Language-Team: ja <trac-dev@googlegroups.com>\n"
@@ -20,7 +20,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -52,7 +52,7 @@
 msgid "Create a copy of this ticket"
 msgstr "このチケットのコピーを作成する"
 
-#: tracopt/ticket/commit_updater.py:275
+#: tracopt/ticket/commit_updater.py:283
 msgid ""
 "Insert a changeset message into the output.\n"
 "\n"
@@ -78,12 +78,16 @@
 " - `repository`: チェンジセットがあるリポジトリ\n"
 " - `revision`: 要求のチェンジセットを示すリビジョン"
 
+#: tracopt/ticket/commit_updater.py:313
+msgid "(The changeset message doesn't reference this ticket)"
+msgstr "(チェンジセットのメッセージはこのチケットを参照していません)"
+
 #: tracopt/ticket/deleter.py:73 tracopt/ticket/deleter.py:90
-#: trac/ticket/templates/report_list.html:82
+#: trac/ticket/templates/report_list.html:92
 msgid "Delete"
 msgstr "削除"
 
-#: tracopt/ticket/deleter.py:74 tracopt/ticket/templates/ticket_delete.html:42
+#: tracopt/ticket/deleter.py:74 tracopt/ticket/templates/ticket_delete.html:51
 msgid "Delete ticket"
 msgstr "チケットの削除"
 
@@ -107,82 +111,105 @@
 msgid "Comment %(num)s not found"
 msgstr "コメント %(num)s が見つかりません"
 
-#: tracopt/ticket/templates/ticket_delete.html:11
+#: tracopt/ticket/templates/ticket_delete.html:21
 #, python-format
 msgid "Delete Ticket #%(id)s"
 msgstr "チケット #%(id)s の削除"
 
-#: tracopt/ticket/templates/ticket_delete.html:12
-#: tracopt/ticket/templates/ticket_delete.html:48
+#: tracopt/ticket/templates/ticket_delete.html:22
+#: tracopt/ticket/templates/ticket_delete.html:58
 #, python-format
 msgid "Delete comment %(num)s on Ticket #%(id)s"
 msgstr "チケット #%(id)s コメント %(num)s の削除"
 
-#: tracopt/ticket/templates/ticket_delete.html:20
+#: tracopt/ticket/templates/ticket_delete.html:30
 #, python-format
 msgid "Delete [1:Ticket #%(id)s]"
 msgstr "[1:チケット #%(id)s] の削除"
 
-#: tracopt/ticket/templates/ticket_delete.html:32
+#: tracopt/ticket/templates/ticket_delete.html:42
 msgid "Are you sure you want to delete this ticket?"
 msgstr "このチケットを削除しますか?"
 
-#: tracopt/ticket/templates/ticket_delete.html:33
+#: tracopt/ticket/templates/ticket_delete.html:43
 #, python-format
 msgid ""
 "(comments: %(comments)s,\n"
 "                 attachments: %(attachments)s)"
 msgstr "(コメント: %(comments)s, 添付ファイル: %(attachments)s)"
 
-#: tracopt/ticket/templates/ticket_delete.html:36
-#: tracopt/ticket/templates/ticket_delete.html:61
-#: trac/templates/attachment.html:70 trac/wiki/templates/wiki_delete.html:95
+#: tracopt/ticket/templates/ticket_delete.html:46
+#: tracopt/ticket/templates/ticket_delete.html:71
+#: trac/templates/attachment.html:80 trac/wiki/templates/wiki_delete.html:105
 msgid "This is an irreversible operation."
 msgstr "これは取り消しの効かない操作です。"
 
-#: tracopt/ticket/templates/ticket_delete.html:41
-#: tracopt/ticket/templates/ticket_delete.html:65
-#: trac/admin/templates/admin_components.html:55
-#: trac/admin/templates/admin_enums.html:24
-#: trac/admin/templates/admin_milestones.html:74
-#: trac/admin/templates/admin_versions.html:50
-#: trac/templates/attachment.html:63 trac/templates/attachment.html:76
-#: trac/ticket/templates/milestone_delete.html:40
-#: trac/ticket/templates/milestone_edit.html:108
-#: trac/ticket/templates/report_delete.html:21
-#: trac/ticket/templates/report_edit.html:44
-#: trac/ticket/templates/ticket_change.html:118
-#: trac/versioncontrol/templates/admin_repositories.html:84
-#: trac/wiki/templates/wiki_delete.html:98
-#: trac/wiki/templates/wiki_edit_form.html:73
-#: trac/wiki/templates/wiki_rename.html:32
+#: tracopt/ticket/templates/ticket_delete.html:52
+#: tracopt/ticket/templates/ticket_delete.html:76
+#: trac/templates/attachment.html:73 trac/templates/attachment.html:87
+#: trac/ticket/templates/admin_components.html:64
+#: trac/ticket/templates/admin_enums.html:38
+#: trac/ticket/templates/admin_milestones.html:93
+#: trac/ticket/templates/admin_versions.html:61
+#: trac/ticket/templates/milestone_delete.html:45
+#: trac/ticket/templates/milestone_edit.html:128
+#: trac/ticket/templates/report_delete.html:32
+#: trac/ticket/templates/report_edit.html:62
+#: trac/ticket/templates/ticket_change.html:127
+#: trac/versioncontrol/templates/admin_repositories.html:98
+#: trac/wiki/templates/wiki_delete.html:112
+#: trac/wiki/templates/wiki_edit_form.html:84
+#: trac/wiki/templates/wiki_rename.html:43
 msgid "Cancel"
 msgstr "取り消し"
 
-#: tracopt/ticket/templates/ticket_delete.html:61
+#: tracopt/ticket/templates/ticket_delete.html:71
 msgid "Are you sure you want to delete this ticket comment?"
 msgstr "このチケットへのコメントを削除しますか?"
 
-#: tracopt/ticket/templates/ticket_delete.html:66
+#: tracopt/ticket/templates/ticket_delete.html:75
 msgid "Delete comment"
 msgstr "コメントを削除"
 
-#: tracopt/versioncontrol/svn/svn_fs.py:283
+#. TRANSLATOR: modified ('diff') (link)
+#: tracopt/versioncontrol/git/git_fs.py:423 trac/ticket/web_ui.py:1745
+#: trac/ticket/templates/ticket_change.html:164 trac/wiki/macros.py:361
+#: trac/wiki/web_ui.py:765
+msgid "diff"
+msgstr "差分"
+
+#: tracopt/versioncontrol/git/git_fs.py:424
+msgid "Diff against this parent (show the changes merged from the other parents)"
+msgstr "この親との差分 (別の親からマージした変更を表示)"
+
+#: tracopt/versioncontrol/git/git_fs.py:433
+msgid ""
+"Note: this is a <strong>merge</strong> changeset, the changes displayed "
+"below correspond to the merge itself."
+msgstr "※ これは<strong>マージ</strong>チェンジセットで、下に表示している変更内容はそのマージ自体に相当します。"
+
+#: tracopt/versioncontrol/git/git_fs.py:440
+msgid ""
+"Use the <tt>(diff)</tt> links above to see all the changes relative to "
+"each parent."
+msgstr "上にある<tt>(差分)</tt>リンクを使うと、個々の親との変更内容を見ることができます。"
+
+#: tracopt/versioncontrol/svn/svn_fs.py:306
 #, python-format
 msgid "Subversion >= 1.0 required, found %(version)s"
 msgstr "Subversion 1.0 以上が必要ですが、%(version)s を見つけました"
 
-#: tracopt/versioncontrol/svn/svn_fs.py:337
+#: tracopt/versioncontrol/svn/svn_fs.py:362
 #, python-format
 msgid "%(path)s does not appear to be a Subversion repository."
 msgstr "%(path)s は Subversion のリポジトリではないみたいです。"
 
-#: tracopt/versioncontrol/svn/svn_fs.py:344
+#: tracopt/versioncontrol/svn/svn_fs.py:369
 #, python-format
 msgid "Couldn't open Subversion repository %(path)s: %(svn_error)s"
 msgstr "Subversion のリポジトリ %(path)s が開けません: %(svn_error)s"
 
-#: tracopt/versioncontrol/svn/svn_fs.py:664
+#: tracopt/versioncontrol/svn/svn_fs.py:694
 #, python-format
 msgid ""
 "Diff mismatch: Base is a %(oldnode)s (%(oldpath)s in revision %(oldrev)s)"
@@ -191,7 +218,7 @@
 "不一致: 元の %(oldnode)s (%(oldpath)s リビジョン %(oldrev)s) と対象の %(newnode)s "
 "(%(newpath)s リビジョン %(newrev)s)。"
 
-#: tracopt/versioncontrol/svn/svn_fs.py:823
+#: tracopt/versioncontrol/svn/svn_fs.py:862
 #, python-format
 msgid "svn blame failed on %(path)s: %(error)s"
 msgstr "svn blame の実行に失敗しました %(path)s: %(error)s"
@@ -200,6 +227,10 @@
 msgid "No svn:externals configured in trac.ini"
 msgstr "trac.ini に svn:externals の設定がありません"
 
+#: tracopt/versioncontrol/svn/svn_prop.py:157
+msgid "needs lock"
+msgstr "ロックが必要になります"
+
 #: tracopt/versioncontrol/svn/svn_prop.py:187
 msgid "blocked"
 msgstr "blocked"
@@ -208,187 +239,193 @@
 msgid "merged"
 msgstr "merged"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:221
+#: tracopt/versioncontrol/svn/svn_prop.py:222
 msgid "non-inheritable"
 msgstr "継承不可"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:223
+#: tracopt/versioncontrol/svn/svn_prop.py:224
 msgid "merged on the directory itself but not below"
 msgstr "この配下ではなくこのディレクトリでマージ"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:239
+#: tracopt/versioncontrol/svn/svn_prop.py:240
+#: tracopt/versioncontrol/svn/svn_prop.py:262
 msgid "eligible"
 msgstr "eligible"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:253
+#: tracopt/versioncontrol/svn/svn_prop.py:270
 msgid "(toggle deleted branches)"
 msgstr "(削除したブランチに切り替える)"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:291
+#: tracopt/versioncontrol/svn/svn_prop.py:308
 msgid "View merge source"
 msgstr "マージ元を見る"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:302
+#: tracopt/versioncontrol/svn/svn_prop.py:319
 msgid "No revisions"
 msgstr "リビジョンがありません"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:309
+#: tracopt/versioncontrol/svn/svn_prop.py:326
 #, python-format
 msgid "%(title)s: %(revs)s"
 msgstr "%(title)s: %(revs)s"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:345
+#: tracopt/versioncontrol/svn/svn_prop.py:362
 msgid "merged: "
 msgstr "マージ: "
 
-#: tracopt/versioncontrol/svn/svn_prop.py:345
+#: tracopt/versioncontrol/svn/svn_prop.py:362
 msgid "blocked: "
 msgstr "ブロック: "
 
-#: tracopt/versioncontrol/svn/svn_prop.py:346
+#: tracopt/versioncontrol/svn/svn_prop.py:363
 msgid "reverse-merged: "
 msgstr "逆マージ: "
 
-#: tracopt/versioncontrol/svn/svn_prop.py:346
+#: tracopt/versioncontrol/svn/svn_prop.py:363
 msgid "un-blocked: "
 msgstr "反ブロック: "
 
-#: tracopt/versioncontrol/svn/svn_prop.py:347
+#: tracopt/versioncontrol/svn/svn_prop.py:364
 msgid "marked as non-inheritable: "
 msgstr "継承不可と表示: "
 
-#: tracopt/versioncontrol/svn/svn_prop.py:348
+#: tracopt/versioncontrol/svn/svn_prop.py:365
 msgid "unmarked as non-inheritable: "
 msgstr "継承不可の表示なし: "
 
-#: tracopt/versioncontrol/svn/svn_prop.py:360
+#: tracopt/versioncontrol/svn/svn_prop.py:409
 msgid " (added)"
 msgstr " (追加)"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:397
+#: tracopt/versioncontrol/svn/svn_prop.py:433
 msgid "removed"
 msgstr "削除"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:400
+#: tracopt/versioncontrol/svn/svn_prop.py:436
 msgid " (with no actual effect on merging)"
 msgstr " (マージには影響しません)"
 
-#: tracopt/versioncontrol/svn/svn_prop.py:401
+#: tracopt/versioncontrol/svn/svn_prop.py:437
 #, python-format
 msgid "Property %(prop)s changed"
 msgstr "%(prop)s プロパティ変更"
 
-#: trac/about.py:47 trac/templates/about.html:10 trac/templates/about.html:29
+#: trac/about.py:47 trac/templates/about.html:20 trac/templates/about.html:41
 msgid "About Trac"
 msgstr "Trac について"
 
-#: trac/attachment.py:165
+#: trac/attachment.py:166
 #, python-format
 msgid "Attachment '%(title)s' does not exist."
 msgstr "添付ファイル '%(title)s' は存在しません。"
 
-#: trac/attachment.py:167
+#: trac/attachment.py:168
 msgid "Invalid Attachment"
 msgstr "不正な添付ファイル"
 
-#: trac/attachment.py:234
+#: trac/attachment.py:235
 msgid "Could not delete attachment"
 msgstr "添付ファイルを削除できません"
 
-#: trac/attachment.py:253
+#: trac/attachment.py:254
 #, python-format
 msgid "Cannot reparent attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
 msgstr "%(realm)s:%(id)s が正しくないため添付ファイル \"%(att)s\" を移動できません"
 
-#: trac/attachment.py:258
+#: trac/attachment.py:259
 #, python-format
 msgid ""
 "Cannot reparent attachment \"%(att)s\" as it already exists in "
 "%(realm)s:%(id)s"
 msgstr "添付ファイル \"%(att)s\" がすでに %(realm)s:%(id)s に存在しているため、添付ファイルを移動できません。"
 
-#: trac/attachment.py:277
+#: trac/attachment.py:278
 #, python-format
 msgid "Could not reparent attachment %(name)s"
 msgstr "添付ファイル %(name)s を移動できませんでした"
 
-#: trac/attachment.py:313
-#, python-format
-msgid "Cannot create attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
-msgstr "%(realm)s:%(id)s が正しくないため添付ファイル \"%(att)s\" を作成できません"
-
-#: trac/attachment.py:396
-#, python-format
-msgid "Attachment '%(filename)s' not found"
-msgstr "添付ファイル '%(filename)s' が見つかりません"
-
-#: trac/attachment.py:480
-msgid "Bad request"
-msgstr "不正なリクエスト"
-
-#: trac/attachment.py:499
-#, python-format
-msgid "Back to %(parent)s"
-msgstr "%(parent)s に戻る"
-
-#: trac/attachment.py:605
-#, python-format
-msgid "%(attachment)s attached to %(resource)s"
-msgstr "%(attachment)s を %(resource)s に添付しました"
-
-#: trac/attachment.py:660
-#, python-format
-msgid "Unparented attachment %(id)s"
-msgstr "移動できなかった添付ファイル %(id)s"
-
-#: trac/attachment.py:668
-#, python-format
-msgid "Attachment '%(id)s' in %(parent)s"
-msgstr "%(parent)s の添付ファイル: '%(id)s'"
-
-#: trac/attachment.py:671
-#, python-format
-msgid "Attachments of %(parent)s"
-msgstr "%(parent)s の添付ファイル"
-
-#: trac/attachment.py:688
+#: trac/attachment.py:311
 #, python-format
 msgid "%(parent)s doesn't exist, can't create attachment"
 msgstr "%(parent)s がないので、添付ファイルを作成できません"
 
-#: trac/attachment.py:695 trac/attachment.py:722 trac/admin/web_ui.py:467
-#: trac/admin/web_ui.py:470 trac/admin/web_ui.py:474
+#: trac/attachment.py:320
+#, python-format
+msgid "Cannot create attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
+msgstr "%(realm)s:%(id)s が正しくないため添付ファイル \"%(att)s\" を作成できません"
+
+#: trac/attachment.py:404
+#, python-format
+msgid "Attachment '%(filename)s' not found"
+msgstr "添付ファイル '%(filename)s' が見つかりません"
+
+#: trac/attachment.py:487
+msgid "Bad request"
+msgstr "不正なリクエスト"
+
+#: trac/attachment.py:504
+#, python-format
+msgid "Parent resource %(parent)s doesn't exist"
+msgstr "親リソース %(parent)s は存在しません"
+
+#: trac/attachment.py:510
+#, python-format
+msgid "Back to %(parent)s"
+msgstr "%(parent)s に戻る"
+
+#: trac/attachment.py:616
+#, python-format
+msgid "%(attachment)s attached to %(resource)s"
+msgstr "%(attachment)s を %(resource)s に添付しました"
+
+#: trac/attachment.py:671
+#, python-format
+msgid "Unparented attachment %(id)s"
+msgstr "移動できなかった添付ファイル %(id)s"
+
+#: trac/attachment.py:679
+#, python-format
+msgid "Attachment '%(id)s' in %(parent)s"
+msgstr "%(parent)s の添付ファイル: '%(id)s'"
+
+#: trac/attachment.py:682
+#, python-format
+msgid "Attachments of %(parent)s"
+msgstr "%(parent)s の添付ファイル"
+
+#: trac/attachment.py:702 trac/attachment.py:729 trac/admin/web_ui.py:471
+#: trac/admin/web_ui.py:474 trac/admin/web_ui.py:478
 msgid "No file uploaded"
 msgstr "アップロードしたファイルはありません"
 
-#: trac/attachment.py:703
+#: trac/attachment.py:710
 msgid "Can't upload empty file"
 msgstr "空のファイルはアップロードできません"
 
-#: trac/attachment.py:708
+#: trac/attachment.py:715
 #, python-format
 msgid "Maximum attachment size: %(num)s bytes"
 msgstr "添付ファイルの上限サイズ: %(num)s バイト"
 
-#: trac/attachment.py:709
+#: trac/attachment.py:716
 msgid "Upload failed"
 msgstr "アップロード失敗"
 
-#: trac/attachment.py:737
+#: trac/attachment.py:744
 #, python-format
 msgid "Attachment field %(field)s is invalid: %(message)s"
 msgstr "添付ファイル情報のフィールド %(field)s が不正です: %(message)s"
 
-#: trac/attachment.py:741
+#: trac/attachment.py:748
 #, python-format
 msgid "Invalid attachment: %(message)s"
 msgstr "添付ファイルが不正です: %(message)s"
 
-#: trac/attachment.py:745
+#: trac/attachment.py:752
 msgid "Note: File must be selected again."
 msgstr "※ もう一度ファイルを選択してください。"
 
-#: trac/attachment.py:758
+#: trac/attachment.py:765
 #, python-format
 msgid ""
 "You don't have permission to replace the attachment %(name)s. You can "
@@ -399,103 +436,135 @@
 "を置き換える権限がありません。置き換えることができるのは自分で添付したファイルのみです。他の人が添付したファイルを置き換えるには "
 "ATTACHMENT_DELETE 権限が必要です。"
 
-#: trac/attachment.py:789
+#: trac/attachment.py:796
 #, python-format
 msgid "%(attachment)s (delete)"
 msgstr "%(attachment)s (削除)"
 
-#: trac/attachment.py:803
+#: trac/attachment.py:810
 #, python-format
 msgid "Maximum total attachment size: %(num)s bytes"
 msgstr "添付ファイルの合計の上限サイズ: %(num)s バイト"
 
-#: trac/attachment.py:804
+#: trac/attachment.py:811
 msgid "Download failed"
 msgstr "ダウンロード失敗"
 
-#: trac/attachment.py:892 trac/versioncontrol/web_ui/browser.py:669
+#: trac/attachment.py:894 trac/versioncontrol/web_ui/browser.py:710
 #: trac/wiki/web_ui.py:73
 msgid "Plain Text"
 msgstr "プレーンテキスト"
 
-#: trac/attachment.py:898 trac/versioncontrol/web_ui/browser.py:675
+#: trac/attachment.py:900 trac/versioncontrol/web_ui/browser.py:716
 msgid "Original Format"
 msgstr "オリジナルフォーマット"
 
-#: trac/attachment.py:940 trac/templates/list_of_attachments.html:20
-#: trac/ticket/templates/ticket_change.html:83
-#: trac/versioncontrol/templates/dir_entries.html:18
-#: trac/versioncontrol/web_ui/browser.py:822
+#: trac/attachment.py:942 trac/templates/list_of_attachments.html:29
+#: trac/ticket/templates/ticket_change.html:92
+#: trac/versioncontrol/templates/dir_entries.html:29
+#: trac/versioncontrol/web_ui/browser.py:865
 msgid "Download"
 msgstr "ダウンロード"
 
-#: trac/attachment.py:1034
+#: trac/attachment.py:1036
 #, python-format
 msgid "Invalid resource identifier '%(id)s'"
 msgstr "'%(id)s' は正しい識別子ではありません"
 
-#: trac/attachment.py:1070 trac/admin/templates/admin_components.html:80
-#: trac/admin/templates/admin_enums.html:48
-#: trac/admin/templates/admin_milestones.html:107
-#: trac/admin/templates/admin_versions.html:83 trac/templates/about.html:69
-#: trac/templates/about.html:90 trac/templates/error.html:160
-#: trac/ticket/admin.py:210 trac/ticket/admin.py:399 trac/ticket/admin.py:559
+#: trac/attachment.py:1072 trac/templates/about.html:81
+#: trac/templates/about.html:102 trac/templates/error.html:179
+#: trac/ticket/admin.py:210 trac/ticket/admin.py:404 trac/ticket/admin.py:574
+#: trac/ticket/templates/admin_components.html:88
+#: trac/ticket/templates/admin_enums.html:61
+#: trac/ticket/templates/admin_milestones.html:124
+#: trac/ticket/templates/admin_versions.html:93
 #: trac/versioncontrol/admin.py:113
-#: trac/versioncontrol/templates/admin_repositories.html:125
-#: trac/web/session.py:417
+#: trac/versioncontrol/templates/admin_repositories.html:138
+#: trac/web/session.py:423
 msgid "Name"
 msgstr "名称"
 
-#: trac/attachment.py:1070
+#: trac/attachment.py:1072
 msgid "Size"
 msgstr "サイズ"
 
-#: trac/attachment.py:1070 trac/templates/history_view.html:30
-#: trac/ticket/templates/ticket.html:350
-#: trac/versioncontrol/templates/revisionlog.html:112
+#: trac/attachment.py:1072 trac/templates/history_view.html:40
+#: trac/ticket/templates/ticket.html:353
+#: trac/versioncontrol/templates/revisionlog.html:122
 msgid "Author"
 msgstr "更新者"
 
-#: trac/attachment.py:1070 trac/templates/history_view.html:29
+#: trac/attachment.py:1072 trac/templates/history_view.html:39
 msgid "Date"
 msgstr "更新日時"
 
-#: trac/attachment.py:1071 trac/templates/attachment.html:93
-#: trac/ticket/api.py:299 trac/ticket/templates/ticket.html:379
-#: trac/ticket/templates/ticket_box.html:92
+#: trac/attachment.py:1073 trac/templates/attachment.html:102
+#: trac/ticket/api.py:308 trac/ticket/api.py:530
+#: trac/ticket/templates/ticket.html:382
+#: trac/ticket/templates/ticket_box.html:101
 msgid "Description"
 msgstr "詳細"
 
-#: trac/attachment.py:1094 trac/wiki/admin.py:108
+#: trac/attachment.py:1096 trac/wiki/admin.py:108
 #, python-format
 msgid "File '%(name)s' exists"
 msgstr "ファイル '%(name)s' が存在しています。"
 
-#: trac/config.py:44
+#: trac/config.py:45
 msgid "Configuration Error"
 msgstr "設定エラー"
 
-#: trac/config.py:265
+#: trac/config.py:49
+msgid "Look in the Trac log for more information."
+msgstr "詳細は Trac のログを参照してください。"
+
+#: trac/config.py:274
 #, python-format
 msgid "Error reading '%(file)s', make sure it is readable."
 msgstr "'%(file)s' の読み込みエラー。読み込み可能か確認してください。"
 
-#: trac/config.py:420
+#: trac/config.py:431
 #, python-format
 msgid "[%(section)s] %(entry)s: expected integer, got %(value)s"
 msgstr "[%(section)s] %(entry)s: %(value)s は整数を入力してください。"
 
-#: trac/config.py:438
+#: trac/config.py:449
 #, python-format
 msgid "[%(section)s] %(entry)s: expected float, got %(value)s"
 msgstr "[%(section)s] %(entry)s: %(value)s は浮動小数点を入力してください。"
 
-#: trac/config.py:666
+#: trac/config.py:622
+msgid "Setting attribute is not allowed."
+msgstr "属性を設定することはできません。"
+
+#: trac/config.py:702
 #, python-format
 msgid "[%(section)s] %(entry)s: expected one of (%(choices)s), got %(value)s"
 msgstr "[%(section)s] %(entry)s: %(value)s は %(choices)s のどれかを入力してください。"
 
-#: trac/config.py:761 trac/config.py:774
+#: trac/config.py:741
+#, python-format
+msgid ""
+"Cannot find an implementation of the %(interface)s interface named "
+"%(implementation)s. Please check that the Component is enabled or update "
+"the option %(option)s in trac.ini."
+msgstr ""
+"%(implementation)s という名前の %(interface)s "
+"インターフェイスの実装を見つけられません。コンポーネントが有効になっているかチェックするか、trac.ini の %(option)s "
+"オプションを更新してください。"
+
+#: trac/config.py:779
+#, python-format
+msgid ""
+"Cannot find implementation(s) of the %(interface)s interface named "
+"%(implementation)s. Please check that the Component is enabled or update "
+"the option %(option)s in trac.ini."
+msgstr ""
+"%(implementation)s という名前の %(interface)s "
+"インターフェイスの実装を見つけられません。コンポーネントが有効になっているかチェックするか、trac.ini の %(option)s "
+"オプションを更新してください。"
+
+#: trac/config.py:819 trac/config.py:832
 #, python-format
 msgid "Option '%(option)s' doesn't exist in section '%(section)s'"
 msgstr "オプション '%(option)s' はセクション '%(section)s' に存在しません"
@@ -504,7 +573,7 @@
 msgid "Trac Error"
 msgstr "Trac エラー"
 
-#: trac/env.py:218
+#: trac/env.py:219
 msgid ""
 "Visit the Trac open source project at<br /><a "
 "href=\"http://trac.edgewall.org/\">http://trac.edgewall.org/</a>"
@@ -512,22 +581,22 @@
 "Trac オープンソースプロジェクトのページへ<br /><a "
 "href=\"http://trac.edgewall.org/\">http://trac.edgewall.org/</a>"
 
-#: trac/env.py:761
+#: trac/env.py:791
 msgid "Database newer than Trac version"
 msgstr "Trac のバージョンよりもデータベースのバージョンの方が新しいです。"
 
-#: trac/env.py:778
+#: trac/env.py:808
 #, python-format
 msgid "No upgrade module for version %(num)i (%(version)s.py)"
 msgstr "バージョン %(num)i (%(version)s.py) に対応するアップグレードモジュールはありません"
 
-#: trac/env.py:825
+#: trac/env.py:854
 msgid ""
 "Missing environment variable \"TRAC_ENV\". Trac requires this variable to"
 " point to a valid Trac environment."
 msgstr "環境変数 \"TRAC_ENV\" が見つかりません。Trac ではこの変数を設定する必要があります。"
 
-#: trac/env.py:854 trac/admin/console.py:281
+#: trac/env.py:883 trac/admin/console.py:283
 #, python-format
 msgid ""
 "The Trac Environment needs to be upgraded.\n"
@@ -538,50 +607,50 @@
 "\n"
 "\"trac-admin %(path)s upgrade\" を実行してください"
 
-#: trac/env.py:893
+#: trac/env.py:922
 msgid "Copying resources from:"
 msgstr "次の場所からリソースをコピー中です:"
 
-#: trac/env.py:911
+#: trac/env.py:940
 msgid "Creating scripts."
 msgstr "スクリプトを作成しています。"
 
-#: trac/env.py:923
+#: trac/env.py:952
 #, python-format
 msgid "Invalid argument '%(arg)s'"
 msgstr "'%(arg)s' は不正な引数です"
 
-#: trac/env.py:928
+#: trac/env.py:957
 #, python-format
 msgid "hotcopy can't overwrite existing '%(dest)s'"
 msgstr "'%(dest)s' が存在するため上書きできません"
 
-#: trac/env.py:937
+#: trac/env.py:966
 #, python-format
 msgid "Hotcopying %(src)s to %(dst)s ..."
 msgstr "%(src)s から %(dst)s へホットコピーを実施しています..."
 
-#: trac/env.py:954
+#: trac/env.py:983
 msgid "The following errors happened while copying the environment:"
 msgstr "コピー中に以下のエラーが発生しました:"
 
-#: trac/env.py:965
+#: trac/env.py:994
 msgid "Backing up database ..."
 msgstr "データベースをバックアップしています... "
 
-#: trac/env.py:970
+#: trac/env.py:999
 msgid "Hotcopy done."
 msgstr "ホットコピーが終了しました。"
 
-#: trac/env.py:975 trac/admin/api.py:131
+#: trac/env.py:1004 trac/admin/api.py:134
 msgid "Invalid arguments"
 msgstr "不正な引数です"
 
-#: trac/env.py:978
+#: trac/env.py:1007
 msgid "Database is up to date, no upgrade necessary."
 msgstr "データベースは最新であり更新の必要はありません。"
 
-#: trac/env.py:984
+#: trac/env.py:1013
 msgid ""
 "The pre-upgrade backup failed.\n"
 "Use '--no-backup' to upgrade without doing a backup.\n"
@@ -589,11 +658,11 @@
 "アップグレード前のバックアップが失敗しました。\n"
 "バックアップしないでアップグレードするには '--no-backup' を使ってください。\n"
 
-#: trac/env.py:988
+#: trac/env.py:1017
 msgid "The upgrade failed. Please fix the issue and try again.\n"
 msgstr "アップグレードが失敗しました。問題を解消させてもう一度試してください。\n"
 
-#: trac/env.py:1000
+#: trac/env.py:1029
 msgid ""
 "Warning: the wiki-macros directory in the environment is non-empty, but "
 "Trac\n"
@@ -602,7 +671,7 @@
 "注意: wiki-macros ディレクトリが空ではありません。もう Trac はそこからプラグ\n"
 "インをロードしません。手動でこのディレクトリを削除してください。"
 
-#: trac/env.py:1011
+#: trac/env.py:1040
 #, python-format
 msgid ""
 "Error while removing wiki-macros: %(err)s\n"
@@ -613,7 +682,7 @@
 "もう Trac はそこからプラグインをロードしません。手動でこのディレクトリを削除\n"
 "してください。"
 
-#: trac/env.py:1016
+#: trac/env.py:1045
 #, python-format
 msgid ""
 "Upgrade done.\n"
@@ -628,106 +697,133 @@
 "\n"
 "  trac-admin %(path)s wiki upgrade"
 
-#: trac/notification.py:159
+#: trac/notification.py:165
+#, python-format
+msgid ""
+"SMTP server connection error (%(error)s). Please modify %(option1)s or "
+"%(option2)s in your configuration."
+msgstr "SMTP サーバの接続エラーです (%(error)s)。設定にある %(option1)s または %(option2)s を修正してください。"
+
+#: trac/notification.py:170
 msgid "TLS enabled but server does not support TLS"
 msgstr "TLS が有効になっていますが、サーバは TLS をサポートしていません"
 
-#: trac/notification.py:312
+#: trac/notification.py:223
+#, python-format
+msgid ""
+"Sendmail error (%(error)s). Please modify %(option)s in your "
+"configuration."
+msgstr "sendmail のエラーです (%(error)s)。設定にある %(option)s を修正してください。"
+
+#: trac/notification.py:330
 #, python-format
 msgid "Invalid email encoding setting: %(pref)s"
 msgstr "メールのエンコーディング設定が不正です: %(pref)s"
 
-#: trac/notification.py:337
+#: trac/notification.py:355
 msgid "Unable to send email due to identity crisis."
 msgstr "送信元を確認できないためメールを送信できません。"
 
-#: trac/notification.py:341
+#: trac/notification.py:362
 #, python-format
 msgid "Neither %(from_)s nor %(reply_to)s are specified in the configuration."
 msgstr "%(from_)s または %(reply_to)s を設定してください。"
 
-#: trac/notification.py:342
+#: trac/notification.py:363
 msgid "SMTP Notification Error"
 msgstr "SMTP 通知処理エラー"
 
-#: trac/notification.py:351
+#: trac/notification.py:374
 msgid "Header length is too short"
 msgstr "ヘッダー長が短すぎます"
 
-#: trac/perm.py:56
+#: trac/perm.py:42
+msgid "Forbidden"
+msgstr "禁止"
+
+#: trac/perm.py:54
 #, python-format
 msgid ""
 "%(perm)s privileges are required to perform this operation on "
 "%(resource)s. You don't have the required permissions."
 msgstr "この操作を %(resource)s に行うには %(perm)s 権限が必要ですがその権限がありません。"
 
-#: trac/perm.py:58
+#: trac/perm.py:56
 #, python-format
 msgid ""
 "%(perm)s privileges are required to perform this operation. You don't "
 "have the required permissions."
 msgstr "この操作を行うには %(perm)s 権限が必要ですがその権限がありません。"
 
-#: trac/perm.py:64
+#: trac/perm.py:60
 msgid "Insufficient privileges to perform this operation."
 msgstr "この操作を行う権限がありません。"
 
-#: trac/perm.py:343
+#: trac/perm.py:341
 #, python-format
 msgid "%(name)s is not a valid action."
 msgstr "%(name)s は有効なアクションではありません。"
 
-#: trac/perm.py:656
+#: trac/perm.py:658
 msgid "User"
 msgstr "ユーザ"
 
-#: trac/perm.py:656 trac/admin/templates/admin_perms.html:63
-#: trac/ticket/templates/batch_modify.html:37
-#: trac/ticket/templates/ticket.html:321
+#: trac/perm.py:658 trac/admin/templates/admin_perms.html:74
+#: trac/ticket/templates/batch_modify.html:47
+#: trac/ticket/templates/ticket.html:324
 msgid "Action"
 msgstr "アクション"
 
-#: trac/perm.py:658
+#: trac/perm.py:660
 msgid "Available actions:"
 msgstr "利用できるアクション:"
 
-#: trac/perm.py:669 trac/admin/web_ui.py:370
+#: trac/perm.py:671 trac/admin/web_ui.py:370
 msgid "All upper-cased tokens are reserved for permission names"
 msgstr "すべて大文字の名称は権限名に予約されているため使えません"
 
-#: trac/perm.py:675
+#: trac/perm.py:677
 #, python-format
 msgid "The user %(user)s already has permission %(action)s."
 msgstr "ユーザ %(user)s は %(action)s 権限をすでに持っています。"
 
-#: trac/perm.py:689
+#: trac/perm.py:692
 #, python-format
-msgid "Cannot remove permission %(action)s for user %(user)s."
-msgstr "ユーザ %(user)s から %(action)s 権限を削除できません。"
+msgid ""
+"Cannot remove permission %(action)s for user %(user)s. The permission is "
+"granted through a meta-permission or group."
+msgstr "ユーザ %(user)s から %(action)s 権限を削除できません。その権限は、メタ権限またはグループから許可しています。"
 
-#: trac/perm.py:706
+#: trac/perm.py:697
+#, python-format
+msgid ""
+"Cannot remove permission %(action)s for user %(user)s. The user has not "
+"been granted the permission."
+msgstr "ユーザ %(user)s から %(action)s 権限を削除できません。ユーザはその権限を持っていません。"
+
+#: trac/perm.py:716
 #, python-format
 msgid "Cannot export to %(filename)s: %(error)s"
 msgstr "%(filename)s にエクスポートできません: %(error)s"
 
-#: trac/perm.py:719
+#: trac/perm.py:729
 #, python-format
 msgid "Invalid row %(line)d. Expected <user>, <action>, [action], [...]"
 msgstr "%(line)d行目が正しくありません。<ユーザ>, <アクション>, [アクション], [...] を期待しています。"
 
-#: trac/perm.py:727
+#: trac/perm.py:737
 #, python-format
 msgid ""
 "Invalid user %(user)s on line %(line)d: All upper-cased tokens are "
 "reserved for permission names."
 msgstr "%(line)d行目: ユーザ名 %(user)s が正しくありません。すべて大文字の名称は権限名に予約されているため使えません。"
 
-#: trac/perm.py:736
+#: trac/perm.py:746
 #, python-format
 msgid "Cannot import from %(filename)s line %(line)d: %(error)s "
 msgstr "%(filename)s %(line)d行目がインポートできません: %(error)s "
 
-#: trac/perm.py:741
+#: trac/perm.py:751
 #, python-format
 msgid "Cannot import from %(filename)s: %(error)s"
 msgstr "%(filename)s がインポートできません: %(error)s "
@@ -737,16 +833,16 @@
 msgid "%(name)s at version %(version)s"
 msgstr "%(name)s (バージョン %(version)s)"
 
-#: trac/admin/api.py:135
+#: trac/admin/api.py:138
 msgid "Command not found"
 msgstr "コマンドが見つかりません"
 
-#: trac/admin/console.py:113
+#: trac/admin/console.py:114
 #, python-format
 msgid "Error: %(msg)s"
 msgstr "エラー: %(msg)s"
 
-#: trac/admin/console.py:132
+#: trac/admin/console.py:133
 #, python-format
 msgid ""
 "Welcome to trac-admin %(version)s\n"
@@ -763,47 +859,47 @@
 "'?' コマンドか 'help' コマンドでヘルプを表示します。\n"
 "        "
 
-#: trac/admin/console.py:166
+#: trac/admin/console.py:168
 #, python-format
 msgid "Failed to open environment: %(err)s"
 msgstr "TracEnv を開けませんでした: %(err)s"
 
-#: trac/admin/console.py:249
+#: trac/admin/console.py:251
 #, python-format
 msgid "Completion error: %(err)s"
 msgstr "補完エラー: %(err)s"
 
-#: trac/admin/console.py:316
+#: trac/admin/console.py:318
 #, python-format
 msgid ""
 "No documentation found for '%(cmd)s'. Use 'help' to see the list of "
 "commands."
 msgstr "'%(cmd)s' のドキュメントは見つかりません。コマンド一覧を見るには 'help' を使います。"
 
-#: trac/admin/console.py:322
+#: trac/admin/console.py:326
 msgid "Did you mean this?"
 msgid_plural "Did you mean one of these?"
 msgstr[0] "これではないですか?"
 
-#: trac/admin/console.py:326
+#: trac/admin/console.py:330
 #, python-format
 msgid "trac-admin - The Trac Administration Console %(version)s"
 msgstr "trac-admin - Trac 管理コンソール %(version)s"
 
-#: trac/admin/console.py:330
+#: trac/admin/console.py:334
 msgid "Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]\n"
 msgstr "Usage: trac-admin </path/to/projenv> [コマンド [サブコマンド] [オプション ...]]\n"
 
-#: trac/admin/console.py:333
+#: trac/admin/console.py:337
 msgid "Invoking trac-admin without command starts interactive mode.\n"
 msgstr "コマンドを省略して trac-admin を実行すると、対話モードで起動します。\n"
 
-#: trac/admin/console.py:373
+#: trac/admin/console.py:377
 #, python-format
 msgid "Creating a new Trac environment at %(envname)s"
 msgstr "新規 Trac Environment %(envname)s の生成"
 
-#: trac/admin/console.py:375
+#: trac/admin/console.py:379
 msgid ""
 "\n"
 "Trac will first ask a few questions about your environment\n"
@@ -819,12 +915,12 @@
 " プロジェクトの名前を入力してください。\n"
 " この名前は、ページのタイトルと説明に使用します。\n"
 
-#: trac/admin/console.py:383
+#: trac/admin/console.py:387
 #, python-format
 msgid "Project Name [%(default)s]> "
 msgstr "プロジェクト名 [%(default)s]> "
 
-#: trac/admin/console.py:385
+#: trac/admin/console.py:389
 msgid ""
 "\n"
 " Please specify the connection string for the database to use.\n"
@@ -840,48 +936,48 @@
 " (Trac では、接続文字列は厳密に表記する必要があります。\n"
 " 詳細は Trac のドキュメントを参照してください)\n"
 
-#: trac/admin/console.py:393
+#: trac/admin/console.py:397
 #, python-format
 msgid "Database connection string [%(default)s]> "
 msgstr "データベース接続文字列 [%(default)s]> "
 
-#: trac/admin/console.py:400
+#: trac/admin/console.py:404
 #, python-format
 msgid "Initenv for '%(env)s' failed."
 msgstr "initenv 失敗: '%(env)s'"
 
-#: trac/admin/console.py:403
+#: trac/admin/console.py:407
 msgid "Does an environment already exist?"
 msgstr "tracenv がすでに存在していませんか?"
 
-#: trac/admin/console.py:407
+#: trac/admin/console.py:411
 msgid "Directory exists and is not empty."
 msgstr "ディレクトリが空ではありません。"
 
-#: trac/admin/console.py:413
+#: trac/admin/console.py:417
 #, python-format
 msgid ""
 "Base directory '%(env)s' does not exist. Please create it manually and "
 "retry."
 msgstr "ベースディレクトリ '%(env)s' がありません。手動で作成してからやり直してください。"
 
-#: trac/admin/console.py:441
+#: trac/admin/console.py:445
 msgid "Creating and Initializing Project"
 msgstr "プロジェクトの生成と初期化"
 
-#: trac/admin/console.py:458
+#: trac/admin/console.py:462
 msgid "Failed to create environment."
 msgstr "tracenv が作成できません。"
 
-#: trac/admin/console.py:464
+#: trac/admin/console.py:468
 msgid " Installing default wiki pages"
 msgstr " デフォルトの Wiki ページのインストール"
 
-#: trac/admin/console.py:473
+#: trac/admin/console.py:477
 msgid " Indexing default repository"
 msgstr " リポジトリのインデックス作成"
 
-#: trac/admin/console.py:476
+#: trac/admin/console.py:480
 msgid ""
 "\n"
 "---------------------------------------------------------------------\n"
@@ -908,7 +1004,7 @@
 "るには trac.ini ファイルの [trac] repository_type と repository_path\n"
 "の設定を再度確認する必要があります。\n"
 
-#: trac/admin/console.py:519
+#: trac/admin/console.py:523
 #, python-format
 msgid ""
 "\n"
@@ -960,7 +1056,7 @@
 "\n"
 "Congratulations!\n"
 
-#: trac/admin/console.py:528
+#: trac/admin/console.py:532
 msgid ""
 "Display help for trac-admin commands.\n"
 "\n"
@@ -982,108 +1078,108 @@
 "[[TracAdminHelp(upgrade)]]      # upgrade コマンド\n"
 "}}}"
 
-#: trac/admin/console.py:580
+#: trac/admin/console.py:578
 #, python-format
 msgid "Non-ascii environment path '%(path)s' not supported."
 msgstr "非 ASCII 文字のパス '%(path)s' はサポートしていません。"
 
-#: trac/admin/web_ui.py:74
+#: trac/admin/web_ui.py:69
 msgid "Admin"
 msgstr "管理"
 
-#: trac/admin/web_ui.py:75 trac/admin/templates/admin.html:16
+#: trac/admin/web_ui.py:70 trac/admin/templates/admin.html:26
 msgid "Administration"
 msgstr "管理コンソール"
 
-#: trac/admin/web_ui.py:91
+#: trac/admin/web_ui.py:86
 msgid "No administration panels available"
 msgstr "利用可能な管理画面はありません"
 
-#: trac/admin/web_ui.py:117 trac/admin/web_ui.py:121
+#: trac/admin/web_ui.py:112 trac/admin/web_ui.py:116
 msgid "Unknown administration panel"
 msgstr "不明な管理画面"
 
-#: trac/admin/web_ui.py:133
+#: trac/admin/web_ui.py:128
 msgid "Untitled"
 msgstr "タイトルなし"
 
-#: trac/admin/web_ui.py:192 trac/ticket/admin.py:66 trac/ticket/admin.py:95
-#: trac/ticket/admin.py:275 trac/ticket/admin.py:455 trac/ticket/admin.py:607
-#: trac/ticket/admin.py:690 trac/ticket/report.py:248
-#: trac/ticket/roadmap.py:779 trac/versioncontrol/admin.py:215
+#: trac/admin/web_ui.py:187 trac/ticket/admin.py:66 trac/ticket/admin.py:95
+#: trac/ticket/admin.py:273 trac/ticket/admin.py:464 trac/ticket/admin.py:626
+#: trac/ticket/admin.py:709 trac/ticket/report.py:253
+#: trac/ticket/roadmap.py:824 trac/versioncontrol/admin.py:214
 msgid "Your changes have been saved."
 msgstr "変更を保存しました。"
 
-#: trac/admin/web_ui.py:197 trac/ticket/admin.py:69
+#: trac/admin/web_ui.py:192 trac/ticket/admin.py:69
 msgid ""
 "Error writing to trac.ini, make sure it is writable by the web server. "
 "Your changes have not been saved."
 msgstr "trac.ini の書き込みエラー。ウェブサーバによる書き込みができるか確認してください。変更は保存できていません。"
 
-#: trac/admin/web_ui.py:210 trac/admin/web_ui.py:268 trac/admin/web_ui.py:356
-#: trac/admin/web_ui.py:443 trac/prefs/web_ui.py:94
-#: trac/prefs/templates/prefs_general.html:9
+#: trac/admin/web_ui.py:205 trac/admin/web_ui.py:265 trac/admin/web_ui.py:356
+#: trac/admin/web_ui.py:449 trac/prefs/web_ui.py:90
+#: trac/prefs/templates/prefs_general.html:19
 msgid "General"
 msgstr "一般設定"
 
-#: trac/admin/web_ui.py:210 trac/admin/templates/admin_basics.html:13
+#: trac/admin/web_ui.py:205 trac/admin/templates/admin_basics.html:23
 msgid "Basic Settings"
 msgstr "基本設定"
 
-#: trac/admin/web_ui.py:268 trac/admin/templates/admin_logging.html:10
-#: trac/admin/templates/admin_logging.html:22
+#: trac/admin/web_ui.py:265 trac/admin/templates/admin_logging.html:20
+#: trac/admin/templates/admin_logging.html:32
 msgid "Logging"
 msgstr "ログ"
 
-#: trac/admin/web_ui.py:277 trac/ticket/templates/milestone_delete.html:31
-#: trac/ticket/templates/milestone_edit.html:87
+#: trac/admin/web_ui.py:274 trac/ticket/templates/milestone_delete.html:35
+#: trac/ticket/templates/milestone_edit.html:99
 msgid "None"
 msgstr "(割り当てない)"
 
-#: trac/admin/web_ui.py:278
+#: trac/admin/web_ui.py:276
 msgid "Console"
 msgstr "コンソール"
 
-#: trac/admin/web_ui.py:280 trac/templates/attachment.html:32
+#: trac/admin/web_ui.py:278 trac/templates/attachment.html:42
 msgid "File"
 msgstr "ファイル"
 
-#: trac/admin/web_ui.py:282
+#: trac/admin/web_ui.py:280
 msgid "Syslog"
 msgstr "Syslog"
 
-#: trac/admin/web_ui.py:284
+#: trac/admin/web_ui.py:283
 msgid "Windows event log"
 msgstr "Windows イベントログ"
 
-#: trac/admin/web_ui.py:297
+#: trac/admin/web_ui.py:296
 #, python-format
 msgid "Unknown log type %(type)s"
 msgstr "不明なログ種別: %(type)s"
 
-#: trac/admin/web_ui.py:298
+#: trac/admin/web_ui.py:297
 msgid "Invalid log type"
 msgstr "不正なログ種別"
 
-#: trac/admin/web_ui.py:312
+#: trac/admin/web_ui.py:311
 #, python-format
 msgid "Unknown log level %(level)s"
 msgstr "不明なログレベル: %(level)s"
 
-#: trac/admin/web_ui.py:313
+#: trac/admin/web_ui.py:312
 msgid "Invalid log level"
 msgstr "不正なログレベル"
 
-#: trac/admin/web_ui.py:326
+#: trac/admin/web_ui.py:325
 msgid "You must specify a log file"
 msgstr "ログファイルの指定が必要です"
 
-#: trac/admin/web_ui.py:327
+#: trac/admin/web_ui.py:326
 msgid "Missing field"
 msgstr "フィールドがありません"
 
-#: trac/admin/web_ui.py:356 trac/admin/templates/admin_perms.html:10
-#: trac/admin/templates/admin_perms.html:60
+#: trac/admin/web_ui.py:356 trac/admin/templates/admin_perms.html:20
+#: trac/admin/templates/admin_perms.html:71
 msgid "Permissions"
 msgstr "権限"
 
@@ -1094,391 +1190,266 @@
 #: trac/admin/web_ui.py:381
 #, python-format
 msgid "The subject %(subject)s has been granted the permission %(action)s."
-msgstr "ユーザ \"%(subject)s\" にアクション \"%(action)s\" を追加しました。"
+msgstr "ユーザ \"%(subject)s\" にアクション \"%(action)s\" を許可しました。"
 
 #: trac/admin/web_ui.py:386
 #, python-format
 msgid "The permission %(action)s was already granted to %(subject)s."
 msgstr "ユーザ \"%(subject)s\" にはすでにアクション \"%(action)s\" を許可しています。"
 
-#: trac/admin/web_ui.py:402
+#: trac/admin/web_ui.py:400
+#, python-format
+msgid ""
+"The subject %(subject)s was not added to the group %(group)s because the "
+"group has %(perm)s permission and users cannot grant permissions they "
+"don't possess."
+msgstr ""
+"グループは %(perm)s 権限を持っているため %(subject)s をグループ %(group)s "
+"に追加しませんでした。ユーザは、保有していない権限を許可することはできません。"
+
+#: trac/admin/web_ui.py:408
 #, python-format
 msgid "The subject %(subject)s has been added to the group %(group)s."
 msgstr "ユーザ \"%(subject)s\" をグループ \"%(group)s\" に追加しました。"
 
-#: trac/admin/web_ui.py:407
+#: trac/admin/web_ui.py:413
 #, python-format
 msgid "The subject %(subject)s was already added to the group %(group)s."
 msgstr "ユーザ \"%(subject)s\" はすでにグループ \"%(group)s\" に追加しています。"
 
-#: trac/admin/web_ui.py:422
+#: trac/admin/web_ui.py:428
 msgid "The selected permissions have been revoked."
 msgstr "選択した権限を無効にしました。"
 
-#: trac/admin/web_ui.py:443 trac/admin/templates/admin_plugins.html:10
+#: trac/admin/web_ui.py:449 trac/admin/templates/admin_plugins.html:20
 msgid "Plugins"
 msgstr "プラグイン"
 
-#: trac/admin/web_ui.py:477
+#: trac/admin/web_ui.py:481
 msgid "Uploaded file is not a Python source file or egg"
 msgstr "アップロードしたファイルは Python ソースファイルでも egg ファイルでもありません"
 
-#: trac/admin/web_ui.py:482
+#: trac/admin/web_ui.py:486
 #, python-format
 msgid "Plugin %(name)s already installed"
 msgstr "プラグイン %(name)s は既にインストール済みです"
 
-#: trac/admin/web_ui.py:551
+#: trac/admin/web_ui.py:555
 msgid "The following component has been disabled:"
 msgid_plural "The following components have been disabled:"
 msgstr[0] "次のコンポーネントを削除しました:"
 
-#: trac/admin/web_ui.py:556
+#: trac/admin/web_ui.py:560
 msgid "The following component has been enabled:"
 msgid_plural "The following components have been enabled:"
 msgstr[0] "次のコンポーネントを追加しました:"
 
-#: trac/admin/templates/admin.html:10
+#: trac/admin/templates/admin.html:20
 msgid "Administration:"
 msgstr "管理:"
 
-#: trac/admin/templates/admin_basics.html:9
+#: trac/admin/templates/admin_basics.html:19
 msgid "Basics"
 msgstr "基本設定"
 
-#: trac/admin/templates/admin_basics.html:17
+#: trac/admin/templates/admin_basics.html:27
 msgid "Project"
 msgstr "プロジェクト"
 
-#: trac/admin/templates/admin_basics.html:19
-#: trac/admin/templates/admin_components.html:37
-#: trac/admin/templates/admin_components.html:66
-#: trac/admin/templates/admin_enums.html:21
-#: trac/admin/templates/admin_enums.html:35
-#: trac/admin/templates/admin_milestones.html:28
-#: trac/admin/templates/admin_milestones.html:85
-#: trac/admin/templates/admin_versions.html:26
-#: trac/admin/templates/admin_versions.html:61
-#: trac/versioncontrol/templates/admin_repositories.html:50
-#: trac/versioncontrol/templates/admin_repositories.html:95
-#: trac/versioncontrol/templates/admin_repositories.html:112
+#: trac/admin/templates/admin_basics.html:29
+#: trac/ticket/templates/admin_components.html:47
+#: trac/ticket/templates/admin_components.html:74
+#: trac/ticket/templates/admin_enums.html:34
+#: trac/ticket/templates/admin_enums.html:48
+#: trac/ticket/templates/admin_milestones.html:45
+#: trac/ticket/templates/admin_milestones.html:103
+#: trac/ticket/templates/admin_versions.html:36
+#: trac/ticket/templates/admin_versions.html:71
+#: trac/versioncontrol/templates/admin_repositories.html:61
+#: trac/versioncontrol/templates/admin_repositories.html:108
+#: trac/versioncontrol/templates/admin_repositories.html:125
 msgid "Name:"
 msgstr "名称:"
 
-#: trac/admin/templates/admin_basics.html:24
-#: trac/versioncontrol/templates/admin_repositories.html:62
+#: trac/admin/templates/admin_basics.html:34
+#: trac/versioncontrol/templates/admin_repositories.html:74
 msgid "URL:"
 msgstr "URL:"
 
-#: trac/admin/templates/admin_basics.html:29
-#: trac/ticket/templates/ticket.html:237
+#: trac/admin/templates/admin_basics.html:39
+#: trac/ticket/templates/ticket.html:241
 msgid "Description:"
 msgstr "詳細:"
 
-#: trac/admin/templates/admin_basics.html:35
+#: trac/admin/templates/admin_basics.html:45
 msgid "Default timezone:"
 msgstr "デフォルトタイムゾーン:"
 
-#: trac/admin/templates/admin_basics.html:37
+#: trac/admin/templates/admin_basics.html:47
 msgid "Server's local time zone"
 msgstr "サーバ側のタイムゾーン"
 
-#: trac/admin/templates/admin_basics.html:44
+#: trac/admin/templates/admin_basics.html:52
+msgid "Install pytz for a complete list of timezones."
+msgstr "pytz をインストールするとすべてのタイムゾーンを表示することができます。"
+
+#: trac/admin/templates/admin_basics.html:57
 msgid "Default language:"
 msgstr "デフォルトの言語:"
 
-#: trac/admin/templates/admin_basics.html:46
-#: trac/admin/templates/admin_basics.html:55
+#: trac/admin/templates/admin_basics.html:58
+#: trac/prefs/templates/prefs_language.html:26
+msgid "Translations are currently unavailable"
+msgstr "現状は翻訳を利用できません"
+
+#: trac/admin/templates/admin_basics.html:60
+#: trac/admin/templates/admin_basics.html:75
 msgid "Browser's language"
 msgstr "ブラウザの言語設定"
 
-#: trac/admin/templates/admin_basics.html:53
+#: trac/admin/templates/admin_basics.html:65
+#: trac/prefs/templates/prefs_language.html:34
+msgid "Install Babel for extended language support."
+msgstr "Babel をインストールすると言語サポートが追加されます。"
+
+#: trac/admin/templates/admin_basics.html:68
+#: trac/prefs/templates/prefs_language.html:37
+msgid "Message catalogs have not been compiled."
+msgstr "メッセージカタログがコンパイルされていません。"
+
+#: trac/admin/templates/admin_basics.html:73
 msgid "Default date format:"
 msgstr "デフォルトの日付書式:"
 
-#: trac/admin/templates/admin_basics.html:57
-#: trac/prefs/templates/prefs_datetime.html:65
+#: trac/admin/templates/admin_basics.html:77
+#: trac/prefs/templates/prefs_datetime.html:75
 msgid "ISO 8601 format"
 msgstr "ISO 8601 書式"
 
-#: trac/admin/templates/admin_basics.html:63
-#: trac/admin/templates/admin_components.html:99
-#: trac/admin/templates/admin_enums.html:70
-#: trac/admin/templates/admin_logging.html:55
-#: trac/admin/templates/admin_milestones.html:132
-#: trac/admin/templates/admin_plugins.html:180
-#: trac/admin/templates/admin_versions.html:100
+#: trac/admin/templates/admin_basics.html:80
+msgid "Install Babel for localized date formats."
+msgstr "Babel をインストールすると日付書式をローカライズします。"
+
+#: trac/admin/templates/admin_basics.html:86
+#: trac/admin/templates/admin_logging.html:65
+#: trac/admin/templates/admin_plugins.html:190
+#: trac/ticket/templates/admin_components.html:106
+#: trac/ticket/templates/admin_enums.html:82
+#: trac/ticket/templates/admin_milestones.html:148
+#: trac/ticket/templates/admin_versions.html:109
 msgid "Apply changes"
 msgstr "変更を適用"
 
-#: trac/admin/templates/admin_components.html:10 trac/ticket/admin.py:77
-msgid "Components"
-msgstr "コンポーネント"
-
-#: trac/admin/templates/admin_components.html:14
-msgid "Manage Components"
-msgstr "コンポーネントの管理"
-
-#: trac/admin/templates/admin_components.html:18
-msgid "Owner:"
-msgstr "担当者:"
-
-#: trac/admin/templates/admin_components.html:35
-msgid "Modify Component:"
-msgstr "コンポーネントの変更:"
-
-#: trac/admin/templates/admin_components.html:42
-msgid ""
-"Description (you may use\n"
-"                [1:WikiFormatting]\n"
-"                here):"
-msgstr "詳細 ([1:WikiFormatting] が使えます):"
-
-#: trac/admin/templates/admin_components.html:56
-#: trac/admin/templates/admin_enums.html:25
-#: trac/admin/templates/admin_milestones.html:75
-#: trac/admin/templates/admin_versions.html:51
-#: trac/versioncontrol/templates/admin_repositories.html:85
-msgid "Save"
-msgstr "保存"
-
-#: trac/admin/templates/admin_components.html:64
-msgid "Add Component:"
-msgstr "コンポーネントの追加:"
-
-#: trac/admin/templates/admin_components.html:70
-#: trac/admin/templates/admin_enums.html:38
-#: trac/admin/templates/admin_milestones.html:96
-#: trac/admin/templates/admin_perms.html:31
-#: trac/admin/templates/admin_perms.html:50
-#: trac/admin/templates/admin_versions.html:73
-#: trac/versioncontrol/templates/admin_repositories.html:102
-#: trac/versioncontrol/templates/admin_repositories.html:116
-msgid "Add"
-msgstr "追加"
-
-#: trac/admin/templates/admin_components.html:80 trac/ticket/admin.py:210
-#: trac/ticket/api.py:293 trac/ticket/web_ui.py:1455
-msgid "Owner"
-msgstr "担当者"
-
-#: trac/admin/templates/admin_components.html:80
-#: trac/admin/templates/admin_enums.html:48
-#: trac/admin/templates/admin_milestones.html:107
-#: trac/admin/templates/admin_versions.html:83
-msgid "Default"
-msgstr "デフォルト"
-
-#: trac/admin/templates/admin_components.html:98
-#: trac/admin/templates/admin_enums.html:69
-#: trac/admin/templates/admin_milestones.html:131
-#: trac/admin/templates/admin_perms.html:109
-#: trac/admin/templates/admin_versions.html:99
-#: trac/versioncontrol/templates/admin_repositories.html:145
-msgid "Remove selected items"
-msgstr "選択した項目を削除"
-
-#: trac/admin/templates/admin_components.html:101
-#: trac/admin/templates/admin_enums.html:72
-#: trac/admin/templates/admin_milestones.html:134
-#: trac/admin/templates/admin_versions.html:102
-msgid ""
-"You can remove all items from this list to completely hide this\n"
-"              field from the user interface."
-msgstr ""
-"すべての項目を削除する事で、このフィールドが、\n"
-"ユーザインターフェースに現れないようにする事が可能です。"
-
-#: trac/admin/templates/admin_components.html:107
-#: trac/admin/templates/admin_enums.html:82
-#: trac/admin/templates/admin_milestones.html:140
-#: trac/admin/templates/admin_versions.html:108
-msgid ""
-"As long as you don't add any items to the list, this field\n"
-"            will remain completely hidden from the user interface."
-msgstr ""
-"項目を追加しない限り、このフィールドは、\n"
-"ユーザインターフェースには現れません。"
-
-#: trac/admin/templates/admin_enums.html:14
-#, python-format
-msgid "Manage %(label_plural)s"
-msgstr "%(label_plural)sの管理"
-
-#: trac/admin/templates/admin_enums.html:19
-#, python-format
-msgid "Modify %(label_singular)s"
-msgstr "%(label_singular)sの変更"
-
-#: trac/admin/templates/admin_enums.html:33
-#, python-format
-msgid "Add %(label_singular)s"
-msgstr "%(label_singular)sの追加"
-
-#: trac/admin/templates/admin_enums.html:48
-msgid "Order"
-msgstr "順序"
-
-#: trac/admin/templates/admin_enums.html:76
-msgid ""
-"[1:Note:] The order of priorities determines the\n"
-"              coloring of entries in the ticket queries and reports."
-msgstr "[1:※] 優先度の順にチケットクエリやレポートでの配色が決まります。"
-
-#: trac/admin/templates/admin_logging.html:26 trac/templates/about.html:85
+#: trac/admin/templates/admin_logging.html:36 trac/templates/about.html:97
 msgid "Configuration"
 msgstr "設定"
 
-#: trac/admin/templates/admin_logging.html:28
-#: trac/versioncontrol/templates/admin_repositories.html:18
+#: trac/admin/templates/admin_logging.html:38
+#: trac/versioncontrol/templates/admin_repositories.html:28
 msgid "Type:"
 msgstr "種別:"
 
-#: trac/admin/templates/admin_logging.html:37
+#: trac/admin/templates/admin_logging.html:47
 msgid "Log level:"
 msgstr "ログレベル:"
 
-#: trac/admin/templates/admin_logging.html:45
+#: trac/admin/templates/admin_logging.html:55
 msgid "Log file:"
 msgstr "ログファイル:"
 
-#: trac/admin/templates/admin_logging.html:48
+#: trac/admin/templates/admin_logging.html:58
 #, python-format
 msgid ""
 "If you specify a relative path, the log file will be stored inside the\n"
 "            [1:log] directory of the project environment ([2:%(dir)s])."
 msgstr "相対パスを指定した場合は、プロジェクトの [1:log] ディレクトリに作成します ([2:%(dir)s])。"
 
-#: trac/admin/templates/admin_milestones.html:10 trac/ticket/admin.py:235
-#: trac/ticket/roadmap.py:963
-msgid "Milestones"
-msgstr "マイルストーン"
-
-#: trac/admin/templates/admin_milestones.html:20
-msgid "Manage Milestones"
-msgstr "マイルストーンの管理"
-
-#: trac/admin/templates/admin_milestones.html:26
-msgid "Modify Milestone:"
-msgstr "マイルストーンの変更:"
-
-#: trac/admin/templates/admin_milestones.html:31
-#: trac/admin/templates/admin_milestones.html:88
-#: trac/ticket/templates/milestone_edit.html:61
-msgid "Due:"
-msgstr "期日:"
-
-#: trac/admin/templates/admin_milestones.html:32
-#: trac/admin/templates/admin_milestones.html:35
-#: trac/admin/templates/admin_milestones.html:45
-#: trac/admin/templates/admin_milestones.html:49
-#: trac/admin/templates/admin_milestones.html:90
-#: trac/admin/templates/admin_versions.html:32
-#: trac/admin/templates/admin_versions.html:35
-#: trac/admin/templates/admin_versions.html:66
-#: trac/admin/templates/admin_versions.html:69
-#: trac/ticket/templates/milestone_edit.html:65
-#: trac/ticket/templates/milestone_edit.html:68
-#: trac/ticket/templates/milestone_edit.html:77
-#: trac/ticket/templates/milestone_edit.html:80
-#, python-format
-msgid "Format: %(datehint)s"
-msgstr "書式: %(datehint)s"
-
-#: trac/admin/templates/admin_milestones.html:41
-#: trac/ticket/templates/milestone_edit.html:73
-msgid "Completed:"
-msgstr "完了:"
-
-#: trac/admin/templates/admin_milestones.html:63
-#: trac/admin/templates/admin_versions.html:40
-#: trac/ticket/templates/milestone_edit.html:99
-#: trac/versioncontrol/templates/admin_repositories.html:73
-msgid "Description (you may use [1:WikiFormatting] here):"
-msgstr "詳細 ([1:WikiFormatting] が使えます):"
-
-#: trac/admin/templates/admin_milestones.html:83
-msgid "Add Milestone:"
-msgstr "マイルストーンの追加:"
-
-#: trac/admin/templates/admin_milestones.html:92
-#, python-format
-msgid "Format: %(datetimehint)s"
-msgstr "書式: %(datetimehint)s"
-
-#: trac/admin/templates/admin_milestones.html:107 trac/ticket/admin.py:399
-msgid "Due"
-msgstr "期日"
-
-#: trac/admin/templates/admin_milestones.html:107 trac/ticket/admin.py:399
-msgid "Completed"
-msgstr "完了日時"
-
-#: trac/admin/templates/admin_milestones.html:107 trac/ticket/web_ui.py:194
-msgid "Tickets"
-msgstr "チケット"
-
-#: trac/admin/templates/admin_perms.html:14
+#: trac/admin/templates/admin_perms.html:24
 msgid "Manage Permissions and Groups"
 msgstr "権限とグループの管理"
 
-#: trac/admin/templates/admin_perms.html:19
+#: trac/admin/templates/admin_perms.html:29
 msgid "Grant Permission:"
 msgstr "権限の付与:"
 
-#: trac/admin/templates/admin_perms.html:21
-#: trac/admin/templates/admin_perms.html:44
+#: trac/admin/templates/admin_perms.html:31
+#: trac/admin/templates/admin_perms.html:55
 msgid "Subject:"
 msgstr "対象:"
 
-#: trac/admin/templates/admin_perms.html:24
+#: trac/admin/templates/admin_perms.html:34
 msgid "Action:"
 msgstr "アクション:"
 
-#: trac/admin/templates/admin_perms.html:33
+#: trac/admin/templates/admin_perms.html:42
+#: trac/admin/templates/admin_perms.html:61
+#: trac/ticket/templates/admin_components.html:78
+#: trac/ticket/templates/admin_enums.html:51
+#: trac/ticket/templates/admin_milestones.html:114
+#: trac/ticket/templates/admin_versions.html:83
+#: trac/versioncontrol/templates/admin_repositories.html:115
+#: trac/versioncontrol/templates/admin_repositories.html:129
+msgid "Add"
+msgstr "追加"
+
+#: trac/admin/templates/admin_perms.html:44
 msgid ""
 "Grant permission for an action to a subject, which can be either a user\n"
 "            or a group."
 msgstr "ユーザやグループに権限を付与します。"
 
-#: trac/admin/templates/admin_perms.html:42
+#: trac/admin/templates/admin_perms.html:53
 msgid "Add Subject to Group:"
 msgstr "グループの追加:"
 
-#: trac/admin/templates/admin_perms.html:47
+#: trac/admin/templates/admin_perms.html:58
 msgid "Group:"
 msgstr "グループ:"
 
-#: trac/admin/templates/admin_perms.html:52
-msgid "Add a user or group to an existing permission group."
-msgstr "対象(ユーザやグループ)に権限グループを追加します。"
-
 #: trac/admin/templates/admin_perms.html:63
-#: trac/admin/templates/admin_perms.html:88
+msgid "Add a user or group to an existing permission group."
+msgstr "ユーザやグループを既存の権限グループに追加します。"
+
+#: trac/admin/templates/admin_perms.html:74
+#: trac/admin/templates/admin_perms.html:103
 msgid "Subject"
 msgstr "対象"
 
-#: trac/admin/templates/admin_perms.html:76
-msgid "Action is no longer defined"
-msgstr "アクションはすでに定義されていません"
+#: trac/admin/templates/admin_perms.html:85
+msgid "You don't have permission to revoke this action"
+msgstr "このアクションを無効にする権限を持っていません"
 
-#: trac/admin/templates/admin_perms.html:81
+#: trac/admin/templates/admin_perms.html:91
+#, python-format
+msgid "%(action)s is no longer defined"
+msgstr "%(action)s はすでに定義されていません"
+
+#: trac/admin/templates/admin_perms.html:96
 msgid "No permissions"
 msgstr "権限なし"
 
-#: trac/admin/templates/admin_perms.html:85
+#: trac/admin/templates/admin_perms.html:100
 msgid "Group Membership"
 msgstr "グループメンバ"
 
-#: trac/admin/templates/admin_perms.html:88
+#: trac/admin/templates/admin_perms.html:103
 msgid "Group"
 msgstr "グループ"
 
-#: trac/admin/templates/admin_perms.html:105
+#: trac/admin/templates/admin_perms.html:120
 msgid "No group memberships"
 msgstr "グループメンバなし"
 
-#: trac/admin/templates/admin_perms.html:113
+#: trac/admin/templates/admin_perms.html:124
+#: trac/ticket/templates/admin_components.html:107
+#: trac/ticket/templates/admin_enums.html:83
+#: trac/ticket/templates/admin_milestones.html:149
+#: trac/ticket/templates/admin_versions.html:110
+#: trac/versioncontrol/templates/admin_repositories.html:158
+msgid "Remove selected items"
+msgstr "選択した項目を削除"
+
+#: trac/admin/templates/admin_perms.html:128
 msgid ""
 "Note that [1:Subject] or [2:Group] names can't be all upper-case,\n"
 "      as that is reserved for permission names."
@@ -1486,122 +1457,139 @@
 "[1:ユーザ]や[2:グループ]には、大文字だけからなる名称は使用できません。\n"
 "大文字だけからなる名称は、権限名に予約されているためです。"
 
-#: trac/admin/templates/admin_plugins.html:57
+#: trac/admin/templates/admin_plugins.html:67
 msgid "Manage Plugins"
 msgstr "プラグインの管理"
 
-#: trac/admin/templates/admin_plugins.html:61
+#: trac/admin/templates/admin_plugins.html:71
 msgid "Install Plugin:"
 msgstr "プラグインのインストール:"
 
-#: trac/admin/templates/admin_plugins.html:63
+#: trac/admin/templates/admin_plugins.html:73
 msgid "File: [1:]"
 msgstr "ファイル: [1:]"
 
-#: trac/admin/templates/admin_plugins.html:68
+#: trac/admin/templates/admin_plugins.html:78
 msgid "Install"
 msgstr "インストール"
 
-#: trac/admin/templates/admin_plugins.html:72
+#: trac/admin/templates/admin_plugins.html:82
 msgid ""
 "The web server does not have sufficient permissions to store files in\n"
 "            the environment plugins directory."
 msgstr "ウェブサーバはプラグインディレクトリにファイルを作成する権限を持っていません。"
 
-#: trac/admin/templates/admin_plugins.html:76
+#: trac/admin/templates/admin_plugins.html:86
 msgid "Upload a plugin packaged as Python egg."
 msgstr "Python egg 形式のプラグインパッケージをアップロードします。"
 
-#: trac/admin/templates/admin_plugins.html:100 trac/templates/diff_view.html:51
-#: trac/versioncontrol/templates/changeset.html:142
+#: trac/admin/templates/admin_plugins.html:110 trac/templates/diff_view.html:61
+#: trac/versioncontrol/templates/changeset.html:152
 msgid "Author:"
 msgstr "更新者:"
 
-#: trac/admin/templates/admin_plugins.html:109
+#: trac/admin/templates/admin_plugins.html:119
 msgid "Home page:"
 msgstr "ホームページ:"
 
-#: trac/admin/templates/admin_plugins.html:116
+#: trac/admin/templates/admin_plugins.html:126
 msgid "License:"
 msgstr "ライセンス:"
 
-#: trac/admin/templates/admin_plugins.html:124 trac/ticket/admin.py:77
-#: trac/ticket/api.py:306
+#: trac/admin/templates/admin_plugins.html:134 trac/ticket/admin.py:77
+#: trac/ticket/api.py:315
 msgid "Component"
 msgstr "コンポーネント"
 
-#: trac/admin/templates/admin_plugins.html:127
+#: trac/admin/templates/admin_plugins.html:137
 msgid "Show all descriptions"
 msgstr "説明をすべて表示する"
 
-#: trac/admin/templates/admin_plugins.html:129
+#: trac/admin/templates/admin_plugins.html:139
 msgid "Hide all descriptions"
 msgstr "説明をすべて隠す"
 
-#: trac/admin/templates/admin_plugins.html:133
+#: trac/admin/templates/admin_plugins.html:143
 msgid "Enabled"
 msgstr "有効"
 
-#: trac/admin/templates/admin_versions.html:10 trac/ticket/admin.py:431
-msgid "Versions"
-msgstr "バージョン"
-
-#: trac/admin/templates/admin_versions.html:19
-msgid "Manage Versions"
-msgstr "バージョンの管理"
-
-#: trac/admin/templates/admin_versions.html:24
-msgid "Modify Version:"
-msgstr "バージョンの変更:"
-
-#: trac/admin/templates/admin_versions.html:31
-msgid "Date:"
-msgstr "日時:"
-
-#: trac/admin/templates/admin_versions.html:59
-msgid "Add Version:"
-msgstr "バージョンの追加:"
-
-#: trac/admin/templates/admin_versions.html:64
-msgid "Released:"
-msgstr "リリース日時:"
-
-#: trac/admin/templates/admin_versions.html:83
-msgid "Released"
-msgstr "リリース日時"
-
-#: trac/db/api.py:308
+#: trac/db/api.py:334
 #, python-format
 msgid "Unsupported database type \"%(scheme)s\""
 msgstr "\"%(scheme)s\" データベースをサポートしていません"
 
-#: trac/db/api.py:347
+#: trac/db/api.py:373
 #, python-format
 msgid ""
 "Unknown scheme \"%(scheme)s\"; database connection string must start with"
 " {scheme}:/"
 msgstr "不明なスキーマ: \"%(scheme)s\"。データベース接続文字列は {scheme}:/ の形式で始まらないといけません"
 
-#: trac/db/mysql_backend.py:87
+#: trac/db/mysql_backend.py:92
 msgid "Cannot load Python bindings for MySQL"
 msgstr "MySQL 向け Python バインディングをロードできません"
 
-#: trac/db/mysql_backend.py:229 trac/db/postgres_backend.py:179
+#: trac/db/mysql_backend.py:248 trac/db/postgres_backend.py:179
 #: trac/db/postgres_backend.py:198
 #, python-format
 msgid "Unable to run %(path)s: %(msg)s"
 msgstr "%(path)s を実行できませんでした: %(msg)s"
 
-#: trac/db/mysql_backend.py:233
+#: trac/db/mysql_backend.py:252
 #, python-format
 msgid "mysqldump failed: %(msg)s"
 msgstr "mysqldump が失敗しました: %(msg)s"
 
-#: trac/db/mysql_backend.py:235 trac/db/postgres_backend.py:204
-#: trac/db/sqlite_backend.py:245
+#: trac/db/mysql_backend.py:254 trac/db/postgres_backend.py:204
+#: trac/db/sqlite_backend.py:247
 msgid "No destination file created"
 msgstr "出力先のファイルが作成されていません"
 
+#: trac/db/mysql_backend.py:290
+#, python-format
+msgid ""
+"All tables must be created as InnoDB or NDB storage engine to support "
+"transactions. The following tables have been created as storage engine "
+"which doesn't support transactions: %(tables)s"
+msgstr ""
+"すべてのテーブルはトランザクションをサポートする InnoDB または NDB "
+"ストレージエンジンで作成する必要があります。次のテーブルがトランザクションをサポートしないストレージエンジンで作成されています: "
+"%(tables)s"
+
+#: trac/db/mysql_backend.py:299
+#, python-format
+msgid ""
+"All tables must be created with utf8_bin or utf8mb4_bin as collation. The"
+" following tables don't have the collations: %(tables)s"
+msgstr ""
+"すべてのテーブルは照合順序として utf8_bin または utf8mb4_bin "
+"で作成されている必要があります。次のテーブルがその照合順序で作成されていません: %(tables)s"
+
+#: trac/db/mysql_backend.py:314
+#, python-format
+msgid ""
+"The current storage engine is %(engine)s. It must be InnoDB or NDB "
+"storage engine to support transactions."
+msgstr "ストレージエンジンは %(engine)s になっています。トランザクションをサポートしている InnoDB または NDB にする必要があります。"
+
+#: trac/db/mysql_backend.py:320
+#, python-format
+msgid ""
+"The current storage engine for TEMPORARY tables is %(engine)s. It must be"
+" InnoDB or NDB storage engine to support transactions."
+msgstr ""
+"TEMPORARY テーブルに対するストレージエンジンは %(engine)s になっています。トランザクションをサポートしている InnoDB "
+"または NDB にする必要があります。"
+
+#: trac/db/mysql_backend.py:332
+#, python-format
+msgid ""
+"The charset and collation of database are '%(charset)s' and "
+"'%(collation)s'. The database must be created with one of %(supported)s."
+msgstr ""
+"データベースのキャラクタセットと照合順序が '%(charset)s' と '%(collation)s' になっています。データベースは "
+"%(supported)s の1つでなければいけません。"
+
 #: trac/db/pool.py:130
 #, python-format
 msgid "Unable to get database connection within %(time)d seconds."
@@ -1616,56 +1604,56 @@
 msgid "pg_dump failed: %(msg)s"
 msgstr "pg_dump が失敗しました: %(msg)s"
 
-#: trac/db/sqlite_backend.py:156
+#: trac/db/sqlite_backend.py:158 trac/db/sqlite_backend.py:261
 msgid "Cannot load Python bindings for SQLite"
 msgstr "SQLite 向け Python バインディングをロードできません"
 
-#: trac/db/sqlite_backend.py:159
+#: trac/db/sqlite_backend.py:161
 #, python-format
 msgid "Need at least PySqlite %(version)s or higher"
 msgstr "PySqlite %(version)s もしくは、それ以上が必要です"
 
-#: trac/db/sqlite_backend.py:162
+#: trac/db/sqlite_backend.py:164
 msgid "PySqlite 2.5.2 - 2.5.4 break Trac, please use 2.5.5 or higher"
 msgstr "PySqlite 2.5.2 ~ 2.5.4 では Trac は動作しません。2.5.5 以降を使ってください"
 
-#: trac/db/sqlite_backend.py:195
+#: trac/db/sqlite_backend.py:197
 #, python-format
 msgid "Database already exists at %(path)s"
 msgstr "データベース %(path)s はすでに存在します"
 
-#: trac/db/sqlite_backend.py:262
+#: trac/db/sqlite_backend.py:265
 #, python-format
 msgid "Database \"%(path)s\" not found."
 msgstr "データベース %(path)s は見つかりません。"
 
-#: trac/db/sqlite_backend.py:271
+#: trac/db/sqlite_backend.py:274
 #, python-format
 msgid ""
 "The user %(user)s requires read _and_ write permissions to the database "
 "file %(path)s and the directory it is located in."
 msgstr "ファイル %(path)s と、そのディレクトリに %(user)s ユーザの読み書き権限が必要です。"
 
-#: trac/mimeview/api.py:685 trac/mimeview/api.py:695
+#: trac/mimeview/api.py:691 trac/mimeview/api.py:701
 #, python-format
 msgid "No available MIME conversions from %(old)s to %(new)s"
 msgstr "%(old)s から %(new)s への MIME 変換は、利用可能ではありません"
 
-#: trac/mimeview/api.py:808
+#: trac/mimeview/api.py:814
 #, python-format
 msgid "HTML preview using %(renderer)s failed (%(err)s)"
 msgstr "(%(err)s) のため、%(renderer)s を使った HTML プレビューに失敗しました"
 
-#: trac/mimeview/api.py:839
+#: trac/mimeview/api.py:845
 #, python-format
 msgid "Can't use %(annotator)s annotator: %(error)s"
 msgstr "%(annotator)s が使えません: %(error)s"
 
-#: trac/mimeview/api.py:1114 trac/templates/error.html:148
+#: trac/mimeview/api.py:1121 trac/templates/error.html:167
 msgid "Line"
 msgstr "行"
 
-#: trac/mimeview/api.py:1114
+#: trac/mimeview/api.py:1121
 msgid "Line numbers"
 msgstr "行番号"
 
@@ -1683,56 +1671,56 @@
 msgid "this hunk was shorter than expected"
 msgstr "この hunk は想定より短いようです"
 
-#: trac/mimeview/pygments.py:132 trac/prefs/templates/prefs_pygments.html:9
+#: trac/mimeview/pygments.py:132 trac/prefs/templates/prefs_pygments.html:19
 msgid "Syntax Highlighting"
 msgstr "シンタックスハイライト"
 
-#: trac/mimeview/pygments.py:141 trac/prefs/web_ui.py:160
+#: trac/mimeview/pygments.py:141 trac/prefs/web_ui.py:170
 msgid "Your preferences have been saved."
 msgstr "個人設定を保存しました。"
 
-#: trac/mimeview/rst.py:125 trac/mimeview/rst.py:148
+#: trac/mimeview/rst.py:126 trac/mimeview/rst.py:149
 #, python-format
 msgid "%(link)s is not a valid TracLink"
 msgstr "%(link)s は有効な TracLink ではありません。"
 
-#: trac/prefs/web_ui.py:56 trac/prefs/templates/prefs.html:16
+#: trac/prefs/web_ui.py:51 trac/prefs/templates/prefs.html:26
 msgid "Preferences"
 msgstr "個人設定"
 
-#: trac/prefs/web_ui.py:83
+#: trac/prefs/web_ui.py:79
 msgid "Unknown preference panel"
 msgstr "不明な設定画面"
 
-#: trac/prefs/web_ui.py:95 trac/prefs/templates/prefs_datetime.html:10
+#: trac/prefs/web_ui.py:91 trac/prefs/templates/prefs_datetime.html:20
 msgid "Date & Time"
 msgstr "日付と時間"
 
-#: trac/prefs/web_ui.py:96 trac/prefs/templates/prefs_keybindings.html:10
+#: trac/prefs/web_ui.py:92 trac/prefs/templates/prefs_keybindings.html:20
 msgid "Keyboard Shortcuts"
 msgstr "キーボードショートカット"
 
-#: trac/prefs/web_ui.py:97 trac/prefs/templates/prefs_userinterface.html:10
+#: trac/prefs/web_ui.py:93 trac/prefs/templates/prefs_userinterface.html:20
 msgid "User Interface"
 msgstr "ユーザインターフェイス"
 
-#: trac/prefs/web_ui.py:99 trac/prefs/templates/prefs_language.html:10
+#: trac/prefs/web_ui.py:95 trac/prefs/templates/prefs_language.html:20
 msgid "Language"
 msgstr "言語"
 
-#: trac/prefs/web_ui.py:101 trac/prefs/templates/prefs_advanced.html:9
+#: trac/prefs/web_ui.py:97 trac/prefs/templates/prefs_advanced.html:19
 msgid "Advanced"
 msgstr "詳細"
 
-#: trac/prefs/web_ui.py:167
+#: trac/prefs/web_ui.py:177
 msgid "The session has been loaded."
 msgstr "セッションをロードしました。"
 
-#: trac/prefs/templates/prefs.html:10
+#: trac/prefs/templates/prefs.html:20
 msgid "Preferences:"
 msgstr "個人設定:"
 
-#: trac/prefs/templates/prefs.html:17
+#: trac/prefs/templates/prefs.html:27
 msgid ""
 "This page lets you customize your personal settings for this site.\n"
 "      These settings are stored on the server and are identified by a "
@@ -1744,19 +1732,19 @@
 "このページではこのサイトでの個人向けの設定を行います。\n"
 "設定内容はサーバに保存しブラウザのクッキー情報を用いて管理しているため、次回訪問時にも同じ情報が使われます。"
 
-#: trac/prefs/templates/prefs.html:33
+#: trac/prefs/templates/prefs.html:43
 msgid "Save changes"
 msgstr "保存"
 
-#: trac/prefs/templates/prefs_advanced.html:14
+#: trac/prefs/templates/prefs_advanced.html:24
 msgid "Session key:"
 msgstr "セッションキー:"
 
-#: trac/prefs/templates/prefs_advanced.html:17
+#: trac/prefs/templates/prefs_advanced.html:27
 msgid "Change"
 msgstr "変更"
 
-#: trac/prefs/templates/prefs_advanced.html:18
+#: trac/prefs/templates/prefs_advanced.html:28
 msgid ""
 "The session key is used to identify stored custom\n"
 "      settings and session data on the server. Although it is\n"
@@ -1769,15 +1757,15 @@
 "他のブラウザでも同じ設定を使用したい場合などには、\n"
 "他の覚えやすい文字列を指定することもできます。"
 
-#: trac/prefs/templates/prefs_advanced.html:26
+#: trac/prefs/templates/prefs_advanced.html:36
 msgid "Restore session:"
 msgstr "セッションの復元:"
 
-#: trac/prefs/templates/prefs_advanced.html:29
+#: trac/prefs/templates/prefs_advanced.html:39
 msgid "Load"
 msgstr "ロード"
 
-#: trac/prefs/templates/prefs_advanced.html:30
+#: trac/prefs/templates/prefs_advanced.html:40
 msgid ""
 "You may load a previously created session by entering the\n"
 "      corresponding session key below. This lets you share settings "
@@ -1787,15 +1775,15 @@
 "以下の欄にセッションキーを指定することで前回のセッションをロードすることができます。\n"
 "これにより複数のコンピュータやブラウザにまたがって設定を共有できるようになっています。"
 
-#: trac/prefs/templates/prefs_datetime.html:16
+#: trac/prefs/templates/prefs_datetime.html:26
 msgid "Time zone:"
 msgstr "タイムゾーン:"
 
-#: trac/prefs/templates/prefs_datetime.html:18
+#: trac/prefs/templates/prefs_datetime.html:28
 msgid "Default time zone"
 msgstr "デフォルトタイムゾーン"
 
-#: trac/prefs/templates/prefs_datetime.html:25
+#: trac/prefs/templates/prefs_datetime.html:35
 msgid ""
 "Configuring your time zone will result in all\n"
 "      dates and times displayed on this site to use your time zone\n"
@@ -1805,24 +1793,24 @@
 "このサイトで表示される日付および時間はサーバでのものではなく、\n"
 "指定したタイムゾーンに変換して表示するようになります。"
 
-#: trac/prefs/templates/prefs_datetime.html:34
+#: trac/prefs/templates/prefs_datetime.html:44
 #, python-format
 msgid "Example: The current time is [1:%(time)s] (UTC)."
 msgstr "例: 現在の時刻はUTC(世界標準時)で [1:%(time)s] です。"
 
-#: trac/prefs/templates/prefs_datetime.html:39
+#: trac/prefs/templates/prefs_datetime.html:49
 #, python-format
 msgid ""
 "In your time zone %(tz)s, this would be displayed as\n"
 "            [1:%(formatted)s]."
 msgstr "選択中のタイムゾーン %(tz)s では [1:%(formatted)s] と表示されます。"
 
-#: trac/prefs/templates/prefs_datetime.html:45
+#: trac/prefs/templates/prefs_datetime.html:55
 #, python-format
 msgid "In the default time zone, this would be displayed as [1:%(formatted)s]."
 msgstr "デフォルトのタイムゾーンでは [1:%(formatted)s] と表示されます。"
 
-#: trac/prefs/templates/prefs_datetime.html:51
+#: trac/prefs/templates/prefs_datetime.html:61
 msgid ""
 "Note: Universal Co-ordinated Time (UTC) is also known as Greenwich Mean "
 "Time (GMT).[1:]\n"
@@ -1832,19 +1820,19 @@
 "※ "
 "協定世界時(UTC)はグリニッジ標準時(GMT)としても知られています。[1:]標準時との差が正であれば、グリニッジより東の(つまり標準時より進んでいる)タイムゾーンになります。"
 
-#: trac/prefs/templates/prefs_datetime.html:59
+#: trac/prefs/templates/prefs_datetime.html:69
 msgid "Date format:"
 msgstr "日付書式:"
 
-#: trac/prefs/templates/prefs_datetime.html:61
+#: trac/prefs/templates/prefs_datetime.html:71
 msgid "Default date format"
 msgstr "デフォルトの日付書式"
 
-#: trac/prefs/templates/prefs_datetime.html:63
+#: trac/prefs/templates/prefs_datetime.html:73
 msgid "Your language setting"
 msgstr "自分の言語設定"
 
-#: trac/prefs/templates/prefs_datetime.html:69
+#: trac/prefs/templates/prefs_datetime.html:79
 msgid ""
 "Configuring your date format will result in formatting\n"
 "      and parsing datetime displayed on this site to use your date format"
@@ -1852,23 +1840,23 @@
 "      instead of that of the server."
 msgstr "日付書式を設定することで、このサイトでの日付の表示書式と入力書式はサーバのものではなく指定した書式になります。"
 
-#: trac/prefs/templates/prefs_datetime.html:75
+#: trac/prefs/templates/prefs_datetime.html:85
 msgid "Date relative/absolute format:"
 msgstr "日付の相対書式・絶対書式:"
 
-#: trac/prefs/templates/prefs_datetime.html:77
+#: trac/prefs/templates/prefs_datetime.html:87
 msgid "Default format"
 msgstr "デフォルト書式"
 
-#: trac/prefs/templates/prefs_datetime.html:79
+#: trac/prefs/templates/prefs_datetime.html:89
 msgid "Relative format"
 msgstr "相対書式"
 
-#: trac/prefs/templates/prefs_datetime.html:81
+#: trac/prefs/templates/prefs_datetime.html:91
 msgid "Absolute format"
 msgstr "絶対書式"
 
-#: trac/prefs/templates/prefs_datetime.html:85
+#: trac/prefs/templates/prefs_datetime.html:95
 msgid ""
 "Configuring your relative/absolute format will result in\n"
 "      formatting datetime displayed on this site to use your format "
@@ -1876,32 +1864,32 @@
 "      that of the server."
 msgstr "相対または絶対書式を設定することで、このサイトで表示される日時はサーバでのものではなく指定した書式になります。"
 
-#: trac/prefs/templates/prefs_general.html:15
+#: trac/prefs/templates/prefs_general.html:25
 msgid "Full name:"
 msgstr "氏名:"
 
-#: trac/prefs/templates/prefs_general.html:20
+#: trac/prefs/templates/prefs_general.html:30
 msgid "Email address:"
 msgstr "メールアドレス:"
 
-#: trac/prefs/templates/prefs_general.html:26
+#: trac/prefs/templates/prefs_general.html:36
 msgid ""
 "This information is used to automatically populate some forms\n"
 "        on this site with your contact details."
 msgstr "この情報は、このサイトのフォームに連絡先情報などを自動的に埋めるのに使用します。"
 
-#: trac/prefs/templates/prefs_general.html:30
+#: trac/prefs/templates/prefs_general.html:40
 msgid ""
 "This information is used to associate your login name with your\n"
 "        email address and full name, which is used for email\n"
 "        notification and RSS feeds, for example."
 msgstr "この情報は自分のログイン名とメールアドレスや氏名を対応させるためのもので、メールによる通知や RSS フィードなどに使用します。"
 
-#: trac/prefs/templates/prefs_keybindings.html:18
+#: trac/prefs/templates/prefs_keybindings.html:28
 msgid "Enable access keys"
 msgstr "アクセスキーを有効にする"
 
-#: trac/prefs/templates/prefs_keybindings.html:21
+#: trac/prefs/templates/prefs_keybindings.html:31
 msgid ""
 "This site provides keyboard shortcuts for\n"
 "      faster access to certain functions of this site. As these shortcuts"
@@ -1915,112 +1903,118 @@
 "これらのショートカットがデスクトップシステムやブラウザが提供するものと衝突することがあるため標準では無効となっています。\n"
 "アクセスキーに関しての詳細は [1:TracAccessibility] を参照してください。"
 
-#: trac/prefs/templates/prefs_language.html:15
+#: trac/prefs/templates/prefs_language.html:25
 msgid "Language:"
 msgstr "言語:"
 
-#: trac/prefs/templates/prefs_language.html:17
+#: trac/prefs/templates/prefs_language.html:28
 msgid "Default language"
 msgstr "デフォルトの言語"
 
-#: trac/prefs/templates/prefs_language.html:23
+#: trac/prefs/templates/prefs_language.html:42
 msgid ""
 "Configuring your language will result in all text\n"
-"      displayed on this site to use your language instead of that of the\n"
-"      server."
+"        displayed on this site to use your language instead of that of "
+"the\n"
+"        server."
 msgstr "この設定を行うことで、このサイトを選択した言語で表示することが可能です。"
 
-#: trac/prefs/templates/prefs_language.html:27
+#: trac/prefs/templates/prefs_language.html:46
 msgid ""
 "The [1:Default language] option uses the browser's\n"
 "        language negotiation feature to select the appropriate language."
 msgstr "[1:デフォルトの言語]を選択すれば、ブラウザの情報から適切な言語を取得します。"
 
-#: trac/prefs/templates/prefs_pygments.html:37
+#: trac/prefs/templates/prefs_pygments.html:47
 msgid ""
 "The Pygments syntax highlighter can be used with\n"
 "      different coloring styles."
 msgstr "Pygments シンタックスハイライトをいろいろな配色スタイルで行います。"
 
-#: trac/prefs/templates/prefs_pygments.html:39
+#: trac/prefs/templates/prefs_pygments.html:49
 msgid "Style:"
 msgstr "スタイル:"
 
-#: trac/prefs/templates/prefs_pygments.html:44
+#: trac/prefs/templates/prefs_pygments.html:54
 msgid "Preview:"
 msgstr "表示例:"
 
-#: trac/prefs/templates/prefs_userinterface.html:18
+#: trac/prefs/templates/prefs_userinterface.html:28
 msgid "Use only symbols for buttons."
 msgstr "ボタンに対してシンボルだけを使う。"
 
-#: trac/prefs/templates/prefs_userinterface.html:21
+#: trac/prefs/templates/prefs_userinterface.html:31
 msgid ""
 "Display only the icon or symbol for\n"
 "      short inline buttons, and hide the text caption."
 msgstr "小さなボタンに対し、アイコンないしシンボルのみとしテキストを表示しません。"
 
-#: trac/prefs/templates/prefs_userinterface.html:29
+#: trac/prefs/templates/prefs_userinterface.html:39
 msgid "Hide help links."
 msgstr "ヘルプを表示しない。"
 
-#: trac/prefs/templates/prefs_userinterface.html:32
+#: trac/prefs/templates/prefs_userinterface.html:42
 msgid ""
 "Don't show the various help links.\n"
 "      This reduces the verbosity of the pages."
 msgstr "ヘルプを非表示とし、ページが冗長ではなくなります。"
 
-#: trac/search/web_ui.py:72 trac/search/templates/search.html:12
-#: trac/search/templates/search.html:26 trac/search/templates/search.html:31
-#: trac/templates/theme.html:29
+#: trac/search/web_ui.py:71 trac/search/templates/search.html:22
+#: trac/search/templates/search.html:33 trac/search/templates/search.html:38
+#: trac/templates/theme.html:39
 msgid "Search"
 msgstr "検索"
 
-#: trac/search/web_ui.py:166
+#: trac/search/web_ui.py:165
 #, python-format
 msgid "Browse repository path %(path)s"
 msgstr "リポジトリパス %(path)s の閲覧"
 
-#: trac/search/web_ui.py:206
+#: trac/search/web_ui.py:205
 #, python-format
 msgid "Search query too short. Query must be at least %(num)s characters long."
 msgstr "検索語は少なくとも%(num)s文字以上が必要です。"
 
-#: trac/search/web_ui.py:245 trac/ticket/query.py:785 trac/ticket/report.py:459
+#: trac/search/web_ui.py:231 trac/ticket/query.py:812 trac/ticket/report.py:458
+#, python-format
+msgid "Page %(num)d"
+msgstr "ページ%(num)d"
+
+#: trac/search/web_ui.py:244 trac/ticket/query.py:799 trac/ticket/report.py:449
 msgid "Next Page"
 msgstr "次のページ"
 
-#: trac/search/web_ui.py:251 trac/ticket/query.py:790 trac/ticket/report.py:462
+#: trac/search/web_ui.py:250 trac/ticket/query.py:804 trac/ticket/report.py:452
 msgid "Previous Page"
 msgstr "前のページ"
 
-#: trac/search/templates/search.html:11
+#: trac/search/templates/search.html:21
 msgid "Search Results"
 msgstr "検索結果"
 
-#: trac/search/templates/search.html:43
-#: trac/ticket/templates/query_results.html:20
-#: trac/ticket/templates/report_view.html:78
+#: trac/search/templates/search.html:50
+#: trac/ticket/templates/query_results.html:29
+#: trac/ticket/templates/report_view.html:88
 msgid "Results"
 msgstr "結果"
 
-#: trac/search/templates/search.html:51
+#: trac/search/templates/search.html:58
 #, python-format
 msgid "Quickjump to %(name)s"
 msgstr "%(name)s へクイックジャンプ"
 
-#: trac/search/templates/search.html:59
+#: trac/search/templates/search.html:66
 #, python-format
 msgid "By %(author)s"
 msgstr "更新者 %(author)s"
 
-#: trac/search/templates/search.html:68
-#: trac/ticket/templates/report_view.html:97
-#: trac/ticket/templates/report_view.html:208
+#: trac/search/templates/search.html:75
+#: trac/ticket/templates/report_view.html:107
+#: trac/ticket/templates/report_view.html:218
 msgid "No matches found."
 msgstr "一致するものが見つかりませんでした。"
 
-#: trac/search/templates/search.html:72
+#: trac/search/templates/search.html:79
 msgid ""
 "[1:Note:] See [2:TracSearch]\n"
 "        for help on searching."
@@ -2028,11 +2022,11 @@
 "[1:※] 詳しい使い方は\n"
 "[2:TracSearch] を参照してください。"
 
-#: trac/templates/about.html:26
+#: trac/templates/about.html:38
 msgid "Trac: Integrated SCM & Project Management"
 msgstr "Trac: Integrated SCM & Project Management"
 
-#: trac/templates/about.html:30
+#: trac/templates/about.html:42
 msgid ""
 "Trac is a web-based software project management and bug/issue\n"
 "        tracking system emphasizing ease of use and low ceremony.\n"
@@ -2046,7 +2040,7 @@
 "Wiki 機能を備え、バージョン管理システムへのインターフェースを持ち、\n"
 "プロジェクト内での出来事や変更を把握するのに役立つたくさんの方法を提供します。"
 
-#: trac/templates/about.html:36
+#: trac/templates/about.html:48
 msgid ""
 "Trac is distributed under the modified BSD License.[1:]\n"
 "        The complete text of the license can be found\n"
@@ -2056,11 +2050,11 @@
 "Trac は BSD ライセンスのもとで配布されています。[1:]\n"
 "このライセンスの全文は、配布ファイルに含まれている [3:COPYING] ファイルと同じものが[2:オンライン]で参照できます。"
 
-#: trac/templates/about.html:41
+#: trac/templates/about.html:53
 msgid "python powered"
 msgstr "python powered"
 
-#: trac/templates/about.html:44
+#: trac/templates/about.html:56
 msgid ""
 "Please visit the Trac open source project:\n"
 "        [1:http://trac.edgewall.org/]"
@@ -2068,7 +2062,7 @@
 "Trac オープンソースプロジェクトのページへ:\n"
 "        [1:http://trac.edgewall.org/]"
 
-#: trac/templates/about.html:46
+#: trac/templates/about.html:58
 msgid ""
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
@@ -2076,110 +2070,111 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
-#: trac/templates/about.html:54
+#: trac/templates/about.html:66
 msgid "System Information"
 msgstr "システム情報"
 
-#: trac/templates/about.html:56
+#: trac/templates/about.html:68
 msgid "Package"
 msgstr "パッケージ"
 
-#: trac/templates/about.html:56 trac/templates/about.html:69
-#: trac/templates/history_view.html:28 trac/ticket/admin.py:431
-#: trac/ticket/api.py:307
+#: trac/templates/about.html:68 trac/templates/about.html:81
+#: trac/templates/history_view.html:38 trac/ticket/admin.py:440
+#: trac/ticket/api.py:316
 msgid "Version"
 msgstr "バージョン"
 
-#: trac/templates/about.html:67
+#: trac/templates/about.html:79
 msgid "Installed Plugins"
 msgstr "インストールしているプラグイン"
 
-#: trac/templates/about.html:69
+#: trac/templates/about.html:81
 msgid "Location"
 msgstr "場所"
 
-#: trac/templates/about.html:77 trac/templates/error.html:192
-#: trac/web/main.py:589
+#: trac/templates/about.html:89 trac/templates/error.html:211
+#: trac/web/main.py:583
 msgid "N/A"
 msgstr "該当なし"
 
-#: trac/templates/about.html:89
+#: trac/templates/about.html:101
 msgid "Section"
 msgstr "セクション"
 
-#: trac/templates/about.html:91 trac/templates/error.html:160
+#: trac/templates/about.html:103 trac/templates/error.html:179
 msgid "Value"
 msgstr "値"
 
-#: trac/templates/attach_file_form.html:15
+#: trac/templates/attach_file_form.html:24
+msgid "Attach another file"
+msgstr "ファイルを添付"
+
+#: trac/templates/attach_file_form.html:24
 msgid "Attach file"
 msgstr "ファイルを添付"
 
-#: trac/templates/attachment.html:12
+#: trac/templates/attachment.html:22
 msgid "– Attachment"
 msgstr "– 添付ファイル"
 
-#: trac/templates/attachment.html:13
+#: trac/templates/attachment.html:23
 msgid "– Attachments"
 msgstr "– 添付ファイル"
 
-#: trac/templates/attachment.html:14
+#: trac/templates/attachment.html:24
 #, python-format
 msgid "%(filename)s on %(parent)s – Attachment"
 msgstr "%(parent)s の %(filename)s – 添付ファイル"
 
-#: trac/templates/attachment.html:29
+#: trac/templates/attachment.html:39
 #, python-format
 msgid "Add Attachment to [1:%(parent)s]"
 msgstr "[1:%(parent)s] に添付ファイルを追加"
 
-#: trac/templates/attachment.html:33
+#: trac/templates/attachment.html:43
 #, python-format
 msgid "(size limit %(value)s)"
 msgstr "(サイズの上限 %(value)s)"
 
-#: trac/templates/attachment.html:37
+#: trac/templates/attachment.html:47
 msgid "Attachment Info"
 msgstr "添付ファイル情報"
 
-#: trac/templates/attachment.html:40 trac/ticket/templates/ticket.html:355
-#: trac/wiki/templates/wiki_edit_form.html:42
+#: trac/templates/attachment.html:50 trac/ticket/templates/ticket.html:358
+#: trac/wiki/templates/wiki_edit_form.html:53
 msgid "Your email or username:"
 msgstr "メールアドレスまたはユーザ名:"
 
-#: trac/templates/attachment.html:46
+#: trac/templates/attachment.html:56
 msgid "Description of the file (optional):"
 msgstr "ファイルの詳細(省略可):"
 
-#: trac/templates/attachment.html:52
+#: trac/templates/attachment.html:62
 msgid "Replace existing attachment of the same name"
 msgstr "同名の添付ファイルを置き換える"
 
-#: trac/templates/attachment.html:62
+#: trac/templates/attachment.html:72
 msgid "Add attachment"
 msgstr "添付ファイルを追加"
 
-#: trac/templates/attachment.html:70
+#: trac/templates/attachment.html:80
 msgid "Are you sure you want to delete this attachment?"
 msgstr "この添付ファイルを削除しますか?"
 
-#: trac/templates/attachment.html:77 trac/templates/attachment.html:119
+#: trac/templates/attachment.html:86 trac/templates/attachment.html:128
 msgid "Delete attachment"
 msgstr "添付ファイルの削除"
 
-#: trac/templates/attachment.html:86
-msgid "Attach another file"
-msgstr "ファイルを添付"
-
-#: trac/templates/attachment.html:98 trac/templates/list_of_attachments.html:21
-#: trac/templates/macros.html:19 trac/util/text.py:621
-#: trac/versioncontrol/templates/browser.html:189
-#: trac/versioncontrol/templates/dir_entries.html:17
+#: trac/templates/attachment.html:107
+#: trac/templates/list_of_attachments.html:30 trac/templates/macros.html:29
+#: trac/util/text.py:633 trac/util/tests/html.py:215
+#: trac/util/tests/html.py:228 trac/versioncontrol/templates/browser.html:199
+#: trac/versioncontrol/templates/dir_entries.html:28
 #, python-format
 msgid "%(size)s bytes"
-msgstr "%(size)s バイト"
+msgstr "%(size)s B"
 
-#: trac/templates/attachment.html:96
+#: trac/templates/attachment.html:105
 #, python-format
 msgid ""
 "File %(file)s,\n"
@@ -2190,42 +2185,42 @@
 "                [1:%(size)s]\n"
 "                (%(author)s が%(date)sに追加)"
 
-#: trac/templates/diff_div.html:72
+#: trac/templates/diff_div.html:81
 #, python-format
 msgid ""
 "Property %(name)s\n"
 "                  changed from %(old)s to %(new)s"
 msgstr "プロパティ %(name)s を %(old)s から %(new)s に変更"
 
-#: trac/templates/diff_div.html:76
+#: trac/templates/diff_div.html:85
 #, python-format
 msgid "Property %(name)s set to %(value)s"
 msgstr "プロパティ %(name)s を %(value)s に設定"
 
-#: trac/templates/diff_div.html:79
+#: trac/templates/diff_div.html:88
 #, python-format
 msgid "Property %(name)s deleted"
 msgstr "プロパティ %(name)s を削除"
 
-#: trac/templates/diff_div.html:86
+#: trac/templates/diff_div.html:95
 msgid "Differences"
 msgstr "差分"
 
-#: trac/templates/diff_options.html:10
-#: trac/versioncontrol/templates/browser.html:138
-#: trac/versioncontrol/templates/browser.html:146
+#: trac/templates/diff_options.html:21
+#: trac/versioncontrol/templates/browser.html:148
+#: trac/versioncontrol/templates/browser.html:156
 msgid "View differences"
-msgstr "更新内容の表示方法"
+msgstr "差分の表示"
 
-#: trac/templates/diff_options.html:13
+#: trac/templates/diff_options.html:24
 msgid "inline"
 msgstr "インラインで表示"
 
-#: trac/templates/diff_options.html:15
+#: trac/templates/diff_options.html:26
 msgid "side by side"
 msgstr "並べて表示"
 
-#: trac/templates/diff_options.html:18
+#: trac/templates/diff_options.html:29
 msgid ""
 "[1:[2:]\n"
 "             Show]\n"
@@ -2237,37 +2232,37 @@
 "      [3:[4:]\n"
 "             行を表示]"
 
-#: trac/templates/diff_options.html:28
+#: trac/templates/diff_options.html:39
 msgid "Show the changes in full context"
 msgstr "全体と合わせて変更を見る"
 
-#: trac/templates/diff_options.html:32
+#: trac/templates/diff_options.html:43
 msgid "Ignore:"
 msgstr "以下の違いを無視:"
 
-#: trac/templates/diff_options.html:36
+#: trac/templates/diff_options.html:47
 msgid "Blank lines"
 msgstr "空白行の有無"
 
-#: trac/templates/diff_options.html:41
+#: trac/templates/diff_options.html:52
 msgid "Case changes"
 msgstr "大文字/小文字の区別"
 
-#: trac/templates/diff_options.html:46
+#: trac/templates/diff_options.html:57
 msgid "White space changes"
 msgstr "空白文字の変更"
 
-#: trac/templates/diff_options.html:50
-#: trac/ticket/templates/milestone_view.html:57
-#: trac/ticket/templates/query.html:220
-#: trac/ticket/templates/report_view.html:49
-#: trac/ticket/templates/roadmap.html:28
-#: trac/timeline/templates/timeline.html:36
-#: trac/versioncontrol/templates/revisionlog.html:80
+#: trac/templates/diff_options.html:61
+#: trac/ticket/templates/milestone_view.html:67
+#: trac/ticket/templates/query.html:230
+#: trac/ticket/templates/report_view.html:59
+#: trac/ticket/templates/roadmap.html:38
+#: trac/timeline/templates/timeline.html:46
+#: trac/versioncontrol/templates/revisionlog.html:90
 msgid "Update"
 msgstr "更新"
 
-#: trac/templates/diff_view.html:18
+#: trac/templates/diff_view.html:28
 #, python-format
 msgid ""
 "Changes between\n"
@@ -2276,7 +2271,7 @@
 "          [3:%(name)s]"
 msgstr "[3:%(name)s] の[1:バージョン %(old)s] と[2:バージョン %(new)s] との変更"
 
-#: trac/templates/diff_view.html:23
+#: trac/templates/diff_view.html:33
 #, python-format
 msgid ""
 "Changes between\n"
@@ -2285,7 +2280,7 @@
 "          [3:%(name)s]"
 msgstr "[3:%(name)s] の[1:初期バージョン]と[2:バージョン %(new)s] との変更"
 
-#: trac/templates/diff_view.html:28
+#: trac/templates/diff_view.html:38
 #, python-format
 msgid ""
 "Changes from\n"
@@ -2293,112 +2288,113 @@
 "          [2:%(name)s]"
 msgstr "[2:%(name)s] の[1:バージョン %(new)s] との変更"
 
-#: trac/templates/diff_view.html:43
-#: trac/versioncontrol/templates/changeset.html:136
+#: trac/templates/diff_view.html:53
+#: trac/versioncontrol/templates/changeset.html:146
 msgid "Timestamp:"
 msgstr "日時:"
 
-#: trac/templates/diff_view.html:45 trac/templates/diff_view.html:53
-#: trac/templates/diff_view.html:59
+#: trac/templates/diff_view.html:55 trac/templates/diff_view.html:63
+#: trac/templates/diff_view.html:69
 msgid "(multiple changes)"
 msgstr "(複数の変更)"
 
-#: trac/templates/diff_view.html:47
+#: trac/templates/diff_view.html:57
 #, python-format
 msgid "%(date)s (%(duration)s ago)"
 msgstr "%(date)s (%(duration)s前)"
 
-#: trac/templates/diff_view.html:55
+#: trac/templates/diff_view.html:65
 #, python-format
 msgid "(IP: %(ipnr)s)"
 msgstr "(IP: %(ipnr)s)"
 
-#: trac/templates/diff_view.html:57 trac/ticket/templates/batch_modify.html:12
+#: trac/templates/diff_view.html:67 trac/ticket/templates/batch_modify.html:22
 #: trac/ticket/templates/batch_ticket_notify_email.txt:9
+#: trac/ticket/templates/milestone_edit.html:107
 #: trac/ticket/templates/ticket_notify_email.txt:21
 msgid "Comment:"
 msgstr "コメント:"
 
-#: trac/templates/diff_view.html:65
-#: trac/versioncontrol/templates/changeset.html:198
-#: trac/versioncontrol/templates/revisionlog.html:86
+#: trac/templates/diff_view.html:75
+#: trac/versioncontrol/templates/changeset.html:208
+#: trac/versioncontrol/templates/revisionlog.html:96
 msgid "Legend:"
 msgstr "凡例:"
 
-#: trac/templates/diff_view.html:67
-#: trac/versioncontrol/templates/changeset.html:200
+#: trac/templates/diff_view.html:77
+#: trac/versioncontrol/templates/changeset.html:210
 msgid "Unmodified"
-msgstr "未変更"
+msgstr "変更なし"
 
-#: trac/templates/diff_view.html:68
-#: trac/versioncontrol/templates/changeset.html:201
-#: trac/versioncontrol/templates/revisionlog.html:88
+#: trac/templates/diff_view.html:78
+#: trac/versioncontrol/templates/changeset.html:211
+#: trac/versioncontrol/templates/revisionlog.html:98
 msgid "Added"
 msgstr "追加"
 
-#: trac/templates/diff_view.html:69
-#: trac/versioncontrol/templates/changeset.html:202
-#: trac/versioncontrol/templates/revisionlog.html:90
+#: trac/templates/diff_view.html:79
+#: trac/versioncontrol/templates/changeset.html:212
+#: trac/versioncontrol/templates/revisionlog.html:100
 msgid "Removed"
 msgstr "削除"
 
-#: trac/templates/diff_view.html:70 trac/ticket/api.py:336
-#: trac/versioncontrol/templates/changeset.html:204
-#: trac/versioncontrol/templates/revisionlog.html:92 trac/wiki/admin.py:197
+#: trac/templates/diff_view.html:80 trac/ticket/api.py:345
+#: trac/versioncontrol/templates/changeset.html:214
+#: trac/versioncontrol/templates/revisionlog.html:102 trac/wiki/admin.py:197
 msgid "Modified"
 msgstr "更新"
 
-#: trac/templates/error.html:10 trac/templates/index.html:18
-#: trac/web/main.py:516
+#: trac/templates/error.html:20 trac/templates/index.html:28
+#: trac/web/api.py:165
 msgid "Error"
 msgstr "エラー"
 
-#: trac/templates/error.html:65
+#: trac/templates/error.html:80
 msgid "Create"
 msgstr "作成"
 
-#: trac/templates/error.html:80
+#: trac/templates/error.html:95
 msgid "Oops…"
 msgstr "エラー発生"
 
-#: trac/templates/error.html:82
+#: trac/templates/error.html:97
 msgid "Trac detected an internal error:"
 msgstr "内部エラーを検出しました。"
 
-#: trac/templates/error.html:87
+#: trac/templates/error.html:102
 msgid ""
 "There was an internal error in Trac.\n"
 "                It is recommended that you notify your local\n"
 "                [1:\n"
 "                    Trac administrator] with the information needed to\n"
 "                reproduce the issue."
-msgstr "内部エラーが発生しました。サイトの [1:Trac 管理者]に連絡し、不具合の再現方法等を伝えることをお勧めします。"
+msgstr "内部エラーが発生しました。このサイトの [1:Trac 管理者]に連絡し、不具合の再現方法等を伝えることをお勧めします。"
 
-#: trac/templates/error.html:95
+#: trac/templates/error.html:110
 #, python-format
 msgid "To that end, you could %(create)s a ticket."
-msgstr "これを受けて、チケットを %(create)s します。"
+msgstr "それにはチケットを %(create)s します。"
 
-#: trac/templates/error.html:97
+#: trac/templates/error.html:112
 msgid "The action that triggered the error was:"
 msgstr "エラーを発生させた操作:"
 
-#: trac/templates/error.html:102
+#: trac/templates/error.html:116 trac/templates/error.html:121
 msgid "This is probably a local installation issue."
 msgstr "おそらくローカルインストールに問題があります。"
 
-#: trac/templates/error.html:103
+#: trac/templates/error.html:122
 #, python-format
 msgid ""
 "You should %(create)s a ticket at the admin Trac to report\n"
 "                    the issue."
-msgstr "Trac プロジェクトページでこの問題のチケットを %(create)s してください。"
+msgstr "管理用の Trac でこの問題についてチケットを %(create)s してください。"
 
-#: trac/templates/error.html:109
+#: trac/templates/error.html:128
 msgid "Found a bug in Trac?"
 msgstr "Trac のバグを見つけましたか?"
 
-#: trac/templates/error.html:110
+#: trac/templates/error.html:129
 msgid ""
 "If you think this should work and you can reproduce the problem,\n"
 "              you should consider creating a bug report."
@@ -2406,20 +2402,20 @@
 "この不具合が再現可能でありきちんと動作すべきものであるようならば、\n"
 "Trac チームに報告することを検討してください。"
 
-#: trac/templates/error.html:113
+#: trac/templates/error.html:132
 #, python-format
 msgid "Note that the %(name)s plugin seems to be involved."
 msgstr "%(name)s プラグインに関係しているようです。"
 
-#: trac/templates/error.html:116
+#: trac/templates/error.html:135
 msgid "Note that the following plugins seem to be involved:"
 msgstr "次のプラグインに関係しているようです。"
 
-#: trac/templates/error.html:120
+#: trac/templates/error.html:139
 msgid "Please report this issue to the plugin maintainer."
 msgstr "プラグインのメンテナにこの問題を報告してください。"
 
-#: trac/templates/error.html:122
+#: trac/templates/error.html:141
 msgid ""
 "Before you do that, though, please first try\n"
 "                [1:[2:searching]\n"
@@ -2436,22 +2432,22 @@
 "また Trac のインストールや設定に関する質問は、\n"
 "チケットを作成するのではなく[3:メーリングリスト]に投稿するのがよいでしょう。"
 
-#: trac/templates/error.html:131
+#: trac/templates/error.html:150
 #, python-format
 msgid ""
 "Otherwise, please %(create)s a new bug report\n"
 "                describing the problem and explain how to reproduce it."
 msgstr "そうでなければ、Trac プロジェクトのサイトにて不具合の詳細と再現手順を書き添えた新しいチケットを %(create)s してください。"
 
-#: trac/templates/error.html:135
+#: trac/templates/error.html:154
 msgid "Python Traceback"
 msgstr "Python トレースバック"
 
-#: trac/templates/error.html:136
+#: trac/templates/error.html:155
 msgid "Most recent call last:"
 msgstr "後方のものほど最近の呼出となります:"
 
-#: trac/templates/error.html:140
+#: trac/templates/error.html:159
 #, python-format
 msgid ""
 "[1:File \"%(file)s\",\n"
@@ -2462,86 +2458,86 @@
 "                        行 [2:%(line)s], 関数]\n"
 "                        [3:%(function)s]"
 
-#: trac/templates/error.html:146
+#: trac/templates/error.html:165
 msgid "Code fragment:"
 msgstr "近辺のコード:"
 
-#: trac/templates/error.html:158
+#: trac/templates/error.html:177
 msgid "Local variables:"
 msgstr "ローカル変数:"
 
-#: trac/templates/error.html:172
+#: trac/templates/error.html:191
 #, python-format
 msgid "File \"%(file)s\", line %(line)s, in %(function)s"
 msgstr "ファイル \"%(file)s\"、行 %(line)s、関数 %(function)s"
 
-#: trac/templates/error.html:175
+#: trac/templates/error.html:194
 msgid "Switch to plain text view"
 msgstr "プレーンテキストで表示"
 
-#: trac/templates/error.html:178
+#: trac/templates/error.html:197
 msgid "System Information:"
 msgstr "システム情報:"
 
-#: trac/templates/error.html:186
+#: trac/templates/error.html:205
 msgid "Enabled Plugins:"
 msgstr "有効にしているプラグイン:"
 
-#: trac/templates/error.html:202
+#: trac/templates/error.html:221
 msgid "TracGuide"
 msgstr "TracGuide"
 
-#: trac/templates/error.html:202
+#: trac/templates/error.html:221
 msgid "— The Trac User and Administration Guide"
 msgstr "— Trac ユーザ/管理者ガイド"
 
-#: trac/templates/history_view.html:16
+#: trac/templates/history_view.html:26
 #, python-format
 msgid "Change History for [1:%(name)s]"
 msgstr "[1:%(name)s] の更新履歴"
 
-#: trac/templates/history_view.html:22 trac/templates/history_view.html:55
-#: trac/versioncontrol/templates/diff_form.html:58
-#: trac/versioncontrol/templates/revisionlog.html:101
-#: trac/versioncontrol/templates/revisionlog.html:204
+#: trac/templates/history_view.html:32 trac/templates/history_view.html:65
+#: trac/versioncontrol/templates/diff_form.html:68
+#: trac/versioncontrol/templates/revisionlog.html:111
+#: trac/versioncontrol/templates/revisionlog.html:214
 msgid "View changes"
 msgstr "差分を表示"
 
-#: trac/templates/history_view.html:24
+#: trac/templates/history_view.html:34
 msgid "Change history"
 msgstr "更新履歴"
 
-#: trac/templates/history_view.html:31
+#: trac/templates/history_view.html:41
 msgid "Comment"
 msgstr "コメント"
 
-#: trac/templates/history_view.html:43
+#: trac/templates/history_view.html:53
 msgid "View this version"
 msgstr "このバージョンを表示"
 
-#: trac/templates/history_view.html:46
+#: trac/templates/history_view.html:56
 #, python-format
 msgid "IP-Address: %(ipnr)s"
 msgstr "IP アドレス: %(ipnr)s"
 
-#: trac/templates/index.html:8 trac/templates/index.html:12
+#: trac/templates/index.html:18 trac/templates/index.html:22
 msgid "Available Projects"
 msgstr "プロジェクト一覧"
 
-#: trac/templates/layout.html:28
+#: trac/templates/layout.html:38
 #, python-format
 msgid "Search %(project)s"
 msgstr "%(project)s の検索"
 
-#: trac/templates/layout.html:69
+#: trac/templates/layout.html:85
 msgid "Download in other formats:"
 msgstr "他のフォーマットでダウンロード:"
 
-#: trac/templates/list_of_attachments.html:19
+#: trac/templates/list_of_attachments.html:28
 msgid "View attachment"
 msgstr "添付ファイルを表示"
 
-#: trac/templates/list_of_attachments.html:18
+#: trac/templates/list_of_attachments.html:27
 #, python-format
 msgid ""
 "[1:%(file)s][2:​]\n"
@@ -2552,93 +2548,94 @@
 "       ([3:%(size)s]) -\n"
 "      [4:%(author)s] が%(date)sに追加。"
 
-#: trac/templates/list_of_attachments.html:28
-#: trac/templates/list_of_attachments.html:44
-#: trac/ticket/templates/ticket.html:378
+#: trac/templates/list_of_attachments.html:37
+#: trac/templates/list_of_attachments.html:53
+#: trac/ticket/templates/ticket.html:381
 msgid "Attachments"
 msgstr "添付ファイル"
 
-#: trac/templates/list_of_attachments.html:38
-#: trac/templates/list_of_attachments.html:54
+#: trac/templates/list_of_attachments.html:47
+#: trac/templates/list_of_attachments.html:63
 msgid "Download all attachments as:"
 msgstr "すべての添付ファイルをダウンロード:"
 
-#: trac/templates/list_of_attachments.html:39
-#: trac/templates/list_of_attachments.html:55
+#: trac/templates/list_of_attachments.html:48
+#: trac/templates/list_of_attachments.html:64
 msgid ".zip"
 msgstr ".zip"
 
-#: trac/templates/macros.html:37 trac/templates/macros.html:38
+#: trac/templates/macros.html:47 trac/templates/macros.html:48
 msgid "Previous"
 msgstr "前の"
 
-#: trac/templates/macros.html:47 trac/templates/macros.html:48
+#: trac/templates/macros.html:57 trac/templates/macros.html:58
 msgid "Next"
 msgstr "次の"
 
-#: trac/templates/preview_file.html:15
+#: trac/templates/preview_file.html:24
 msgid "(The file is empty)"
 msgstr "(ファイルは空です)"
 
-#: trac/templates/preview_file.html:19
+#: trac/templates/preview_file.html:28
 #, python-format
 msgid ""
 "[1:HTML preview not available], since the file size exceeds %(size)s "
 "bytes."
 msgstr "ファイルサイズが %(size)s バイトを超えているため、[1:HTML プレビューは利用できません]。"
 
-#: trac/templates/preview_file.html:22
+#: trac/templates/preview_file.html:31
 msgid "[1:HTML preview not available], since no preview renderer could handle it."
 msgstr "このファイルのプレビュー方法が不明なため、[1:HTML プレビューは利用できません]。"
 
-#: trac/templates/preview_file.html:26
+#: trac/templates/preview_file.html:35
 msgid "Try [1:downloading] the file instead."
 msgstr "代わりにファイルを[1:ダウンロード]してください。"
 
-#: trac/templates/progress_bar.html:26
+#: trac/templates/progress_bar.html:35
 #, python-format
 msgid "%(count)s/%(total)s %(title)s"
 msgstr "%(count)s/%(total)s %(title)s"
 
-#: trac/templates/progress_bar.html:37
+#: trac/templates/progress_bar.html:46
 #, python-format
 msgid "Total number of %(unit)s: %(count)s"
 msgstr "%(unit)s の件数: %(count)s"
 
-#: trac/templates/progress_bar.html:41
+#: trac/templates/progress_bar.html:50
 #, python-format
 msgid "%(title)s: %(count)s"
 msgstr "%(title)s: %(count)s"
 
-#: trac/templates/progress_bar_grouped.html:17
+#: trac/templates/progress_bar_grouped.html:26
+#: trac/ticket/default_workflow.py:233
 msgid "(none)"
 msgstr "(無し)"
 
-#: trac/templates/theme.html:27
+#: trac/templates/theme.html:37
 msgid "Search:"
 msgstr "検索:"
 
-#: trac/templates/theme.html:41
+#: trac/templates/theme.html:51
 msgid "Context Navigation"
 msgstr "コンテキストナビゲーション"
 
-#: trac/templates/theme.html:50
+#: trac/templates/theme.html:60
 msgid "Hide this warning"
 msgstr "この警告を非表示にする"
 
-#: trac/templates/theme.html:50 trac/templates/theme.html:58
+#: trac/templates/theme.html:60 trac/templates/theme.html:68
 msgid "close"
 msgstr "閉じる"
 
-#: trac/templates/theme.html:52
+#: trac/templates/theme.html:62
 msgid "Warning:"
 msgstr "警告:"
 
-#: trac/templates/theme.html:58
+#: trac/templates/theme.html:68
 msgid "Hide this notice"
 msgstr "この通知を非表示にする"
 
-#: trac/templates/theme.html:72
+#: trac/templates/theme.html:82
 #, python-format
 msgid ""
 "Powered by [1:[2:Trac %(version)s]][3:]\n"
@@ -2647,14 +2644,18 @@
 "Powered by [1:[2:Trac %(version)s]][3:]\n"
 "        By [4:Edgewall Software]."
 
-#: trac/ticket/admin.py:37
+#: trac/ticket/admin.py:39
 msgid "(Undefined)"
 msgstr "(不明)"
 
-#: trac/ticket/admin.py:48
+#: trac/ticket/admin.py:49
 msgid "Ticket System"
 msgstr "チケットシステム"
 
+#: trac/ticket/admin.py:77 trac/ticket/templates/admin_components.html:20
+msgid "Components"
+msgstr "コンポーネント"
+
 #: trac/ticket/admin.py:93
 #, python-format
 msgid "The component \"%(name)s\" already exists."
@@ -2665,7 +2666,7 @@
 msgid "The component \"%(name)s\" has been added."
 msgstr "コンポーネント \"%(name)s\" を追加しました。"
 
-#: trac/ticket/admin.py:122 trac/ticket/model.py:859 trac/ticket/model.py:878
+#: trac/ticket/admin.py:122 trac/ticket/model.py:871 trac/ticket/model.py:890
 msgid "Invalid component name."
 msgstr "コンポーネントの名称が不正です。"
 
@@ -2682,305 +2683,359 @@
 msgid "The selected components have been removed."
 msgstr "選択したコンポーネントを削除しました。"
 
-#: trac/ticket/admin.py:235 trac/ticket/api.py:305
+#: trac/ticket/admin.py:210 trac/ticket/api.py:302 trac/ticket/web_ui.py:1468
+#: trac/ticket/templates/admin_components.html:88
+msgid "Owner"
+msgstr "担当者"
+
+#: trac/ticket/admin.py:235 trac/ticket/api.py:314
 msgid "Milestone"
 msgstr "マイルストーン"
 
-#: trac/ticket/admin.py:266 trac/ticket/roadmap.py:757
+#: trac/ticket/admin.py:235 trac/ticket/roadmap.py:1031
+#: trac/ticket/templates/admin_milestones.html:24
+msgid "Milestones"
+msgstr "マイルストーン"
+
+#: trac/ticket/admin.py:264 trac/ticket/roadmap.py:786
 msgid "Completion date may not be in the future"
 msgstr "完了日時に未来の日時を指定しようとしています"
 
-#: trac/ticket/admin.py:268
+#: trac/ticket/admin.py:266
 msgid "Invalid Completion Date"
 msgstr "完了日時が不正です"
 
-#: trac/ticket/admin.py:273
+#: trac/ticket/admin.py:271
 #, python-format
 msgid "The milestone \"%(name)s\" already exists."
 msgstr "マイルストーン \"%(name)s\" はすでに存在しています。"
 
-#: trac/ticket/admin.py:300
+#: trac/ticket/admin.py:298
 #, python-format
 msgid "The milestone \"%(name)s\" has been added."
 msgstr "マイルストーン \"%(name)s\" を追加しました。"
 
-#: trac/ticket/admin.py:305 trac/ticket/model.py:1038 trac/ticket/model.py:1060
+#: trac/ticket/admin.py:303 trac/ticket/model.py:1047 trac/ticket/model.py:1069
 msgid "Invalid milestone name."
 msgstr "マイルストーンの名称が不正です。"
 
-#: trac/ticket/admin.py:306
+#: trac/ticket/admin.py:304
 #, python-format
 msgid "Milestone %(name)s already exists."
 msgstr "マイルストーン %(name)s はすでに存在しています。"
 
-#: trac/ticket/admin.py:314
+#: trac/ticket/admin.py:312
 msgid "No milestone selected"
 msgstr "マイルストーンを選択していません"
 
-#: trac/ticket/admin.py:321
+#: trac/ticket/admin.py:319
 msgid "The selected milestones have been removed."
 msgstr "選択したマイルストーンを削除しました。"
 
-#: trac/ticket/admin.py:452
+#: trac/ticket/admin.py:404 trac/ticket/templates/admin_milestones.html:124
+msgid "Due"
+msgstr "期日"
+
+#: trac/ticket/admin.py:404 trac/ticket/templates/admin_milestones.html:124
+msgid "Completed"
+msgstr "完了日時"
+
+#: trac/ticket/admin.py:440 trac/ticket/templates/admin_versions.html:20
+msgid "Versions"
+msgstr "バージョン"
+
+#: trac/ticket/admin.py:461
 #, python-format
 msgid "The version \"%(name)s\" already exists."
 msgstr "バージョン \"%(name)s\" はすでに存在しています。"
 
-#: trac/ticket/admin.py:479
+#: trac/ticket/admin.py:488
 #, python-format
 msgid "The version \"%(name)s\" has been added."
 msgstr "バージョン \"%(name)s\" を追加しました。"
 
-#: trac/ticket/admin.py:484 trac/ticket/model.py:1165 trac/ticket/model.py:1183
+#: trac/ticket/admin.py:493 trac/ticket/model.py:1206 trac/ticket/model.py:1224
 msgid "Invalid version name."
 msgstr "バージョンの名称が不正です。"
 
-#: trac/ticket/admin.py:485
+#: trac/ticket/admin.py:494
 #, python-format
 msgid "Version %(name)s already exists."
 msgstr "バージョン %(name)s はすでに存在しています。"
 
-#: trac/ticket/admin.py:492
+#: trac/ticket/admin.py:501
 msgid "No version selected"
 msgstr "バージョンを選択していません"
 
-#: trac/ticket/admin.py:499
+#: trac/ticket/admin.py:508
 msgid "The selected versions have been removed."
 msgstr "選択したバージョンを削除しました。"
 
-#: trac/ticket/admin.py:559
+#: trac/ticket/admin.py:574
 msgid "Time"
 msgstr "日時"
 
-#: trac/ticket/admin.py:605 trac/ticket/admin.py:633
+#: trac/ticket/admin.py:624 trac/ticket/admin.py:652
 #, python-format
 msgid "%(type)s value \"%(name)s\" already exists"
 msgstr "%(type)s \"%(name)s\" はすでに存在しています。"
 
-#: trac/ticket/admin.py:625
+#: trac/ticket/admin.py:644
 #, python-format
 msgid "The %(field)s value \"%(name)s\" has been added."
 msgstr "%(field)s \"%(name)s\" を追加しました。"
 
-#: trac/ticket/admin.py:631
+#: trac/ticket/admin.py:650
 #, python-format
 msgid "Invalid %(type)s value."
 msgstr "%(type)s の名称が不正です。"
 
-#: trac/ticket/admin.py:640
+#: trac/ticket/admin.py:659
 #, python-format
 msgid "No %s selected"
 msgstr "%s を選択していません"
 
-#: trac/ticket/admin.py:646
+#: trac/ticket/admin.py:665
 #, python-format
 msgid "The selected %(field)s values have been removed."
 msgstr "選択した%(field)sを削除しました。"
 
-#: trac/ticket/admin.py:668
+#: trac/ticket/admin.py:687
 msgid ""
 "Error writing to trac.ini, make sure it is writable by the web server. "
 "The default value has not been saved."
 msgstr "trac.ini の書き込みエラー。ウェブサーバによる書き込みができるか確認してください。デフォルト値は保存されませんでした。"
 
-#: trac/ticket/admin.py:680
+#: trac/ticket/admin.py:699
 msgid "Order numbers must be unique"
 msgstr "順序数はユニークでなくてはなりません"
 
-#: trac/ticket/admin.py:741
+#: trac/ticket/admin.py:760
 msgid "Possible Values"
 msgstr "名称"
 
-#: trac/ticket/admin.py:758
+#: trac/ticket/admin.py:777
 #, python-format
 msgid "Invalid up/down value: %(value)s"
 msgstr "up/down 以外の値です: %(value)s"
 
-#: trac/ticket/admin.py:777 trac/ticket/api.py:304
+#: trac/ticket/admin.py:796 trac/ticket/api.py:313
 msgid "Priority"
 msgstr "優先度"
 
-#: trac/ticket/admin.py:777
+#: trac/ticket/admin.py:796
 msgid "Priorities"
 msgstr "優先度"
 
-#: trac/ticket/admin.py:783 trac/ticket/api.py:309
+#: trac/ticket/admin.py:802 trac/ticket/api.py:318
 msgid "Resolution"
 msgstr "解決方法"
 
-#: trac/ticket/admin.py:783
+#: trac/ticket/admin.py:802
 msgid "Resolutions"
 msgstr "解決方法"
 
-#: trac/ticket/admin.py:789 trac/ticket/api.py:308
+#: trac/ticket/admin.py:808 trac/ticket/api.py:317
 msgid "Severity"
 msgstr "重要度"
 
-#: trac/ticket/admin.py:789
+#: trac/ticket/admin.py:808
 msgid "Severities"
 msgstr "重要度"
 
-#: trac/ticket/admin.py:795
+#: trac/ticket/admin.py:814
 msgid "Ticket Type"
 msgstr "分類"
 
-#: trac/ticket/admin.py:795
+#: trac/ticket/admin.py:814
 msgid "Ticket Types"
 msgstr "分類"
 
-#: trac/ticket/admin.py:823
+#: trac/ticket/admin.py:842
 msgid "<number> must be a number"
 msgstr "<number> は数値の必要があります"
 
-#: trac/ticket/admin.py:826
+#: trac/ticket/admin.py:845
 #, python-format
 msgid "Ticket #%(num)s and all associated data removed."
 msgstr "チケット #%(num)s とそれに関するデータを削除しました。"
 
-#: trac/ticket/api.py:257
+#: trac/ticket/api.py:266
 msgid "Attachment"
 msgstr "添付ファイル"
 
-#: trac/ticket/api.py:287
+#: trac/ticket/api.py:296
 msgid "Summary"
 msgstr "概要"
 
-#: trac/ticket/api.py:289 trac/ticket/templates/ticket.html:351
+#: trac/ticket/api.py:298 trac/ticket/templates/ticket.html:354
 msgid "Reporter"
 msgstr "報告者"
 
-#: trac/ticket/api.py:302 trac/versioncontrol/admin.py:113
-#: trac/versioncontrol/templates/admin_repositories.html:125
+#: trac/ticket/api.py:311 trac/versioncontrol/admin.py:113
+#: trac/versioncontrol/templates/admin_repositories.html:138
 msgid "Type"
 msgstr "分類"
 
-#: trac/ticket/api.py:303
+#: trac/ticket/api.py:312
 msgid "Status"
 msgstr "ステータス"
 
-#: trac/ticket/api.py:328
+#: trac/ticket/api.py:337
 msgid "Keywords"
 msgstr "キーワード"
 
-#: trac/ticket/api.py:330
+#: trac/ticket/api.py:339
 msgid "Cc"
 msgstr "関係者"
 
-#: trac/ticket/api.py:334
+#: trac/ticket/api.py:343
 msgid "Created"
 msgstr "作成"
 
-#: trac/ticket/api.py:480
+#: trac/ticket/api.py:489
 #, python-format
 msgid "Tickets %(ranges)s"
 msgstr "チケット %(ranges)s"
 
-#: trac/ticket/api.py:504
+#: trac/ticket/api.py:516
+msgid "ticket comment does not exist"
+msgstr "チケットのコメントは存在しません"
+
+#: trac/ticket/api.py:523
+#, python-format
+msgid "Description for Ticket #%(id)s"
+msgstr "チケット #%(id)s の詳細"
+
+#: trac/ticket/api.py:526
 #, python-format
 msgid "Comment %(cnum)s for Ticket #%(id)s"
 msgstr "チケット #%(id)s のコメント %(cnum)s"
 
-#: trac/ticket/api.py:529
+#: trac/ticket/api.py:531
+#, python-format
+msgid "Comment %(cnum)s"
+msgstr "コメント %(cnum)s"
+
+#: trac/ticket/api.py:535
+msgid "no permission to view ticket"
+msgstr "チケットを参照する権限がありません"
+
+#: trac/ticket/api.py:538
+msgid "ticket does not exist"
+msgstr "チケットは存在しません"
+
+#: trac/ticket/api.py:558
 #, python-format
 msgid "Ticket #%(shortname)s"
 msgstr "チケット #%(shortname)s"
 
-#: trac/ticket/batch.py:95
+#: trac/ticket/batch.py:96
 msgid "add"
 msgstr "追加"
 
-#: trac/ticket/batch.py:96
+#: trac/ticket/batch.py:97
 msgid "remove"
 msgstr "削除"
 
-#: trac/ticket/batch.py:97
+#: trac/ticket/batch.py:98
 msgid "add / remove"
 msgstr "追加 / 削除"
 
-#: trac/ticket/batch.py:98
+#: trac/ticket/batch.py:99
 msgid "set to"
 msgstr "設定"
 
-#: trac/ticket/batch.py:180
+#: trac/ticket/batch.py:181 trac/ticket/roadmap.py:731
+#: trac/ticket/roadmap.py:820
 #, python-format
 msgid ""
 "The changes have been saved, but an error occurred while sending "
 "notifications: %(message)s"
 msgstr "変更をは保存しましたが、通知処理中にエラーが発生しました: %(message)s"
 
-#: trac/ticket/default_workflow.py:241
+#: trac/ticket/default_workflow.py:238
+msgid "from invalid state"
+msgstr "(不正な状態から)"
+
+#: trac/ticket/default_workflow.py:239
 msgid "Current state no longer exists"
 msgstr "現在のステータスは破棄されます"
 
-#: trac/ticket/default_workflow.py:243
+#: trac/ticket/default_workflow.py:241
 msgid "The ticket will be disowned"
 msgstr "担当を外します"
 
-#: trac/ticket/default_workflow.py:261 trac/ticket/default_workflow.py:280
+#: trac/ticket/default_workflow.py:259 trac/ticket/default_workflow.py:269
+#: trac/ticket/default_workflow.py:278
 #, python-format
 msgid "to %(owner)s"
 msgstr "(担当 %(owner)s)"
 
-#: trac/ticket/default_workflow.py:263
+#: trac/ticket/default_workflow.py:261
 #, python-format
-msgid "The owner will be changed from %(current_owner)s"
-msgstr "担当者を %(current_owner)s から変更します"
+msgid "The owner will be changed from %(current_owner)s to the specified user"
+msgstr "担当者を %(current_owner)s から選択したユーザに変更します"
 
 #: trac/ticket/default_workflow.py:271
 #, python-format
-msgid "to %(owner)s "
-msgstr "(担当 %(owner)s)"
-
-#: trac/ticket/default_workflow.py:273
-#, python-format
 msgid "The owner will be changed from %(current_owner)s to %(selected_owner)s"
 msgstr "担当者を %(current_owner)s から %(selected_owner)s に変更します"
 
-#: trac/ticket/default_workflow.py:283
+#: trac/ticket/default_workflow.py:281
 #, python-format
 msgid "The owner will be changed from %(current_owner)s to the selected user"
 msgstr "担当者を %(current_owner)s から選択したユーザに変更します"
 
-#: trac/ticket/default_workflow.py:288
+#: trac/ticket/default_workflow.py:289
 #, python-format
 msgid "The owner will be changed from %(current_owner)s to %(authname)s"
 msgstr "担当者を %(current_owner)s から %(authname)s に変更します"
 
-#: trac/ticket/default_workflow.py:298
+#: trac/ticket/default_workflow.py:297
 msgid ""
 "Your workflow attempts to set a resolution but none is defined "
 "(configuration issue, please contact your Trac admin)."
 msgstr "ワークフローによる解決の種類が正しく設定されていません。(設定の問題なので Trac 管理者へお問い合わせください)"
 
-#: trac/ticket/default_workflow.py:306 trac/ticket/default_workflow.py:316
+#: trac/ticket/default_workflow.py:305 trac/ticket/default_workflow.py:315
 #, python-format
 msgid "as %(resolution)s"
 msgstr "(解決方法 %(resolution)s)"
 
-#: trac/ticket/default_workflow.py:308
+#: trac/ticket/default_workflow.py:307
 #, python-format
 msgid "The resolution will be set to %(name)s"
 msgstr "解決方法を %(name)s に設定します"
 
-#: trac/ticket/default_workflow.py:319
+#: trac/ticket/default_workflow.py:318
 msgid "The resolution will be set"
 msgstr "解決方法を設定します"
 
-#: trac/ticket/default_workflow.py:321
+#: trac/ticket/default_workflow.py:320
 msgid "The resolution will be deleted"
 msgstr "解決方法を抹消します"
 
-#: trac/ticket/default_workflow.py:324
+#: trac/ticket/default_workflow.py:323
 #, python-format
-msgid "as %(status)s "
+msgid "as %(status)s"
 msgstr "(ステータス %(status)s)"
 
-#: trac/ticket/default_workflow.py:328
+#: trac/ticket/default_workflow.py:326
+#, python-format
+msgid "The owner will remain %(current_owner)s"
+msgstr "担当者を %(current_owner)s のままにする"
+
+#: trac/ticket/default_workflow.py:329
+msgid "The ticket will remain with no owner"
+msgstr "担当者なしのままにする"
+
+#: trac/ticket/default_workflow.py:332
 #, python-format
 msgid "Next status will be '%(name)s'"
 msgstr "次のステータスは '%(name)s' です"
 
-#: trac/ticket/default_workflow.py:418
+#: trac/ticket/default_workflow.py:424
 msgid ""
 "Render a workflow graph.\n"
 "\n"
@@ -3060,7 +3115,7 @@
 "    }}}\n"
 "}}}"
 
-#: trac/ticket/default_workflow.py:493
+#: trac/ticket/default_workflow.py:499
 msgid "Enable JavaScript to display the workflow graph."
 msgstr "ワークフローグラフを表示するには JavaScript を有効にしてください。"
 
@@ -3077,150 +3132,145 @@
 msgid "Multi-values fields not supported yet"
 msgstr "複数値のフィールドはまだサポートしていません"
 
-#: trac/ticket/model.py:685
+#: trac/ticket/model.py:697
 #, python-format
 msgid "%(type)s %(name)s does not exist."
 msgstr "%(type)s %(name)s は存在しません。"
 
-#: trac/ticket/model.py:727 trac/ticket/model.py:752
+#: trac/ticket/model.py:739 trac/ticket/model.py:764
 #, python-format
 msgid "Invalid %(type)s name."
 msgstr "%(type)s の名称が不正です。"
 
-#: trac/ticket/model.py:831
+#: trac/ticket/model.py:843
 #, python-format
 msgid "Component %(name)s does not exist."
 msgstr "コンポーネント %(name)s は存在しません。 "
 
-#: trac/ticket/model.py:976
+#: trac/ticket/model.py:988 trac/ticket/model.py:1114
 #, python-format
 msgid "Milestone %(name)s does not exist."
 msgstr "マイルストーン %(name)s は存在しません。"
 
-#: trac/ticket/model.py:977
+#: trac/ticket/model.py:989 trac/ticket/model.py:1115
 msgid "Invalid milestone name"
 msgstr "マイルストーンの名称が不正です。"
 
-#: trac/ticket/model.py:1116
+#: trac/ticket/model.py:1157
 msgid "Open (by due date)"
 msgstr "進行中 (期日あり)"
 
-#: trac/ticket/model.py:1117
+#: trac/ticket/model.py:1158
 msgid "Open (no due date)"
 msgstr "進行中 (期日なし)"
 
-#: trac/ticket/model.py:1120
+#: trac/ticket/model.py:1161
 msgid "Closed"
 msgstr "完了済"
 
-#: trac/ticket/model.py:1137
+#: trac/ticket/model.py:1178
 #, python-format
 msgid "Version %(name)s does not exist."
 msgstr "バージョン %(name)s は存在しません。"
 
-#: trac/ticket/query.py:59
+#: trac/ticket/query.py:60
 msgid "Invalid query constraint value"
 msgstr "条件の値が不正です"
 
-#: trac/ticket/query.py:93
+#: trac/ticket/query.py:94
 #, python-format
 msgid "Query page %(page)s is invalid."
 msgstr "ページ番号 %(page)s は不正です。"
 
-#: trac/ticket/query.py:108
+#: trac/ticket/query.py:109
 #, python-format
 msgid "Query max %(max)s is invalid."
 msgstr "表示件数 %(max)s は不正です。"
 
-#: trac/ticket/query.py:167
+#: trac/ticket/query.py:168
 msgid "Query filter requires field and constraints separated by a \"=\""
 msgstr "各フィルタは、フィールドの名前と条件が \"=\" で区切る必要があります"
 
-#: trac/ticket/query.py:180
+#: trac/ticket/query.py:181
 msgid "Query filter requires field name"
 msgstr "フィルタには、フィールド名を含んでいる必要があります"
 
-#: trac/ticket/query.py:313
+#: trac/ticket/query.py:314
 #, python-format
 msgid "Page %(page)s is beyond the number of pages in the query"
 msgstr "%(page)s ページはクエリのページ数を超えています。"
 
-#: trac/ticket/query.py:573
+#: trac/ticket/query.py:581
 #, python-format
 msgid "Invalid ticket id list: %(value)s"
 msgstr "チケット番号が不正です: %(value)s"
 
-#: trac/ticket/query.py:672 trac/ticket/query.py:680
+#: trac/ticket/query.py:680 trac/ticket/query.py:688
 msgid "contains"
 msgstr "に次が含まれる"
 
-#: trac/ticket/query.py:673 trac/ticket/query.py:681
+#: trac/ticket/query.py:681 trac/ticket/query.py:689
 msgid "doesn't contain"
 msgstr "は次を含まない"
 
-#: trac/ticket/query.py:674
+#: trac/ticket/query.py:682
 msgid "begins with"
 msgstr "が次で始まる"
 
-#: trac/ticket/query.py:675
+#: trac/ticket/query.py:683
 msgid "ends with"
 msgstr "が次で終わる"
 
-#: trac/ticket/query.py:676 trac/ticket/query.py:684 trac/ticket/query.py:688
+#: trac/ticket/query.py:684 trac/ticket/query.py:692 trac/ticket/query.py:696
 msgid "is"
 msgstr "が次と等しい"
 
-#: trac/ticket/query.py:677 trac/ticket/query.py:685 trac/ticket/query.py:689
+#: trac/ticket/query.py:685 trac/ticket/query.py:693 trac/ticket/query.py:697
 msgid "is not"
 msgstr "は次と等しくない"
 
-#: trac/ticket/query.py:721 trac/ticket/query.py:727
+#: trac/ticket/query.py:734 trac/ticket/query.py:741
 msgid "Ticket"
 msgstr "チケット"
 
-#: trac/ticket/query.py:798 trac/ticket/report.py:468
-#, python-format
-msgid "Page %(num)d"
-msgstr "ページ%(num)d"
-
-#: trac/ticket/query.py:847 trac/ticket/report.py:328 trac/ticket/report.py:627
+#: trac/ticket/query.py:861 trac/ticket/report.py:324 trac/ticket/report.py:617
 #: trac/ticket/web_ui.py:140 trac/timeline/web_ui.py:235
-#: trac/versioncontrol/web_ui/log.py:319
+#: trac/versioncontrol/web_ui/log.py:327
 msgid "RSS Feed"
 msgstr "RSS フィード"
 
-#: trac/ticket/query.py:849 trac/ticket/report.py:330 trac/ticket/report.py:629
+#: trac/ticket/query.py:863 trac/ticket/report.py:326 trac/ticket/report.py:619
 #: trac/ticket/web_ui.py:136
 msgid "Comma-delimited Text"
 msgstr "CSV(カンマ区切り)"
 
-#: trac/ticket/query.py:851 trac/ticket/report.py:332 trac/ticket/report.py:631
+#: trac/ticket/query.py:865 trac/ticket/report.py:328 trac/ticket/report.py:621
 #: trac/ticket/web_ui.py:138
 msgid "Tab-delimited Text"
 msgstr "TSV(タブ区切り)"
 
-#: trac/ticket/query.py:873 trac/ticket/report.py:131
+#: trac/ticket/query.py:888 trac/ticket/report.py:132
 msgid "View Tickets"
 msgstr "チケットを見る"
 
-#: trac/ticket/query.py:1086 trac/ticket/query.py:1097
-#: trac/ticket/report.py:197 trac/ticket/templates/report_list.html:57
+#: trac/ticket/query.py:1104 trac/ticket/query.py:1115
+#: trac/ticket/report.py:202 trac/ticket/templates/report_list.html:67
 msgid "Custom Query"
 msgstr "カスタムクエリ"
 
-#: trac/ticket/query.py:1096 trac/ticket/report.py:187
-#: trac/ticket/report.py:188 trac/ticket/report.py:190
-#: trac/ticket/templates/report_list.html:10
-#: trac/ticket/templates/report_list.html:28
+#: trac/ticket/query.py:1114 trac/ticket/report.py:194
+#: trac/ticket/report.py:195 trac/ticket/report.py:197
+#: trac/ticket/templates/report_list.html:20
+#: trac/ticket/templates/report_list.html:38
 msgid "Available Reports"
 msgstr "レポート一覧"
 
-#: trac/ticket/query.py:1195
+#: trac/ticket/query.py:1213
 #, python-format
 msgid "[Error: %(error)s]"
 msgstr "[エラー: %(error)s]"
 
-#: trac/ticket/query.py:1201
+#: trac/ticket/query.py:1219
 msgid ""
 "Wiki macro listing tickets that match certain criteria.\n"
 "\n"
@@ -3235,7 +3285,7 @@
 "\n"
 "\n"
 "Groups of field constraints to be OR-ed together can be separated by a\n"
-"litteral `or` argument.\n"
+"literal `or` argument.\n"
 "\n"
 "In addition to filters, several other named parameters can be used\n"
 "to control how the results are presented. All of them are optional.\n"
@@ -3273,6 +3323,9 @@
 "The `rows` parameter can be used to specify which field(s) should\n"
 "be viewed as a row, e.g. `rows=description|summary`\n"
 "\n"
+"The `col` parameter can be used to specify which fields should\n"
+"be viewed as columns. For '''table''' format only.\n"
+"\n"
 "For compatibility with Trac 0.10, if there's a last positional parameter\n"
 "given to the macro, it will be used to specify the `format`.\n"
 "Also, using \"&\" as a field separator still works (except for `order`)\n"
@@ -3318,98 +3371,96 @@
 "`rows` パラメータは1行に表示するフィールドを指定することができます。\n"
 "例: `rows=description|summary`\n"
 "\n"
+"`col` パラメータは列に表示するフィールドを指定することができます。\n"
+"'''table''' フォーマットでのみ使えます。\n"
+"\n"
 "Trac 0.10 との互換性のため、マクロの最後に位置にする引数は `format` "
 "パラメータとして使います。また、フィールドのセパレータとして \"&\" を使ってもまだ機能しますが非推奨です (`order` "
 "パラメータを除く)。"
 
-#: trac/ticket/query.py:1378
+#: trac/ticket/query.py:1333
+#, python-format
+msgid "%(num)d ticket for which %(query)s"
+msgid_plural "%(num)d tickets for which %(query)s"
+msgstr[0] "%(num)dチケット: %(query)s"
+
+#: trac/ticket/query.py:1408
 #, python-format
 msgid "Ticket completion status for each %(group)s"
 msgstr "%(group)sごとのチケット完了ステータス"
 
-#: trac/ticket/query.py:1393
+#: trac/ticket/query.py:1423
 msgid "No results"
 msgstr "一致するものがありません"
 
-#: trac/ticket/query.py:1411
+#: trac/ticket/query.py:1441
 #, python-format
 msgid "%(groupvalue)s %(groupname)s tickets matching %(query)s"
 msgstr "%(query)s に %(groupvalue)s %(groupname)s のチケットが該当"
 
-#: trac/ticket/query.py:1435
+#: trac/ticket/query.py:1465
 #, python-format
 msgid "%(groupvalue)s %(groupname)s tickets:"
 msgstr "%(groupvalue)s %(groupname)s のチケット:"
 
-#: trac/ticket/report.py:223
+#: trac/ticket/report.py:228
 msgid "The report has been created."
 msgstr "レポートを作成しました。"
 
-#: trac/ticket/report.py:233
+#: trac/ticket/report.py:238
 #, python-format
 msgid "The report {%(id)d} has been deleted."
 msgstr "レポート {%(id)d} を削除しました。"
 
-#: trac/ticket/report.py:257
+#: trac/ticket/report.py:260
 #, python-format
 msgid "Delete Report {%(num)s} %(title)s"
 msgstr "レポート {%(num)s} %(title)s の削除"
 
-#: trac/ticket/report.py:262 trac/ticket/report.py:273
-#: trac/ticket/report.py:352
-#, python-format
-msgid "Report {%(num)s} does not exist."
-msgstr "レポート %(num)s は存在しません。"
-
-#: trac/ticket/report.py:263 trac/ticket/report.py:274
-#: trac/ticket/report.py:353
-msgid "Invalid Report Number"
-msgstr "レポート番号が不正です"
-
-#: trac/ticket/report.py:286
+#: trac/ticket/report.py:280
 msgid "Create New Report"
 msgstr "新しいレポートの作成"
 
-#: trac/ticket/report.py:290
+#: trac/ticket/report.py:284
 #, python-format
 msgid "Edit Report {%(num)d} %(title)s"
 msgstr "レポート \"{%(num)d} %(title)s\" の編集"
 
-#: trac/ticket/report.py:357
+#: trac/ticket/report.py:347
 #, python-format
 msgid "Report failed: %(error)s"
 msgstr "レポートの実行に失敗しました: %(error)s"
 
-#: trac/ticket/report.py:372
+#: trac/ticket/report.py:362
 #, python-format
 msgid "When specified, the report number should be \"%(num)s\"."
 msgstr "番号を指定するとレポート番号は \"%(num)s\" になります。"
 
-#: trac/ticket/report.py:444
+#: trac/ticket/report.py:434
 #, python-format
 msgid "Report execution failed: %(error)s %(sql)s"
 msgstr "レポートの実行に失敗しました: %(error)s %(sql)s"
 
-#: trac/ticket/report.py:635
+#: trac/ticket/report.py:625
 msgid "SQL Query"
 msgstr "SQL クエリ"
 
-#: trac/ticket/report.py:659
+#: trac/ticket/report.py:649
 #, python-format
 msgid "The following arguments are missing: %(args)s"
 msgstr "以下の引数が見つかりません: %(args)s"
 
-#: trac/ticket/report.py:676
+#: trac/ticket/report.py:666
 #, python-format
 msgid "Report {%(num)s} has no SQL query."
 msgstr "レポート %(num)s には SQL クエリが定義されていません。"
 
-#: trac/ticket/report.py:713
+#: trac/ticket/report.py:709
 #, python-format
 msgid "Query parameter \"sort=%(sort_col)s\"  is invalid"
 msgstr "クエリのパラメータ \"sort=%(sort_col)s\" は不正です"
 
-#: trac/ticket/report.py:756
+#: trac/ticket/report.py:755
 #, python-format
 msgid ""
 "Hint: if the report failed due to automatic modification of the ORDER BY "
@@ -3420,15 +3471,32 @@
 "ヒント: ORDER BY 句の自動補正や LIMIT/OFFSET の追加が原因でレポートが失敗する場合は、TracReports 中の "
 "%(sort_column)s と %(limit_offset)s を調べてレポートがどのように書き換えられるかを確かめてください。"
 
-#: trac/ticket/roadmap.py:243
+#: trac/ticket/report.py:779
+#, python-format
+msgid "Report {%(num)s} does not exist."
+msgstr "レポート %(num)s は存在しません。"
+
+#: trac/ticket/report.py:780
+msgid "Invalid Report Number"
+msgstr "レポート番号が不正です"
+
+#: trac/ticket/report.py:931
+msgid "report does not exist"
+msgstr "レポートは存在しません"
+
+#: trac/ticket/report.py:938
+msgid "no permission to view report"
+msgstr "レポートを参照する権限がありません"
+
+#: trac/ticket/roadmap.py:244
 msgid "ticket status"
 msgstr "チケットステータス"
 
-#: trac/ticket/roadmap.py:243
+#: trac/ticket/roadmap.py:244
 msgid "tickets"
 msgstr "チケット"
 
-#: trac/ticket/roadmap.py:253
+#: trac/ticket/roadmap.py:254
 #, python-format
 msgid ""
 "'%(group1)s' and '%(group2)s' milestone groups both are declared to be "
@@ -3437,7 +3505,7 @@
 "'%(group1)s' と '%(group2)s' のマイルストーングループは、どちらも \"catch-all\" "
 "グループです。設定を確認して下さい。"
 
-#: trac/ticket/roadmap.py:269
+#: trac/ticket/roadmap.py:270
 #, python-format
 msgid ""
 "'%(groupname)s' milestone group reused status '%(status)s' already taken "
@@ -3446,73 +3514,122 @@
 "マイルストーングループ '%(groupname)s' は、他がすでに使用しているステータス '%(status)s' "
 "を使っています。設定を確認して下さい。"
 
-#: trac/ticket/roadmap.py:403 trac/ticket/roadmap.py:527
-#: trac/ticket/roadmap.py:661 trac/ticket/templates/roadmap.html:10
-#: trac/ticket/templates/roadmap.html:32
+#: trac/ticket/roadmap.py:411 trac/ticket/roadmap.py:535
+#: trac/ticket/roadmap.py:669 trac/ticket/templates/roadmap.html:20
+#: trac/ticket/templates/roadmap.html:42
 msgid "Roadmap"
 msgstr "ロードマップ"
 
-#: trac/ticket/roadmap.py:452
+#: trac/ticket/roadmap.py:460
 msgid "iCalendar"
 msgstr "iCalendar"
 
-#: trac/ticket/roadmap.py:539 trac/ticket/roadmap.py:937
-#: trac/ticket/templates/milestone_view.html:10
-#: trac/ticket/templates/milestone_view.html:23
+#: trac/ticket/roadmap.py:547 trac/ticket/roadmap.py:1005
+#: trac/ticket/templates/milestone_view.html:20
+#: trac/ticket/templates/milestone_view.html:33
 #, python-format
 msgid "Milestone %(name)s"
 msgstr "マイルストーン %(name)s"
 
-#: trac/ticket/roadmap.py:557
+#: trac/ticket/roadmap.py:565
 #, python-format
 msgid "Ticket #%(num)s: %(summary)s"
 msgstr "チケット #%(num)s: %(summary)s"
 
-#: trac/ticket/roadmap.py:617
+#: trac/ticket/roadmap.py:625
 msgid "Milestones reached"
 msgstr "マイルストーンの完了"
 
-#: trac/ticket/roadmap.py:643
+#: trac/ticket/roadmap.py:651
 #, python-format
 msgid "Milestone %(name)s completed"
 msgstr "マイルストーン %(name)s が完了しました"
 
-#: trac/ticket/roadmap.py:702
+#: trac/ticket/roadmap.py:712
 #, python-format
 msgid "The milestone \"%(name)s\" has been deleted."
 msgstr "マイルストーン \"%(name)s\" を削除しました。"
 
-#: trac/ticket/roadmap.py:745
+#: trac/ticket/roadmap.py:715
+#, python-format
+msgid ""
+"The tickets associated with milestone \"%(name)s\" have been retargeted "
+"to milestone \"%(retarget)s\"."
+msgstr "マイルストーンが \"%(name)s\" のチケットを \"%(retarget)s\" に変更しました。"
+
+#: trac/ticket/roadmap.py:720
+msgid "Tickets retargeted after milestone deleted"
+msgstr "チケットはマイルストーン削除後そのマイルストーンを変更しました"
+
+#: trac/ticket/roadmap.py:774
 #, python-format
 msgid "Milestone \"%(name)s\" already exists, please choose another name."
 msgstr "マイルストーン \"%(name)s\" は既に存在します。別の名前を指定してください。"
 
-#: trac/ticket/roadmap.py:748
+#: trac/ticket/roadmap.py:777
 msgid "You must provide a name for the milestone."
 msgstr "マイルストーンの名前を指定してください。"
 
-#: trac/ticket/roadmap.py:877
+#: trac/ticket/roadmap.py:802
+#, python-format
+msgid ""
+"The open tickets associated with milestone \"%(name)s\" have been "
+"retargeted to milestone \"%(retarget)s\"."
+msgstr "マイルストーンが \"%(name)s\" の未解決チケットを \"%(retarget)s\" に変更しました。"
+
+#: trac/ticket/roadmap.py:808
+msgid "Open tickets retargeted after milestone closed"
+msgstr "未解決チケットはマイルストーン完了後そのマイルストーンを変更しました"
+
+#: trac/ticket/roadmap.py:865
+#, python-format
+msgid "Milestone %(name)s does not exist. You can create it here."
+msgstr "マイルストーン %(name)s がありません。ここから作成できます。"
+
+#: trac/ticket/roadmap.py:925
 #, python-format
 msgid "Milestone \"%(name)s\""
 msgstr "マイルストーン \"%(name)s\""
 
-#: trac/ticket/roadmap.py:891
+#: trac/ticket/roadmap.py:939
 msgid "Previous Milestone"
 msgstr "前のマイルストーン"
 
-#: trac/ticket/roadmap.py:891
+#: trac/ticket/roadmap.py:939
 msgid "Next Milestone"
 msgstr "次のマイルストーン"
 
-#: trac/ticket/roadmap.py:892
+#: trac/ticket/roadmap.py:940
 msgid "Back to Roadmap"
 msgstr "ロードマップに戻る"
 
-#: trac/ticket/web_ui.py:65
+#: trac/ticket/roadmap.py:974 trac/ticket/templates/milestone_view.html:37
+#: trac/ticket/templates/roadmap.html:53
+#, python-format
+msgid "Completed %(duration)s ago (%(date)s)"
+msgstr "%(duration)s前に完了 (%(date)s)"
+
+#: trac/ticket/roadmap.py:979
+#, python-format
+msgid "%(duration)s late (%(date)s)"
+msgstr "%(duration)s遅延 (%(date)s)"
+
+#: trac/ticket/roadmap.py:984 trac/ticket/templates/milestone_view.html:47
+#: trac/ticket/templates/roadmap.html:63
+#, python-format
+msgid "Due in %(duration)s (%(date)s)"
+msgstr "期日まで %(duration)s (%(date)s)"
+
+#: trac/ticket/roadmap.py:987 trac/ticket/templates/milestone_view.html:51
+#: trac/ticket/templates/roadmap.html:67
+msgid "No date set"
+msgstr "期日なし"
+
+#: trac/ticket/web_ui.py:63
 msgid "Invalid Ticket"
 msgstr "チケットエラー"
 
-#: trac/ticket/web_ui.py:162 trac/ticket/templates/ticket.html:14
+#: trac/ticket/web_ui.py:162 trac/ticket/templates/ticket.html:24
 msgid "New Ticket"
 msgstr "チケット登録"
 
@@ -3520,8 +3637,12 @@
 msgid "id can't be set for a new ticket request."
 msgstr "チケット登録時はidを指定することはできません。"
 
+#: trac/ticket/web_ui.py:194 trac/ticket/templates/admin_milestones.html:124
+msgid "Tickets"
+msgstr "チケット"
+
 #: trac/ticket/web_ui.py:228 trac/ticket/web_ui.py:279
-#: trac/versioncontrol/web_ui/changeset.py:1042
+#: trac/versioncontrol/web_ui/changeset.py:1012
 #, python-format
 msgid "%(title)s: %(message)s"
 msgstr "%(title)s: %(message)s"
@@ -3616,8 +3737,8 @@
 #: trac/ticket/web_ui.py:901 trac/ticket/web_ui.py:958
 #: trac/ticket/web_ui.py:966 trac/ticket/web_ui.py:1037
 #: trac/ticket/web_ui.py:1082 trac/ticket/web_ui.py:1089
-#: trac/wiki/web_ui.py:449 trac/wiki/web_ui.py:455 trac/wiki/web_ui.py:653
-#: trac/wiki/web_ui.py:667
+#: trac/wiki/web_ui.py:467 trac/wiki/web_ui.py:473 trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:685
 #, python-format
 msgid "Version %(num)s"
 msgstr "バージョン %(num)s"
@@ -3636,12 +3757,12 @@
 msgstr "プロパティ %(label)s %(rendered)s"
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:372 trac/wiki/web_ui.py:486
 msgid "Previous Change"
 msgstr "前の変更"
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:372 trac/wiki/web_ui.py:486
 msgid "Next Change"
 msgstr "次の変更"
 
@@ -3685,7 +3806,7 @@
 msgid_plural "%(labels)s deleted"
 msgstr[0] "%(labels)s を削除"
 
-#: trac/ticket/web_ui.py:1175 trac/ticket/web_ui.py:1755
+#: trac/ticket/web_ui.py:1175 trac/ticket/web_ui.py:1768
 msgid "; "
 msgstr "、"
 
@@ -3715,45 +3836,55 @@
 msgid "Tickets must contain a summary."
 msgstr "チケットの概要を記述する必要があります。"
 
-#: trac/ticket/web_ui.py:1248
+#: trac/ticket/web_ui.py:1244
+#, python-format
+msgid "\"%(value)s\" is not a valid value for the %(name)s field."
+msgstr "\"%(value)s\" は %(name)s フィールドとして正しい値ではありません。"
+
+#: trac/ticket/web_ui.py:1249
 #, python-format
 msgid "field %(name)s must be set"
 msgstr "フィールド %(name)s を入力してください"
 
-#: trac/ticket/web_ui.py:1254
+#: trac/ticket/web_ui.py:1255
 #, python-format
 msgid "Ticket description is too long (must be less than %(num)s characters)"
 msgstr "チケットの詳細が長過ぎます (%(num)s 文字未満にしてください)"
 
-#: trac/ticket/web_ui.py:1261
+#: trac/ticket/web_ui.py:1262
 #, python-format
 msgid "Ticket comment is too long (must be less than %(num)s characters)"
 msgstr "チケットのコメントが長過ぎます (%(num)s 文字未満にしてください)"
 
-#: trac/ticket/web_ui.py:1274
+#: trac/ticket/web_ui.py:1269
+#, python-format
+msgid "Ticket summary is too long (must be less than %(num)s characters)"
+msgstr "チケットの概要が長過ぎます (%(num)s 文字未満にしてください)"
+
+#: trac/ticket/web_ui.py:1282
 msgid "Invalid comment threading identifier"
 msgstr "コメント番号が不正です"
 
-#: trac/ticket/web_ui.py:1281
+#: trac/ticket/web_ui.py:1289
 #, python-format
 msgid "The ticket field '%(field)s' is invalid: %(message)s"
 msgstr "チケットのフィールド '%(field)s' の内容が不正です: %(message)s"
 
-#: trac/ticket/web_ui.py:1300
+#: trac/ticket/web_ui.py:1308
 #, python-format
 msgid ""
 "The ticket has been created, but an error occurred while sending "
 "notifications: %(message)s"
 msgstr "チケットは作成できましたが、通知処理中にエラーが発生しました: %(message)s"
 
-#: trac/ticket/web_ui.py:1305
+#: trac/ticket/web_ui.py:1313
 #, python-format
 msgid ""
 "The ticket %(ticketref)s has been created. You can now attach the desired"
 " files."
 msgstr "チケット %(ticketref)s を作成しました。ファイルを添付することができます。"
 
-#: trac/ticket/web_ui.py:1311
+#: trac/ticket/web_ui.py:1319
 #, python-format
 msgid ""
 "The ticket %(ticketref)s has been created, but you don't have permission "
@@ -3761,109 +3892,256 @@
 msgstr "チケット %(ticketref)s は作成できましたが、参照する権限がありません。"
 
 #. TRANSLATOR: The 'change' has been saved... (link)
-#: trac/ticket/web_ui.py:1338
+#: trac/ticket/web_ui.py:1346
 msgid "change"
 msgstr "変更内容"
 
-#: trac/ticket/web_ui.py:1344
+#: trac/ticket/web_ui.py:1352
 #, python-format
 msgid ""
 "The %(change)s has been saved, but an error occurred while sending "
 "notifications: %(message)s"
 msgstr "%(change)sを保存しましたが、通知処理中にエラーが発生しました: %(message)s"
 
-#: trac/ticket/web_ui.py:1482
+#: trac/ticket/web_ui.py:1495
 msgid "Add to Cc"
 msgstr "Cc に追加"
 
-#: trac/ticket/web_ui.py:1483
+#: trac/ticket/web_ui.py:1496
 msgid "Remove from Cc"
 msgstr "Cc から削除"
 
-#: trac/ticket/web_ui.py:1484
+#: trac/ticket/web_ui.py:1497
 msgid "Add/Remove from Cc"
 msgstr "Cc への追加/削除"
 
-#: trac/ticket/web_ui.py:1485
+#: trac/ticket/web_ui.py:1498
 msgid "<Author field>"
 msgstr "更新者"
 
-#: trac/ticket/web_ui.py:1518 trac/ticket/templates/query.html:114
+#: trac/ticket/web_ui.py:1531 trac/ticket/templates/query.html:124
 msgid "yes"
 msgstr "はい"
 
-#: trac/ticket/web_ui.py:1518 trac/ticket/templates/query.html:117
+#: trac/ticket/web_ui.py:1531 trac/ticket/templates/query.html:127
 msgid "no"
 msgstr "いいえ"
 
-#: trac/ticket/web_ui.py:1724
+#: trac/ticket/web_ui.py:1737
 msgid "set"
 msgstr "選択"
 
-#: trac/ticket/web_ui.py:1724
+#: trac/ticket/web_ui.py:1737
 msgid "unset"
 msgstr "解除"
 
-#: trac/ticket/web_ui.py:1727 trac/versioncontrol/templates/changeset.html:189
+#: trac/ticket/web_ui.py:1740 trac/versioncontrol/templates/changeset.html:199
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "modified"
 msgstr "更新"
 
-#. TRANSLATOR: modified ('diff') (link)
-#: trac/ticket/web_ui.py:1732 trac/ticket/templates/ticket_change.html:155
-#: trac/wiki/web_ui.py:747
-msgid "diff"
-msgstr "差分表示"
-
-#: trac/ticket/web_ui.py:1733
+#: trac/ticket/web_ui.py:1746
 #, python-format
 msgid "modified (%(diff)s)"
 msgstr "更新 (%(diff)s)"
 
-#: trac/ticket/web_ui.py:1751
+#: trac/ticket/web_ui.py:1764
 #, python-format
 msgid "%(items)s added"
 msgid_plural "%(items)s added"
 msgstr[0] "%(items)s を追加"
 
-#: trac/ticket/web_ui.py:1753
+#: trac/ticket/web_ui.py:1766
 #, python-format
 msgid "%(items)s removed"
 msgid_plural "%(items)s removed"
 msgstr[0] "%(items)s を削除"
 
-#: trac/ticket/web_ui.py:1762
+#: trac/ticket/web_ui.py:1775
 #, python-format
 msgid "%(value)s deleted"
 msgstr "%(value)s を削除"
 
-#: trac/ticket/web_ui.py:1764
+#: trac/ticket/web_ui.py:1777
 #, python-format
 msgid "set to %(value)s"
 msgstr "%(value)s に設定"
 
-#: trac/ticket/web_ui.py:1767
+#: trac/ticket/web_ui.py:1780
 #, python-format
 msgid "changed from %(old)s to %(new)s"
 msgstr "%(old)s から %(new)s に変更"
 
-#: trac/ticket/templates/batch_modify.html:8
+#: trac/ticket/templates/admin_components.html:24
+msgid "Manage Components"
+msgstr "コンポーネントの管理"
+
+#: trac/ticket/templates/admin_components.html:28
+msgid "Owner:"
+msgstr "担当者:"
+
+#: trac/ticket/templates/admin_components.html:45
+msgid "Modify Component:"
+msgstr "コンポーネントの変更:"
+
+#: trac/ticket/templates/admin_components.html:52
+#: trac/ticket/templates/admin_milestones.html:81
+#: trac/ticket/templates/admin_versions.html:50
+#: trac/ticket/templates/milestone_edit.html:116
+#: trac/ticket/templates/report_edit.html:43
+#: trac/versioncontrol/templates/admin_repositories.html:86
+msgid "Description: (you may use [1:WikiFormatting] here)"
+msgstr "詳細: ([1:WikiFormatting] が使えます)"
+
+#: trac/ticket/templates/admin_components.html:63
+#: trac/ticket/templates/admin_enums.html:37
+#: trac/ticket/templates/admin_milestones.html:92
+#: trac/ticket/templates/admin_versions.html:60
+#: trac/versioncontrol/templates/admin_repositories.html:97
+msgid "Save"
+msgstr "保存"
+
+#: trac/ticket/templates/admin_components.html:72
+msgid "Add Component:"
+msgstr "コンポーネントの追加:"
+
+#: trac/ticket/templates/admin_components.html:88
+#: trac/ticket/templates/admin_enums.html:61
+#: trac/ticket/templates/admin_milestones.html:124
+#: trac/ticket/templates/admin_versions.html:93
+msgid "Default"
+msgstr "デフォルト"
+
+#: trac/ticket/templates/admin_components.html:109
+#: trac/ticket/templates/admin_enums.html:85
+#: trac/ticket/templates/admin_milestones.html:151
+#: trac/ticket/templates/admin_versions.html:112
+msgid ""
+"You can remove all items from this list to completely hide this\n"
+"              field from the user interface."
+msgstr ""
+"すべての項目を削除する事で、このフィールドが、\n"
+"ユーザインターフェースに現れないようにする事が可能です。"
+
+#: trac/ticket/templates/admin_components.html:115
+#: trac/ticket/templates/admin_enums.html:95
+#: trac/ticket/templates/admin_milestones.html:157
+#: trac/ticket/templates/admin_versions.html:118
+msgid ""
+"As long as you don't add any items to the list, this field\n"
+"            will remain completely hidden from the user interface."
+msgstr ""
+"項目を追加しない限り、このフィールドは、\n"
+"ユーザインターフェースには現れません。"
+
+#: trac/ticket/templates/admin_enums.html:25
+#, python-format
+msgid "Manage %(label_plural)s"
+msgstr "%(label_plural)sの管理"
+
+#: trac/ticket/templates/admin_enums.html:32
+#, python-format
+msgid "Modify %(label_singular)s:"
+msgstr "%(label_singular)sの変更:"
+
+#: trac/ticket/templates/admin_enums.html:46
+#, python-format
+msgid "Add %(label_singular)s:"
+msgstr "%(label_singular)sの追加:"
+
+#: trac/ticket/templates/admin_enums.html:61
+msgid "Order"
+msgstr "順序"
+
+#: trac/ticket/templates/admin_enums.html:89
+msgid ""
+"[1:Note:] The order of priorities determines the\n"
+"              coloring of entries in the ticket queries and reports."
+msgstr "[1:※] 優先度の順にチケットクエリやレポートでの配色が決まります。"
+
+#: trac/ticket/templates/admin_milestones.html:36
+msgid "Manage Milestones"
+msgstr "マイルストーンの管理"
+
+#: trac/ticket/templates/admin_milestones.html:43
+msgid "Modify Milestone:"
+msgstr "マイルストーンの変更:"
+
+#: trac/ticket/templates/admin_milestones.html:49
+#: trac/ticket/templates/admin_milestones.html:106
+#: trac/ticket/templates/milestone_edit.html:73
+msgid "Due:"
+msgstr "期日:"
+
+#: trac/ticket/templates/admin_milestones.html:50
+#: trac/ticket/templates/admin_milestones.html:53
+#: trac/ticket/templates/admin_milestones.html:63
+#: trac/ticket/templates/admin_milestones.html:67
+#: trac/ticket/templates/admin_milestones.html:108
+#: trac/ticket/templates/admin_versions.html:42
+#: trac/ticket/templates/admin_versions.html:45
+#: trac/ticket/templates/admin_versions.html:76
+#: trac/ticket/templates/admin_versions.html:79
+#: trac/ticket/templates/milestone_edit.html:77
+#: trac/ticket/templates/milestone_edit.html:80
+#: trac/ticket/templates/milestone_edit.html:89
+#: trac/ticket/templates/milestone_edit.html:92
+#, python-format
+msgid "Format: %(datehint)s"
+msgstr "書式: %(datehint)s"
+
+#: trac/ticket/templates/admin_milestones.html:59
+#: trac/ticket/templates/milestone_edit.html:85
+msgid "Completed:"
+msgstr "完了:"
+
+#: trac/ticket/templates/admin_milestones.html:101
+msgid "Add Milestone:"
+msgstr "マイルストーンの追加:"
+
+#: trac/ticket/templates/admin_milestones.html:110
+#, python-format
+msgid "Format: %(datetimehint)s"
+msgstr "書式: %(datetimehint)s"
+
+#: trac/ticket/templates/admin_versions.html:29
+msgid "Manage Versions"
+msgstr "バージョンの管理"
+
+#: trac/ticket/templates/admin_versions.html:34
+msgid "Modify Version:"
+msgstr "バージョンの変更:"
+
+#: trac/ticket/templates/admin_versions.html:41
+#: trac/ticket/templates/admin_versions.html:74
+msgid "Released:"
+msgstr "リリース日時:"
+
+#: trac/ticket/templates/admin_versions.html:69
+msgid "Add Version:"
+msgstr "バージョンの追加:"
+
+#: trac/ticket/templates/admin_versions.html:93
+msgid "Released"
+msgstr "リリース日時"
+
+#: trac/ticket/templates/batch_modify.html:18
 msgid "Batch Modify"
 msgstr "一括更新"
 
-#: trac/ticket/templates/batch_modify.html:9
+#: trac/ticket/templates/batch_modify.html:19
 msgid "Batch modification fields"
 msgstr "一括更新フィールド"
 
-#: trac/ticket/templates/batch_modify.html:21
+#: trac/ticket/templates/batch_modify.html:31
 msgid "Add Field:"
 msgstr "フィールドの追加:"
 
-#: trac/ticket/templates/batch_modify.html:50
+#: trac/ticket/templates/batch_modify.html:60
 msgid "[1:Note:] See [2:TracBatchModify] for help on using batch modify."
 msgstr "[1:※] 詳しい使い方は [2:TracBatchModify] を参照してください。"
 
-#: trac/ticket/templates/batch_modify.html:57
+#: trac/ticket/templates/batch_modify.html:67
 msgid "Change tickets"
 msgstr "チケットを変更"
 
@@ -3882,26 +4160,26 @@
 msgid "Tickets URL: <%(link)s>"
 msgstr "Tickets URL: <%(link)s>"
 
-#: trac/ticket/templates/milestone_delete.html:10
-#: trac/ticket/templates/milestone_delete.html:22
+#: trac/ticket/templates/milestone_delete.html:20
+#: trac/ticket/templates/milestone_delete.html:27
 #, python-format
 msgid "Delete Milestone %(name)s"
 msgstr "マイルストーン %(name)s の削除"
 
-#: trac/ticket/templates/milestone_delete.html:27
+#: trac/ticket/templates/milestone_delete.html:32
 msgid "Are you sure you want to delete this milestone?"
 msgstr "このマイルストーンを削除しますか?"
 
-#: trac/ticket/templates/milestone_delete.html:29
+#: trac/ticket/templates/milestone_delete.html:33
 msgid "Retarget associated tickets to milestone"
 msgstr "このマイルストーンに属するチケットを次のものに割り当てる:"
 
-#: trac/ticket/templates/milestone_delete.html:41
-#: trac/ticket/templates/milestone_view.html:88
+#: trac/ticket/templates/milestone_delete.html:44
+#: trac/ticket/templates/milestone_view.html:98
 msgid "Delete milestone"
 msgstr "マイルストーンを削除"
 
-#: trac/ticket/templates/milestone_delete.html:45
+#: trac/ticket/templates/milestone_delete.html:49
 msgid ""
 "[1:Note:] See\n"
 "      [2:TracRoadmap] for help on using\n"
@@ -3910,44 +4188,44 @@
 "[1:※] 詳しい使い方は\n"
 "[2:TracRoadmap] を参照してください。"
 
-#: trac/ticket/templates/milestone_edit.html:11
-#: trac/ticket/templates/milestone_edit.html:45
+#: trac/ticket/templates/milestone_edit.html:21
+#: trac/ticket/templates/milestone_edit.html:56
 #, python-format
 msgid "Edit Milestone %(name)s"
 msgstr "マイルストーン %(name)s の編集"
 
-#: trac/ticket/templates/milestone_edit.html:12
-#: trac/ticket/templates/milestone_edit.html:46
+#: trac/ticket/templates/milestone_edit.html:22
+#: trac/ticket/templates/milestone_edit.html:57
 msgid "New Milestone"
 msgstr "新規マイルストーン"
 
-#: trac/ticket/templates/milestone_edit.html:53
+#: trac/ticket/templates/milestone_edit.html:64
 msgid "Name of the milestone:"
 msgstr "マイルストーンの名称:"
 
-#: trac/ticket/templates/milestone_edit.html:58
+#: trac/ticket/templates/milestone_edit.html:70
 msgid "Schedule"
 msgstr "スケジュール"
 
-#: trac/ticket/templates/milestone_edit.html:85
+#: trac/ticket/templates/milestone_edit.html:97
 msgid "Retarget associated open tickets to milestone:"
 msgstr "このマイルストーンの完了していないチケットを次のものに割り当てる:"
 
-#: trac/ticket/templates/milestone_edit.html:106
-#: trac/ticket/templates/ticket.html:388
-#: trac/ticket/templates/ticket_change.html:116
-#: trac/wiki/templates/wiki_edit_form.html:66
-#: trac/wiki/templates/wiki_edit_form.html:71
+#: trac/ticket/templates/milestone_edit.html:124
+#: trac/ticket/templates/ticket.html:391
+#: trac/ticket/templates/ticket_change.html:125
+#: trac/wiki/templates/wiki_edit_form.html:77
+#: trac/wiki/templates/wiki_edit_form.html:82
 msgid "Submit changes"
 msgstr "変更を送信"
 
-#: trac/ticket/templates/milestone_edit.html:107
+#: trac/ticket/templates/milestone_edit.html:126
 msgid "Add milestone"
 msgstr "マイルストーンを登録"
 
-#: trac/ticket/templates/milestone_edit.html:112
-#: trac/ticket/templates/milestone_view.html:94
-#: trac/ticket/templates/roadmap.html:80
+#: trac/ticket/templates/milestone_edit.html:132
+#: trac/ticket/templates/milestone_view.html:104
+#: trac/ticket/templates/roadmap.html:90
 msgid ""
 "[1:Note:] See\n"
 "        [2:TracRoadmap] for help on using\n"
@@ -3956,67 +4234,50 @@
 "[1:※] 詳しい使い方は\n"
 "[2:TracRoadmap] を参照してください。"
 
-#: trac/ticket/templates/milestone_view.html:11
+#: trac/ticket/templates/milestone_view.html:21
 msgid "Edit this milestone"
 msgstr "マイルストーンの編集"
 
-#: trac/ticket/templates/milestone_view.html:27
-#: trac/ticket/templates/roadmap.html:43
-#, python-format
-msgid "Completed %(duration)s ago (%(date)s)"
-msgstr "%(duration)s前に完了 (%(date)s)"
-
-#: trac/ticket/templates/milestone_view.html:32
-#: trac/ticket/templates/roadmap.html:48
+#: trac/ticket/templates/milestone_view.html:42
+#: trac/ticket/templates/roadmap.html:58
 #, python-format
 msgid "[1:%(duration)s late] (%(date)s)"
 msgstr "[1:%(duration)s遅延] (%(date)s)"
 
-#: trac/ticket/templates/milestone_view.html:37
-#: trac/ticket/templates/roadmap.html:53
-#, python-format
-msgid "Due in %(duration)s (%(date)s)"
-msgstr "期日まで %(duration)s (%(date)s)"
-
-#: trac/ticket/templates/milestone_view.html:41
-#: trac/ticket/templates/roadmap.html:57
-msgid "No date set"
-msgstr "期日なし"
-
-#: trac/ticket/templates/milestone_view.html:51
+#: trac/ticket/templates/milestone_view.html:61
 #, python-format
 msgid "%(stat_title)s by"
 msgstr "%(stat_title)s:"
 
-#: trac/ticket/templates/milestone_view.html:82
+#: trac/ticket/templates/milestone_view.html:92
 msgid "Edit milestone"
 msgstr "マイルストーンを編集"
 
-#: trac/ticket/templates/query.html:35
-#: trac/ticket/templates/report_view.html:21
-#: trac/ticket/templates/report_view.html:97
+#: trac/ticket/templates/query.html:45
+#: trac/ticket/templates/report_view.html:31
+#: trac/ticket/templates/report_view.html:107
 #, python-format
 msgid "%(num)s match"
 msgid_plural "%(num)s matches"
 msgstr[0] "%(num)s件が該当"
 
-#: trac/ticket/templates/query.html:44
+#: trac/ticket/templates/query.html:54
 msgid "Filters"
 msgstr "フィルタ"
 
-#: trac/ticket/templates/query.html:45
+#: trac/ticket/templates/query.html:55
 msgid "Query filters"
 msgstr "検索フィルタ"
 
-#: trac/ticket/templates/query.html:51 trac/ticket/templates/query.html:155
+#: trac/ticket/templates/query.html:61 trac/ticket/templates/query.html:165
 msgid "Or"
 msgstr "Or"
 
-#: trac/ticket/templates/query.html:79
+#: trac/ticket/templates/query.html:89
 msgid "or"
 msgstr "or"
 
-#: trac/ticket/templates/query.html:127
+#: trac/ticket/templates/query.html:137
 msgid ""
 "[1:between]\n"
 "                            [2:]\n"
@@ -4024,63 +4285,63 @@
 "                            [4:]"
 msgstr "[1:][2:] [3:から] [4:]"
 
-#: trac/ticket/templates/query.html:141
+#: trac/ticket/templates/query.html:151
 msgid "And"
 msgstr "And"
 
-#: trac/ticket/templates/query.html:174
+#: trac/ticket/templates/query.html:184
 msgid "Columns"
 msgstr "カラム"
 
-#: trac/ticket/templates/query.html:187
+#: trac/ticket/templates/query.html:197
 msgid "Group results by"
 msgstr "結果のグループ化:"
 
-#: trac/ticket/templates/query.html:198
+#: trac/ticket/templates/query.html:208
 msgid "descending"
 msgstr "降順"
 
-#: trac/ticket/templates/query.html:202
+#: trac/ticket/templates/query.html:212
 msgid "Show under each result:"
 msgstr "各検索結果の下に表示:"
 
-#: trac/ticket/templates/query.html:212
-#: trac/ticket/templates/report_view.html:27
+#: trac/ticket/templates/query.html:222
+#: trac/ticket/templates/report_view.html:37
 msgid "Max items per page"
 msgstr "1ページに表示する件数"
 
-#: trac/ticket/templates/query.html:235
+#: trac/ticket/templates/query.html:245
 #, python-format
 msgid "Edit report {%(id)s} corresponding to this query"
 msgstr "このクエリに対応するレポート {%(id)s} を編集する"
 
-#: trac/ticket/templates/query.html:235
+#: trac/ticket/templates/query.html:245
 msgid "Edit query"
 msgstr "このクエリを編集"
 
-#: trac/ticket/templates/query.html:244
+#: trac/ticket/templates/query.html:254
 msgid "Save query"
 msgstr "このクエリを保存"
 
-#: trac/ticket/templates/query.html:244
+#: trac/ticket/templates/query.html:254
 #, python-format
 msgid "Save updated query in report {%(id)s}"
 msgstr "更新されたクエリをレポート {%(id)s} に保存する"
 
-#: trac/ticket/templates/query.html:244
+#: trac/ticket/templates/query.html:254
 msgid "Create new report from current query"
 msgstr "現在のクエリから新しいレポートを作成する"
 
-#: trac/ticket/templates/query.html:252
+#: trac/ticket/templates/query.html:262
 #, python-format
 msgid "Delete report {%(id)s} corresponding to this query"
 msgstr "このクエリに対応するレポート {%(id)s} を削除する"
 
-#: trac/ticket/templates/query.html:252
+#: trac/ticket/templates/query.html:262
 msgid "Delete query"
 msgstr "このクエリを削除"
 
-#: trac/ticket/templates/query.html:259
+#: trac/ticket/templates/query.html:269
 msgid ""
 "[1:Note:] See [2:TracQuery]\n"
 "        for help on using queries."
@@ -4088,233 +4349,237 @@
 "[1:※] 詳しい使い方は\n"
 "[2:TracQuery] を参照してください。"
 
-#: trac/ticket/templates/query_results.html:25
+#: trac/ticket/templates/query_results.html:34
 #, python-format
 msgid "%(grouplabel)s: %(groupname)s [1:(%(count)s)]"
 msgstr "%(grouplabel)s: %(groupname)s [1:(%(count)s)]"
 
-#: trac/ticket/templates/query_results.html:37
+#: trac/ticket/templates/query_results.html:46
 msgid "(ascending)"
 msgstr "(昇順)"
 
-#: trac/ticket/templates/query_results.html:37
+#: trac/ticket/templates/query_results.html:46
 msgid "(descending)"
 msgstr "(降順)"
 
-#: trac/ticket/templates/query_results.html:38
-#: trac/versioncontrol/templates/sortable_th.html:18
+#: trac/ticket/templates/query_results.html:47
+#: trac/versioncontrol/templates/sortable_th.html:28
 #, python-format
 msgid "Sort by %(col)s %(direction)s"
 msgstr "%(col)sでソート %(direction)s"
 
-#: trac/ticket/templates/query_results.html:61
+#: trac/ticket/templates/query_results.html:70
 msgid "No tickets found"
 msgstr "一致するものが見つかりませんでした。"
 
-#: trac/ticket/templates/query_results.html:75
-#: trac/ticket/templates/query_results.html:78
+#: trac/ticket/templates/query_results.html:84
+#: trac/ticket/templates/query_results.html:88
 msgid "View ticket"
 msgstr "チケットの表示"
 
-#: trac/ticket/templates/query_results.html:83
-#: trac/ticket/templates/report_view.html:185
+#: trac/ticket/templates/query_results.html:93
+#: trac/ticket/templates/report_view.html:195
 msgid "View milestone"
 msgstr "マイルストーンの表示"
 
-#: trac/ticket/templates/query_results.html:95
+#: trac/ticket/templates/query_results.html:106
 msgid "(this ticket)"
 msgstr "(このチケット)"
 
-#: trac/ticket/templates/query_results.html:111
+#: trac/ticket/templates/query_results.html:122
 msgid "(more results for this group on next page)"
 msgstr "(次のページにこのグループの続きがあります)"
 
-#: trac/ticket/templates/report_delete.html:17
+#: trac/ticket/templates/report_delete.html:27
 msgid "Are you sure you want to delete this report?"
 msgstr "このレポートを削除しますか?"
 
-#: trac/ticket/templates/report_delete.html:22
-#: trac/ticket/templates/report_list.html:82
-#: trac/ticket/templates/report_view.html:74
+#: trac/ticket/templates/report_delete.html:31
+#: trac/ticket/templates/report_list.html:92
+#: trac/ticket/templates/report_view.html:84
 msgid "Delete report"
 msgstr "このレポートを削除"
 
-#: trac/ticket/templates/report_delete.html:26
-#: trac/ticket/templates/report_edit.html:49
-#: trac/ticket/templates/report_list.html:116
-#: trac/ticket/templates/report_view.html:210
+#: trac/ticket/templates/report_delete.html:36
+#: trac/ticket/templates/report_edit.html:66
+#: trac/ticket/templates/report_list.html:126
+#: trac/ticket/templates/report_view.html:220
 msgid ""
 "[1:Note:]\n"
 "        See [2:TracReports] for help on using and creating reports."
 msgstr "[1:※] 詳しい使い方は [2:TracReports] を参照してください。"
 
-#: trac/ticket/templates/report_edit.html:16
+#: trac/ticket/templates/report_edit.html:28
 msgid "New Report"
 msgstr "新規レポート"
 
-#: trac/ticket/templates/report_edit.html:21
-msgid "Report Title:"
-msgstr "レポートの名称:"
-
-#: trac/ticket/templates/report_edit.html:25
-msgid "Description: (you may use [1:WikiFormatting] here)"
-msgstr "詳細 ([1:WikiFormatting] が使えます)"
-
 #: trac/ticket/templates/report_edit.html:34
+msgid "Create Report:"
+msgstr "レポートの作成:"
+
+#: trac/ticket/templates/report_edit.html:35
+msgid "Modify Report:"
+msgstr "レポートの変更:"
+
+#: trac/ticket/templates/report_edit.html:39
+msgid "Title:"
+msgstr "タイトル:"
+
+#: trac/ticket/templates/report_edit.html:51
 msgid "Error:"
 msgstr "エラー:"
 
-#: trac/ticket/templates/report_edit.html:36
+#: trac/ticket/templates/report_edit.html:53
 msgid ""
-"Query for Report: (can be either SQL or, if starting with [1:query:],\n"
+"Query: (can be either SQL or, if starting with [1:query:],\n"
 "              a [2:TracQuery] expression)"
-msgstr "レポートのクエリ: (SQL 文または [1:query:] で始まる [2:TracQuery] が使えます)"
+msgstr "クエリ: (SQL 文または [1:query:] で始まる [2:TracQuery] が使えます)"
 
-#: trac/ticket/templates/report_edit.html:43
+#: trac/ticket/templates/report_edit.html:61
 msgid "Save report"
 msgstr "保存"
 
-#: trac/ticket/templates/report_list.html:33
+#: trac/ticket/templates/report_list.html:43
 msgid "Show Descriptions"
 msgstr "説明を表示する"
 
-#: trac/ticket/templates/report_list.html:45
+#: trac/ticket/templates/report_list.html:55
 msgid "Clear"
 msgstr "クリア"
 
-#: trac/ticket/templates/report_list.html:45
+#: trac/ticket/templates/report_list.html:55
 msgid "Forget last query"
 msgstr "直前のクエリを消す"
 
-#: trac/ticket/templates/report_list.html:48
+#: trac/ticket/templates/report_list.html:58
 msgid "Return to Last Query"
 msgstr "直前のクエリに戻る"
 
-#: trac/ticket/templates/report_list.html:51
+#: trac/ticket/templates/report_list.html:61
 msgid ""
 "Continue browsing through the current list of results,\n"
 "              from the last selected report or custom query."
 msgstr "引き続き、最後に選択したレポートまたはカスタムクエリから最新の結果を参照する。"
 
-#: trac/ticket/templates/report_list.html:60
+#: trac/ticket/templates/report_list.html:70
 msgid "Compose a new ticket query by selecting filters and columns to display."
 msgstr "フィルタと表示する項目を選択して新しいチケットクエリを作成する。"
 
-#: trac/ticket/templates/report_list.html:65
+#: trac/ticket/templates/report_list.html:75
 msgid "SQL reports and saved custom queries"
 msgstr "SQL レポートと保存しているカスタムクエリ"
 
-#: trac/ticket/templates/report_list.html:67
+#: trac/ticket/templates/report_list.html:77
 msgid "Sort by:"
 msgstr "ソート:"
 
-#: trac/ticket/templates/report_list.html:70
+#: trac/ticket/templates/report_list.html:80
 msgid "Identifier"
 msgstr "ID"
 
-#: trac/ticket/templates/report_list.html:73 trac/wiki/admin.py:197
+#: trac/ticket/templates/report_list.html:83 trac/wiki/admin.py:197
 msgid "Title"
 msgstr "タイトル"
 
-#: trac/ticket/templates/report_list.html:89
-#: trac/ticket/templates/ticket_change.html:65
-#: trac/wiki/templates/wiki_edit.html:149
+#: trac/ticket/templates/report_list.html:99
+#: trac/ticket/templates/ticket_change.html:74
+#: trac/wiki/templates/wiki_edit.html:162
 msgid "Edit"
 msgstr "編集"
 
-#: trac/ticket/templates/report_list.html:89
-#: trac/ticket/templates/report_view.html:62
+#: trac/ticket/templates/report_list.html:99
+#: trac/ticket/templates/report_view.html:72
 msgid "Edit report"
 msgstr "このレポートを編集"
 
-#: trac/ticket/templates/report_list.html:93
-#: trac/ticket/templates/report_view.html:136
+#: trac/ticket/templates/report_list.html:103
+#: trac/ticket/templates/report_view.html:146
 msgid "View report"
 msgstr "レポートを表示する"
 
-#: trac/ticket/templates/report_list.html:103
+#: trac/ticket/templates/report_list.html:113
 msgid "No reports available."
 msgstr "利用できるレポートはありません。"
 
-#: trac/ticket/templates/report_list.html:111
+#: trac/ticket/templates/report_list.html:121
 msgid "Create new report"
 msgstr "新しいレポートの作成"
 
-#: trac/ticket/templates/report_view.html:32
+#: trac/ticket/templates/report_view.html:42
 msgid "Arguments"
 msgstr "引数"
 
-#: trac/ticket/templates/report_view.html:33
+#: trac/ticket/templates/report_view.html:43
 msgid "Report arguments"
 msgstr "レポート引数"
 
-#: trac/ticket/templates/report_view.html:68
+#: trac/ticket/templates/report_view.html:78
 msgid "Copy report"
 msgstr "このレポートをコピー"
 
-#: trac/ticket/templates/report_view.html:110
+#: trac/ticket/templates/report_view.html:120
 msgid "(empty)"
 msgstr "(値なし)"
 
-#: trac/ticket/templates/report_view.html:143
-#: trac/ticket/templates/report_view.html:151
+#: trac/ticket/templates/report_view.html:153
+#: trac/ticket/templates/report_view.html:161
 #, python-format
 msgid "View %(realm)s"
 msgstr "%(realm)s の表示"
 
-#: trac/ticket/templates/roadmap.html:20
+#: trac/ticket/templates/roadmap.html:30
 msgid "Show completed milestones"
 msgstr "完了したマイルストーンも表示する"
 
-#: trac/ticket/templates/roadmap.html:25
+#: trac/ticket/templates/roadmap.html:35
 msgid "Hide milestones with no due date"
 msgstr "期限のないマイルストールを表示しない"
 
-#: trac/ticket/templates/roadmap.html:38
+#: trac/ticket/templates/roadmap.html:48
 msgid "Milestone:"
 msgstr "マイルストーン:"
 
-#: trac/ticket/templates/roadmap.html:76
+#: trac/ticket/templates/roadmap.html:86
 msgid "Add new milestone"
 msgstr "新しいマイルストーンを登録"
 
-#: trac/ticket/templates/ticket.html:132
+#: trac/ticket/templates/ticket.html:136
 msgid "Go to the ticket editor"
 msgstr "チケットの編集領域に移動"
 
-#: trac/ticket/templates/ticket.html:132
+#: trac/ticket/templates/ticket.html:136
 msgid "Modify"
 msgstr "変更"
 
-#: trac/ticket/templates/ticket.html:134
+#: trac/ticket/templates/ticket.html:138
 msgid "Create New Ticket"
 msgstr "チケットの新規作成"
 
-#: trac/ticket/templates/ticket.html:152
+#: trac/ticket/templates/ticket.html:156
 msgid "Oldest first"
 msgstr "古い順"
 
-#: trac/ticket/templates/ticket.html:154
+#: trac/ticket/templates/ticket.html:158
 msgid "Newest first"
 msgstr "新しい順"
 
-#: trac/ticket/templates/ticket.html:157
+#: trac/ticket/templates/ticket.html:161
 msgid "Threaded"
 msgstr "スレッド"
 
-#: trac/ticket/templates/ticket.html:162
+#: trac/ticket/templates/ticket.html:166
 msgid "Comments only"
 msgstr "コメントのみ"
 
-#: trac/ticket/templates/ticket.html:167
+#: trac/ticket/templates/ticket.html:171
 msgid "Change History"
 msgstr "更新履歴"
 
-#: trac/ticket/templates/ticket.html:186
+#: trac/ticket/templates/ticket.html:191
 msgid "Add Comment"
 msgstr "コメントの追加"
 
-#: trac/ticket/templates/ticket.html:188
+#: trac/ticket/templates/ticket.html:193
 msgid ""
 "This ticket has been modified since you started editing. You should "
 "review the\n"
@@ -4324,231 +4589,232 @@
 " wish so."
 msgstr "このチケットは編集開始以降に更新されています。上に追加されている「[1:他の人の変更内容]」と下のプレビューに表示されている「[2:衝突内容]」を確認してください。それでも続行する場合は変更内容を送信することもできます。"
 
-#: trac/ticket/templates/ticket.html:198
+#: trac/ticket/templates/ticket.html:203
 msgid ""
 "You may use\n"
 "                [1:WikiFormatting]\n"
 "                here."
 msgstr "[1:WikiFormatting] が使えます"
 
-#: trac/ticket/templates/ticket.html:209
+#: trac/ticket/templates/ticket.html:214
 msgid "Modify Ticket"
 msgstr "チケットの変更"
 
-#: trac/ticket/templates/ticket.html:215
+#: trac/ticket/templates/ticket.html:220
 msgid "Change Properties"
 msgstr "属性変更"
 
-#: trac/ticket/templates/ticket.html:216
+#: trac/ticket/templates/ticket.html:221
 msgid "Properties"
 msgstr "属性"
 
-#: trac/ticket/templates/ticket.html:220
+#: trac/ticket/templates/ticket.html:226
 msgid "Summary:"
 msgstr "概要:"
 
-#: trac/ticket/templates/ticket.html:228
+#: trac/ticket/templates/ticket.html:234
 msgid "Reporter:"
 msgstr "報告者:"
 
-#: trac/ticket/templates/ticket.html:240
+#: trac/ticket/templates/ticket.html:244
 msgid ""
 "You may use\n"
-"                          [1:WikiFormatting] here."
+"                        [1:WikiFormatting] here."
 msgstr "[1:WikiFormatting] が使えます"
 
-#: trac/ticket/templates/ticket.html:256
-#: trac/ticket/templates/ticket_box.html:75
+#: trac/ticket/templates/ticket.html:259
+#: trac/ticket/templates/ticket_box.html:84
 #, python-format
 msgid "%(field)s:"
 msgstr "%(field)s:"
 
-#: trac/ticket/templates/ticket.html:295
+#: trac/ticket/templates/ticket.html:298
 msgid "This checkbox allows you to add or remove yourself from the CC list."
 msgstr "関係者に自分自身を登録または削除することができます。"
 
-#: trac/ticket/templates/ticket.html:301
+#: trac/ticket/templates/ticket.html:304
 msgid "Space or comma delimited email addresses and usernames are accepted."
 msgstr "空白かカンマ区切りで、メールアドレスかユーザ名を設定できます。"
 
-#: trac/ticket/templates/ticket.html:364
-#: trac/wiki/templates/wiki_edit_form.html:46
+#: trac/ticket/templates/ticket.html:367
+#: trac/wiki/templates/wiki_edit_form.html:57
 msgid "E-mail address and user name can be saved in the [1:Preferences]."
 msgstr "メールアドレスとユーザ名は[1:個人設定]で保存できます。"
 
-#: trac/ticket/templates/ticket.html:372
+#: trac/ticket/templates/ticket.html:375
 msgid "I have files to attach to this ticket"
 msgstr "このチケットにファイルを添付します"
 
-#: trac/ticket/templates/ticket.html:378
+#: trac/ticket/templates/ticket.html:381
 msgid "Go to the list of attachments"
 msgstr "添付ファイルの一覧に移動"
 
-#: trac/ticket/templates/ticket.html:379
+#: trac/ticket/templates/ticket.html:382
 msgid "View the ticket description"
 msgstr "チケットの詳細を表示"
 
-#: trac/ticket/templates/ticket.html:387
-#: trac/ticket/templates/ticket_change.html:114
-#: trac/wiki/templates/wiki_edit.html:95 trac/wiki/templates/wiki_edit.html:153
-#: trac/wiki/templates/wiki_edit_form.html:64
+#: trac/ticket/templates/ticket.html:390
+#: trac/ticket/templates/ticket_change.html:123
+#: trac/wiki/templates/wiki_edit.html:102
+#: trac/wiki/templates/wiki_edit.html:166
+#: trac/wiki/templates/wiki_edit_form.html:75
 msgid "Preview"
 msgstr "プレビュー"
 
-#: trac/ticket/templates/ticket.html:388
+#: trac/ticket/templates/ticket.html:391
 msgid "Create ticket"
 msgstr "チケットの新規作成"
 
-#: trac/ticket/templates/ticket.html:396
+#: trac/ticket/templates/ticket.html:399
 msgid ""
 "[1:Note:] See\n"
 "        [2:TracTickets] for help on using\n"
 "        tickets."
 msgstr "[1:※] 詳しい使い方は [2:TracTickets] を参照してください。"
 
-#: trac/ticket/templates/ticket_box.html:22
+#: trac/ticket/templates/ticket_box.html:31
 #, python-format
 msgid "Opened %(created)s"
 msgstr "登録: %(created)s"
 
-#: trac/ticket/templates/ticket_box.html:23
+#: trac/ticket/templates/ticket_box.html:32
 #, python-format
 msgid "Closed %(closed)s"
 msgstr "完了: %(closed)s"
 
-#: trac/ticket/templates/ticket_box.html:24
+#: trac/ticket/templates/ticket_box.html:33
 #, python-format
 msgid "Last modified %(modified)s"
 msgstr "最終更新: %(modified)s"
 
-#: trac/ticket/templates/ticket_box.html:26
+#: trac/ticket/templates/ticket_box.html:35
 msgid "(ticket not yet created)"
 msgstr "(まだ作成していません)"
 
-#: trac/ticket/templates/ticket_box.html:48
+#: trac/ticket/templates/ticket_box.html:57
 msgid "at [1:Initial Version]"
 msgstr "[1:初期バージョン]"
 
-#: trac/ticket/templates/ticket_box.html:51
+#: trac/ticket/templates/ticket_box.html:60
 #, python-format
 msgid "at [1:Version %(version)s]"
 msgstr "[1:バージョン %(version)s]"
 
-#: trac/ticket/templates/ticket_box.html:62
+#: trac/ticket/templates/ticket_box.html:71
 msgid "Reported by:"
 msgstr "報告者:"
 
-#: trac/ticket/templates/ticket_box.html:64
+#: trac/ticket/templates/ticket_box.html:73
 msgid "Owned by:"
 msgstr "担当者:"
 
-#: trac/ticket/templates/ticket_box.html:96
+#: trac/ticket/templates/ticket_box.html:105
 #, python-format
 msgid "(last modified by %(author)s)"
 msgstr "(最終更新者 %(author)s)"
 
-#: trac/ticket/templates/ticket_box.html:105
-#: trac/ticket/templates/ticket_change.html:72
+#: trac/ticket/templates/ticket_box.html:114
+#: trac/ticket/templates/ticket_change.html:81
 msgid "Reply"
 msgstr "返信"
 
-#: trac/ticket/templates/ticket_box.html:105
+#: trac/ticket/templates/ticket_box.html:114
 msgid "Reply, quoting this description"
 msgstr "説明文を引用してコメントする"
 
-#: trac/ticket/templates/ticket_change.html:37
+#: trac/ticket/templates/ticket_change.html:46
 msgid "in reply to:"
 msgstr "親コメント:"
 
-#: trac/ticket/templates/ticket_change.html:42
+#: trac/ticket/templates/ticket_change.html:51
 msgid "follow-up:"
 msgid_plural "follow-ups:"
 msgstr[0] "フォローアップ"
 
-#: trac/ticket/templates/ticket_change.html:53
+#: trac/ticket/templates/ticket_change.html:62
 #, python-format
 msgid "Changed %(date)s by %(author)s"
 msgstr "更新者: %(author)s (%(date)s)"
 
-#: trac/ticket/templates/ticket_change.html:56
+#: trac/ticket/templates/ticket_change.html:65
 #, python-format
 msgid "Changed by %(author)s"
 msgstr "更新者: %(author)s"
 
-#: trac/ticket/templates/ticket_change.html:65
+#: trac/ticket/templates/ticket_change.html:74
 #, python-format
 msgid "Edit comment %(cnum)s"
 msgstr "コメント %(cnum)s の編集"
 
-#: trac/ticket/templates/ticket_change.html:72
+#: trac/ticket/templates/ticket_change.html:81
 #, python-format
 msgid "Reply to comment %(cnum)s"
 msgstr "コメント %(cnum)s に返信する"
 
-#: trac/ticket/templates/ticket_change.html:81
+#: trac/ticket/templates/ticket_change.html:90
 #, python-format
 msgid ""
 "[1:[2:%(name)s]][3:​]\n"
 "          added"
 msgstr "[1:[2:%(name)s]][3:​] を追加"
 
-#: trac/ticket/templates/ticket_change.html:88
+#: trac/ticket/templates/ticket_change.html:97
 #, python-format
 msgid "changed from [1:%(old)s] to [2:%(new)s]"
 msgstr "[1:%(old)s] から [2:%(new)s] に変更"
 
-#: trac/ticket/templates/ticket_change.html:91
+#: trac/ticket/templates/ticket_change.html:100
 #, python-format
 msgid "set to [1:%(value)s]"
 msgstr "[1:%(value)s] に設定"
 
-#: trac/ticket/templates/ticket_change.html:94
+#: trac/ticket/templates/ticket_change.html:103
 #, python-format
 msgid "[1:%(value)s] deleted"
 msgstr "[1:%(value)s] を削除"
 
-#: trac/ticket/templates/ticket_change.html:98
+#: trac/ticket/templates/ticket_change.html:107
 msgid "Revert this change"
 msgstr "この変更を取り消す"
 
-#: trac/ticket/templates/ticket_change.html:100
+#: trac/ticket/templates/ticket_change.html:109
 msgid "revert"
 msgstr "取り消す"
 
-#: trac/ticket/templates/ticket_change.html:114
+#: trac/ticket/templates/ticket_change.html:123
 #, python-format
 msgid "Preview changes to comment %(cnum)s"
 msgstr "コメント %(cnum)s に対する変更のプレビューを見る"
 
-#: trac/ticket/templates/ticket_change.html:116
+#: trac/ticket/templates/ticket_change.html:125
 #, python-format
 msgid "Submit changes to comment %(cnum)s"
 msgstr "コメント %(cnum)s に対する変更を送信する"
 
-#: trac/ticket/templates/ticket_change.html:118
+#: trac/ticket/templates/ticket_change.html:127
 msgid "Cancel comment edit"
 msgstr "コメントの編集を取り消す"
 
-#: trac/ticket/templates/ticket_change.html:137
+#: trac/ticket/templates/ticket_change.html:146
 #, python-format
 msgid ""
 "Version %(version)s, edited %(date)s\n"
 "        by %(author)s"
 msgstr "バージョン %(version)s、更新者 %(author)s (%(date)s)"
 
-#: trac/ticket/templates/ticket_change.html:141
+#: trac/ticket/templates/ticket_change.html:150
 #, python-format
 msgid ""
 "Last edited %(date)s\n"
 "        by %(author)s"
 msgstr "最終更新者: %(author)s (%(date)s)"
 
-#: trac/ticket/templates/ticket_change.html:147
-#: trac/versioncontrol/templates/changeset.html:129
+#: trac/ticket/templates/ticket_change.html:156
+#: trac/versioncontrol/templates/changeset.html:139
 msgid "previous"
 msgstr "前版"
 
-#: trac/ticket/templates/ticket_change.html:151
+#: trac/ticket/templates/ticket_change.html:160
 msgid "next"
 msgstr "次版"
 
@@ -4572,8 +4838,8 @@
 msgid "Ticket URL: <%(link)s>"
 msgstr "Ticket URL: <%(link)s>"
 
-#: trac/timeline/web_ui.py:78 trac/timeline/templates/timeline.html:10
-#: trac/timeline/templates/timeline.html:21
+#: trac/timeline/web_ui.py:78 trac/timeline/templates/timeline.html:20
+#: trac/timeline/templates/timeline.html:31
 msgid "Timeline"
 msgstr "タイムライン"
 
@@ -4585,12 +4851,12 @@
 msgid "Next Period"
 msgstr "次の期間"
 
-#: trac/timeline/web_ui.py:290
+#: trac/timeline/web_ui.py:290 trac/web/chrome.py:885
 #, python-format
 msgid "at %(iso8601)s"
 msgstr "%(iso8601)s"
 
-#: trac/timeline/web_ui.py:294
+#: trac/timeline/web_ui.py:294 trac/web/chrome.py:889
 #, python-format
 msgid "on %(date)s at %(time)s"
 msgstr "%(date)s %(time)s"
@@ -4600,7 +4866,7 @@
 msgid "See timeline %(relativetime)s ago"
 msgstr "タイムラインで%(relativetime)s前を見る"
 
-#: trac/timeline/web_ui.py:298 trac/web/chrome.py:865 trac/web/chrome.py:867
+#: trac/timeline/web_ui.py:298 trac/web/chrome.py:890 trac/web/chrome.py:892
 #, python-format
 msgid "%(relativetime)s ago"
 msgstr "%(relativetime)s前"
@@ -4630,7 +4896,7 @@
 "タイムラインから%(other_events)sを参照してみるか、エラーを Trac 管理者に連絡してください "
 "(エラーの詳細はログに出力されます)。"
 
-#: trac/timeline/templates/timeline.html:24
+#: trac/timeline/templates/timeline.html:34
 msgid ""
 "[1:View changes from [2:]] [3:]\n"
 "        and [4:[5:] days back][6:]\n"
@@ -4640,22 +4906,22 @@
 "        から [4:[5:] 日前まで][6:]\n"
 "        [7:更新者 [8:]]"
 
-#: trac/timeline/templates/timeline.html:41
+#: trac/timeline/templates/timeline.html:51
 msgid "Today"
 msgstr "本日"
 
-#: trac/timeline/templates/timeline.html:41
+#: trac/timeline/templates/timeline.html:51
 msgid "Yesterday"
 msgstr "昨日"
 
-#: trac/timeline/templates/timeline.html:48
+#: trac/timeline/templates/timeline.html:58
 #, python-format
 msgid ""
 "[1:%(time)s] %(title)s\n"
 "                  by [2:%(author)s]"
 msgstr "[1:%(time)s] %(title)s (更新者 [2:%(author)s])"
 
-#: trac/timeline/templates/timeline.html:64
+#: trac/timeline/templates/timeline.html:74
 msgid ""
 "[1:Note:] See [2:TracTimeline]\n"
 "        for information about the timeline view."
@@ -4710,60 +4976,69 @@
 "\n"
 "  %(new_path)s\n"
 
-#: trac/util/datefmt.py:118
+#: trac/util/datefmt.py:123
 #, python-format
 msgid "%(num)d year"
 msgid_plural "%(num)d years"
 msgstr[0] "%(num)d年"
 
-#: trac/util/datefmt.py:119
+#: trac/util/datefmt.py:124
 #, python-format
 msgid "%(num)d month"
 msgid_plural "%(num)d months"
 msgstr[0] "%(num)dヵ月"
 
-#: trac/util/datefmt.py:120
+#: trac/util/datefmt.py:125
 #, python-format
 msgid "%(num)d week"
 msgid_plural "%(num)d weeks"
 msgstr[0] "%(num)d週"
 
-#: trac/util/datefmt.py:121
+#: trac/util/datefmt.py:126
 #, python-format
 msgid "%(num)d day"
 msgid_plural "%(num)d days"
 msgstr[0] "%(num)d日"
 
-#: trac/util/datefmt.py:122
+#: trac/util/datefmt.py:127
 #, python-format
 msgid "%(num)d hour"
 msgid_plural "%(num)d hours"
 msgstr[0] "%(num)d時間"
 
-#: trac/util/datefmt.py:123
+#: trac/util/datefmt.py:128
 #, python-format
 msgid "%(num)d minute"
 msgid_plural "%(num)d minutes"
 msgstr[0] "%(num)d分"
 
-#: trac/util/datefmt.py:142
+#: trac/util/datefmt.py:147
 #, python-format
 msgid "%(num)i second"
 msgid_plural "%(num)i seconds"
 msgstr[0] "%(num)i秒"
 
-#: trac/util/datefmt.py:464
+#: trac/util/datefmt.py:501
+#, python-format
+msgid ""
+"\"%(date)s\" is an invalid date, or the date format is not known. Try "
+"\"%(hint)s\" or \"%(isohint)s\" instead."
+msgstr ""
+"\"%(date)s\" は間違っているか、日付書式が不明です。代わりに \"%(hint)s\" または \"%(isohint)s\" "
+"を試してください。"
+
+#: trac/util/datefmt.py:503
 #, python-format
 msgid ""
 "\"%(date)s\" is an invalid date, or the date format is not known. Try "
 "\"%(hint)s\" instead."
 msgstr "\"%(date)s\" は間違っているか、日付書式が不明です。代わりに \"%(hint)s\" を試してください。"
 
-#: trac/util/datefmt.py:466 trac/util/datefmt.py:474
+#: trac/util/datefmt.py:506 trac/util/datefmt.py:514
 msgid "Invalid Date"
 msgstr "不正な日付"
 
-#: trac/util/datefmt.py:472
+#: trac/util/datefmt.py:512
 #, python-format
 msgid ""
 "The date \"%(date)s\" is outside valid range. Try a date closer to "
@@ -4785,7 +5060,7 @@
 msgstr "エイリアス"
 
 #: trac/versioncontrol/admin.py:113
-#: trac/versioncontrol/templates/admin_repositories.html:125
+#: trac/versioncontrol/templates/admin_repositories.html:138
 msgid "Directory"
 msgstr "ディレクトリ"
 
@@ -4793,11 +5068,11 @@
 msgid "Cannot synchronize a single revision on multiple repositories"
 msgstr "複数のリポジトリのリビジョンに同期できません"
 
-#: trac/versioncontrol/admin.py:127 trac/versioncontrol/admin.py:196
-#: trac/versioncontrol/web_ui/browser.py:356
-#: trac/versioncontrol/web_ui/changeset.py:248
-#: trac/versioncontrol/web_ui/changeset.py:1104
-#: trac/versioncontrol/web_ui/log.py:93 trac/versioncontrol/web_ui/log.py:413
+#: trac/versioncontrol/admin.py:127 trac/versioncontrol/admin.py:194
+#: trac/versioncontrol/web_ui/browser.py:355
+#: trac/versioncontrol/web_ui/changeset.py:250
+#: trac/versioncontrol/web_ui/changeset.py:1074
+#: trac/versioncontrol/web_ui/log.py:97 trac/versioncontrol/web_ui/log.py:426
 #, python-format
 msgid "Repository '%(repo)s' not found"
 msgstr "リポジトリ '%(repo)s' が見つかりません"
@@ -4827,65 +5102,75 @@
 msgstr "バージョンコントロール"
 
 #: trac/versioncontrol/admin.py:181
-#: trac/versioncontrol/templates/admin_repositories.html:10
+#: trac/versioncontrol/templates/admin_repositories.html:20
 msgid "Repositories"
 msgstr "リポジトリ"
 
-#: trac/versioncontrol/admin.py:220 trac/versioncontrol/admin.py:262
+#: trac/versioncontrol/admin.py:219 trac/versioncontrol/admin.py:268
 #, python-format
 msgid "You should now run %(resync)s to synchronize Trac with the repository."
 msgstr "Trac をリポジトリと同期させるために %(resync)s を実行してください。"
 
-#: trac/versioncontrol/admin.py:225
+#: trac/versioncontrol/admin.py:224
 #, python-format
 msgid "You may have to run %(resync)s to synchronize Trac with the repository."
 msgstr "Trac をリポジトリと同期させるために %(resync)s を実行しなければならないかも知れません。"
 
-#: trac/versioncontrol/admin.py:233
+#: trac/versioncontrol/admin.py:232
 #, python-format
 msgid ""
 "You will need to update your post-commit hook to call %(cset_added)s with"
 " the new repository name."
 msgstr "新しいリポジトリの名前で %(cset_added)s を呼び出すように post-commit フックを修正する必要があります。"
 
-#: trac/versioncontrol/admin.py:253
+#: trac/versioncontrol/admin.py:254
 msgid "Missing arguments to add a repository."
 msgstr "リポジトリを追加するにはパラメータが足りません。"
 
-#: trac/versioncontrol/admin.py:258
+#: trac/versioncontrol/admin.py:261 trac/versioncontrol/api.py:262
+#, python-format
+msgid "The repository \"%(name)s\" already exists."
+msgstr "リポジトリ \"%(name)s\" はすでに存在しています。"
+
+#: trac/versioncontrol/admin.py:264
 #, python-format
 msgid "The repository \"%(name)s\" has been added."
 msgstr "リポジトリ \"%(name)s\" を追加しました。"
 
-#: trac/versioncontrol/admin.py:268
+#: trac/versioncontrol/admin.py:274
 #, python-format
 msgid ""
 "You should also set up a post-commit hook on the repository to call "
 "%(cset_added)s for each committed changeset."
 msgstr "コミットごとに %(cset_added)s を呼ぶようにリポジトリの post-commit フックを設定してください。"
 
-#: trac/versioncontrol/admin.py:281
+#: trac/versioncontrol/admin.py:289
+#, python-format
+msgid "The alias \"%(name)s\" already exists."
+msgstr "エイリアス \"%(name)s\" はすでに存在しています。"
+
+#: trac/versioncontrol/admin.py:292
 #, python-format
 msgid "The alias \"%(name)s\" has been added."
 msgstr "エイリアス \"%(name)s\" を追加しました。"
 
-#: trac/versioncontrol/admin.py:284
+#: trac/versioncontrol/admin.py:295
 msgid "Missing arguments to add an alias."
 msgstr "エイリアスを追加するにはパラメータが足りません。"
 
-#: trac/versioncontrol/admin.py:297
+#: trac/versioncontrol/admin.py:308
 msgid "The selected repositories have been removed."
 msgstr "選択したリポジトリを解除しました。"
 
-#: trac/versioncontrol/admin.py:300
+#: trac/versioncontrol/admin.py:311
 msgid "No repositories were selected."
 msgstr "リポジトリを選択していません。"
 
-#: trac/versioncontrol/admin.py:341
+#: trac/versioncontrol/admin.py:352
 msgid "The repository directory must be an absolute path."
 msgstr "リポジトリディレクトリは絶対パスでなければなりません。"
 
-#: trac/versioncontrol/admin.py:350
+#: trac/versioncontrol/admin.py:361
 #, python-format
 msgid ""
 "The repository directory must be located below one of the following "
@@ -4893,13 +5178,13 @@
 msgstr "リポジトリディレクトリは次にあげるディレクトリのどれかの中になければなりません: %(dirs)s"
 
 #: trac/versioncontrol/api.py:34
-#: trac/versioncontrol/templates/admin_repositories.html:20
-#: trac/versioncontrol/templates/admin_repositories.html:33
-#: trac/versioncontrol/templates/admin_repositories.html:132
-#: trac/versioncontrol/templates/admin_repositories.html:134
-#: trac/versioncontrol/web_ui/browser.py:911
-#: trac/versioncontrol/web_ui/changeset.py:875
-#: trac/versioncontrol/web_ui/changeset.py:1015
+#: trac/versioncontrol/templates/admin_repositories.html:30
+#: trac/versioncontrol/templates/admin_repositories.html:43
+#: trac/versioncontrol/templates/admin_repositories.html:145
+#: trac/versioncontrol/templates/admin_repositories.html:147
+#: trac/versioncontrol/web_ui/browser.py:954
+#: trac/versioncontrol/web_ui/changeset.py:845
+#: trac/versioncontrol/web_ui/changeset.py:985
 msgid "(default)"
 msgstr "(デフォルト)"
 
@@ -4918,7 +5203,7 @@
 msgid "You may have to run \"repository resync %(name)s\"."
 msgstr "\"repository resync %(name)s\" を実行しなければならないかも知れません。"
 
-#: trac/versioncontrol/api.py:207 trac/versioncontrol/api.py:262
+#: trac/versioncontrol/api.py:207 trac/versioncontrol/api.py:271
 msgid "The repository directory must be absolute"
 msgstr "リポジトリディレクトリは絶対パスでなければなりません"
 
@@ -4927,62 +5212,76 @@
 msgid "The repository type '%(type)s' is not supported"
 msgstr "リポジトリ '%(type)s' はサポートしていません"
 
-#: trac/versioncontrol/api.py:356
+#: trac/versioncontrol/api.py:365
 #, python-format
 msgid ""
 "Can't synchronize with repository \"%(name)s\" (%(error)s). Look in the "
 "Trac log for more information."
 msgstr "リポジトリ \"%(name)s\" との同期ができません (%(error)s)。詳細は Trac のログを参照してください"
 
-#: trac/versioncontrol/api.py:377
+#: trac/versioncontrol/api.py:372
+#, python-format
+msgid ""
+"Failed to sync with repository \"%(name)s\": %(error)s; repository "
+"information may be out of date. Look in the Trac log for more information"
+" including mitigation strategies."
+msgstr ""
+"リポジトリ \"%(name)s\" との同期に失敗しました: %(error)s; "
+"リポジトリのデータは最新でない可能性があります。詳細と軽減する方法については Trac のログを参照してください。"
+
+#: trac/versioncontrol/api.py:401
 #, python-format
 msgid "Changeset %(rev)s in %(repo)s"
 msgstr "チェンジセット %(rev)s (%(repo)s)"
 
-#: trac/versioncontrol/api.py:379
+#: trac/versioncontrol/api.py:403
 #, python-format
 msgid "Changeset %(rev)s"
 msgstr "チェンジセット %(rev)s"
 
-#: trac/versioncontrol/api.py:389
+#: trac/versioncontrol/api.py:413
 msgid "directory"
 msgstr "ディレクトリ"
 
-#: trac/versioncontrol/api.py:391
+#: trac/versioncontrol/api.py:415
 msgid "file"
 msgstr "ファイル"
 
-#: trac/versioncontrol/api.py:393
+#: trac/versioncontrol/api.py:417
 #, python-format
 msgid " at version %(rev)s"
 msgstr " バージョン %(rev)s"
 
-#: trac/versioncontrol/api.py:395
+#: trac/versioncontrol/api.py:419
 msgid "path"
 msgstr "パス"
 
-#: trac/versioncontrol/api.py:398
+#: trac/versioncontrol/api.py:422
 #, python-format
 msgid " in %(repo)s"
 msgstr " (%(repo)s)"
 
 #. TRANSLATOR: file /path/to/file.py at version 13 in reponame
-#: trac/versioncontrol/api.py:400
+#: trac/versioncontrol/api.py:424
 #, python-format
 msgid "%(kind)s %(id)s%(at_version)s%(in_repo)s"
 msgstr "%(kind)s %(id)s%(at_version)s%(in_repo)s"
 
-#: trac/versioncontrol/api.py:403
+#: trac/versioncontrol/api.py:428
+msgid "Default repository"
+msgstr "デフォルトリポジトリ"
+
+#: trac/versioncontrol/api.py:429
 #, python-format
 msgid "Repository %(repo)s"
 msgstr "リポジトリ %(repo)s"
 
-#: trac/versioncontrol/api.py:711
+#: trac/versioncontrol/api.py:741
 #, python-format
 msgid "Unsupported version control system \"%(name)s\": %(error)s"
 msgstr "サポートしていないバージョン管理システム \"%(name)s\": \"%(error)s\""
 
-#: trac/versioncontrol/api.py:714
+#: trac/versioncontrol/api.py:744
 #, python-format
 msgid ""
 "Unsupported version control system \"%(name)s\": Can't find an "
@@ -4991,30 +5290,30 @@
 "サポートしていないバージョン管理システム \"%(name)s\": "
 "適切なコンポーネントを見つけることができません。対応するプラグインは、有効になっていますか? "
 
-#: trac/versioncontrol/api.py:722
+#: trac/versioncontrol/api.py:752
 #, python-format
 msgid "No changeset %(rev)s in the repository"
 msgstr "チェンジセット %(rev)s がリポジトリに存在しません"
 
-#: trac/versioncontrol/api.py:724
+#: trac/versioncontrol/api.py:754
 msgid "No such changeset"
 msgstr "チェンジセットが存在しません"
 
-#: trac/versioncontrol/api.py:730
+#: trac/versioncontrol/api.py:760
 #, python-format
 msgid "No node %(path)s at revision %(rev)s"
 msgstr "リビジョン %(rev)s に %(path)s はありません"
 
-#: trac/versioncontrol/api.py:732
+#: trac/versioncontrol/api.py:762
 #, python-format
 msgid "%(msg)s: No node %(path)s at revision %(rev)s"
 msgstr "%(msg)s: リビジョン %(rev)s に %(path)s はありません"
 
-#: trac/versioncontrol/api.py:734
+#: trac/versioncontrol/api.py:764
 msgid "No such node"
 msgstr "ノードが存在しません"
 
-#: trac/versioncontrol/cache.py:147
+#: trac/versioncontrol/cache.py:245
 #, python-format
 msgid ""
 "The repository directory has changed, you should resynchronize the "
@@ -5033,27 +5332,27 @@
 msgid "Line %(lineno)d: Invalid entry"
 msgstr "行 %(lineno)d: 不正なエントリ"
 
-#: trac/versioncontrol/templates/admin_repositories.html:14
+#: trac/versioncontrol/templates/admin_repositories.html:24
 msgid "Manage Repositories"
 msgstr "リポジトリの管理"
 
-#: trac/versioncontrol/templates/admin_repositories.html:24
+#: trac/versioncontrol/templates/admin_repositories.html:34
 msgid "Default:"
 msgstr "デフォルト:"
 
-#: trac/versioncontrol/templates/admin_repositories.html:30
+#: trac/versioncontrol/templates/admin_repositories.html:40
 msgid "Repository:"
 msgstr "リポジトリ:"
 
-#: trac/versioncontrol/templates/admin_repositories.html:43
+#: trac/versioncontrol/templates/admin_repositories.html:54
 msgid "Modify Repository:"
 msgstr "リポジトリの変更:"
 
-#: trac/versioncontrol/templates/admin_repositories.html:44
+#: trac/versioncontrol/templates/admin_repositories.html:55
 msgid "View Repository:"
 msgstr "リポジトリの参照:"
 
-#: trac/versioncontrol/templates/admin_repositories.html:45
+#: trac/versioncontrol/templates/admin_repositories.html:56
 msgid ""
 "[1:Note:]\n"
 "            This repository is defined in [2:[3:trac.ini]]\n"
@@ -5062,109 +5361,109 @@
 "[1:※]\n"
 "このリポジトリは [2:[3:trac.ini]] で設定しているため、このページでは変更できません。"
 
-#: trac/versioncontrol/templates/admin_repositories.html:59
-#: trac/versioncontrol/templates/admin_repositories.html:99
+#: trac/versioncontrol/templates/admin_repositories.html:71
+#: trac/versioncontrol/templates/admin_repositories.html:112
 msgid "Directory:"
 msgstr "ディレクトリ:"
 
-#: trac/versioncontrol/templates/admin_repositories.html:67
+#: trac/versioncontrol/templates/admin_repositories.html:80
 msgid "Hide from repository index"
 msgstr "リポジトリ一覧に表示しない"
 
-#: trac/versioncontrol/templates/admin_repositories.html:93
+#: trac/versioncontrol/templates/admin_repositories.html:106
 msgid "Add Repository:"
 msgstr "リポジトリの追加:"
 
-#: trac/versioncontrol/templates/admin_repositories.html:110
+#: trac/versioncontrol/templates/admin_repositories.html:123
 msgid "Add Alias:"
 msgstr "エイリアスの追加:"
 
-#: trac/versioncontrol/templates/admin_repositories.html:125
+#: trac/versioncontrol/templates/admin_repositories.html:138
 msgid "Revision"
 msgstr "リビジョン"
 
-#: trac/versioncontrol/templates/admin_repositories.html:137
+#: trac/versioncontrol/templates/admin_repositories.html:150
 #, python-format
 msgid "Alias of %(repo)s"
 msgstr "%(repo)s のエイリアス"
 
-#: trac/versioncontrol/templates/admin_repositories.html:144
+#: trac/versioncontrol/templates/admin_repositories.html:157
 msgid "Refresh"
 msgstr "再読み込み"
 
-#: trac/versioncontrol/templates/browser.html:13
+#: trac/versioncontrol/templates/browser.html:23
 #, python-format
 msgid "%(basename)s in %(dirname)s"
 msgstr "%(dirname)s の %(basename)s"
 
-#: trac/versioncontrol/templates/browser.html:55
+#: trac/versioncontrol/templates/browser.html:65
 msgid "Default Repository"
 msgstr "デフォルトリポジトリ"
 
-#: trac/versioncontrol/templates/browser.html:62
+#: trac/versioncontrol/templates/browser.html:72
 msgid "Show the diff against a specific revision"
 msgstr "指定のリビジョンに対して差分を表示する"
 
-#: trac/versioncontrol/templates/browser.html:63
+#: trac/versioncontrol/templates/browser.html:73
 msgid "View diff against:"
 msgstr "次に対して差分を表示:"
 
-#: trac/versioncontrol/templates/browser.html:76
+#: trac/versioncontrol/templates/browser.html:86
 msgid "Hint: clear the field to view latest revision"
 msgstr "※ 最新リビジョンを見るにはこの項目を空にします"
 
-#: trac/versioncontrol/templates/browser.html:76
+#: trac/versioncontrol/templates/browser.html:86
 msgid "View revision:"
 msgstr "リビジョン指定:"
 
-#: trac/versioncontrol/templates/browser.html:86
+#: trac/versioncontrol/templates/browser.html:96
 msgid "Visit:"
 msgstr "ジャンプ:"
 
-#: trac/versioncontrol/templates/browser.html:94
+#: trac/versioncontrol/templates/browser.html:104
 msgid "Go!"
 msgstr "移動"
 
-#: trac/versioncontrol/templates/browser.html:94
+#: trac/versioncontrol/templates/browser.html:104
 msgid "Jump to the chosen preselected path"
 msgstr "選択されたパスにジャンプします"
 
-#: trac/versioncontrol/templates/browser.html:100
-#: trac/versioncontrol/templates/revisionlog.html:179
+#: trac/versioncontrol/templates/browser.html:110
+#: trac/versioncontrol/templates/revisionlog.html:189
 msgid "Branch head"
 msgstr "ブランチの先頭"
 
-#: trac/versioncontrol/templates/browser.html:100
-#: trac/versioncontrol/templates/revisionlog.html:179
+#: trac/versioncontrol/templates/browser.html:110
+#: trac/versioncontrol/templates/revisionlog.html:189
 msgid "Branch"
 msgstr "ブランチ"
 
-#: trac/versioncontrol/templates/browser.html:103
-#: trac/versioncontrol/templates/revisionlog.html:182
+#: trac/versioncontrol/templates/browser.html:113
+#: trac/versioncontrol/templates/revisionlog.html:192
 msgid "Tag"
 msgstr "タグ"
 
-#: trac/versioncontrol/templates/browser.html:114
+#: trac/versioncontrol/templates/browser.html:124
 msgid "Parent Directory"
 msgstr "親ディレクトリ"
 
-#: trac/versioncontrol/templates/browser.html:120
+#: trac/versioncontrol/templates/browser.html:130
 msgid "No files found"
 msgstr "一致するものが見つかりませんでした。"
 
-#: trac/versioncontrol/templates/browser.html:128
-#: trac/wiki/templates/wiki_edit.html:137 trac/wiki/templates/wiki_view.html:34
+#: trac/versioncontrol/templates/browser.html:138
+#: trac/wiki/templates/wiki_edit.html:150 trac/wiki/templates/wiki_view.html:45
 msgid "Revision info"
 msgstr "リビジョン情報"
 
-#: trac/versioncontrol/templates/browser.html:140
-#: trac/versioncontrol/templates/browser.html:148
-#: trac/versioncontrol/templates/path_links.html:32
+#: trac/versioncontrol/templates/browser.html:150
+#: trac/versioncontrol/templates/browser.html:158
+#: trac/versioncontrol/templates/path_links.html:42
 #, python-format
 msgid "View changeset %(rev)s"
 msgstr "チェンジセット %(rev)s 参照"
 
-#: trac/versioncontrol/templates/browser.html:137
+#: trac/versioncontrol/templates/browser.html:147
 #, python-format
 msgid ""
 "[1:Last change]\n"
@@ -5175,7 +5474,7 @@
 "このファイルの %(stickyrev)s 以降における[1:最終更新内容]は [2:%(rev)s] で %(author)s が "
 "%(age)s に更新しました"
 
-#: trac/versioncontrol/templates/browser.html:145
+#: trac/versioncontrol/templates/browser.html:155
 #, python-format
 msgid ""
 "[1:Last change]\n"
@@ -5184,12 +5483,12 @@
 "                  checked in by %(author)s, %(age)s"
 msgstr "このファイルの[1:最終更新内容]は [2:%(rev)s] で %(author)s が %(age)s に更新しました"
 
-#: trac/versioncontrol/templates/browser.html:180
+#: trac/versioncontrol/templates/browser.html:190
 #, python-format
 msgid "Property [1:%(name)s] set to %(value)s"
 msgstr "プロパティ [1:%(name)s] が %(value)s に設定"
 
-#: trac/versioncontrol/templates/browser.html:186
+#: trac/versioncontrol/templates/browser.html:196
 #, python-format
 msgid ""
 "[1:\n"
@@ -5198,65 +5497,65 @@
 "          ]"
 msgstr "[1:[2:ファイルサイズ:] [3:%(size)s]]"
 
-#: trac/versioncontrol/templates/browser.html:200
+#: trac/versioncontrol/templates/browser.html:210
 msgid "Repository Index"
 msgstr "リポジトリ一覧"
 
-#: trac/versioncontrol/templates/browser.html:217
+#: trac/versioncontrol/templates/browser.html:227
 msgid "View changes..."
 msgstr "変更箇所を見る..."
 
-#: trac/versioncontrol/templates/browser.html:217
+#: trac/versioncontrol/templates/browser.html:227
 msgid "Select paths and revs for Diff"
 msgstr "差分をとるパスとリビジョンを指定します"
 
-#: trac/versioncontrol/templates/browser.html:222
+#: trac/versioncontrol/templates/browser.html:232
 msgid ""
 "[1:Note:] See [2:TracBrowser]\n"
 "        for help on using the repository browser."
 msgstr "[1:※] 詳しい使い方は [2:TracBrowser] を参照してください。"
 
-#: trac/versioncontrol/templates/changeset.html:36
 #: trac/versioncontrol/templates/changeset.html:46
-#: trac/versioncontrol/templates/changeset.html:48
+#: trac/versioncontrol/templates/changeset.html:56
 #: trac/versioncontrol/templates/changeset.html:58
 #: trac/versioncontrol/templates/changeset.html:68
-#: trac/versioncontrol/templates/changeset.html:70
+#: trac/versioncontrol/templates/changeset.html:78
+#: trac/versioncontrol/templates/changeset.html:80
 msgid "Show full changeset"
 msgstr "チェンジセット全体を表示する"
 
-#: trac/versioncontrol/templates/changeset.html:37
-#: trac/versioncontrol/templates/changeset.html:40
-#: trac/versioncontrol/templates/changeset.html:45
 #: trac/versioncontrol/templates/changeset.html:47
-#: trac/versioncontrol/templates/changeset.html:59
-#: trac/versioncontrol/templates/changeset.html:62
-#: trac/versioncontrol/templates/changeset.html:67
+#: trac/versioncontrol/templates/changeset.html:50
+#: trac/versioncontrol/templates/changeset.html:55
+#: trac/versioncontrol/templates/changeset.html:57
 #: trac/versioncontrol/templates/changeset.html:69
-#: trac/versioncontrol/templates/changeset.html:105
+#: trac/versioncontrol/templates/changeset.html:72
+#: trac/versioncontrol/templates/changeset.html:77
+#: trac/versioncontrol/templates/changeset.html:79
+#: trac/versioncontrol/templates/changeset.html:115
 msgid "Show entry in browser"
 msgstr "ブラウザで表示する"
 
-#: trac/versioncontrol/templates/changeset.html:35
+#: trac/versioncontrol/templates/changeset.html:45
 #, python-format
 msgid ""
 "Changeset [1:%(new_rev)s] in %(reponame)s\n"
 "              for [2:%(new_path)s]"
 msgstr "チェンジセット [1:%(new_rev)s] (%(reponame)s、[2:%(new_path)s])"
 
-#: trac/versioncontrol/templates/changeset.html:41
-#: trac/versioncontrol/templates/changeset.html:63
+#: trac/versioncontrol/templates/changeset.html:51
+#: trac/versioncontrol/templates/changeset.html:73
 msgid "Show revision log"
 msgstr "更新履歴を表示する"
 
-#: trac/versioncontrol/templates/changeset.html:39
+#: trac/versioncontrol/templates/changeset.html:49
 #, python-format
 msgid ""
 "Changes in [1:%(new_path)s]\n"
 "              [2:\\[%(old_rev)s:%(new_rev)s\\]] in %(reponame)s"
 msgstr "[1:%(new_path)s] [2:\\[%(old_rev)s:%(new_rev)s\\]] (%(reponame)s) の変更"
 
-#: trac/versioncontrol/templates/changeset.html:43
+#: trac/versioncontrol/templates/changeset.html:53
 #, python-format
 msgid ""
 "Changes in %(reponame)s\n"
@@ -5268,26 +5567,26 @@
 "%(reponame)s [1:%(old_path)s] [2:r%(old_rev)s] から [3:%(new_path)s] "
 "[4:r%(new_rev)s] の変更"
 
-#: trac/versioncontrol/templates/changeset.html:50
+#: trac/versioncontrol/templates/changeset.html:60
 #, python-format
 msgid "Changeset [1:%(new_rev)s] in %(reponame)s"
 msgstr "チェンジセット [1:%(new_rev)s] (%(reponame)s)"
 
-#: trac/versioncontrol/templates/changeset.html:57
+#: trac/versioncontrol/templates/changeset.html:67
 #, python-format
 msgid ""
 "Changeset [1:%(new_rev)s]\n"
 "              for [2:%(new_path)s]"
 msgstr "チェンジセット [1:%(new_rev)s] ([2:%(new_path)s])"
 
-#: trac/versioncontrol/templates/changeset.html:61
+#: trac/versioncontrol/templates/changeset.html:71
 #, python-format
 msgid ""
 "Changes in [1:%(new_path)s]\n"
 "              [2:\\[%(old_rev)s:%(new_rev)s\\]]"
 msgstr "[1:%(new_path)s] [2:\\[%(old_rev)s:%(new_rev)s\\]] の変更"
 
-#: trac/versioncontrol/templates/changeset.html:65
+#: trac/versioncontrol/templates/changeset.html:75
 #, python-format
 msgid ""
 "Changes\n"
@@ -5297,134 +5596,134 @@
 "              at [4:r%(new_rev)s]"
 msgstr "[1:%(old_path)s] [2:r%(old_rev)s] から [3:%(new_path)s] [4:r%(new_rev)s] の変更"
 
-#: trac/versioncontrol/templates/changeset.html:72
+#: trac/versioncontrol/templates/changeset.html:82
 #, python-format
 msgid "Changeset [1:%(new_rev)s]"
 msgstr "チェンジセット [1:%(new_rev)s]"
 
-#: trac/versioncontrol/templates/changeset.html:101
+#: trac/versioncontrol/templates/changeset.html:111
 #, python-format
 msgid "Show what was removed (content at revision %(old_rev)s)"
 msgstr "削除された項目を表示する (リビジョン %(old_rev)s)"
 
-#: trac/versioncontrol/templates/changeset.html:106
+#: trac/versioncontrol/templates/changeset.html:116
 msgid "(root)"
 msgstr "(ルート)"
 
-#: trac/versioncontrol/templates/changeset.html:112
+#: trac/versioncontrol/templates/changeset.html:122
 #, python-format
 msgid "Show original file (revision %(old_rev)s)"
 msgstr "元のファイルを表示する (リビジョン %(old_rev)s)"
 
-#: trac/versioncontrol/templates/changeset.html:111
+#: trac/versioncontrol/templates/changeset.html:121
 #, python-format
 msgid ""
 "(%(kind)s from [1:\n"
 "                %(old_path)s])"
 msgstr "([1:%(old_path)s] から%(kind)s)"
 
-#: trac/versioncontrol/templates/changeset.html:119
-#: trac/versioncontrol/templates/changeset.html:122
+#: trac/versioncontrol/templates/changeset.html:129
+#: trac/versioncontrol/templates/changeset.html:132
 msgid "Show differences"
 msgstr "差分を表示する"
 
-#: trac/versioncontrol/templates/changeset.html:119
+#: trac/versioncontrol/templates/changeset.html:129
 msgid "view diffs"
-msgstr "差分を表示する"
+msgstr "差分表示"
 
-#: trac/versioncontrol/templates/changeset.html:122
+#: trac/versioncontrol/templates/changeset.html:132
 #, python-format
 msgid "%(num)d diff"
 msgid_plural "%(num)d diffs"
 msgstr[0] "%(num)d個の差分"
 
-#: trac/versioncontrol/templates/changeset.html:125
+#: trac/versioncontrol/templates/changeset.html:135
 #, python-format
 msgid "%(num)d prop"
 msgid_plural "%(num)d props"
 msgstr[0] "%(num)d個のプロパティ"
 
-#: trac/versioncontrol/templates/changeset.html:129
+#: trac/versioncontrol/templates/changeset.html:139
 msgid "Show previous version in browser"
 msgstr "前のバージョンをブラウザで表示する"
 
-#: trac/versioncontrol/templates/changeset.html:139
+#: trac/versioncontrol/templates/changeset.html:149
 msgid "(less than one hour ago)"
 msgstr "(1時間未満)"
 
-#: trac/versioncontrol/templates/changeset.html:140
+#: trac/versioncontrol/templates/changeset.html:150
 #, python-format
 msgid "(%(age)s ago)"
 msgstr "(%(age)s前)"
 
-#: trac/versioncontrol/templates/changeset.html:154
+#: trac/versioncontrol/templates/changeset.html:164
 msgid "Message:"
 msgstr "ログメッセージ:"
 
-#: trac/versioncontrol/templates/changeset.html:166
+#: trac/versioncontrol/templates/changeset.html:176
 msgid "Location:"
 msgstr "場所:"
 
-#: trac/versioncontrol/templates/changeset.html:170
+#: trac/versioncontrol/templates/changeset.html:180
 msgid "File:"
 msgid_plural "Files:"
 msgstr[0] "ファイル:"
 
-#: trac/versioncontrol/templates/changeset.html:170
+#: trac/versioncontrol/templates/changeset.html:180
 msgid "(No files)"
 msgstr "(ファイルなし)"
 
-#: trac/versioncontrol/templates/changeset.html:175
+#: trac/versioncontrol/templates/changeset.html:185
 #, python-format
 msgid "%(num)d added"
 msgid_plural "%(num)d added"
 msgstr[0] "%(num)d個の追加"
 
-#: trac/versioncontrol/templates/changeset.html:176
+#: trac/versioncontrol/templates/changeset.html:186
 #, python-format
 msgid "%(num)d deleted"
 msgid_plural "%(num)d deleted"
 msgstr[0] "%(num)d個の削除"
 
-#: trac/versioncontrol/templates/changeset.html:177
+#: trac/versioncontrol/templates/changeset.html:187
 #, python-format
 msgid "%(num)d edited"
 msgid_plural "%(num)d edited"
 msgstr[0] "%(num)d個の更新"
 
-#: trac/versioncontrol/templates/changeset.html:178
+#: trac/versioncontrol/templates/changeset.html:188
 #, python-format
 msgid "%(num)d copied"
 msgid_plural "%(num)d copied"
 msgstr[0] "%(num)d個のコピー"
 
-#: trac/versioncontrol/templates/changeset.html:179
+#: trac/versioncontrol/templates/changeset.html:189
 #, python-format
 msgid "%(num)d moved"
 msgid_plural "%(num)d moved"
 msgstr[0] "%(num)d個の移動"
 
-#: trac/versioncontrol/templates/changeset.html:185
+#: trac/versioncontrol/templates/changeset.html:195
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "added"
 msgstr "追加"
 
-#: trac/versioncontrol/templates/changeset.html:186
+#: trac/versioncontrol/templates/changeset.html:196
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "deleted"
 msgstr "削除"
 
-#: trac/versioncontrol/templates/changeset.html:187
+#: trac/versioncontrol/templates/changeset.html:197
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "copied"
 msgstr "コピー"
 
-#: trac/versioncontrol/templates/changeset.html:188
+#: trac/versioncontrol/templates/changeset.html:198
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "moved"
 msgstr "移動"
 
-#: trac/versioncontrol/templates/changeset.html:211
+#: trac/versioncontrol/templates/changeset.html:221
 msgid ""
 "[1:Note:] See [2:TracChangeset]\n"
 "          for help on using the changeset viewer."
@@ -5432,29 +5731,29 @@
 "[1:※] 詳しい使い方は\n"
 "[2:TracChangeset] を参照してください。"
 
-#: trac/versioncontrol/templates/diff_form.html:10
-#: trac/versioncontrol/templates/diff_form.html:21
+#: trac/versioncontrol/templates/diff_form.html:20
+#: trac/versioncontrol/templates/diff_form.html:31
 msgid "Prepare Diff"
 msgstr "差分表示の指定"
 
-#: trac/versioncontrol/templates/diff_form.html:27
+#: trac/versioncontrol/templates/diff_form.html:37
 msgid "Select the base and the target for the diff:"
 msgstr "差分を取る元と対象を指定して下さい:"
 
-#: trac/versioncontrol/templates/diff_form.html:30
+#: trac/versioncontrol/templates/diff_form.html:40
 msgid "From:"
 msgstr "元のパス:"
 
-#: trac/versioncontrol/templates/diff_form.html:34
 #: trac/versioncontrol/templates/diff_form.html:44
+#: trac/versioncontrol/templates/diff_form.html:54
 msgid "at revision:"
 msgstr "リビジョン:"
 
-#: trac/versioncontrol/templates/diff_form.html:40
+#: trac/versioncontrol/templates/diff_form.html:50
 msgid "To:"
 msgstr "対象のパス:"
 
-#: trac/versioncontrol/templates/diff_form.html:50
+#: trac/versioncontrol/templates/diff_form.html:60
 msgid ""
 "For either path, you can start typing the path and will be\n"
 "              presented a list of existing directories and files to "
@@ -5465,7 +5764,7 @@
 "2つのパス指定欄では文字を入力すると合致する既存のディレクトリあるいはファイルの候補がポップアップ表示されます。\n"
 "その中から目的のものをクリックするか、カーソルキーで選択後にタブキーを押すことで選択することができます。"
 
-#: trac/versioncontrol/templates/diff_form.html:62
+#: trac/versioncontrol/templates/diff_form.html:72
 msgid ""
 "[1:Note:] See\n"
 "        [2:TracChangeset]\n"
@@ -5474,79 +5773,79 @@
 "[1:※] 詳しい使い方は\n"
 "[2:TracChangeset] を参照してください。"
 
-#: trac/versioncontrol/templates/dir_entries.html:12
+#: trac/versioncontrol/templates/dir_entries.html:23
 msgid "View Directory"
 msgstr "ディレクトリの表示"
 
-#: trac/versioncontrol/templates/dir_entries.html:12
+#: trac/versioncontrol/templates/dir_entries.html:23
 msgid "View File"
 msgstr "ファイルの表示"
 
-#: trac/versioncontrol/templates/dir_entries.html:18
-#: trac/versioncontrol/templates/repository_index.html:22
-#: trac/versioncontrol/web_ui/browser.py:823
+#: trac/versioncontrol/templates/dir_entries.html:29
+#: trac/versioncontrol/templates/repository_index.html:33
+#: trac/versioncontrol/web_ui/browser.py:866
 msgid "Download as Zip archive"
 msgstr "Zip 書庫でダウンロード"
 
-#: trac/versioncontrol/templates/dir_entries.html:22
-#: trac/versioncontrol/templates/repository_index.html:26
+#: trac/versioncontrol/templates/dir_entries.html:33
+#: trac/versioncontrol/templates/repository_index.html:37
 msgid "View Revision Log"
 msgstr "更新履歴の表示"
 
-#: trac/versioncontrol/templates/dir_entries.html:23
-#: trac/versioncontrol/templates/repository_index.html:27
+#: trac/versioncontrol/templates/dir_entries.html:34
+#: trac/versioncontrol/templates/repository_index.html:38
 msgid "View Changeset"
 msgstr "チェンジセットの表示"
 
-#: trac/versioncontrol/templates/dirlist_thead.html:9
-#: trac/versioncontrol/templates/revisionlog.html:110
-#: trac/versioncontrol/web_ui/browser.py:831
+#: trac/versioncontrol/templates/dirlist_thead.html:20
+#: trac/versioncontrol/templates/revisionlog.html:120
+#: trac/versioncontrol/web_ui/browser.py:874
 msgid "Rev"
 msgstr "Rev"
 
-#: trac/versioncontrol/templates/dirlist_thead.html:12
-#: trac/versioncontrol/web_ui/browser.py:457
+#: trac/versioncontrol/templates/dirlist_thead.html:23
+#: trac/versioncontrol/web_ui/browser.py:475
 msgid "Last Change"
 msgstr "最終更新内容"
 
-#: trac/versioncontrol/templates/path_links.html:17
+#: trac/versioncontrol/templates/path_links.html:27
 msgid "Go to repository index"
 msgstr "リポジトリ一覧に移動する"
 
-#: trac/versioncontrol/templates/path_links.html:17
+#: trac/versioncontrol/templates/path_links.html:27
 msgid "Go to repository root"
 msgstr "リポジトリのルートに移動する"
 
-#: trac/versioncontrol/templates/path_links.html:26
+#: trac/versioncontrol/templates/path_links.html:36
 #, python-format
 msgid "View %(name)s"
 msgstr "%(name)s 参照"
 
-#: trac/versioncontrol/templates/repository_index.html:15
+#: trac/versioncontrol/templates/repository_index.html:26
 msgid "View Root Directory"
 msgstr "ルートディレクトリに移動"
 
-#: trac/versioncontrol/templates/revisionlog.html:10
+#: trac/versioncontrol/templates/revisionlog.html:20
 msgid "(log)"
 msgstr "(ログ)"
 
-#: trac/versioncontrol/templates/revisionlog.html:36
+#: trac/versioncontrol/templates/revisionlog.html:46
 msgid "Revision Log Mode:"
 msgstr "更新履歴の表示"
 
-#: trac/versioncontrol/templates/revisionlog.html:40
+#: trac/versioncontrol/templates/revisionlog.html:50
 msgid "Stop on copy"
 msgstr "コピーされた時点まで"
 
-#: trac/versioncontrol/templates/revisionlog.html:46
+#: trac/versioncontrol/templates/revisionlog.html:56
 msgid "Follow copies"
 msgstr "コピー元をたどる"
 
-#: trac/versioncontrol/templates/revisionlog.html:52
+#: trac/versioncontrol/templates/revisionlog.html:62
 msgid "Show only adds and deletes"
 msgstr "追加と削除だけ"
 
-#: trac/versioncontrol/templates/revisionlog.html:57
+#: trac/versioncontrol/templates/revisionlog.html:67
 msgid ""
 "[1:\n"
 "              View log starting at\n"
@@ -5558,7 +5857,7 @@
 "            ]"
 msgstr "[1:リビジョン[2:]][3:から[4:]までのログを表示]"
 
-#: trac/versioncontrol/templates/revisionlog.html:67
+#: trac/versioncontrol/templates/revisionlog.html:77
 msgid ""
 "[1:\n"
 "              Show at most\n"
@@ -5567,78 +5866,78 @@
 "            ]"
 msgstr "[1:1ページあたり最大[2:]リビジョンを表示]"
 
-#: trac/versioncontrol/templates/revisionlog.html:75
+#: trac/versioncontrol/templates/revisionlog.html:85
 msgid "Show full log messages"
 msgstr "ログメッセージ全体を表示"
 
-#: trac/versioncontrol/templates/revisionlog.html:93
+#: trac/versioncontrol/templates/revisionlog.html:103
 msgid "Copied or renamed"
 msgstr "コピーまたは名前の変更"
 
-#: trac/versioncontrol/templates/revisionlog.html:101
-#: trac/versioncontrol/templates/revisionlog.html:204
+#: trac/versioncontrol/templates/revisionlog.html:111
+#: trac/versioncontrol/templates/revisionlog.html:214
 msgid "Diff from Old Revision to New Revision (as selected in the Diff column)"
 msgstr "差分列で選択した2つのリビジョンを比較する"
 
-#: trac/versioncontrol/templates/revisionlog.html:107
+#: trac/versioncontrol/templates/revisionlog.html:117
 msgid "Graph"
 msgstr "グラフ"
 
-#: trac/versioncontrol/templates/revisionlog.html:108
+#: trac/versioncontrol/templates/revisionlog.html:118
 msgid "Old / New"
 msgstr "古 / 新"
 
-#: trac/versioncontrol/templates/revisionlog.html:108
+#: trac/versioncontrol/templates/revisionlog.html:118
 msgid "Diff"
 msgstr "差分"
 
-#: trac/versioncontrol/templates/revisionlog.html:111
+#: trac/versioncontrol/templates/revisionlog.html:121
 msgid "Age"
 msgstr "時期"
 
-#: trac/versioncontrol/templates/revisionlog.html:113
+#: trac/versioncontrol/templates/revisionlog.html:123
 msgid "Log Message"
 msgstr "ログメッセージ"
 
-#: trac/versioncontrol/templates/revisionlog.html:121
+#: trac/versioncontrol/templates/revisionlog.html:131
 msgid "No revisions found"
 msgstr "一致するものが見つかりませんでした。"
 
-#: trac/versioncontrol/templates/revisionlog.html:135
+#: trac/versioncontrol/templates/revisionlog.html:145
 #, python-format
 msgid "copied from [1:%(path)s]:"
 msgstr "[1:%(path)s] からのコピー:"
 
-#: trac/versioncontrol/templates/revisionlog.html:142
+#: trac/versioncontrol/templates/revisionlog.html:152
 #, python-format
 msgid "From [%(rev)s]"
 msgstr "[%(rev)s] から"
 
-#: trac/versioncontrol/templates/revisionlog.html:145
+#: trac/versioncontrol/templates/revisionlog.html:155
 #, python-format
 msgid "To [%(rev)s]"
 msgstr "[%(rev)s] まで"
 
-#: trac/versioncontrol/templates/revisionlog.html:151
+#: trac/versioncontrol/templates/revisionlog.html:161
 msgid "View log starting at this revision"
 msgstr "このリビジョン以前のログを表示する"
 
-#: trac/versioncontrol/templates/revisionlog.html:158
+#: trac/versioncontrol/templates/revisionlog.html:168
 #, python-format
 msgid "Browse at revision %(rev)s"
 msgstr "リビジョン %(rev)s を表示する"
 
-#: trac/versioncontrol/templates/revisionlog.html:162
+#: trac/versioncontrol/templates/revisionlog.html:172
 #, python-format
 msgid "View removal changeset [%(rev)s]"
 msgstr "削除のチェンジセット [%(rev)s] を表示する"
 
-#: trac/versioncontrol/templates/revisionlog.html:164
+#: trac/versioncontrol/templates/revisionlog.html:174
 #, python-format
 msgid "View changeset [%(rev)s] restricted to %(path)s"
 msgstr "%(path)s に限定してチェンジセット [%(rev)s] を表示する"
 
-#: trac/versioncontrol/templates/revisionlog.html:209
+#: trac/versioncontrol/templates/revisionlog.html:219
 msgid ""
 "[1:Note:] See [2:TracRevisionLog]\n"
 "        for help on using the revision log."
@@ -5667,70 +5966,78 @@
 msgid "Invalid changeset number"
 msgstr "不正なチェンジセット番号"
 
-#: trac/versioncontrol/web_ui/browser.py:400
+#: trac/versioncontrol/web_ui/browser.py:416
+msgid "No viewable repositories"
+msgstr "参照可能なリポジトリはありません"
+
+#: trac/versioncontrol/web_ui/browser.py:418
 #, python-format
 msgid "No node %(path)s"
 msgstr "%(path)s はありません"
 
-#: trac/versioncontrol/web_ui/browser.py:440
-#: trac/versioncontrol/web_ui/browser.py:450
+#: trac/versioncontrol/web_ui/browser.py:458
+#: trac/versioncontrol/web_ui/browser.py:468
 #, python-format
 msgid "Revision %(num)s"
 msgstr "リビジョン %(num)s"
 
-#: trac/versioncontrol/web_ui/browser.py:451
+#: trac/versioncontrol/web_ui/browser.py:469
 msgid "Previous Revision"
 msgstr "前のリビジョン"
 
-#: trac/versioncontrol/web_ui/browser.py:451
+#: trac/versioncontrol/web_ui/browser.py:469
 msgid "Next Revision"
 msgstr "次のリビジョン"
 
-#: trac/versioncontrol/web_ui/browser.py:452
+#: trac/versioncontrol/web_ui/browser.py:470
 msgid "Latest Revision"
 msgstr "最新リビジョン"
 
-#: trac/versioncontrol/web_ui/browser.py:456
-#: trac/versioncontrol/web_ui/log.py:315
+#: trac/versioncontrol/web_ui/browser.py:474
+#: trac/versioncontrol/web_ui/log.py:323
 msgid "Parent directory"
 msgstr "親ディレクトリ"
 
-#: trac/versioncontrol/web_ui/browser.py:463
+#: trac/versioncontrol/web_ui/browser.py:481
 msgid "Normal"
 msgstr "通常表示"
 
-#: trac/versioncontrol/web_ui/browser.py:464
+#: trac/versioncontrol/web_ui/browser.py:482
 msgid "View file without annotations"
 msgstr "注釈なしでファイルを表示する"
 
-#: trac/versioncontrol/web_ui/browser.py:469
+#: trac/versioncontrol/web_ui/browser.py:487
 msgid "Blame"
 msgstr "注釈履歴"
 
-#: trac/versioncontrol/web_ui/browser.py:470
+#: trac/versioncontrol/web_ui/browser.py:488
 msgid ""
 "Annotate each line with the last changed revision (this can be time "
 "consuming...)"
 msgstr "各行に変更されたりビジョンを注釈として付け加えます(時間がかかります)"
 
-#: trac/versioncontrol/web_ui/browser.py:477
+#: trac/versioncontrol/web_ui/browser.py:495
 msgid "Revision Log"
 msgstr "更新履歴"
 
-#: trac/versioncontrol/web_ui/browser.py:483
+#: trac/versioncontrol/web_ui/browser.py:501
 msgid "Repository URL"
 msgstr "リポジトリ URL"
 
-#: trac/versioncontrol/web_ui/browser.py:612
-#: trac/versioncontrol/web_ui/changeset.py:364
+#: trac/versioncontrol/web_ui/browser.py:631
+#: trac/versioncontrol/web_ui/changeset.py:365
 msgid "Zip Archive"
 msgstr "Zip 書庫"
 
-#: trac/versioncontrol/web_ui/browser.py:831
+#: trac/versioncontrol/web_ui/browser.py:654
+msgid "Path not available for download"
+msgstr "ダウンロードが利用可能なパスではありません"
+
+#: trac/versioncontrol/web_ui/browser.py:874
 msgid "Revision in which the line changed"
 msgstr "各行が更新されたリビジョン"
 
-#: trac/versioncontrol/web_ui/browser.py:846
+#: trac/versioncontrol/web_ui/browser.py:889
 msgid ""
 "Display the list of available repositories.\n"
 "\n"
@@ -5771,116 +6078,122 @@
 "\n"
 "(''0.12 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:913
+#: trac/versioncontrol/web_ui/browser.py:956
 #, python-format
 msgid "View repository %(repo)s"
 msgstr "リポジトリ %(repo)s を表示する"
 
-#: trac/versioncontrol/web_ui/changeset.py:241
+#: trac/versioncontrol/web_ui/changeset.py:105
+#, python-format
+msgid "Property %(name)s"
+msgstr "プロパティ %(name)s"
+
+#: trac/versioncontrol/web_ui/changeset.py:243
 #, python-format
 msgid "Can't compare across different repositories: %(old)s vs. %(new)s"
 msgstr "別のリポジトリとの間で比較を行うことはできません: %(old)s と %(new)s"
 
-#: trac/versioncontrol/web_ui/changeset.py:250
+#: trac/versioncontrol/web_ui/changeset.py:252
+#: trac/versioncontrol/web_ui/log.py:93
 msgid "No repository specified and no default repository configured."
 msgstr "リポジトリが指定されておらず、また、デフォルトリポジトリが設定されていません。"
 
-#: trac/versioncontrol/web_ui/changeset.py:262
+#: trac/versioncontrol/web_ui/changeset.py:260
 msgid "Invalid Changeset Number"
 msgstr "不正なチェンジセット番号"
 
-#: trac/versioncontrol/web_ui/changeset.py:362
+#: trac/versioncontrol/web_ui/changeset.py:363
 msgid "Unified Diff"
 msgstr "差分形式(unified)"
 
-#: trac/versioncontrol/web_ui/changeset.py:373
+#: trac/versioncontrol/web_ui/changeset.py:374
 msgid "Previous Changeset"
 msgstr "前のチェンジセット"
 
-#: trac/versioncontrol/web_ui/changeset.py:373
+#: trac/versioncontrol/web_ui/changeset.py:374
 msgid "Next Changeset"
 msgstr "次のチェンジセット"
 
-#: trac/versioncontrol/web_ui/changeset.py:377
+#: trac/versioncontrol/web_ui/changeset.py:378
 msgid "Reverse Diff"
 msgstr "変更内容を反転"
 
-#: trac/versioncontrol/web_ui/changeset.py:415
+#: trac/versioncontrol/web_ui/changeset.py:416
 #, python-format
 msgid "Changeset %(id)s for %(path)s"
 msgstr "%(path)s のチェンジセット %(id)s"
 
-#: trac/versioncontrol/web_ui/changeset.py:418
-#: trac/versioncontrol/web_ui/changeset.py:444
-#: trac/versioncontrol/web_ui/changeset.py:466
+#: trac/versioncontrol/web_ui/changeset.py:419
+#: trac/versioncontrol/web_ui/changeset.py:445
+#: trac/versioncontrol/web_ui/changeset.py:467
 #, python-format
 msgid "Changeset %(id)s"
 msgstr "チェンジセット %(id)s"
 
-#: trac/versioncontrol/web_ui/changeset.py:493
+#: trac/versioncontrol/web_ui/changeset.py:494
 #, python-format
 msgid "Show revision %(rev)s of this file in browser"
 msgstr "このファイルのリビジョン %(rev)s を表示する"
 
-#: trac/versioncontrol/web_ui/changeset.py:639
+#: trac/versioncontrol/web_ui/changeset.py:640
 #, python-format
 msgid "Show the changeset %(id)s restricted to %(path)s"
 msgstr "%(path)s に限定してチェンジセット %(id)s を表示する"
 
-#: trac/versioncontrol/web_ui/changeset.py:651
+#: trac/versioncontrol/web_ui/changeset.py:652
 #, python-format
 msgid "Show the %(range)s differences restricted to %(path)s"
 msgstr "%(path)s に限定して範囲 %(range)s の差分を表示する"
 
 #. TRANSLATOR: 'latest' (revision)
-#: trac/versioncontrol/web_ui/changeset.py:800
+#: trac/versioncontrol/web_ui/changeset.py:770
 msgid "latest"
 msgstr "最新"
 
-#: trac/versioncontrol/web_ui/changeset.py:803
+#: trac/versioncontrol/web_ui/changeset.py:773
 #, python-format
 msgid "Diff [%(old_rev)s:%(new_rev)s] for %(path)s"
 msgstr "%(path)s の [%(old_rev)s:%(new_rev)s] における差分"
 
-#: trac/versioncontrol/web_ui/changeset.py:809
+#: trac/versioncontrol/web_ui/changeset.py:779
 #, python-format
 msgid "Diff from %(old_path)s@%(old_rev)s to %(new_path)s@%(new_rev)s"
 msgstr "%(old_path)s@%(old_rev)s から %(new_path)s@%(new_rev)s への差分"
 
-#: trac/versioncontrol/web_ui/changeset.py:881
+#: trac/versioncontrol/web_ui/changeset.py:851
 msgid "Changesets in all repositories"
 msgstr "すべてのリポジトリのチェンジセット"
 
-#: trac/versioncontrol/web_ui/changeset.py:883
+#: trac/versioncontrol/web_ui/changeset.py:853
 msgid "Repository changesets"
 msgstr "リポジトリのチェンジセット"
 
-#: trac/versioncontrol/web_ui/changeset.py:1019
+#: trac/versioncontrol/web_ui/changeset.py:989
 #, python-format
 msgid "Changeset in %(repo)s "
 msgid_plural "Changesets in %(repo)s "
 msgstr[0] "チェンジセット (%(repo)s)"
 
-#: trac/versioncontrol/web_ui/changeset.py:1021
+#: trac/versioncontrol/web_ui/changeset.py:991
 msgid "Changeset "
 msgid_plural "Changesets "
 msgstr[0] "チェンジセット"
 
-#: trac/versioncontrol/web_ui/changeset.py:1102
+#: trac/versioncontrol/web_ui/changeset.py:1072
 #, python-format
 msgid "No permission to view changeset %(rev)s on %(repos)s"
 msgstr "%(repos)s の チェンジセット %(rev)s にアクセスする権限が不足しています"
 
-#: trac/versioncontrol/web_ui/changeset.py:1106
-#: trac/versioncontrol/web_ui/log.py:415
+#: trac/versioncontrol/web_ui/changeset.py:1076
+#: trac/versioncontrol/web_ui/log.py:428
 msgid "No default repository defined"
 msgstr "デフォルトのリポジトリを定義していません"
 
-#: trac/versioncontrol/web_ui/changeset.py:1147
+#: trac/versioncontrol/web_ui/changeset.py:1117
 msgid "Changesets"
 msgstr "チェンジセット"
 
-#: trac/versioncontrol/web_ui/log.py:209
+#: trac/versioncontrol/web_ui/log.py:215
 #, python-format
 msgid ""
 "The file or directory '%(path)s' doesn't exist at revision %(rev)s or at "
@@ -5889,49 +6202,54 @@
 "ファイルまたはディレクトリ '%(path)s' はリビジョン %(rev)s あるいは\"\n"
 "\"それ以前のリビジョンにおいて存在しません。"
 
-#: trac/versioncontrol/web_ui/log.py:209
+#: trac/versioncontrol/web_ui/log.py:216
 msgid "Nonexistent path"
 msgstr "存在しないパス"
 
-#: trac/versioncontrol/web_ui/log.py:249
+#: trac/versioncontrol/web_ui/log.py:257
 #, python-format
 msgid "Revision Log (restarting at %(path)s, rev. %(rev)s)"
 msgstr "更新履歴 (%(path)s のリビジョン %(rev)s から開始)"
 
-#: trac/versioncontrol/web_ui/log.py:323
+#: trac/versioncontrol/web_ui/log.py:331
 msgid "ChangeLog"
 msgstr "変更内容(ChangeLog)"
 
-#: trac/versioncontrol/web_ui/log.py:325
+#: trac/versioncontrol/web_ui/log.py:334
 msgid "View Latest Revision"
 msgstr "最新リビジョンの表示"
 
-#: trac/versioncontrol/web_ui/log.py:329
+#: trac/versioncontrol/web_ui/log.py:338
 msgid "Older Revisions"
 msgstr "古いリビジョン"
 
-#: trac/versioncontrol/web_ui/log.py:411
+#: trac/versioncontrol/web_ui/log.py:424
 msgid "No permission to view change log"
 msgstr "変更内容を参照する権限がありません"
 
 #. TRANSLATOR: You can 'search' in the repository history... (link)
-#: trac/versioncontrol/web_ui/util.py:73
+#: trac/versioncontrol/web_ui/util.py:77
 msgid "search"
 msgstr "検索"
 
-#: trac/versioncontrol/web_ui/util.py:78
+#: trac/versioncontrol/web_ui/util.py:82
 #, python-format
 msgid ""
 "You can %(search)s in the repository history to see if that path existed "
 "but was later removed"
 msgstr "削除されたパスがあれば、リポジトリの履歴から %(search)s して閲覧する事ができます。"
 
-#: trac/web/api.py:340
+#: trac/web/api.py:170
+#, python-format
+msgid "Error: %(message)s"
+msgstr "エラー: %(message)s"
+
+#: trac/web/api.py:377
 #, python-format
 msgid "Invalid URL encoding (was %(path_info)r)"
 msgstr "不正な URL エンコーディング (%(path_info)r)"
 
-#: trac/web/api.py:559
+#: trac/web/api.py:601
 #, python-format
 msgid "File %(path)s not found"
 msgstr "ファイル %(path)s が見つかりません"
@@ -5945,92 +6263,96 @@
 msgid "Logout"
 msgstr "ログアウト"
 
-#: trac/web/auth.py:114
+#: trac/web/auth.py:117
 msgid "Login"
 msgstr "ログイン"
 
 #. TRANSLATOR: ... refer to the 'installation documentation'. (link)
-#: trac/web/auth.py:147
+#: trac/web/auth.py:150
 msgid "installation documentation"
 msgstr "インストールドキュメント"
 
-#: trac/web/auth.py:148
+#: trac/web/auth.py:151
 msgid "Configuring Authentication"
 msgstr "認証の構成"
 
-#: trac/web/auth.py:151
+#: trac/web/auth.py:154
 #, python-format
 msgid ""
 "Authentication information not available. Please refer to the "
 "%(inst_doc)s."
 msgstr "認証情報が利用できません。%(inst_doc)sを参照してください。"
 
-#: trac/web/auth.py:159
+#: trac/web/auth.py:162
 #, python-format
 msgid "Already logged in as %(user)s."
 msgstr "すでに %(user)s としてログイン中です。"
 
-#: trac/web/chrome.py:734
+#: trac/web/chrome.py:626
+#, python-format
+msgid "Invalid chrome path %(path)s."
+msgstr "Invalid chrome path %(path)s は正しくない chrome パスです。"
+
+#: trac/web/chrome.py:752
 #, python-format
 msgid "Error with navigation contributor \"%(name)s\""
 msgstr "Navigation contributor \"%(name)s\" でエラー"
 
-#: trac/web/chrome.py:1029
+#: trac/web/chrome.py:1056
 msgid "(unknown template location)"
 msgstr "(ファイル名不明)"
 
-#: trac/web/chrome.py:1030
+#: trac/web/chrome.py:1057
 #, python-format
 msgid "Genshi %(error)s error while rendering template %(location)s"
 msgstr "テンプレート %(location)s のレンダリング中にGenshi %(error)s エラーが発生"
 
-#: trac/web/chrome.py:1078 trac/web/chrome.py:1086
+#: trac/web/chrome.py:1105 trac/web/chrome.py:1113
 msgid "anonymous"
 msgstr "匿名"
 
-#: trac/web/main.py:206
+#: trac/web/main.py:141
+msgid "Authentication error. Please contact your administrator."
+msgstr "認証エラーです。管理者に連絡してください。"
+
+#: trac/web/main.py:213
 msgid "Secure cookies are enabled, you must use https to submit forms."
 msgstr "Cookie のセキュリティ強化が有効になっています。フォームデータを送信するには https を使用してください。"
 
-#: trac/web/main.py:209
+#: trac/web/main.py:216
 msgid "Do you have cookies enabled?"
 msgstr "Cookie は有効になっていますか?"
 
-#: trac/web/main.py:210
+#: trac/web/main.py:217
 #, python-format
 msgid "Missing or invalid form token. %(msg)s"
 msgstr "フォームトークンが見つからない、もしくは、不正なトークンです。%(msg)s"
 
-#: trac/web/main.py:220
+#: trac/web/main.py:227
 msgid ""
 "Clearsilver templates are no longer supported, please contact your Trac "
 "administrator."
 msgstr "Clearsilver テンプレートはもうサポートしていません。Trac 管理者へお問い合わせください。"
 
-#: trac/web/main.py:521
-#, python-format
-msgid "Error: %(message)s"
-msgstr "エラー: %(message)s"
-
 #. TRANSLATOR: ... not logged in, you may want to 'do so' now (link)
-#: trac/web/main.py:537
+#: trac/web/main.py:525
 msgid "do so"
 msgstr "こちら"
 
-#: trac/web/main.py:539
+#: trac/web/main.py:526
 #, python-format
 msgid "You are currently not logged in. You may want to %(do_so)s now."
 msgstr "現在ログインしていません。%(do_so)sからログインしてください。"
 
-#: trac/web/main.py:592
+#: trac/web/main.py:586
 msgid "''System information not available''\n"
 msgstr "''システム情報が利用できません''\n"
 
-#: trac/web/main.py:593
+#: trac/web/main.py:587
 msgid "''Plugin information not available''\n"
 msgstr "''プラグイン情報が利用できません''\n"
 
-#: trac/web/main.py:617
+#: trac/web/main.py:611
 #, python-format
 msgid ""
 "==== How to Reproduce ====\n"
@@ -6076,42 +6398,42 @@
 "{{{\n"
 "%(traceback)s}}}"
 
-#: trac/web/session.py:245
+#: trac/web/session.py:246
 #, python-format
 msgid "Session '%(id)s' already exists. Please choose a different session ID."
 msgstr "セッション '%(id)s' はすでに存在します。別のセッション ID を入力してください。"
 
-#: trac/web/session.py:248
+#: trac/web/session.py:249
 msgid "Error renaming session"
 msgstr "セッション変更中のエラー:"
 
-#: trac/web/session.py:417
+#: trac/web/session.py:423
 msgid "SID"
 msgstr "SID"
 
-#: trac/web/session.py:417
+#: trac/web/session.py:423
 msgid "Auth"
 msgstr "Auth"
 
-#: trac/web/session.py:417
+#: trac/web/session.py:423
 msgid "Last Visit"
 msgstr "前回訪問"
 
-#: trac/web/session.py:418
+#: trac/web/session.py:424
 msgid "Email"
 msgstr "メールアドレス"
 
-#: trac/web/session.py:427
+#: trac/web/session.py:433
 #, python-format
 msgid "Session '%(sid)s' already exists"
 msgstr "セッション '%(sid)s' はすでに存在しています"
 
-#: trac/web/session.py:438
+#: trac/web/session.py:444
 #, python-format
 msgid "Invalid attribute '%(attr)s'"
 msgstr "属性 '%(attr)s' は不正です"
 
-#: trac/web/session.py:445
+#: trac/web/session.py:451
 #, python-format
 msgid "Session '%(sid)s' not found"
 msgstr "セッション '%(sid)s' が見つかりません"
@@ -6121,8 +6443,8 @@
 msgid "Page '%(page)s' not found"
 msgstr "ページ '%(page)s' が見つかりません"
 
-#: trac/wiki/admin.py:118 trac/wiki/model.py:127 trac/wiki/model.py:174
-#: trac/wiki/web_ui.py:119
+#: trac/wiki/admin.py:118 trac/wiki/model.py:128 trac/wiki/model.py:176
+#: trac/wiki/web_ui.py:120
 #, python-format
 msgid "Invalid Wiki page name '%(name)s'"
 msgstr "Wiki ページ名 '%(name)s' は正しくありません"
@@ -6151,7 +6473,7 @@
 msgid "Edits"
 msgstr "編集"
 
-#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:300
+#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:310
 msgid "A new name is mandatory for a rename."
 msgstr "変更後の新しい名前が必要です。"
 
@@ -6159,7 +6481,7 @@
 msgid "The new name is invalid."
 msgstr "新しい名前が不正です。"
 
-#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:307
+#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:317
 #, python-format
 msgid "The page %(name)s already exists."
 msgstr "ページ %(name)s はすでに存在しています。"
@@ -6177,48 +6499,87 @@
 msgid "no permission to view this wiki page"
 msgstr "このページを参照する権限がありません"
 
-#: trac/wiki/formatter.py:222
+#: trac/wiki/formatter.py:189
+#, python-format
+msgid "No macro or processor named '%(name)s' found"
+msgstr "'%(name)s' という名前のマクロまたはプロセッサはありません。"
+
+#: trac/wiki/formatter.py:224
 #, python-format
 msgid "HTML parsing error: %(message)s"
 msgstr "HTMLパースエラー: %(message)s"
 
-#: trac/wiki/formatter.py:226
+#: trac/wiki/formatter.py:228
 msgid "Error: Forbidden character sequence \"--\" in htmlcomment wiki code block"
 msgstr "エラー: htmlcomment ブロック内では文字列 \"--\" は許可していません"
 
-#: trac/wiki/formatter.py:304
+#: trac/wiki/formatter.py:306
 #, python-format
 msgid "!#%(name)s must contain at most one table"
 msgstr "!#%(name)s には2つ以上のテーブルを含められません"
 
-#: trac/wiki/formatter.py:308
+#: trac/wiki/formatter.py:310
 #, python-format
 msgid "!#%(name)s must contain at least one table cell (and table cells only)"
 msgstr "!#%(name)s には少なくとも1つテーブルのセルがないといけません"
 
-#: trac/wiki/formatter.py:684 trac/wiki/interwiki.py:104
+#: trac/wiki/formatter.py:355
+#, python-format
+msgid "Error: Failed to load processor %(name)s"
+msgstr "エラー: プロセッサ %(name)s のロードに失敗しました"
+
+#: trac/wiki/formatter.py:707 trac/wiki/interwiki.py:104
 #, python-format
 msgid "%(target)s in %(name)s"
 msgstr "%(name)s の %(target)s"
 
+#: trac/wiki/formatter.py:792
+#, python-format
+msgid "Error: Macro %(name)s(%(args)s) failed"
+msgstr "エラー: マクロ %(name)s(%(args)s) が失敗しました"
+
+#: trac/wiki/formatter.py:1185
+#, python-format
+msgid "Error: Processor %(name)s failed"
+msgstr "エラー: プロセッサ %(name)s が失敗しました"
+
 #: trac/wiki/intertrac.py:94
 #, python-format
-msgid "Can't view %(link)s:"
-msgstr "%(link)s が表示できません:"
+msgid ""
+"Can't view %(link)s. Resource doesn't exist or you don't have the "
+"required permission."
+msgstr "%(link)s が表示できません。リソースが存在しないか表示に必要な権限がありません。"
 
-#: trac/wiki/intertrac.py:106
+#: trac/wiki/intertrac.py:107
 msgid "Provide a list of known InterTrac prefixes."
 msgstr "設定済みの InterTrac プレフィックスの一覧です。"
 
-#: trac/wiki/intertrac.py:119
+#: trac/wiki/intertrac.py:120
 msgid "The Trac Project"
 msgstr "Trac プロジェクト"
 
+#: trac/wiki/intertrac.py:128
+#, python-format
+msgid "Alias for %(name)s"
+msgstr "%(name)s のエイリアス"
+
+#: trac/wiki/intertrac.py:138 trac/wiki/interwiki.py:175
+msgid "Prefix"
+msgstr "プレフィックス"
+
+#: trac/wiki/intertrac.py:139
+msgid "Trac Site"
+msgstr "Trac サイト"
+
 #: trac/wiki/interwiki.py:163
 msgid "Provide a description list for the known InterWiki prefixes."
 msgstr "設定済みの InterWiki プレフィックスの一覧です。"
 
-#: trac/wiki/macros.py:83
+#: trac/wiki/interwiki.py:176
+msgid "Site"
+msgstr "サイト"
+
+#: trac/wiki/macros.py:85
 msgid ""
 "Insert an alphabetic list of all wiki pages into the output.\n"
 "\n"
@@ -6277,7 +6638,7 @@
 "\n"
 "`include` と `exclude` はシェル形式のパターンを受け付けます。"
 
-#: trac/wiki/macros.py:296
+#: trac/wiki/macros.py:298
 msgid ""
 "List all pages that have recently been modified, ordered by the\n"
 "time they were last modified.\n"
@@ -6318,7 +6679,7 @@
 "ヒント: プレフィックス文字列でフィルタを行わずに項目の最大数のみを指定したい場合には、1つ目のパラメータを空にします。例えば "
 "`[[RecentChanges(,10,group=none)]]` とします。"
 
-#: trac/wiki/macros.py:374
+#: trac/wiki/macros.py:378
 msgid ""
 "Display a structural outline of the current wiki page, each item in the\n"
 "outline being a link to the corresponding heading.\n"
@@ -6365,7 +6726,7 @@
 " * 4つ目のパラメータはアウトラインを番号付きにするかどうかを指定します。`numbered` または `unnumbered` "
 "のどちらかにすることができます (前者がデフォルトです)。このパラメータは `inline` スタイルでのみ効果があります。"
 
-#: trac/wiki/macros.py:440
+#: trac/wiki/macros.py:444
 msgid ""
 "Embed an image in wiki-formatted text.\n"
 "\n"
@@ -6414,24 +6775,22 @@
 "\n"
 "Examples:\n"
 "{{{\n"
-"    [[Image(photo.jpg)]]                           # simplest\n"
-"    [[Image(photo.jpg, 120px)]]                    # with image width "
-"size\n"
-"    [[Image(photo.jpg, right)]]                    # aligned by keyword\n"
-"    [[Image(photo.jpg, nolink)]]                   # without link to "
-"source\n"
-"    [[Image(photo.jpg, align=right)]]              # aligned by attribute"
-"\n"
+"[[Image(photo.jpg)]]               # simplest\n"
+"[[Image(photo.jpg, 120px)]]        # with image width size\n"
+"[[Image(photo.jpg, right)]]        # aligned by keyword\n"
+"[[Image(photo.jpg, nolink)]]       # without link to source\n"
+"[[Image(photo.jpg, align=right)]]  # aligned by attribute\n"
 "}}}\n"
 "\n"
-"You can use image from other page, other ticket or other module.\n"
+"You can use an image from a wiki page, ticket or other module.\n"
 "{{{\n"
-"    [[Image(OtherPage:foo.bmp)]]    # if current module is wiki\n"
-"    [[Image(base/sub:bar.bmp)]]     # from hierarchical wiki page\n"
-"    [[Image(#3:baz.bmp)]]           # if in a ticket, point to #3\n"
-"    [[Image(ticket:36:boo.jpg)]]\n"
-"    [[Image(source:/images/bee.jpg)]] # straight from the repository!\n"
-"    [[Image(htdocs:foo/bar.png)]]   # image file in project htdocs dir.\n"
+"[[Image(OtherPage:foo.bmp)]]    # from a wiki page\n"
+"[[Image(base/sub:bar.bmp)]]     # from hierarchical wiki page\n"
+"[[Image(#3:baz.bmp)]]           # from another ticket\n"
+"[[Image(ticket:36:boo.jpg)]]    # from another ticket (long form)\n"
+"[[Image(source:/img/bee.jpg)]]  # from the repository\n"
+"[[Image(htdocs:foo/bar.png)]]   # from project htdocs dir\n"
+"[[Image(shared:foo/bar.png)]]   # from shared htdocs dir (since 1.0.2)\n"
 "}}}\n"
 "\n"
 "''Adapted from the Image.py macro created by Shun-ichi Goto\n"
@@ -6468,31 +6827,32 @@
 "\n"
 "例:\n"
 "{{{\n"
-"    [[Image(photo.jpg)]]                           # 単純な形式\n"
-"    [[Image(photo.jpg, 120px)]]                    # 画像サイズ付き\n"
-"    [[Image(photo.jpg, right)]]                    # キーワードでの整列\n"
-"    [[Image(photo.jpg, nolink)]]                   # 元ファイルへのリンクなし\n"
-"    [[Image(photo.jpg, align=right)]]              # 属性での整列\n"
+"[[Image(photo.jpg)]]               # 単純な形式\n"
+"[[Image(photo.jpg, 120px)]]        # 画像サイズ付き\n"
+"[[Image(photo.jpg, right)]]        # キーワードでの整列\n"
+"[[Image(photo.jpg, nolink)]]       # 元ファイルへのリンクなし\n"
+"[[Image(photo.jpg, align=right)]]  # 属性での整列\n"
 "}}}\n"
 "\n"
-"他のページやチケット、他のモジュールの画像を使うことができます。\n"
+"Wiki ページやチケット、他のモジュールの画像を使うことができます。\n"
 "{{{\n"
-"    [[Image(OtherPage:foo.bmp)]]    # 現在のモジュールが Wiki の場合\n"
-"    [[Image(base/sub:bar.bmp)]]     # 階層化している Wiki ページから\n"
-"    [[Image(#3:baz.bmp)]]           # #3 を指すチケットにある場合\n"
-"    [[Image(ticket:36:boo.jpg)]]\n"
-"    [[Image(source:/images/bee.jpg)]] # リポジトリから直接\n"
-"    [[Image(htdocs:foo/bar.png)]]   # プロジェクトの htdocs ディレクトリにある画像ファイル\n"
+"[[Image(OtherPage:foo.bmp)]]    # Wiki ページから\n"
+"[[Image(base/sub:bar.bmp)]]     # 階層化している Wiki ページから\n"
+"[[Image(#3:baz.bmp)]]           # チケットから\n"
+"[[Image(ticket:36:boo.jpg)]]    # チケットから (長い形式)\n"
+"[[Image(source:/img/bee.jpg)]]  # リポジトリから\n"
+"[[Image(htdocs:foo/bar.png)]]   # プロジェクトの htdocs ディレクトリから\n"
+"[[Image(shared:foo/bar.png)]]   # 共有している htdocs ディレクトリから (1.0.2 以降)\n"
 "}}}\n"
 "\n"
 "''Shun-ichi Goto <gotoh@taiyo.co.jp> が作成した Image.py マクロが元になっています。''"
 
-#: trac/wiki/macros.py:648
+#: trac/wiki/macros.py:659
 #, python-format
 msgid "No image \"%(id)s\" attached to %(parent)s"
 msgstr "画像 \"%(id)s\" は %(parent)s に添付してありません"
 
-#: trac/wiki/macros.py:664
+#: trac/wiki/macros.py:675
 msgid ""
 "Display a list of all installed Wiki macros, including documentation if\n"
 "available.\n"
@@ -6510,20 +6870,20 @@
 "\n"
 "※ このマクロは mod_python の `PythonOptimize` オプションを有効にしているとマクロのドキュメントを表示できません。"
 
-#: trac/wiki/macros.py:693
+#: trac/wiki/macros.py:704
 #, python-format
 msgid "Error: Can't get description for macro %(name)s"
 msgstr "エラー: マクロ %(name)s の詳細説明が取得できません"
 
-#: trac/wiki/macros.py:716
+#: trac/wiki/macros.py:727
 msgid "Aliases:"
 msgstr "エイリアス:"
 
-#: trac/wiki/macros.py:719
+#: trac/wiki/macros.py:730
 msgid "Sorry, no documentation found"
 msgstr "ドキュメントが見つかりません"
 
-#: trac/wiki/macros.py:726
+#: trac/wiki/macros.py:737
 msgid ""
 "Produce documentation for the Trac configuration file.\n"
 "\n"
@@ -6537,11 +6897,11 @@
 "これは通常 TracIni "
 "ページで使います。引数を指定するとセクションとオプションの名前のフィルタになり、そのフィルタの内容で始まるセクションとオプションのみを出力します。"
 
-#: trac/wiki/macros.py:775
+#: trac/wiki/macros.py:779
 msgid "(no default)"
 msgstr "(デフォルトなし)"
 
-#: trac/wiki/macros.py:795
+#: trac/wiki/macros.py:800
 msgid ""
 "List all known mime-types which can be used as WikiProcessors.\n"
 "\n"
@@ -6552,11 +6912,11 @@
 "\n"
 "引数を指定すると mime-type をフィルタリングすることができます。"
 
-#: trac/wiki/macros.py:818
+#: trac/wiki/macros.py:823
 msgid "MIME Types"
 msgstr "MIME タイプ"
 
-#: trac/wiki/macros.py:833
+#: trac/wiki/macros.py:838
 msgid ""
 "Display a table of content for the Trac guide.\n"
 "\n"
@@ -6571,190 +6931,208 @@
 "目次は Trac* と WikiFormatting ページを含んでいてカスタマイズできません。\n"
 "目次のカスタマイズには !TocMacro を検索してください。"
 
-#: trac/wiki/macros.py:879
+#: trac/wiki/macros.py:884
 msgid "Table of Contents"
 msgstr "目次"
 
-#: trac/wiki/model.py:132
+#: trac/wiki/model.py:88
+msgid "Cannot delete non-existent page"
+msgstr "存在しないページでは削除を行えません"
+
+#: trac/wiki/model.py:133
 msgid "Page not modified"
 msgstr "ページは更新されませんでした"
 
-#: trac/wiki/model.py:181
+#: trac/wiki/model.py:173
+msgid "Cannot rename non-existent page"
+msgstr "存在しないページでは名前の変更を行えません"
+
+#: trac/wiki/model.py:183
 #, python-format
 msgid "Can't rename to existing %(name)s page."
 msgstr "存在している %(name)s ページに名前を変更できません。"
 
-#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:754
+#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:772
 msgid "Wiki"
 msgstr "Wiki"
 
-#: trac/wiki/web_ui.py:89
+#: trac/wiki/web_ui.py:90
 msgid "Help/Guide"
 msgstr "ヘルプ/ガイド"
 
-#: trac/wiki/web_ui.py:130
+#: trac/wiki/web_ui.py:128 trac/wiki/web_ui.py:138
 #, python-format
 msgid "No version \"%(num)s\" for Wiki page \"%(name)s\""
 msgstr "Wiki ページ \"%(name)s\" のバージョン \"%(num)s\" はありません"
 
-#: trac/wiki/web_ui.py:195
+#: trac/wiki/web_ui.py:204
 #, python-format
 msgid "The wiki page is too long (must be less than %(num)s characters)"
 msgstr "Wiki ページの内容が長過ぎます (%(num)s文字未満にしてください)"
 
-#: trac/wiki/web_ui.py:205
+#: trac/wiki/web_ui.py:214
 #, python-format
 msgid "The Wiki page field '%(field)s' is invalid: %(message)s"
 msgstr "Wiki ページのフィールド '%(field)s' が不正です: %(message)s"
 
-#: trac/wiki/web_ui.py:209
+#: trac/wiki/web_ui.py:218
 #, python-format
 msgid "Invalid Wiki page: %(message)s"
 msgstr "不正な Wiki ページです: %(message)s"
 
 #. TRANSLATOR: wiki page
-#: trac/wiki/web_ui.py:236
+#: trac/wiki/web_ui.py:245
 msgid "currently edited"
 msgstr "編集中のもの"
 
-#: trac/wiki/web_ui.py:269
+#: trac/wiki/web_ui.py:279
 #, python-format
 msgid "The page %(name)s has been deleted."
 msgstr "%(name)s を削除しました。"
 
-#: trac/wiki/web_ui.py:274
+#: trac/wiki/web_ui.py:284
 #, python-format
 msgid "The versions %(from_)d to %(to)d of the page %(name)s have been deleted."
 msgstr "ページ %(name)s のバージョン %(from_)d から %(to)d を削除しました。"
 
-#: trac/wiki/web_ui.py:278
+#: trac/wiki/web_ui.py:288
 #, python-format
 msgid "The version %(version)d of the page %(name)s has been deleted."
 msgstr "ページ %(name)s のバージョン %(version)d を削除しました。"
 
-#: trac/wiki/web_ui.py:302
+#: trac/wiki/web_ui.py:312
 msgid ""
 "The new name is invalid (a name which is separated with slashes cannot be"
 " '.' or '..')."
 msgstr "新しい名前が正しくありません (スラッシュで分割した名前を '.' または '..' にできません)。"
 
-#: trac/wiki/web_ui.py:305
+#: trac/wiki/web_ui.py:315
 msgid "The new name must be different from the old name."
 msgstr "新しい名前は元の名前とは違うものにしてください。"
 
-#: trac/wiki/web_ui.py:316
+#: trac/wiki/web_ui.py:326
 #, python-format
 msgid "See [wiki:\"%(name)s\"]."
 msgstr "[wiki:\"%(name)s\"] を見る。"
 
-#: trac/wiki/web_ui.py:340
+#: trac/wiki/web_ui.py:332
+#, python-format
+msgid "The page %(old_name)s has been renamed to %(new_name)s."
+msgstr "ページ %(old_name)s を %(new_name)s に変更しました。"
+
+#: trac/wiki/web_ui.py:336
+#, python-format
+msgid "The page %(old_name)s has been recreated with a redirect to %(new_name)s."
+msgstr "ページ %(old_name)s に %(new_name)s へのリダイレクト用のページを作成しました。"
+
+#: trac/wiki/web_ui.py:358
 #, python-format
 msgid "Your changes have been saved in version %(version)s."
 msgstr "バージョン %(version)s に変更を保存しました。"
 
-#: trac/wiki/web_ui.py:345
+#: trac/wiki/web_ui.py:363
 msgid "Page not modified, showing latest version."
 msgstr "変更がありません、最新のバージョンを表示します。"
 
-#: trac/wiki/web_ui.py:399
+#: trac/wiki/web_ui.py:417
 #, python-format
 msgid "Version %(num)s of page \"%(name)s\" does not exist"
 msgstr "ページ %(name)s のバージョン %(num)s は存在しません"
 
-#: trac/wiki/web_ui.py:451
+#: trac/wiki/web_ui.py:469
 msgid "Page history"
 msgstr "ページ履歴"
 
-#: trac/wiki/web_ui.py:469
+#: trac/wiki/web_ui.py:487
 msgid "Wiki History"
 msgstr "ページ履歴"
 
-#: trac/wiki/web_ui.py:499
+#: trac/wiki/web_ui.py:519
 #, python-format
 msgid "Reverted to version %(version)s."
 msgstr "バージョン %(version)s に戻しました。"
 
-#: trac/wiki/web_ui.py:562
+#: trac/wiki/web_ui.py:584
 #, python-format
 msgid "Page %(name)s does not exist"
 msgstr "ページ %(name)s は存在しません"
 
-#: trac/wiki/web_ui.py:576
+#: trac/wiki/web_ui.py:598
 #, python-format
 msgid "Back to %(wikipage)s"
 msgstr "%(wikipage)s に戻る"
 
-#: trac/wiki/web_ui.py:604
+#: trac/wiki/web_ui.py:623
 #, python-format
 msgid "Page %(name)s not found"
 msgstr "ページ %(name)s が見つかりません"
 
-#: trac/wiki/web_ui.py:658
+#: trac/wiki/web_ui.py:676
 msgid "View latest version"
 msgstr "最新バージョンを表示する"
 
-#: trac/wiki/web_ui.py:662
+#: trac/wiki/web_ui.py:680
 msgid "View parent page"
 msgstr "上位のページを表示"
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:689
 msgid "Previous Version"
 msgstr "前のバージョン"
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:689
 msgid "Next Version"
 msgstr "次のバージョン"
 
-#: trac/wiki/web_ui.py:672
+#: trac/wiki/web_ui.py:690
 msgid "View Latest Version"
 msgstr "最新バージョンの表示"
 
-#: trac/wiki/web_ui.py:675
+#: trac/wiki/web_ui.py:693
 msgid "Up"
 msgstr "上へ"
 
-#: trac/wiki/web_ui.py:700
+#: trac/wiki/web_ui.py:718
 msgid "Start Page"
 msgstr "スタートページ"
 
-#: trac/wiki/web_ui.py:701
+#: trac/wiki/web_ui.py:719
 msgid "Index"
 msgstr "ページ一覧"
 
-#: trac/wiki/web_ui.py:703
+#: trac/wiki/web_ui.py:721
 msgid "History"
 msgstr "履歴"
 
-#: trac/wiki/web_ui.py:710
+#: trac/wiki/web_ui.py:728
 msgid "Wiki changes"
 msgstr "Wiki ページの更新"
 
-#: trac/wiki/web_ui.py:737
+#: trac/wiki/web_ui.py:755
 #, python-format
 msgid "%(page)s edited"
 msgstr "%(page)s を更新しました"
 
-#: trac/wiki/web_ui.py:739
+#: trac/wiki/web_ui.py:757
 #, python-format
 msgid "%(page)s created"
 msgstr "%(page)s を作成しました"
 
-#: trac/wiki/templates/wiki_delete.html:18
+#: trac/wiki/templates/wiki_delete.html:28
 #, python-format
 msgid "Delete versions %(from)s to %(to)s of [1:%(name)s]"
 msgstr "[1:%(name)s] のバージョン %(from)s から %(to)s を削除しました。"
 
-#: trac/wiki/templates/wiki_delete.html:23
+#: trac/wiki/templates/wiki_delete.html:33
 #, python-format
 msgid "Delete version %(version)s of [1:%(name)s]"
 msgstr "[1:%(name)s] のバージョン %(version)s を削除"
 
-#: trac/wiki/templates/wiki_delete.html:28
+#: trac/wiki/templates/wiki_delete.html:38
 #, python-format
 msgid "Delete [1:%(name)s]"
 msgstr "[1:%(name)s] の削除"
 
-#: trac/wiki/templates/wiki_delete.html:38
+#: trac/wiki/templates/wiki_delete.html:48
 #, python-format
 msgid ""
 "[1:\n"
@@ -6770,12 +7148,12 @@
 "[1:このページのバージョン %(from)s から %(to)s を削除しますか?][2:][3:%(versions)s "
 "複数のバージョン]を削除しようとしています。最初の更新は%(first_modified)s、最後の更新は%(last_modified)sです。"
 
-#: trac/wiki/templates/wiki_delete.html:50
+#: trac/wiki/templates/wiki_delete.html:60
 #, python-format
 msgid "Are you sure you want to delete version %(version)s of this page?"
 msgstr "このページのバージョン %(version)s を削除しますか?"
 
-#: trac/wiki/templates/wiki_delete.html:57
+#: trac/wiki/templates/wiki_delete.html:67
 #, python-format
 msgid ""
 "This is the only [1:\n"
@@ -6784,16 +7162,16 @@
 "completely!"
 msgstr "ただ一つの[1:バージョン]のため(%(created)sに作成)、ページ自体を削除します。"
 
-#: trac/wiki/templates/wiki_delete.html:64
+#: trac/wiki/templates/wiki_delete.html:74
 #, python-format
 msgid "Modified %(modified)s."
 msgstr "最終更新: %(modified)s"
 
-#: trac/wiki/templates/wiki_delete.html:71
+#: trac/wiki/templates/wiki_delete.html:81
 msgid "Are you sure you want to completely delete this page?"
 msgstr "このページをすべて削除しますか?"
 
-#: trac/wiki/templates/wiki_delete.html:77
+#: trac/wiki/templates/wiki_delete.html:87
 #, python-format
 msgid ""
 "Removing the one and only [1:\n"
@@ -6801,7 +7179,7 @@
 "%(created)s."
 msgstr "[1:バージョン]が1つしかないページを削除しようとしています。このページは%(created)sに作成されたものです。"
 
-#: trac/wiki/templates/wiki_delete.html:83
+#: trac/wiki/templates/wiki_delete.html:93
 #, python-format
 msgid ""
 "Removing all [1:\n"
@@ -6810,117 +7188,117 @@
 "%(modified)s."
 msgstr "すべての[1:%(versions)sバージョン]を削除しようとしています。作成は%(created)s、最終更新は%(modified)sです。"
 
-#: trac/wiki/templates/wiki_delete.html:99
+#: trac/wiki/templates/wiki_delete.html:108
 msgid "Delete those versions"
 msgstr "これらのバージョンを削除"
 
-#: trac/wiki/templates/wiki_delete.html:99
-#: trac/wiki/templates/wiki_view.html:125
+#: trac/wiki/templates/wiki_delete.html:108
+#: trac/wiki/templates/wiki_view.html:136
 msgid "Delete this version"
 msgstr "このバージョンを削除"
 
-#: trac/wiki/templates/wiki_delete.html:99
-#: trac/wiki/templates/wiki_view.html:127
+#: trac/wiki/templates/wiki_delete.html:108
+#: trac/wiki/templates/wiki_view.html:138
 msgid "Delete page"
 msgstr "このページを削除"
 
-#: trac/wiki/templates/wiki_diff.html:17
+#: trac/wiki/templates/wiki_diff.html:27
 #, python-format
 msgid "Delete version %(old_version)d to version %(version)d"
 msgstr "バージョン %(old_version)d から %(version)d を削除"
 
-#: trac/wiki/templates/wiki_diff.html:18
+#: trac/wiki/templates/wiki_diff.html:28
 #, python-format
 msgid "Delete version %(version)d"
 msgstr "バージョン %(version)d を削除"
 
-#: trac/wiki/templates/wiki_edit.html:94
+#: trac/wiki/templates/wiki_edit.html:101
 msgid "See the diffs"
 msgstr "差分を見る"
 
-#: trac/wiki/templates/wiki_edit.html:94
+#: trac/wiki/templates/wiki_edit.html:101
 msgid "Review"
 msgstr "確認"
 
-#: trac/wiki/templates/wiki_edit.html:95
+#: trac/wiki/templates/wiki_edit.html:102
 msgid "See the preview"
 msgstr "プレビューを見る"
 
-#: trac/wiki/templates/wiki_edit.html:98
+#: trac/wiki/templates/wiki_edit.html:105
 #, python-format
 msgid "Editing %(name)s"
 msgstr "%(name)s の編集"
 
-#: trac/wiki/templates/wiki_edit.html:100
+#: trac/wiki/templates/wiki_edit.html:133
+msgid ""
+"Sorry, this page has been modified by somebody else since you started\n"
+"            editing. Your changes cannot be saved."
+msgstr "このページは編集開始以降に誰かが更新を行っているため、保存することができません。"
+
+#: trac/wiki/templates/wiki_edit.html:140
 msgid "Someone else has modified that page since you started your edits."
 msgstr "このページは編集開始以降に誰かが更新を行っています。"
 
-#: trac/wiki/templates/wiki_edit.html:101
+#: trac/wiki/templates/wiki_edit.html:141
 msgid ""
 "[1:If you save right away, you risk to revert those changes]\n"
-"        (highlighted below as deletions)."
+"            (highlighted below as deletions)."
 msgstr ""
 "[1:このまま保存を行うと、それらの修正を失うことになります]\n"
 "(削除になるところを以下で強調しています)。"
 
-#: trac/wiki/templates/wiki_edit.html:103
+#: trac/wiki/templates/wiki_edit.html:143
 msgid ""
 "Please review all those changes and manually merge them with your\n"
-"        own changes. [1:]\n"
-"        If you're unsure about what you're doing, please press [2:Cancel]"
-"\n"
-"        (losing your changes) and start editing the latest version of the"
-" page\n"
-"        again."
+"            own changes. [1:]\n"
+"            If you're unsure about what you're doing, please press "
+"[2:Cancel]\n"
+"            (losing your changes) and start editing the latest version of"
+" the page\n"
+"            again."
 msgstr "表示された変更内容を確認して、両者の変更内容を手動でマージして下さい。[1:]もし何を行なっているのかよくわからない場合は[2:取り消し]ボタンを押して(変更内容はなくなります)、再度最新のバージョンを編集し直して下さい。"
 
-#: trac/wiki/templates/wiki_edit.html:139
+#: trac/wiki/templates/wiki_edit.html:152
 #, python-format
 msgid ""
 "Change information for future version %(version)s (modified by "
 "%(author)s):"
 msgstr "新しいバージョンでの更新情報: バージョン %(version)s (更新者 %(author)s):"
 
-#: trac/wiki/templates/wiki_edit.html:149
+#: trac/wiki/templates/wiki_edit.html:162
 msgid "Go to the editor"
 msgstr "編集領域に移動"
 
-#: trac/wiki/templates/wiki_edit.html:152
-#: trac/wiki/templates/wiki_edit_form.html:70
+#: trac/wiki/templates/wiki_edit.html:165
+#: trac/wiki/templates/wiki_edit_form.html:81
 msgid "Review Changes"
 msgstr "変更を確認"
 
-#: trac/wiki/templates/wiki_edit.html:154
+#: trac/wiki/templates/wiki_edit.html:167
 msgid "No changes"
 msgstr "変更なし"
 
-#: trac/wiki/templates/wiki_edit.html:165
+#: trac/wiki/templates/wiki_edit.html:178
 msgid "Go to Save, Preview, Review or Cancel buttons"
 msgstr "保存、プレビュー、確認、取り消しボタンに移動"
 
-#: trac/wiki/templates/wiki_edit.html:166
+#: trac/wiki/templates/wiki_edit.html:179
 msgid "Actions"
 msgstr "アクション"
 
-#: trac/wiki/templates/wiki_edit.html:171
-msgid ""
-"Sorry, this page has been modified by somebody else since you started\n"
-"            editing. Your changes cannot be saved."
-msgstr "このページは編集開始以降に誰かが更新を行っているため、保存することができません。"
-
-#: trac/wiki/templates/wiki_edit_form.html:16
+#: trac/wiki/templates/wiki_edit_form.html:26
 msgid "Adjust edit area height:"
 msgstr "編集領域の高さを変更:"
 
-#: trac/wiki/templates/wiki_edit_form.html:24
+#: trac/wiki/templates/wiki_edit_form.html:34
 msgid "Selecting and pressing 'Preview' enters a two-column [edit|preview] mode"
 msgstr "選択してプレビューボタンを押せば2カラムモード(編集|プレビュー)になります"
 
-#: trac/wiki/templates/wiki_edit_form.html:24
+#: trac/wiki/templates/wiki_edit_form.html:34
 msgid "Edit side-by-side"
 msgstr "並べて編集"
 
-#: trac/wiki/templates/wiki_edit_form.html:33
+#: trac/wiki/templates/wiki_edit_form.html:44
 msgid ""
 "[1:Note:] See [2:WikiFormatting] and\n"
 "        [3:TracWiki] for help on editing wiki content."
@@ -6928,126 +7306,126 @@
 "[1:※] Wiki ページの編集方法については [2:WikiFormatting] および\n"
 "          [3:TracWiki] を参照して下さい。"
 
-#: trac/wiki/templates/wiki_edit_form.html:39
+#: trac/wiki/templates/wiki_edit_form.html:50
 msgid "Change information"
 msgstr "更新情報"
 
-#: trac/wiki/templates/wiki_edit_form.html:50
+#: trac/wiki/templates/wiki_edit_form.html:61
 msgid "Comment about this change (optional):"
 msgstr "この変更についてのコメント(省略可):"
 
-#: trac/wiki/templates/wiki_edit_form.html:57
+#: trac/wiki/templates/wiki_edit_form.html:68
 msgid "Page is read-only"
 msgstr "このページを読み込み専用にする"
 
-#: trac/wiki/templates/wiki_edit_form.html:65
+#: trac/wiki/templates/wiki_edit_form.html:76
 msgid "Merge changes"
 msgstr "マージする"
 
-#: trac/wiki/templates/wiki_edit_form.html:69
+#: trac/wiki/templates/wiki_edit_form.html:80
 msgid "Preview Page"
 msgstr "プレビュー"
 
-#: trac/wiki/templates/wiki_page_path.html:6
+#: trac/wiki/templates/wiki_page_path.html:16
 msgid "View WikiStart"
 msgstr "WikiStart の表示"
 
-#: trac/wiki/templates/wiki_page_path.html:6
+#: trac/wiki/templates/wiki_page_path.html:16
 msgid "wiki:"
 msgstr "wiki:"
 
-#: trac/wiki/templates/wiki_page_path.html:8
+#: trac/wiki/templates/wiki_page_path.html:18
 #, python-format
 msgid "View %(path)s"
 msgstr "%(path)s の表示"
 
-#: trac/wiki/templates/wiki_rename.html:15
+#: trac/wiki/templates/wiki_rename.html:25
 #, python-format
 msgid "Rename [1:%(name)s]"
 msgstr "[1:%(name)s] の名前変更"
 
-#: trac/wiki/templates/wiki_rename.html:19
+#: trac/wiki/templates/wiki_rename.html:29
 msgid "Renaming the page will rename all existing versions of the page in place."
 msgstr "ページ名の変更はページの履歴をすべて変更します。"
 
-#: trac/wiki/templates/wiki_rename.html:19
+#: trac/wiki/templates/wiki_rename.html:29
 msgid "The complete history of the page will be moved to the new location."
 msgstr "ページの履歴はすべて新しいページへと変更します。"
 
-#: trac/wiki/templates/wiki_rename.html:23
+#: trac/wiki/templates/wiki_rename.html:33
 msgid "New name:"
 msgstr "新しい名前:"
 
-#: trac/wiki/templates/wiki_rename.html:27
+#: trac/wiki/templates/wiki_rename.html:37
 msgid "Leave a redirection page at the old location"
 msgstr "旧ページにはリダイレクト用のページを残す"
 
-#: trac/wiki/templates/wiki_rename.html:33
-#: trac/wiki/templates/wiki_view.html:117
+#: trac/wiki/templates/wiki_rename.html:42
+#: trac/wiki/templates/wiki_view.html:128
 msgid "Rename page"
 msgstr "ページ名を変更"
 
-#: trac/wiki/templates/wiki_view.html:15
+#: trac/wiki/templates/wiki_view.html:26
 msgid "Revert page to this version"
 msgstr "このバージョンにページを戻す"
 
-#: trac/wiki/templates/wiki_view.html:15 trac/wiki/templates/wiki_view.html:93
+#: trac/wiki/templates/wiki_view.html:26 trac/wiki/templates/wiki_view.html:104
 msgid "Edit this page"
 msgstr "このページを編集"
 
-#: trac/wiki/templates/wiki_view.html:36
+#: trac/wiki/templates/wiki_view.html:47
 #, python-format
 msgid ""
 "Version %(version)s (modified by %(author)s, %(date)s)\n"
 "               ([1:diff])"
 msgstr "バージョン %(version)s (更新者 %(author)s、%(date)s) ([1:diff])"
 
-#: trac/wiki/templates/wiki_view.html:50
+#: trac/wiki/templates/wiki_view.html:61
 #, python-format
 msgid "Version %(version)s by %(author)s: %(comment)s"
 msgstr "バージョン %(version)s 更新者 %(author)s: %(comment)s"
 
-#: trac/wiki/templates/wiki_view.html:50
+#: trac/wiki/templates/wiki_view.html:61
 #, python-format
 msgid "Version %(version)s by %(author)s"
 msgstr "バージョン %(version)s 更新者 %(author)s"
 
-#: trac/wiki/templates/wiki_view.html:58
+#: trac/wiki/templates/wiki_view.html:69
 #, python-format
 msgid "[1:Last modified] %(reldate)s"
 msgstr "[1:最終更新] %(reldate)s"
 
-#: trac/wiki/templates/wiki_view.html:62
+#: trac/wiki/templates/wiki_view.html:73
 #, python-format
 msgid "Last modified on %(date)s"
 msgstr "最終更新 %(date)s"
 
-#: trac/wiki/templates/wiki_view.html:66
+#: trac/wiki/templates/wiki_view.html:77
 #, python-format
 msgid "The page %(name)s does not exist. You can create it here."
 msgstr "ページ %(name)s がありません。ここから作成できます。"
 
-#: trac/wiki/templates/wiki_view.html:68
+#: trac/wiki/templates/wiki_view.html:79
 msgid "You could also create the same page higher in the hierarchy:"
 msgstr "また、上位の階層にページを作成することもできます。"
 
-#: trac/wiki/templates/wiki_view.html:90
+#: trac/wiki/templates/wiki_view.html:101
 msgid "Revert to this version"
 msgstr "このバージョンに戻す"
 
-#: trac/wiki/templates/wiki_view.html:96
+#: trac/wiki/templates/wiki_view.html:107
 msgid "Create this page"
 msgstr "ページの作成"
 
-#: trac/wiki/templates/wiki_view.html:98
+#: trac/wiki/templates/wiki_view.html:109
 msgid "Using the template:"
 msgstr "テンプレートの使用:"
 
-#: trac/wiki/templates/wiki_view.html:101
+#: trac/wiki/templates/wiki_view.html:112
 msgid "(blank page)"
 msgstr "(空のページ)"
 
-#: trac/wiki/templates/wiki_view.html:135
+#: trac/wiki/templates/wiki_view.html:146
 msgid "The following pages have a name similar to this page, and may be related:"
 msgstr "以下のページはこのページの名前に似ているので、関係があるかもしれません。"
 
diff --git a/trac/trac/locale/ja/LC_MESSAGES/tracini.po b/trac/trac/locale/ja/LC_MESSAGES/tracini.po
index af39624..7c96ff5 100644
--- a/trac/trac/locale/ja/LC_MESSAGES/tracini.po
+++ b/trac/trac/locale/ja/LC_MESSAGES/tracini.po
@@ -1,13 +1,13 @@
 # Japanese translations for Trac.
-# Copyright (C) 2013 Edgewall Software
+# Copyright (C) 2013-2014 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# Jun Omae <jun66j5@gmail.com>, 2013.
+# Jun Omae <jun66j5@gmail.com>, 2013-2014.
 #
 msgid ""
 msgstr ""
-"Project-Id-Version: Trac 1.0\n"
+"Project-Id-Version: Trac 1.0.2\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2014-09-01 12:43+0000\n"
 "PO-Revision-Date: 2011-02-23 22:27+0900\n"
 "Last-Translator: Jun Omae <jun66j5@gmail.com>\n"
 "Language-Team: ja <LL@li.org>\n"
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/enscript.py:105
 msgid "Path to the Enscript executable."
@@ -59,11 +59,11 @@
 "には変換に使う SilverCity のモード、`quality` にはその変換処理に対する割合を指定します。\n"
 "またこれは SilverCity の描画時に使われるデフォルトの比率3を上書きすることもできます。(''0.10 以降'')"
 
-#: tracopt/perm/authz_policy.py:132
+#: tracopt/perm/authz_policy.py:134
 msgid "Location of authz policy configuration file."
 msgstr "authz ポリシーの設定ファイルのパスを指定します。"
 
-#: tracopt/perm/config_perm_provider.py:24
+#: tracopt/perm/config_perm_provider.py:27
 msgid ""
 "This section provides a way to add arbitrary permissions to a\n"
 "Trac environment. This can be useful for adding new permissions to use\n"
@@ -74,17 +74,21 @@
 "and a comma-separated list of permissions. For example:\n"
 "{{{\n"
 "[extra-permissions]\n"
-"extra_admin = extra_view, extra_modify, extra_delete\n"
+"EXTRA_ADMIN = EXTRA_VIEW, EXTRA_MODIFY, EXTRA_DELETE\n"
 "}}}\n"
 "This entry will define three new permissions `EXTRA_VIEW`,\n"
 "`EXTRA_MODIFY` and `EXTRA_DELETE`, as well as a meta-permissions\n"
 "`EXTRA_ADMIN` that grants all three permissions.\n"
 "\n"
+"The permissions are created in upper-case characters regardless of\n"
+"the casing of the definitions in `trac.ini`. For example, the\n"
+"definition `extra_view` would create the permission `EXTRA_VIEW`.\n"
+"\n"
 "If you don't want a meta-permission, start the meta-name with an\n"
 "underscore (`_`):\n"
 "{{{\n"
 "[extra-permissions]\n"
-"_perms = extra_view, extra_modify\n"
+"_perms = EXTRA_VIEW, EXTRA_MODIFY\n"
 "}}}"
 msgstr ""
 "このセクションは、Trac の環境に任意の権限を追加する方法を提供します。例えば、ワークフローのアクションで使う権限を追加するのに利用できます。\n"
@@ -95,15 +99,18 @@
 "例:\n"
 "{{{\n"
 "[extra-permissions]\n"
-"extra_admin = extra_view, extra_modify, extra_delete\n"
+"EXTRA_ADMIN = EXTRA_VIEW, EXTRA_MODIFY, EXTRA_DELETE\n"
 "}}}\n"
 "この項目では、3つの権限 `EXTRA_VIEW`、`EXTRA_MODIFY`、`EXTRA_DELETE` "
 "および3つの権限すべてを許可したメタ権限 `EXTRA_ADMIN` を定義します。\n"
 "\n"
+"`trac.ini` での定義の大文字小文字に関わらず、権限名は大文字になります。例えば、`extra_view` という定義は "
+"`EXTRA_VIEW` 権限を作成します。\n"
+"\n"
 "メタ権限が必要でない場合は、名前をアンダースコア (`_`) で始めてください。\n"
 "{{{\n"
 "[extra-permissions]\n"
-"_perms = extra_view, extra_modify\n"
+"_perms = EXTRA_VIEW, EXTRA_MODIFY\n"
 "}}}"
 
 #: tracopt/ticket/commit_updater.py:107
@@ -149,39 +156,39 @@
 msgid "Send ticket change notification when updating a ticket."
 msgstr "チケットの更新時にチケットの変更通知を送信するかどうかを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:169
+#: tracopt/versioncontrol/git/git_fs.py:269
 msgid "Enable persistent caching of commit tree."
 msgstr "コミットツリーの永続的なキャッシュを有効にするかどうかを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:172
+#: tracopt/versioncontrol/git/git_fs.py:272
 msgid "Wrap `GitRepository` in `CachedRepository`."
 msgstr "`GitRepository` を `CachedRepository` でラップするかを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:175
+#: tracopt/versioncontrol/git/git_fs.py:275
 msgid ""
 "The length at which a sha1 should be abbreviated to (must\n"
 "be >= 4 and <= 40)."
 msgstr "sha1 ハッシュを省略する際の長さを指定します (4~40であること)。"
 
-#: tracopt/versioncontrol/git/git_fs.py:180
+#: tracopt/versioncontrol/git/git_fs.py:280
 msgid ""
 "The minimum length of an hex-string for which\n"
 "auto-detection as sha1 is performed (must be >= 4 and <= 40)."
 msgstr "sha1 ハッシュを自動的に検出する際における16進文字列の最小長を指定します (4~40であること)。"
 
-#: tracopt/versioncontrol/git/git_fs.py:185
+#: tracopt/versioncontrol/git/git_fs.py:285
 msgid ""
 "Enable reverse mapping of git email addresses to trac user ids\n"
 "(costly if you have many users)."
 msgstr "git のメールアドレスから Trac ユーザ名への逆マッピングを有効にするかを指定します (ユーザが多い場合には高コストになります)。"
 
-#: tracopt/versioncontrol/git/git_fs.py:189
+#: tracopt/versioncontrol/git/git_fs.py:289
 msgid ""
 "Use git-committer id instead of git-author id for the\n"
 "changeset ''Author'' field."
 msgstr "チェンジセットの''更新者''項目として git-author id の代わりに git-committer id を使うかどうかを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:194
+#: tracopt/versioncontrol/git/git_fs.py:294
 msgid ""
 "Use git-committer timestamp instead of git-author timestamp\n"
 "for the changeset ''Timestamp'' field."
@@ -189,30 +196,30 @@
 "チェンジセットの''日時''項目として git-author timestamp の代わりに git-committer timestamp "
 "を使うかどうかを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:199
+#: tracopt/versioncontrol/git/git_fs.py:299
 msgid "Define charset encoding of paths within git repositories."
 msgstr "git リポジトリ中のパスに対する文字エンコーディングを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:202
+#: tracopt/versioncontrol/git/git_fs.py:302
 msgid "Path to the git executable."
 msgstr "git 実行ファイルへのパスを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:749
+#: tracopt/versioncontrol/git/git_fs.py:879
 msgid "Path to a gitweb-formatted projects.list"
 msgstr "gitweb 形式の projects.list ファイルへのパスを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:752
+#: tracopt/versioncontrol/git/git_fs.py:882
 msgid "Path to the base of your git projects"
 msgstr "git プロジェクトのベースディレクトリのパスを指定します。"
 
-#: tracopt/versioncontrol/git/git_fs.py:755
+#: tracopt/versioncontrol/git/git_fs.py:885
 #, python-format
 msgid ""
 "Template for project URLs. %s will be replaced with the repo\n"
 "name"
 msgstr "プロジェクト URL のテンプレート。%s はリポジトリ名に置換されます。"
 
-#: tracopt/versioncontrol/svn/svn_fs.py:253
+#: tracopt/versioncontrol/svn/svn_fs.py:265
 msgid ""
 "Comma separated list of paths categorized as branches.\n"
 "If a path ends with '*', then all the directory entries found below\n"
@@ -223,7 +230,7 @@
 "パスが '*' で終わっている場合は、そのパス配下にあるすべてのエントリになります。\n"
 "例: `/trunk, /branches/*, /projectAlpha/trunk, /sandbox/*`"
 
-#: tracopt/versioncontrol/svn/svn_fs.py:260
+#: tracopt/versioncontrol/svn/svn_fs.py:272
 msgid ""
 "Comma separated list of paths categorized as tags.\n"
 "\n"
@@ -236,6 +243,24 @@
 "パスが '*' で終わっている場合は、そのパス配下にあるすべてのエントリになります。\n"
 "例: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1`"
 
+#: tracopt/versioncontrol/svn/svn_fs.py:280
+msgid ""
+"End-of-Line character sequences when `svn:eol-style` property is\n"
+"`native`.\n"
+"\n"
+"If `native` (the default), substitute with the native EOL marker on\n"
+"the server. Otherwise, if `LF`, `CRLF` or `CR`, substitute with the\n"
+"specified EOL marker.\n"
+"\n"
+"(''since 1.0.2'')"
+msgstr ""
+"`svn:eol-style` プロパティが `native` の場合の改行文字列を指定します。\n"
+"\n"
+"`native` (デフォルト) の場合、サーバ上での改行コードに展開されます。それ以外の `LF`, `CRLF`, `CR` "
+"の場合、指定した改行コードに展開されます。\n"
+"\n"
+"(''1.0.2 以降'')"
+
 #: tracopt/versioncontrol/svn/svn_prop.py:37
 msgid ""
 "The TracBrowser for Subversion can interpret the `svn:externals`\n"
@@ -314,13 +339,11 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/attachment.py:430
-msgid ""
-"Maximum allowed file size (in bytes) for ticket and wiki\n"
-"attachments."
-msgstr "チケットと Wiki の添付ファイルの上限サイズを指定します (バイト単位)"
+#: trac/attachment.py:438
+msgid "Maximum allowed file size (in bytes) for attachments."
+msgstr "添付ファイルの上限サイズを指定します (バイト単位)"
 
-#: trac/attachment.py:434
+#: trac/attachment.py:441
 msgid ""
 "Maximum allowed total size (in bytes) for an attachment list to be\n"
 "downloadable as a `.zip`. Set this to -1 to disable download as `.zip`.\n"
@@ -329,7 +352,7 @@
 "`.zip` ダウンロードを許可する合計に対する上限サイズ (バイト単位) です。-1 を指定した場合は `.zip` "
 "でのダウンロード機能を無効にします。(''1.0 以降'')"
 
-#: trac/attachment.py:439
+#: trac/attachment.py:446
 msgid ""
 "Whether attachments should be rendered in the browser, or\n"
 "only made downloadable.\n"
@@ -348,7 +371,7 @@
 "\n"
 "匿名ユーザが添付ファイルを作成できるような公開サイトでは、このオプションを無効 (これがデフォルトです) のままにしておくことを推奨します。"
 
-#: trac/env.py:123
+#: trac/env.py:124
 msgid ""
 "This section is used to enable or disable components\n"
 "provided by plugins, as well as by Trac itself. The component\n"
@@ -408,7 +431,7 @@
 "\n"
 "関連項目: TracPlugins"
 
-#: trac/env.py:158
+#: trac/env.py:159
 msgid ""
 "Path to the //shared plugins directory//.\n"
 "\n"
@@ -424,7 +447,7 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/env.py:167
+#: trac/env.py:168
 msgid ""
 "Reference URL for the Trac deployment.\n"
 "\n"
@@ -438,7 +461,7 @@
 "これは Web アクセスではないところからドキュメントを提示する場合に使用するベース URL です。例えば、通知メールで Trac "
 "のリソースを指し示す URL を入れる場合などです。"
 
-#: trac/env.py:175
+#: trac/env.py:176
 msgid ""
 "Optionally use `[trac] base_url` for redirects.\n"
 "\n"
@@ -456,7 +479,7 @@
 "`base_url` 設定値にリダイレクトさせるこのオプションを設定しなければいけないかも知れません。これにより、リダイレクトに使われるこの "
 "URL からだけこの環境を利用できるような制限が設けられます。''(0.10.5 以降)''"
 
-#: trac/env.py:187
+#: trac/env.py:188
 msgid ""
 "Restrict cookies to HTTPS connections.\n"
 "\n"
@@ -470,26 +493,26 @@
 "true に設定すると、すべての Cookie に `secure` フラグを設定することでサーバへ HTTPS 接続でのみ Cookie "
 "を送信するようにします。Trac を HTTPS 経由でだけアクセス可能にする場合、これを指定します。(''0.11.2 以降'')"
 
-#: trac/env.py:195
+#: trac/env.py:196
 msgid "Name of the project."
 msgstr "プロジェクトの名前を指定します。"
 
-#: trac/env.py:198
+#: trac/env.py:199
 msgid "Short description of the project."
 msgstr "プロジェクトの概要を指定します。"
 
-#: trac/env.py:201
+#: trac/env.py:202
 msgid ""
 "URL of the main project web site, usually the website in\n"
 "which the `base_url` resides. This is used in notification\n"
 "e-mails."
 msgstr "プロジェクトに対するメインの Web サイトの URL を指定します。通常は `base_url` のサイトにあります。通知メールでこれを使います。"
 
-#: trac/env.py:206
+#: trac/env.py:207
 msgid "E-Mail address of the project's administrator."
 msgstr "プロジェクト管理者のメールアドレスを指定します。"
 
-#: trac/env.py:209
+#: trac/env.py:210
 msgid ""
 "Base URL of a Trac instance where errors in this Trac\n"
 "should be reported.\n"
@@ -503,15 +526,15 @@
 "これには、完全 URL または相対 URL、もしくはこの Trac を指す '.' "
 "を指定できます。空の値にすると報告用のボタンは使えなくなります。 (''0.11.3 以降'')"
 
-#: trac/env.py:218
+#: trac/env.py:219
 msgid "Page footer text (right-aligned)."
 msgstr "ページフッタのテキストを指定します (右揃え)。"
 
-#: trac/env.py:223
+#: trac/env.py:224
 msgid "URL of the icon of the project."
 msgstr "プロジェクトアイコンの URL を指定します。"
 
-#: trac/env.py:226
+#: trac/env.py:227
 msgid ""
 "Logging facility to use.\n"
 "\n"
@@ -521,7 +544,7 @@
 "\n"
 "(`none`, `file`, `stderr`, `syslog`, `winlog`) のうちのどれかを指定します。"
 
-#: trac/env.py:231
+#: trac/env.py:232
 msgid ""
 "If `log_type` is `file`, this should be a path to the\n"
 "log-file.  Relative paths are resolved relative to the `log`\n"
@@ -530,7 +553,7 @@
 "`log_type` が `file` の場合、ログファイルのパスを指定します。相対パスは、Trac 環境にある `log` "
 "ディレクトリから相対的に解釈します。"
 
-#: trac/env.py:236
+#: trac/env.py:237
 msgid ""
 "Level of verbosity in log.\n"
 "\n"
@@ -540,7 +563,7 @@
 "\n"
 "(`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`) のうちのどれかを指定します。"
 
-#: trac/env.py:241
+#: trac/env.py:242
 msgid ""
 "Custom logging format.\n"
 "\n"
@@ -586,7 +609,7 @@
 "\n"
 "''(0.10.5 以降)''"
 
-#: trac/notification.py:50
+#: trac/notification.py:52
 msgid ""
 "Name of the component implementing `IEmailSender`.\n"
 "\n"
@@ -601,59 +624,61 @@
 " と `sendmail` と互換性のあるコマンドを実行する `SendmailEmailSender` を用意しています。(''0.12 "
 "以降'')"
 
-#: trac/notification.py:59
+#: trac/notification.py:61
 msgid "Enable email notification."
 msgstr "メール通知機能を有効にするかどうかを指定します。"
 
-#: trac/notification.py:62
+#: trac/notification.py:64
 msgid "Sender address to use in notification emails."
 msgstr "通知メールに使用する送信者アドレスを指定します。"
 
-#: trac/notification.py:65
+#: trac/notification.py:67
 msgid "Sender name to use in notification emails."
 msgstr "通知メールに使用する送信者名を指定します。"
 
-#: trac/notification.py:68
+#: trac/notification.py:70
 msgid ""
 "Use the action author as the sender of notification emails.\n"
 "(''since 1.0'')"
 msgstr "通知メールの送信者として実行者の名前を使うかどうかを指定します。(''1.0 以降'')"
 
-#: trac/notification.py:72
+#: trac/notification.py:74
 msgid "Reply-To address to use in notification emails."
 msgstr "通知メールに使用する返信先アドレスを指定します。"
 
-#: trac/notification.py:75
+#: trac/notification.py:77
 msgid ""
 "Email address(es) to always send notifications to,\n"
 "addresses can be seen by all recipients (Cc:)."
-msgstr "メールアドレスがすべての受信者から見えるように常に通知を送信するかどうかを指定します (Cc:)。"
+msgstr "通知を常に送信するメールアドレスを指定します。メールアドレスはすべての受信者が見ることができます (Cc:)。"
 
-#: trac/notification.py:79
+#: trac/notification.py:81
 msgid ""
 "Email address(es) to always send notifications to,\n"
 "addresses do not appear publicly (Bcc:). (''since 0.10'')"
-msgstr "メールアドレスが公開状態で現れないように常に通知を送信するかどうかを指定します (Bcc:)。(''0.10 以降'')"
+msgstr ""
+"通知を常に送信するメールアドレスを指定します。メールアドレスは見えないようになります (Bcc:)。\n"
+"(''0.10 以降'')"
 
-#: trac/notification.py:83
+#: trac/notification.py:85
 msgid ""
 "Default host/domain to append to address that do not specify\n"
 "one."
 msgstr "ドメインがないアドレスに付加するデフォルトのホスト名/ドメイン名を指定します。"
 
-#: trac/notification.py:87
+#: trac/notification.py:89
 msgid ""
 "Comma-separated list of domains that should not be considered\n"
 "part of email addresses (for usernames with Kerberos domains)."
 msgstr "メールアドレスだと判定させたくないドメイン名をカンマ区切りで指定します。(Kerberos ドメイン付きのユーザ名のため)"
 
-#: trac/notification.py:91
+#: trac/notification.py:93
 msgid ""
 "Comma-separated list of domains that should be considered as\n"
 "valid for email addresses (such as localdomain)."
 msgstr "メールアドレス (localdomain のような) として有効だと判定させたいドメイン名をカンマ区切りで指定します。"
 
-#: trac/notification.py:95
+#: trac/notification.py:97
 msgid ""
 "Specifies the MIME encoding scheme for emails.\n"
 "\n"
@@ -667,7 +692,7 @@
 "有効な設定は、Base64 エンコーディングの `base64`、Quoted-Printable の `qp`、エンコーディングなしの "
 "`none` で内容がすべて ASCII コードなら 7bit、そうでないなら 8bit として送信します。(''0.10 以降'')"
 
-#: trac/notification.py:103
+#: trac/notification.py:105
 msgid ""
 "Recipients can see email addresses of other CC'ed recipients.\n"
 "\n"
@@ -678,7 +703,7 @@
 "\n"
 "このオプションに無効 (デフォルト) を設定している場合、受信者アドレスは Bcc に列挙します。(''0.10 以降'')"
 
-#: trac/notification.py:109
+#: trac/notification.py:111
 msgid ""
 "Permit email address without a host/domain (i.e. username only).\n"
 "\n"
@@ -689,7 +714,7 @@
 "\n"
 "SMTP サーバはそれらのアドレスを受け入れて、FQDN を追加する、またはローカル配送のどちらかを行うべきです。(''0.10 以降'')"
 
-#: trac/notification.py:115
+#: trac/notification.py:117
 msgid ""
 "Text to prepend to subject line of notification emails.\n"
 "\n"
@@ -702,27 +727,27 @@
 "設定が定義されていない場合は、[$project_name] プレフィックスとなります。\n"
 "プレフィックスが必要ない場合は、空を指定すると無効になります。(''0.10.1 以降'')"
 
-#: trac/notification.py:133
+#: trac/notification.py:135
 msgid "SMTP server hostname to use for email notifications."
 msgstr "メール通知に使用する SMTP サーバのホスト名を指定します。"
 
-#: trac/notification.py:136
+#: trac/notification.py:138
 msgid "SMTP server port to use for email notification."
 msgstr "メール通知に使用する SMTP サーバのポート番号を指定します。"
 
-#: trac/notification.py:139
+#: trac/notification.py:141
 msgid "Username for SMTP server. (''since 0.9'')"
 msgstr "SMTP サーバのユーザ名を指定します。(''0.9 以降'')"
 
-#: trac/notification.py:142
+#: trac/notification.py:144
 msgid "Password for SMTP server. (''since 0.9'')"
 msgstr "SMTP サーバのパスワードを指定します。(''0.9 以降'')"
 
-#: trac/notification.py:145
+#: trac/notification.py:147
 msgid "Use SSL/TLS to send notifications over SMTP. (''since 0.10'')"
 msgstr "SMTP 接続により通知を送信する際に SSL/TLS を使用するかどうかを指定します。(''0.10 以降'')"
 
-#: trac/notification.py:189
+#: trac/notification.py:200
 msgid ""
 "Path to the sendmail executable.\n"
 "\n"
@@ -733,13 +758,13 @@
 "\n"
 "sendmail プログラムは `-i` と `-f` オプションを受け付ける必要があります。(''0.12 以降'')"
 
-#: trac/perm.py:312
+#: trac/perm.py:310
 msgid ""
 "Name of the component implementing `IPermissionStore`, which is used\n"
 "for managing user and group permissions."
 msgstr "`IPermissionStore` を実装しているコンポーネントの名前を指定します。これはユーザとグループの権限を管理するのに使います。"
 
-#: trac/perm.py:317
+#: trac/perm.py:315
 msgid ""
 "List of components implementing `IPermissionPolicy`, in the order in\n"
 "which they will be applied. These components manage fine-grained access\n"
@@ -753,7 +778,7 @@
 "`DefaultPermissionPolicy` (0.11 以前の挙動) と `LegacyAttachmentPolicy` "
 "(ATTACHMENT_* 権限を個々のレルムの権限にマッピング) がデフォルトです。"
 
-#: trac/db/api.py:227
+#: trac/db/api.py:228
 msgid ""
 "Database connection\n"
 "[wiki:TracEnvironment#DatabaseConnectionStrings string] for this\n"
@@ -762,11 +787,11 @@
 "このプロジェクトのデータベース接続[wiki:TracEnvironment#DatabaseConnectionStrings "
 "文字列]を指定します。"
 
-#: trac/db/api.py:232
+#: trac/db/api.py:233
 msgid "Database backup location"
 msgstr "データベースバックアップの場所を指定します。"
 
-#: trac/db/api.py:235
+#: trac/db/api.py:236
 msgid ""
 "Timeout value for database connection, in seconds.\n"
 "Use '0' to specify ''no timeout''. ''(Since 0.11)''"
@@ -774,13 +799,13 @@
 "データベース接続のタイムアウトを秒単位で指定します。\n"
 "''タイムアウトなし''を指定するには '0' を使います。''(0.11 以降)''"
 
-#: trac/db/api.py:239
+#: trac/db/api.py:240
 msgid ""
 "Show the SQL queries in the Trac log, at DEBUG level.\n"
 "''(Since 0.11.5)''"
 msgstr "DEBUG レベルのときに SQL クエリをログに表示するかどうかを指定します。''(0.11.5 以降)''"
 
-#: trac/db/mysql_backend.py:78
+#: trac/db/mysql_backend.py:83
 msgid "Location of mysqldump for MySQL database backups"
 msgstr "MySQL データベースのバックアップに使う mysqldump の場所を指定します。"
 
@@ -788,25 +813,25 @@
 msgid "Location of pg_dump for Postgres database backups"
 msgstr "Postgres データベースのバックアップに使う pg_dump の場所を指定します。"
 
-#: trac/db/sqlite_backend.py:143
+#: trac/db/sqlite_backend.py:145
 msgid ""
 "Paths to sqlite extensions, relative to Trac environment's\n"
 "directory or absolute. (''since 0.12'')"
 msgstr "SQLite 拡張のパスを Trac 環境ディレクトリからの相対パス、または、絶対パスで指定します。(''0.12 以降'')"
 
-#: trac/mimeview/api.py:613
+#: trac/mimeview/api.py:619
 msgid "Charset to be used when in doubt."
 msgstr "文字コードが不明な場合に使用する文字コードを指定します。"
 
-#: trac/mimeview/api.py:616
+#: trac/mimeview/api.py:622
 msgid "Displayed tab width in file preview. (''since 0.9'')"
 msgstr "ファイルのプレビューで表示するタブの幅を指定します。(''0.9 以降'')"
 
-#: trac/mimeview/api.py:619
+#: trac/mimeview/api.py:625
 msgid "Maximum file size for HTML preview. (''since 0.9'')"
 msgstr "HTML プレビューするファイルの上限サイズを指定します。(''0.9 以降'')"
 
-#: trac/mimeview/api.py:622
+#: trac/mimeview/api.py:628
 msgid ""
 "List of additional MIME types and keyword mappings.\n"
 "Mappings are comma-separated, and for each MIME type,\n"
@@ -817,7 +842,7 @@
 "マッピングはカンマ区切りで、MIME タイプごとに関連するキーワードまたはファイル拡張子のコロン区切り (\":\") "
 "のリストを持ちます。(''0.10 以降'')"
 
-#: trac/mimeview/api.py:629
+#: trac/mimeview/api.py:635
 msgid ""
 "List of additional MIME types associated to filename patterns.\n"
 "Mappings are comma-separated, and each mapping consists of a MIME type\n"
@@ -827,7 +852,7 @@
 "追加する MIME タイプとそれに対応させるファイルパターンを指定します。マッピングはカンマ区切りで、マッピングごとに MIME "
 "タイプとファイルマッチングに使う Python 正規表現をコロン (`:`) で区切るようにします。(''1.0 以降'')"
 
-#: trac/mimeview/api.py:636
+#: trac/mimeview/api.py:642
 msgid ""
 "Comma-separated list of MIME types that should be treated as\n"
 "binary data. (''since 0.11.5'')"
@@ -855,11 +880,11 @@
 "には変換に使う Pygments のモード、`quality` にはその変換処理に対する割合を指定します。またこれは Pygments "
 "の描画時に使われるデフォルトの比率を上書きすることもできます。"
 
-#: trac/search/web_ui.py:49
+#: trac/search/web_ui.py:48
 msgid "Minimum length of query string allowed when performing a search."
 msgstr "検索の実行を許可する検索文字列の最小の長さを指定します。"
 
-#: trac/search/web_ui.py:52
+#: trac/search/web_ui.py:51
 msgid ""
 "Specifies which search filters should be disabled by\n"
 "default on the search page. This will also restrict the\n"
@@ -877,7 +902,7 @@
 "インターフェイスの実装を探してください。`get_search_method()` "
 "メソッドが返すタプルの最初の要素になります。無効にしても、検索ページでユーザが手動でフィルタを有効にすることができます。(0.12 以降)"
 
-#: trac/ticket/api.py:166
+#: trac/ticket/api.py:175
 msgid ""
 "In this section, you can define additional fields for tickets. See\n"
 "TracTicketsCustomFields for more details."
@@ -885,13 +910,13 @@
 "このセクションでは、チケットに追加したいフィールドを定義することができます。詳細については TracTicketsCustomFields "
 "を参照してください。"
 
-#: trac/ticket/api.py:170
+#: trac/ticket/api.py:179
 msgid ""
 "Ordered list of workflow controllers to use for ticket actions\n"
 "(''since 0.11'')."
 msgstr "チケットのアクションに使用するワークフローコントローラを順番に指定します。(''0.11 以降'')"
 
-#: trac/ticket/api.py:176
+#: trac/ticket/api.py:185
 msgid ""
 "Make the owner field of tickets use a drop-down menu.\n"
 "Be sure to understand the performance implications before activating\n"
@@ -911,57 +936,57 @@
 "\n"
 "(''0.9 以降'')"
 
-#: trac/ticket/api.py:187
+#: trac/ticket/api.py:196
 msgid "Default version for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトのバージョンを指定します。"
 
-#: trac/ticket/api.py:190
+#: trac/ticket/api.py:199
 msgid "Default type for newly created tickets (''since 0.9'')."
 msgstr "新規登録チケットに対するデフォルトの分類を指定します。(''0.9 以降'')"
 
-#: trac/ticket/api.py:193
+#: trac/ticket/api.py:202
 msgid "Default priority for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトの優先度を指定します。"
 
-#: trac/ticket/api.py:196
+#: trac/ticket/api.py:205
 msgid "Default milestone for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトのマイルストーンを指定します。"
 
-#: trac/ticket/api.py:199
+#: trac/ticket/api.py:208
 msgid "Default component for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトのコンポーネントを指定します。"
 
-#: trac/ticket/api.py:202
+#: trac/ticket/api.py:211
 msgid "Default severity for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトの重要度を指定します。"
 
-#: trac/ticket/api.py:205
+#: trac/ticket/api.py:214
 msgid "Default summary (title) for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトの概要 (タイトル) を指定します。"
 
-#: trac/ticket/api.py:208
+#: trac/ticket/api.py:217
 msgid "Default description for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトの詳細を指定します。"
 
-#: trac/ticket/api.py:211
+#: trac/ticket/api.py:220
 msgid "Default keywords for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトのキーワードを指定します。"
 
-#: trac/ticket/api.py:214
+#: trac/ticket/api.py:223
 msgid "Default owner for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトの担当者を指定します。"
 
-#: trac/ticket/api.py:217
+#: trac/ticket/api.py:226
 msgid "Default cc: list for newly created tickets."
 msgstr "新規登録チケットに対するデフォルトの関係者を指定します。"
 
-#: trac/ticket/api.py:220
+#: trac/ticket/api.py:229
 msgid ""
 "Default resolution for resolving (closing) tickets\n"
 "(''since 0.11'')."
 msgstr "解決 (クローズ) になるチケットに対するデフォルトの解決方法を指定します (''0.11 以降'')。"
 
-#: trac/ticket/default_workflow.py:105
+#: trac/ticket/default_workflow.py:108
 msgid ""
 "The workflow for tickets is controlled by plugins. By default,\n"
 "there's only a `ConfigurableTicketWorkflow` component in charge.\n"
@@ -977,23 +1002,23 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/ticket/notification.py:36
+#: trac/ticket/notification.py:38
 msgid "Always send notifications to the ticket owner (''since 0.9'')."
 msgstr "常にチケット担当者に通知を送信するかどうかを指定します。('' 0.9 以降'')"
 
-#: trac/ticket/notification.py:40
+#: trac/ticket/notification.py:42
 msgid ""
 "Always send notifications to any address in the ''reporter''\n"
 "field."
 msgstr "常に''報告者''フィールドにあるアドレスに通知を送信するかどうかを指定します。"
 
-#: trac/ticket/notification.py:46
+#: trac/ticket/notification.py:48
 msgid ""
 "Always send notifications to the person who causes the ticket\n"
 "property change and to any previous updater of that ticket."
 msgstr "チケットを変更した人とこれまでにそのチケットを変更した人に常に通知を送信するかどうかを指定します。"
 
-#: trac/ticket/notification.py:51
+#: trac/ticket/notification.py:53
 msgid ""
 "A Genshi text template snippet used to get the notification subject.\n"
 "\n"
@@ -1007,7 +1032,7 @@
 "`$prefix` は `smtp_subject_prefix` の設定値になります。\n"
 "''(0.11 以降)''"
 
-#: trac/ticket/notification.py:59
+#: trac/ticket/notification.py:61
 msgid ""
 "Like ticket_subject_template but for batch modifications.\n"
 "\n"
@@ -1018,7 +1043,7 @@
 "\n"
 "デフォルトでは `$prefix Batch modify: $tickets_descr` になります ''(1.0 以降)''。"
 
-#: trac/ticket/notification.py:66
+#: trac/ticket/notification.py:68
 msgid ""
 "Which width of ambiguous characters (e.g. 'single' or\n"
 "'double') should be used in the table of notification mail.\n"
@@ -1034,7 +1059,7 @@
 "ASCII 文字の倍の幅になります。これは CJK ユーザが期待するものです。\n"
 "''(0.12.2 以降)''"
 
-#: trac/ticket/query.py:824
+#: trac/ticket/query.py:838
 msgid ""
 "The default query for authenticated users. The query is either\n"
 "in [TracQuery#QueryLanguage query language] syntax, or a URL query\n"
@@ -1047,7 +1072,7 @@
 "クエリ文字列のどちらかを指定します。\n"
 "(''0.11.2 以降'')"
 
-#: trac/ticket/query.py:832
+#: trac/ticket/query.py:846
 msgid ""
 "The default query for anonymous users. The query is either\n"
 "in [TracQuery#QueryLanguage query language] syntax, or a URL query\n"
@@ -1060,25 +1085,25 @@
 "クエリ文字列のどちらかを指定します。\n"
 "(''0.11.2 以降'')"
 
-#: trac/ticket/query.py:840
+#: trac/ticket/query.py:854
 msgid ""
 "Number of tickets displayed per page in ticket queries,\n"
 "by default (''since 0.11'')"
 msgstr "チケットクエリで1ページあたりに表示するチケット数のデフォルトを指定します (''0.11 以降'')"
 
-#: trac/ticket/report.py:116
+#: trac/ticket/report.py:117
 msgid ""
 "Number of tickets displayed per page in ticket reports,\n"
 "by default (''since 0.11'')"
 msgstr "チケットのレポートで1ページあたりに表示するチケット数のデフォルトを指定します (''0.11 以降'')"
 
-#: trac/ticket/report.py:120
+#: trac/ticket/report.py:121
 msgid ""
 "Number of tickets displayed in the rss feeds for reports\n"
 "(''since 0.11'')"
 msgstr "レポートの RSS フィードに表示するチケット数を指定します (''0.11 以降'')"
 
-#: trac/ticket/roadmap.py:144
+#: trac/ticket/roadmap.py:145
 msgid ""
 "As the workflow for tickets is now configurable, there can\n"
 "be many ticket states, and simply displaying closed tickets\n"
@@ -1192,7 +1217,7 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/ticket/roadmap.py:388
+#: trac/ticket/roadmap.py:396
 msgid ""
 "Name of the component implementing `ITicketGroupStatsProvider`,\n"
 "which is used to collect statistics on groups of tickets for display\n"
@@ -1201,7 +1226,7 @@
 "`ITicketGroupStatsProvider` を実装しているコンポーネントの名前を指定します。\n"
 "これはロードマップの表示でチケットのグループごとに集計を行うのに使います。"
 
-#: trac/ticket/roadmap.py:590
+#: trac/ticket/roadmap.py:598
 msgid ""
 "Name of the component implementing `ITicketGroupStatsProvider`,\n"
 "which is used to collect statistics on groups of tickets for display\n"
@@ -1210,23 +1235,25 @@
 "`ITicketGroupStatsProvider` を実装しているコンポーネントの名前を指定します。\n"
 "これはマイルストーンの表示でチケットのグループごとに集計を行うのに使います。"
 
-#: trac/ticket/web_ui.py:75
+#: trac/ticket/web_ui.py:73
 msgid ""
 "Enable the display of all ticket changes in the timeline, not only\n"
 "open / close operations (''since 0.9'')."
 msgstr "タイムラインにチケットの登録と解決だけでなくすべてのチケットの変更を表示するかどうかを指定します。(''0.9 以降'')"
 
-#: trac/ticket/web_ui.py:79
+#: trac/ticket/web_ui.py:77
 msgid ""
-"Don't accept tickets with a too big description.\n"
-"(''since 0.11'')."
-msgstr "チケットの詳細が大き過ぎる場合、チケットを受け付けないようにします。(''0.11 以降'')"
+"Maximum allowed description size in characters.\n"
+"(//since 0.11//)."
+msgstr "チケットの詳細の最大文字数を指定します。(//0.11 以降//)"
 
-#: trac/ticket/web_ui.py:83
-msgid ""
-"Don't accept tickets with a too big comment.\n"
-"(''since 0.11.2'')"
-msgstr "チケットのコメントが大き過ぎる場合、チケットを受け付けないようにします。(''0.11.2 以降'')"
+#: trac/ticket/web_ui.py:81
+msgid "Maximum allowed comment size in characters. (//since 0.11.2//)."
+msgstr "コメントの最大文字数で指定します。(//0.11.2 以降//)"
+
+#: trac/ticket/web_ui.py:84
+msgid "Maximum allowed summary size in characters. (//since 1.0.2//)."
+msgstr "概要の最大文字数で指定します。(//1.0.2 以降//)"
 
 #: trac/ticket/web_ui.py:87
 msgid ""
@@ -1302,7 +1329,7 @@
 "\n"
 "(''0.12.1 以降'')"
 
-#: trac/versioncontrol/api.py:284
+#: trac/versioncontrol/api.py:293
 msgid ""
 "One of the alternatives for registering new repositories is to\n"
 "populate the `[repositories]` section of the `trac.ini`.\n"
@@ -1325,7 +1352,7 @@
 "\n"
 "(''0.12 以降'')"
 
-#: trac/versioncontrol/api.py:298
+#: trac/versioncontrol/api.py:307
 msgid ""
 "Default repository connector type. (''since 0.10'')\n"
 "\n"
@@ -1338,7 +1365,7 @@
 "[[TracIni#repositories-section repositories]] "
 "セクションで定義するリポジトリや「リポジトリ」管理パネルなどでも使用します。(''0.12 以降'')"
 
-#: trac/versioncontrol/api.py:306
+#: trac/versioncontrol/api.py:315
 msgid ""
 "Path to the default repository. This can also be a relative path\n"
 "(''since 0.11'').\n"
@@ -1352,7 +1379,7 @@
 "このオプションは非推奨で、リポジトリは [TracIni#repositories-section repositories] "
 "セクションで定義、もしくは「リポジトリ」管理パネルを使うようにしてください。(''0.12 以降'')"
 
-#: trac/versioncontrol/api.py:314
+#: trac/versioncontrol/api.py:323
 msgid ""
 "List of repositories that should be synchronized on every page\n"
 "request.\n"
@@ -1392,7 +1419,7 @@
 "repository. If left empty, the global section is used."
 msgstr "`authz_file` で使うデフォルトのリポジトリに対応するモジュール名を指定します。空の場合はグローバルセクションを使用します。"
 
-#: trac/versioncontrol/web_ui/browser.py:118
+#: trac/versioncontrol/web_ui/browser.py:117
 msgid ""
 "Comma-separated list of version control properties to render\n"
 "as wiki content in the repository browser.\n"
@@ -1403,7 +1430,7 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:125
+#: trac/versioncontrol/web_ui/browser.py:124
 msgid ""
 "Comma-separated list of version control properties to render\n"
 "as oneliner wiki content in the repository browser.\n"
@@ -1414,7 +1441,7 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:184
+#: trac/versioncontrol/web_ui/browser.py:183
 msgid ""
 "List of repository paths that can be downloaded.\n"
 "\n"
@@ -1436,7 +1463,7 @@
 "また、パスのプレフィックスマッチング処理はシンプルhもので、自動でエイリアスの解決は行わないことに注意してください。\n"
 "(''0.10 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:197
+#: trac/versioncontrol/web_ui/browser.py:196
 msgid ""
 "Enable colorization of the ''age'' column.\n"
 "\n"
@@ -1449,7 +1476,7 @@
 "これにはソースコードの注釈と同じカラースケールを使います。青が古く、赤が新しいものとなります。\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:206
+#: trac/versioncontrol/web_ui/browser.py:205
 msgid ""
 "(r,g,b) color triple to use for the color corresponding\n"
 "to the newest color, for the color scale used in ''blame'' or\n"
@@ -1460,7 +1487,7 @@
 "が有効の場合に、''注釈履歴''やソースブラウザの''時期''列で使います。\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:214
+#: trac/versioncontrol/web_ui/browser.py:213
 msgid ""
 "(r,g,b) color triple to use for the color corresponding\n"
 "to the oldest color, for the color scale used in ''blame'' or\n"
@@ -1471,7 +1498,7 @@
 "が有効の場合に、''注釈履歴''やソースブラウザの''時期''列で使います。\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:220
+#: trac/versioncontrol/web_ui/browser.py:219
 msgid ""
 "If set to a value between 0 and 1 (exclusive), this will be the\n"
 "point chosen to set the `intermediate_color` for interpolating\n"
@@ -1482,7 +1509,7 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:226
+#: trac/versioncontrol/web_ui/browser.py:225
 msgid ""
 "(r,g,b) color triple to use for the color corresponding\n"
 "to the intermediate color, if two linear interpolations are used\n"
@@ -1495,7 +1522,7 @@
 "参照)。指定しない場合は、`oldest_color` と `newest_color` の中間色を使います。\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/browser.py:234
+#: trac/versioncontrol/web_ui/browser.py:233
 msgid ""
 "Whether raw files should be rendered in the browser, or only made\n"
 "downloadable.\n"
@@ -1514,7 +1541,7 @@
 "\n"
 "誰でもファイルをチェックインできる公開リポジトリでは、この設定を無効のままにすることを推奨します。(これがデフォルトです)"
 
-#: trac/versioncontrol/web_ui/browser.py:246
+#: trac/versioncontrol/web_ui/browser.py:245
 msgid ""
 "Comma-separated list of version control properties to hide from\n"
 "the repository browser.\n"
@@ -1523,7 +1550,7 @@
 "リポジトリブラウザで非表示にするバージョン管理のプロパティをカンマ区切りで指定します。\n"
 "(''0.9 以降'')"
 
-#: trac/versioncontrol/web_ui/changeset.py:129
+#: trac/versioncontrol/web_ui/changeset.py:131
 msgid ""
 "Number of files to show (`-1` for unlimited, `0` to disable).\n"
 "\n"
@@ -1534,7 +1561,7 @@
 "\n"
 "`location` を指定することもできます。変更ファイルの共通しているプレフィックスを表示するようになります。(0.11 以降)"
 
-#: trac/versioncontrol/web_ui/changeset.py:136
+#: trac/versioncontrol/web_ui/changeset.py:138
 msgid ""
 "Whether wiki-formatted changeset messages should be multiline or\n"
 "not.\n"
@@ -1548,7 +1575,7 @@
 "この設定を指定しないか false にして `wiki_format_messages` を true "
 "にすると、チェンジセットのメッセージは1行スタイルになり、箇条書きなどの書式は反映されないようになります。"
 
-#: trac/versioncontrol/web_ui/changeset.py:145
+#: trac/versioncontrol/web_ui/changeset.py:147
 msgid ""
 "Whether consecutive changesets from the same author having\n"
 "exactly the same message should be presented as one event.\n"
@@ -1559,20 +1586,20 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/changeset.py:152
+#: trac/versioncontrol/web_ui/changeset.py:154
 msgid ""
 "Maximum number of modified files for which the changeset view will\n"
 "attempt to show the diffs inlined (''since 0.10'')."
 msgstr "チェンジセットページで差分をインラインで表示するファイルの最大数を指定します。(''0.10 以降'')"
 
-#: trac/versioncontrol/web_ui/changeset.py:156
+#: trac/versioncontrol/web_ui/changeset.py:158
 msgid ""
 "Maximum total size in bytes of the modified files (their old size\n"
 "plus their new size) for which the changeset view will attempt to show\n"
 "the diffs inlined (''since 0.10'')."
 msgstr "チェンジセットページで差分をインラインで表示するファイルの合計の上限バイト数 (旧サイズと新サイズの和) を指定します。(''0.10 以降'')"
 
-#: trac/versioncontrol/web_ui/changeset.py:161
+#: trac/versioncontrol/web_ui/changeset.py:163
 msgid ""
 "Whether wiki formatting should be applied to changeset messages.\n"
 "\n"
@@ -1583,7 +1610,7 @@
 "\n"
 "この設定を無効にすると、チェンジセットのメッセージは pre フォーマットのテキストとして表示します。"
 
-#: trac/versioncontrol/web_ui/log.py:47
+#: trac/versioncontrol/web_ui/log.py:46
 msgid ""
 "Default value for the limit argument in the TracRevisionLog.\n"
 "(''since 0.11'')"
@@ -1591,7 +1618,7 @@
 "TracRevisionLog の limit 引数に対するデフォルト値を指定します。\n"
 "(''0.11 以降'')"
 
-#: trac/versioncontrol/web_ui/log.py:51
+#: trac/versioncontrol/web_ui/log.py:50
 msgid ""
 "Comma-separated list of colors to use for the TracRevisionLog\n"
 "graph display. (''since 1.0'')"
@@ -1633,7 +1660,7 @@
 "認証用クッキーのパスを指定します。クッキーを他の Trac インスタンスと共有したい場合には、共通になるベースのパスを設定します。(''0.12 "
 "以降'')"
 
-#: trac/web/chrome.py:342
+#: trac/web/chrome.py:350
 msgid ""
 "Path to the //shared templates directory//.\n"
 "\n"
@@ -1649,7 +1676,7 @@
 "\n"
 "(''0.11 以降'')"
 
-#: trac/web/chrome.py:350
+#: trac/web/chrome.py:358
 msgid ""
 "Path to the //shared htdocs directory//.\n"
 "\n"
@@ -1670,11 +1697,11 @@
 "\n"
 "(''1.0 以降'')"
 
-#: trac/web/chrome.py:361
+#: trac/web/chrome.py:369
 msgid "Automatically reload template files after modification."
 msgstr "テンプレートの変更後に自動でリロードするかどうかを指定します。"
 
-#: trac/web/chrome.py:364
+#: trac/web/chrome.py:372
 msgid ""
 "The maximum number of templates that the template loader will cache\n"
 "in memory. The default value is 128. You may want to choose a higher\n"
@@ -1683,7 +1710,7 @@
 "memory."
 msgstr "テンプレートローダがメモリにキャッシュするテンプレートの最大数を指定します。デフォルト値は128です。もっと多くのテンプレートを使っていてメモリに余裕がある場合は、大きな値にすることもできます。メモリが少ない場合には、小さくすることもできます。"
 
-#: trac/web/chrome.py:371
+#: trac/web/chrome.py:379
 msgid ""
 "Base URL for serving the core static resources below\n"
 "`/chrome/common/`.\n"
@@ -1708,7 +1735,7 @@
 "ディレクトリにしか適用されず、デプロイしているリソース (例えばプラグインのリソース) に対してはこの方法は有効ではありません。Web "
 "サーバに対し、追加でリライトルールが必要になります。"
 
-#: trac/web/chrome.py:386
+#: trac/web/chrome.py:394
 msgid ""
 "Location of the jQuery !JavaScript library (version 1.7.2).\n"
 "\n"
@@ -1732,7 +1759,7 @@
 "\n"
 "(''1.0 以降'')"
 
-#: trac/web/chrome.py:398
+#: trac/web/chrome.py:406
 msgid ""
 "Location of the jQuery UI !JavaScript library (version 1.8.21).\n"
 "\n"
@@ -1756,7 +1783,7 @@
 "\n"
 "(''1.0 以降'')"
 
-#: trac/web/chrome.py:410
+#: trac/web/chrome.py:418
 msgid ""
 "Location of the theme to be used with the jQuery UI !JavaScript\n"
 "library (version 1.8.21).\n"
@@ -1786,23 +1813,23 @@
 "\n"
 "(''1.0 以降'')"
 
-#: trac/web/chrome.py:425
+#: trac/web/chrome.py:433
 msgid ""
 "Order of the items to display in the `metanav` navigation bar,\n"
 "listed by IDs. See also TracNavigation."
 msgstr "`metanav` ナビゲーションバーに表示する項目の順番を ID で指定します。TracNavigation も参照してください。"
 
-#: trac/web/chrome.py:430
+#: trac/web/chrome.py:438
 msgid ""
 "Order of the items to display in the `mainnav` navigation bar,\n"
 "listed by IDs. See also TracNavigation."
 msgstr "`mainnav` ナビゲーションバーに表示する項目の順番を ID で指定します。TracNavigation も参照してください。"
 
-#: trac/web/chrome.py:436
+#: trac/web/chrome.py:444
 msgid "URL to link to, from the header logo."
 msgstr "ヘッダロゴからリンクする URL を指定します。"
 
-#: trac/web/chrome.py:439
+#: trac/web/chrome.py:447
 msgid ""
 "URL of the image to use as header logo.\n"
 "It can be absolute, server relative or relative.\n"
@@ -1821,25 +1848,25 @@
 "にすると、[#trac-section htdocs_location] URL に対応したフォルダにある `your-logo.png` "
 "を指します。単に `your-logo.png` を指定した場合は、後者と同じになります。"
 
-#: trac/web/chrome.py:450
+#: trac/web/chrome.py:458
 msgid "Alternative text for the header logo."
 msgstr "ヘッダロゴ画像の代替テキストを指定します。"
 
-#: trac/web/chrome.py:454
+#: trac/web/chrome.py:462
 msgid "Width of the header logo image in pixels."
 msgstr "ヘッダロゴ画像の幅をピクセル単位で指定します。"
 
-#: trac/web/chrome.py:457
+#: trac/web/chrome.py:465
 msgid "Height of the header logo image in pixels."
 msgstr "ヘッダロゴ画像の高さをピクセル単位で指定します。"
 
-#: trac/web/chrome.py:460
+#: trac/web/chrome.py:468
 msgid ""
 "Show email addresses instead of usernames. If false, we obfuscate\n"
 "email addresses. (''since 0.11'')"
 msgstr "ユーザ名の代わりにメールアドレスを表示するかどうかを指定します。false の場合、メールアドレスはぼかすようにします。(''0.11 以降'')"
 
-#: trac/web/chrome.py:464
+#: trac/web/chrome.py:472
 msgid ""
 "Never obfuscate `mailto:` links explicitly written in the wiki,\n"
 "even if `show_email_addresses` is false or the user has not the\n"
@@ -1848,7 +1875,7 @@
 "`show_email_addresses` が false、または、EMAIL_VIEW 権限がない場合でも、Wiki 中に明示的に記述してある"
 " `mailto:` リンクをぼかさないようにするかを指定します。(''0.11.6 以降'')"
 
-#: trac/web/chrome.py:470
+#: trac/web/chrome.py:478
 msgid ""
 "Show IP addresses for resource edits (e.g. wiki).\n"
 "(''since 0.11.3'')"
@@ -1856,7 +1883,7 @@
 "リソース編集時の IP アドレスを表示するかどうかを指定します (例えば Wiki)。\n"
 "(''0.11.3 以降'')"
 
-#: trac/web/chrome.py:474
+#: trac/web/chrome.py:482
 msgid ""
 "Make `<textarea>` fields resizable. Requires !JavaScript.\n"
 "(''since 0.12'')"
@@ -1864,7 +1891,13 @@
 "`<textarea>` フィールドをリサイズできるようにするかどうかを指定します。!JavaScript が必要になります。\n"
 "(''0.12 以降'')"
 
-#: trac/web/chrome.py:478
+#: trac/web/chrome.py:486
+msgid ""
+"Add a simple toolbar on top of Wiki `<textarea>`s.\n"
+"(''since 1.0.2'')"
+msgstr "Wiki `<textarea>` の上部にシンプルなツールバーを追加するかを指定します。(''1.0.2 以降'')"
+
+#: trac/web/chrome.py:490
 msgid ""
 "Inactivity timeout in seconds after which the automatic wiki preview\n"
 "triggers an update. This option can contain floating-point values. The\n"
@@ -1876,7 +1909,7 @@
 "プレビューの自動更新の時間を秒で指定します。この設定には浮動小数点を指定できます。値を小さくするとサーバが大量のリクエストを処理することになります。0を指定すると自動プレビューを無効にします。デフォルトは2.0秒です。(''0.12"
 " 以降'')"
 
-#: trac/web/chrome.py:485
+#: trac/web/chrome.py:497
 msgid ""
 "The date information format. Valid options are 'relative' for\n"
 "displaying relative format and 'absolute' for displaying absolute\n"
@@ -1976,7 +2009,7 @@
 "の場合に、外部リンクとして表示します。\n"
 "(''0.11.8 以降'')"
 
-#: trac/wiki/intertrac.py:36
+#: trac/wiki/intertrac.py:35
 msgid ""
 "This section configures InterTrac prefixes. Options in this section\n"
 "whose name contain a \".\" define aspects of the InterTrac prefix\n"
@@ -1997,7 +2030,7 @@
 "   it doesn't know how to dispatch an InterTrac link, and it's up to\n"
 "   the local Trac to prepare the correct link. Not all links will work\n"
 "   that way, but the most common do. This is called the compatibility\n"
-"   mode, and is `true` by default.\n"
+"   mode, and is `false` by default.\n"
 " * If you know that the remote Trac knows how to dispatch InterTrac\n"
 "   links, you can explicitly disable this compatibility mode and then\n"
 "   ''any'' TracLinks can become InterTrac links.\n"
@@ -2024,7 +2057,7 @@
 "`.compat` は''互換''モードを有効または無効にするのに使います。\n"
 " * 対象の Trac が [trac:milestone:0.10 0.10] 以下のバージョン (正確には [trac:r3526 "
 "r3526]) で動作している場合、InterTrac リンクを処理する方法を知らないのでローカルの Trac "
-"が正しいリンクを用意します。すべてのリンクが機能するとは限らないが、大抵は動作します。これを互換モードと呼んでいて `true` "
+"が正しいリンクを用意します。すべてのリンクが機能するとは限らないが、大抵は動作します。これを互換モードと呼んでいて `false` "
 "を設定します。これがデフォルトです。\n"
 " * リモートの Trac が InterTrac "
 "リンクを処理する方法を知っている場合は、明示的にこの互換モードを無効にすることができ、そのときは ''any'' を設定して TracLinks "
@@ -2071,6 +2104,6 @@
 "}}}"
 
 #: trac/wiki/web_ui.py:64
-msgid "Maximum allowed wiki page size in bytes. (''since 0.11.2'')"
-msgstr "Wiki ページの最大サイズをバイト数で指定します。(''0.11.2 以降'')"
+msgid "Maximum allowed wiki page size in characters. (''since 0.11.2'')"
+msgstr "Wiki ページの最大文字数を指定します。(''0.11.2 以降'')"
 
diff --git a/trac/trac/locale/ko/LC_MESSAGES/tracini.po b/trac/trac/locale/ko/LC_MESSAGES/tracini.po
index ea47e3f..a9c1155 100644
--- a/trac/trac/locale/ko/LC_MESSAGES/tracini.po
+++ b/trac/trac/locale/ko/LC_MESSAGES/tracini.po
@@ -282,7 +282,7 @@
 "\n"
 "하지만 대상 저장소를 탐색할 수 있는 다른 Trac 인스턴스(또는 [http://www.viewvc.org/ ViewVC]와 같은 "
 "저장소 탐색기)가 있다면, 외부 URL에 대해 어떤 저장소 탐색기를 사용할 지 Trac에 지시할 수 있습니다. 이 매핑은 "
-"[[TracIni]]의 `[svn:externals]` 구역에 정의합니다.\n"
+"[[wiki:TracIni]]의 `[svn:externals]` 구역에 정의합니다.\n"
 "\n"
 "예제:\n"
 "{{{\n"
diff --git a/trac/trac/locale/messages-js.pot b/trac/trac/locale/messages-js.pot
index d65bc9e..bc89075 100644
--- a/trac/trac/locale/messages-js.pot
+++ b/trac/trac/locale/messages-js.pot
@@ -1,14 +1,14 @@
 # Translations template for Trac.
-# Copyright (C) 2013 Edgewall Software
+# Copyright (C) 2014 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2013.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
 #
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: Trac 1.0.1\n"
+"Project-Id-Version: Trac 1.0.2\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2014-09-01 12:43+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -76,7 +76,7 @@
 
 #. TRANSLATOR: Link that closes the datepicker
 #. TRANSLATOR: Link that closes the timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:7 trac/htdocs/js/jquery-ui-i18n.js:39
+#: trac/htdocs/js/jquery-ui-i18n.js:7 trac/htdocs/js/jquery-ui-i18n.js:40
 msgid "Done"
 msgstr ""
 
@@ -101,34 +101,34 @@
 msgstr ""
 
 #. TRANSLATOR: Heading of the standalone timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:30
+#: trac/htdocs/js/jquery-ui-i18n.js:31
 msgid "Choose Time"
 msgstr ""
 
 #. TRANSLATOR: Time selector label
-#: trac/htdocs/js/jquery-ui-i18n.js:32
+#: trac/htdocs/js/jquery-ui-i18n.js:33
 msgid "Time"
 msgstr ""
 
 #. TRANSLATOR: Time labels in the timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:34
+#: trac/htdocs/js/jquery-ui-i18n.js:35
 msgid "Hour"
 msgstr ""
 
-#: trac/htdocs/js/jquery-ui-i18n.js:34
+#: trac/htdocs/js/jquery-ui-i18n.js:35
 msgid "Minute"
 msgstr ""
 
-#: trac/htdocs/js/jquery-ui-i18n.js:34
+#: trac/htdocs/js/jquery-ui-i18n.js:35
 msgid "Second"
 msgstr ""
 
-#: trac/htdocs/js/jquery-ui-i18n.js:35
+#: trac/htdocs/js/jquery-ui-i18n.js:36
 msgid "Time Zone"
 msgstr ""
 
 #. TRANSLATOR: Link to pick the current time in the timepicker
-#: trac/htdocs/js/jquery-ui-i18n.js:37
+#: trac/htdocs/js/jquery-ui-i18n.js:38
 msgid "Now"
 msgstr ""
 
@@ -169,7 +169,7 @@
 msgid "Select ticket %(id)s for modification"
 msgstr ""
 
-#: trac/htdocs/js/query.js:387
+#: trac/htdocs/js/query.js:388
 msgid "Toggle selection of all tickets shown in this group"
 msgstr ""
 
@@ -177,39 +177,39 @@
 msgid "Link here"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:56
+#: trac/htdocs/js/wikitoolbar.js:59
 msgid "Bold text: '''Example'''"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:59
+#: trac/htdocs/js/wikitoolbar.js:62
 msgid "Italic text: ''Example''"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:62
+#: trac/htdocs/js/wikitoolbar.js:65
 msgid "Heading: == Example =="
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:65
+#: trac/htdocs/js/wikitoolbar.js:68
 msgid "Link: [http://www.example.com/ Example]"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:68
+#: trac/htdocs/js/wikitoolbar.js:71
 msgid "Code block: {{{ example }}}"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:71
+#: trac/htdocs/js/wikitoolbar.js:74
 msgid "Horizontal rule: ----"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:74
+#: trac/htdocs/js/wikitoolbar.js:77
 msgid "New paragraph"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:77
+#: trac/htdocs/js/wikitoolbar.js:80
 msgid "Line break: [[BR]]"
 msgstr ""
 
-#: trac/htdocs/js/wikitoolbar.js:80
+#: trac/htdocs/js/wikitoolbar.js:83
 msgid "Image: [[Image()]]"
 msgstr ""
 
diff --git a/trac/trac/locale/messages.pot b/trac/trac/locale/messages.pot
index 2f5f6e7..b978f22 100644
--- a/trac/trac/locale/messages.pot
+++ b/trac/trac/locale/messages.pot
@@ -1,14 +1,14 @@
 # Translations template for Trac.
-# Copyright (C) 2013 Edgewall Software
+# Copyright (C) 2014 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2013.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
 #
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: Trac 1.0.1\n"
+"Project-Id-Version: Trac 1.0.2\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2014-09-01 12:43+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -44,7 +44,7 @@
 msgid "Create a copy of this ticket"
 msgstr ""
 
-#: tracopt/ticket/commit_updater.py:275
+#: tracopt/ticket/commit_updater.py:283
 msgid ""
 "Insert a changeset message into the output.\n"
 "\n"
@@ -59,12 +59,16 @@
 " - `revision`: the revision of the desired changeset"
 msgstr ""
 
+#: tracopt/ticket/commit_updater.py:313
+msgid "(The changeset message doesn't reference this ticket)"
+msgstr ""
+
 #: tracopt/ticket/deleter.py:73 tracopt/ticket/deleter.py:90
-#: trac/ticket/templates/report_list.html:82
+#: trac/ticket/templates/report_list.html:92
 msgid "Delete"
 msgstr ""
 
-#: tracopt/ticket/deleter.py:74 tracopt/ticket/templates/ticket_delete.html:42
+#: tracopt/ticket/deleter.py:74 tracopt/ticket/templates/ticket_delete.html:51
 msgid "Delete ticket"
 msgstr ""
 
@@ -88,89 +92,112 @@
 msgid "Comment %(num)s not found"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:11
+#: tracopt/ticket/templates/ticket_delete.html:21
 #, python-format
 msgid "Delete Ticket #%(id)s"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:12
-#: tracopt/ticket/templates/ticket_delete.html:48
+#: tracopt/ticket/templates/ticket_delete.html:22
+#: tracopt/ticket/templates/ticket_delete.html:58
 #, python-format
 msgid "Delete comment %(num)s on Ticket #%(id)s"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:20
+#: tracopt/ticket/templates/ticket_delete.html:30
 #, python-format
 msgid "Delete [1:Ticket #%(id)s]"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:32
+#: tracopt/ticket/templates/ticket_delete.html:42
 msgid "Are you sure you want to delete this ticket?"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:33
+#: tracopt/ticket/templates/ticket_delete.html:43
 #, python-format
 msgid ""
 "(comments: %(comments)s,\n"
 "                 attachments: %(attachments)s)"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:36
-#: tracopt/ticket/templates/ticket_delete.html:61
-#: trac/templates/attachment.html:70 trac/wiki/templates/wiki_delete.html:95
+#: tracopt/ticket/templates/ticket_delete.html:46
+#: tracopt/ticket/templates/ticket_delete.html:71
+#: trac/templates/attachment.html:80 trac/wiki/templates/wiki_delete.html:105
 msgid "This is an irreversible operation."
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:41
-#: tracopt/ticket/templates/ticket_delete.html:65
-#: trac/admin/templates/admin_components.html:55
-#: trac/admin/templates/admin_enums.html:24
-#: trac/admin/templates/admin_milestones.html:74
-#: trac/admin/templates/admin_versions.html:50
-#: trac/templates/attachment.html:63 trac/templates/attachment.html:76
-#: trac/ticket/templates/milestone_delete.html:40
-#: trac/ticket/templates/milestone_edit.html:108
-#: trac/ticket/templates/report_delete.html:21
-#: trac/ticket/templates/report_edit.html:44
-#: trac/ticket/templates/ticket_change.html:118
-#: trac/versioncontrol/templates/admin_repositories.html:84
-#: trac/wiki/templates/wiki_delete.html:98
-#: trac/wiki/templates/wiki_edit_form.html:73
-#: trac/wiki/templates/wiki_rename.html:32
+#: tracopt/ticket/templates/ticket_delete.html:52
+#: tracopt/ticket/templates/ticket_delete.html:76
+#: trac/templates/attachment.html:73 trac/templates/attachment.html:87
+#: trac/ticket/templates/admin_components.html:64
+#: trac/ticket/templates/admin_enums.html:38
+#: trac/ticket/templates/admin_milestones.html:93
+#: trac/ticket/templates/admin_versions.html:61
+#: trac/ticket/templates/milestone_delete.html:45
+#: trac/ticket/templates/milestone_edit.html:128
+#: trac/ticket/templates/report_delete.html:32
+#: trac/ticket/templates/report_edit.html:62
+#: trac/ticket/templates/ticket_change.html:127
+#: trac/versioncontrol/templates/admin_repositories.html:98
+#: trac/wiki/templates/wiki_delete.html:112
+#: trac/wiki/templates/wiki_edit_form.html:84
+#: trac/wiki/templates/wiki_rename.html:43
 msgid "Cancel"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:61
+#: tracopt/ticket/templates/ticket_delete.html:71
 msgid "Are you sure you want to delete this ticket comment?"
 msgstr ""
 
-#: tracopt/ticket/templates/ticket_delete.html:66
+#: tracopt/ticket/templates/ticket_delete.html:75
 msgid "Delete comment"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_fs.py:283
+#. TRANSLATOR: modified ('diff') (link)
+#: tracopt/versioncontrol/git/git_fs.py:423 trac/ticket/web_ui.py:1745
+#: trac/ticket/templates/ticket_change.html:164 trac/wiki/macros.py:361
+#: trac/wiki/web_ui.py:765
+msgid "diff"
+msgstr ""
+
+#: tracopt/versioncontrol/git/git_fs.py:424
+msgid "Diff against this parent (show the changes merged from the other parents)"
+msgstr ""
+
+#: tracopt/versioncontrol/git/git_fs.py:433
+msgid ""
+"Note: this is a <strong>merge</strong> changeset, the changes displayed "
+"below correspond to the merge itself."
+msgstr ""
+
+#: tracopt/versioncontrol/git/git_fs.py:440
+msgid ""
+"Use the <tt>(diff)</tt> links above to see all the changes relative to "
+"each parent."
+msgstr ""
+
+#: tracopt/versioncontrol/svn/svn_fs.py:306
 #, python-format
 msgid "Subversion >= 1.0 required, found %(version)s"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_fs.py:337
+#: tracopt/versioncontrol/svn/svn_fs.py:362
 #, python-format
 msgid "%(path)s does not appear to be a Subversion repository."
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_fs.py:344
+#: tracopt/versioncontrol/svn/svn_fs.py:369
 #, python-format
 msgid "Couldn't open Subversion repository %(path)s: %(svn_error)s"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_fs.py:664
+#: tracopt/versioncontrol/svn/svn_fs.py:694
 #, python-format
 msgid ""
 "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)."
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_fs.py:823
+#: tracopt/versioncontrol/svn/svn_fs.py:862
 #, python-format
 msgid "svn blame failed on %(path)s: %(error)s"
 msgstr ""
@@ -179,6 +206,10 @@
 msgid "No svn:externals configured in trac.ini"
 msgstr ""
 
+#: tracopt/versioncontrol/svn/svn_prop.py:157
+msgid "needs lock"
+msgstr ""
+
 #: tracopt/versioncontrol/svn/svn_prop.py:187
 msgid "blocked"
 msgstr ""
@@ -187,187 +218,193 @@
 msgid "merged"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:221
+#: tracopt/versioncontrol/svn/svn_prop.py:222
 msgid "non-inheritable"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:223
+#: tracopt/versioncontrol/svn/svn_prop.py:224
 msgid "merged on the directory itself but not below"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:239
+#: tracopt/versioncontrol/svn/svn_prop.py:240
+#: tracopt/versioncontrol/svn/svn_prop.py:262
 msgid "eligible"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:253
+#: tracopt/versioncontrol/svn/svn_prop.py:270
 msgid "(toggle deleted branches)"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:291
+#: tracopt/versioncontrol/svn/svn_prop.py:308
 msgid "View merge source"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:302
+#: tracopt/versioncontrol/svn/svn_prop.py:319
 msgid "No revisions"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:309
+#: tracopt/versioncontrol/svn/svn_prop.py:326
 #, python-format
 msgid "%(title)s: %(revs)s"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:345
+#: tracopt/versioncontrol/svn/svn_prop.py:362
 msgid "merged: "
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:345
+#: tracopt/versioncontrol/svn/svn_prop.py:362
 msgid "blocked: "
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:346
+#: tracopt/versioncontrol/svn/svn_prop.py:363
 msgid "reverse-merged: "
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:346
+#: tracopt/versioncontrol/svn/svn_prop.py:363
 msgid "un-blocked: "
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:347
+#: tracopt/versioncontrol/svn/svn_prop.py:364
 msgid "marked as non-inheritable: "
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:348
+#: tracopt/versioncontrol/svn/svn_prop.py:365
 msgid "unmarked as non-inheritable: "
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:360
+#: tracopt/versioncontrol/svn/svn_prop.py:409
 msgid " (added)"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:397
+#: tracopt/versioncontrol/svn/svn_prop.py:433
 msgid "removed"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:400
+#: tracopt/versioncontrol/svn/svn_prop.py:436
 msgid " (with no actual effect on merging)"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_prop.py:401
+#: tracopt/versioncontrol/svn/svn_prop.py:437
 #, python-format
 msgid "Property %(prop)s changed"
 msgstr ""
 
-#: trac/about.py:47 trac/templates/about.html:10 trac/templates/about.html:29
+#: trac/about.py:47 trac/templates/about.html:20 trac/templates/about.html:41
 msgid "About Trac"
 msgstr ""
 
-#: trac/attachment.py:165
+#: trac/attachment.py:166
 #, python-format
 msgid "Attachment '%(title)s' does not exist."
 msgstr ""
 
-#: trac/attachment.py:167
+#: trac/attachment.py:168
 msgid "Invalid Attachment"
 msgstr ""
 
-#: trac/attachment.py:234
+#: trac/attachment.py:235
 msgid "Could not delete attachment"
 msgstr ""
 
-#: trac/attachment.py:253
+#: trac/attachment.py:254
 #, python-format
 msgid "Cannot reparent attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
 msgstr ""
 
-#: trac/attachment.py:258
+#: trac/attachment.py:259
 #, python-format
 msgid ""
 "Cannot reparent attachment \"%(att)s\" as it already exists in "
 "%(realm)s:%(id)s"
 msgstr ""
 
-#: trac/attachment.py:277
+#: trac/attachment.py:278
 #, python-format
 msgid "Could not reparent attachment %(name)s"
 msgstr ""
 
-#: trac/attachment.py:313
-#, python-format
-msgid "Cannot create attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
-msgstr ""
-
-#: trac/attachment.py:396
-#, python-format
-msgid "Attachment '%(filename)s' not found"
-msgstr ""
-
-#: trac/attachment.py:480
-msgid "Bad request"
-msgstr ""
-
-#: trac/attachment.py:499
-#, python-format
-msgid "Back to %(parent)s"
-msgstr ""
-
-#: trac/attachment.py:605
-#, python-format
-msgid "%(attachment)s attached to %(resource)s"
-msgstr ""
-
-#: trac/attachment.py:660
-#, python-format
-msgid "Unparented attachment %(id)s"
-msgstr ""
-
-#: trac/attachment.py:668
-#, python-format
-msgid "Attachment '%(id)s' in %(parent)s"
-msgstr ""
-
-#: trac/attachment.py:671
-#, python-format
-msgid "Attachments of %(parent)s"
-msgstr ""
-
-#: trac/attachment.py:688
+#: trac/attachment.py:311
 #, python-format
 msgid "%(parent)s doesn't exist, can't create attachment"
 msgstr ""
 
-#: trac/attachment.py:695 trac/attachment.py:722 trac/admin/web_ui.py:467
-#: trac/admin/web_ui.py:470 trac/admin/web_ui.py:474
+#: trac/attachment.py:320
+#, python-format
+msgid "Cannot create attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
+msgstr ""
+
+#: trac/attachment.py:404
+#, python-format
+msgid "Attachment '%(filename)s' not found"
+msgstr ""
+
+#: trac/attachment.py:487
+msgid "Bad request"
+msgstr ""
+
+#: trac/attachment.py:504
+#, python-format
+msgid "Parent resource %(parent)s doesn't exist"
+msgstr ""
+
+#: trac/attachment.py:510
+#, python-format
+msgid "Back to %(parent)s"
+msgstr ""
+
+#: trac/attachment.py:616
+#, python-format
+msgid "%(attachment)s attached to %(resource)s"
+msgstr ""
+
+#: trac/attachment.py:671
+#, python-format
+msgid "Unparented attachment %(id)s"
+msgstr ""
+
+#: trac/attachment.py:679
+#, python-format
+msgid "Attachment '%(id)s' in %(parent)s"
+msgstr ""
+
+#: trac/attachment.py:682
+#, python-format
+msgid "Attachments of %(parent)s"
+msgstr ""
+
+#: trac/attachment.py:702 trac/attachment.py:729 trac/admin/web_ui.py:471
+#: trac/admin/web_ui.py:474 trac/admin/web_ui.py:478
 msgid "No file uploaded"
 msgstr ""
 
-#: trac/attachment.py:703
+#: trac/attachment.py:710
 msgid "Can't upload empty file"
 msgstr ""
 
-#: trac/attachment.py:708
+#: trac/attachment.py:715
 #, python-format
 msgid "Maximum attachment size: %(num)s bytes"
 msgstr ""
 
-#: trac/attachment.py:709
+#: trac/attachment.py:716
 msgid "Upload failed"
 msgstr ""
 
-#: trac/attachment.py:737
+#: trac/attachment.py:744
 #, python-format
 msgid "Attachment field %(field)s is invalid: %(message)s"
 msgstr ""
 
-#: trac/attachment.py:741
+#: trac/attachment.py:748
 #, python-format
 msgid "Invalid attachment: %(message)s"
 msgstr ""
 
-#: trac/attachment.py:745
+#: trac/attachment.py:752
 msgid "Note: File must be selected again."
 msgstr ""
 
-#: trac/attachment.py:758
+#: trac/attachment.py:765
 #, python-format
 msgid ""
 "You don't have permission to replace the attachment %(name)s. You can "
@@ -375,103 +412,129 @@
 " ATTACHMENT_DELETE permission."
 msgstr ""
 
-#: trac/attachment.py:789
+#: trac/attachment.py:796
 #, python-format
 msgid "%(attachment)s (delete)"
 msgstr ""
 
-#: trac/attachment.py:803
+#: trac/attachment.py:810
 #, python-format
 msgid "Maximum total attachment size: %(num)s bytes"
 msgstr ""
 
-#: trac/attachment.py:804
+#: trac/attachment.py:811
 msgid "Download failed"
 msgstr ""
 
-#: trac/attachment.py:892 trac/versioncontrol/web_ui/browser.py:669
+#: trac/attachment.py:894 trac/versioncontrol/web_ui/browser.py:710
 #: trac/wiki/web_ui.py:73
 msgid "Plain Text"
 msgstr ""
 
-#: trac/attachment.py:898 trac/versioncontrol/web_ui/browser.py:675
+#: trac/attachment.py:900 trac/versioncontrol/web_ui/browser.py:716
 msgid "Original Format"
 msgstr ""
 
-#: trac/attachment.py:940 trac/templates/list_of_attachments.html:20
-#: trac/ticket/templates/ticket_change.html:83
-#: trac/versioncontrol/templates/dir_entries.html:18
-#: trac/versioncontrol/web_ui/browser.py:822
+#: trac/attachment.py:942 trac/templates/list_of_attachments.html:29
+#: trac/ticket/templates/ticket_change.html:92
+#: trac/versioncontrol/templates/dir_entries.html:29
+#: trac/versioncontrol/web_ui/browser.py:865
 msgid "Download"
 msgstr ""
 
-#: trac/attachment.py:1034
+#: trac/attachment.py:1036
 #, python-format
 msgid "Invalid resource identifier '%(id)s'"
 msgstr ""
 
-#: trac/attachment.py:1070 trac/admin/templates/admin_components.html:80
-#: trac/admin/templates/admin_enums.html:48
-#: trac/admin/templates/admin_milestones.html:107
-#: trac/admin/templates/admin_versions.html:83 trac/templates/about.html:69
-#: trac/templates/about.html:90 trac/templates/error.html:160
-#: trac/ticket/admin.py:210 trac/ticket/admin.py:399 trac/ticket/admin.py:559
+#: trac/attachment.py:1072 trac/templates/about.html:81
+#: trac/templates/about.html:102 trac/templates/error.html:179
+#: trac/ticket/admin.py:210 trac/ticket/admin.py:404 trac/ticket/admin.py:574
+#: trac/ticket/templates/admin_components.html:88
+#: trac/ticket/templates/admin_enums.html:61
+#: trac/ticket/templates/admin_milestones.html:124
+#: trac/ticket/templates/admin_versions.html:93
 #: trac/versioncontrol/admin.py:113
-#: trac/versioncontrol/templates/admin_repositories.html:125
-#: trac/web/session.py:417
+#: trac/versioncontrol/templates/admin_repositories.html:138
+#: trac/web/session.py:423
 msgid "Name"
 msgstr ""
 
-#: trac/attachment.py:1070
+#: trac/attachment.py:1072
 msgid "Size"
 msgstr ""
 
-#: trac/attachment.py:1070 trac/templates/history_view.html:30
-#: trac/ticket/templates/ticket.html:350
-#: trac/versioncontrol/templates/revisionlog.html:112
+#: trac/attachment.py:1072 trac/templates/history_view.html:40
+#: trac/ticket/templates/ticket.html:353
+#: trac/versioncontrol/templates/revisionlog.html:122
 msgid "Author"
 msgstr ""
 
-#: trac/attachment.py:1070 trac/templates/history_view.html:29
+#: trac/attachment.py:1072 trac/templates/history_view.html:39
 msgid "Date"
 msgstr ""
 
-#: trac/attachment.py:1071 trac/templates/attachment.html:93
-#: trac/ticket/api.py:299 trac/ticket/templates/ticket.html:379
-#: trac/ticket/templates/ticket_box.html:92
+#: trac/attachment.py:1073 trac/templates/attachment.html:102
+#: trac/ticket/api.py:308 trac/ticket/api.py:530
+#: trac/ticket/templates/ticket.html:382
+#: trac/ticket/templates/ticket_box.html:101
 msgid "Description"
 msgstr ""
 
-#: trac/attachment.py:1094 trac/wiki/admin.py:108
+#: trac/attachment.py:1096 trac/wiki/admin.py:108
 #, python-format
 msgid "File '%(name)s' exists"
 msgstr ""
 
-#: trac/config.py:44
+#: trac/config.py:45
 msgid "Configuration Error"
 msgstr ""
 
-#: trac/config.py:265
+#: trac/config.py:49
+msgid "Look in the Trac log for more information."
+msgstr ""
+
+#: trac/config.py:274
 #, python-format
 msgid "Error reading '%(file)s', make sure it is readable."
 msgstr ""
 
-#: trac/config.py:420
+#: trac/config.py:431
 #, python-format
 msgid "[%(section)s] %(entry)s: expected integer, got %(value)s"
 msgstr ""
 
-#: trac/config.py:438
+#: trac/config.py:449
 #, python-format
 msgid "[%(section)s] %(entry)s: expected float, got %(value)s"
 msgstr ""
 
-#: trac/config.py:666
+#: trac/config.py:622
+msgid "Setting attribute is not allowed."
+msgstr ""
+
+#: trac/config.py:702
 #, python-format
 msgid "[%(section)s] %(entry)s: expected one of (%(choices)s), got %(value)s"
 msgstr ""
 
-#: trac/config.py:761 trac/config.py:774
+#: trac/config.py:741
+#, python-format
+msgid ""
+"Cannot find an implementation of the %(interface)s interface named "
+"%(implementation)s. Please check that the Component is enabled or update "
+"the option %(option)s in trac.ini."
+msgstr ""
+
+#: trac/config.py:779
+#, python-format
+msgid ""
+"Cannot find implementation(s) of the %(interface)s interface named "
+"%(implementation)s. Please check that the Component is enabled or update "
+"the option %(option)s in trac.ini."
+msgstr ""
+
+#: trac/config.py:819 trac/config.py:832
 #, python-format
 msgid "Option '%(option)s' doesn't exist in section '%(section)s'"
 msgstr ""
@@ -480,28 +543,28 @@
 msgid "Trac Error"
 msgstr ""
 
-#: trac/env.py:218
+#: trac/env.py:219
 msgid ""
 "Visit the Trac open source project at<br /><a "
 "href=\"http://trac.edgewall.org/\">http://trac.edgewall.org/</a>"
 msgstr ""
 
-#: trac/env.py:761
+#: trac/env.py:791
 msgid "Database newer than Trac version"
 msgstr ""
 
-#: trac/env.py:778
+#: trac/env.py:808
 #, python-format
 msgid "No upgrade module for version %(num)i (%(version)s.py)"
 msgstr ""
 
-#: trac/env.py:825
+#: trac/env.py:854
 msgid ""
 "Missing environment variable \"TRAC_ENV\". Trac requires this variable to"
 " point to a valid Trac environment."
 msgstr ""
 
-#: trac/env.py:854 trac/admin/console.py:281
+#: trac/env.py:883 trac/admin/console.py:283
 #, python-format
 msgid ""
 "The Trac Environment needs to be upgraded.\n"
@@ -509,67 +572,67 @@
 "Run \"trac-admin %(path)s upgrade\""
 msgstr ""
 
-#: trac/env.py:893
+#: trac/env.py:922
 msgid "Copying resources from:"
 msgstr ""
 
-#: trac/env.py:911
+#: trac/env.py:940
 msgid "Creating scripts."
 msgstr ""
 
-#: trac/env.py:923
+#: trac/env.py:952
 #, python-format
 msgid "Invalid argument '%(arg)s'"
 msgstr ""
 
-#: trac/env.py:928
+#: trac/env.py:957
 #, python-format
 msgid "hotcopy can't overwrite existing '%(dest)s'"
 msgstr ""
 
-#: trac/env.py:937
+#: trac/env.py:966
 #, python-format
 msgid "Hotcopying %(src)s to %(dst)s ..."
 msgstr ""
 
-#: trac/env.py:954
+#: trac/env.py:983
 msgid "The following errors happened while copying the environment:"
 msgstr ""
 
-#: trac/env.py:965
+#: trac/env.py:994
 msgid "Backing up database ..."
 msgstr ""
 
-#: trac/env.py:970
+#: trac/env.py:999
 msgid "Hotcopy done."
 msgstr ""
 
-#: trac/env.py:975 trac/admin/api.py:131
+#: trac/env.py:1004 trac/admin/api.py:134
 msgid "Invalid arguments"
 msgstr ""
 
-#: trac/env.py:978
+#: trac/env.py:1007
 msgid "Database is up to date, no upgrade necessary."
 msgstr ""
 
-#: trac/env.py:984
+#: trac/env.py:1013
 msgid ""
 "The pre-upgrade backup failed.\n"
 "Use '--no-backup' to upgrade without doing a backup.\n"
 msgstr ""
 
-#: trac/env.py:988
+#: trac/env.py:1017
 msgid "The upgrade failed. Please fix the issue and try again.\n"
 msgstr ""
 
-#: trac/env.py:1000
+#: trac/env.py:1029
 msgid ""
 "Warning: the wiki-macros directory in the environment is non-empty, but "
 "Trac\n"
 "doesn't load plugins from there anymore. Please remove it by hand."
 msgstr ""
 
-#: trac/env.py:1011
+#: trac/env.py:1040
 #, python-format
 msgid ""
 "Error while removing wiki-macros: %(err)s\n"
@@ -577,7 +640,7 @@
 "hand."
 msgstr ""
 
-#: trac/env.py:1016
+#: trac/env.py:1045
 #, python-format
 msgid ""
 "Upgrade done.\n"
@@ -587,106 +650,133 @@
 "  trac-admin %(path)s wiki upgrade"
 msgstr ""
 
-#: trac/notification.py:159
+#: trac/notification.py:165
+#, python-format
+msgid ""
+"SMTP server connection error (%(error)s). Please modify %(option1)s or "
+"%(option2)s in your configuration."
+msgstr ""
+
+#: trac/notification.py:170
 msgid "TLS enabled but server does not support TLS"
 msgstr ""
 
-#: trac/notification.py:312
+#: trac/notification.py:223
+#, python-format
+msgid ""
+"Sendmail error (%(error)s). Please modify %(option)s in your "
+"configuration."
+msgstr ""
+
+#: trac/notification.py:330
 #, python-format
 msgid "Invalid email encoding setting: %(pref)s"
 msgstr ""
 
-#: trac/notification.py:337
+#: trac/notification.py:355
 msgid "Unable to send email due to identity crisis."
 msgstr ""
 
-#: trac/notification.py:341
+#: trac/notification.py:362
 #, python-format
 msgid "Neither %(from_)s nor %(reply_to)s are specified in the configuration."
 msgstr ""
 
-#: trac/notification.py:342
+#: trac/notification.py:363
 msgid "SMTP Notification Error"
 msgstr ""
 
-#: trac/notification.py:351
+#: trac/notification.py:374
 msgid "Header length is too short"
 msgstr ""
 
-#: trac/perm.py:56
+#: trac/perm.py:42
+msgid "Forbidden"
+msgstr ""
+
+#: trac/perm.py:54
 #, python-format
 msgid ""
 "%(perm)s privileges are required to perform this operation on "
 "%(resource)s. You don't have the required permissions."
 msgstr ""
 
-#: trac/perm.py:58
+#: trac/perm.py:56
 #, python-format
 msgid ""
 "%(perm)s privileges are required to perform this operation. You don't "
 "have the required permissions."
 msgstr ""
 
-#: trac/perm.py:64
+#: trac/perm.py:60
 msgid "Insufficient privileges to perform this operation."
 msgstr ""
 
-#: trac/perm.py:343
+#: trac/perm.py:341
 #, python-format
 msgid "%(name)s is not a valid action."
 msgstr ""
 
-#: trac/perm.py:656
+#: trac/perm.py:658
 msgid "User"
 msgstr ""
 
-#: trac/perm.py:656 trac/admin/templates/admin_perms.html:63
-#: trac/ticket/templates/batch_modify.html:37
-#: trac/ticket/templates/ticket.html:321
+#: trac/perm.py:658 trac/admin/templates/admin_perms.html:74
+#: trac/ticket/templates/batch_modify.html:47
+#: trac/ticket/templates/ticket.html:324
 msgid "Action"
 msgstr ""
 
-#: trac/perm.py:658
+#: trac/perm.py:660
 msgid "Available actions:"
 msgstr ""
 
-#: trac/perm.py:669 trac/admin/web_ui.py:370
+#: trac/perm.py:671 trac/admin/web_ui.py:370
 msgid "All upper-cased tokens are reserved for permission names"
 msgstr ""
 
-#: trac/perm.py:675
+#: trac/perm.py:677
 #, python-format
 msgid "The user %(user)s already has permission %(action)s."
 msgstr ""
 
-#: trac/perm.py:689
+#: trac/perm.py:692
 #, python-format
-msgid "Cannot remove permission %(action)s for user %(user)s."
+msgid ""
+"Cannot remove permission %(action)s for user %(user)s. The permission is "
+"granted through a meta-permission or group."
 msgstr ""
 
-#: trac/perm.py:706
+#: trac/perm.py:697
+#, python-format
+msgid ""
+"Cannot remove permission %(action)s for user %(user)s. The user has not "
+"been granted the permission."
+msgstr ""
+
+#: trac/perm.py:716
 #, python-format
 msgid "Cannot export to %(filename)s: %(error)s"
 msgstr ""
 
-#: trac/perm.py:719
+#: trac/perm.py:729
 #, python-format
 msgid "Invalid row %(line)d. Expected <user>, <action>, [action], [...]"
 msgstr ""
 
-#: trac/perm.py:727
+#: trac/perm.py:737
 #, python-format
 msgid ""
 "Invalid user %(user)s on line %(line)d: All upper-cased tokens are "
 "reserved for permission names."
 msgstr ""
 
-#: trac/perm.py:736
+#: trac/perm.py:746
 #, python-format
 msgid "Cannot import from %(filename)s line %(line)d: %(error)s "
 msgstr ""
 
-#: trac/perm.py:741
+#: trac/perm.py:751
 #, python-format
 msgid "Cannot import from %(filename)s: %(error)s"
 msgstr ""
@@ -696,16 +786,16 @@
 msgid "%(name)s at version %(version)s"
 msgstr ""
 
-#: trac/admin/api.py:135
+#: trac/admin/api.py:138
 msgid "Command not found"
 msgstr ""
 
-#: trac/admin/console.py:113
+#: trac/admin/console.py:114
 #, python-format
 msgid "Error: %(msg)s"
 msgstr ""
 
-#: trac/admin/console.py:132
+#: trac/admin/console.py:133
 #, python-format
 msgid ""
 "Welcome to trac-admin %(version)s\n"
@@ -716,48 +806,48 @@
 "        "
 msgstr ""
 
-#: trac/admin/console.py:166
+#: trac/admin/console.py:168
 #, python-format
 msgid "Failed to open environment: %(err)s"
 msgstr ""
 
-#: trac/admin/console.py:249
+#: trac/admin/console.py:251
 #, python-format
 msgid "Completion error: %(err)s"
 msgstr ""
 
-#: trac/admin/console.py:316
+#: trac/admin/console.py:318
 #, python-format
 msgid ""
 "No documentation found for '%(cmd)s'. Use 'help' to see the list of "
 "commands."
 msgstr ""
 
-#: trac/admin/console.py:322
+#: trac/admin/console.py:326
 msgid "Did you mean this?"
 msgid_plural "Did you mean one of these?"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/admin/console.py:326
+#: trac/admin/console.py:330
 #, python-format
 msgid "trac-admin - The Trac Administration Console %(version)s"
 msgstr ""
 
-#: trac/admin/console.py:330
+#: trac/admin/console.py:334
 msgid "Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]\n"
 msgstr ""
 
-#: trac/admin/console.py:333
+#: trac/admin/console.py:337
 msgid "Invoking trac-admin without command starts interactive mode.\n"
 msgstr ""
 
-#: trac/admin/console.py:373
+#: trac/admin/console.py:377
 #, python-format
 msgid "Creating a new Trac environment at %(envname)s"
 msgstr ""
 
-#: trac/admin/console.py:375
+#: trac/admin/console.py:379
 msgid ""
 "\n"
 "Trac will first ask a few questions about your environment\n"
@@ -767,12 +857,12 @@
 " This name will be used in page titles and descriptions.\n"
 msgstr ""
 
-#: trac/admin/console.py:383
+#: trac/admin/console.py:387
 #, python-format
 msgid "Project Name [%(default)s]> "
 msgstr ""
 
-#: trac/admin/console.py:385
+#: trac/admin/console.py:389
 msgid ""
 "\n"
 " Please specify the connection string for the database to use.\n"
@@ -782,48 +872,48 @@
 " connection string syntax).\n"
 msgstr ""
 
-#: trac/admin/console.py:393
+#: trac/admin/console.py:397
 #, python-format
 msgid "Database connection string [%(default)s]> "
 msgstr ""
 
-#: trac/admin/console.py:400
+#: trac/admin/console.py:404
 #, python-format
 msgid "Initenv for '%(env)s' failed."
 msgstr ""
 
-#: trac/admin/console.py:403
+#: trac/admin/console.py:407
 msgid "Does an environment already exist?"
 msgstr ""
 
-#: trac/admin/console.py:407
+#: trac/admin/console.py:411
 msgid "Directory exists and is not empty."
 msgstr ""
 
-#: trac/admin/console.py:413
+#: trac/admin/console.py:417
 #, python-format
 msgid ""
 "Base directory '%(env)s' does not exist. Please create it manually and "
 "retry."
 msgstr ""
 
-#: trac/admin/console.py:441
+#: trac/admin/console.py:445
 msgid "Creating and Initializing Project"
 msgstr ""
 
-#: trac/admin/console.py:458
+#: trac/admin/console.py:462
 msgid "Failed to create environment."
 msgstr ""
 
-#: trac/admin/console.py:464
+#: trac/admin/console.py:468
 msgid " Installing default wiki pages"
 msgstr ""
 
-#: trac/admin/console.py:473
+#: trac/admin/console.py:477
 msgid " Indexing default repository"
 msgstr ""
 
-#: trac/admin/console.py:476
+#: trac/admin/console.py:480
 msgid ""
 "\n"
 "---------------------------------------------------------------------\n"
@@ -838,7 +928,7 @@
 "repository_type and repository_path settings.\n"
 msgstr ""
 
-#: trac/admin/console.py:519
+#: trac/admin/console.py:523
 #, python-format
 msgid ""
 "\n"
@@ -867,7 +957,7 @@
 "Congratulations!\n"
 msgstr ""
 
-#: trac/admin/console.py:528
+#: trac/admin/console.py:532
 msgid ""
 "Display help for trac-admin commands.\n"
 "\n"
@@ -880,108 +970,108 @@
 "}}}"
 msgstr ""
 
-#: trac/admin/console.py:580
+#: trac/admin/console.py:578
 #, python-format
 msgid "Non-ascii environment path '%(path)s' not supported."
 msgstr ""
 
-#: trac/admin/web_ui.py:74
+#: trac/admin/web_ui.py:69
 msgid "Admin"
 msgstr ""
 
-#: trac/admin/web_ui.py:75 trac/admin/templates/admin.html:16
+#: trac/admin/web_ui.py:70 trac/admin/templates/admin.html:26
 msgid "Administration"
 msgstr ""
 
-#: trac/admin/web_ui.py:91
+#: trac/admin/web_ui.py:86
 msgid "No administration panels available"
 msgstr ""
 
-#: trac/admin/web_ui.py:117 trac/admin/web_ui.py:121
+#: trac/admin/web_ui.py:112 trac/admin/web_ui.py:116
 msgid "Unknown administration panel"
 msgstr ""
 
-#: trac/admin/web_ui.py:133
+#: trac/admin/web_ui.py:128
 msgid "Untitled"
 msgstr ""
 
-#: trac/admin/web_ui.py:192 trac/ticket/admin.py:66 trac/ticket/admin.py:95
-#: trac/ticket/admin.py:275 trac/ticket/admin.py:455 trac/ticket/admin.py:607
-#: trac/ticket/admin.py:690 trac/ticket/report.py:248
-#: trac/ticket/roadmap.py:779 trac/versioncontrol/admin.py:215
+#: trac/admin/web_ui.py:187 trac/ticket/admin.py:66 trac/ticket/admin.py:95
+#: trac/ticket/admin.py:273 trac/ticket/admin.py:464 trac/ticket/admin.py:626
+#: trac/ticket/admin.py:709 trac/ticket/report.py:253
+#: trac/ticket/roadmap.py:824 trac/versioncontrol/admin.py:214
 msgid "Your changes have been saved."
 msgstr ""
 
-#: trac/admin/web_ui.py:197 trac/ticket/admin.py:69
+#: trac/admin/web_ui.py:192 trac/ticket/admin.py:69
 msgid ""
 "Error writing to trac.ini, make sure it is writable by the web server. "
 "Your changes have not been saved."
 msgstr ""
 
-#: trac/admin/web_ui.py:210 trac/admin/web_ui.py:268 trac/admin/web_ui.py:356
-#: trac/admin/web_ui.py:443 trac/prefs/web_ui.py:94
-#: trac/prefs/templates/prefs_general.html:9
+#: trac/admin/web_ui.py:205 trac/admin/web_ui.py:265 trac/admin/web_ui.py:356
+#: trac/admin/web_ui.py:449 trac/prefs/web_ui.py:90
+#: trac/prefs/templates/prefs_general.html:19
 msgid "General"
 msgstr ""
 
-#: trac/admin/web_ui.py:210 trac/admin/templates/admin_basics.html:13
+#: trac/admin/web_ui.py:205 trac/admin/templates/admin_basics.html:23
 msgid "Basic Settings"
 msgstr ""
 
-#: trac/admin/web_ui.py:268 trac/admin/templates/admin_logging.html:10
-#: trac/admin/templates/admin_logging.html:22
+#: trac/admin/web_ui.py:265 trac/admin/templates/admin_logging.html:20
+#: trac/admin/templates/admin_logging.html:32
 msgid "Logging"
 msgstr ""
 
-#: trac/admin/web_ui.py:277 trac/ticket/templates/milestone_delete.html:31
-#: trac/ticket/templates/milestone_edit.html:87
+#: trac/admin/web_ui.py:274 trac/ticket/templates/milestone_delete.html:35
+#: trac/ticket/templates/milestone_edit.html:99
 msgid "None"
 msgstr ""
 
-#: trac/admin/web_ui.py:278
+#: trac/admin/web_ui.py:276
 msgid "Console"
 msgstr ""
 
-#: trac/admin/web_ui.py:280 trac/templates/attachment.html:32
+#: trac/admin/web_ui.py:278 trac/templates/attachment.html:42
 msgid "File"
 msgstr ""
 
-#: trac/admin/web_ui.py:282
+#: trac/admin/web_ui.py:280
 msgid "Syslog"
 msgstr ""
 
-#: trac/admin/web_ui.py:284
+#: trac/admin/web_ui.py:283
 msgid "Windows event log"
 msgstr ""
 
-#: trac/admin/web_ui.py:297
+#: trac/admin/web_ui.py:296
 #, python-format
 msgid "Unknown log type %(type)s"
 msgstr ""
 
-#: trac/admin/web_ui.py:298
+#: trac/admin/web_ui.py:297
 msgid "Invalid log type"
 msgstr ""
 
-#: trac/admin/web_ui.py:312
+#: trac/admin/web_ui.py:311
 #, python-format
 msgid "Unknown log level %(level)s"
 msgstr ""
 
-#: trac/admin/web_ui.py:313
+#: trac/admin/web_ui.py:312
 msgid "Invalid log level"
 msgstr ""
 
-#: trac/admin/web_ui.py:326
+#: trac/admin/web_ui.py:325
 msgid "You must specify a log file"
 msgstr ""
 
-#: trac/admin/web_ui.py:327
+#: trac/admin/web_ui.py:326
 msgid "Missing field"
 msgstr ""
 
-#: trac/admin/web_ui.py:356 trac/admin/templates/admin_perms.html:10
-#: trac/admin/templates/admin_perms.html:60
+#: trac/admin/web_ui.py:356 trac/admin/templates/admin_perms.html:20
+#: trac/admin/templates/admin_perms.html:71
 msgid "Permissions"
 msgstr ""
 
@@ -999,503 +1089,388 @@
 msgid "The permission %(action)s was already granted to %(subject)s."
 msgstr ""
 
-#: trac/admin/web_ui.py:402
+#: trac/admin/web_ui.py:400
+#, python-format
+msgid ""
+"The subject %(subject)s was not added to the group %(group)s because the "
+"group has %(perm)s permission and users cannot grant permissions they "
+"don't possess."
+msgstr ""
+
+#: trac/admin/web_ui.py:408
 #, python-format
 msgid "The subject %(subject)s has been added to the group %(group)s."
 msgstr ""
 
-#: trac/admin/web_ui.py:407
+#: trac/admin/web_ui.py:413
 #, python-format
 msgid "The subject %(subject)s was already added to the group %(group)s."
 msgstr ""
 
-#: trac/admin/web_ui.py:422
+#: trac/admin/web_ui.py:428
 msgid "The selected permissions have been revoked."
 msgstr ""
 
-#: trac/admin/web_ui.py:443 trac/admin/templates/admin_plugins.html:10
+#: trac/admin/web_ui.py:449 trac/admin/templates/admin_plugins.html:20
 msgid "Plugins"
 msgstr ""
 
-#: trac/admin/web_ui.py:477
+#: trac/admin/web_ui.py:481
 msgid "Uploaded file is not a Python source file or egg"
 msgstr ""
 
-#: trac/admin/web_ui.py:482
+#: trac/admin/web_ui.py:486
 #, python-format
 msgid "Plugin %(name)s already installed"
 msgstr ""
 
-#: trac/admin/web_ui.py:551
+#: trac/admin/web_ui.py:555
 msgid "The following component has been disabled:"
 msgid_plural "The following components have been disabled:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/admin/web_ui.py:556
+#: trac/admin/web_ui.py:560
 msgid "The following component has been enabled:"
 msgid_plural "The following components have been enabled:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/admin/templates/admin.html:10
+#: trac/admin/templates/admin.html:20
 msgid "Administration:"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:9
+#: trac/admin/templates/admin_basics.html:19
 msgid "Basics"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:17
+#: trac/admin/templates/admin_basics.html:27
 msgid "Project"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:19
-#: trac/admin/templates/admin_components.html:37
-#: trac/admin/templates/admin_components.html:66
-#: trac/admin/templates/admin_enums.html:21
-#: trac/admin/templates/admin_enums.html:35
-#: trac/admin/templates/admin_milestones.html:28
-#: trac/admin/templates/admin_milestones.html:85
-#: trac/admin/templates/admin_versions.html:26
-#: trac/admin/templates/admin_versions.html:61
-#: trac/versioncontrol/templates/admin_repositories.html:50
-#: trac/versioncontrol/templates/admin_repositories.html:95
-#: trac/versioncontrol/templates/admin_repositories.html:112
+#: trac/admin/templates/admin_basics.html:29
+#: trac/ticket/templates/admin_components.html:47
+#: trac/ticket/templates/admin_components.html:74
+#: trac/ticket/templates/admin_enums.html:34
+#: trac/ticket/templates/admin_enums.html:48
+#: trac/ticket/templates/admin_milestones.html:45
+#: trac/ticket/templates/admin_milestones.html:103
+#: trac/ticket/templates/admin_versions.html:36
+#: trac/ticket/templates/admin_versions.html:71
+#: trac/versioncontrol/templates/admin_repositories.html:61
+#: trac/versioncontrol/templates/admin_repositories.html:108
+#: trac/versioncontrol/templates/admin_repositories.html:125
 msgid "Name:"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:24
-#: trac/versioncontrol/templates/admin_repositories.html:62
+#: trac/admin/templates/admin_basics.html:34
+#: trac/versioncontrol/templates/admin_repositories.html:74
 msgid "URL:"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:29
-#: trac/ticket/templates/ticket.html:237
+#: trac/admin/templates/admin_basics.html:39
+#: trac/ticket/templates/ticket.html:241
 msgid "Description:"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:35
+#: trac/admin/templates/admin_basics.html:45
 msgid "Default timezone:"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:37
+#: trac/admin/templates/admin_basics.html:47
 msgid "Server's local time zone"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:44
-msgid "Default language:"
-msgstr ""
-
-#: trac/admin/templates/admin_basics.html:46
-#: trac/admin/templates/admin_basics.html:55
-msgid "Browser's language"
-msgstr ""
-
-#: trac/admin/templates/admin_basics.html:53
-msgid "Default date format:"
+#: trac/admin/templates/admin_basics.html:52
+msgid "Install pytz for a complete list of timezones."
 msgstr ""
 
 #: trac/admin/templates/admin_basics.html:57
-#: trac/prefs/templates/prefs_datetime.html:65
+msgid "Default language:"
+msgstr ""
+
+#: trac/admin/templates/admin_basics.html:58
+#: trac/prefs/templates/prefs_language.html:26
+msgid "Translations are currently unavailable"
+msgstr ""
+
+#: trac/admin/templates/admin_basics.html:60
+#: trac/admin/templates/admin_basics.html:75
+msgid "Browser's language"
+msgstr ""
+
+#: trac/admin/templates/admin_basics.html:65
+#: trac/prefs/templates/prefs_language.html:34
+msgid "Install Babel for extended language support."
+msgstr ""
+
+#: trac/admin/templates/admin_basics.html:68
+#: trac/prefs/templates/prefs_language.html:37
+msgid "Message catalogs have not been compiled."
+msgstr ""
+
+#: trac/admin/templates/admin_basics.html:73
+msgid "Default date format:"
+msgstr ""
+
+#: trac/admin/templates/admin_basics.html:77
+#: trac/prefs/templates/prefs_datetime.html:75
 msgid "ISO 8601 format"
 msgstr ""
 
-#: trac/admin/templates/admin_basics.html:63
-#: trac/admin/templates/admin_components.html:99
-#: trac/admin/templates/admin_enums.html:70
-#: trac/admin/templates/admin_logging.html:55
-#: trac/admin/templates/admin_milestones.html:132
-#: trac/admin/templates/admin_plugins.html:180
-#: trac/admin/templates/admin_versions.html:100
+#: trac/admin/templates/admin_basics.html:80
+msgid "Install Babel for localized date formats."
+msgstr ""
+
+#: trac/admin/templates/admin_basics.html:86
+#: trac/admin/templates/admin_logging.html:65
+#: trac/admin/templates/admin_plugins.html:190
+#: trac/ticket/templates/admin_components.html:106
+#: trac/ticket/templates/admin_enums.html:82
+#: trac/ticket/templates/admin_milestones.html:148
+#: trac/ticket/templates/admin_versions.html:109
 msgid "Apply changes"
 msgstr ""
 
-#: trac/admin/templates/admin_components.html:10 trac/ticket/admin.py:77
-msgid "Components"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:14
-msgid "Manage Components"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:18
-msgid "Owner:"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:35
-msgid "Modify Component:"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:42
-msgid ""
-"Description (you may use\n"
-"                [1:WikiFormatting]\n"
-"                here):"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:56
-#: trac/admin/templates/admin_enums.html:25
-#: trac/admin/templates/admin_milestones.html:75
-#: trac/admin/templates/admin_versions.html:51
-#: trac/versioncontrol/templates/admin_repositories.html:85
-msgid "Save"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:64
-msgid "Add Component:"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:70
-#: trac/admin/templates/admin_enums.html:38
-#: trac/admin/templates/admin_milestones.html:96
-#: trac/admin/templates/admin_perms.html:31
-#: trac/admin/templates/admin_perms.html:50
-#: trac/admin/templates/admin_versions.html:73
-#: trac/versioncontrol/templates/admin_repositories.html:102
-#: trac/versioncontrol/templates/admin_repositories.html:116
-msgid "Add"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:80 trac/ticket/admin.py:210
-#: trac/ticket/api.py:293 trac/ticket/web_ui.py:1455
-msgid "Owner"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:80
-#: trac/admin/templates/admin_enums.html:48
-#: trac/admin/templates/admin_milestones.html:107
-#: trac/admin/templates/admin_versions.html:83
-msgid "Default"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:98
-#: trac/admin/templates/admin_enums.html:69
-#: trac/admin/templates/admin_milestones.html:131
-#: trac/admin/templates/admin_perms.html:109
-#: trac/admin/templates/admin_versions.html:99
-#: trac/versioncontrol/templates/admin_repositories.html:145
-msgid "Remove selected items"
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:101
-#: trac/admin/templates/admin_enums.html:72
-#: trac/admin/templates/admin_milestones.html:134
-#: trac/admin/templates/admin_versions.html:102
-msgid ""
-"You can remove all items from this list to completely hide this\n"
-"              field from the user interface."
-msgstr ""
-
-#: trac/admin/templates/admin_components.html:107
-#: trac/admin/templates/admin_enums.html:82
-#: trac/admin/templates/admin_milestones.html:140
-#: trac/admin/templates/admin_versions.html:108
-msgid ""
-"As long as you don't add any items to the list, this field\n"
-"            will remain completely hidden from the user interface."
-msgstr ""
-
-#: trac/admin/templates/admin_enums.html:14
-#, python-format
-msgid "Manage %(label_plural)s"
-msgstr ""
-
-#: trac/admin/templates/admin_enums.html:19
-#, python-format
-msgid "Modify %(label_singular)s"
-msgstr ""
-
-#: trac/admin/templates/admin_enums.html:33
-#, python-format
-msgid "Add %(label_singular)s"
-msgstr ""
-
-#: trac/admin/templates/admin_enums.html:48
-msgid "Order"
-msgstr ""
-
-#: trac/admin/templates/admin_enums.html:76
-msgid ""
-"[1:Note:] The order of priorities determines the\n"
-"              coloring of entries in the ticket queries and reports."
-msgstr ""
-
-#: trac/admin/templates/admin_logging.html:26 trac/templates/about.html:85
+#: trac/admin/templates/admin_logging.html:36 trac/templates/about.html:97
 msgid "Configuration"
 msgstr ""
 
-#: trac/admin/templates/admin_logging.html:28
-#: trac/versioncontrol/templates/admin_repositories.html:18
+#: trac/admin/templates/admin_logging.html:38
+#: trac/versioncontrol/templates/admin_repositories.html:28
 msgid "Type:"
 msgstr ""
 
-#: trac/admin/templates/admin_logging.html:37
+#: trac/admin/templates/admin_logging.html:47
 msgid "Log level:"
 msgstr ""
 
-#: trac/admin/templates/admin_logging.html:45
+#: trac/admin/templates/admin_logging.html:55
 msgid "Log file:"
 msgstr ""
 
-#: trac/admin/templates/admin_logging.html:48
+#: trac/admin/templates/admin_logging.html:58
 #, python-format
 msgid ""
 "If you specify a relative path, the log file will be stored inside the\n"
 "            [1:log] directory of the project environment ([2:%(dir)s])."
 msgstr ""
 
-#: trac/admin/templates/admin_milestones.html:10 trac/ticket/admin.py:235
-#: trac/ticket/roadmap.py:963
-msgid "Milestones"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:20
-msgid "Manage Milestones"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:26
-msgid "Modify Milestone:"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:31
-#: trac/admin/templates/admin_milestones.html:88
-#: trac/ticket/templates/milestone_edit.html:61
-msgid "Due:"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:32
-#: trac/admin/templates/admin_milestones.html:35
-#: trac/admin/templates/admin_milestones.html:45
-#: trac/admin/templates/admin_milestones.html:49
-#: trac/admin/templates/admin_milestones.html:90
-#: trac/admin/templates/admin_versions.html:32
-#: trac/admin/templates/admin_versions.html:35
-#: trac/admin/templates/admin_versions.html:66
-#: trac/admin/templates/admin_versions.html:69
-#: trac/ticket/templates/milestone_edit.html:65
-#: trac/ticket/templates/milestone_edit.html:68
-#: trac/ticket/templates/milestone_edit.html:77
-#: trac/ticket/templates/milestone_edit.html:80
-#, python-format
-msgid "Format: %(datehint)s"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:41
-#: trac/ticket/templates/milestone_edit.html:73
-msgid "Completed:"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:63
-#: trac/admin/templates/admin_versions.html:40
-#: trac/ticket/templates/milestone_edit.html:99
-#: trac/versioncontrol/templates/admin_repositories.html:73
-msgid "Description (you may use [1:WikiFormatting] here):"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:83
-msgid "Add Milestone:"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:92
-#, python-format
-msgid "Format: %(datetimehint)s"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:107 trac/ticket/admin.py:399
-msgid "Due"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:107 trac/ticket/admin.py:399
-msgid "Completed"
-msgstr ""
-
-#: trac/admin/templates/admin_milestones.html:107 trac/ticket/web_ui.py:194
-msgid "Tickets"
-msgstr ""
-
-#: trac/admin/templates/admin_perms.html:14
+#: trac/admin/templates/admin_perms.html:24
 msgid "Manage Permissions and Groups"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:19
+#: trac/admin/templates/admin_perms.html:29
 msgid "Grant Permission:"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:21
-#: trac/admin/templates/admin_perms.html:44
+#: trac/admin/templates/admin_perms.html:31
+#: trac/admin/templates/admin_perms.html:55
 msgid "Subject:"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:24
+#: trac/admin/templates/admin_perms.html:34
 msgid "Action:"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:33
+#: trac/admin/templates/admin_perms.html:42
+#: trac/admin/templates/admin_perms.html:61
+#: trac/ticket/templates/admin_components.html:78
+#: trac/ticket/templates/admin_enums.html:51
+#: trac/ticket/templates/admin_milestones.html:114
+#: trac/ticket/templates/admin_versions.html:83
+#: trac/versioncontrol/templates/admin_repositories.html:115
+#: trac/versioncontrol/templates/admin_repositories.html:129
+msgid "Add"
+msgstr ""
+
+#: trac/admin/templates/admin_perms.html:44
 msgid ""
 "Grant permission for an action to a subject, which can be either a user\n"
 "            or a group."
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:42
+#: trac/admin/templates/admin_perms.html:53
 msgid "Add Subject to Group:"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:47
+#: trac/admin/templates/admin_perms.html:58
 msgid "Group:"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:52
+#: trac/admin/templates/admin_perms.html:63
 msgid "Add a user or group to an existing permission group."
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:63
-#: trac/admin/templates/admin_perms.html:88
+#: trac/admin/templates/admin_perms.html:74
+#: trac/admin/templates/admin_perms.html:103
 msgid "Subject"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:76
-msgid "Action is no longer defined"
+#: trac/admin/templates/admin_perms.html:85
+msgid "You don't have permission to revoke this action"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:81
+#: trac/admin/templates/admin_perms.html:91
+#, python-format
+msgid "%(action)s is no longer defined"
+msgstr ""
+
+#: trac/admin/templates/admin_perms.html:96
 msgid "No permissions"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:85
+#: trac/admin/templates/admin_perms.html:100
 msgid "Group Membership"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:88
+#: trac/admin/templates/admin_perms.html:103
 msgid "Group"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:105
+#: trac/admin/templates/admin_perms.html:120
 msgid "No group memberships"
 msgstr ""
 
-#: trac/admin/templates/admin_perms.html:113
+#: trac/admin/templates/admin_perms.html:124
+#: trac/ticket/templates/admin_components.html:107
+#: trac/ticket/templates/admin_enums.html:83
+#: trac/ticket/templates/admin_milestones.html:149
+#: trac/ticket/templates/admin_versions.html:110
+#: trac/versioncontrol/templates/admin_repositories.html:158
+msgid "Remove selected items"
+msgstr ""
+
+#: trac/admin/templates/admin_perms.html:128
 msgid ""
 "Note that [1:Subject] or [2:Group] names can't be all upper-case,\n"
 "      as that is reserved for permission names."
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:57
+#: trac/admin/templates/admin_plugins.html:67
 msgid "Manage Plugins"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:61
+#: trac/admin/templates/admin_plugins.html:71
 msgid "Install Plugin:"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:63
+#: trac/admin/templates/admin_plugins.html:73
 msgid "File: [1:]"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:68
+#: trac/admin/templates/admin_plugins.html:78
 msgid "Install"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:72
+#: trac/admin/templates/admin_plugins.html:82
 msgid ""
 "The web server does not have sufficient permissions to store files in\n"
 "            the environment plugins directory."
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:76
+#: trac/admin/templates/admin_plugins.html:86
 msgid "Upload a plugin packaged as Python egg."
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:100 trac/templates/diff_view.html:51
-#: trac/versioncontrol/templates/changeset.html:142
+#: trac/admin/templates/admin_plugins.html:110 trac/templates/diff_view.html:61
+#: trac/versioncontrol/templates/changeset.html:152
 msgid "Author:"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:109
+#: trac/admin/templates/admin_plugins.html:119
 msgid "Home page:"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:116
+#: trac/admin/templates/admin_plugins.html:126
 msgid "License:"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:124 trac/ticket/admin.py:77
-#: trac/ticket/api.py:306
+#: trac/admin/templates/admin_plugins.html:134 trac/ticket/admin.py:77
+#: trac/ticket/api.py:315
 msgid "Component"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:127
+#: trac/admin/templates/admin_plugins.html:137
 msgid "Show all descriptions"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:129
+#: trac/admin/templates/admin_plugins.html:139
 msgid "Hide all descriptions"
 msgstr ""
 
-#: trac/admin/templates/admin_plugins.html:133
+#: trac/admin/templates/admin_plugins.html:143
 msgid "Enabled"
 msgstr ""
 
-#: trac/admin/templates/admin_versions.html:10 trac/ticket/admin.py:431
-msgid "Versions"
-msgstr ""
-
-#: trac/admin/templates/admin_versions.html:19
-msgid "Manage Versions"
-msgstr ""
-
-#: trac/admin/templates/admin_versions.html:24
-msgid "Modify Version:"
-msgstr ""
-
-#: trac/admin/templates/admin_versions.html:31
-msgid "Date:"
-msgstr ""
-
-#: trac/admin/templates/admin_versions.html:59
-msgid "Add Version:"
-msgstr ""
-
-#: trac/admin/templates/admin_versions.html:64
-msgid "Released:"
-msgstr ""
-
-#: trac/admin/templates/admin_versions.html:83
-msgid "Released"
-msgstr ""
-
-#: trac/db/api.py:308
+#: trac/db/api.py:334
 #, python-format
 msgid "Unsupported database type \"%(scheme)s\""
 msgstr ""
 
-#: trac/db/api.py:347
+#: trac/db/api.py:373
 #, python-format
 msgid ""
 "Unknown scheme \"%(scheme)s\"; database connection string must start with"
 " {scheme}:/"
 msgstr ""
 
-#: trac/db/mysql_backend.py:87
+#: trac/db/mysql_backend.py:92
 msgid "Cannot load Python bindings for MySQL"
 msgstr ""
 
-#: trac/db/mysql_backend.py:229 trac/db/postgres_backend.py:179
+#: trac/db/mysql_backend.py:248 trac/db/postgres_backend.py:179
 #: trac/db/postgres_backend.py:198
 #, python-format
 msgid "Unable to run %(path)s: %(msg)s"
 msgstr ""
 
-#: trac/db/mysql_backend.py:233
+#: trac/db/mysql_backend.py:252
 #, python-format
 msgid "mysqldump failed: %(msg)s"
 msgstr ""
 
-#: trac/db/mysql_backend.py:235 trac/db/postgres_backend.py:204
-#: trac/db/sqlite_backend.py:245
+#: trac/db/mysql_backend.py:254 trac/db/postgres_backend.py:204
+#: trac/db/sqlite_backend.py:247
 msgid "No destination file created"
 msgstr ""
 
+#: trac/db/mysql_backend.py:290
+#, python-format
+msgid ""
+"All tables must be created as InnoDB or NDB storage engine to support "
+"transactions. The following tables have been created as storage engine "
+"which doesn't support transactions: %(tables)s"
+msgstr ""
+
+#: trac/db/mysql_backend.py:299
+#, python-format
+msgid ""
+"All tables must be created with utf8_bin or utf8mb4_bin as collation. The"
+" following tables don't have the collations: %(tables)s"
+msgstr ""
+
+#: trac/db/mysql_backend.py:314
+#, python-format
+msgid ""
+"The current storage engine is %(engine)s. It must be InnoDB or NDB "
+"storage engine to support transactions."
+msgstr ""
+
+#: trac/db/mysql_backend.py:320
+#, python-format
+msgid ""
+"The current storage engine for TEMPORARY tables is %(engine)s. It must be"
+" InnoDB or NDB storage engine to support transactions."
+msgstr ""
+
+#: trac/db/mysql_backend.py:332
+#, python-format
+msgid ""
+"The charset and collation of database are '%(charset)s' and "
+"'%(collation)s'. The database must be created with one of %(supported)s."
+msgstr ""
+
 #: trac/db/pool.py:130
 #, python-format
 msgid "Unable to get database connection within %(time)d seconds."
@@ -1510,56 +1485,56 @@
 msgid "pg_dump failed: %(msg)s"
 msgstr ""
 
-#: trac/db/sqlite_backend.py:156
+#: trac/db/sqlite_backend.py:158 trac/db/sqlite_backend.py:261
 msgid "Cannot load Python bindings for SQLite"
 msgstr ""
 
-#: trac/db/sqlite_backend.py:159
+#: trac/db/sqlite_backend.py:161
 #, python-format
 msgid "Need at least PySqlite %(version)s or higher"
 msgstr ""
 
-#: trac/db/sqlite_backend.py:162
+#: trac/db/sqlite_backend.py:164
 msgid "PySqlite 2.5.2 - 2.5.4 break Trac, please use 2.5.5 or higher"
 msgstr ""
 
-#: trac/db/sqlite_backend.py:195
+#: trac/db/sqlite_backend.py:197
 #, python-format
 msgid "Database already exists at %(path)s"
 msgstr ""
 
-#: trac/db/sqlite_backend.py:262
+#: trac/db/sqlite_backend.py:265
 #, python-format
 msgid "Database \"%(path)s\" not found."
 msgstr ""
 
-#: trac/db/sqlite_backend.py:271
+#: trac/db/sqlite_backend.py:274
 #, python-format
 msgid ""
 "The user %(user)s requires read _and_ write permissions to the database "
 "file %(path)s and the directory it is located in."
 msgstr ""
 
-#: trac/mimeview/api.py:685 trac/mimeview/api.py:695
+#: trac/mimeview/api.py:691 trac/mimeview/api.py:701
 #, python-format
 msgid "No available MIME conversions from %(old)s to %(new)s"
 msgstr ""
 
-#: trac/mimeview/api.py:808
+#: trac/mimeview/api.py:814
 #, python-format
 msgid "HTML preview using %(renderer)s failed (%(err)s)"
 msgstr ""
 
-#: trac/mimeview/api.py:839
+#: trac/mimeview/api.py:845
 #, python-format
 msgid "Can't use %(annotator)s annotator: %(error)s"
 msgstr ""
 
-#: trac/mimeview/api.py:1114 trac/templates/error.html:148
+#: trac/mimeview/api.py:1121 trac/templates/error.html:167
 msgid "Line"
 msgstr ""
 
-#: trac/mimeview/api.py:1114
+#: trac/mimeview/api.py:1121
 msgid "Line numbers"
 msgstr ""
 
@@ -1577,56 +1552,56 @@
 msgid "this hunk was shorter than expected"
 msgstr ""
 
-#: trac/mimeview/pygments.py:132 trac/prefs/templates/prefs_pygments.html:9
+#: trac/mimeview/pygments.py:132 trac/prefs/templates/prefs_pygments.html:19
 msgid "Syntax Highlighting"
 msgstr ""
 
-#: trac/mimeview/pygments.py:141 trac/prefs/web_ui.py:160
+#: trac/mimeview/pygments.py:141 trac/prefs/web_ui.py:170
 msgid "Your preferences have been saved."
 msgstr ""
 
-#: trac/mimeview/rst.py:125 trac/mimeview/rst.py:148
+#: trac/mimeview/rst.py:126 trac/mimeview/rst.py:149
 #, python-format
 msgid "%(link)s is not a valid TracLink"
 msgstr ""
 
-#: trac/prefs/web_ui.py:56 trac/prefs/templates/prefs.html:16
+#: trac/prefs/web_ui.py:51 trac/prefs/templates/prefs.html:26
 msgid "Preferences"
 msgstr ""
 
-#: trac/prefs/web_ui.py:83
+#: trac/prefs/web_ui.py:79
 msgid "Unknown preference panel"
 msgstr ""
 
-#: trac/prefs/web_ui.py:95 trac/prefs/templates/prefs_datetime.html:10
+#: trac/prefs/web_ui.py:91 trac/prefs/templates/prefs_datetime.html:20
 msgid "Date & Time"
 msgstr ""
 
-#: trac/prefs/web_ui.py:96 trac/prefs/templates/prefs_keybindings.html:10
+#: trac/prefs/web_ui.py:92 trac/prefs/templates/prefs_keybindings.html:20
 msgid "Keyboard Shortcuts"
 msgstr ""
 
-#: trac/prefs/web_ui.py:97 trac/prefs/templates/prefs_userinterface.html:10
+#: trac/prefs/web_ui.py:93 trac/prefs/templates/prefs_userinterface.html:20
 msgid "User Interface"
 msgstr ""
 
-#: trac/prefs/web_ui.py:99 trac/prefs/templates/prefs_language.html:10
+#: trac/prefs/web_ui.py:95 trac/prefs/templates/prefs_language.html:20
 msgid "Language"
 msgstr ""
 
-#: trac/prefs/web_ui.py:101 trac/prefs/templates/prefs_advanced.html:9
+#: trac/prefs/web_ui.py:97 trac/prefs/templates/prefs_advanced.html:19
 msgid "Advanced"
 msgstr ""
 
-#: trac/prefs/web_ui.py:167
+#: trac/prefs/web_ui.py:177
 msgid "The session has been loaded."
 msgstr ""
 
-#: trac/prefs/templates/prefs.html:10
+#: trac/prefs/templates/prefs.html:20
 msgid "Preferences:"
 msgstr ""
 
-#: trac/prefs/templates/prefs.html:17
+#: trac/prefs/templates/prefs.html:27
 msgid ""
 "This page lets you customize your personal settings for this site.\n"
 "      These settings are stored on the server and are identified by a "
@@ -1636,19 +1611,19 @@
 "      restored on subsequent visits."
 msgstr ""
 
-#: trac/prefs/templates/prefs.html:33
+#: trac/prefs/templates/prefs.html:43
 msgid "Save changes"
 msgstr ""
 
-#: trac/prefs/templates/prefs_advanced.html:14
+#: trac/prefs/templates/prefs_advanced.html:24
 msgid "Session key:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_advanced.html:17
+#: trac/prefs/templates/prefs_advanced.html:27
 msgid "Change"
 msgstr ""
 
-#: trac/prefs/templates/prefs_advanced.html:18
+#: trac/prefs/templates/prefs_advanced.html:28
 msgid ""
 "The session key is used to identify stored custom\n"
 "      settings and session data on the server. Although it is\n"
@@ -1657,15 +1632,15 @@
 "      in a different web browser."
 msgstr ""
 
-#: trac/prefs/templates/prefs_advanced.html:26
+#: trac/prefs/templates/prefs_advanced.html:36
 msgid "Restore session:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_advanced.html:29
+#: trac/prefs/templates/prefs_advanced.html:39
 msgid "Load"
 msgstr ""
 
-#: trac/prefs/templates/prefs_advanced.html:30
+#: trac/prefs/templates/prefs_advanced.html:40
 msgid ""
 "You may load a previously created session by entering the\n"
 "      corresponding session key below. This lets you share settings "
@@ -1673,39 +1648,39 @@
 "      multiple computers and web browsers."
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:16
+#: trac/prefs/templates/prefs_datetime.html:26
 msgid "Time zone:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:18
+#: trac/prefs/templates/prefs_datetime.html:28
 msgid "Default time zone"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:25
+#: trac/prefs/templates/prefs_datetime.html:35
 msgid ""
 "Configuring your time zone will result in all\n"
 "      dates and times displayed on this site to use your time zone\n"
 "      instead of that of the server."
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:34
+#: trac/prefs/templates/prefs_datetime.html:44
 #, python-format
 msgid "Example: The current time is [1:%(time)s] (UTC)."
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:39
+#: trac/prefs/templates/prefs_datetime.html:49
 #, python-format
 msgid ""
 "In your time zone %(tz)s, this would be displayed as\n"
 "            [1:%(formatted)s]."
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:45
+#: trac/prefs/templates/prefs_datetime.html:55
 #, python-format
 msgid "In the default time zone, this would be displayed as [1:%(formatted)s]."
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:51
+#: trac/prefs/templates/prefs_datetime.html:61
 msgid ""
 "Note: Universal Co-ordinated Time (UTC) is also known as Greenwich Mean "
 "Time (GMT).[1:]\n"
@@ -1713,19 +1688,19 @@
 "Greenwich, i.e. ahead of Universal Time."
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:59
+#: trac/prefs/templates/prefs_datetime.html:69
 msgid "Date format:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:61
+#: trac/prefs/templates/prefs_datetime.html:71
 msgid "Default date format"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:63
+#: trac/prefs/templates/prefs_datetime.html:73
 msgid "Your language setting"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:69
+#: trac/prefs/templates/prefs_datetime.html:79
 msgid ""
 "Configuring your date format will result in formatting\n"
 "      and parsing datetime displayed on this site to use your date format"
@@ -1733,23 +1708,23 @@
 "      instead of that of the server."
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:75
+#: trac/prefs/templates/prefs_datetime.html:85
 msgid "Date relative/absolute format:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:77
+#: trac/prefs/templates/prefs_datetime.html:87
 msgid "Default format"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:79
+#: trac/prefs/templates/prefs_datetime.html:89
 msgid "Relative format"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:81
+#: trac/prefs/templates/prefs_datetime.html:91
 msgid "Absolute format"
 msgstr ""
 
-#: trac/prefs/templates/prefs_datetime.html:85
+#: trac/prefs/templates/prefs_datetime.html:95
 msgid ""
 "Configuring your relative/absolute format will result in\n"
 "      formatting datetime displayed on this site to use your format "
@@ -1757,32 +1732,32 @@
 "      that of the server."
 msgstr ""
 
-#: trac/prefs/templates/prefs_general.html:15
+#: trac/prefs/templates/prefs_general.html:25
 msgid "Full name:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_general.html:20
+#: trac/prefs/templates/prefs_general.html:30
 msgid "Email address:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_general.html:26
+#: trac/prefs/templates/prefs_general.html:36
 msgid ""
 "This information is used to automatically populate some forms\n"
 "        on this site with your contact details."
 msgstr ""
 
-#: trac/prefs/templates/prefs_general.html:30
+#: trac/prefs/templates/prefs_general.html:40
 msgid ""
 "This information is used to associate your login name with your\n"
 "        email address and full name, which is used for email\n"
 "        notification and RSS feeds, for example."
 msgstr ""
 
-#: trac/prefs/templates/prefs_keybindings.html:18
+#: trac/prefs/templates/prefs_keybindings.html:28
 msgid "Enable access keys"
 msgstr ""
 
-#: trac/prefs/templates/prefs_keybindings.html:21
+#: trac/prefs/templates/prefs_keybindings.html:31
 msgid ""
 "This site provides keyboard shortcuts for\n"
 "      faster access to certain functions of this site. As these shortcuts"
@@ -1793,122 +1768,128 @@
 "      for more information on access keys."
 msgstr ""
 
-#: trac/prefs/templates/prefs_language.html:15
+#: trac/prefs/templates/prefs_language.html:25
 msgid "Language:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_language.html:17
+#: trac/prefs/templates/prefs_language.html:28
 msgid "Default language"
 msgstr ""
 
-#: trac/prefs/templates/prefs_language.html:23
+#: trac/prefs/templates/prefs_language.html:42
 msgid ""
 "Configuring your language will result in all text\n"
-"      displayed on this site to use your language instead of that of the\n"
-"      server."
+"        displayed on this site to use your language instead of that of "
+"the\n"
+"        server."
 msgstr ""
 
-#: trac/prefs/templates/prefs_language.html:27
+#: trac/prefs/templates/prefs_language.html:46
 msgid ""
 "The [1:Default language] option uses the browser's\n"
 "        language negotiation feature to select the appropriate language."
 msgstr ""
 
-#: trac/prefs/templates/prefs_pygments.html:37
+#: trac/prefs/templates/prefs_pygments.html:47
 msgid ""
 "The Pygments syntax highlighter can be used with\n"
 "      different coloring styles."
 msgstr ""
 
-#: trac/prefs/templates/prefs_pygments.html:39
+#: trac/prefs/templates/prefs_pygments.html:49
 msgid "Style:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_pygments.html:44
+#: trac/prefs/templates/prefs_pygments.html:54
 msgid "Preview:"
 msgstr ""
 
-#: trac/prefs/templates/prefs_userinterface.html:18
+#: trac/prefs/templates/prefs_userinterface.html:28
 msgid "Use only symbols for buttons."
 msgstr ""
 
-#: trac/prefs/templates/prefs_userinterface.html:21
+#: trac/prefs/templates/prefs_userinterface.html:31
 msgid ""
 "Display only the icon or symbol for\n"
 "      short inline buttons, and hide the text caption."
 msgstr ""
 
-#: trac/prefs/templates/prefs_userinterface.html:29
+#: trac/prefs/templates/prefs_userinterface.html:39
 msgid "Hide help links."
 msgstr ""
 
-#: trac/prefs/templates/prefs_userinterface.html:32
+#: trac/prefs/templates/prefs_userinterface.html:42
 msgid ""
 "Don't show the various help links.\n"
 "      This reduces the verbosity of the pages."
 msgstr ""
 
-#: trac/search/web_ui.py:72 trac/search/templates/search.html:12
-#: trac/search/templates/search.html:26 trac/search/templates/search.html:31
-#: trac/templates/theme.html:29
+#: trac/search/web_ui.py:71 trac/search/templates/search.html:22
+#: trac/search/templates/search.html:33 trac/search/templates/search.html:38
+#: trac/templates/theme.html:39
 msgid "Search"
 msgstr ""
 
-#: trac/search/web_ui.py:166
+#: trac/search/web_ui.py:165
 #, python-format
 msgid "Browse repository path %(path)s"
 msgstr ""
 
-#: trac/search/web_ui.py:206
+#: trac/search/web_ui.py:205
 #, python-format
 msgid "Search query too short. Query must be at least %(num)s characters long."
 msgstr ""
 
-#: trac/search/web_ui.py:245 trac/ticket/query.py:785 trac/ticket/report.py:459
+#: trac/search/web_ui.py:231 trac/ticket/query.py:812 trac/ticket/report.py:458
+#, python-format
+msgid "Page %(num)d"
+msgstr ""
+
+#: trac/search/web_ui.py:244 trac/ticket/query.py:799 trac/ticket/report.py:449
 msgid "Next Page"
 msgstr ""
 
-#: trac/search/web_ui.py:251 trac/ticket/query.py:790 trac/ticket/report.py:462
+#: trac/search/web_ui.py:250 trac/ticket/query.py:804 trac/ticket/report.py:452
 msgid "Previous Page"
 msgstr ""
 
-#: trac/search/templates/search.html:11
+#: trac/search/templates/search.html:21
 msgid "Search Results"
 msgstr ""
 
-#: trac/search/templates/search.html:43
-#: trac/ticket/templates/query_results.html:20
-#: trac/ticket/templates/report_view.html:78
+#: trac/search/templates/search.html:50
+#: trac/ticket/templates/query_results.html:29
+#: trac/ticket/templates/report_view.html:88
 msgid "Results"
 msgstr ""
 
-#: trac/search/templates/search.html:51
+#: trac/search/templates/search.html:58
 #, python-format
 msgid "Quickjump to %(name)s"
 msgstr ""
 
-#: trac/search/templates/search.html:59
+#: trac/search/templates/search.html:66
 #, python-format
 msgid "By %(author)s"
 msgstr ""
 
-#: trac/search/templates/search.html:68
-#: trac/ticket/templates/report_view.html:97
-#: trac/ticket/templates/report_view.html:208
+#: trac/search/templates/search.html:75
+#: trac/ticket/templates/report_view.html:107
+#: trac/ticket/templates/report_view.html:218
 msgid "No matches found."
 msgstr ""
 
-#: trac/search/templates/search.html:72
+#: trac/search/templates/search.html:79
 msgid ""
 "[1:Note:] See [2:TracSearch]\n"
 "        for help on searching."
 msgstr ""
 
-#: trac/templates/about.html:26
+#: trac/templates/about.html:38
 msgid "Trac: Integrated SCM & Project Management"
 msgstr ""
 
-#: trac/templates/about.html:30
+#: trac/templates/about.html:42
 msgid ""
 "Trac is a web-based software project management and bug/issue\n"
 "        tracking system emphasizing ease of use and low ceremony.\n"
@@ -1918,7 +1899,7 @@
 "        and changes within a project."
 msgstr ""
 
-#: trac/templates/about.html:36
+#: trac/templates/about.html:48
 msgid ""
 "Trac is distributed under the modified BSD License.[1:]\n"
 "        The complete text of the license can be found\n"
@@ -1926,126 +1907,127 @@
 "        as well as in the [3:COPYING] file included in the distribution."
 msgstr ""
 
-#: trac/templates/about.html:41
+#: trac/templates/about.html:53
 msgid "python powered"
 msgstr ""
 
-#: trac/templates/about.html:44
+#: trac/templates/about.html:56
 msgid ""
 "Please visit the Trac open source project:\n"
 "        [1:http://trac.edgewall.org/]"
 msgstr ""
 
-#: trac/templates/about.html:46
+#: trac/templates/about.html:58
 msgid ""
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
 
-#: trac/templates/about.html:54
+#: trac/templates/about.html:66
 msgid "System Information"
 msgstr ""
 
-#: trac/templates/about.html:56
+#: trac/templates/about.html:68
 msgid "Package"
 msgstr ""
 
-#: trac/templates/about.html:56 trac/templates/about.html:69
-#: trac/templates/history_view.html:28 trac/ticket/admin.py:431
-#: trac/ticket/api.py:307
+#: trac/templates/about.html:68 trac/templates/about.html:81
+#: trac/templates/history_view.html:38 trac/ticket/admin.py:440
+#: trac/ticket/api.py:316
 msgid "Version"
 msgstr ""
 
-#: trac/templates/about.html:67
+#: trac/templates/about.html:79
 msgid "Installed Plugins"
 msgstr ""
 
-#: trac/templates/about.html:69
+#: trac/templates/about.html:81
 msgid "Location"
 msgstr ""
 
-#: trac/templates/about.html:77 trac/templates/error.html:192
-#: trac/web/main.py:589
+#: trac/templates/about.html:89 trac/templates/error.html:211
+#: trac/web/main.py:583
 msgid "N/A"
 msgstr ""
 
-#: trac/templates/about.html:89
+#: trac/templates/about.html:101
 msgid "Section"
 msgstr ""
 
-#: trac/templates/about.html:91 trac/templates/error.html:160
+#: trac/templates/about.html:103 trac/templates/error.html:179
 msgid "Value"
 msgstr ""
 
-#: trac/templates/attach_file_form.html:15
+#: trac/templates/attach_file_form.html:24
+msgid "Attach another file"
+msgstr ""
+
+#: trac/templates/attach_file_form.html:24
 msgid "Attach file"
 msgstr ""
 
-#: trac/templates/attachment.html:12
+#: trac/templates/attachment.html:22
 msgid "– Attachment"
 msgstr ""
 
-#: trac/templates/attachment.html:13
+#: trac/templates/attachment.html:23
 msgid "– Attachments"
 msgstr ""
 
-#: trac/templates/attachment.html:14
+#: trac/templates/attachment.html:24
 #, python-format
 msgid "%(filename)s on %(parent)s – Attachment"
 msgstr ""
 
-#: trac/templates/attachment.html:29
+#: trac/templates/attachment.html:39
 #, python-format
 msgid "Add Attachment to [1:%(parent)s]"
 msgstr ""
 
-#: trac/templates/attachment.html:33
+#: trac/templates/attachment.html:43
 #, python-format
 msgid "(size limit %(value)s)"
 msgstr ""
 
-#: trac/templates/attachment.html:37
+#: trac/templates/attachment.html:47
 msgid "Attachment Info"
 msgstr ""
 
-#: trac/templates/attachment.html:40 trac/ticket/templates/ticket.html:355
-#: trac/wiki/templates/wiki_edit_form.html:42
+#: trac/templates/attachment.html:50 trac/ticket/templates/ticket.html:358
+#: trac/wiki/templates/wiki_edit_form.html:53
 msgid "Your email or username:"
 msgstr ""
 
-#: trac/templates/attachment.html:46
+#: trac/templates/attachment.html:56
 msgid "Description of the file (optional):"
 msgstr ""
 
-#: trac/templates/attachment.html:52
+#: trac/templates/attachment.html:62
 msgid "Replace existing attachment of the same name"
 msgstr ""
 
-#: trac/templates/attachment.html:62
+#: trac/templates/attachment.html:72
 msgid "Add attachment"
 msgstr ""
 
-#: trac/templates/attachment.html:70
+#: trac/templates/attachment.html:80
 msgid "Are you sure you want to delete this attachment?"
 msgstr ""
 
-#: trac/templates/attachment.html:77 trac/templates/attachment.html:119
+#: trac/templates/attachment.html:86 trac/templates/attachment.html:128
 msgid "Delete attachment"
 msgstr ""
 
-#: trac/templates/attachment.html:86
-msgid "Attach another file"
-msgstr ""
-
-#: trac/templates/attachment.html:98 trac/templates/list_of_attachments.html:21
-#: trac/templates/macros.html:19 trac/util/text.py:621
-#: trac/versioncontrol/templates/browser.html:189
-#: trac/versioncontrol/templates/dir_entries.html:17
+#: trac/templates/attachment.html:107
+#: trac/templates/list_of_attachments.html:30 trac/templates/macros.html:29
+#: trac/util/text.py:633 trac/util/tests/html.py:215
+#: trac/util/tests/html.py:228 trac/versioncontrol/templates/browser.html:199
+#: trac/versioncontrol/templates/dir_entries.html:28
 #, python-format
 msgid "%(size)s bytes"
 msgstr ""
 
-#: trac/templates/attachment.html:96
+#: trac/templates/attachment.html:105
 #, python-format
 msgid ""
 "File %(file)s,\n"
@@ -2053,42 +2035,42 @@
 "                (added by %(author)s, %(date)s)"
 msgstr ""
 
-#: trac/templates/diff_div.html:72
+#: trac/templates/diff_div.html:81
 #, python-format
 msgid ""
 "Property %(name)s\n"
 "                  changed from %(old)s to %(new)s"
 msgstr ""
 
-#: trac/templates/diff_div.html:76
+#: trac/templates/diff_div.html:85
 #, python-format
 msgid "Property %(name)s set to %(value)s"
 msgstr ""
 
-#: trac/templates/diff_div.html:79
+#: trac/templates/diff_div.html:88
 #, python-format
 msgid "Property %(name)s deleted"
 msgstr ""
 
-#: trac/templates/diff_div.html:86
+#: trac/templates/diff_div.html:95
 msgid "Differences"
 msgstr ""
 
-#: trac/templates/diff_options.html:10
-#: trac/versioncontrol/templates/browser.html:138
-#: trac/versioncontrol/templates/browser.html:146
+#: trac/templates/diff_options.html:21
+#: trac/versioncontrol/templates/browser.html:148
+#: trac/versioncontrol/templates/browser.html:156
 msgid "View differences"
 msgstr ""
 
-#: trac/templates/diff_options.html:13
+#: trac/templates/diff_options.html:24
 msgid "inline"
 msgstr ""
 
-#: trac/templates/diff_options.html:15
+#: trac/templates/diff_options.html:26
 msgid "side by side"
 msgstr ""
 
-#: trac/templates/diff_options.html:18
+#: trac/templates/diff_options.html:29
 msgid ""
 "[1:[2:]\n"
 "             Show]\n"
@@ -2096,37 +2078,37 @@
 "             lines around each change]"
 msgstr ""
 
-#: trac/templates/diff_options.html:28
+#: trac/templates/diff_options.html:39
 msgid "Show the changes in full context"
 msgstr ""
 
-#: trac/templates/diff_options.html:32
+#: trac/templates/diff_options.html:43
 msgid "Ignore:"
 msgstr ""
 
-#: trac/templates/diff_options.html:36
+#: trac/templates/diff_options.html:47
 msgid "Blank lines"
 msgstr ""
 
-#: trac/templates/diff_options.html:41
+#: trac/templates/diff_options.html:52
 msgid "Case changes"
 msgstr ""
 
-#: trac/templates/diff_options.html:46
+#: trac/templates/diff_options.html:57
 msgid "White space changes"
 msgstr ""
 
-#: trac/templates/diff_options.html:50
-#: trac/ticket/templates/milestone_view.html:57
-#: trac/ticket/templates/query.html:220
-#: trac/ticket/templates/report_view.html:49
-#: trac/ticket/templates/roadmap.html:28
-#: trac/timeline/templates/timeline.html:36
-#: trac/versioncontrol/templates/revisionlog.html:80
+#: trac/templates/diff_options.html:61
+#: trac/ticket/templates/milestone_view.html:67
+#: trac/ticket/templates/query.html:230
+#: trac/ticket/templates/report_view.html:59
+#: trac/ticket/templates/roadmap.html:38
+#: trac/timeline/templates/timeline.html:46
+#: trac/versioncontrol/templates/revisionlog.html:90
 msgid "Update"
 msgstr ""
 
-#: trac/templates/diff_view.html:18
+#: trac/templates/diff_view.html:28
 #, python-format
 msgid ""
 "Changes between\n"
@@ -2135,7 +2117,7 @@
 "          [3:%(name)s]"
 msgstr ""
 
-#: trac/templates/diff_view.html:23
+#: trac/templates/diff_view.html:33
 #, python-format
 msgid ""
 "Changes between\n"
@@ -2144,7 +2126,7 @@
 "          [3:%(name)s]"
 msgstr ""
 
-#: trac/templates/diff_view.html:28
+#: trac/templates/diff_view.html:38
 #, python-format
 msgid ""
 "Changes from\n"
@@ -2152,79 +2134,80 @@
 "          [2:%(name)s]"
 msgstr ""
 
-#: trac/templates/diff_view.html:43
-#: trac/versioncontrol/templates/changeset.html:136
+#: trac/templates/diff_view.html:53
+#: trac/versioncontrol/templates/changeset.html:146
 msgid "Timestamp:"
 msgstr ""
 
-#: trac/templates/diff_view.html:45 trac/templates/diff_view.html:53
-#: trac/templates/diff_view.html:59
+#: trac/templates/diff_view.html:55 trac/templates/diff_view.html:63
+#: trac/templates/diff_view.html:69
 msgid "(multiple changes)"
 msgstr ""
 
-#: trac/templates/diff_view.html:47
+#: trac/templates/diff_view.html:57
 #, python-format
 msgid "%(date)s (%(duration)s ago)"
 msgstr ""
 
-#: trac/templates/diff_view.html:55
+#: trac/templates/diff_view.html:65
 #, python-format
 msgid "(IP: %(ipnr)s)"
 msgstr ""
 
-#: trac/templates/diff_view.html:57 trac/ticket/templates/batch_modify.html:12
+#: trac/templates/diff_view.html:67 trac/ticket/templates/batch_modify.html:22
 #: trac/ticket/templates/batch_ticket_notify_email.txt:9
+#: trac/ticket/templates/milestone_edit.html:107
 #: trac/ticket/templates/ticket_notify_email.txt:21
 msgid "Comment:"
 msgstr ""
 
-#: trac/templates/diff_view.html:65
-#: trac/versioncontrol/templates/changeset.html:198
-#: trac/versioncontrol/templates/revisionlog.html:86
+#: trac/templates/diff_view.html:75
+#: trac/versioncontrol/templates/changeset.html:208
+#: trac/versioncontrol/templates/revisionlog.html:96
 msgid "Legend:"
 msgstr ""
 
-#: trac/templates/diff_view.html:67
-#: trac/versioncontrol/templates/changeset.html:200
+#: trac/templates/diff_view.html:77
+#: trac/versioncontrol/templates/changeset.html:210
 msgid "Unmodified"
 msgstr ""
 
-#: trac/templates/diff_view.html:68
-#: trac/versioncontrol/templates/changeset.html:201
-#: trac/versioncontrol/templates/revisionlog.html:88
+#: trac/templates/diff_view.html:78
+#: trac/versioncontrol/templates/changeset.html:211
+#: trac/versioncontrol/templates/revisionlog.html:98
 msgid "Added"
 msgstr ""
 
-#: trac/templates/diff_view.html:69
-#: trac/versioncontrol/templates/changeset.html:202
-#: trac/versioncontrol/templates/revisionlog.html:90
+#: trac/templates/diff_view.html:79
+#: trac/versioncontrol/templates/changeset.html:212
+#: trac/versioncontrol/templates/revisionlog.html:100
 msgid "Removed"
 msgstr ""
 
-#: trac/templates/diff_view.html:70 trac/ticket/api.py:336
-#: trac/versioncontrol/templates/changeset.html:204
-#: trac/versioncontrol/templates/revisionlog.html:92 trac/wiki/admin.py:197
+#: trac/templates/diff_view.html:80 trac/ticket/api.py:345
+#: trac/versioncontrol/templates/changeset.html:214
+#: trac/versioncontrol/templates/revisionlog.html:102 trac/wiki/admin.py:197
 msgid "Modified"
 msgstr ""
 
-#: trac/templates/error.html:10 trac/templates/index.html:18
-#: trac/web/main.py:516
+#: trac/templates/error.html:20 trac/templates/index.html:28
+#: trac/web/api.py:165
 msgid "Error"
 msgstr ""
 
-#: trac/templates/error.html:65
+#: trac/templates/error.html:80
 msgid "Create"
 msgstr ""
 
-#: trac/templates/error.html:80
+#: trac/templates/error.html:95
 msgid "Oops…"
 msgstr ""
 
-#: trac/templates/error.html:82
+#: trac/templates/error.html:97
 msgid "Trac detected an internal error:"
 msgstr ""
 
-#: trac/templates/error.html:87
+#: trac/templates/error.html:102
 msgid ""
 "There was an internal error in Trac.\n"
 "                It is recommended that you notify your local\n"
@@ -2233,50 +2216,50 @@
 "                reproduce the issue."
 msgstr ""
 
-#: trac/templates/error.html:95
+#: trac/templates/error.html:110
 #, python-format
 msgid "To that end, you could %(create)s a ticket."
 msgstr ""
 
-#: trac/templates/error.html:97
+#: trac/templates/error.html:112
 msgid "The action that triggered the error was:"
 msgstr ""
 
-#: trac/templates/error.html:102
+#: trac/templates/error.html:116 trac/templates/error.html:121
 msgid "This is probably a local installation issue."
 msgstr ""
 
-#: trac/templates/error.html:103
+#: trac/templates/error.html:122
 #, python-format
 msgid ""
 "You should %(create)s a ticket at the admin Trac to report\n"
 "                    the issue."
 msgstr ""
 
-#: trac/templates/error.html:109
+#: trac/templates/error.html:128
 msgid "Found a bug in Trac?"
 msgstr ""
 
-#: trac/templates/error.html:110
+#: trac/templates/error.html:129
 msgid ""
 "If you think this should work and you can reproduce the problem,\n"
 "              you should consider creating a bug report."
 msgstr ""
 
-#: trac/templates/error.html:113
+#: trac/templates/error.html:132
 #, python-format
 msgid "Note that the %(name)s plugin seems to be involved."
 msgstr ""
 
-#: trac/templates/error.html:116
+#: trac/templates/error.html:135
 msgid "Note that the following plugins seem to be involved:"
 msgstr ""
 
-#: trac/templates/error.html:120
+#: trac/templates/error.html:139
 msgid "Please report this issue to the plugin maintainer."
 msgstr ""
 
-#: trac/templates/error.html:122
+#: trac/templates/error.html:141
 msgid ""
 "Before you do that, though, please first try\n"
 "                [1:[2:searching]\n"
@@ -2289,22 +2272,22 @@
 "                instead of creating a ticket."
 msgstr ""
 
-#: trac/templates/error.html:131
+#: trac/templates/error.html:150
 #, python-format
 msgid ""
 "Otherwise, please %(create)s a new bug report\n"
 "                describing the problem and explain how to reproduce it."
 msgstr ""
 
-#: trac/templates/error.html:135
+#: trac/templates/error.html:154
 msgid "Python Traceback"
 msgstr ""
 
-#: trac/templates/error.html:136
+#: trac/templates/error.html:155
 msgid "Most recent call last:"
 msgstr ""
 
-#: trac/templates/error.html:140
+#: trac/templates/error.html:159
 #, python-format
 msgid ""
 "[1:File \"%(file)s\",\n"
@@ -2312,86 +2295,86 @@
 "                        [3:%(function)s]"
 msgstr ""
 
-#: trac/templates/error.html:146
+#: trac/templates/error.html:165
 msgid "Code fragment:"
 msgstr ""
 
-#: trac/templates/error.html:158
+#: trac/templates/error.html:177
 msgid "Local variables:"
 msgstr ""
 
-#: trac/templates/error.html:172
+#: trac/templates/error.html:191
 #, python-format
 msgid "File \"%(file)s\", line %(line)s, in %(function)s"
 msgstr ""
 
-#: trac/templates/error.html:175
+#: trac/templates/error.html:194
 msgid "Switch to plain text view"
 msgstr ""
 
-#: trac/templates/error.html:178
+#: trac/templates/error.html:197
 msgid "System Information:"
 msgstr ""
 
-#: trac/templates/error.html:186
+#: trac/templates/error.html:205
 msgid "Enabled Plugins:"
 msgstr ""
 
-#: trac/templates/error.html:202
+#: trac/templates/error.html:221
 msgid "TracGuide"
 msgstr ""
 
-#: trac/templates/error.html:202
+#: trac/templates/error.html:221
 msgid "— The Trac User and Administration Guide"
 msgstr ""
 
-#: trac/templates/history_view.html:16
+#: trac/templates/history_view.html:26
 #, python-format
 msgid "Change History for [1:%(name)s]"
 msgstr ""
 
-#: trac/templates/history_view.html:22 trac/templates/history_view.html:55
-#: trac/versioncontrol/templates/diff_form.html:58
-#: trac/versioncontrol/templates/revisionlog.html:101
-#: trac/versioncontrol/templates/revisionlog.html:204
+#: trac/templates/history_view.html:32 trac/templates/history_view.html:65
+#: trac/versioncontrol/templates/diff_form.html:68
+#: trac/versioncontrol/templates/revisionlog.html:111
+#: trac/versioncontrol/templates/revisionlog.html:214
 msgid "View changes"
 msgstr ""
 
-#: trac/templates/history_view.html:24
+#: trac/templates/history_view.html:34
 msgid "Change history"
 msgstr ""
 
-#: trac/templates/history_view.html:31
+#: trac/templates/history_view.html:41
 msgid "Comment"
 msgstr ""
 
-#: trac/templates/history_view.html:43
+#: trac/templates/history_view.html:53
 msgid "View this version"
 msgstr ""
 
-#: trac/templates/history_view.html:46
+#: trac/templates/history_view.html:56
 #, python-format
 msgid "IP-Address: %(ipnr)s"
 msgstr ""
 
-#: trac/templates/index.html:8 trac/templates/index.html:12
+#: trac/templates/index.html:18 trac/templates/index.html:22
 msgid "Available Projects"
 msgstr ""
 
-#: trac/templates/layout.html:28
+#: trac/templates/layout.html:38
 #, python-format
 msgid "Search %(project)s"
 msgstr ""
 
-#: trac/templates/layout.html:69
+#: trac/templates/layout.html:85
 msgid "Download in other formats:"
 msgstr ""
 
-#: trac/templates/list_of_attachments.html:19
+#: trac/templates/list_of_attachments.html:28
 msgid "View attachment"
 msgstr ""
 
-#: trac/templates/list_of_attachments.html:18
+#: trac/templates/list_of_attachments.html:27
 #, python-format
 msgid ""
 "[1:%(file)s][2:​]\n"
@@ -2399,107 +2382,112 @@
 "      added by [4:%(author)s] %(date)s."
 msgstr ""
 
-#: trac/templates/list_of_attachments.html:28
-#: trac/templates/list_of_attachments.html:44
-#: trac/ticket/templates/ticket.html:378
+#: trac/templates/list_of_attachments.html:37
+#: trac/templates/list_of_attachments.html:53
+#: trac/ticket/templates/ticket.html:381
 msgid "Attachments"
 msgstr ""
 
-#: trac/templates/list_of_attachments.html:38
-#: trac/templates/list_of_attachments.html:54
+#: trac/templates/list_of_attachments.html:47
+#: trac/templates/list_of_attachments.html:63
 msgid "Download all attachments as:"
 msgstr ""
 
-#: trac/templates/list_of_attachments.html:39
-#: trac/templates/list_of_attachments.html:55
+#: trac/templates/list_of_attachments.html:48
+#: trac/templates/list_of_attachments.html:64
 msgid ".zip"
 msgstr ""
 
-#: trac/templates/macros.html:37 trac/templates/macros.html:38
+#: trac/templates/macros.html:47 trac/templates/macros.html:48
 msgid "Previous"
 msgstr ""
 
-#: trac/templates/macros.html:47 trac/templates/macros.html:48
+#: trac/templates/macros.html:57 trac/templates/macros.html:58
 msgid "Next"
 msgstr ""
 
-#: trac/templates/preview_file.html:15
+#: trac/templates/preview_file.html:24
 msgid "(The file is empty)"
 msgstr ""
 
-#: trac/templates/preview_file.html:19
+#: trac/templates/preview_file.html:28
 #, python-format
 msgid ""
 "[1:HTML preview not available], since the file size exceeds %(size)s "
 "bytes."
 msgstr ""
 
-#: trac/templates/preview_file.html:22
+#: trac/templates/preview_file.html:31
 msgid "[1:HTML preview not available], since no preview renderer could handle it."
 msgstr ""
 
-#: trac/templates/preview_file.html:26
+#: trac/templates/preview_file.html:35
 msgid "Try [1:downloading] the file instead."
 msgstr ""
 
-#: trac/templates/progress_bar.html:26
+#: trac/templates/progress_bar.html:35
 #, python-format
 msgid "%(count)s/%(total)s %(title)s"
 msgstr ""
 
-#: trac/templates/progress_bar.html:37
+#: trac/templates/progress_bar.html:46
 #, python-format
 msgid "Total number of %(unit)s: %(count)s"
 msgstr ""
 
-#: trac/templates/progress_bar.html:41
+#: trac/templates/progress_bar.html:50
 #, python-format
 msgid "%(title)s: %(count)s"
 msgstr ""
 
-#: trac/templates/progress_bar_grouped.html:17
+#: trac/templates/progress_bar_grouped.html:26
+#: trac/ticket/default_workflow.py:233
 msgid "(none)"
 msgstr ""
 
-#: trac/templates/theme.html:27
+#: trac/templates/theme.html:37
 msgid "Search:"
 msgstr ""
 
-#: trac/templates/theme.html:41
+#: trac/templates/theme.html:51
 msgid "Context Navigation"
 msgstr ""
 
-#: trac/templates/theme.html:50
+#: trac/templates/theme.html:60
 msgid "Hide this warning"
 msgstr ""
 
-#: trac/templates/theme.html:50 trac/templates/theme.html:58
+#: trac/templates/theme.html:60 trac/templates/theme.html:68
 msgid "close"
 msgstr ""
 
-#: trac/templates/theme.html:52
+#: trac/templates/theme.html:62
 msgid "Warning:"
 msgstr ""
 
-#: trac/templates/theme.html:58
+#: trac/templates/theme.html:68
 msgid "Hide this notice"
 msgstr ""
 
-#: trac/templates/theme.html:72
+#: trac/templates/theme.html:82
 #, python-format
 msgid ""
 "Powered by [1:[2:Trac %(version)s]][3:]\n"
 "        By [4:Edgewall Software]."
 msgstr ""
 
-#: trac/ticket/admin.py:37
+#: trac/ticket/admin.py:39
 msgid "(Undefined)"
 msgstr ""
 
-#: trac/ticket/admin.py:48
+#: trac/ticket/admin.py:49
 msgid "Ticket System"
 msgstr ""
 
+#: trac/ticket/admin.py:77 trac/ticket/templates/admin_components.html:20
+msgid "Components"
+msgstr ""
+
 #: trac/ticket/admin.py:93
 #, python-format
 msgid "The component \"%(name)s\" already exists."
@@ -2510,7 +2498,7 @@
 msgid "The component \"%(name)s\" has been added."
 msgstr ""
 
-#: trac/ticket/admin.py:122 trac/ticket/model.py:859 trac/ticket/model.py:878
+#: trac/ticket/admin.py:122 trac/ticket/model.py:871 trac/ticket/model.py:890
 msgid "Invalid component name."
 msgstr ""
 
@@ -2527,305 +2515,359 @@
 msgid "The selected components have been removed."
 msgstr ""
 
-#: trac/ticket/admin.py:235 trac/ticket/api.py:305
+#: trac/ticket/admin.py:210 trac/ticket/api.py:302 trac/ticket/web_ui.py:1468
+#: trac/ticket/templates/admin_components.html:88
+msgid "Owner"
+msgstr ""
+
+#: trac/ticket/admin.py:235 trac/ticket/api.py:314
 msgid "Milestone"
 msgstr ""
 
-#: trac/ticket/admin.py:266 trac/ticket/roadmap.py:757
+#: trac/ticket/admin.py:235 trac/ticket/roadmap.py:1031
+#: trac/ticket/templates/admin_milestones.html:24
+msgid "Milestones"
+msgstr ""
+
+#: trac/ticket/admin.py:264 trac/ticket/roadmap.py:786
 msgid "Completion date may not be in the future"
 msgstr ""
 
-#: trac/ticket/admin.py:268
+#: trac/ticket/admin.py:266
 msgid "Invalid Completion Date"
 msgstr ""
 
-#: trac/ticket/admin.py:273
+#: trac/ticket/admin.py:271
 #, python-format
 msgid "The milestone \"%(name)s\" already exists."
 msgstr ""
 
-#: trac/ticket/admin.py:300
+#: trac/ticket/admin.py:298
 #, python-format
 msgid "The milestone \"%(name)s\" has been added."
 msgstr ""
 
-#: trac/ticket/admin.py:305 trac/ticket/model.py:1038 trac/ticket/model.py:1060
+#: trac/ticket/admin.py:303 trac/ticket/model.py:1047 trac/ticket/model.py:1069
 msgid "Invalid milestone name."
 msgstr ""
 
-#: trac/ticket/admin.py:306
+#: trac/ticket/admin.py:304
 #, python-format
 msgid "Milestone %(name)s already exists."
 msgstr ""
 
-#: trac/ticket/admin.py:314
+#: trac/ticket/admin.py:312
 msgid "No milestone selected"
 msgstr ""
 
-#: trac/ticket/admin.py:321
+#: trac/ticket/admin.py:319
 msgid "The selected milestones have been removed."
 msgstr ""
 
-#: trac/ticket/admin.py:452
+#: trac/ticket/admin.py:404 trac/ticket/templates/admin_milestones.html:124
+msgid "Due"
+msgstr ""
+
+#: trac/ticket/admin.py:404 trac/ticket/templates/admin_milestones.html:124
+msgid "Completed"
+msgstr ""
+
+#: trac/ticket/admin.py:440 trac/ticket/templates/admin_versions.html:20
+msgid "Versions"
+msgstr ""
+
+#: trac/ticket/admin.py:461
 #, python-format
 msgid "The version \"%(name)s\" already exists."
 msgstr ""
 
-#: trac/ticket/admin.py:479
+#: trac/ticket/admin.py:488
 #, python-format
 msgid "The version \"%(name)s\" has been added."
 msgstr ""
 
-#: trac/ticket/admin.py:484 trac/ticket/model.py:1165 trac/ticket/model.py:1183
+#: trac/ticket/admin.py:493 trac/ticket/model.py:1206 trac/ticket/model.py:1224
 msgid "Invalid version name."
 msgstr ""
 
-#: trac/ticket/admin.py:485
+#: trac/ticket/admin.py:494
 #, python-format
 msgid "Version %(name)s already exists."
 msgstr ""
 
-#: trac/ticket/admin.py:492
+#: trac/ticket/admin.py:501
 msgid "No version selected"
 msgstr ""
 
-#: trac/ticket/admin.py:499
+#: trac/ticket/admin.py:508
 msgid "The selected versions have been removed."
 msgstr ""
 
-#: trac/ticket/admin.py:559
+#: trac/ticket/admin.py:574
 msgid "Time"
 msgstr ""
 
-#: trac/ticket/admin.py:605 trac/ticket/admin.py:633
+#: trac/ticket/admin.py:624 trac/ticket/admin.py:652
 #, python-format
 msgid "%(type)s value \"%(name)s\" already exists"
 msgstr ""
 
-#: trac/ticket/admin.py:625
+#: trac/ticket/admin.py:644
 #, python-format
 msgid "The %(field)s value \"%(name)s\" has been added."
 msgstr ""
 
-#: trac/ticket/admin.py:631
+#: trac/ticket/admin.py:650
 #, python-format
 msgid "Invalid %(type)s value."
 msgstr ""
 
-#: trac/ticket/admin.py:640
+#: trac/ticket/admin.py:659
 #, python-format
 msgid "No %s selected"
 msgstr ""
 
-#: trac/ticket/admin.py:646
+#: trac/ticket/admin.py:665
 #, python-format
 msgid "The selected %(field)s values have been removed."
 msgstr ""
 
-#: trac/ticket/admin.py:668
+#: trac/ticket/admin.py:687
 msgid ""
 "Error writing to trac.ini, make sure it is writable by the web server. "
 "The default value has not been saved."
 msgstr ""
 
-#: trac/ticket/admin.py:680
+#: trac/ticket/admin.py:699
 msgid "Order numbers must be unique"
 msgstr ""
 
-#: trac/ticket/admin.py:741
+#: trac/ticket/admin.py:760
 msgid "Possible Values"
 msgstr ""
 
-#: trac/ticket/admin.py:758
+#: trac/ticket/admin.py:777
 #, python-format
 msgid "Invalid up/down value: %(value)s"
 msgstr ""
 
-#: trac/ticket/admin.py:777 trac/ticket/api.py:304
+#: trac/ticket/admin.py:796 trac/ticket/api.py:313
 msgid "Priority"
 msgstr ""
 
-#: trac/ticket/admin.py:777
+#: trac/ticket/admin.py:796
 msgid "Priorities"
 msgstr ""
 
-#: trac/ticket/admin.py:783 trac/ticket/api.py:309
+#: trac/ticket/admin.py:802 trac/ticket/api.py:318
 msgid "Resolution"
 msgstr ""
 
-#: trac/ticket/admin.py:783
+#: trac/ticket/admin.py:802
 msgid "Resolutions"
 msgstr ""
 
-#: trac/ticket/admin.py:789 trac/ticket/api.py:308
+#: trac/ticket/admin.py:808 trac/ticket/api.py:317
 msgid "Severity"
 msgstr ""
 
-#: trac/ticket/admin.py:789
+#: trac/ticket/admin.py:808
 msgid "Severities"
 msgstr ""
 
-#: trac/ticket/admin.py:795
+#: trac/ticket/admin.py:814
 msgid "Ticket Type"
 msgstr ""
 
-#: trac/ticket/admin.py:795
+#: trac/ticket/admin.py:814
 msgid "Ticket Types"
 msgstr ""
 
-#: trac/ticket/admin.py:823
+#: trac/ticket/admin.py:842
 msgid "<number> must be a number"
 msgstr ""
 
-#: trac/ticket/admin.py:826
+#: trac/ticket/admin.py:845
 #, python-format
 msgid "Ticket #%(num)s and all associated data removed."
 msgstr ""
 
-#: trac/ticket/api.py:257
+#: trac/ticket/api.py:266
 msgid "Attachment"
 msgstr ""
 
-#: trac/ticket/api.py:287
+#: trac/ticket/api.py:296
 msgid "Summary"
 msgstr ""
 
-#: trac/ticket/api.py:289 trac/ticket/templates/ticket.html:351
+#: trac/ticket/api.py:298 trac/ticket/templates/ticket.html:354
 msgid "Reporter"
 msgstr ""
 
-#: trac/ticket/api.py:302 trac/versioncontrol/admin.py:113
-#: trac/versioncontrol/templates/admin_repositories.html:125
+#: trac/ticket/api.py:311 trac/versioncontrol/admin.py:113
+#: trac/versioncontrol/templates/admin_repositories.html:138
 msgid "Type"
 msgstr ""
 
-#: trac/ticket/api.py:303
+#: trac/ticket/api.py:312
 msgid "Status"
 msgstr ""
 
-#: trac/ticket/api.py:328
+#: trac/ticket/api.py:337
 msgid "Keywords"
 msgstr ""
 
-#: trac/ticket/api.py:330
+#: trac/ticket/api.py:339
 msgid "Cc"
 msgstr ""
 
-#: trac/ticket/api.py:334
+#: trac/ticket/api.py:343
 msgid "Created"
 msgstr ""
 
-#: trac/ticket/api.py:480
+#: trac/ticket/api.py:489
 #, python-format
 msgid "Tickets %(ranges)s"
 msgstr ""
 
-#: trac/ticket/api.py:504
+#: trac/ticket/api.py:516
+msgid "ticket comment does not exist"
+msgstr ""
+
+#: trac/ticket/api.py:523
+#, python-format
+msgid "Description for Ticket #%(id)s"
+msgstr ""
+
+#: trac/ticket/api.py:526
 #, python-format
 msgid "Comment %(cnum)s for Ticket #%(id)s"
 msgstr ""
 
-#: trac/ticket/api.py:529
+#: trac/ticket/api.py:531
+#, python-format
+msgid "Comment %(cnum)s"
+msgstr ""
+
+#: trac/ticket/api.py:535
+msgid "no permission to view ticket"
+msgstr ""
+
+#: trac/ticket/api.py:538
+msgid "ticket does not exist"
+msgstr ""
+
+#: trac/ticket/api.py:558
 #, python-format
 msgid "Ticket #%(shortname)s"
 msgstr ""
 
-#: trac/ticket/batch.py:95
+#: trac/ticket/batch.py:96
 msgid "add"
 msgstr ""
 
-#: trac/ticket/batch.py:96
+#: trac/ticket/batch.py:97
 msgid "remove"
 msgstr ""
 
-#: trac/ticket/batch.py:97
+#: trac/ticket/batch.py:98
 msgid "add / remove"
 msgstr ""
 
-#: trac/ticket/batch.py:98
+#: trac/ticket/batch.py:99
 msgid "set to"
 msgstr ""
 
-#: trac/ticket/batch.py:180
+#: trac/ticket/batch.py:181 trac/ticket/roadmap.py:731
+#: trac/ticket/roadmap.py:820
 #, python-format
 msgid ""
 "The changes have been saved, but an error occurred while sending "
 "notifications: %(message)s"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:241
+#: trac/ticket/default_workflow.py:238
+msgid "from invalid state"
+msgstr ""
+
+#: trac/ticket/default_workflow.py:239
 msgid "Current state no longer exists"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:243
+#: trac/ticket/default_workflow.py:241
 msgid "The ticket will be disowned"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:261 trac/ticket/default_workflow.py:280
+#: trac/ticket/default_workflow.py:259 trac/ticket/default_workflow.py:269
+#: trac/ticket/default_workflow.py:278
 #, python-format
 msgid "to %(owner)s"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:263
+#: trac/ticket/default_workflow.py:261
 #, python-format
-msgid "The owner will be changed from %(current_owner)s"
+msgid "The owner will be changed from %(current_owner)s to the specified user"
 msgstr ""
 
 #: trac/ticket/default_workflow.py:271
 #, python-format
-msgid "to %(owner)s "
-msgstr ""
-
-#: trac/ticket/default_workflow.py:273
-#, python-format
 msgid "The owner will be changed from %(current_owner)s to %(selected_owner)s"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:283
+#: trac/ticket/default_workflow.py:281
 #, python-format
 msgid "The owner will be changed from %(current_owner)s to the selected user"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:288
+#: trac/ticket/default_workflow.py:289
 #, python-format
 msgid "The owner will be changed from %(current_owner)s to %(authname)s"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:298
+#: trac/ticket/default_workflow.py:297
 msgid ""
 "Your workflow attempts to set a resolution but none is defined "
 "(configuration issue, please contact your Trac admin)."
 msgstr ""
 
-#: trac/ticket/default_workflow.py:306 trac/ticket/default_workflow.py:316
+#: trac/ticket/default_workflow.py:305 trac/ticket/default_workflow.py:315
 #, python-format
 msgid "as %(resolution)s"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:308
+#: trac/ticket/default_workflow.py:307
 #, python-format
 msgid "The resolution will be set to %(name)s"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:319
+#: trac/ticket/default_workflow.py:318
 msgid "The resolution will be set"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:321
+#: trac/ticket/default_workflow.py:320
 msgid "The resolution will be deleted"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:324
+#: trac/ticket/default_workflow.py:323
 #, python-format
-msgid "as %(status)s "
+msgid "as %(status)s"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:328
+#: trac/ticket/default_workflow.py:326
+#, python-format
+msgid "The owner will remain %(current_owner)s"
+msgstr ""
+
+#: trac/ticket/default_workflow.py:329
+msgid "The ticket will remain with no owner"
+msgstr ""
+
+#: trac/ticket/default_workflow.py:332
 #, python-format
 msgid "Next status will be '%(name)s'"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:418
+#: trac/ticket/default_workflow.py:424
 msgid ""
 "Render a workflow graph.\n"
 "\n"
@@ -2867,7 +2909,7 @@
 "}}}"
 msgstr ""
 
-#: trac/ticket/default_workflow.py:493
+#: trac/ticket/default_workflow.py:499
 msgid "Enable JavaScript to display the workflow graph."
 msgstr ""
 
@@ -2884,150 +2926,145 @@
 msgid "Multi-values fields not supported yet"
 msgstr ""
 
-#: trac/ticket/model.py:685
+#: trac/ticket/model.py:697
 #, python-format
 msgid "%(type)s %(name)s does not exist."
 msgstr ""
 
-#: trac/ticket/model.py:727 trac/ticket/model.py:752
+#: trac/ticket/model.py:739 trac/ticket/model.py:764
 #, python-format
 msgid "Invalid %(type)s name."
 msgstr ""
 
-#: trac/ticket/model.py:831
+#: trac/ticket/model.py:843
 #, python-format
 msgid "Component %(name)s does not exist."
 msgstr ""
 
-#: trac/ticket/model.py:976
+#: trac/ticket/model.py:988 trac/ticket/model.py:1114
 #, python-format
 msgid "Milestone %(name)s does not exist."
 msgstr ""
 
-#: trac/ticket/model.py:977
+#: trac/ticket/model.py:989 trac/ticket/model.py:1115
 msgid "Invalid milestone name"
 msgstr ""
 
-#: trac/ticket/model.py:1116
+#: trac/ticket/model.py:1157
 msgid "Open (by due date)"
 msgstr ""
 
-#: trac/ticket/model.py:1117
+#: trac/ticket/model.py:1158
 msgid "Open (no due date)"
 msgstr ""
 
-#: trac/ticket/model.py:1120
+#: trac/ticket/model.py:1161
 msgid "Closed"
 msgstr ""
 
-#: trac/ticket/model.py:1137
+#: trac/ticket/model.py:1178
 #, python-format
 msgid "Version %(name)s does not exist."
 msgstr ""
 
-#: trac/ticket/query.py:59
+#: trac/ticket/query.py:60
 msgid "Invalid query constraint value"
 msgstr ""
 
-#: trac/ticket/query.py:93
+#: trac/ticket/query.py:94
 #, python-format
 msgid "Query page %(page)s is invalid."
 msgstr ""
 
-#: trac/ticket/query.py:108
+#: trac/ticket/query.py:109
 #, python-format
 msgid "Query max %(max)s is invalid."
 msgstr ""
 
-#: trac/ticket/query.py:167
+#: trac/ticket/query.py:168
 msgid "Query filter requires field and constraints separated by a \"=\""
 msgstr ""
 
-#: trac/ticket/query.py:180
+#: trac/ticket/query.py:181
 msgid "Query filter requires field name"
 msgstr ""
 
-#: trac/ticket/query.py:313
+#: trac/ticket/query.py:314
 #, python-format
 msgid "Page %(page)s is beyond the number of pages in the query"
 msgstr ""
 
-#: trac/ticket/query.py:573
+#: trac/ticket/query.py:581
 #, python-format
 msgid "Invalid ticket id list: %(value)s"
 msgstr ""
 
-#: trac/ticket/query.py:672 trac/ticket/query.py:680
+#: trac/ticket/query.py:680 trac/ticket/query.py:688
 msgid "contains"
 msgstr ""
 
-#: trac/ticket/query.py:673 trac/ticket/query.py:681
+#: trac/ticket/query.py:681 trac/ticket/query.py:689
 msgid "doesn't contain"
 msgstr ""
 
-#: trac/ticket/query.py:674
+#: trac/ticket/query.py:682
 msgid "begins with"
 msgstr ""
 
-#: trac/ticket/query.py:675
+#: trac/ticket/query.py:683
 msgid "ends with"
 msgstr ""
 
-#: trac/ticket/query.py:676 trac/ticket/query.py:684 trac/ticket/query.py:688
+#: trac/ticket/query.py:684 trac/ticket/query.py:692 trac/ticket/query.py:696
 msgid "is"
 msgstr ""
 
-#: trac/ticket/query.py:677 trac/ticket/query.py:685 trac/ticket/query.py:689
+#: trac/ticket/query.py:685 trac/ticket/query.py:693 trac/ticket/query.py:697
 msgid "is not"
 msgstr ""
 
-#: trac/ticket/query.py:721 trac/ticket/query.py:727
+#: trac/ticket/query.py:734 trac/ticket/query.py:741
 msgid "Ticket"
 msgstr ""
 
-#: trac/ticket/query.py:798 trac/ticket/report.py:468
-#, python-format
-msgid "Page %(num)d"
-msgstr ""
-
-#: trac/ticket/query.py:847 trac/ticket/report.py:328 trac/ticket/report.py:627
+#: trac/ticket/query.py:861 trac/ticket/report.py:324 trac/ticket/report.py:617
 #: trac/ticket/web_ui.py:140 trac/timeline/web_ui.py:235
-#: trac/versioncontrol/web_ui/log.py:319
+#: trac/versioncontrol/web_ui/log.py:327
 msgid "RSS Feed"
 msgstr ""
 
-#: trac/ticket/query.py:849 trac/ticket/report.py:330 trac/ticket/report.py:629
+#: trac/ticket/query.py:863 trac/ticket/report.py:326 trac/ticket/report.py:619
 #: trac/ticket/web_ui.py:136
 msgid "Comma-delimited Text"
 msgstr ""
 
-#: trac/ticket/query.py:851 trac/ticket/report.py:332 trac/ticket/report.py:631
+#: trac/ticket/query.py:865 trac/ticket/report.py:328 trac/ticket/report.py:621
 #: trac/ticket/web_ui.py:138
 msgid "Tab-delimited Text"
 msgstr ""
 
-#: trac/ticket/query.py:873 trac/ticket/report.py:131
+#: trac/ticket/query.py:888 trac/ticket/report.py:132
 msgid "View Tickets"
 msgstr ""
 
-#: trac/ticket/query.py:1086 trac/ticket/query.py:1097
-#: trac/ticket/report.py:197 trac/ticket/templates/report_list.html:57
+#: trac/ticket/query.py:1104 trac/ticket/query.py:1115
+#: trac/ticket/report.py:202 trac/ticket/templates/report_list.html:67
 msgid "Custom Query"
 msgstr ""
 
-#: trac/ticket/query.py:1096 trac/ticket/report.py:187
-#: trac/ticket/report.py:188 trac/ticket/report.py:190
-#: trac/ticket/templates/report_list.html:10
-#: trac/ticket/templates/report_list.html:28
+#: trac/ticket/query.py:1114 trac/ticket/report.py:194
+#: trac/ticket/report.py:195 trac/ticket/report.py:197
+#: trac/ticket/templates/report_list.html:20
+#: trac/ticket/templates/report_list.html:38
 msgid "Available Reports"
 msgstr ""
 
-#: trac/ticket/query.py:1195
+#: trac/ticket/query.py:1213
 #, python-format
 msgid "[Error: %(error)s]"
 msgstr ""
 
-#: trac/ticket/query.py:1201
+#: trac/ticket/query.py:1219
 msgid ""
 "Wiki macro listing tickets that match certain criteria.\n"
 "\n"
@@ -3042,7 +3079,7 @@
 "\n"
 "\n"
 "Groups of field constraints to be OR-ed together can be separated by a\n"
-"litteral `or` argument.\n"
+"literal `or` argument.\n"
 "\n"
 "In addition to filters, several other named parameters can be used\n"
 "to control how the results are presented. All of them are optional.\n"
@@ -3080,100 +3117,99 @@
 "The `rows` parameter can be used to specify which field(s) should\n"
 "be viewed as a row, e.g. `rows=description|summary`\n"
 "\n"
+"The `col` parameter can be used to specify which fields should\n"
+"be viewed as columns. For '''table''' format only.\n"
+"\n"
 "For compatibility with Trac 0.10, if there's a last positional parameter\n"
 "given to the macro, it will be used to specify the `format`.\n"
 "Also, using \"&\" as a field separator still works (except for `order`)\n"
 "but is deprecated."
 msgstr ""
 
-#: trac/ticket/query.py:1378
+#: trac/ticket/query.py:1333
+#, python-format
+msgid "%(num)d ticket for which %(query)s"
+msgid_plural "%(num)d tickets for which %(query)s"
+msgstr[0] ""
+msgstr[1] ""
+
+#: trac/ticket/query.py:1408
 #, python-format
 msgid "Ticket completion status for each %(group)s"
 msgstr ""
 
-#: trac/ticket/query.py:1393
+#: trac/ticket/query.py:1423
 msgid "No results"
 msgstr ""
 
-#: trac/ticket/query.py:1411
+#: trac/ticket/query.py:1441
 #, python-format
 msgid "%(groupvalue)s %(groupname)s tickets matching %(query)s"
 msgstr ""
 
-#: trac/ticket/query.py:1435
+#: trac/ticket/query.py:1465
 #, python-format
 msgid "%(groupvalue)s %(groupname)s tickets:"
 msgstr ""
 
-#: trac/ticket/report.py:223
+#: trac/ticket/report.py:228
 msgid "The report has been created."
 msgstr ""
 
-#: trac/ticket/report.py:233
+#: trac/ticket/report.py:238
 #, python-format
 msgid "The report {%(id)d} has been deleted."
 msgstr ""
 
-#: trac/ticket/report.py:257
+#: trac/ticket/report.py:260
 #, python-format
 msgid "Delete Report {%(num)s} %(title)s"
 msgstr ""
 
-#: trac/ticket/report.py:262 trac/ticket/report.py:273
-#: trac/ticket/report.py:352
-#, python-format
-msgid "Report {%(num)s} does not exist."
-msgstr ""
-
-#: trac/ticket/report.py:263 trac/ticket/report.py:274
-#: trac/ticket/report.py:353
-msgid "Invalid Report Number"
-msgstr ""
-
-#: trac/ticket/report.py:286
+#: trac/ticket/report.py:280
 msgid "Create New Report"
 msgstr ""
 
-#: trac/ticket/report.py:290
+#: trac/ticket/report.py:284
 #, python-format
 msgid "Edit Report {%(num)d} %(title)s"
 msgstr ""
 
-#: trac/ticket/report.py:357
+#: trac/ticket/report.py:347
 #, python-format
 msgid "Report failed: %(error)s"
 msgstr ""
 
-#: trac/ticket/report.py:372
+#: trac/ticket/report.py:362
 #, python-format
 msgid "When specified, the report number should be \"%(num)s\"."
 msgstr ""
 
-#: trac/ticket/report.py:444
+#: trac/ticket/report.py:434
 #, python-format
 msgid "Report execution failed: %(error)s %(sql)s"
 msgstr ""
 
-#: trac/ticket/report.py:635
+#: trac/ticket/report.py:625
 msgid "SQL Query"
 msgstr ""
 
-#: trac/ticket/report.py:659
+#: trac/ticket/report.py:649
 #, python-format
 msgid "The following arguments are missing: %(args)s"
 msgstr ""
 
-#: trac/ticket/report.py:676
+#: trac/ticket/report.py:666
 #, python-format
 msgid "Report {%(num)s} has no SQL query."
 msgstr ""
 
-#: trac/ticket/report.py:713
+#: trac/ticket/report.py:709
 #, python-format
 msgid "Query parameter \"sort=%(sort_col)s\"  is invalid"
 msgstr ""
 
-#: trac/ticket/report.py:756
+#: trac/ticket/report.py:755
 #, python-format
 msgid ""
 "Hint: if the report failed due to automatic modification of the ORDER BY "
@@ -3182,95 +3218,161 @@
 "over report rewriting."
 msgstr ""
 
-#: trac/ticket/roadmap.py:243
+#: trac/ticket/report.py:779
+#, python-format
+msgid "Report {%(num)s} does not exist."
+msgstr ""
+
+#: trac/ticket/report.py:780
+msgid "Invalid Report Number"
+msgstr ""
+
+#: trac/ticket/report.py:931
+msgid "report does not exist"
+msgstr ""
+
+#: trac/ticket/report.py:938
+msgid "no permission to view report"
+msgstr ""
+
+#: trac/ticket/roadmap.py:244
 msgid "ticket status"
 msgstr ""
 
-#: trac/ticket/roadmap.py:243
+#: trac/ticket/roadmap.py:244
 msgid "tickets"
 msgstr ""
 
-#: trac/ticket/roadmap.py:253
+#: trac/ticket/roadmap.py:254
 #, python-format
 msgid ""
 "'%(group1)s' and '%(group2)s' milestone groups both are declared to be "
 "\"catch-all\" groups. Please check your configuration."
 msgstr ""
 
-#: trac/ticket/roadmap.py:269
+#: trac/ticket/roadmap.py:270
 #, python-format
 msgid ""
 "'%(groupname)s' milestone group reused status '%(status)s' already taken "
 "by other groups. Please check your configuration."
 msgstr ""
 
-#: trac/ticket/roadmap.py:403 trac/ticket/roadmap.py:527
-#: trac/ticket/roadmap.py:661 trac/ticket/templates/roadmap.html:10
-#: trac/ticket/templates/roadmap.html:32
+#: trac/ticket/roadmap.py:411 trac/ticket/roadmap.py:535
+#: trac/ticket/roadmap.py:669 trac/ticket/templates/roadmap.html:20
+#: trac/ticket/templates/roadmap.html:42
 msgid "Roadmap"
 msgstr ""
 
-#: trac/ticket/roadmap.py:452
+#: trac/ticket/roadmap.py:460
 msgid "iCalendar"
 msgstr ""
 
-#: trac/ticket/roadmap.py:539 trac/ticket/roadmap.py:937
-#: trac/ticket/templates/milestone_view.html:10
-#: trac/ticket/templates/milestone_view.html:23
+#: trac/ticket/roadmap.py:547 trac/ticket/roadmap.py:1005
+#: trac/ticket/templates/milestone_view.html:20
+#: trac/ticket/templates/milestone_view.html:33
 #, python-format
 msgid "Milestone %(name)s"
 msgstr ""
 
-#: trac/ticket/roadmap.py:557
+#: trac/ticket/roadmap.py:565
 #, python-format
 msgid "Ticket #%(num)s: %(summary)s"
 msgstr ""
 
-#: trac/ticket/roadmap.py:617
+#: trac/ticket/roadmap.py:625
 msgid "Milestones reached"
 msgstr ""
 
-#: trac/ticket/roadmap.py:643
+#: trac/ticket/roadmap.py:651
 #, python-format
 msgid "Milestone %(name)s completed"
 msgstr ""
 
-#: trac/ticket/roadmap.py:702
+#: trac/ticket/roadmap.py:712
 #, python-format
 msgid "The milestone \"%(name)s\" has been deleted."
 msgstr ""
 
-#: trac/ticket/roadmap.py:745
+#: trac/ticket/roadmap.py:715
+#, python-format
+msgid ""
+"The tickets associated with milestone \"%(name)s\" have been retargeted "
+"to milestone \"%(retarget)s\"."
+msgstr ""
+
+#: trac/ticket/roadmap.py:720
+msgid "Tickets retargeted after milestone deleted"
+msgstr ""
+
+#: trac/ticket/roadmap.py:774
 #, python-format
 msgid "Milestone \"%(name)s\" already exists, please choose another name."
 msgstr ""
 
-#: trac/ticket/roadmap.py:748
+#: trac/ticket/roadmap.py:777
 msgid "You must provide a name for the milestone."
 msgstr ""
 
-#: trac/ticket/roadmap.py:877
+#: trac/ticket/roadmap.py:802
+#, python-format
+msgid ""
+"The open tickets associated with milestone \"%(name)s\" have been "
+"retargeted to milestone \"%(retarget)s\"."
+msgstr ""
+
+#: trac/ticket/roadmap.py:808
+msgid "Open tickets retargeted after milestone closed"
+msgstr ""
+
+#: trac/ticket/roadmap.py:865
+#, python-format
+msgid "Milestone %(name)s does not exist. You can create it here."
+msgstr ""
+
+#: trac/ticket/roadmap.py:925
 #, python-format
 msgid "Milestone \"%(name)s\""
 msgstr ""
 
-#: trac/ticket/roadmap.py:891
+#: trac/ticket/roadmap.py:939
 msgid "Previous Milestone"
 msgstr ""
 
-#: trac/ticket/roadmap.py:891
+#: trac/ticket/roadmap.py:939
 msgid "Next Milestone"
 msgstr ""
 
-#: trac/ticket/roadmap.py:892
+#: trac/ticket/roadmap.py:940
 msgid "Back to Roadmap"
 msgstr ""
 
-#: trac/ticket/web_ui.py:65
+#: trac/ticket/roadmap.py:974 trac/ticket/templates/milestone_view.html:37
+#: trac/ticket/templates/roadmap.html:53
+#, python-format
+msgid "Completed %(duration)s ago (%(date)s)"
+msgstr ""
+
+#: trac/ticket/roadmap.py:979
+#, python-format
+msgid "%(duration)s late (%(date)s)"
+msgstr ""
+
+#: trac/ticket/roadmap.py:984 trac/ticket/templates/milestone_view.html:47
+#: trac/ticket/templates/roadmap.html:63
+#, python-format
+msgid "Due in %(duration)s (%(date)s)"
+msgstr ""
+
+#: trac/ticket/roadmap.py:987 trac/ticket/templates/milestone_view.html:51
+#: trac/ticket/templates/roadmap.html:67
+msgid "No date set"
+msgstr ""
+
+#: trac/ticket/web_ui.py:63
 msgid "Invalid Ticket"
 msgstr ""
 
-#: trac/ticket/web_ui.py:162 trac/ticket/templates/ticket.html:14
+#: trac/ticket/web_ui.py:162 trac/ticket/templates/ticket.html:24
 msgid "New Ticket"
 msgstr ""
 
@@ -3278,8 +3380,12 @@
 msgid "id can't be set for a new ticket request."
 msgstr ""
 
+#: trac/ticket/web_ui.py:194 trac/ticket/templates/admin_milestones.html:124
+msgid "Tickets"
+msgstr ""
+
 #: trac/ticket/web_ui.py:228 trac/ticket/web_ui.py:279
-#: trac/versioncontrol/web_ui/changeset.py:1042
+#: trac/versioncontrol/web_ui/changeset.py:1012
 #, python-format
 msgid "%(title)s: %(message)s"
 msgstr ""
@@ -3375,8 +3481,8 @@
 #: trac/ticket/web_ui.py:901 trac/ticket/web_ui.py:958
 #: trac/ticket/web_ui.py:966 trac/ticket/web_ui.py:1037
 #: trac/ticket/web_ui.py:1082 trac/ticket/web_ui.py:1089
-#: trac/wiki/web_ui.py:449 trac/wiki/web_ui.py:455 trac/wiki/web_ui.py:653
-#: trac/wiki/web_ui.py:667
+#: trac/wiki/web_ui.py:467 trac/wiki/web_ui.py:473 trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:685
 #, python-format
 msgid "Version %(num)s"
 msgstr ""
@@ -3395,12 +3501,12 @@
 msgstr ""
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:372 trac/wiki/web_ui.py:486
 msgid "Previous Change"
 msgstr ""
 
 #: trac/ticket/web_ui.py:968 trac/ticket/web_ui.py:1091
-#: trac/versioncontrol/web_ui/changeset.py:371 trac/wiki/web_ui.py:468
+#: trac/versioncontrol/web_ui/changeset.py:372 trac/wiki/web_ui.py:486
 msgid "Next Change"
 msgstr ""
 
@@ -3446,7 +3552,7 @@
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/ticket/web_ui.py:1175 trac/ticket/web_ui.py:1755
+#: trac/ticket/web_ui.py:1175 trac/ticket/web_ui.py:1768
 msgid "; "
 msgstr ""
 
@@ -3476,45 +3582,55 @@
 msgid "Tickets must contain a summary."
 msgstr ""
 
-#: trac/ticket/web_ui.py:1248
+#: trac/ticket/web_ui.py:1244
+#, python-format
+msgid "\"%(value)s\" is not a valid value for the %(name)s field."
+msgstr ""
+
+#: trac/ticket/web_ui.py:1249
 #, python-format
 msgid "field %(name)s must be set"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1254
+#: trac/ticket/web_ui.py:1255
 #, python-format
 msgid "Ticket description is too long (must be less than %(num)s characters)"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1261
+#: trac/ticket/web_ui.py:1262
 #, python-format
 msgid "Ticket comment is too long (must be less than %(num)s characters)"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1274
+#: trac/ticket/web_ui.py:1269
+#, python-format
+msgid "Ticket summary is too long (must be less than %(num)s characters)"
+msgstr ""
+
+#: trac/ticket/web_ui.py:1282
 msgid "Invalid comment threading identifier"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1281
+#: trac/ticket/web_ui.py:1289
 #, python-format
 msgid "The ticket field '%(field)s' is invalid: %(message)s"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1300
+#: trac/ticket/web_ui.py:1308
 #, python-format
 msgid ""
 "The ticket has been created, but an error occurred while sending "
 "notifications: %(message)s"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1305
+#: trac/ticket/web_ui.py:1313
 #, python-format
 msgid ""
 "The ticket %(ticketref)s has been created. You can now attach the desired"
 " files."
 msgstr ""
 
-#: trac/ticket/web_ui.py:1311
+#: trac/ticket/web_ui.py:1319
 #, python-format
 msgid ""
 "The ticket %(ticketref)s has been created, but you don't have permission "
@@ -3522,111 +3638,254 @@
 msgstr ""
 
 #. TRANSLATOR: The 'change' has been saved... (link)
-#: trac/ticket/web_ui.py:1338
+#: trac/ticket/web_ui.py:1346
 msgid "change"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1344
+#: trac/ticket/web_ui.py:1352
 #, python-format
 msgid ""
 "The %(change)s has been saved, but an error occurred while sending "
 "notifications: %(message)s"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1482
+#: trac/ticket/web_ui.py:1495
 msgid "Add to Cc"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1483
+#: trac/ticket/web_ui.py:1496
 msgid "Remove from Cc"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1484
+#: trac/ticket/web_ui.py:1497
 msgid "Add/Remove from Cc"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1485
+#: trac/ticket/web_ui.py:1498
 msgid "<Author field>"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1518 trac/ticket/templates/query.html:114
+#: trac/ticket/web_ui.py:1531 trac/ticket/templates/query.html:124
 msgid "yes"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1518 trac/ticket/templates/query.html:117
+#: trac/ticket/web_ui.py:1531 trac/ticket/templates/query.html:127
 msgid "no"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1724
+#: trac/ticket/web_ui.py:1737
 msgid "set"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1724
+#: trac/ticket/web_ui.py:1737
 msgid "unset"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1727 trac/versioncontrol/templates/changeset.html:189
+#: trac/ticket/web_ui.py:1740 trac/versioncontrol/templates/changeset.html:199
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "modified"
 msgstr ""
 
-#. TRANSLATOR: modified ('diff') (link)
-#: trac/ticket/web_ui.py:1732 trac/ticket/templates/ticket_change.html:155
-#: trac/wiki/web_ui.py:747
-msgid "diff"
-msgstr ""
-
-#: trac/ticket/web_ui.py:1733
+#: trac/ticket/web_ui.py:1746
 #, python-format
 msgid "modified (%(diff)s)"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1751
+#: trac/ticket/web_ui.py:1764
 #, python-format
 msgid "%(items)s added"
 msgid_plural "%(items)s added"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/ticket/web_ui.py:1753
+#: trac/ticket/web_ui.py:1766
 #, python-format
 msgid "%(items)s removed"
 msgid_plural "%(items)s removed"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/ticket/web_ui.py:1762
+#: trac/ticket/web_ui.py:1775
 #, python-format
 msgid "%(value)s deleted"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1764
+#: trac/ticket/web_ui.py:1777
 #, python-format
 msgid "set to %(value)s"
 msgstr ""
 
-#: trac/ticket/web_ui.py:1767
+#: trac/ticket/web_ui.py:1780
 #, python-format
 msgid "changed from %(old)s to %(new)s"
 msgstr ""
 
-#: trac/ticket/templates/batch_modify.html:8
+#: trac/ticket/templates/admin_components.html:24
+msgid "Manage Components"
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:28
+msgid "Owner:"
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:45
+msgid "Modify Component:"
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:52
+#: trac/ticket/templates/admin_milestones.html:81
+#: trac/ticket/templates/admin_versions.html:50
+#: trac/ticket/templates/milestone_edit.html:116
+#: trac/ticket/templates/report_edit.html:43
+#: trac/versioncontrol/templates/admin_repositories.html:86
+msgid "Description: (you may use [1:WikiFormatting] here)"
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:63
+#: trac/ticket/templates/admin_enums.html:37
+#: trac/ticket/templates/admin_milestones.html:92
+#: trac/ticket/templates/admin_versions.html:60
+#: trac/versioncontrol/templates/admin_repositories.html:97
+msgid "Save"
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:72
+msgid "Add Component:"
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:88
+#: trac/ticket/templates/admin_enums.html:61
+#: trac/ticket/templates/admin_milestones.html:124
+#: trac/ticket/templates/admin_versions.html:93
+msgid "Default"
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:109
+#: trac/ticket/templates/admin_enums.html:85
+#: trac/ticket/templates/admin_milestones.html:151
+#: trac/ticket/templates/admin_versions.html:112
+msgid ""
+"You can remove all items from this list to completely hide this\n"
+"              field from the user interface."
+msgstr ""
+
+#: trac/ticket/templates/admin_components.html:115
+#: trac/ticket/templates/admin_enums.html:95
+#: trac/ticket/templates/admin_milestones.html:157
+#: trac/ticket/templates/admin_versions.html:118
+msgid ""
+"As long as you don't add any items to the list, this field\n"
+"            will remain completely hidden from the user interface."
+msgstr ""
+
+#: trac/ticket/templates/admin_enums.html:25
+#, python-format
+msgid "Manage %(label_plural)s"
+msgstr ""
+
+#: trac/ticket/templates/admin_enums.html:32
+#, python-format
+msgid "Modify %(label_singular)s:"
+msgstr ""
+
+#: trac/ticket/templates/admin_enums.html:46
+#, python-format
+msgid "Add %(label_singular)s:"
+msgstr ""
+
+#: trac/ticket/templates/admin_enums.html:61
+msgid "Order"
+msgstr ""
+
+#: trac/ticket/templates/admin_enums.html:89
+msgid ""
+"[1:Note:] The order of priorities determines the\n"
+"              coloring of entries in the ticket queries and reports."
+msgstr ""
+
+#: trac/ticket/templates/admin_milestones.html:36
+msgid "Manage Milestones"
+msgstr ""
+
+#: trac/ticket/templates/admin_milestones.html:43
+msgid "Modify Milestone:"
+msgstr ""
+
+#: trac/ticket/templates/admin_milestones.html:49
+#: trac/ticket/templates/admin_milestones.html:106
+#: trac/ticket/templates/milestone_edit.html:73
+msgid "Due:"
+msgstr ""
+
+#: trac/ticket/templates/admin_milestones.html:50
+#: trac/ticket/templates/admin_milestones.html:53
+#: trac/ticket/templates/admin_milestones.html:63
+#: trac/ticket/templates/admin_milestones.html:67
+#: trac/ticket/templates/admin_milestones.html:108
+#: trac/ticket/templates/admin_versions.html:42
+#: trac/ticket/templates/admin_versions.html:45
+#: trac/ticket/templates/admin_versions.html:76
+#: trac/ticket/templates/admin_versions.html:79
+#: trac/ticket/templates/milestone_edit.html:77
+#: trac/ticket/templates/milestone_edit.html:80
+#: trac/ticket/templates/milestone_edit.html:89
+#: trac/ticket/templates/milestone_edit.html:92
+#, python-format
+msgid "Format: %(datehint)s"
+msgstr ""
+
+#: trac/ticket/templates/admin_milestones.html:59
+#: trac/ticket/templates/milestone_edit.html:85
+msgid "Completed:"
+msgstr ""
+
+#: trac/ticket/templates/admin_milestones.html:101
+msgid "Add Milestone:"
+msgstr ""
+
+#: trac/ticket/templates/admin_milestones.html:110
+#, python-format
+msgid "Format: %(datetimehint)s"
+msgstr ""
+
+#: trac/ticket/templates/admin_versions.html:29
+msgid "Manage Versions"
+msgstr ""
+
+#: trac/ticket/templates/admin_versions.html:34
+msgid "Modify Version:"
+msgstr ""
+
+#: trac/ticket/templates/admin_versions.html:41
+#: trac/ticket/templates/admin_versions.html:74
+msgid "Released:"
+msgstr ""
+
+#: trac/ticket/templates/admin_versions.html:69
+msgid "Add Version:"
+msgstr ""
+
+#: trac/ticket/templates/admin_versions.html:93
+msgid "Released"
+msgstr ""
+
+#: trac/ticket/templates/batch_modify.html:18
 msgid "Batch Modify"
 msgstr ""
 
-#: trac/ticket/templates/batch_modify.html:9
+#: trac/ticket/templates/batch_modify.html:19
 msgid "Batch modification fields"
 msgstr ""
 
-#: trac/ticket/templates/batch_modify.html:21
+#: trac/ticket/templates/batch_modify.html:31
 msgid "Add Field:"
 msgstr ""
 
-#: trac/ticket/templates/batch_modify.html:50
+#: trac/ticket/templates/batch_modify.html:60
 msgid "[1:Note:] See [2:TracBatchModify] for help on using batch modify."
 msgstr ""
 
-#: trac/ticket/templates/batch_modify.html:57
+#: trac/ticket/templates/batch_modify.html:67
 msgid "Change tickets"
 msgstr ""
 
@@ -3645,138 +3904,121 @@
 msgid "Tickets URL: <%(link)s>"
 msgstr ""
 
-#: trac/ticket/templates/milestone_delete.html:10
-#: trac/ticket/templates/milestone_delete.html:22
+#: trac/ticket/templates/milestone_delete.html:20
+#: trac/ticket/templates/milestone_delete.html:27
 #, python-format
 msgid "Delete Milestone %(name)s"
 msgstr ""
 
-#: trac/ticket/templates/milestone_delete.html:27
+#: trac/ticket/templates/milestone_delete.html:32
 msgid "Are you sure you want to delete this milestone?"
 msgstr ""
 
-#: trac/ticket/templates/milestone_delete.html:29
+#: trac/ticket/templates/milestone_delete.html:33
 msgid "Retarget associated tickets to milestone"
 msgstr ""
 
-#: trac/ticket/templates/milestone_delete.html:41
-#: trac/ticket/templates/milestone_view.html:88
+#: trac/ticket/templates/milestone_delete.html:44
+#: trac/ticket/templates/milestone_view.html:98
 msgid "Delete milestone"
 msgstr ""
 
-#: trac/ticket/templates/milestone_delete.html:45
+#: trac/ticket/templates/milestone_delete.html:49
 msgid ""
 "[1:Note:] See\n"
 "      [2:TracRoadmap] for help on using\n"
 "      the roadmap."
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:11
-#: trac/ticket/templates/milestone_edit.html:45
+#: trac/ticket/templates/milestone_edit.html:21
+#: trac/ticket/templates/milestone_edit.html:56
 #, python-format
 msgid "Edit Milestone %(name)s"
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:12
-#: trac/ticket/templates/milestone_edit.html:46
+#: trac/ticket/templates/milestone_edit.html:22
+#: trac/ticket/templates/milestone_edit.html:57
 msgid "New Milestone"
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:53
+#: trac/ticket/templates/milestone_edit.html:64
 msgid "Name of the milestone:"
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:58
+#: trac/ticket/templates/milestone_edit.html:70
 msgid "Schedule"
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:85
+#: trac/ticket/templates/milestone_edit.html:97
 msgid "Retarget associated open tickets to milestone:"
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:106
-#: trac/ticket/templates/ticket.html:388
-#: trac/ticket/templates/ticket_change.html:116
-#: trac/wiki/templates/wiki_edit_form.html:66
-#: trac/wiki/templates/wiki_edit_form.html:71
+#: trac/ticket/templates/milestone_edit.html:124
+#: trac/ticket/templates/ticket.html:391
+#: trac/ticket/templates/ticket_change.html:125
+#: trac/wiki/templates/wiki_edit_form.html:77
+#: trac/wiki/templates/wiki_edit_form.html:82
 msgid "Submit changes"
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:107
+#: trac/ticket/templates/milestone_edit.html:126
 msgid "Add milestone"
 msgstr ""
 
-#: trac/ticket/templates/milestone_edit.html:112
-#: trac/ticket/templates/milestone_view.html:94
-#: trac/ticket/templates/roadmap.html:80
+#: trac/ticket/templates/milestone_edit.html:132
+#: trac/ticket/templates/milestone_view.html:104
+#: trac/ticket/templates/roadmap.html:90
 msgid ""
 "[1:Note:] See\n"
 "        [2:TracRoadmap] for help on using\n"
 "        the roadmap."
 msgstr ""
 
-#: trac/ticket/templates/milestone_view.html:11
+#: trac/ticket/templates/milestone_view.html:21
 msgid "Edit this milestone"
 msgstr ""
 
-#: trac/ticket/templates/milestone_view.html:27
-#: trac/ticket/templates/roadmap.html:43
-#, python-format
-msgid "Completed %(duration)s ago (%(date)s)"
-msgstr ""
-
-#: trac/ticket/templates/milestone_view.html:32
-#: trac/ticket/templates/roadmap.html:48
+#: trac/ticket/templates/milestone_view.html:42
+#: trac/ticket/templates/roadmap.html:58
 #, python-format
 msgid "[1:%(duration)s late] (%(date)s)"
 msgstr ""
 
-#: trac/ticket/templates/milestone_view.html:37
-#: trac/ticket/templates/roadmap.html:53
-#, python-format
-msgid "Due in %(duration)s (%(date)s)"
-msgstr ""
-
-#: trac/ticket/templates/milestone_view.html:41
-#: trac/ticket/templates/roadmap.html:57
-msgid "No date set"
-msgstr ""
-
-#: trac/ticket/templates/milestone_view.html:51
+#: trac/ticket/templates/milestone_view.html:61
 #, python-format
 msgid "%(stat_title)s by"
 msgstr ""
 
-#: trac/ticket/templates/milestone_view.html:82
+#: trac/ticket/templates/milestone_view.html:92
 msgid "Edit milestone"
 msgstr ""
 
-#: trac/ticket/templates/query.html:35
-#: trac/ticket/templates/report_view.html:21
-#: trac/ticket/templates/report_view.html:97
+#: trac/ticket/templates/query.html:45
+#: trac/ticket/templates/report_view.html:31
+#: trac/ticket/templates/report_view.html:107
 #, python-format
 msgid "%(num)s match"
 msgid_plural "%(num)s matches"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/ticket/templates/query.html:44
+#: trac/ticket/templates/query.html:54
 msgid "Filters"
 msgstr ""
 
-#: trac/ticket/templates/query.html:45
+#: trac/ticket/templates/query.html:55
 msgid "Query filters"
 msgstr ""
 
-#: trac/ticket/templates/query.html:51 trac/ticket/templates/query.html:155
+#: trac/ticket/templates/query.html:61 trac/ticket/templates/query.html:165
 msgid "Or"
 msgstr ""
 
-#: trac/ticket/templates/query.html:79
+#: trac/ticket/templates/query.html:89
 msgid "or"
 msgstr ""
 
-#: trac/ticket/templates/query.html:127
+#: trac/ticket/templates/query.html:137
 msgid ""
 "[1:between]\n"
 "                            [2:]\n"
@@ -3784,295 +4026,299 @@
 "                            [4:]"
 msgstr ""
 
-#: trac/ticket/templates/query.html:141
+#: trac/ticket/templates/query.html:151
 msgid "And"
 msgstr ""
 
-#: trac/ticket/templates/query.html:174
+#: trac/ticket/templates/query.html:184
 msgid "Columns"
 msgstr ""
 
-#: trac/ticket/templates/query.html:187
+#: trac/ticket/templates/query.html:197
 msgid "Group results by"
 msgstr ""
 
-#: trac/ticket/templates/query.html:198
+#: trac/ticket/templates/query.html:208
 msgid "descending"
 msgstr ""
 
-#: trac/ticket/templates/query.html:202
+#: trac/ticket/templates/query.html:212
 msgid "Show under each result:"
 msgstr ""
 
-#: trac/ticket/templates/query.html:212
-#: trac/ticket/templates/report_view.html:27
+#: trac/ticket/templates/query.html:222
+#: trac/ticket/templates/report_view.html:37
 msgid "Max items per page"
 msgstr ""
 
-#: trac/ticket/templates/query.html:235
+#: trac/ticket/templates/query.html:245
 #, python-format
 msgid "Edit report {%(id)s} corresponding to this query"
 msgstr ""
 
-#: trac/ticket/templates/query.html:235
+#: trac/ticket/templates/query.html:245
 msgid "Edit query"
 msgstr ""
 
-#: trac/ticket/templates/query.html:244
+#: trac/ticket/templates/query.html:254
 msgid "Save query"
 msgstr ""
 
-#: trac/ticket/templates/query.html:244
+#: trac/ticket/templates/query.html:254
 #, python-format
 msgid "Save updated query in report {%(id)s}"
 msgstr ""
 
-#: trac/ticket/templates/query.html:244
+#: trac/ticket/templates/query.html:254
 msgid "Create new report from current query"
 msgstr ""
 
-#: trac/ticket/templates/query.html:252
+#: trac/ticket/templates/query.html:262
 #, python-format
 msgid "Delete report {%(id)s} corresponding to this query"
 msgstr ""
 
-#: trac/ticket/templates/query.html:252
+#: trac/ticket/templates/query.html:262
 msgid "Delete query"
 msgstr ""
 
-#: trac/ticket/templates/query.html:259
+#: trac/ticket/templates/query.html:269
 msgid ""
 "[1:Note:] See [2:TracQuery]\n"
 "        for help on using queries."
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:25
+#: trac/ticket/templates/query_results.html:34
 #, python-format
 msgid "%(grouplabel)s: %(groupname)s [1:(%(count)s)]"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:37
+#: trac/ticket/templates/query_results.html:46
 msgid "(ascending)"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:37
+#: trac/ticket/templates/query_results.html:46
 msgid "(descending)"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:38
-#: trac/versioncontrol/templates/sortable_th.html:18
+#: trac/ticket/templates/query_results.html:47
+#: trac/versioncontrol/templates/sortable_th.html:28
 #, python-format
 msgid "Sort by %(col)s %(direction)s"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:61
+#: trac/ticket/templates/query_results.html:70
 msgid "No tickets found"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:75
-#: trac/ticket/templates/query_results.html:78
+#: trac/ticket/templates/query_results.html:84
+#: trac/ticket/templates/query_results.html:88
 msgid "View ticket"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:83
-#: trac/ticket/templates/report_view.html:185
+#: trac/ticket/templates/query_results.html:93
+#: trac/ticket/templates/report_view.html:195
 msgid "View milestone"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:95
+#: trac/ticket/templates/query_results.html:106
 msgid "(this ticket)"
 msgstr ""
 
-#: trac/ticket/templates/query_results.html:111
+#: trac/ticket/templates/query_results.html:122
 msgid "(more results for this group on next page)"
 msgstr ""
 
-#: trac/ticket/templates/report_delete.html:17
+#: trac/ticket/templates/report_delete.html:27
 msgid "Are you sure you want to delete this report?"
 msgstr ""
 
-#: trac/ticket/templates/report_delete.html:22
-#: trac/ticket/templates/report_list.html:82
-#: trac/ticket/templates/report_view.html:74
+#: trac/ticket/templates/report_delete.html:31
+#: trac/ticket/templates/report_list.html:92
+#: trac/ticket/templates/report_view.html:84
 msgid "Delete report"
 msgstr ""
 
-#: trac/ticket/templates/report_delete.html:26
-#: trac/ticket/templates/report_edit.html:49
-#: trac/ticket/templates/report_list.html:116
-#: trac/ticket/templates/report_view.html:210
+#: trac/ticket/templates/report_delete.html:36
+#: trac/ticket/templates/report_edit.html:66
+#: trac/ticket/templates/report_list.html:126
+#: trac/ticket/templates/report_view.html:220
 msgid ""
 "[1:Note:]\n"
 "        See [2:TracReports] for help on using and creating reports."
 msgstr ""
 
-#: trac/ticket/templates/report_edit.html:16
+#: trac/ticket/templates/report_edit.html:28
 msgid "New Report"
 msgstr ""
 
-#: trac/ticket/templates/report_edit.html:21
-msgid "Report Title:"
-msgstr ""
-
-#: trac/ticket/templates/report_edit.html:25
-msgid "Description: (you may use [1:WikiFormatting] here)"
-msgstr ""
-
 #: trac/ticket/templates/report_edit.html:34
+msgid "Create Report:"
+msgstr ""
+
+#: trac/ticket/templates/report_edit.html:35
+msgid "Modify Report:"
+msgstr ""
+
+#: trac/ticket/templates/report_edit.html:39
+msgid "Title:"
+msgstr ""
+
+#: trac/ticket/templates/report_edit.html:51
 msgid "Error:"
 msgstr ""
 
-#: trac/ticket/templates/report_edit.html:36
+#: trac/ticket/templates/report_edit.html:53
 msgid ""
-"Query for Report: (can be either SQL or, if starting with [1:query:],\n"
+"Query: (can be either SQL or, if starting with [1:query:],\n"
 "              a [2:TracQuery] expression)"
 msgstr ""
 
-#: trac/ticket/templates/report_edit.html:43
+#: trac/ticket/templates/report_edit.html:61
 msgid "Save report"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:33
+#: trac/ticket/templates/report_list.html:43
 msgid "Show Descriptions"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:45
+#: trac/ticket/templates/report_list.html:55
 msgid "Clear"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:45
+#: trac/ticket/templates/report_list.html:55
 msgid "Forget last query"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:48
+#: trac/ticket/templates/report_list.html:58
 msgid "Return to Last Query"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:51
+#: trac/ticket/templates/report_list.html:61
 msgid ""
 "Continue browsing through the current list of results,\n"
 "              from the last selected report or custom query."
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:60
+#: trac/ticket/templates/report_list.html:70
 msgid "Compose a new ticket query by selecting filters and columns to display."
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:65
+#: trac/ticket/templates/report_list.html:75
 msgid "SQL reports and saved custom queries"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:67
+#: trac/ticket/templates/report_list.html:77
 msgid "Sort by:"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:70
+#: trac/ticket/templates/report_list.html:80
 msgid "Identifier"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:73 trac/wiki/admin.py:197
+#: trac/ticket/templates/report_list.html:83 trac/wiki/admin.py:197
 msgid "Title"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:89
-#: trac/ticket/templates/ticket_change.html:65
-#: trac/wiki/templates/wiki_edit.html:149
+#: trac/ticket/templates/report_list.html:99
+#: trac/ticket/templates/ticket_change.html:74
+#: trac/wiki/templates/wiki_edit.html:162
 msgid "Edit"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:89
-#: trac/ticket/templates/report_view.html:62
+#: trac/ticket/templates/report_list.html:99
+#: trac/ticket/templates/report_view.html:72
 msgid "Edit report"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:93
-#: trac/ticket/templates/report_view.html:136
+#: trac/ticket/templates/report_list.html:103
+#: trac/ticket/templates/report_view.html:146
 msgid "View report"
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:103
+#: trac/ticket/templates/report_list.html:113
 msgid "No reports available."
 msgstr ""
 
-#: trac/ticket/templates/report_list.html:111
+#: trac/ticket/templates/report_list.html:121
 msgid "Create new report"
 msgstr ""
 
-#: trac/ticket/templates/report_view.html:32
+#: trac/ticket/templates/report_view.html:42
 msgid "Arguments"
 msgstr ""
 
-#: trac/ticket/templates/report_view.html:33
+#: trac/ticket/templates/report_view.html:43
 msgid "Report arguments"
 msgstr ""
 
-#: trac/ticket/templates/report_view.html:68
+#: trac/ticket/templates/report_view.html:78
 msgid "Copy report"
 msgstr ""
 
-#: trac/ticket/templates/report_view.html:110
+#: trac/ticket/templates/report_view.html:120
 msgid "(empty)"
 msgstr ""
 
-#: trac/ticket/templates/report_view.html:143
-#: trac/ticket/templates/report_view.html:151
+#: trac/ticket/templates/report_view.html:153
+#: trac/ticket/templates/report_view.html:161
 #, python-format
 msgid "View %(realm)s"
 msgstr ""
 
-#: trac/ticket/templates/roadmap.html:20
+#: trac/ticket/templates/roadmap.html:30
 msgid "Show completed milestones"
 msgstr ""
 
-#: trac/ticket/templates/roadmap.html:25
+#: trac/ticket/templates/roadmap.html:35
 msgid "Hide milestones with no due date"
 msgstr ""
 
-#: trac/ticket/templates/roadmap.html:38
+#: trac/ticket/templates/roadmap.html:48
 msgid "Milestone:"
 msgstr ""
 
-#: trac/ticket/templates/roadmap.html:76
+#: trac/ticket/templates/roadmap.html:86
 msgid "Add new milestone"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:132
+#: trac/ticket/templates/ticket.html:136
 msgid "Go to the ticket editor"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:132
+#: trac/ticket/templates/ticket.html:136
 msgid "Modify"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:134
+#: trac/ticket/templates/ticket.html:138
 msgid "Create New Ticket"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:152
+#: trac/ticket/templates/ticket.html:156
 msgid "Oldest first"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:154
+#: trac/ticket/templates/ticket.html:158
 msgid "Newest first"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:157
+#: trac/ticket/templates/ticket.html:161
 msgid "Threaded"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:162
+#: trac/ticket/templates/ticket.html:166
 msgid "Comments only"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:167
+#: trac/ticket/templates/ticket.html:171
 msgid "Change History"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:186
+#: trac/ticket/templates/ticket.html:191
 msgid "Add Comment"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:188
+#: trac/ticket/templates/ticket.html:193
 msgid ""
 "This ticket has been modified since you started editing. You should "
 "review the\n"
@@ -4082,232 +4328,233 @@
 " wish so."
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:198
+#: trac/ticket/templates/ticket.html:203
 msgid ""
 "You may use\n"
 "                [1:WikiFormatting]\n"
 "                here."
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:209
+#: trac/ticket/templates/ticket.html:214
 msgid "Modify Ticket"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:215
+#: trac/ticket/templates/ticket.html:220
 msgid "Change Properties"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:216
+#: trac/ticket/templates/ticket.html:221
 msgid "Properties"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:220
+#: trac/ticket/templates/ticket.html:226
 msgid "Summary:"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:228
+#: trac/ticket/templates/ticket.html:234
 msgid "Reporter:"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:240
+#: trac/ticket/templates/ticket.html:244
 msgid ""
 "You may use\n"
-"                          [1:WikiFormatting] here."
+"                        [1:WikiFormatting] here."
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:256
-#: trac/ticket/templates/ticket_box.html:75
+#: trac/ticket/templates/ticket.html:259
+#: trac/ticket/templates/ticket_box.html:84
 #, python-format
 msgid "%(field)s:"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:295
+#: trac/ticket/templates/ticket.html:298
 msgid "This checkbox allows you to add or remove yourself from the CC list."
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:301
+#: trac/ticket/templates/ticket.html:304
 msgid "Space or comma delimited email addresses and usernames are accepted."
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:364
-#: trac/wiki/templates/wiki_edit_form.html:46
+#: trac/ticket/templates/ticket.html:367
+#: trac/wiki/templates/wiki_edit_form.html:57
 msgid "E-mail address and user name can be saved in the [1:Preferences]."
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:372
+#: trac/ticket/templates/ticket.html:375
 msgid "I have files to attach to this ticket"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:378
+#: trac/ticket/templates/ticket.html:381
 msgid "Go to the list of attachments"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:379
+#: trac/ticket/templates/ticket.html:382
 msgid "View the ticket description"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:387
-#: trac/ticket/templates/ticket_change.html:114
-#: trac/wiki/templates/wiki_edit.html:95 trac/wiki/templates/wiki_edit.html:153
-#: trac/wiki/templates/wiki_edit_form.html:64
+#: trac/ticket/templates/ticket.html:390
+#: trac/ticket/templates/ticket_change.html:123
+#: trac/wiki/templates/wiki_edit.html:102
+#: trac/wiki/templates/wiki_edit.html:166
+#: trac/wiki/templates/wiki_edit_form.html:75
 msgid "Preview"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:388
+#: trac/ticket/templates/ticket.html:391
 msgid "Create ticket"
 msgstr ""
 
-#: trac/ticket/templates/ticket.html:396
+#: trac/ticket/templates/ticket.html:399
 msgid ""
 "[1:Note:] See\n"
 "        [2:TracTickets] for help on using\n"
 "        tickets."
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:22
+#: trac/ticket/templates/ticket_box.html:31
 #, python-format
 msgid "Opened %(created)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:23
+#: trac/ticket/templates/ticket_box.html:32
 #, python-format
 msgid "Closed %(closed)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:24
+#: trac/ticket/templates/ticket_box.html:33
 #, python-format
 msgid "Last modified %(modified)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:26
+#: trac/ticket/templates/ticket_box.html:35
 msgid "(ticket not yet created)"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:48
+#: trac/ticket/templates/ticket_box.html:57
 msgid "at [1:Initial Version]"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:51
+#: trac/ticket/templates/ticket_box.html:60
 #, python-format
 msgid "at [1:Version %(version)s]"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:62
+#: trac/ticket/templates/ticket_box.html:71
 msgid "Reported by:"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:64
+#: trac/ticket/templates/ticket_box.html:73
 msgid "Owned by:"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:96
+#: trac/ticket/templates/ticket_box.html:105
 #, python-format
 msgid "(last modified by %(author)s)"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:105
-#: trac/ticket/templates/ticket_change.html:72
+#: trac/ticket/templates/ticket_box.html:114
+#: trac/ticket/templates/ticket_change.html:81
 msgid "Reply"
 msgstr ""
 
-#: trac/ticket/templates/ticket_box.html:105
+#: trac/ticket/templates/ticket_box.html:114
 msgid "Reply, quoting this description"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:37
+#: trac/ticket/templates/ticket_change.html:46
 msgid "in reply to:"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:42
+#: trac/ticket/templates/ticket_change.html:51
 msgid "follow-up:"
 msgid_plural "follow-ups:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/ticket/templates/ticket_change.html:53
+#: trac/ticket/templates/ticket_change.html:62
 #, python-format
 msgid "Changed %(date)s by %(author)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:56
+#: trac/ticket/templates/ticket_change.html:65
 #, python-format
 msgid "Changed by %(author)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:65
+#: trac/ticket/templates/ticket_change.html:74
 #, python-format
 msgid "Edit comment %(cnum)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:72
+#: trac/ticket/templates/ticket_change.html:81
 #, python-format
 msgid "Reply to comment %(cnum)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:81
+#: trac/ticket/templates/ticket_change.html:90
 #, python-format
 msgid ""
 "[1:[2:%(name)s]][3:​]\n"
 "          added"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:88
+#: trac/ticket/templates/ticket_change.html:97
 #, python-format
 msgid "changed from [1:%(old)s] to [2:%(new)s]"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:91
+#: trac/ticket/templates/ticket_change.html:100
 #, python-format
 msgid "set to [1:%(value)s]"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:94
+#: trac/ticket/templates/ticket_change.html:103
 #, python-format
 msgid "[1:%(value)s] deleted"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:98
+#: trac/ticket/templates/ticket_change.html:107
 msgid "Revert this change"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:100
+#: trac/ticket/templates/ticket_change.html:109
 msgid "revert"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:114
+#: trac/ticket/templates/ticket_change.html:123
 #, python-format
 msgid "Preview changes to comment %(cnum)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:116
+#: trac/ticket/templates/ticket_change.html:125
 #, python-format
 msgid "Submit changes to comment %(cnum)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:118
+#: trac/ticket/templates/ticket_change.html:127
 msgid "Cancel comment edit"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:137
+#: trac/ticket/templates/ticket_change.html:146
 #, python-format
 msgid ""
 "Version %(version)s, edited %(date)s\n"
 "        by %(author)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:141
+#: trac/ticket/templates/ticket_change.html:150
 #, python-format
 msgid ""
 "Last edited %(date)s\n"
 "        by %(author)s"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:147
-#: trac/versioncontrol/templates/changeset.html:129
+#: trac/ticket/templates/ticket_change.html:156
+#: trac/versioncontrol/templates/changeset.html:139
 msgid "previous"
 msgstr ""
 
-#: trac/ticket/templates/ticket_change.html:151
+#: trac/ticket/templates/ticket_change.html:160
 msgid "next"
 msgstr ""
 
@@ -4331,8 +4578,8 @@
 msgid "Ticket URL: <%(link)s>"
 msgstr ""
 
-#: trac/timeline/web_ui.py:78 trac/timeline/templates/timeline.html:10
-#: trac/timeline/templates/timeline.html:21
+#: trac/timeline/web_ui.py:78 trac/timeline/templates/timeline.html:20
+#: trac/timeline/templates/timeline.html:31
 msgid "Timeline"
 msgstr ""
 
@@ -4344,12 +4591,12 @@
 msgid "Next Period"
 msgstr ""
 
-#: trac/timeline/web_ui.py:290
+#: trac/timeline/web_ui.py:290 trac/web/chrome.py:885
 #, python-format
 msgid "at %(iso8601)s"
 msgstr ""
 
-#: trac/timeline/web_ui.py:294
+#: trac/timeline/web_ui.py:294 trac/web/chrome.py:889
 #, python-format
 msgid "on %(date)s at %(time)s"
 msgstr ""
@@ -4359,7 +4606,7 @@
 msgid "See timeline %(relativetime)s ago"
 msgstr ""
 
-#: trac/timeline/web_ui.py:298 trac/web/chrome.py:865 trac/web/chrome.py:867
+#: trac/timeline/web_ui.py:298 trac/web/chrome.py:890 trac/web/chrome.py:892
 #, python-format
 msgid "%(relativetime)s ago"
 msgstr ""
@@ -4387,29 +4634,29 @@
 "the log)."
 msgstr ""
 
-#: trac/timeline/templates/timeline.html:24
+#: trac/timeline/templates/timeline.html:34
 msgid ""
 "[1:View changes from [2:]] [3:]\n"
 "        and [4:[5:] days back][6:]\n"
 "        [7:done by [8:]]"
 msgstr ""
 
-#: trac/timeline/templates/timeline.html:41
+#: trac/timeline/templates/timeline.html:51
 msgid "Today"
 msgstr ""
 
-#: trac/timeline/templates/timeline.html:41
+#: trac/timeline/templates/timeline.html:51
 msgid "Yesterday"
 msgstr ""
 
-#: trac/timeline/templates/timeline.html:48
+#: trac/timeline/templates/timeline.html:58
 #, python-format
 msgid ""
 "[1:%(time)s] %(title)s\n"
 "                  by [2:%(author)s]"
 msgstr ""
 
-#: trac/timeline/templates/timeline.html:64
+#: trac/timeline/templates/timeline.html:74
 msgid ""
 "[1:Note:] See [2:TracTimeline]\n"
 "        for information about the timeline view."
@@ -4445,67 +4692,74 @@
 "  %(new_path)s\n"
 msgstr ""
 
-#: trac/util/datefmt.py:118
+#: trac/util/datefmt.py:123
 #, python-format
 msgid "%(num)d year"
 msgid_plural "%(num)d years"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/util/datefmt.py:119
+#: trac/util/datefmt.py:124
 #, python-format
 msgid "%(num)d month"
 msgid_plural "%(num)d months"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/util/datefmt.py:120
+#: trac/util/datefmt.py:125
 #, python-format
 msgid "%(num)d week"
 msgid_plural "%(num)d weeks"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/util/datefmt.py:121
+#: trac/util/datefmt.py:126
 #, python-format
 msgid "%(num)d day"
 msgid_plural "%(num)d days"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/util/datefmt.py:122
+#: trac/util/datefmt.py:127
 #, python-format
 msgid "%(num)d hour"
 msgid_plural "%(num)d hours"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/util/datefmt.py:123
+#: trac/util/datefmt.py:128
 #, python-format
 msgid "%(num)d minute"
 msgid_plural "%(num)d minutes"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/util/datefmt.py:142
+#: trac/util/datefmt.py:147
 #, python-format
 msgid "%(num)i second"
 msgid_plural "%(num)i seconds"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/util/datefmt.py:464
+#: trac/util/datefmt.py:501
+#, python-format
+msgid ""
+"\"%(date)s\" is an invalid date, or the date format is not known. Try "
+"\"%(hint)s\" or \"%(isohint)s\" instead."
+msgstr ""
+
+#: trac/util/datefmt.py:503
 #, python-format
 msgid ""
 "\"%(date)s\" is an invalid date, or the date format is not known. Try "
 "\"%(hint)s\" instead."
 msgstr ""
 
-#: trac/util/datefmt.py:466 trac/util/datefmt.py:474
+#: trac/util/datefmt.py:506 trac/util/datefmt.py:514
 msgid "Invalid Date"
 msgstr ""
 
-#: trac/util/datefmt.py:472
+#: trac/util/datefmt.py:512
 #, python-format
 msgid ""
 "The date \"%(date)s\" is outside valid range. Try a date closer to "
@@ -4527,7 +4781,7 @@
 msgstr ""
 
 #: trac/versioncontrol/admin.py:113
-#: trac/versioncontrol/templates/admin_repositories.html:125
+#: trac/versioncontrol/templates/admin_repositories.html:138
 msgid "Directory"
 msgstr ""
 
@@ -4535,11 +4789,11 @@
 msgid "Cannot synchronize a single revision on multiple repositories"
 msgstr ""
 
-#: trac/versioncontrol/admin.py:127 trac/versioncontrol/admin.py:196
-#: trac/versioncontrol/web_ui/browser.py:356
-#: trac/versioncontrol/web_ui/changeset.py:248
-#: trac/versioncontrol/web_ui/changeset.py:1104
-#: trac/versioncontrol/web_ui/log.py:93 trac/versioncontrol/web_ui/log.py:413
+#: trac/versioncontrol/admin.py:127 trac/versioncontrol/admin.py:194
+#: trac/versioncontrol/web_ui/browser.py:355
+#: trac/versioncontrol/web_ui/changeset.py:250
+#: trac/versioncontrol/web_ui/changeset.py:1074
+#: trac/versioncontrol/web_ui/log.py:97 trac/versioncontrol/web_ui/log.py:426
 #, python-format
 msgid "Repository '%(repo)s' not found"
 msgstr ""
@@ -4570,65 +4824,75 @@
 msgstr ""
 
 #: trac/versioncontrol/admin.py:181
-#: trac/versioncontrol/templates/admin_repositories.html:10
+#: trac/versioncontrol/templates/admin_repositories.html:20
 msgid "Repositories"
 msgstr ""
 
-#: trac/versioncontrol/admin.py:220 trac/versioncontrol/admin.py:262
+#: trac/versioncontrol/admin.py:219 trac/versioncontrol/admin.py:268
 #, python-format
 msgid "You should now run %(resync)s to synchronize Trac with the repository."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:225
+#: trac/versioncontrol/admin.py:224
 #, python-format
 msgid "You may have to run %(resync)s to synchronize Trac with the repository."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:233
+#: trac/versioncontrol/admin.py:232
 #, python-format
 msgid ""
 "You will need to update your post-commit hook to call %(cset_added)s with"
 " the new repository name."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:253
+#: trac/versioncontrol/admin.py:254
 msgid "Missing arguments to add a repository."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:258
+#: trac/versioncontrol/admin.py:261 trac/versioncontrol/api.py:262
+#, python-format
+msgid "The repository \"%(name)s\" already exists."
+msgstr ""
+
+#: trac/versioncontrol/admin.py:264
 #, python-format
 msgid "The repository \"%(name)s\" has been added."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:268
+#: trac/versioncontrol/admin.py:274
 #, python-format
 msgid ""
 "You should also set up a post-commit hook on the repository to call "
 "%(cset_added)s for each committed changeset."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:281
+#: trac/versioncontrol/admin.py:289
+#, python-format
+msgid "The alias \"%(name)s\" already exists."
+msgstr ""
+
+#: trac/versioncontrol/admin.py:292
 #, python-format
 msgid "The alias \"%(name)s\" has been added."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:284
+#: trac/versioncontrol/admin.py:295
 msgid "Missing arguments to add an alias."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:297
+#: trac/versioncontrol/admin.py:308
 msgid "The selected repositories have been removed."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:300
+#: trac/versioncontrol/admin.py:311
 msgid "No repositories were selected."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:341
+#: trac/versioncontrol/admin.py:352
 msgid "The repository directory must be an absolute path."
 msgstr ""
 
-#: trac/versioncontrol/admin.py:350
+#: trac/versioncontrol/admin.py:361
 #, python-format
 msgid ""
 "The repository directory must be located below one of the following "
@@ -4636,13 +4900,13 @@
 msgstr ""
 
 #: trac/versioncontrol/api.py:34
-#: trac/versioncontrol/templates/admin_repositories.html:20
-#: trac/versioncontrol/templates/admin_repositories.html:33
-#: trac/versioncontrol/templates/admin_repositories.html:132
-#: trac/versioncontrol/templates/admin_repositories.html:134
-#: trac/versioncontrol/web_ui/browser.py:911
-#: trac/versioncontrol/web_ui/changeset.py:875
-#: trac/versioncontrol/web_ui/changeset.py:1015
+#: trac/versioncontrol/templates/admin_repositories.html:30
+#: trac/versioncontrol/templates/admin_repositories.html:43
+#: trac/versioncontrol/templates/admin_repositories.html:145
+#: trac/versioncontrol/templates/admin_repositories.html:147
+#: trac/versioncontrol/web_ui/browser.py:954
+#: trac/versioncontrol/web_ui/changeset.py:845
+#: trac/versioncontrol/web_ui/changeset.py:985
 msgid "(default)"
 msgstr ""
 
@@ -4661,7 +4925,7 @@
 msgid "You may have to run \"repository resync %(name)s\"."
 msgstr ""
 
-#: trac/versioncontrol/api.py:207 trac/versioncontrol/api.py:262
+#: trac/versioncontrol/api.py:207 trac/versioncontrol/api.py:271
 msgid "The repository directory must be absolute"
 msgstr ""
 
@@ -4670,92 +4934,104 @@
 msgid "The repository type '%(type)s' is not supported"
 msgstr ""
 
-#: trac/versioncontrol/api.py:356
+#: trac/versioncontrol/api.py:365
 #, python-format
 msgid ""
 "Can't synchronize with repository \"%(name)s\" (%(error)s). Look in the "
 "Trac log for more information."
 msgstr ""
 
-#: trac/versioncontrol/api.py:377
+#: trac/versioncontrol/api.py:372
+#, python-format
+msgid ""
+"Failed to sync with repository \"%(name)s\": %(error)s; repository "
+"information may be out of date. Look in the Trac log for more information"
+" including mitigation strategies."
+msgstr ""
+
+#: trac/versioncontrol/api.py:401
 #, python-format
 msgid "Changeset %(rev)s in %(repo)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:379
+#: trac/versioncontrol/api.py:403
 #, python-format
 msgid "Changeset %(rev)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:389
+#: trac/versioncontrol/api.py:413
 msgid "directory"
 msgstr ""
 
-#: trac/versioncontrol/api.py:391
+#: trac/versioncontrol/api.py:415
 msgid "file"
 msgstr ""
 
-#: trac/versioncontrol/api.py:393
+#: trac/versioncontrol/api.py:417
 #, python-format
 msgid " at version %(rev)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:395
+#: trac/versioncontrol/api.py:419
 msgid "path"
 msgstr ""
 
-#: trac/versioncontrol/api.py:398
+#: trac/versioncontrol/api.py:422
 #, python-format
 msgid " in %(repo)s"
 msgstr ""
 
 #. TRANSLATOR: file /path/to/file.py at version 13 in reponame
-#: trac/versioncontrol/api.py:400
+#: trac/versioncontrol/api.py:424
 #, python-format
 msgid "%(kind)s %(id)s%(at_version)s%(in_repo)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:403
+#: trac/versioncontrol/api.py:428
+msgid "Default repository"
+msgstr ""
+
+#: trac/versioncontrol/api.py:429
 #, python-format
 msgid "Repository %(repo)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:711
+#: trac/versioncontrol/api.py:741
 #, python-format
 msgid "Unsupported version control system \"%(name)s\": %(error)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:714
+#: trac/versioncontrol/api.py:744
 #, python-format
 msgid ""
 "Unsupported version control system \"%(name)s\": Can't find an "
 "appropriate component, maybe the corresponding plugin was not enabled? "
 msgstr ""
 
-#: trac/versioncontrol/api.py:722
+#: trac/versioncontrol/api.py:752
 #, python-format
 msgid "No changeset %(rev)s in the repository"
 msgstr ""
 
-#: trac/versioncontrol/api.py:724
+#: trac/versioncontrol/api.py:754
 msgid "No such changeset"
 msgstr ""
 
-#: trac/versioncontrol/api.py:730
+#: trac/versioncontrol/api.py:760
 #, python-format
 msgid "No node %(path)s at revision %(rev)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:732
+#: trac/versioncontrol/api.py:762
 #, python-format
 msgid "%(msg)s: No node %(path)s at revision %(rev)s"
 msgstr ""
 
-#: trac/versioncontrol/api.py:734
+#: trac/versioncontrol/api.py:764
 msgid "No such node"
 msgstr ""
 
-#: trac/versioncontrol/cache.py:147
+#: trac/versioncontrol/cache.py:245
 #, python-format
 msgid ""
 "The repository directory has changed, you should resynchronize the "
@@ -4772,136 +5048,136 @@
 msgid "Line %(lineno)d: Invalid entry"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:14
+#: trac/versioncontrol/templates/admin_repositories.html:24
 msgid "Manage Repositories"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:24
+#: trac/versioncontrol/templates/admin_repositories.html:34
 msgid "Default:"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:30
+#: trac/versioncontrol/templates/admin_repositories.html:40
 msgid "Repository:"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:43
+#: trac/versioncontrol/templates/admin_repositories.html:54
 msgid "Modify Repository:"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:44
+#: trac/versioncontrol/templates/admin_repositories.html:55
 msgid "View Repository:"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:45
+#: trac/versioncontrol/templates/admin_repositories.html:56
 msgid ""
 "[1:Note:]\n"
 "            This repository is defined in [2:[3:trac.ini]]\n"
 "            and cannot be edited on this page."
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:59
-#: trac/versioncontrol/templates/admin_repositories.html:99
+#: trac/versioncontrol/templates/admin_repositories.html:71
+#: trac/versioncontrol/templates/admin_repositories.html:112
 msgid "Directory:"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:67
+#: trac/versioncontrol/templates/admin_repositories.html:80
 msgid "Hide from repository index"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:93
+#: trac/versioncontrol/templates/admin_repositories.html:106
 msgid "Add Repository:"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:110
+#: trac/versioncontrol/templates/admin_repositories.html:123
 msgid "Add Alias:"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:125
+#: trac/versioncontrol/templates/admin_repositories.html:138
 msgid "Revision"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:137
+#: trac/versioncontrol/templates/admin_repositories.html:150
 #, python-format
 msgid "Alias of %(repo)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/admin_repositories.html:144
+#: trac/versioncontrol/templates/admin_repositories.html:157
 msgid "Refresh"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:13
+#: trac/versioncontrol/templates/browser.html:23
 #, python-format
 msgid "%(basename)s in %(dirname)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:55
+#: trac/versioncontrol/templates/browser.html:65
 msgid "Default Repository"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:62
+#: trac/versioncontrol/templates/browser.html:72
 msgid "Show the diff against a specific revision"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:63
+#: trac/versioncontrol/templates/browser.html:73
 msgid "View diff against:"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:76
+#: trac/versioncontrol/templates/browser.html:86
 msgid "Hint: clear the field to view latest revision"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:76
+#: trac/versioncontrol/templates/browser.html:86
 msgid "View revision:"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:86
+#: trac/versioncontrol/templates/browser.html:96
 msgid "Visit:"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:94
+#: trac/versioncontrol/templates/browser.html:104
 msgid "Go!"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:94
+#: trac/versioncontrol/templates/browser.html:104
 msgid "Jump to the chosen preselected path"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:100
-#: trac/versioncontrol/templates/revisionlog.html:179
+#: trac/versioncontrol/templates/browser.html:110
+#: trac/versioncontrol/templates/revisionlog.html:189
 msgid "Branch head"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:100
-#: trac/versioncontrol/templates/revisionlog.html:179
+#: trac/versioncontrol/templates/browser.html:110
+#: trac/versioncontrol/templates/revisionlog.html:189
 msgid "Branch"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:103
-#: trac/versioncontrol/templates/revisionlog.html:182
+#: trac/versioncontrol/templates/browser.html:113
+#: trac/versioncontrol/templates/revisionlog.html:192
 msgid "Tag"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:114
+#: trac/versioncontrol/templates/browser.html:124
 msgid "Parent Directory"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:120
+#: trac/versioncontrol/templates/browser.html:130
 msgid "No files found"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:128
-#: trac/wiki/templates/wiki_edit.html:137 trac/wiki/templates/wiki_view.html:34
+#: trac/versioncontrol/templates/browser.html:138
+#: trac/wiki/templates/wiki_edit.html:150 trac/wiki/templates/wiki_view.html:45
 msgid "Revision info"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:140
-#: trac/versioncontrol/templates/browser.html:148
-#: trac/versioncontrol/templates/path_links.html:32
+#: trac/versioncontrol/templates/browser.html:150
+#: trac/versioncontrol/templates/browser.html:158
+#: trac/versioncontrol/templates/path_links.html:42
 #, python-format
 msgid "View changeset %(rev)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:137
+#: trac/versioncontrol/templates/browser.html:147
 #, python-format
 msgid ""
 "[1:Last change]\n"
@@ -4910,7 +5186,7 @@
 "                  checked in by %(author)s, %(age)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:145
+#: trac/versioncontrol/templates/browser.html:155
 #, python-format
 msgid ""
 "[1:Last change]\n"
@@ -4919,12 +5195,12 @@
 "                  checked in by %(author)s, %(age)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:180
+#: trac/versioncontrol/templates/browser.html:190
 #, python-format
 msgid "Property [1:%(name)s] set to %(value)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:186
+#: trac/versioncontrol/templates/browser.html:196
 #, python-format
 msgid ""
 "[1:\n"
@@ -4933,65 +5209,65 @@
 "          ]"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:200
+#: trac/versioncontrol/templates/browser.html:210
 msgid "Repository Index"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:217
+#: trac/versioncontrol/templates/browser.html:227
 msgid "View changes..."
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:217
+#: trac/versioncontrol/templates/browser.html:227
 msgid "Select paths and revs for Diff"
 msgstr ""
 
-#: trac/versioncontrol/templates/browser.html:222
+#: trac/versioncontrol/templates/browser.html:232
 msgid ""
 "[1:Note:] See [2:TracBrowser]\n"
 "        for help on using the repository browser."
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:36
 #: trac/versioncontrol/templates/changeset.html:46
-#: trac/versioncontrol/templates/changeset.html:48
+#: trac/versioncontrol/templates/changeset.html:56
 #: trac/versioncontrol/templates/changeset.html:58
 #: trac/versioncontrol/templates/changeset.html:68
-#: trac/versioncontrol/templates/changeset.html:70
+#: trac/versioncontrol/templates/changeset.html:78
+#: trac/versioncontrol/templates/changeset.html:80
 msgid "Show full changeset"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:37
-#: trac/versioncontrol/templates/changeset.html:40
-#: trac/versioncontrol/templates/changeset.html:45
 #: trac/versioncontrol/templates/changeset.html:47
-#: trac/versioncontrol/templates/changeset.html:59
-#: trac/versioncontrol/templates/changeset.html:62
-#: trac/versioncontrol/templates/changeset.html:67
+#: trac/versioncontrol/templates/changeset.html:50
+#: trac/versioncontrol/templates/changeset.html:55
+#: trac/versioncontrol/templates/changeset.html:57
 #: trac/versioncontrol/templates/changeset.html:69
-#: trac/versioncontrol/templates/changeset.html:105
+#: trac/versioncontrol/templates/changeset.html:72
+#: trac/versioncontrol/templates/changeset.html:77
+#: trac/versioncontrol/templates/changeset.html:79
+#: trac/versioncontrol/templates/changeset.html:115
 msgid "Show entry in browser"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:35
+#: trac/versioncontrol/templates/changeset.html:45
 #, python-format
 msgid ""
 "Changeset [1:%(new_rev)s] in %(reponame)s\n"
 "              for [2:%(new_path)s]"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:41
-#: trac/versioncontrol/templates/changeset.html:63
+#: trac/versioncontrol/templates/changeset.html:51
+#: trac/versioncontrol/templates/changeset.html:73
 msgid "Show revision log"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:39
+#: trac/versioncontrol/templates/changeset.html:49
 #, python-format
 msgid ""
 "Changes in [1:%(new_path)s]\n"
 "              [2:\\[%(old_rev)s:%(new_rev)s\\]] in %(reponame)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:43
+#: trac/versioncontrol/templates/changeset.html:53
 #, python-format
 msgid ""
 "Changes in %(reponame)s\n"
@@ -5001,26 +5277,26 @@
 "              at [4:r%(new_rev)s]"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:50
+#: trac/versioncontrol/templates/changeset.html:60
 #, python-format
 msgid "Changeset [1:%(new_rev)s] in %(reponame)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:57
+#: trac/versioncontrol/templates/changeset.html:67
 #, python-format
 msgid ""
 "Changeset [1:%(new_rev)s]\n"
 "              for [2:%(new_path)s]"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:61
+#: trac/versioncontrol/templates/changeset.html:71
 #, python-format
 msgid ""
 "Changes in [1:%(new_path)s]\n"
 "              [2:\\[%(old_rev)s:%(new_rev)s\\]]"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:65
+#: trac/versioncontrol/templates/changeset.html:75
 #, python-format
 msgid ""
 "Changes\n"
@@ -5030,170 +5306,170 @@
 "              at [4:r%(new_rev)s]"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:72
+#: trac/versioncontrol/templates/changeset.html:82
 #, python-format
 msgid "Changeset [1:%(new_rev)s]"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:101
+#: trac/versioncontrol/templates/changeset.html:111
 #, python-format
 msgid "Show what was removed (content at revision %(old_rev)s)"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:106
+#: trac/versioncontrol/templates/changeset.html:116
 msgid "(root)"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:112
+#: trac/versioncontrol/templates/changeset.html:122
 #, python-format
 msgid "Show original file (revision %(old_rev)s)"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:111
+#: trac/versioncontrol/templates/changeset.html:121
 #, python-format
 msgid ""
 "(%(kind)s from [1:\n"
 "                %(old_path)s])"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:119
-#: trac/versioncontrol/templates/changeset.html:122
+#: trac/versioncontrol/templates/changeset.html:129
+#: trac/versioncontrol/templates/changeset.html:132
 msgid "Show differences"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:119
+#: trac/versioncontrol/templates/changeset.html:129
 msgid "view diffs"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:122
+#: trac/versioncontrol/templates/changeset.html:132
 #, python-format
 msgid "%(num)d diff"
 msgid_plural "%(num)d diffs"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:125
+#: trac/versioncontrol/templates/changeset.html:135
 #, python-format
 msgid "%(num)d prop"
 msgid_plural "%(num)d props"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:129
+#: trac/versioncontrol/templates/changeset.html:139
 msgid "Show previous version in browser"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:139
+#: trac/versioncontrol/templates/changeset.html:149
 msgid "(less than one hour ago)"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:140
+#: trac/versioncontrol/templates/changeset.html:150
 #, python-format
 msgid "(%(age)s ago)"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:154
+#: trac/versioncontrol/templates/changeset.html:164
 msgid "Message:"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:166
+#: trac/versioncontrol/templates/changeset.html:176
 msgid "Location:"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:170
+#: trac/versioncontrol/templates/changeset.html:180
 msgid "File:"
 msgid_plural "Files:"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:170
+#: trac/versioncontrol/templates/changeset.html:180
 msgid "(No files)"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:175
+#: trac/versioncontrol/templates/changeset.html:185
 #, python-format
 msgid "%(num)d added"
 msgid_plural "%(num)d added"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:176
+#: trac/versioncontrol/templates/changeset.html:186
 #, python-format
 msgid "%(num)d deleted"
 msgid_plural "%(num)d deleted"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:177
+#: trac/versioncontrol/templates/changeset.html:187
 #, python-format
 msgid "%(num)d edited"
 msgid_plural "%(num)d edited"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:178
+#: trac/versioncontrol/templates/changeset.html:188
 #, python-format
 msgid "%(num)d copied"
 msgid_plural "%(num)d copied"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:179
+#: trac/versioncontrol/templates/changeset.html:189
 #, python-format
 msgid "%(num)d moved"
 msgid_plural "%(num)d moved"
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/templates/changeset.html:185
+#: trac/versioncontrol/templates/changeset.html:195
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "added"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:186
+#: trac/versioncontrol/templates/changeset.html:196
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "deleted"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:187
+#: trac/versioncontrol/templates/changeset.html:197
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "copied"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:188
+#: trac/versioncontrol/templates/changeset.html:198
 #: trac/versioncontrol/templates/revisionlog.txt:12
 msgid "moved"
 msgstr ""
 
-#: trac/versioncontrol/templates/changeset.html:211
+#: trac/versioncontrol/templates/changeset.html:221
 msgid ""
 "[1:Note:] See [2:TracChangeset]\n"
 "          for help on using the changeset viewer."
 msgstr ""
 
-#: trac/versioncontrol/templates/diff_form.html:10
-#: trac/versioncontrol/templates/diff_form.html:21
+#: trac/versioncontrol/templates/diff_form.html:20
+#: trac/versioncontrol/templates/diff_form.html:31
 msgid "Prepare Diff"
 msgstr ""
 
-#: trac/versioncontrol/templates/diff_form.html:27
+#: trac/versioncontrol/templates/diff_form.html:37
 msgid "Select the base and the target for the diff:"
 msgstr ""
 
-#: trac/versioncontrol/templates/diff_form.html:30
+#: trac/versioncontrol/templates/diff_form.html:40
 msgid "From:"
 msgstr ""
 
-#: trac/versioncontrol/templates/diff_form.html:34
 #: trac/versioncontrol/templates/diff_form.html:44
+#: trac/versioncontrol/templates/diff_form.html:54
 msgid "at revision:"
 msgstr ""
 
-#: trac/versioncontrol/templates/diff_form.html:40
+#: trac/versioncontrol/templates/diff_form.html:50
 msgid "To:"
 msgstr ""
 
-#: trac/versioncontrol/templates/diff_form.html:50
+#: trac/versioncontrol/templates/diff_form.html:60
 msgid ""
 "For either path, you can start typing the path and will be\n"
 "              presented a list of existing directories and files to "
@@ -5202,86 +5478,86 @@
 "              up/down cursor keys and hitting tab."
 msgstr ""
 
-#: trac/versioncontrol/templates/diff_form.html:62
+#: trac/versioncontrol/templates/diff_form.html:72
 msgid ""
 "[1:Note:] See\n"
 "        [2:TracChangeset]\n"
 "        for help on using the diff feature."
 msgstr ""
 
-#: trac/versioncontrol/templates/dir_entries.html:12
+#: trac/versioncontrol/templates/dir_entries.html:23
 msgid "View Directory"
 msgstr ""
 
-#: trac/versioncontrol/templates/dir_entries.html:12
+#: trac/versioncontrol/templates/dir_entries.html:23
 msgid "View File"
 msgstr ""
 
-#: trac/versioncontrol/templates/dir_entries.html:18
-#: trac/versioncontrol/templates/repository_index.html:22
-#: trac/versioncontrol/web_ui/browser.py:823
+#: trac/versioncontrol/templates/dir_entries.html:29
+#: trac/versioncontrol/templates/repository_index.html:33
+#: trac/versioncontrol/web_ui/browser.py:866
 msgid "Download as Zip archive"
 msgstr ""
 
-#: trac/versioncontrol/templates/dir_entries.html:22
-#: trac/versioncontrol/templates/repository_index.html:26
+#: trac/versioncontrol/templates/dir_entries.html:33
+#: trac/versioncontrol/templates/repository_index.html:37
 msgid "View Revision Log"
 msgstr ""
 
-#: trac/versioncontrol/templates/dir_entries.html:23
-#: trac/versioncontrol/templates/repository_index.html:27
+#: trac/versioncontrol/templates/dir_entries.html:34
+#: trac/versioncontrol/templates/repository_index.html:38
 msgid "View Changeset"
 msgstr ""
 
-#: trac/versioncontrol/templates/dirlist_thead.html:9
-#: trac/versioncontrol/templates/revisionlog.html:110
-#: trac/versioncontrol/web_ui/browser.py:831
+#: trac/versioncontrol/templates/dirlist_thead.html:20
+#: trac/versioncontrol/templates/revisionlog.html:120
+#: trac/versioncontrol/web_ui/browser.py:874
 msgid "Rev"
 msgstr ""
 
-#: trac/versioncontrol/templates/dirlist_thead.html:12
-#: trac/versioncontrol/web_ui/browser.py:457
+#: trac/versioncontrol/templates/dirlist_thead.html:23
+#: trac/versioncontrol/web_ui/browser.py:475
 msgid "Last Change"
 msgstr ""
 
-#: trac/versioncontrol/templates/path_links.html:17
+#: trac/versioncontrol/templates/path_links.html:27
 msgid "Go to repository index"
 msgstr ""
 
-#: trac/versioncontrol/templates/path_links.html:17
+#: trac/versioncontrol/templates/path_links.html:27
 msgid "Go to repository root"
 msgstr ""
 
-#: trac/versioncontrol/templates/path_links.html:26
+#: trac/versioncontrol/templates/path_links.html:36
 #, python-format
 msgid "View %(name)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/repository_index.html:15
+#: trac/versioncontrol/templates/repository_index.html:26
 msgid "View Root Directory"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:10
+#: trac/versioncontrol/templates/revisionlog.html:20
 msgid "(log)"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:36
+#: trac/versioncontrol/templates/revisionlog.html:46
 msgid "Revision Log Mode:"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:40
+#: trac/versioncontrol/templates/revisionlog.html:50
 msgid "Stop on copy"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:46
+#: trac/versioncontrol/templates/revisionlog.html:56
 msgid "Follow copies"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:52
+#: trac/versioncontrol/templates/revisionlog.html:62
 msgid "Show only adds and deletes"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:57
+#: trac/versioncontrol/templates/revisionlog.html:67
 msgid ""
 "[1:\n"
 "              View log starting at\n"
@@ -5293,7 +5569,7 @@
 "            ]"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:67
+#: trac/versioncontrol/templates/revisionlog.html:77
 msgid ""
 "[1:\n"
 "              Show at most\n"
@@ -5302,78 +5578,78 @@
 "            ]"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:75
+#: trac/versioncontrol/templates/revisionlog.html:85
 msgid "Show full log messages"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:93
+#: trac/versioncontrol/templates/revisionlog.html:103
 msgid "Copied or renamed"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:101
-#: trac/versioncontrol/templates/revisionlog.html:204
+#: trac/versioncontrol/templates/revisionlog.html:111
+#: trac/versioncontrol/templates/revisionlog.html:214
 msgid "Diff from Old Revision to New Revision (as selected in the Diff column)"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:107
+#: trac/versioncontrol/templates/revisionlog.html:117
 msgid "Graph"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:108
+#: trac/versioncontrol/templates/revisionlog.html:118
 msgid "Old / New"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:108
+#: trac/versioncontrol/templates/revisionlog.html:118
 msgid "Diff"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:111
+#: trac/versioncontrol/templates/revisionlog.html:121
 msgid "Age"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:113
+#: trac/versioncontrol/templates/revisionlog.html:123
 msgid "Log Message"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:121
+#: trac/versioncontrol/templates/revisionlog.html:131
 msgid "No revisions found"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:135
-#, python-format
-msgid "copied from [1:%(path)s]:"
-msgstr ""
-
-#: trac/versioncontrol/templates/revisionlog.html:142
-#, python-format
-msgid "From [%(rev)s]"
-msgstr ""
-
 #: trac/versioncontrol/templates/revisionlog.html:145
 #, python-format
+msgid "copied from [1:%(path)s]:"
+msgstr ""
+
+#: trac/versioncontrol/templates/revisionlog.html:152
+#, python-format
+msgid "From [%(rev)s]"
+msgstr ""
+
+#: trac/versioncontrol/templates/revisionlog.html:155
+#, python-format
 msgid "To [%(rev)s]"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:151
+#: trac/versioncontrol/templates/revisionlog.html:161
 msgid "View log starting at this revision"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:158
+#: trac/versioncontrol/templates/revisionlog.html:168
 #, python-format
 msgid "Browse at revision %(rev)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:162
+#: trac/versioncontrol/templates/revisionlog.html:172
 #, python-format
 msgid "View removal changeset [%(rev)s]"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:164
+#: trac/versioncontrol/templates/revisionlog.html:174
 #, python-format
 msgid "View changeset [%(rev)s] restricted to %(path)s"
 msgstr ""
 
-#: trac/versioncontrol/templates/revisionlog.html:209
+#: trac/versioncontrol/templates/revisionlog.html:219
 msgid ""
 "[1:Note:] See [2:TracRevisionLog]\n"
 "        for help on using the revision log."
@@ -5402,70 +5678,78 @@
 msgid "Invalid changeset number"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:400
+#: trac/versioncontrol/web_ui/browser.py:416
+msgid "No viewable repositories"
+msgstr ""
+
+#: trac/versioncontrol/web_ui/browser.py:418
 #, python-format
 msgid "No node %(path)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:440
-#: trac/versioncontrol/web_ui/browser.py:450
+#: trac/versioncontrol/web_ui/browser.py:458
+#: trac/versioncontrol/web_ui/browser.py:468
 #, python-format
 msgid "Revision %(num)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:451
+#: trac/versioncontrol/web_ui/browser.py:469
 msgid "Previous Revision"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:451
+#: trac/versioncontrol/web_ui/browser.py:469
 msgid "Next Revision"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:452
+#: trac/versioncontrol/web_ui/browser.py:470
 msgid "Latest Revision"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:456
-#: trac/versioncontrol/web_ui/log.py:315
+#: trac/versioncontrol/web_ui/browser.py:474
+#: trac/versioncontrol/web_ui/log.py:323
 msgid "Parent directory"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:463
+#: trac/versioncontrol/web_ui/browser.py:481
 msgid "Normal"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:464
+#: trac/versioncontrol/web_ui/browser.py:482
 msgid "View file without annotations"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:469
+#: trac/versioncontrol/web_ui/browser.py:487
 msgid "Blame"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:470
+#: trac/versioncontrol/web_ui/browser.py:488
 msgid ""
 "Annotate each line with the last changed revision (this can be time "
 "consuming...)"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:477
+#: trac/versioncontrol/web_ui/browser.py:495
 msgid "Revision Log"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:483
+#: trac/versioncontrol/web_ui/browser.py:501
 msgid "Repository URL"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:612
-#: trac/versioncontrol/web_ui/changeset.py:364
+#: trac/versioncontrol/web_ui/browser.py:631
+#: trac/versioncontrol/web_ui/changeset.py:365
 msgid "Zip Archive"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:831
+#: trac/versioncontrol/web_ui/browser.py:654
+msgid "Path not available for download"
+msgstr ""
+
+#: trac/versioncontrol/web_ui/browser.py:874
 msgid "Revision in which the line changed"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:846
+#: trac/versioncontrol/web_ui/browser.py:889
 msgid ""
 "Display the list of available repositories.\n"
 "\n"
@@ -5489,167 +5773,178 @@
 "(''since 0.12'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:913
+#: trac/versioncontrol/web_ui/browser.py:956
 #, python-format
 msgid "View repository %(repo)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:241
+#: trac/versioncontrol/web_ui/changeset.py:105
+#, python-format
+msgid "Property %(name)s"
+msgstr ""
+
+#: trac/versioncontrol/web_ui/changeset.py:243
 #, python-format
 msgid "Can't compare across different repositories: %(old)s vs. %(new)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:250
+#: trac/versioncontrol/web_ui/changeset.py:252
+#: trac/versioncontrol/web_ui/log.py:93
 msgid "No repository specified and no default repository configured."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:262
+#: trac/versioncontrol/web_ui/changeset.py:260
 msgid "Invalid Changeset Number"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:362
+#: trac/versioncontrol/web_ui/changeset.py:363
 msgid "Unified Diff"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:373
+#: trac/versioncontrol/web_ui/changeset.py:374
 msgid "Previous Changeset"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:373
+#: trac/versioncontrol/web_ui/changeset.py:374
 msgid "Next Changeset"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:377
+#: trac/versioncontrol/web_ui/changeset.py:378
 msgid "Reverse Diff"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:415
+#: trac/versioncontrol/web_ui/changeset.py:416
 #, python-format
 msgid "Changeset %(id)s for %(path)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:418
-#: trac/versioncontrol/web_ui/changeset.py:444
-#: trac/versioncontrol/web_ui/changeset.py:466
+#: trac/versioncontrol/web_ui/changeset.py:419
+#: trac/versioncontrol/web_ui/changeset.py:445
+#: trac/versioncontrol/web_ui/changeset.py:467
 #, python-format
 msgid "Changeset %(id)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:493
+#: trac/versioncontrol/web_ui/changeset.py:494
 #, python-format
 msgid "Show revision %(rev)s of this file in browser"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:639
+#: trac/versioncontrol/web_ui/changeset.py:640
 #, python-format
 msgid "Show the changeset %(id)s restricted to %(path)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:651
+#: trac/versioncontrol/web_ui/changeset.py:652
 #, python-format
 msgid "Show the %(range)s differences restricted to %(path)s"
 msgstr ""
 
 #. TRANSLATOR: 'latest' (revision)
-#: trac/versioncontrol/web_ui/changeset.py:800
+#: trac/versioncontrol/web_ui/changeset.py:770
 msgid "latest"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:803
+#: trac/versioncontrol/web_ui/changeset.py:773
 #, python-format
 msgid "Diff [%(old_rev)s:%(new_rev)s] for %(path)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:809
+#: trac/versioncontrol/web_ui/changeset.py:779
 #, python-format
 msgid "Diff from %(old_path)s@%(old_rev)s to %(new_path)s@%(new_rev)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:881
+#: trac/versioncontrol/web_ui/changeset.py:851
 msgid "Changesets in all repositories"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:883
+#: trac/versioncontrol/web_ui/changeset.py:853
 msgid "Repository changesets"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:1019
+#: trac/versioncontrol/web_ui/changeset.py:989
 #, python-format
 msgid "Changeset in %(repo)s "
 msgid_plural "Changesets in %(repo)s "
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/web_ui/changeset.py:1021
+#: trac/versioncontrol/web_ui/changeset.py:991
 msgid "Changeset "
 msgid_plural "Changesets "
 msgstr[0] ""
 msgstr[1] ""
 
-#: trac/versioncontrol/web_ui/changeset.py:1102
+#: trac/versioncontrol/web_ui/changeset.py:1072
 #, python-format
 msgid "No permission to view changeset %(rev)s on %(repos)s"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:1106
-#: trac/versioncontrol/web_ui/log.py:415
+#: trac/versioncontrol/web_ui/changeset.py:1076
+#: trac/versioncontrol/web_ui/log.py:428
 msgid "No default repository defined"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:1147
+#: trac/versioncontrol/web_ui/changeset.py:1117
 msgid "Changesets"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:209
+#: trac/versioncontrol/web_ui/log.py:215
 #, python-format
 msgid ""
 "The file or directory '%(path)s' doesn't exist at revision %(rev)s or at "
 "any previous revision."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:209
+#: trac/versioncontrol/web_ui/log.py:216
 msgid "Nonexistent path"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:249
+#: trac/versioncontrol/web_ui/log.py:257
 #, python-format
 msgid "Revision Log (restarting at %(path)s, rev. %(rev)s)"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:323
+#: trac/versioncontrol/web_ui/log.py:331
 msgid "ChangeLog"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:325
+#: trac/versioncontrol/web_ui/log.py:334
 msgid "View Latest Revision"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:329
+#: trac/versioncontrol/web_ui/log.py:338
 msgid "Older Revisions"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:411
+#: trac/versioncontrol/web_ui/log.py:424
 msgid "No permission to view change log"
 msgstr ""
 
 #. TRANSLATOR: You can 'search' in the repository history... (link)
-#: trac/versioncontrol/web_ui/util.py:73
+#: trac/versioncontrol/web_ui/util.py:77
 msgid "search"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/util.py:78
+#: trac/versioncontrol/web_ui/util.py:82
 #, python-format
 msgid ""
 "You can %(search)s in the repository history to see if that path existed "
 "but was later removed"
 msgstr ""
 
-#: trac/web/api.py:340
+#: trac/web/api.py:170
+#, python-format
+msgid "Error: %(message)s"
+msgstr ""
+
+#: trac/web/api.py:377
 #, python-format
 msgid "Invalid URL encoding (was %(path_info)r)"
 msgstr ""
 
-#: trac/web/api.py:559
+#: trac/web/api.py:601
 #, python-format
 msgid "File %(path)s not found"
 msgstr ""
@@ -5663,92 +5958,96 @@
 msgid "Logout"
 msgstr ""
 
-#: trac/web/auth.py:114
+#: trac/web/auth.py:117
 msgid "Login"
 msgstr ""
 
 #. TRANSLATOR: ... refer to the 'installation documentation'. (link)
-#: trac/web/auth.py:147
+#: trac/web/auth.py:150
 msgid "installation documentation"
 msgstr ""
 
-#: trac/web/auth.py:148
+#: trac/web/auth.py:151
 msgid "Configuring Authentication"
 msgstr ""
 
-#: trac/web/auth.py:151
+#: trac/web/auth.py:154
 #, python-format
 msgid ""
 "Authentication information not available. Please refer to the "
 "%(inst_doc)s."
 msgstr ""
 
-#: trac/web/auth.py:159
+#: trac/web/auth.py:162
 #, python-format
 msgid "Already logged in as %(user)s."
 msgstr ""
 
-#: trac/web/chrome.py:734
+#: trac/web/chrome.py:626
+#, python-format
+msgid "Invalid chrome path %(path)s."
+msgstr ""
+
+#: trac/web/chrome.py:752
 #, python-format
 msgid "Error with navigation contributor \"%(name)s\""
 msgstr ""
 
-#: trac/web/chrome.py:1029
+#: trac/web/chrome.py:1056
 msgid "(unknown template location)"
 msgstr ""
 
-#: trac/web/chrome.py:1030
+#: trac/web/chrome.py:1057
 #, python-format
 msgid "Genshi %(error)s error while rendering template %(location)s"
 msgstr ""
 
-#: trac/web/chrome.py:1078 trac/web/chrome.py:1086
+#: trac/web/chrome.py:1105 trac/web/chrome.py:1113
 msgid "anonymous"
 msgstr ""
 
-#: trac/web/main.py:206
+#: trac/web/main.py:141
+msgid "Authentication error. Please contact your administrator."
+msgstr ""
+
+#: trac/web/main.py:213
 msgid "Secure cookies are enabled, you must use https to submit forms."
 msgstr ""
 
-#: trac/web/main.py:209
+#: trac/web/main.py:216
 msgid "Do you have cookies enabled?"
 msgstr ""
 
-#: trac/web/main.py:210
+#: trac/web/main.py:217
 #, python-format
 msgid "Missing or invalid form token. %(msg)s"
 msgstr ""
 
-#: trac/web/main.py:220
+#: trac/web/main.py:227
 msgid ""
 "Clearsilver templates are no longer supported, please contact your Trac "
 "administrator."
 msgstr ""
 
-#: trac/web/main.py:521
-#, python-format
-msgid "Error: %(message)s"
-msgstr ""
-
 #. TRANSLATOR: ... not logged in, you may want to 'do so' now (link)
-#: trac/web/main.py:537
+#: trac/web/main.py:525
 msgid "do so"
 msgstr ""
 
-#: trac/web/main.py:539
+#: trac/web/main.py:526
 #, python-format
 msgid "You are currently not logged in. You may want to %(do_so)s now."
 msgstr ""
 
-#: trac/web/main.py:592
+#: trac/web/main.py:586
 msgid "''System information not available''\n"
 msgstr ""
 
-#: trac/web/main.py:593
+#: trac/web/main.py:587
 msgid "''Plugin information not available''\n"
 msgstr ""
 
-#: trac/web/main.py:617
+#: trac/web/main.py:611
 #, python-format
 msgid ""
 "==== How to Reproduce ====\n"
@@ -5774,42 +6073,42 @@
 "%(traceback)s}}}"
 msgstr ""
 
-#: trac/web/session.py:245
+#: trac/web/session.py:246
 #, python-format
 msgid "Session '%(id)s' already exists. Please choose a different session ID."
 msgstr ""
 
-#: trac/web/session.py:248
+#: trac/web/session.py:249
 msgid "Error renaming session"
 msgstr ""
 
-#: trac/web/session.py:417
+#: trac/web/session.py:423
 msgid "SID"
 msgstr ""
 
-#: trac/web/session.py:417
+#: trac/web/session.py:423
 msgid "Auth"
 msgstr ""
 
-#: trac/web/session.py:417
+#: trac/web/session.py:423
 msgid "Last Visit"
 msgstr ""
 
-#: trac/web/session.py:418
+#: trac/web/session.py:424
 msgid "Email"
 msgstr ""
 
-#: trac/web/session.py:427
+#: trac/web/session.py:433
 #, python-format
 msgid "Session '%(sid)s' already exists"
 msgstr ""
 
-#: trac/web/session.py:438
+#: trac/web/session.py:444
 #, python-format
 msgid "Invalid attribute '%(attr)s'"
 msgstr ""
 
-#: trac/web/session.py:445
+#: trac/web/session.py:451
 #, python-format
 msgid "Session '%(sid)s' not found"
 msgstr ""
@@ -5819,8 +6118,8 @@
 msgid "Page '%(page)s' not found"
 msgstr ""
 
-#: trac/wiki/admin.py:118 trac/wiki/model.py:127 trac/wiki/model.py:174
-#: trac/wiki/web_ui.py:119
+#: trac/wiki/admin.py:118 trac/wiki/model.py:128 trac/wiki/model.py:176
+#: trac/wiki/web_ui.py:120
 #, python-format
 msgid "Invalid Wiki page name '%(name)s'"
 msgstr ""
@@ -5849,7 +6148,7 @@
 msgid "Edits"
 msgstr ""
 
-#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:300
+#: trac/wiki/admin.py:203 trac/wiki/web_ui.py:310
 msgid "A new name is mandatory for a rename."
 msgstr ""
 
@@ -5857,7 +6156,7 @@
 msgid "The new name is invalid."
 msgstr ""
 
-#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:307
+#: trac/wiki/admin.py:208 trac/wiki/web_ui.py:317
 #, python-format
 msgid "The page %(name)s already exists."
 msgstr ""
@@ -5875,48 +6174,87 @@
 msgid "no permission to view this wiki page"
 msgstr ""
 
-#: trac/wiki/formatter.py:222
+#: trac/wiki/formatter.py:189
+#, python-format
+msgid "No macro or processor named '%(name)s' found"
+msgstr ""
+
+#: trac/wiki/formatter.py:224
 #, python-format
 msgid "HTML parsing error: %(message)s"
 msgstr ""
 
-#: trac/wiki/formatter.py:226
+#: trac/wiki/formatter.py:228
 msgid "Error: Forbidden character sequence \"--\" in htmlcomment wiki code block"
 msgstr ""
 
-#: trac/wiki/formatter.py:304
+#: trac/wiki/formatter.py:306
 #, python-format
 msgid "!#%(name)s must contain at most one table"
 msgstr ""
 
-#: trac/wiki/formatter.py:308
+#: trac/wiki/formatter.py:310
 #, python-format
 msgid "!#%(name)s must contain at least one table cell (and table cells only)"
 msgstr ""
 
-#: trac/wiki/formatter.py:684 trac/wiki/interwiki.py:104
+#: trac/wiki/formatter.py:355
+#, python-format
+msgid "Error: Failed to load processor %(name)s"
+msgstr ""
+
+#: trac/wiki/formatter.py:707 trac/wiki/interwiki.py:104
 #, python-format
 msgid "%(target)s in %(name)s"
 msgstr ""
 
-#: trac/wiki/intertrac.py:94
+#: trac/wiki/formatter.py:792
 #, python-format
-msgid "Can't view %(link)s:"
+msgid "Error: Macro %(name)s(%(args)s) failed"
 msgstr ""
 
-#: trac/wiki/intertrac.py:106
+#: trac/wiki/formatter.py:1185
+#, python-format
+msgid "Error: Processor %(name)s failed"
+msgstr ""
+
+#: trac/wiki/intertrac.py:94
+#, python-format
+msgid ""
+"Can't view %(link)s. Resource doesn't exist or you don't have the "
+"required permission."
+msgstr ""
+
+#: trac/wiki/intertrac.py:107
 msgid "Provide a list of known InterTrac prefixes."
 msgstr ""
 
-#: trac/wiki/intertrac.py:119
+#: trac/wiki/intertrac.py:120
 msgid "The Trac Project"
 msgstr ""
 
+#: trac/wiki/intertrac.py:128
+#, python-format
+msgid "Alias for %(name)s"
+msgstr ""
+
+#: trac/wiki/intertrac.py:138 trac/wiki/interwiki.py:175
+msgid "Prefix"
+msgstr ""
+
+#: trac/wiki/intertrac.py:139
+msgid "Trac Site"
+msgstr ""
+
 #: trac/wiki/interwiki.py:163
 msgid "Provide a description list for the known InterWiki prefixes."
 msgstr ""
 
-#: trac/wiki/macros.py:83
+#: trac/wiki/interwiki.py:176
+msgid "Site"
+msgstr ""
+
+#: trac/wiki/macros.py:85
 msgid ""
 "Insert an alphabetic list of all wiki pages into the output.\n"
 "\n"
@@ -5949,7 +6287,7 @@
 "The `include` and `exclude` lists accept shell-style patterns."
 msgstr ""
 
-#: trac/wiki/macros.py:296
+#: trac/wiki/macros.py:298
 msgid ""
 "List all pages that have recently been modified, ordered by the\n"
 "time they were last modified.\n"
@@ -5975,7 +6313,7 @@
 "e.g. `[[RecentChanges(,10,group=none)]]`."
 msgstr ""
 
-#: trac/wiki/macros.py:374
+#: trac/wiki/macros.py:378
 msgid ""
 "Display a structural outline of the current wiki page, each item in the\n"
 "outline being a link to the corresponding heading.\n"
@@ -6008,7 +6346,7 @@
 "   default). This parameter only has an effect in `inline` style."
 msgstr ""
 
-#: trac/wiki/macros.py:440
+#: trac/wiki/macros.py:444
 msgid ""
 "Embed an image in wiki-formatted text.\n"
 "\n"
@@ -6057,36 +6395,34 @@
 "\n"
 "Examples:\n"
 "{{{\n"
-"    [[Image(photo.jpg)]]                           # simplest\n"
-"    [[Image(photo.jpg, 120px)]]                    # with image width "
-"size\n"
-"    [[Image(photo.jpg, right)]]                    # aligned by keyword\n"
-"    [[Image(photo.jpg, nolink)]]                   # without link to "
-"source\n"
-"    [[Image(photo.jpg, align=right)]]              # aligned by attribute"
-"\n"
+"[[Image(photo.jpg)]]               # simplest\n"
+"[[Image(photo.jpg, 120px)]]        # with image width size\n"
+"[[Image(photo.jpg, right)]]        # aligned by keyword\n"
+"[[Image(photo.jpg, nolink)]]       # without link to source\n"
+"[[Image(photo.jpg, align=right)]]  # aligned by attribute\n"
 "}}}\n"
 "\n"
-"You can use image from other page, other ticket or other module.\n"
+"You can use an image from a wiki page, ticket or other module.\n"
 "{{{\n"
-"    [[Image(OtherPage:foo.bmp)]]    # if current module is wiki\n"
-"    [[Image(base/sub:bar.bmp)]]     # from hierarchical wiki page\n"
-"    [[Image(#3:baz.bmp)]]           # if in a ticket, point to #3\n"
-"    [[Image(ticket:36:boo.jpg)]]\n"
-"    [[Image(source:/images/bee.jpg)]] # straight from the repository!\n"
-"    [[Image(htdocs:foo/bar.png)]]   # image file in project htdocs dir.\n"
+"[[Image(OtherPage:foo.bmp)]]    # from a wiki page\n"
+"[[Image(base/sub:bar.bmp)]]     # from hierarchical wiki page\n"
+"[[Image(#3:baz.bmp)]]           # from another ticket\n"
+"[[Image(ticket:36:boo.jpg)]]    # from another ticket (long form)\n"
+"[[Image(source:/img/bee.jpg)]]  # from the repository\n"
+"[[Image(htdocs:foo/bar.png)]]   # from project htdocs dir\n"
+"[[Image(shared:foo/bar.png)]]   # from shared htdocs dir (since 1.0.2)\n"
 "}}}\n"
 "\n"
 "''Adapted from the Image.py macro created by Shun-ichi Goto\n"
 "<gotoh@taiyo.co.jp>''"
 msgstr ""
 
-#: trac/wiki/macros.py:648
+#: trac/wiki/macros.py:659
 #, python-format
 msgid "No image \"%(id)s\" attached to %(parent)s"
 msgstr ""
 
-#: trac/wiki/macros.py:664
+#: trac/wiki/macros.py:675
 msgid ""
 "Display a list of all installed Wiki macros, including documentation if\n"
 "available.\n"
@@ -6099,20 +6435,20 @@
 "macros if the `PythonOptimize` option is enabled for mod_python!"
 msgstr ""
 
-#: trac/wiki/macros.py:693
+#: trac/wiki/macros.py:704
 #, python-format
 msgid "Error: Can't get description for macro %(name)s"
 msgstr ""
 
-#: trac/wiki/macros.py:716
+#: trac/wiki/macros.py:727
 msgid "Aliases:"
 msgstr ""
 
-#: trac/wiki/macros.py:719
+#: trac/wiki/macros.py:730
 msgid "Sorry, no documentation found"
 msgstr ""
 
-#: trac/wiki/macros.py:726
+#: trac/wiki/macros.py:737
 msgid ""
 "Produce documentation for the Trac configuration file.\n"
 "\n"
@@ -6122,11 +6458,11 @@
 "options whose section and name start with the filters are output."
 msgstr ""
 
-#: trac/wiki/macros.py:775
+#: trac/wiki/macros.py:779
 msgid "(no default)"
 msgstr ""
 
-#: trac/wiki/macros.py:795
+#: trac/wiki/macros.py:800
 msgid ""
 "List all known mime-types which can be used as WikiProcessors.\n"
 "\n"
@@ -6134,11 +6470,11 @@
 "filter."
 msgstr ""
 
-#: trac/wiki/macros.py:818
+#: trac/wiki/macros.py:823
 msgid "MIME Types"
 msgstr ""
 
-#: trac/wiki/macros.py:833
+#: trac/wiki/macros.py:838
 msgid ""
 "Display a table of content for the Trac guide.\n"
 "\n"
@@ -6148,190 +6484,208 @@
 "a more customizable table of contents."
 msgstr ""
 
-#: trac/wiki/macros.py:879
+#: trac/wiki/macros.py:884
 msgid "Table of Contents"
 msgstr ""
 
-#: trac/wiki/model.py:132
+#: trac/wiki/model.py:88
+msgid "Cannot delete non-existent page"
+msgstr ""
+
+#: trac/wiki/model.py:133
 msgid "Page not modified"
 msgstr ""
 
-#: trac/wiki/model.py:181
+#: trac/wiki/model.py:173
+msgid "Cannot rename non-existent page"
+msgstr ""
+
+#: trac/wiki/model.py:183
 #, python-format
 msgid "Can't rename to existing %(name)s page."
 msgstr ""
 
-#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:754
+#: trac/wiki/web_ui.py:87 trac/wiki/web_ui.py:772
 msgid "Wiki"
 msgstr ""
 
-#: trac/wiki/web_ui.py:89
+#: trac/wiki/web_ui.py:90
 msgid "Help/Guide"
 msgstr ""
 
-#: trac/wiki/web_ui.py:130
+#: trac/wiki/web_ui.py:128 trac/wiki/web_ui.py:138
 #, python-format
 msgid "No version \"%(num)s\" for Wiki page \"%(name)s\""
 msgstr ""
 
-#: trac/wiki/web_ui.py:195
+#: trac/wiki/web_ui.py:204
 #, python-format
 msgid "The wiki page is too long (must be less than %(num)s characters)"
 msgstr ""
 
-#: trac/wiki/web_ui.py:205
+#: trac/wiki/web_ui.py:214
 #, python-format
 msgid "The Wiki page field '%(field)s' is invalid: %(message)s"
 msgstr ""
 
-#: trac/wiki/web_ui.py:209
+#: trac/wiki/web_ui.py:218
 #, python-format
 msgid "Invalid Wiki page: %(message)s"
 msgstr ""
 
 #. TRANSLATOR: wiki page
-#: trac/wiki/web_ui.py:236
+#: trac/wiki/web_ui.py:245
 msgid "currently edited"
 msgstr ""
 
-#: trac/wiki/web_ui.py:269
+#: trac/wiki/web_ui.py:279
 #, python-format
 msgid "The page %(name)s has been deleted."
 msgstr ""
 
-#: trac/wiki/web_ui.py:274
+#: trac/wiki/web_ui.py:284
 #, python-format
 msgid "The versions %(from_)d to %(to)d of the page %(name)s have been deleted."
 msgstr ""
 
-#: trac/wiki/web_ui.py:278
+#: trac/wiki/web_ui.py:288
 #, python-format
 msgid "The version %(version)d of the page %(name)s has been deleted."
 msgstr ""
 
-#: trac/wiki/web_ui.py:302
+#: trac/wiki/web_ui.py:312
 msgid ""
 "The new name is invalid (a name which is separated with slashes cannot be"
 " '.' or '..')."
 msgstr ""
 
-#: trac/wiki/web_ui.py:305
+#: trac/wiki/web_ui.py:315
 msgid "The new name must be different from the old name."
 msgstr ""
 
-#: trac/wiki/web_ui.py:316
+#: trac/wiki/web_ui.py:326
 #, python-format
 msgid "See [wiki:\"%(name)s\"]."
 msgstr ""
 
-#: trac/wiki/web_ui.py:340
+#: trac/wiki/web_ui.py:332
+#, python-format
+msgid "The page %(old_name)s has been renamed to %(new_name)s."
+msgstr ""
+
+#: trac/wiki/web_ui.py:336
+#, python-format
+msgid "The page %(old_name)s has been recreated with a redirect to %(new_name)s."
+msgstr ""
+
+#: trac/wiki/web_ui.py:358
 #, python-format
 msgid "Your changes have been saved in version %(version)s."
 msgstr ""
 
-#: trac/wiki/web_ui.py:345
+#: trac/wiki/web_ui.py:363
 msgid "Page not modified, showing latest version."
 msgstr ""
 
-#: trac/wiki/web_ui.py:399
+#: trac/wiki/web_ui.py:417
 #, python-format
 msgid "Version %(num)s of page \"%(name)s\" does not exist"
 msgstr ""
 
-#: trac/wiki/web_ui.py:451
+#: trac/wiki/web_ui.py:469
 msgid "Page history"
 msgstr ""
 
-#: trac/wiki/web_ui.py:469
+#: trac/wiki/web_ui.py:487
 msgid "Wiki History"
 msgstr ""
 
-#: trac/wiki/web_ui.py:499
+#: trac/wiki/web_ui.py:519
 #, python-format
 msgid "Reverted to version %(version)s."
 msgstr ""
 
-#: trac/wiki/web_ui.py:562
+#: trac/wiki/web_ui.py:584
 #, python-format
 msgid "Page %(name)s does not exist"
 msgstr ""
 
-#: trac/wiki/web_ui.py:576
+#: trac/wiki/web_ui.py:598
 #, python-format
 msgid "Back to %(wikipage)s"
 msgstr ""
 
-#: trac/wiki/web_ui.py:604
+#: trac/wiki/web_ui.py:623
 #, python-format
 msgid "Page %(name)s not found"
 msgstr ""
 
-#: trac/wiki/web_ui.py:658
+#: trac/wiki/web_ui.py:676
 msgid "View latest version"
 msgstr ""
 
-#: trac/wiki/web_ui.py:662
+#: trac/wiki/web_ui.py:680
 msgid "View parent page"
 msgstr ""
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:689
 msgid "Previous Version"
 msgstr ""
 
-#: trac/wiki/web_ui.py:671
+#: trac/wiki/web_ui.py:689
 msgid "Next Version"
 msgstr ""
 
-#: trac/wiki/web_ui.py:672
+#: trac/wiki/web_ui.py:690
 msgid "View Latest Version"
 msgstr ""
 
-#: trac/wiki/web_ui.py:675
+#: trac/wiki/web_ui.py:693
 msgid "Up"
 msgstr ""
 
-#: trac/wiki/web_ui.py:700
+#: trac/wiki/web_ui.py:718
 msgid "Start Page"
 msgstr ""
 
-#: trac/wiki/web_ui.py:701
+#: trac/wiki/web_ui.py:719
 msgid "Index"
 msgstr ""
 
-#: trac/wiki/web_ui.py:703
+#: trac/wiki/web_ui.py:721
 msgid "History"
 msgstr ""
 
-#: trac/wiki/web_ui.py:710
+#: trac/wiki/web_ui.py:728
 msgid "Wiki changes"
 msgstr ""
 
-#: trac/wiki/web_ui.py:737
+#: trac/wiki/web_ui.py:755
 #, python-format
 msgid "%(page)s edited"
 msgstr ""
 
-#: trac/wiki/web_ui.py:739
+#: trac/wiki/web_ui.py:757
 #, python-format
 msgid "%(page)s created"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:18
+#: trac/wiki/templates/wiki_delete.html:28
 #, python-format
 msgid "Delete versions %(from)s to %(to)s of [1:%(name)s]"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:23
+#: trac/wiki/templates/wiki_delete.html:33
 #, python-format
 msgid "Delete version %(version)s of [1:%(name)s]"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:28
+#: trac/wiki/templates/wiki_delete.html:38
 #, python-format
 msgid "Delete [1:%(name)s]"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:38
+#: trac/wiki/templates/wiki_delete.html:48
 #, python-format
 msgid ""
 "[1:\n"
@@ -6345,12 +6699,12 @@
 "modified %(last_modified)s."
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:50
+#: trac/wiki/templates/wiki_delete.html:60
 #, python-format
 msgid "Are you sure you want to delete version %(version)s of this page?"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:57
+#: trac/wiki/templates/wiki_delete.html:67
 #, python-format
 msgid ""
 "This is the only [1:\n"
@@ -6359,16 +6713,16 @@
 "completely!"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:64
+#: trac/wiki/templates/wiki_delete.html:74
 #, python-format
 msgid "Modified %(modified)s."
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:71
+#: trac/wiki/templates/wiki_delete.html:81
 msgid "Are you sure you want to completely delete this page?"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:77
+#: trac/wiki/templates/wiki_delete.html:87
 #, python-format
 msgid ""
 "Removing the one and only [1:\n"
@@ -6376,7 +6730,7 @@
 "%(created)s."
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:83
+#: trac/wiki/templates/wiki_delete.html:93
 #, python-format
 msgid ""
 "Removing all [1:\n"
@@ -6385,240 +6739,240 @@
 "%(modified)s."
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:99
+#: trac/wiki/templates/wiki_delete.html:108
 msgid "Delete those versions"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:99
-#: trac/wiki/templates/wiki_view.html:125
+#: trac/wiki/templates/wiki_delete.html:108
+#: trac/wiki/templates/wiki_view.html:136
 msgid "Delete this version"
 msgstr ""
 
-#: trac/wiki/templates/wiki_delete.html:99
-#: trac/wiki/templates/wiki_view.html:127
+#: trac/wiki/templates/wiki_delete.html:108
+#: trac/wiki/templates/wiki_view.html:138
 msgid "Delete page"
 msgstr ""
 
-#: trac/wiki/templates/wiki_diff.html:17
+#: trac/wiki/templates/wiki_diff.html:27
 #, python-format
 msgid "Delete version %(old_version)d to version %(version)d"
 msgstr ""
 
-#: trac/wiki/templates/wiki_diff.html:18
+#: trac/wiki/templates/wiki_diff.html:28
 #, python-format
 msgid "Delete version %(version)d"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:94
+#: trac/wiki/templates/wiki_edit.html:101
 msgid "See the diffs"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:94
+#: trac/wiki/templates/wiki_edit.html:101
 msgid "Review"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:95
+#: trac/wiki/templates/wiki_edit.html:102
 msgid "See the preview"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:98
+#: trac/wiki/templates/wiki_edit.html:105
 #, python-format
 msgid "Editing %(name)s"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:100
+#: trac/wiki/templates/wiki_edit.html:133
+msgid ""
+"Sorry, this page has been modified by somebody else since you started\n"
+"            editing. Your changes cannot be saved."
+msgstr ""
+
+#: trac/wiki/templates/wiki_edit.html:140
 msgid "Someone else has modified that page since you started your edits."
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:101
+#: trac/wiki/templates/wiki_edit.html:141
 msgid ""
 "[1:If you save right away, you risk to revert those changes]\n"
-"        (highlighted below as deletions)."
+"            (highlighted below as deletions)."
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:103
+#: trac/wiki/templates/wiki_edit.html:143
 msgid ""
 "Please review all those changes and manually merge them with your\n"
-"        own changes. [1:]\n"
-"        If you're unsure about what you're doing, please press [2:Cancel]"
-"\n"
-"        (losing your changes) and start editing the latest version of the"
-" page\n"
-"        again."
+"            own changes. [1:]\n"
+"            If you're unsure about what you're doing, please press "
+"[2:Cancel]\n"
+"            (losing your changes) and start editing the latest version of"
+" the page\n"
+"            again."
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:139
+#: trac/wiki/templates/wiki_edit.html:152
 #, python-format
 msgid ""
 "Change information for future version %(version)s (modified by "
 "%(author)s):"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:149
+#: trac/wiki/templates/wiki_edit.html:162
 msgid "Go to the editor"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:152
-#: trac/wiki/templates/wiki_edit_form.html:70
+#: trac/wiki/templates/wiki_edit.html:165
+#: trac/wiki/templates/wiki_edit_form.html:81
 msgid "Review Changes"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:154
+#: trac/wiki/templates/wiki_edit.html:167
 msgid "No changes"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:165
+#: trac/wiki/templates/wiki_edit.html:178
 msgid "Go to Save, Preview, Review or Cancel buttons"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:166
+#: trac/wiki/templates/wiki_edit.html:179
 msgid "Actions"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit.html:171
-msgid ""
-"Sorry, this page has been modified by somebody else since you started\n"
-"            editing. Your changes cannot be saved."
-msgstr ""
-
-#: trac/wiki/templates/wiki_edit_form.html:16
+#: trac/wiki/templates/wiki_edit_form.html:26
 msgid "Adjust edit area height:"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:24
+#: trac/wiki/templates/wiki_edit_form.html:34
 msgid "Selecting and pressing 'Preview' enters a two-column [edit|preview] mode"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:24
+#: trac/wiki/templates/wiki_edit_form.html:34
 msgid "Edit side-by-side"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:33
+#: trac/wiki/templates/wiki_edit_form.html:44
 msgid ""
 "[1:Note:] See [2:WikiFormatting] and\n"
 "        [3:TracWiki] for help on editing wiki content."
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:39
+#: trac/wiki/templates/wiki_edit_form.html:50
 msgid "Change information"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:50
+#: trac/wiki/templates/wiki_edit_form.html:61
 msgid "Comment about this change (optional):"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:57
+#: trac/wiki/templates/wiki_edit_form.html:68
 msgid "Page is read-only"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:65
+#: trac/wiki/templates/wiki_edit_form.html:76
 msgid "Merge changes"
 msgstr ""
 
-#: trac/wiki/templates/wiki_edit_form.html:69
+#: trac/wiki/templates/wiki_edit_form.html:80
 msgid "Preview Page"
 msgstr ""
 
-#: trac/wiki/templates/wiki_page_path.html:6
+#: trac/wiki/templates/wiki_page_path.html:16
 msgid "View WikiStart"
 msgstr ""
 
-#: trac/wiki/templates/wiki_page_path.html:6
+#: trac/wiki/templates/wiki_page_path.html:16
 msgid "wiki:"
 msgstr ""
 
-#: trac/wiki/templates/wiki_page_path.html:8
+#: trac/wiki/templates/wiki_page_path.html:18
 #, python-format
 msgid "View %(path)s"
 msgstr ""
 
-#: trac/wiki/templates/wiki_rename.html:15
+#: trac/wiki/templates/wiki_rename.html:25
 #, python-format
 msgid "Rename [1:%(name)s]"
 msgstr ""
 
-#: trac/wiki/templates/wiki_rename.html:19
+#: trac/wiki/templates/wiki_rename.html:29
 msgid "Renaming the page will rename all existing versions of the page in place."
 msgstr ""
 
-#: trac/wiki/templates/wiki_rename.html:19
+#: trac/wiki/templates/wiki_rename.html:29
 msgid "The complete history of the page will be moved to the new location."
 msgstr ""
 
-#: trac/wiki/templates/wiki_rename.html:23
+#: trac/wiki/templates/wiki_rename.html:33
 msgid "New name:"
 msgstr ""
 
-#: trac/wiki/templates/wiki_rename.html:27
+#: trac/wiki/templates/wiki_rename.html:37
 msgid "Leave a redirection page at the old location"
 msgstr ""
 
-#: trac/wiki/templates/wiki_rename.html:33
-#: trac/wiki/templates/wiki_view.html:117
+#: trac/wiki/templates/wiki_rename.html:42
+#: trac/wiki/templates/wiki_view.html:128
 msgid "Rename page"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:15
+#: trac/wiki/templates/wiki_view.html:26
 msgid "Revert page to this version"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:15 trac/wiki/templates/wiki_view.html:93
+#: trac/wiki/templates/wiki_view.html:26 trac/wiki/templates/wiki_view.html:104
 msgid "Edit this page"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:36
+#: trac/wiki/templates/wiki_view.html:47
 #, python-format
 msgid ""
 "Version %(version)s (modified by %(author)s, %(date)s)\n"
 "               ([1:diff])"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:50
+#: trac/wiki/templates/wiki_view.html:61
 #, python-format
 msgid "Version %(version)s by %(author)s: %(comment)s"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:50
+#: trac/wiki/templates/wiki_view.html:61
 #, python-format
 msgid "Version %(version)s by %(author)s"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:58
+#: trac/wiki/templates/wiki_view.html:69
 #, python-format
 msgid "[1:Last modified] %(reldate)s"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:62
+#: trac/wiki/templates/wiki_view.html:73
 #, python-format
 msgid "Last modified on %(date)s"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:66
+#: trac/wiki/templates/wiki_view.html:77
 #, python-format
 msgid "The page %(name)s does not exist. You can create it here."
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:68
+#: trac/wiki/templates/wiki_view.html:79
 msgid "You could also create the same page higher in the hierarchy:"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:90
+#: trac/wiki/templates/wiki_view.html:101
 msgid "Revert to this version"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:96
+#: trac/wiki/templates/wiki_view.html:107
 msgid "Create this page"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:98
+#: trac/wiki/templates/wiki_view.html:109
 msgid "Using the template:"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:101
+#: trac/wiki/templates/wiki_view.html:112
 msgid "(blank page)"
 msgstr ""
 
-#: trac/wiki/templates/wiki_view.html:135
+#: trac/wiki/templates/wiki_view.html:146
 msgid "The following pages have a name similar to this page, and may be related:"
 msgstr ""
 
diff --git a/trac/trac/locale/nb/LC_MESSAGES/messages-js.po b/trac/trac/locale/nb/LC_MESSAGES/messages-js.po
index 22e7648..c35a16b 100644
--- a/trac/trac/locale/nb/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/nb/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/nb/LC_MESSAGES/messages.po b/trac/trac/locale/nb/LC_MESSAGES/messages.po
index f5de49e..d9440e7 100644
--- a/trac/trac/locale/nb/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/nb/LC_MESSAGES/messages.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -7008,3 +7008,8 @@
 msgid "The following pages have a name similar to this page, and may be related:"
 msgstr "Følgende sider har navn som ligner denne siden, og kan være relatert:"
 
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/nl/LC_MESSAGES/messages.po b/trac/trac/locale/nl/LC_MESSAGES/messages.po
index 445aad9..474c84c 100644
--- a/trac/trac/locale/nl/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/nl/LC_MESSAGES/messages.po
@@ -11,7 +11,7 @@
 msgstr ""
 "Project-Id-Version:  Trac\n"
 "Report-Msgid-Bugs-To: http://trac.edgewall.org/\n"
-"POT-Creation-Date: 2012-10-29 19:08+0100\n"
+"POT-Creation-Date: 2013-02-17 15:58+0100\n"
 "PO-Revision-Date: 2013-01-27 17:56+0000\n"
 "Last-Translator: Pander <pander@users.sourceforge.net>\n"
 "Language-Team: Dutch "
@@ -764,7 +764,7 @@
 msgstr ""
 "Welkom in trac-admin %(version)s\n"
 "Interactieve Trac-administratieomgeving.\n"
-"Copyright (c) 2003-2010 Edgewall Software\n"
+"Copyright (c) 2003-2013 Edgewall Software\n"
 "\n"
 "Type:  '?' of 'help' voor hulp bij commando’s.\n"
 "        "
@@ -2162,7 +2162,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2010\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
diff --git a/trac/trac/locale/pl/LC_MESSAGES/messages.po b/trac/trac/locale/pl/LC_MESSAGES/messages.po
index 78fdca2..83adb55 100644
--- a/trac/trac/locale/pl/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/pl/LC_MESSAGES/messages.po
@@ -19,7 +19,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -7042,3 +7042,8 @@
 "Następujące strony mają nazwę podobną do tej strony i mogą zostać "
 "powiązane:"
 
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/pt/LC_MESSAGES/messages.po b/trac/trac/locale/pt/LC_MESSAGES/messages.po
index 011371c..f155a8d 100644
--- a/trac/trac/locale/pt/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/pt/LC_MESSAGES/messages.po
@@ -17,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -6877,3 +6877,17 @@
 msgid "The following pages have a name similar to this page, and may be related:"
 msgstr ""
 
+#~ msgid ""
+#~ "Welcome to trac-admin %(version)s\n"
+#~ "Interactive Trac administration console.\n"
+#~ "Copyright (C) 2003-2012 Edgewall Software\n"
+#~ "\n"
+#~ "Type:  '?' or 'help' for help on commands.\n"
+#~ "        "
+#~ msgstr ""
+
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/pt_BR/LC_MESSAGES/messages-js.po b/trac/trac/locale/pt_BR/LC_MESSAGES/messages-js.po
index 8e886a3..45ce969 100644
--- a/trac/trac/locale/pt_BR/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/pt_BR/LC_MESSAGES/messages-js.po
@@ -1,8 +1,9 @@
 # Portuguese (Brazil) translations for Trac.
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2012 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# Leslie Harlley Watter <leslie@watter.org>, 2010.
 #
+# Translators:
+# Leslie Harlley Watter <leslie@watter.org>, 2010.
 msgid ""
 msgstr ""
 "Project-Id-Version: Trac 0.12\n"
@@ -15,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/pt_BR/LC_MESSAGES/messages.po b/trac/trac/locale/pt_BR/LC_MESSAGES/messages.po
index 609bb90..c9bc890 100644
--- a/trac/trac/locale/pt_BR/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/pt_BR/LC_MESSAGES/messages.po
@@ -1,8 +1,10 @@
 # Portuguese (Brazil) translations for Trac.
-# Copyright (C) 2007-2008 Edgewall Software
+# Copyright (C) 2012 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>, 2007.
 #
+# Translators:
+# Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>, 2007.
+# Manuel  <mfcn2000@gmail.com>, 2012.
 msgid ""
 msgstr ""
 "Project-Id-Version: Trac 0.12\n"
@@ -15,7 +17,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -786,7 +788,7 @@
 msgstr ""
 "Bem vindo ao trac-admin versão %(version)s\n"
 "Console de Administração Interativa do Trac \n"
-"Copyright (C) 2003-2012 Edgewall Software\n"
+"Copyright (C) 2003-2013 Edgewall Software\n"
 "\n"
 "Digite:  '?' ou 'help' para ajuda nos comandos.\n"
 "        "
@@ -2193,12 +2195,11 @@
 "        [1:http://trac.edgewall.org/]"
 
 #: trac/templates/about.html:46
-#, fuzzy
 msgid ""
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2012\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
@@ -2698,7 +2699,7 @@
 msgstr "Visualizar anexo"
 
 #: trac/templates/list_of_attachments.html:18
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "[1:%(file)s][2:​]\n"
 "       ([3:%(size)s]) -\n"
@@ -4732,7 +4733,7 @@
 msgstr[1] "seguem:"
 
 #: trac/ticket/templates/ticket_change.html:53
-#, fuzzy, python-format
+#, python-format
 msgid "Changed %(date)s by %(author)s"
 msgstr "Modificado %(date)s atrás por %(author)s"
 
@@ -7574,3 +7575,17 @@
 "As seguintes páginas tem um nome similar a esta, e podem estar "
 "relacionadas com ela:"
 
+#~ msgid ""
+#~ "Welcome to trac-admin %(version)s\n"
+#~ "Interactive Trac administration console.\n"
+#~ "Copyright (C) 2003-2012 Edgewall Software\n"
+#~ "\n"
+#~ "Type:  '?' or 'help' for help on commands.\n"
+#~ "        "
+#~ msgstr ""
+
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/ro/LC_MESSAGES/messages.po b/trac/trac/locale/ro/LC_MESSAGES/messages.po
index 39f7ede..27e2117 100644
--- a/trac/trac/locale/ro/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/ro/LC_MESSAGES/messages.po
@@ -16,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -6869,3 +6869,8 @@
 msgid "The following pages have a name similar to this page, and may be related:"
 msgstr ""
 
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/ru/LC_MESSAGES/messages-js.po b/trac/trac/locale/ru/LC_MESSAGES/messages-js.po
index 6bd95ea..44e209b 100644
--- a/trac/trac/locale/ru/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/ru/LC_MESSAGES/messages-js.po
@@ -16,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/ru/LC_MESSAGES/messages.po b/trac/trac/locale/ru/LC_MESSAGES/messages.po
index ecb2ae1..d5916be 100644
--- a/trac/trac/locale/ru/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/ru/LC_MESSAGES/messages.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -762,7 +762,7 @@
 msgstr ""
 "Добро пожаловать в trac-admin %(version)s\n"
 "Интерактивная консоль администрирования Trac.\n"
-"Авторские права (c) 2003-2012 Edgewall Software\n"
+"Авторские права (c) 2003-2013 Edgewall Software\n"
 "\n"
 "Введите:  \"?\" или \"help\" для получения справки по командам.\n"
 "        "
@@ -2154,7 +2154,7 @@
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Авторские права © 2003-2012\n"
+"Авторские права © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
diff --git a/trac/trac/locale/sl/LC_MESSAGES/messages-js.po b/trac/trac/locale/sl/LC_MESSAGES/messages-js.po
index 84172fa..61d7556 100644
--- a/trac/trac/locale/sl/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/sl/LC_MESSAGES/messages-js.po
@@ -16,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/sl/LC_MESSAGES/messages.po b/trac/trac/locale/sl/LC_MESSAGES/messages.po
index a1d5450..96bca0f 100644
--- a/trac/trac/locale/sl/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/sl/LC_MESSAGES/messages.po
@@ -16,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
diff --git a/trac/trac/locale/sv/LC_MESSAGES/messages-js.po b/trac/trac/locale/sv/LC_MESSAGES/messages-js.po
index 076bcd7..97dde5d 100644
--- a/trac/trac/locale/sv/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/sv/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/tr/LC_MESSAGES/messages-js.po b/trac/trac/locale/tr/LC_MESSAGES/messages-js.po
index 95039c7..8141dad 100644
--- a/trac/trac/locale/tr/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/tr/LC_MESSAGES/messages-js.po
@@ -1,8 +1,9 @@
 # Turkish translations for Trac.
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2012 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
 #
+# Translators:
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
 msgid ""
 msgstr ""
 "Project-Id-Version: Trac 0.12\n"
@@ -15,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/tr/LC_MESSAGES/messages.po b/trac/trac/locale/tr/LC_MESSAGES/messages.po
index 07e29bc..e25504a 100644
--- a/trac/trac/locale/tr/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/tr/LC_MESSAGES/messages.po
@@ -1,8 +1,9 @@
-# Turkish (Turkey) translations for Trac.
-# Copyright (C) 2008-2009 Edgewall Software
+# Turkish translations for Trac.
+# Copyright (C) 2012 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>, 2008.
 #
+# Translators:
+# Jeroen Ruigrok van der Werven <asmodai@in-nomine.org>, 2008.
 msgid ""
 msgstr ""
 "Project-Id-Version: Trac 0.12\n"
@@ -15,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -283,11 +284,9 @@
 msgstr "Ek dosya silinemiyor."
 
 #: trac/attachment.py:253
-#, fuzzy, python-format
+#, python-format
 msgid "Cannot reparent attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
 msgstr ""
-"Ek sahibi değiştirilemiyor \"%(att)s\" , zaten %(realm)s:%(id)s üzerinde "
-"mevcut."
 
 #: trac/attachment.py:258
 #, python-format
@@ -304,11 +303,9 @@
 msgstr "Ek %(name)s sahibi değiştirilemiyor"
 
 #: trac/attachment.py:313
-#, fuzzy, python-format
+#, python-format
 msgid "Cannot create attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
 msgstr ""
-"Ek sahibi değiştirilemiyor \"%(att)s\" , zaten %(realm)s:%(id)s üzerinde "
-"mevcut."
 
 #: trac/attachment.py:396
 #, python-format
@@ -758,12 +755,6 @@
 "Type:  '?' or 'help' for help on commands.\n"
 "        "
 msgstr ""
-"Hoşgeldiniz, trac-admin %(version)s\n"
-"İnteraktif Trac yönetim konsolu.\n"
-"Copyright (c) 2003-2010 Edgewall Software\n"
-"\n"
-"Yazın:  '?' veya 'help' ile komutlar hakkında yardım için.\n"
-"        "
 
 #: trac/admin/console.py:166
 #, python-format
@@ -780,7 +771,7 @@
 msgid ""
 "No documentation found for '%(cmd)s'. Use 'help' to see the list of "
 "commands."
-msgstr "'%(cmd)s' için döküman bulunamadı"
+msgstr ""
 
 #: trac/admin/console.py:322
 msgid "Did you mean this?"
@@ -1624,9 +1615,9 @@
 msgstr "Hedef dosya oluşturulmadı"
 
 #: trac/db/pool.py:130
-#, fuzzy, python-format
+#, python-format
 msgid "Unable to get database connection within %(time)d seconds."
-msgstr " %(time)d saniye boyunca veritabanı bağlantısı kurulamadı"
+msgstr ""
 
 #: trac/db/postgres_backend.py:81
 msgid "Cannot load Python bindings for PostgreSQL"
@@ -1715,7 +1706,7 @@
 msgstr "Seçenekleriniz kaydedildi."
 
 #: trac/mimeview/rst.py:125 trac/mimeview/rst.py:148
-#, fuzzy, python-format
+#, python-format
 msgid "%(link)s is not a valid TracLink"
 msgstr ""
 
@@ -1748,9 +1739,8 @@
 msgstr "Gelişmiş"
 
 #: trac/prefs/web_ui.py:167
-#, fuzzy
 msgid "The session has been loaded."
-msgstr "Sürüm \"%(name)s\" eklendi."
+msgstr ""
 
 #: trac/prefs/templates/prefs.html:10
 msgid "Preferences:"
@@ -1778,9 +1768,8 @@
 msgstr "Oturum anahtarı:"
 
 #: trac/prefs/templates/prefs_advanced.html:17
-#, fuzzy
 msgid "Change"
-msgstr "değişiklik"
+msgstr ""
 
 #: trac/prefs/templates/prefs_advanced.html:18
 msgid ""
@@ -1961,6 +1950,11 @@
 "      [1:TracAccessibility]\n"
 "      for more information on access keys."
 msgstr ""
+"Bu site bazı fonksiyonlarına erişimini kısayol \n"
+"      tuşlarıyla yapılabilmesin sağlar. Ancak \n"
+"      bu kısayollar masa üstü ve diğer programların \n"
+"      kısayolları ile çakışa bildiğinden başlangıçta devre\n"
+"      dışıdır. Bakınız [1:TracAccessibility]."
 
 #: trac/prefs/templates/prefs_language.html:15
 msgid "Language:"
@@ -2128,13 +2122,10 @@
 "        [1:http://trac.edgewall.org/]"
 
 #: trac/templates/about.html:46
-#, fuzzy
 msgid ""
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2010\n"
-"        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
 msgid "System Information"
@@ -2630,16 +2621,12 @@
 msgstr "Ek dosyayı görüntüle"
 
 #: trac/templates/list_of_attachments.html:18
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "[1:%(file)s][2:​]\n"
 "       ([3:%(size)s]) -\n"
 "      added by [4:%(author)s] %(date)s."
 msgstr ""
-"[1:%(file)s]\n"
-"      [2:[3:]]\n"
-"       ([4:%(size)s]) -\n"
-"      eklendi [5:%(author)s] tarafından %(date)s önce."
 
 #: trac/templates/list_of_attachments.html:28
 #: trac/templates/list_of_attachments.html:44
@@ -2935,9 +2922,8 @@
 msgstr "Bilet #%(num)s ve tüm ilgili veri silindi."
 
 #: trac/ticket/api.py:257
-#, fuzzy
 msgid "Attachment"
-msgstr "Ek Dosyalar"
+msgstr ""
 
 #: trac/ticket/api.py:287
 msgid "Summary"
@@ -4535,7 +4521,7 @@
 msgstr[0] "takip:"
 
 #: trac/ticket/templates/ticket_change.html:53
-#, fuzzy, python-format
+#, python-format
 msgid "Changed %(date)s by %(author)s"
 msgstr "Değiştirildi %(date)s önce %(author)s tarafından"
 
@@ -4560,9 +4546,6 @@
 "[1:[2:%(name)s]][3:​]\n"
 "          added"
 msgstr ""
-"[1:[2:%(name)s]]\n"
-"          [3:[4:]]\n"
-"          eklendi"
 
 #: trac/ticket/templates/ticket_change.html:88
 #, python-format
@@ -5815,9 +5798,8 @@
 msgstr "Dosyayı notlar olmadan görüntüle"
 
 #: trac/versioncontrol/web_ui/browser.py:469
-#, fuzzy
 msgid "Blame"
-msgstr "İsim"
+msgstr ""
 
 #: trac/versioncontrol/web_ui/browser.py:470
 msgid ""
@@ -6175,9 +6157,9 @@
 "%(traceback)s}}}"
 
 #: trac/web/session.py:245
-#, fuzzy, python-format
+#, python-format
 msgid "Session '%(id)s' already exists. Please choose a different session ID."
-msgstr "Oturum '%(id)s' zaten mevcut.<br />Lütfen farklı bir oturum ID seçin."
+msgstr ""
 
 #: trac/web/session.py:248
 msgid "Error renaming session"
@@ -6188,21 +6170,19 @@
 msgstr "SID"
 
 #: trac/web/session.py:417
-#, fuzzy
 msgid "Auth"
-msgstr "yol"
+msgstr ""
 
 #: trac/web/session.py:417
-#, fuzzy
 msgid "Last Visit"
-msgstr "Son Revizyon"
+msgstr ""
 
 #: trac/web/session.py:418
 msgid "Email"
 msgstr "Eposta"
 
 #: trac/web/session.py:427
-#, fuzzy, python-format
+#, python-format
 msgid "Session '%(sid)s' already exists"
 msgstr ""
 
@@ -6212,18 +6192,18 @@
 msgstr ""
 
 #: trac/web/session.py:445
-#, fuzzy, python-format
+#, python-format
 msgid "Session '%(sid)s' not found"
-msgstr "Oturum idsi %(sid)s bulunamadı"
+msgstr ""
 
 #: trac/wiki/admin.py:113
-#, fuzzy, python-format
+#, python-format
 msgid "Page '%(page)s' not found"
 msgstr ""
 
 #: trac/wiki/admin.py:118 trac/wiki/model.py:127 trac/wiki/model.py:174
 #: trac/wiki/web_ui.py:119
-#, fuzzy, python-format
+#, python-format
 msgid "Invalid Wiki page name '%(name)s'"
 msgstr ""
 
@@ -6311,9 +6291,8 @@
 msgstr ""
 
 #: trac/wiki/intertrac.py:119
-#, fuzzy
 msgid "The Trac Project"
-msgstr "Ara %(project)s"
+msgstr ""
 
 #: trac/wiki/interwiki.py:163
 msgid "Provide a description list for the known InterWiki prefixes."
@@ -7011,7 +6990,7 @@
 msgstr "[1:Son Değişiklik] %(reldate)s önce"
 
 #: trac/wiki/templates/wiki_view.html:62
-#, fuzzy, python-format
+#, python-format
 msgid "Last modified on %(date)s"
 msgstr ""
 
@@ -7045,3 +7024,17 @@
 msgid "The following pages have a name similar to this page, and may be related:"
 msgstr "Bu sayfalar, mevcut sayfaya benzer isimler taşıyor, ilgili olabilir:"
 
+#~ msgid ""
+#~ "Welcome to trac-admin %(version)s\n"
+#~ "Interactive Trac administration console.\n"
+#~ "Copyright (C) 2003-2012 Edgewall Software\n"
+#~ "\n"
+#~ "Type:  '?' or 'help' for help on commands.\n"
+#~ "        "
+#~ msgstr ""
+
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/tracini.pot b/trac/trac/locale/tracini.pot
index 0ad6d57..c97d2f3 100644
--- a/trac/trac/locale/tracini.pot
+++ b/trac/trac/locale/tracini.pot
@@ -1,14 +1,14 @@
 # Translations template for Trac.
-# Copyright (C) 2013 Edgewall Software
+# Copyright (C) 2014 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2013.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
 #
 #, fuzzy
 msgid ""
 msgstr ""
-"Project-Id-Version: Trac 1.0.1\n"
+"Project-Id-Version: Trac 1.0.2\n"
 "Report-Msgid-Bugs-To: trac-dev@googlegroups.com\n"
-"POT-Creation-Date: 2013-01-27 11:21+0900\n"
+"POT-Creation-Date: 2014-09-01 12:43+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -51,11 +51,11 @@
 "(''since 0.10'')."
 msgstr ""
 
-#: tracopt/perm/authz_policy.py:132
+#: tracopt/perm/authz_policy.py:134
 msgid "Location of authz policy configuration file."
 msgstr ""
 
-#: tracopt/perm/config_perm_provider.py:24
+#: tracopt/perm/config_perm_provider.py:27
 msgid ""
 "This section provides a way to add arbitrary permissions to a\n"
 "Trac environment. This can be useful for adding new permissions to use\n"
@@ -66,17 +66,21 @@
 "and a comma-separated list of permissions. For example:\n"
 "{{{\n"
 "[extra-permissions]\n"
-"extra_admin = extra_view, extra_modify, extra_delete\n"
+"EXTRA_ADMIN = EXTRA_VIEW, EXTRA_MODIFY, EXTRA_DELETE\n"
 "}}}\n"
 "This entry will define three new permissions `EXTRA_VIEW`,\n"
 "`EXTRA_MODIFY` and `EXTRA_DELETE`, as well as a meta-permissions\n"
 "`EXTRA_ADMIN` that grants all three permissions.\n"
 "\n"
+"The permissions are created in upper-case characters regardless of\n"
+"the casing of the definitions in `trac.ini`. For example, the\n"
+"definition `extra_view` would create the permission `EXTRA_VIEW`.\n"
+"\n"
 "If you don't want a meta-permission, start the meta-name with an\n"
 "underscore (`_`):\n"
 "{{{\n"
 "[extra-permissions]\n"
-"_perms = extra_view, extra_modify\n"
+"_perms = EXTRA_VIEW, EXTRA_MODIFY\n"
 "}}}"
 msgstr ""
 
@@ -113,68 +117,68 @@
 msgid "Send ticket change notification when updating a ticket."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:169
+#: tracopt/versioncontrol/git/git_fs.py:269
 msgid "Enable persistent caching of commit tree."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:172
+#: tracopt/versioncontrol/git/git_fs.py:272
 msgid "Wrap `GitRepository` in `CachedRepository`."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:175
+#: tracopt/versioncontrol/git/git_fs.py:275
 msgid ""
 "The length at which a sha1 should be abbreviated to (must\n"
 "be >= 4 and <= 40)."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:180
+#: tracopt/versioncontrol/git/git_fs.py:280
 msgid ""
 "The minimum length of an hex-string for which\n"
 "auto-detection as sha1 is performed (must be >= 4 and <= 40)."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:185
+#: tracopt/versioncontrol/git/git_fs.py:285
 msgid ""
 "Enable reverse mapping of git email addresses to trac user ids\n"
 "(costly if you have many users)."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:189
+#: tracopt/versioncontrol/git/git_fs.py:289
 msgid ""
 "Use git-committer id instead of git-author id for the\n"
 "changeset ''Author'' field."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:194
+#: tracopt/versioncontrol/git/git_fs.py:294
 msgid ""
 "Use git-committer timestamp instead of git-author timestamp\n"
 "for the changeset ''Timestamp'' field."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:199
+#: tracopt/versioncontrol/git/git_fs.py:299
 msgid "Define charset encoding of paths within git repositories."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:202
+#: tracopt/versioncontrol/git/git_fs.py:302
 msgid "Path to the git executable."
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:749
+#: tracopt/versioncontrol/git/git_fs.py:879
 msgid "Path to a gitweb-formatted projects.list"
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:752
+#: tracopt/versioncontrol/git/git_fs.py:882
 msgid "Path to the base of your git projects"
 msgstr ""
 
-#: tracopt/versioncontrol/git/git_fs.py:755
+#: tracopt/versioncontrol/git/git_fs.py:885
 #, python-format
 msgid ""
 "Template for project URLs. %s will be replaced with the repo\n"
 "name"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_fs.py:253
+#: tracopt/versioncontrol/svn/svn_fs.py:265
 msgid ""
 "Comma separated list of paths categorized as branches.\n"
 "If a path ends with '*', then all the directory entries found below\n"
@@ -182,7 +186,7 @@
 "Example: `/trunk, /branches/*, /projectAlpha/trunk, /sandbox/*`"
 msgstr ""
 
-#: tracopt/versioncontrol/svn/svn_fs.py:260
+#: tracopt/versioncontrol/svn/svn_fs.py:272
 msgid ""
 "Comma separated list of paths categorized as tags.\n"
 "\n"
@@ -191,6 +195,18 @@
 "Example: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1`"
 msgstr ""
 
+#: tracopt/versioncontrol/svn/svn_fs.py:280
+msgid ""
+"End-of-Line character sequences when `svn:eol-style` property is\n"
+"`native`.\n"
+"\n"
+"If `native` (the default), substitute with the native EOL marker on\n"
+"the server. Otherwise, if `LF`, `CRLF` or `CR`, substitute with the\n"
+"specified EOL marker.\n"
+"\n"
+"(''since 1.0.2'')"
+msgstr ""
+
 #: tracopt/versioncontrol/svn/svn_prop.py:37
 msgid ""
 "The TracBrowser for Subversion can interpret the `svn:externals`\n"
@@ -236,20 +252,18 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/attachment.py:430
-msgid ""
-"Maximum allowed file size (in bytes) for ticket and wiki\n"
-"attachments."
+#: trac/attachment.py:438
+msgid "Maximum allowed file size (in bytes) for attachments."
 msgstr ""
 
-#: trac/attachment.py:434
+#: trac/attachment.py:441
 msgid ""
 "Maximum allowed total size (in bytes) for an attachment list to be\n"
 "downloadable as a `.zip`. Set this to -1 to disable download as `.zip`.\n"
 "(''since 1.0'')"
 msgstr ""
 
-#: trac/attachment.py:439
+#: trac/attachment.py:446
 msgid ""
 "Whether attachments should be rendered in the browser, or\n"
 "only made downloadable.\n"
@@ -262,7 +276,7 @@
 "recommended to leave this option disabled (which is the default)."
 msgstr ""
 
-#: trac/env.py:123
+#: trac/env.py:124
 msgid ""
 "This section is used to enable or disable components\n"
 "provided by plugins, as well as by Trac itself. The component\n"
@@ -298,7 +312,7 @@
 "See also: TracPlugins"
 msgstr ""
 
-#: trac/env.py:158
+#: trac/env.py:159
 msgid ""
 "Path to the //shared plugins directory//.\n"
 "\n"
@@ -309,7 +323,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/env.py:167
+#: trac/env.py:168
 msgid ""
 "Reference URL for the Trac deployment.\n"
 "\n"
@@ -319,7 +333,7 @@
 "resources in notification e-mails."
 msgstr ""
 
-#: trac/env.py:175
+#: trac/env.py:176
 msgid ""
 "Optionally use `[trac] base_url` for redirects.\n"
 "\n"
@@ -332,7 +346,7 @@
 "as redirects are frequently used. ''(since 0.10.5)''"
 msgstr ""
 
-#: trac/env.py:187
+#: trac/env.py:188
 msgid ""
 "Restrict cookies to HTTPS connections.\n"
 "\n"
@@ -342,26 +356,26 @@
 "0.11.2'')"
 msgstr ""
 
-#: trac/env.py:195
+#: trac/env.py:196
 msgid "Name of the project."
 msgstr ""
 
-#: trac/env.py:198
+#: trac/env.py:199
 msgid "Short description of the project."
 msgstr ""
 
-#: trac/env.py:201
+#: trac/env.py:202
 msgid ""
 "URL of the main project web site, usually the website in\n"
 "which the `base_url` resides. This is used in notification\n"
 "e-mails."
 msgstr ""
 
-#: trac/env.py:206
+#: trac/env.py:207
 msgid "E-Mail address of the project's administrator."
 msgstr ""
 
-#: trac/env.py:209
+#: trac/env.py:210
 msgid ""
 "Base URL of a Trac instance where errors in this Trac\n"
 "should be reported.\n"
@@ -371,36 +385,36 @@
 "buttons.  (''since 0.11.3'')"
 msgstr ""
 
-#: trac/env.py:218
+#: trac/env.py:219
 msgid "Page footer text (right-aligned)."
 msgstr ""
 
-#: trac/env.py:223
+#: trac/env.py:224
 msgid "URL of the icon of the project."
 msgstr ""
 
-#: trac/env.py:226
+#: trac/env.py:227
 msgid ""
 "Logging facility to use.\n"
 "\n"
 "Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`)."
 msgstr ""
 
-#: trac/env.py:231
+#: trac/env.py:232
 msgid ""
 "If `log_type` is `file`, this should be a path to the\n"
 "log-file.  Relative paths are resolved relative to the `log`\n"
 "directory of the environment."
 msgstr ""
 
-#: trac/env.py:236
+#: trac/env.py:237
 msgid ""
 "Level of verbosity in log.\n"
 "\n"
 "Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`)."
 msgstr ""
 
-#: trac/env.py:241
+#: trac/env.py:242
 msgid ""
 "Custom logging format.\n"
 "\n"
@@ -425,7 +439,7 @@
 "''(since 0.10.5)''"
 msgstr ""
 
-#: trac/notification.py:50
+#: trac/notification.py:52
 msgid ""
 "Name of the component implementing `IEmailSender`.\n"
 "\n"
@@ -435,59 +449,59 @@
 "executable. (''since 0.12'')"
 msgstr ""
 
-#: trac/notification.py:59
+#: trac/notification.py:61
 msgid "Enable email notification."
 msgstr ""
 
-#: trac/notification.py:62
+#: trac/notification.py:64
 msgid "Sender address to use in notification emails."
 msgstr ""
 
-#: trac/notification.py:65
+#: trac/notification.py:67
 msgid "Sender name to use in notification emails."
 msgstr ""
 
-#: trac/notification.py:68
+#: trac/notification.py:70
 msgid ""
 "Use the action author as the sender of notification emails.\n"
 "(''since 1.0'')"
 msgstr ""
 
-#: trac/notification.py:72
+#: trac/notification.py:74
 msgid "Reply-To address to use in notification emails."
 msgstr ""
 
-#: trac/notification.py:75
+#: trac/notification.py:77
 msgid ""
 "Email address(es) to always send notifications to,\n"
 "addresses can be seen by all recipients (Cc:)."
 msgstr ""
 
-#: trac/notification.py:79
+#: trac/notification.py:81
 msgid ""
 "Email address(es) to always send notifications to,\n"
 "addresses do not appear publicly (Bcc:). (''since 0.10'')"
 msgstr ""
 
-#: trac/notification.py:83
+#: trac/notification.py:85
 msgid ""
 "Default host/domain to append to address that do not specify\n"
 "one."
 msgstr ""
 
-#: trac/notification.py:87
+#: trac/notification.py:89
 msgid ""
 "Comma-separated list of domains that should not be considered\n"
 "part of email addresses (for usernames with Kerberos domains)."
 msgstr ""
 
-#: trac/notification.py:91
+#: trac/notification.py:93
 msgid ""
 "Comma-separated list of domains that should be considered as\n"
 "valid for email addresses (such as localdomain)."
 msgstr ""
 
-#: trac/notification.py:95
+#: trac/notification.py:97
 msgid ""
 "Specifies the MIME encoding scheme for emails.\n"
 "\n"
@@ -497,7 +511,7 @@
 "(''since 0.10'')"
 msgstr ""
 
-#: trac/notification.py:103
+#: trac/notification.py:105
 msgid ""
 "Recipients can see email addresses of other CC'ed recipients.\n"
 "\n"
@@ -505,7 +519,7 @@
 "(''since 0.10'')"
 msgstr ""
 
-#: trac/notification.py:109
+#: trac/notification.py:111
 msgid ""
 "Permit email address without a host/domain (i.e. username only).\n"
 "\n"
@@ -513,7 +527,7 @@
 "a FQDN or use local delivery. (''since 0.10'')"
 msgstr ""
 
-#: trac/notification.py:115
+#: trac/notification.py:117
 msgid ""
 "Text to prepend to subject line of notification emails.\n"
 "\n"
@@ -522,27 +536,27 @@
 "will disable it. (''since 0.10.1'')"
 msgstr ""
 
-#: trac/notification.py:133
+#: trac/notification.py:135
 msgid "SMTP server hostname to use for email notifications."
 msgstr ""
 
-#: trac/notification.py:136
+#: trac/notification.py:138
 msgid "SMTP server port to use for email notification."
 msgstr ""
 
-#: trac/notification.py:139
+#: trac/notification.py:141
 msgid "Username for SMTP server. (''since 0.9'')"
 msgstr ""
 
-#: trac/notification.py:142
+#: trac/notification.py:144
 msgid "Password for SMTP server. (''since 0.9'')"
 msgstr ""
 
-#: trac/notification.py:145
+#: trac/notification.py:147
 msgid "Use SSL/TLS to send notifications over SMTP. (''since 0.10'')"
 msgstr ""
 
-#: trac/notification.py:189
+#: trac/notification.py:200
 msgid ""
 "Path to the sendmail executable.\n"
 "\n"
@@ -550,13 +564,13 @@
 " (''since 0.12'')"
 msgstr ""
 
-#: trac/perm.py:312
+#: trac/perm.py:310
 msgid ""
 "Name of the component implementing `IPermissionStore`, which is used\n"
 "for managing user and group permissions."
 msgstr ""
 
-#: trac/perm.py:317
+#: trac/perm.py:315
 msgid ""
 "List of components implementing `IPermissionPolicy`, in the order in\n"
 "which they will be applied. These components manage fine-grained access\n"
@@ -566,30 +580,30 @@
 "ones)"
 msgstr ""
 
-#: trac/db/api.py:227
+#: trac/db/api.py:228
 msgid ""
 "Database connection\n"
 "[wiki:TracEnvironment#DatabaseConnectionStrings string] for this\n"
 "project"
 msgstr ""
 
-#: trac/db/api.py:232
+#: trac/db/api.py:233
 msgid "Database backup location"
 msgstr ""
 
-#: trac/db/api.py:235
+#: trac/db/api.py:236
 msgid ""
 "Timeout value for database connection, in seconds.\n"
 "Use '0' to specify ''no timeout''. ''(Since 0.11)''"
 msgstr ""
 
-#: trac/db/api.py:239
+#: trac/db/api.py:240
 msgid ""
 "Show the SQL queries in the Trac log, at DEBUG level.\n"
 "''(Since 0.11.5)''"
 msgstr ""
 
-#: trac/db/mysql_backend.py:78
+#: trac/db/mysql_backend.py:83
 msgid "Location of mysqldump for MySQL database backups"
 msgstr ""
 
@@ -597,25 +611,25 @@
 msgid "Location of pg_dump for Postgres database backups"
 msgstr ""
 
-#: trac/db/sqlite_backend.py:143
+#: trac/db/sqlite_backend.py:145
 msgid ""
 "Paths to sqlite extensions, relative to Trac environment's\n"
 "directory or absolute. (''since 0.12'')"
 msgstr ""
 
-#: trac/mimeview/api.py:613
+#: trac/mimeview/api.py:619
 msgid "Charset to be used when in doubt."
 msgstr ""
 
-#: trac/mimeview/api.py:616
+#: trac/mimeview/api.py:622
 msgid "Displayed tab width in file preview. (''since 0.9'')"
 msgstr ""
 
-#: trac/mimeview/api.py:619
+#: trac/mimeview/api.py:625
 msgid "Maximum file size for HTML preview. (''since 0.9'')"
 msgstr ""
 
-#: trac/mimeview/api.py:622
+#: trac/mimeview/api.py:628
 msgid ""
 "List of additional MIME types and keyword mappings.\n"
 "Mappings are comma-separated, and for each MIME type,\n"
@@ -623,7 +637,7 @@
 "or file extensions. (''since 0.10'')"
 msgstr ""
 
-#: trac/mimeview/api.py:629
+#: trac/mimeview/api.py:635
 msgid ""
 "List of additional MIME types associated to filename patterns.\n"
 "Mappings are comma-separated, and each mapping consists of a MIME type\n"
@@ -631,7 +645,7 @@
 "(\":\"). (''since 1.0'')"
 msgstr ""
 
-#: trac/mimeview/api.py:636
+#: trac/mimeview/api.py:642
 msgid ""
 "Comma-separated list of MIME types that should be treated as\n"
 "binary data. (''since 0.11.5'')"
@@ -654,11 +668,11 @@
 "Pygments render."
 msgstr ""
 
-#: trac/search/web_ui.py:49
+#: trac/search/web_ui.py:48
 msgid "Minimum length of query string allowed when performing a search."
 msgstr ""
 
-#: trac/search/web_ui.py:52
+#: trac/search/web_ui.py:51
 msgid ""
 "Specifies which search filters should be disabled by\n"
 "default on the search page. This will also restrict the\n"
@@ -672,19 +686,19 @@
 "(since 0.12)"
 msgstr ""
 
-#: trac/ticket/api.py:166
+#: trac/ticket/api.py:175
 msgid ""
 "In this section, you can define additional fields for tickets. See\n"
 "TracTicketsCustomFields for more details."
 msgstr ""
 
-#: trac/ticket/api.py:170
+#: trac/ticket/api.py:179
 msgid ""
 "Ordered list of workflow controllers to use for ticket actions\n"
 "(''since 0.11'')."
 msgstr ""
 
-#: trac/ticket/api.py:176
+#: trac/ticket/api.py:185
 msgid ""
 "Make the owner field of tickets use a drop-down menu.\n"
 "Be sure to understand the performance implications before activating\n"
@@ -697,57 +711,57 @@
 "(''since 0.9'')"
 msgstr ""
 
-#: trac/ticket/api.py:187
+#: trac/ticket/api.py:196
 msgid "Default version for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:190
+#: trac/ticket/api.py:199
 msgid "Default type for newly created tickets (''since 0.9'')."
 msgstr ""
 
-#: trac/ticket/api.py:193
+#: trac/ticket/api.py:202
 msgid "Default priority for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:196
+#: trac/ticket/api.py:205
 msgid "Default milestone for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:199
+#: trac/ticket/api.py:208
 msgid "Default component for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:202
+#: trac/ticket/api.py:211
 msgid "Default severity for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:205
+#: trac/ticket/api.py:214
 msgid "Default summary (title) for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:208
+#: trac/ticket/api.py:217
 msgid "Default description for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:211
+#: trac/ticket/api.py:220
 msgid "Default keywords for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:214
+#: trac/ticket/api.py:223
 msgid "Default owner for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:217
+#: trac/ticket/api.py:226
 msgid "Default cc: list for newly created tickets."
 msgstr ""
 
-#: trac/ticket/api.py:220
+#: trac/ticket/api.py:229
 msgid ""
 "Default resolution for resolving (closing) tickets\n"
 "(''since 0.11'')."
 msgstr ""
 
-#: trac/ticket/default_workflow.py:105
+#: trac/ticket/default_workflow.py:108
 msgid ""
 "The workflow for tickets is controlled by plugins. By default,\n"
 "there's only a `ConfigurableTicketWorkflow` component in charge.\n"
@@ -757,23 +771,23 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/ticket/notification.py:36
+#: trac/ticket/notification.py:38
 msgid "Always send notifications to the ticket owner (''since 0.9'')."
 msgstr ""
 
-#: trac/ticket/notification.py:40
+#: trac/ticket/notification.py:42
 msgid ""
 "Always send notifications to any address in the ''reporter''\n"
 "field."
 msgstr ""
 
-#: trac/ticket/notification.py:46
+#: trac/ticket/notification.py:48
 msgid ""
 "Always send notifications to the person who causes the ticket\n"
 "property change and to any previous updater of that ticket."
 msgstr ""
 
-#: trac/ticket/notification.py:51
+#: trac/ticket/notification.py:53
 msgid ""
 "A Genshi text template snippet used to get the notification subject.\n"
 "\n"
@@ -782,7 +796,7 @@
 "''(since 0.11)''"
 msgstr ""
 
-#: trac/ticket/notification.py:59
+#: trac/ticket/notification.py:61
 msgid ""
 "Like ticket_subject_template but for batch modifications.\n"
 "\n"
@@ -790,7 +804,7 @@
 "''(since 1.0)''"
 msgstr ""
 
-#: trac/ticket/notification.py:66
+#: trac/ticket/notification.py:68
 msgid ""
 "Which width of ambiguous characters (e.g. 'single' or\n"
 "'double') should be used in the table of notification mail.\n"
@@ -801,7 +815,7 @@
 "0.12.2)''"
 msgstr ""
 
-#: trac/ticket/query.py:824
+#: trac/ticket/query.py:838
 msgid ""
 "The default query for authenticated users. The query is either\n"
 "in [TracQuery#QueryLanguage query language] syntax, or a URL query\n"
@@ -810,7 +824,7 @@
 "(''since 0.11.2'')"
 msgstr ""
 
-#: trac/ticket/query.py:832
+#: trac/ticket/query.py:846
 msgid ""
 "The default query for anonymous users. The query is either\n"
 "in [TracQuery#QueryLanguage query language] syntax, or a URL query\n"
@@ -819,25 +833,25 @@
 "(''since 0.11.2'')"
 msgstr ""
 
-#: trac/ticket/query.py:840
+#: trac/ticket/query.py:854
 msgid ""
 "Number of tickets displayed per page in ticket queries,\n"
 "by default (''since 0.11'')"
 msgstr ""
 
-#: trac/ticket/report.py:116
+#: trac/ticket/report.py:117
 msgid ""
 "Number of tickets displayed per page in ticket reports,\n"
 "by default (''since 0.11'')"
 msgstr ""
 
-#: trac/ticket/report.py:120
+#: trac/ticket/report.py:121
 msgid ""
 "Number of tickets displayed in the rss feeds for reports\n"
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/ticket/roadmap.py:144
+#: trac/ticket/roadmap.py:145
 msgid ""
 "As the workflow for tickets is now configurable, there can\n"
 "be many ticket states, and simply displaying closed tickets\n"
@@ -901,36 +915,38 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/ticket/roadmap.py:388
+#: trac/ticket/roadmap.py:396
 msgid ""
 "Name of the component implementing `ITicketGroupStatsProvider`,\n"
 "which is used to collect statistics on groups of tickets for display\n"
 "in the roadmap views."
 msgstr ""
 
-#: trac/ticket/roadmap.py:590
+#: trac/ticket/roadmap.py:598
 msgid ""
 "Name of the component implementing `ITicketGroupStatsProvider`,\n"
 "which is used to collect statistics on groups of tickets for display\n"
 "in the milestone views."
 msgstr ""
 
-#: trac/ticket/web_ui.py:75
+#: trac/ticket/web_ui.py:73
 msgid ""
 "Enable the display of all ticket changes in the timeline, not only\n"
 "open / close operations (''since 0.9'')."
 msgstr ""
 
-#: trac/ticket/web_ui.py:79
+#: trac/ticket/web_ui.py:77
 msgid ""
-"Don't accept tickets with a too big description.\n"
-"(''since 0.11'')."
+"Maximum allowed description size in characters.\n"
+"(//since 0.11//)."
 msgstr ""
 
-#: trac/ticket/web_ui.py:83
-msgid ""
-"Don't accept tickets with a too big comment.\n"
-"(''since 0.11.2'')"
+#: trac/ticket/web_ui.py:81
+msgid "Maximum allowed comment size in characters. (//since 0.11.2//)."
+msgstr ""
+
+#: trac/ticket/web_ui.py:84
+msgid "Maximum allowed summary size in characters. (//since 1.0.2//)."
 msgstr ""
 
 #: trac/ticket/web_ui.py:87
@@ -988,7 +1004,7 @@
 "allowed. (''since 0.12.1'')"
 msgstr ""
 
-#: trac/versioncontrol/api.py:284
+#: trac/versioncontrol/api.py:293
 msgid ""
 "One of the alternatives for registering new repositories is to\n"
 "populate the `[repositories]` section of the `trac.ini`.\n"
@@ -1004,7 +1020,7 @@
 "(''since 0.12'')"
 msgstr ""
 
-#: trac/versioncontrol/api.py:298
+#: trac/versioncontrol/api.py:307
 msgid ""
 "Default repository connector type. (''since 0.10'')\n"
 "\n"
@@ -1013,7 +1029,7 @@
 "\"Repositories\" admin panel. (''since 0.12'')"
 msgstr ""
 
-#: trac/versioncontrol/api.py:306
+#: trac/versioncontrol/api.py:315
 msgid ""
 "Path to the default repository. This can also be a relative path\n"
 "(''since 0.11'').\n"
@@ -1023,7 +1039,7 @@
 "\"Repositories\" admin panel. (''since 0.12'')"
 msgstr ""
 
-#: trac/versioncontrol/api.py:314
+#: trac/versioncontrol/api.py:323
 msgid ""
 "List of repositories that should be synchronized on every page\n"
 "request.\n"
@@ -1052,7 +1068,7 @@
 "repository. If left empty, the global section is used."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:118
+#: trac/versioncontrol/web_ui/browser.py:117
 msgid ""
 "Comma-separated list of version control properties to render\n"
 "as wiki content in the repository browser.\n"
@@ -1060,7 +1076,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:125
+#: trac/versioncontrol/web_ui/browser.py:124
 msgid ""
 "Comma-separated list of version control properties to render\n"
 "as oneliner wiki content in the repository browser.\n"
@@ -1068,7 +1084,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:184
+#: trac/versioncontrol/web_ui/browser.py:183
 msgid ""
 "List of repository paths that can be downloaded.\n"
 "\n"
@@ -1082,7 +1098,7 @@
 "(''since 0.10'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:197
+#: trac/versioncontrol/web_ui/browser.py:196
 msgid ""
 "Enable colorization of the ''age'' column.\n"
 "\n"
@@ -1091,7 +1107,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:206
+#: trac/versioncontrol/web_ui/browser.py:205
 msgid ""
 "(r,g,b) color triple to use for the color corresponding\n"
 "to the newest color, for the color scale used in ''blame'' or\n"
@@ -1099,7 +1115,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:214
+#: trac/versioncontrol/web_ui/browser.py:213
 msgid ""
 "(r,g,b) color triple to use for the color corresponding\n"
 "to the oldest color, for the color scale used in ''blame'' or\n"
@@ -1107,7 +1123,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:220
+#: trac/versioncontrol/web_ui/browser.py:219
 msgid ""
 "If set to a value between 0 and 1 (exclusive), this will be the\n"
 "point chosen to set the `intermediate_color` for interpolating\n"
@@ -1115,7 +1131,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:226
+#: trac/versioncontrol/web_ui/browser.py:225
 msgid ""
 "(r,g,b) color triple to use for the color corresponding\n"
 "to the intermediate color, if two linear interpolations are used\n"
@@ -1125,7 +1141,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:234
+#: trac/versioncontrol/web_ui/browser.py:233
 msgid ""
 "Whether raw files should be rendered in the browser, or only made\n"
 "downloadable.\n"
@@ -1138,14 +1154,14 @@
 "recommended to leave this option disabled (which is the default)."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/browser.py:246
+#: trac/versioncontrol/web_ui/browser.py:245
 msgid ""
 "Comma-separated list of version control properties to hide from\n"
 "the repository browser.\n"
 "(''since 0.9'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:129
+#: trac/versioncontrol/web_ui/changeset.py:131
 msgid ""
 "Number of files to show (`-1` for unlimited, `0` to disable).\n"
 "\n"
@@ -1153,7 +1169,7 @@
 "changed files. (since 0.11)."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:136
+#: trac/versioncontrol/web_ui/changeset.py:138
 msgid ""
 "Whether wiki-formatted changeset messages should be multiline or\n"
 "not.\n"
@@ -1163,7 +1179,7 @@
 "some formatting (bullet points, etc)."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:145
+#: trac/versioncontrol/web_ui/changeset.py:147
 msgid ""
 "Whether consecutive changesets from the same author having\n"
 "exactly the same message should be presented as one event.\n"
@@ -1171,20 +1187,20 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:152
+#: trac/versioncontrol/web_ui/changeset.py:154
 msgid ""
 "Maximum number of modified files for which the changeset view will\n"
 "attempt to show the diffs inlined (''since 0.10'')."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:156
+#: trac/versioncontrol/web_ui/changeset.py:158
 msgid ""
 "Maximum total size in bytes of the modified files (their old size\n"
 "plus their new size) for which the changeset view will attempt to show\n"
 "the diffs inlined (''since 0.10'')."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/changeset.py:161
+#: trac/versioncontrol/web_ui/changeset.py:163
 msgid ""
 "Whether wiki formatting should be applied to changeset messages.\n"
 "\n"
@@ -1192,13 +1208,13 @@
 "pre-formatted text."
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:47
+#: trac/versioncontrol/web_ui/log.py:46
 msgid ""
 "Default value for the limit argument in the TracRevisionLog.\n"
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/versioncontrol/web_ui/log.py:51
+#: trac/versioncontrol/web_ui/log.py:50
 msgid ""
 "Comma-separated list of colors to use for the TracRevisionLog\n"
 "graph display. (''since 1.0'')"
@@ -1234,7 +1250,7 @@
 "the cookie.  (''since 0.12'')"
 msgstr ""
 
-#: trac/web/chrome.py:342
+#: trac/web/chrome.py:350
 msgid ""
 "Path to the //shared templates directory//.\n"
 "\n"
@@ -1244,7 +1260,7 @@
 "(''since 0.11'')"
 msgstr ""
 
-#: trac/web/chrome.py:350
+#: trac/web/chrome.py:358
 msgid ""
 "Path to the //shared htdocs directory//.\n"
 "\n"
@@ -1257,11 +1273,11 @@
 "(''since 1.0'')"
 msgstr ""
 
-#: trac/web/chrome.py:361
+#: trac/web/chrome.py:369
 msgid "Automatically reload template files after modification."
 msgstr ""
 
-#: trac/web/chrome.py:364
+#: trac/web/chrome.py:372
 msgid ""
 "The maximum number of templates that the template loader will cache\n"
 "in memory. The default value is 128. You may want to choose a higher\n"
@@ -1270,7 +1286,7 @@
 "memory."
 msgstr ""
 
-#: trac/web/chrome.py:371
+#: trac/web/chrome.py:379
 msgid ""
 "Base URL for serving the core static resources below\n"
 "`/chrome/common/`.\n"
@@ -1287,7 +1303,7 @@
 "rules will be needed in the web server."
 msgstr ""
 
-#: trac/web/chrome.py:386
+#: trac/web/chrome.py:394
 msgid ""
 "Location of the jQuery !JavaScript library (version 1.7.2).\n"
 "\n"
@@ -1301,7 +1317,7 @@
 "(''since 1.0'')"
 msgstr ""
 
-#: trac/web/chrome.py:398
+#: trac/web/chrome.py:406
 msgid ""
 "Location of the jQuery UI !JavaScript library (version 1.8.21).\n"
 "\n"
@@ -1315,7 +1331,7 @@
 "(''since 1.0'')"
 msgstr ""
 
-#: trac/web/chrome.py:410
+#: trac/web/chrome.py:418
 msgid ""
 "Location of the theme to be used with the jQuery UI !JavaScript\n"
 "library (version 1.8.21).\n"
@@ -1334,23 +1350,23 @@
 "(''since 1.0'')"
 msgstr ""
 
-#: trac/web/chrome.py:425
+#: trac/web/chrome.py:433
 msgid ""
 "Order of the items to display in the `metanav` navigation bar,\n"
 "listed by IDs. See also TracNavigation."
 msgstr ""
 
-#: trac/web/chrome.py:430
+#: trac/web/chrome.py:438
 msgid ""
 "Order of the items to display in the `mainnav` navigation bar,\n"
 "listed by IDs. See also TracNavigation."
 msgstr ""
 
-#: trac/web/chrome.py:436
+#: trac/web/chrome.py:444
 msgid "URL to link to, from the header logo."
 msgstr ""
 
-#: trac/web/chrome.py:439
+#: trac/web/chrome.py:447
 msgid ""
 "URL of the image to use as header logo.\n"
 "It can be absolute, server relative or relative.\n"
@@ -1363,44 +1379,50 @@
 "Only specifying `your-logo.png` is equivalent to the latter."
 msgstr ""
 
-#: trac/web/chrome.py:450
+#: trac/web/chrome.py:458
 msgid "Alternative text for the header logo."
 msgstr ""
 
-#: trac/web/chrome.py:454
+#: trac/web/chrome.py:462
 msgid "Width of the header logo image in pixels."
 msgstr ""
 
-#: trac/web/chrome.py:457
+#: trac/web/chrome.py:465
 msgid "Height of the header logo image in pixels."
 msgstr ""
 
-#: trac/web/chrome.py:460
+#: trac/web/chrome.py:468
 msgid ""
 "Show email addresses instead of usernames. If false, we obfuscate\n"
 "email addresses. (''since 0.11'')"
 msgstr ""
 
-#: trac/web/chrome.py:464
+#: trac/web/chrome.py:472
 msgid ""
 "Never obfuscate `mailto:` links explicitly written in the wiki,\n"
 "even if `show_email_addresses` is false or the user has not the\n"
 "EMAIL_VIEW permission (''since 0.11.6'')."
 msgstr ""
 
-#: trac/web/chrome.py:470
+#: trac/web/chrome.py:478
 msgid ""
 "Show IP addresses for resource edits (e.g. wiki).\n"
 "(''since 0.11.3'')"
 msgstr ""
 
-#: trac/web/chrome.py:474
+#: trac/web/chrome.py:482
 msgid ""
 "Make `<textarea>` fields resizable. Requires !JavaScript.\n"
 "(''since 0.12'')"
 msgstr ""
 
-#: trac/web/chrome.py:478
+#: trac/web/chrome.py:486
+msgid ""
+"Add a simple toolbar on top of Wiki `<textarea>`s.\n"
+"(''since 1.0.2'')"
+msgstr ""
+
+#: trac/web/chrome.py:490
 msgid ""
 "Inactivity timeout in seconds after which the automatic wiki preview\n"
 "triggers an update. This option can contain floating-point values. The\n"
@@ -1409,7 +1431,7 @@
 "(''since 0.12'')"
 msgstr ""
 
-#: trac/web/chrome.py:485
+#: trac/web/chrome.py:497
 msgid ""
 "The date information format. Valid options are 'relative' for\n"
 "displaying relative format and 'absolute' for displaying absolute\n"
@@ -1487,7 +1509,7 @@
 "(''since 0.11.8'')"
 msgstr ""
 
-#: trac/wiki/intertrac.py:36
+#: trac/wiki/intertrac.py:35
 msgid ""
 "This section configures InterTrac prefixes. Options in this section\n"
 "whose name contain a \".\" define aspects of the InterTrac prefix\n"
@@ -1508,7 +1530,7 @@
 "   it doesn't know how to dispatch an InterTrac link, and it's up to\n"
 "   the local Trac to prepare the correct link. Not all links will work\n"
 "   that way, but the most common do. This is called the compatibility\n"
-"   mode, and is `true` by default.\n"
+"   mode, and is `false` by default.\n"
 " * If you know that the remote Trac knows how to dispatch InterTrac\n"
 "   links, you can explicitly disable this compatibility mode and then\n"
 "   ''any'' TracLinks can become InterTrac links.\n"
@@ -1543,6 +1565,6 @@
 msgstr ""
 
 #: trac/wiki/web_ui.py:64
-msgid "Maximum allowed wiki page size in bytes. (''since 0.11.2'')"
+msgid "Maximum allowed wiki page size in characters. (''since 0.11.2'')"
 msgstr ""
 
diff --git a/trac/trac/locale/vi/LC_MESSAGES/messages.po b/trac/trac/locale/vi/LC_MESSAGES/messages.po
index 49962b0..e15b73b 100644
--- a/trac/trac/locale/vi/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/vi/LC_MESSAGES/messages.po
@@ -16,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -6928,3 +6928,17 @@
 msgid "The following pages have a name similar to this page, and may be related:"
 msgstr ""
 
+#~ msgid ""
+#~ "Welcome to trac-admin %(version)s\n"
+#~ "Interactive Trac administration console.\n"
+#~ "Copyright (C) 2003-2012 Edgewall Software\n"
+#~ "\n"
+#~ "Type:  '?' or 'help' for help on commands.\n"
+#~ "        "
+#~ msgstr ""
+
+#~ msgid ""
+#~ "Copyright © 2003-2012\n"
+#~ "        [1:Edgewall Software]"
+#~ msgstr ""
+
diff --git a/trac/trac/locale/zh_CN/LC_MESSAGES/messages-js.po b/trac/trac/locale/zh_CN/LC_MESSAGES/messages-js.po
index efc33fe..68a894a 100644
--- a/trac/trac/locale/zh_CN/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/zh_CN/LC_MESSAGES/messages-js.po
@@ -1,8 +1,9 @@
 # Chinese (China) translations for Trac.
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2012 Edgewall Software
 # This file is distributed under the same license as the Trac project.
-# Zeng Jie <zengjie@gmail.com>, 2010.
 #
+# Translators:
+# Shuning Hong <>, 2012.
 msgid ""
 msgstr ""
 "Project-Id-Version: Trac 0.12\n"
@@ -15,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
@@ -27,24 +28,24 @@
 
 #: trac/htdocs/js/diff.js:119
 msgid "Tabular"
-msgstr "表式"
+msgstr ""
 
 #: trac/htdocs/js/diff.js:125
 msgid "Unified"
-msgstr "标准"
+msgstr ""
 
 #: trac/htdocs/js/expand_dir.js:42
 msgid "Expand sub-directory in place"
-msgstr "就地展开子目录"
+msgstr "在此处展开子目录"
 
 #: trac/htdocs/js/expand_dir.js:71
 msgid "Re-expand directory"
-msgstr "再次展开目录"
+msgstr "重新展开目录"
 
 #: trac/htdocs/js/expand_dir.js:121
 #, python-format
 msgid "Loading %(entry)s..."
-msgstr "加载 %(entry)s..."
+msgstr "正在装载%(entry)s..."
 
 #: trac/htdocs/js/expand_dir.js:149
 msgid "(empty)"
@@ -56,7 +57,7 @@
 
 #: trac/htdocs/js/expand_dir.js:164
 msgid "Fold directory"
-msgstr "收起目录"
+msgstr "折叠目录"
 
 #: trac/htdocs/js/folding.js:62
 #, python-format
@@ -66,7 +67,7 @@
 #: trac/htdocs/js/folding.js:78
 #, python-format
 msgid "%(title)s (click to hide column)"
-msgstr "%(title)s (点击隐藏列)"
+msgstr "%(title)s (点击隐藏本列)"
 
 #. TRANSLATOR: Format in month heading in the datepicker, extracts yearSuffix
 #. and showMonthAfterYear
@@ -136,7 +137,7 @@
 
 #: trac/htdocs/js/query.js:132
 msgid "A filter already exists for that property"
-msgstr "此属性已有过滤器"
+msgstr "该属性的过滤器已存在"
 
 #: trac/htdocs/js/query.js:159
 msgid "or"
@@ -156,7 +157,7 @@
 
 #: trac/htdocs/js/query.js:190
 msgid "and"
-msgstr "到"
+msgstr "和"
 
 #: trac/htdocs/js/query.js:337
 msgid " remove:"
@@ -177,31 +178,31 @@
 
 #: trac/htdocs/js/trac.js:7
 msgid "Link here"
-msgstr "链接到此"
+msgstr ""
 
 #: trac/htdocs/js/wikitoolbar.js:56
 msgid "Bold text: '''Example'''"
-msgstr "粗体字: '''示例'''"
+msgstr "加粗文字: '''Example'''"
 
 #: trac/htdocs/js/wikitoolbar.js:59
 msgid "Italic text: ''Example''"
-msgstr "斜体字: ''示例''"
+msgstr "斜体文字: ''Example''"
 
 #: trac/htdocs/js/wikitoolbar.js:62
 msgid "Heading: == Example =="
-msgstr "标题: == 示例 =="
+msgstr "标题: == Example =="
 
 #: trac/htdocs/js/wikitoolbar.js:65
 msgid "Link: [http://www.example.com/ Example]"
-msgstr "链接: [http://www.example.com/ 示例]"
+msgstr "链接: [http://www.example.com/ Example]"
 
 #: trac/htdocs/js/wikitoolbar.js:68
 msgid "Code block: {{{ example }}}"
-msgstr "代码块: {{{ 示例 }}}"
+msgstr "代码块: {{{ example }}}"
 
 #: trac/htdocs/js/wikitoolbar.js:71
 msgid "Horizontal rule: ----"
-msgstr "水平线: ----"
+msgstr "水平分割线: ----"
 
 #: trac/htdocs/js/wikitoolbar.js:74
 msgid "New paragraph"
@@ -209,22 +210,22 @@
 
 #: trac/htdocs/js/wikitoolbar.js:77
 msgid "Line break: [[BR]]"
-msgstr "折行: [[BR]]"
+msgstr "换行: [[BR]]"
 
 #: trac/htdocs/js/wikitoolbar.js:80
 msgid "Image: [[Image()]]"
-msgstr "图片: [[Image()]]"
+msgstr "图像: [[Image()]]"
 
 #: trac/ticket/templates/ticket.html:3 trac/wiki/templates/wiki_view.html:3
 msgid "Link to this section"
-msgstr "链接到这一节"
+msgstr "链接至本节"
 
 #: trac/versioncontrol/templates/changeset.html:7
 msgid "Link to this diff"
-msgstr "链接到这一差异"
+msgstr "链接至该diff"
 
 #: trac/wiki/templates/wiki_view.html:5
 #, python-format
 msgid "Link to #%(id)s"
-msgstr "链接到#%(id)s"
+msgstr "链接至#%(id)s"
 
diff --git a/trac/trac/locale/zh_CN/LC_MESSAGES/messages.po b/trac/trac/locale/zh_CN/LC_MESSAGES/messages.po
index 3c180c2..75d6407 100644
--- a/trac/trac/locale/zh_CN/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/zh_CN/LC_MESSAGES/messages.po
@@ -1,10 +1,12 @@
 # Chinese (China) translations for Trac.
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2012 Edgewall Software
 # This file is distributed under the same license as the Trac project.
 #
+# Translators:
 # Hong Shuning <hongshuning@gmail.com>, 2010.
-# Zeng Jie <zengjie@gmail.com>, 2010.
 # Jake Li <gnozil@gmail.com>, 2010.
+# Shuning Hong <>, 2012.
+# Zeng Jie <zengjie@gmail.com>, 2010.
 msgid ""
 msgstr ""
 "Project-Id-Version: Trac 0.12\n"
@@ -17,7 +19,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -282,9 +284,9 @@
 msgstr "不能删除附件"
 
 #: trac/attachment.py:253
-#, fuzzy, python-format
+#, python-format
 msgid "Cannot reparent attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
-msgstr "不能为附件 \"%(att)s\" 重定父页面,因为它已经在 %(realm)s:%(id)s 中存在"
+msgstr ""
 
 #: trac/attachment.py:258
 #, python-format
@@ -299,9 +301,9 @@
 msgstr "不能为附件 %(name)s 重定父页面"
 
 #: trac/attachment.py:313
-#, fuzzy, python-format
+#, python-format
 msgid "Cannot create attachment \"%(att)s\" as %(realm)s:%(id)s is invalid"
-msgstr "不能为附件 \"%(att)s\" 重定父页面,因为它已经在 %(realm)s:%(id)s 中存在"
+msgstr "无法创建附件\"%(att)s\",因为%(realm)s:%(id)s非法"
 
 #: trac/attachment.py:396
 #, python-format
@@ -485,7 +487,7 @@
 #: trac/config.py:761 trac/config.py:774
 #, python-format
 msgid "Option '%(option)s' doesn't exist in section '%(section)s'"
-msgstr ""
+msgstr "在'%(section)s'节中没有'%(option)s'选项"
 
 #: trac/core.py:33
 msgid "Trac Error"
@@ -742,10 +744,11 @@
 "        "
 msgstr ""
 "欢迎使用 trac-admin %(version)s\n"
-"交互式Trac管理控制台。\n"
-"Copyright (c) 2003-2010 Edgewall Software\n"
+"Trac交互式管理控制台。\n"
+"Copyright (C) 2003-2013 Edgewall Software\n"
 "\n"
-"输入 '?' 或 'help' 获取命令帮助"
+"输入:  '?' 或 'help' 可获得命令帮助。\n"
+"        "
 
 #: trac/admin/console.py:166
 #, python-format
@@ -762,7 +765,7 @@
 msgid ""
 "No documentation found for '%(cmd)s'. Use 'help' to see the list of "
 "commands."
-msgstr "没有找到 '%(cmd)s' 的文档"
+msgstr ""
 
 #: trac/admin/console.py:322
 msgid "Did you mean this?"
@@ -1383,9 +1386,9 @@
 msgstr "增加里程碑:"
 
 #: trac/admin/templates/admin_milestones.html:92
-#, fuzzy, python-format
+#, python-format
 msgid "Format: %(datetimehint)s"
-msgstr "格式:%(datetimehint)s"
+msgstr "格式: %(datetimehint)s"
 
 #: trac/admin/templates/admin_milestones.html:107 trac/ticket/admin.py:399
 msgid "Due"
@@ -1584,14 +1587,13 @@
 
 #: trac/db/mysql_backend.py:235 trac/db/postgres_backend.py:204
 #: trac/db/sqlite_backend.py:245
-#, fuzzy
 msgid "No destination file created"
-msgstr "没有选择版本"
+msgstr "未创建目标文件"
 
 #: trac/db/pool.py:130
-#, fuzzy, python-format
+#, python-format
 msgid "Unable to get database connection within %(time)d seconds."
-msgstr "无法在 %(time)d 秒内获得数据库连接"
+msgstr "无法在%(time)d秒内获取数据库连接。"
 
 #: trac/db/postgres_backend.py:81
 msgid "Cannot load Python bindings for PostgreSQL"
@@ -1711,7 +1713,6 @@
 msgstr "高级"
 
 #: trac/prefs/web_ui.py:167
-#, fuzzy
 msgid "The session has been loaded."
 msgstr ""
 
@@ -1741,9 +1742,8 @@
 msgstr "会话标志:"
 
 #: trac/prefs/templates/prefs_advanced.html:17
-#, fuzzy
 msgid "Change"
-msgstr "变更"
+msgstr ""
 
 #: trac/prefs/templates/prefs_advanced.html:18
 msgid ""
@@ -2078,12 +2078,11 @@
 "        [1:http://trac.edgewall.org/]"
 
 #: trac/templates/about.html:46
-#, fuzzy
 msgid ""
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"Copyright © 2003-2010\n"
+"Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
@@ -2572,16 +2571,12 @@
 msgstr "查看附件"
 
 #: trac/templates/list_of_attachments.html:18
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "[1:%(file)s][2:​]\n"
 "       ([3:%(size)s]) -\n"
 "      added by [4:%(author)s] %(date)s."
 msgstr ""
-"[1:%(file)s]\n"
-"        [2:[3:]]\n"
-"        ([4:%(size)s) -\n"
-"        (由[5:%(author)s]在%(date)s前添加)"
 
 #: trac/templates/list_of_attachments.html:28
 #: trac/templates/list_of_attachments.html:44
@@ -2875,7 +2870,6 @@
 msgstr "任务单 #%(num)s 和所有相关数据均已删除。"
 
 #: trac/ticket/api.py:257
-#, fuzzy
 msgid "Attachment"
 msgstr "附件"
 
@@ -3486,9 +3480,8 @@
 msgstr "%(title)s: %(message)s:"
 
 #: trac/ticket/web_ui.py:242
-#, fuzzy
 msgid "Tickets opened and closed"
-msgstr "任务单启闭"
+msgstr ""
 
 #: trac/ticket/web_ui.py:244
 msgid "Ticket updates"
@@ -3707,18 +3700,18 @@
 msgstr "该任务单已创建,但在发送邮件通知时发生错误: %(message)s"
 
 #: trac/ticket/web_ui.py:1305
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "The ticket %(ticketref)s has been created. You can now attach the desired"
 " files."
-msgstr "你的任务单 %(ticketref)s 已经创建,但你没有权限查看它。"
+msgstr ""
 
 #: trac/ticket/web_ui.py:1311
-#, fuzzy, python-format
+#, python-format
 msgid ""
 "The ticket %(ticketref)s has been created, but you don't have permission "
 "to view it."
-msgstr "你的任务单 %(ticketref)s 已经创建,但你没有权限查看它。"
+msgstr "任务单%(ticketref)s已创建,但你没有权限查看它。"
 
 #. TRANSLATOR: The 'change' has been saved... (link)
 #: trac/ticket/web_ui.py:1338
@@ -4436,7 +4429,7 @@
 msgstr[0] "跟随:"
 
 #: trac/ticket/templates/ticket_change.html:53
-#, fuzzy, python-format
+#, python-format
 msgid "Changed %(date)s by %(author)s"
 msgstr "由 %(author)s 在 %(date)s 前变更"
 
@@ -4461,8 +4454,6 @@
 "[1:[2:%(name)s]][3:​]\n"
 "          added"
 msgstr ""
-"[1:[2:%(name)s]]\n"
-"          [3:[4:]]被添加"
 
 #: trac/ticket/templates/ticket_change.html:88
 #, python-format
@@ -5684,9 +5675,8 @@
 msgstr "查看文件(不包含标注)"
 
 #: trac/versioncontrol/web_ui/browser.py:469
-#, fuzzy
 msgid "Blame"
-msgstr "名称"
+msgstr ""
 
 #: trac/versioncontrol/web_ui/browser.py:470
 msgid ""
@@ -6040,9 +6030,9 @@
 "%(traceback)s}}}"
 
 #: trac/web/session.py:245
-#, fuzzy, python-format
+#, python-format
 msgid "Session '%(id)s' already exists. Please choose a different session ID."
-msgstr "会话'%(id)s'已经存在,<br />请选择其它会话标识。"
+msgstr ""
 
 #: trac/web/session.py:248
 msgid "Error renaming session"
@@ -6053,21 +6043,19 @@
 msgstr "SID"
 
 #: trac/web/session.py:417
-#, fuzzy
 msgid "Auth"
-msgstr "路径"
+msgstr ""
 
 #: trac/web/session.py:417
-#, fuzzy
 msgid "Last Visit"
-msgstr "最新修订版"
+msgstr "上次访问"
 
 #: trac/web/session.py:418
 msgid "Email"
 msgstr "邮件地址"
 
 #: trac/web/session.py:427
-#, fuzzy, python-format
+#, python-format
 msgid "Session '%(sid)s' already exists"
 msgstr ""
 
@@ -6077,12 +6065,12 @@
 msgstr "非法属性'%(attr)s'"
 
 #: trac/web/session.py:445
-#, fuzzy, python-format
+#, python-format
 msgid "Session '%(sid)s' not found"
-msgstr "没有找到会话标识%(sid)s"
+msgstr ""
 
 #: trac/wiki/admin.py:113
-#, fuzzy, python-format
+#, python-format
 msgid "Page '%(page)s' not found"
 msgstr ""
 
@@ -6176,9 +6164,8 @@
 msgstr ""
 
 #: trac/wiki/intertrac.py:119
-#, fuzzy
 msgid "The Trac Project"
-msgstr "搜索 %(project)s"
+msgstr "The Trac Project"
 
 #: trac/wiki/interwiki.py:163
 msgid "Provide a description list for the known InterWiki prefixes."
@@ -6870,7 +6857,7 @@
 msgstr "[1:最后修改于] %(reldate)s 前"
 
 #: trac/wiki/templates/wiki_view.html:62
-#, fuzzy, python-format
+#, python-format
 msgid "Last modified on %(date)s"
 msgstr ""
 
diff --git a/trac/trac/locale/zh_TW/LC_MESSAGES/messages-js.po b/trac/trac/locale/zh_TW/LC_MESSAGES/messages-js.po
index 0aac7e5..147117d 100644
--- a/trac/trac/locale/zh_TW/LC_MESSAGES/messages-js.po
+++ b/trac/trac/locale/zh_TW/LC_MESSAGES/messages-js.po
@@ -15,7 +15,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: trac/htdocs/js/blame.js:84
 msgid "(no changeset information)"
diff --git a/trac/trac/locale/zh_TW/LC_MESSAGES/messages.po b/trac/trac/locale/zh_TW/LC_MESSAGES/messages.po
index 73ae413..ef7f111 100644
--- a/trac/trac/locale/zh_TW/LC_MESSAGES/messages.po
+++ b/trac/trac/locale/zh_TW/LC_MESSAGES/messages.po
@@ -16,7 +16,7 @@
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 0.9.6dev-r0\n"
+"Generated-By: Babel 0.9.6\n"
 
 #: tracopt/mimeview/php.py:96
 msgid ""
@@ -739,7 +739,7 @@
 msgstr ""
 "歡迎來到 trac-admin %(version)s\n"
 "互動式 Trac 管理終端機\n"
-"版權所有 (C) 2003-2012 Edgewall Software\n"
+"版權所有 (C) 2003-2013 Edgewall Software\n"
 "\n"
 "輸入 '?' 或 'help' 來取得命令說明\n"
 "        "
@@ -2051,12 +2051,11 @@
 "        [1:http://trac.edgewall.org/]"
 
 #: trac/templates/about.html:46
-#, fuzzy
 msgid ""
 "Copyright © 2003-2013\n"
 "        [1:Edgewall Software]"
 msgstr ""
-"版權所有 © 2003-2012\n"
+"版權所有 © 2003-2013\n"
 "        [1:Edgewall Software]"
 
 #: trac/templates/about.html:54
diff --git a/trac/trac/mimeview/__init__.py b/trac/trac/mimeview/__init__.py
index 06b4ed5..cfdd3e2 100644
--- a/trac/trac/mimeview/__init__.py
+++ b/trac/trac/mimeview/__init__.py
@@ -1 +1,14 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
+
 from trac.mimeview.api import *
diff --git a/trac/trac/mimeview/api.py b/trac/trac/mimeview/api.py
index 2e3ca1d..7f01008 100644
--- a/trac/trac/mimeview/api.py
+++ b/trac/trac/mimeview/api.py
@@ -107,7 +107,7 @@
     set up nested contexts for each matching ticket that will be used for
     rendering the ticket descriptions.
 
-    :since: version 0.11
+    :since: version 1.0
     """
 
     def __init__(self, resource, href=None, perm=None):
@@ -131,7 +131,9 @@
 
     @staticmethod
     def from_request(*args, **kwargs):
-        """:deprecated: since 1.0, use `web_context` instead."""
+        """:deprecated: since 1.0, use `web_context` instead. Will be removed
+                        in release 1.3.1.
+        """
         from trac.web.chrome import web_context
         return web_context(*args, **kwargs)
 
@@ -279,63 +281,67 @@
 
 
 class Context(RenderingContext):
-    """:deprecated: old name kept for compatibility, use `RenderingContext`."""
+    """
+    :deprecated: since 1.0, use `RenderingContext` instead. `Context` is
+                 kept for compatibility and will be removed release 1.3.1.
+    """
 
 
 # Some common MIME types and their associated keywords and/or file extensions
 
 KNOWN_MIME_TYPES = {
-    'application/javascript': 'js',
-    'application/msword':     'doc dot',
-    'application/pdf':        'pdf',
-    'application/postscript': 'ps',
-    'application/rtf':        'rtf',
-    'application/x-sh':       'sh',
-    'application/x-csh':      'csh',
-    'application/x-troff':    'nroff roff troff',
-    'application/x-yaml':     'yml yaml',
+    'application/javascript':  'js',
+    'application/msword':      'doc dot',
+    'application/pdf':         'pdf',
+    'application/postscript':  'ps',
+    'application/rtf':         'rtf',
+    'application/x-dos-batch': 'bat batch cmd dos',
+    'application/x-sh':        'sh',
+    'application/x-csh':       'csh',
+    'application/x-troff':     'nroff roff troff',
+    'application/x-yaml':      'yml yaml',
 
-    'application/rss+xml':    'rss',
-    'application/xsl+xml':    'xsl',
-    'application/xslt+xml':   'xslt',
+    'application/rss+xml':     'rss',
+    'application/xsl+xml':     'xsl',
+    'application/xslt+xml':    'xslt',
 
-    'image/x-icon':           'ico',
-    'image/svg+xml':          'svg',
+    'image/x-icon':            'ico',
+    'image/svg+xml':           'svg',
 
-    'model/vrml':             'vrml wrl',
+    'model/vrml':              'vrml wrl',
 
-    'text/css':               'css',
-    'text/html':              'html htm',
-    'text/plain':             'txt TXT text README INSTALL '
-                              'AUTHORS COPYING ChangeLog RELEASE',
-    'text/xml':               'xml',
+    'text/css':                'css',
+    'text/html':               'html htm',
+    'text/plain':              'txt TXT text README INSTALL '
+                               'AUTHORS COPYING ChangeLog RELEASE',
+    'text/xml':                'xml',
 
     # see also TEXT_X_TYPES below
-    'text/x-csrc':            'c xs',
-    'text/x-chdr':            'h',
-    'text/x-c++src':          'cc CC cpp C c++ C++',
-    'text/x-c++hdr':          'hh HH hpp H',
-    'text/x-csharp':          'cs c# C#',
-    'text/x-diff':            'patch',
-    'text/x-eiffel':          'e',
-    'text/x-elisp':           'el',
-    'text/x-fortran':         'f',
-    'text/x-haskell':         'hs',
-    'text/x-ini':             'ini cfg',
-    'text/x-objc':            'm mm',
-    'text/x-ocaml':           'ml mli',
-    'text/x-makefile':        'make mk Makefile GNUMakefile',
-    'text/x-pascal':          'pas',
-    'text/x-perl':            'pl pm PL',
-    'text/x-php':             'php3 php4',
-    'text/x-python':          'py',
-    'text/x-pyrex':           'pyx',
-    'text/x-ruby':            'rb',
-    'text/x-scheme':          'scm',
-    'text/x-textile':         'txtl',
-    'text/x-vba':             'vb vba bas',
-    'text/x-verilog':         'v',
-    'text/x-vhdl':            'vhd',
+    'text/x-csrc':             'c xs',
+    'text/x-chdr':             'h',
+    'text/x-c++src':           'cc CC cpp C c++ C++',
+    'text/x-c++hdr':           'hh HH hpp H',
+    'text/x-csharp':           'cs c# C#',
+    'text/x-diff':             'patch',
+    'text/x-eiffel':           'e',
+    'text/x-elisp':            'el',
+    'text/x-fortran':          'f',
+    'text/x-haskell':          'hs',
+    'text/x-ini':              'ini cfg',
+    'text/x-objc':             'm mm',
+    'text/x-ocaml':            'ml mli',
+    'text/x-makefile':         'make mk Makefile GNUMakefile',
+    'text/x-pascal':           'pas',
+    'text/x-perl':             'pl pm PL',
+    'text/x-php':              'php3 php4',
+    'text/x-python':           'py',
+    'text/x-pyrex':            'pyx',
+    'text/x-ruby':             'rb',
+    'text/x-scheme':           'scm',
+    'text/x-textile':          'txtl',
+    'text/x-vba':              'vb vba bas',
+    'text/x-verilog':          'v',
+    'text/x-vhdl':             'vhd',
 }
 for t in KNOWN_MIME_TYPES.keys():
     types = KNOWN_MIME_TYPES[t].split()
@@ -866,7 +872,8 @@
         )
 
     def get_max_preview_size(self):
-        """:deprecated: use `max_preview_size` attribute directly."""
+        """:deprecated: since 0.10, use `max_preview_size` attribute directly.
+        """
         return self.max_preview_size
 
     def get_charset(self, content='', mimetype=None):
@@ -1108,7 +1115,7 @@
     """Text annotator that adds a column with line numbers."""
     implements(IHTMLPreviewAnnotator)
 
-    # ITextAnnotator methods
+    # IHTMLPreviewAnnotator methods
 
     def get_annotation_type(self):
         return 'lineno', _('Line'), _('Line numbers')
diff --git a/trac/trac/mimeview/rst.py b/trac/trac/mimeview/rst.py
index bce5cac..4df4aca 100644
--- a/trac/trac/mimeview/rst.py
+++ b/trac/trac/mimeview/rst.py
@@ -29,6 +29,7 @@
     from docutils import nodes
     from docutils.core import publish_parts
     from docutils.parsers import rst
+    from docutils.readers import standalone
     from docutils import __version__
     has_docutils = True
 except ImportError:
@@ -270,9 +271,10 @@
         inliner.trac = (self.env, context)
         parser = rst.Parser(inliner=inliner)
         content = content_to_unicode(self.env, content, mimetype)
+        # The default Reader is explicitly passed as a workaround for #11248
         parts = publish_parts(content, writer=writer, parser=parser,
+                              reader=standalone.Reader(parser),
                               settings_overrides={'halt_level': 6,
-                                                  'warning_stream': False,
                                                   'file_insertion_enabled': 0,
                                                   'raw_enabled': 0,
                                                   'warning_stream': False})
diff --git a/trac/trac/mimeview/tests/__init__.py b/trac/trac/mimeview/tests/__init__.py
index 7a2a95a..ad903ce 100644
--- a/trac/trac/mimeview/tests/__init__.py
+++ b/trac/trac/mimeview/tests/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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/.
+
 from trac.mimeview.tests import api, patch, pygments
 
 import unittest
diff --git a/trac/trac/mimeview/tests/api.py b/trac/trac/mimeview/tests/api.py
index d1845dc..56668af 100644
--- a/trac/trac/mimeview/tests/api.py
+++ b/trac/trac/mimeview/tests/api.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2006-2009 Edgewall Software
+# Copyright (C) 2006-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -14,8 +14,8 @@
 import doctest
 import unittest
 from StringIO import StringIO
-import sys
 
+import trac.tests.compat
 from trac.core import *
 from trac.test import EnvironmentStub
 from trac.mimeview import api
@@ -112,22 +112,22 @@
     def test_text_only_stream(self):
         input = [(TEXT, "test", (None, -1, -1))]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), 1)
-        self.assertTrue(isinstance(lines[0], Stream))
-        self.assertEquals(lines[0].events, input)
+        self.assertEqual(len(lines), 1)
+        self.assertIsInstance(lines[0], Stream)
+        self.assertEqual(lines[0].events, input)
 
     def test_text_only_stream2(self):
         input = [(TEXT, "test\n", (None, -1, -1))]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), 1)
-        self.assertTrue(isinstance(lines[0], Stream))
-        self.assertEquals(lines[0].events, [(TEXT, "test", (None, -1, -1))])
+        self.assertEqual(len(lines), 1)
+        self.assertIsInstance(lines[0], Stream)
+        self.assertEqual(lines[0].events, [(TEXT, "test", (None, -1, -1))])
 
     def test_simplespan(self):
         input = HTMLParser(StringIO(u"<span>test</span>"), encoding=None)
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), 1)
-        self.assertTrue(isinstance(lines[0], Stream))
+        self.assertEqual(len(lines), 1)
+        self.assertIsInstance(lines[0], Stream)
         for (a, b) in zip(lines[0], input):
             self.assertEqual(a, b)
 
@@ -137,17 +137,17 @@
         """
         input = [(TEXT, "", (None, -1, -1))]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), 0)
+        self.assertEqual(len(lines), 0)
 
     def test_newline_stream(self):
         input = [(TEXT, "\n", (None, -1, -1))]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), 1)
+        self.assertEqual(len(lines), 1)
 
     def test_newline_stream2(self):
         input = [(TEXT, "\n\n\n", (None, -1, -1))]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), 3)
+        self.assertEqual(len(lines), 3)
 
     def test_empty_text_in_span(self):
         """
@@ -172,9 +172,9 @@
                     '<span class="c">b</span>',
                    ]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), len(expected))
+        self.assertEqual(len(lines), len(expected))
         for a, b in zip(lines, expected):
-            self.assertEquals(a.render('html'), b)
+            self.assertEqual(a.render('html'), b)
 
     def test_newline2(self):
         """
@@ -187,9 +187,9 @@
                     '<span class="c">b</span>',
                    ]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), len(expected))
+        self.assertEqual(len(lines), len(expected))
         for a, b in zip(lines, expected):
-            self.assertEquals(a.render('html'), b)
+            self.assertEqual(a.render('html'), b)
 
     def test_multinewline(self):
         """
@@ -203,17 +203,17 @@
                     '<span class="c">a</span>',
                    ]
         lines = list(_group_lines(input))
-        self.assertEquals(len(lines), len(expected))
+        self.assertEqual(len(lines), len(expected))
         for a, b in zip(lines, expected):
-            self.assertEquals(a.render('html'), b)
+            self.assertEqual(a.render('html'), b)
 
 
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(api))
-    suite.addTest(unittest.makeSuite(GetMimeTypeTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(MimeviewTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(GroupLinesTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(GetMimeTypeTestCase))
+    suite.addTest(unittest.makeSuite(MimeviewTestCase))
+    suite.addTest(unittest.makeSuite(GroupLinesTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/mimeview/tests/patch.html b/trac/trac/mimeview/tests/patch.html
index 69a8734..b9a25d0 100644
--- a/trac/trac/mimeview/tests/patch.html
+++ b/trac/trac/mimeview/tests/patch.html
@@ -36,26 +36,26 @@
               </thead>
             <tbody class="unmod">
                   <tr>
-                          <th>1</th><th>1</th><td class="l"><span>----</span> </td>
+                          <th>1</th><th>1</th><td class="l"><span>----</span></td>
                   </tr>
             </tbody><tbody class="mod">
                       <tr class="first">
-                        <th>2</th><th> </th><td class="l"><span>b<del>as</del>e</span> </td>
+                        <th>2</th><th> </th><td class="l"><span>b<del>as</del>e</span></td>
                       </tr><tr>
-                        <th>3</th><th> </th><td class="l"><span><del></del>base</span> </td>
+                        <th>3</th><th> </th><td class="l"><span><del></del>base</span></td>
                       </tr><tr>
-                        <th>4</th><th> </th><td class="l"><span>base<del></del></span> </td>
+                        <th>4</th><th> </th><td class="l"><span>base<del></del></span></td>
                       </tr>
                       <tr>
-                        <th> </th><th>2</th><td class="r"><span>b<ins></ins>e</span> </td>
+                        <th> </th><th>2</th><td class="r"><span>b<ins></ins>e</span></td>
                       </tr><tr>
-                        <th> </th><th>3</th><td class="r"><span><ins>the </ins>base</span> </td>
+                        <th> </th><th>3</th><td class="r"><span><ins>the </ins>base</span></td>
                       </tr><tr class="last">
-                        <th> </th><th>4</th><td class="r"><span>base<ins> modified</ins></span> </td>
+                        <th> </th><th>4</th><td class="r"><span>base<ins> modified</ins></span></td>
                       </tr>
             </tbody><tbody class="unmod">
                   <tr>
-                          <th>5</th><th>5</th><td class="l"><span>.</span> </td>
+                          <th>5</th><th>5</th><td class="l"><span>.</span></td>
                   </tr>
             </tbody>
         </table>
@@ -88,12 +88,12 @@
               </thead>
             <tbody class="mod">
                       <tr class="first">
-                        <th>1</th><th>&nbsp;</th><td class="l"><span>ONELINE</span>&nbsp;</td>
+                        <th>1</th><th>&nbsp;</th><td class="l"><span>ONELINE</span></td>
                       </tr><tr>
-                        <th>2</th><th>&nbsp;</th><td class="l"><span><em>&nbsp;No newline at end of file</em></span>&nbsp;</td>
+                        <th>2</th><th>&nbsp;</th><td class="l"><span><em>&nbsp;No newline at end of file</em></span></td>
                       </tr>
                       <tr class="last">
-                        <th>&nbsp;</th><th>1</th><td class="r"><span>ONELINE</span>&nbsp;</td>
+                        <th>&nbsp;</th><th>1</th><td class="r"><span>ONELINE</span></td>
                       </tr>
             </tbody>
         </table>
@@ -126,12 +126,12 @@
               </thead>
             <tbody class="mod">
                       <tr class="first">
-                        <th>1</th><th> </th><td class="l"><span>ONELINE</span> </td>
+                        <th>1</th><th> </th><td class="l"><span>ONELINE</span></td>
                       </tr>
                       <tr>
-                        <th> </th><th>1</th><td class="r"><span>ONELINE</span> </td>
+                        <th> </th><th>1</th><td class="r"><span>ONELINE</span></td>
                       </tr><tr class="last">
-                        <th> </th><th>2</th><td class="r"><span><em>&nbsp;No newline at end of file</em></span> </td>
+                        <th> </th><th>2</th><td class="r"><span><em>&nbsp;No newline at end of file</em></span></td>
                       </tr>
             </tbody>
         </table>
diff --git a/trac/trac/mimeview/tests/patch.py b/trac/trac/mimeview/tests/patch.py
index a539115..a83f5e3 100644
--- a/trac/trac/mimeview/tests/patch.py
+++ b/trac/trac/mimeview/tests/patch.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2006-2009 Edgewall Software
+# Copyright (C) 2006-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -17,7 +17,7 @@
 from genshi.core import Stream
 from genshi.input import HTMLParser, XML
 
-from trac.mimeview.api import Mimeview, RenderingContext
+from trac.mimeview.api import Mimeview
 from trac.mimeview.patch import PatchRenderer
 from trac.test import EnvironmentStub, Mock, MockPerm
 from trac.web.chrome import Chrome, web_context
@@ -45,8 +45,8 @@
         result = XML(result.render(encoding='utf-8')).render(encoding='utf-8')
         expected, result = expected.splitlines(), result.splitlines()
         for exp, res in zip(expected, result):
-            self.assertEquals(exp, res)
-        self.assertEquals(len(expected), len(result))
+            self.assertEqual(exp, res)
+        self.assertEqual(len(expected), len(result))
 
     def test_simple(self):
         """
@@ -105,10 +105,10 @@
              '@@ -1 +1 @@',
              '-aa\tb',
              '+aaxb'], 8)
-        self.assertEquals('aa<del>&nbsp; &nbsp; &nbsp; </del>b',
-                          str(changes[0]['diffs'][0][0]['base']['lines'][0]))
-        self.assertEquals('aa<ins>x</ins>b',
-                          str(changes[0]['diffs'][0][0]['changed']['lines'][0]))
+        self.assertEqual('aa<del>&nbsp; &nbsp; &nbsp; </del>b',
+                         str(changes[0]['diffs'][0][0]['base']['lines'][0]))
+        self.assertEqual('aa<ins>x</ins>b',
+                         str(changes[0]['diffs'][0][0]['changed']['lines'][0]))
 
     def test_diff_to_hdf_leading_ws(self):
         """Regression test related to #5795"""
@@ -118,14 +118,14 @@
              '@@ -1 +1 @@',
              '-*a',
              '+ *a'], 8)
-        self.assertEquals('<del></del>*a',
-                          str(changes[0]['diffs'][0][0]['base']['lines'][0]))
-        self.assertEquals('<ins>&nbsp;</ins>*a',
-                          str(changes[0]['diffs'][0][0]['changed']['lines'][0]))
+        self.assertEqual('<del></del>*a',
+                         str(changes[0]['diffs'][0][0]['base']['lines'][0]))
+        self.assertEqual('<ins>&nbsp;</ins>*a',
+                         str(changes[0]['diffs'][0][0]['changed']['lines'][0]))
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(PatchRendererTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(PatchRendererTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/mimeview/tests/pygments.py b/trac/trac/mimeview/tests/pygments.py
index 43ff411..3da341f 100644
--- a/trac/trac/mimeview/tests/pygments.py
+++ b/trac/trac/mimeview/tests/pygments.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2006-2009 Edgewall Software
+# Copyright (C) 2006-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -23,7 +23,8 @@
 except ImportError:
     have_pygments = False
 
-from trac.mimeview.api import Mimeview, RenderingContext
+import trac.tests.compat
+from trac.mimeview.api import Mimeview
 if have_pygments:
     from trac.mimeview.pygments import PygmentsRenderer
 from trac.test import EnvironmentStub, Mock
@@ -56,8 +57,8 @@
         #print "\nR: " + repr(result)
         expected, result = expected.splitlines(), result.splitlines()
         for exp, res in zip(expected, result):
-            self.assertEquals(exp, res)
-        self.assertEquals(len(expected), len(result))
+            self.assertEqual(exp, res)
+        self.assertEqual(len(expected), len(result))
 
     def test_python_hello(self):
         """
@@ -108,7 +109,7 @@
         pygments when rendering empty files.
         """
         result = self.pygments.render(self.context, 'text/x-python', '')
-        self.assertEqual(None, result)
+        self.assertIsNone(result)
 
     def test_extra_mimetypes(self):
         """
@@ -126,7 +127,7 @@
 def suite():
     suite = unittest.TestSuite()
     if have_pygments:
-        suite.addTest(unittest.makeSuite(PygmentsRendererTestCase, 'test'))
+        suite.addTest(unittest.makeSuite(PygmentsRendererTestCase))
     else:
         print 'SKIP: mimeview/tests/pygments (no pygments installed)'
     return suite
diff --git a/trac/trac/notification.py b/trac/trac/notification.py
index 9a45394..8409182 100644
--- a/trac/trac/notification.py
+++ b/trac/trac/notification.py
@@ -22,11 +22,13 @@
 from genshi.builder import tag
 
 from trac import __version__
-from trac.config import BoolOption, ExtensionOption, IntOption, Option
+from trac.config import BoolOption, ConfigurationError, ExtensionOption, \
+                        IntOption, Option
 from trac.core import *
 from trac.util.compat import close_fds
-from trac.util.text import CRLF, fix_eol
-from trac.util.translation import _, deactivate, reactivate
+from trac.util.html import to_fragment
+from trac.util.text import CRLF, fix_eol, to_unicode
+from trac.util.translation import _, deactivate, reactivate, tag_
 
 MAXHEADERLEN = 76
 EMAIL_LOOKALIKE_PATTERN = (
@@ -151,11 +153,20 @@
 
         self.log.info("Sending notification through SMTP at %s:%d to %s"
                       % (self.smtp_server, self.smtp_port, recipients))
-        server = smtplib.SMTP(self.smtp_server, self.smtp_port)
+        try:
+            server = smtplib.SMTP(self.smtp_server, self.smtp_port)
+        except smtplib.socket.error, e:
+            raise ConfigurationError(
+                tag_("SMTP server connection error (%(error)s). Please "
+                     "modify %(option1)s or %(option2)s in your "
+                     "configuration.",
+                     error=to_unicode(e),
+                     option1=tag.tt("[notification] smtp_server"),
+                     option2=tag.tt("[notification] smtp_port")))
         # server.set_debuglevel(True)
         if self.use_tls:
             server.ehlo()
-            if not server.esmtp_features.has_key('starttls'):
+            if 'starttls' not in server.esmtp_features:
                 raise TracError(_("TLS enabled but server does not support " \
                                   "TLS"))
             server.starttls()
@@ -201,8 +212,15 @@
         cmdline = [self.sendmail_path, "-i", "-f", from_addr]
         cmdline.extend(recipients)
         self.log.debug("Sendmail command line: %s" % cmdline)
-        child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE,
-                      stderr=PIPE, close_fds=close_fds)
+        try:
+            child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE,
+                          stderr=PIPE, close_fds=close_fds)
+        except OSError, e:
+            raise ConfigurationError(
+                tag_("Sendmail error (%(error)s). Please modify %(option)s "
+                     "in your configuration.",
+                     error=to_unicode(e),
+                     option=tag.tt("[notification] sendmail_path")))
         out, err = child.communicate(message)
         if child.returncode or err:
             raise Exception("Sendmail failed with (%s, %s), command: '%s'"
@@ -227,7 +245,7 @@
         self.data = Chrome(self.env).populate_data(None, {'CRLF': CRLF})
 
     def notify(self, resid):
-        (torcpts, ccrcpts) = self.get_recipients(resid)
+        torcpts, ccrcpts = self.get_recipients(resid)
         self.begin_send()
         self.send(torcpts, ccrcpts)
         self.finish_send()
@@ -333,30 +351,44 @@
         self.from_email = from_email or self.replyto_email
         self.from_name = from_name
         if not self.from_email and not self.replyto_email:
-            raise TracError(tag(
-                    tag.p(_('Unable to send email due to identity crisis.')),
-                    tag.p(_('Neither %(from_)s nor %(reply_to)s are specified '
-                            'in the configuration.',
-                            from_=tag.b('notification.from'),
-                            reply_to=tag.b('notification.reply_to')))),
-                _('SMTP Notification Error'))
+            message = tag(
+                tag.p(_('Unable to send email due to identity crisis.')),
+                # convert explicitly to `Fragment` to avoid breaking message
+                # when passing `LazyProxy` object to `Fragment`
+                tag.p(to_fragment(tag_(
+                    "Neither %(from_)s nor %(reply_to)s are specified in the "
+                    "configuration.",
+                    from_=tag.strong('[notification] smtp_from'),
+                    reply_to=tag.strong('[notification] smtp_replyto')))))
+            raise TracError(message, _('SMTP Notification Error'))
 
         Notify.notify(self, resid)
 
+    _mime_encoding_re = re.compile(r'=\?[^?]+\?[bq]\?[^?]+\?=', re.IGNORECASE)
+
     def format_header(self, key, name, email=None):
         from email.Header import Header
         maxlength = MAXHEADERLEN-(len(key)+2)
         # Do not sent ridiculous short headers
         if maxlength < 10:
             raise TracError(_("Header length is too short"))
-        try:
-            tmp = name.encode('ascii')
-            header = Header(tmp, 'ascii', maxlinelen=maxlength)
-        except UnicodeEncodeError:
-            header = Header(name, self._charset, maxlinelen=maxlength)
+        # when it matches mime-encoding, encode as mime even if only
+        # ascii characters
+        header = None
+        if not self._mime_encoding_re.search(name):
+            try:
+                tmp = name.encode('ascii')
+                header = Header(tmp, 'ascii', maxlinelen=maxlength)
+            except UnicodeEncodeError:
+                pass
+        if not header:
+            header = Header(name.encode(self._charset.output_codec),
+                            self._charset, maxlinelen=maxlength)
         if not email:
             return header
         else:
+            header = str(header).replace('\\', r'\\') \
+                                .replace('"', r'\"')
             return '"%s" <%s>' % (header, email)
 
     def add_headers(self, msg, headers):
diff --git a/trac/trac/perm.py b/trac/trac/perm.py
index 9ba9184..4280f15 100644
--- a/trac/trac/perm.py
+++ b/trac/trac/perm.py
@@ -31,7 +31,7 @@
 from trac.util import file_or_std
 from trac.util.text import path_to_unicode, print_table, printout, \
                            stream_encoding, to_unicode, wrap
-from trac.util.translation import _
+from trac.util.translation import _, N_
 
 __all__ = ['IPermissionRequestor', 'IPermissionStore', 'IPermissionPolicy',
            'IPermissionGroupProvider', 'PermissionError', 'PermissionSystem']
@@ -40,29 +40,27 @@
 class PermissionError(StandardError):
     """Insufficient permissions to complete the operation"""
 
+    title = N_("Forbidden")
+
     def __init__ (self, action=None, resource=None, env=None, msg=None):
-        StandardError.__init__(self)
         self.action = action
         self.resource = resource
         self.env = env
-        self.msg = msg
-
-    def __unicode__ (self):
         if self.action:
             if self.resource:
-                return _('%(perm)s privileges are required to perform '
-                         'this operation on %(resource)s. You don\'t have the '
-                         'required permissions.',
-                         perm=self.action,
-                         resource=get_resource_name(self.env, self.resource))
+                msg = _("%(perm)s privileges are required to perform "
+                        "this operation on %(resource)s. You don't have the "
+                        "required permissions.",
+                        perm=self.action,
+                        resource=get_resource_name(self.env, self.resource))
             else:
-                return _('%(perm)s privileges are required to perform this '
-                         'operation. You don\'t have the required '
-                         'permissions.', perm=self.action)
-        elif self.msg:
-            return self.msg
-        else:
-            return _('Insufficient privileges to perform this operation.')
+                msg = _("%(perm)s privileges are required to perform this "
+                        "operation. You don't have the required "
+                        "permissions.", perm=self.action)
+        elif msg is None:
+            msg = _("Insufficient privileges to perform this operation.")
+        self.msg = msg
+        StandardError.__init__(self, msg)
 
 
 class IPermissionRequestor(Interface):
@@ -595,10 +593,14 @@
 
     __contains__ = has_permission
 
-    def require(self, action, realm_or_resource=None, id=False, version=False):
+    def require(self, action, realm_or_resource=None, id=False, version=False,
+                message=None):
         resource = self._normalize_resource(realm_or_resource, id, version)
         if not self._has_permission(action, resource):
-            raise PermissionError(action, resource, self.env)
+            if message is None:
+                raise PermissionError(action, resource, self.env)
+            else:
+                raise PermissionError(msg=message)
     assert_permission = require
 
     def permissions(self):
@@ -707,9 +709,17 @@
                     permsys.revoke_permission(u, a)
                     found = True
             if not found:
-                raise AdminCommandError(
-                    _("Cannot remove permission %(action)s for user %(user)s.",
-                      action=action, user=user))
+                if user in self.get_user_list() and \
+                        action in permsys.get_user_permissions(user):
+                    msg = _("Cannot remove permission %(action)s for user "
+                            "%(user)s. The permission is granted through "
+                            "a meta-permission or group.", action=action,
+                            user=user)
+                else:
+                    msg = _("Cannot remove permission %(action)s for user "
+                            "%(user)s. The user has not been granted the "
+                            "permission.", action=action, user=user)
+                raise AdminCommandError(msg)
 
     def _do_export(self, filename=None):
         try:
diff --git a/trac/trac/prefs/__init__.py b/trac/trac/prefs/__init__.py
index da92b64..dea6712 100644
--- a/trac/trac/prefs/__init__.py
+++ b/trac/trac/prefs/__init__.py
@@ -1 +1,14 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2014 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.prefs.api import *
diff --git a/trac/trac/prefs/api.py b/trac/trac/prefs/api.py
index 28ad39f..f3b5c64 100644
--- a/trac/trac/prefs/api.py
+++ b/trac/trac/prefs/api.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2006-2009 Edgewall Software
+# Copyright (C) 2006-2014 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
diff --git a/trac/trac/prefs/templates/prefs.html b/trac/trac/prefs/templates/prefs.html
index 15745e6..4587643 100644
--- a/trac/trac/prefs/templates/prefs.html
+++ b/trac/trac/prefs/templates/prefs.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/prefs/templates/prefs_advanced.html b/trac/trac/prefs/templates/prefs_advanced.html
index b28c223..099ca29 100644
--- a/trac/trac/prefs/templates/prefs_advanced.html
+++ b/trac/trac/prefs/templates/prefs_advanced.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/prefs/templates/prefs_datetime.html b/trac/trac/prefs/templates/prefs_datetime.html
index ea501d2..f812c98 100644
--- a/trac/trac/prefs/templates/prefs_datetime.html
+++ b/trac/trac/prefs/templates/prefs_datetime.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/prefs/templates/prefs_general.html b/trac/trac/prefs/templates/prefs_general.html
index 1e07e03..c666d1a 100644
--- a/trac/trac/prefs/templates/prefs_general.html
+++ b/trac/trac/prefs/templates/prefs_general.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/prefs/templates/prefs_keybindings.html b/trac/trac/prefs/templates/prefs_keybindings.html
index 14d68ca..f1d4d58 100644
--- a/trac/trac/prefs/templates/prefs_keybindings.html
+++ b/trac/trac/prefs/templates/prefs_keybindings.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/prefs/templates/prefs_language.html b/trac/trac/prefs/templates/prefs_language.html
index b3ea0ff..4ab76ef 100644
--- a/trac/trac/prefs/templates/prefs_language.html
+++ b/trac/trac/prefs/templates/prefs_language.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2008-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -13,22 +23,31 @@
 
     <div class="field" py:with="session_language = settings.session.get('language', '').replace('-', '_')">
       <label>Language:
-        <select name="language">
+        <select name="language" disabled="${'disabled' if not languages else None}"
+                title="${_('Translations are currently unavailable') if not languages else None}">
           <option value="">Default language</option>
           <option py:for="locale, language in languages"
                   selected="${session_language == locale or None}"
                   value="$locale">$language</option>
         </select>
       </label>
-      <p class="hint">Configuring your language will result in all text
-      displayed on this site to use your language instead of that of the
-      server.</p>
+      <span py:if="not has_babel" class="hint">
+        Install Babel for extended language support.
+      </span>
+      <span py:if="'TRAC_ADMIN' in req.perm and has_babel and not languages" class="hint">
+        Message catalogs have not been compiled.
+      </span>
 
-      <p class="hint" i18n:msg="">The <strong>Default language</strong> option uses the browser's
+      <div py:if="languages">
+        <p class="hint">Configuring your language will result in all text
+        displayed on this site to use your language instead of that of the
+        server.</p>
+
+        <p class="hint" i18n:msg="">The <strong>Default language</strong> option uses the browser's
         language negotiation feature to select the appropriate language.</p>
+      </div>
 
       <p py:if="not languages" class="hint" xml:lang="en">
-        <strong>Note:</strong> Translations are currently unavailable.
         <py:choose>
           <py:when test="'TRAC_ADMIN' in req.perm">
             Trac has been localized to more than a dozen of languages but in order
diff --git a/trac/trac/prefs/templates/prefs_pygments.html b/trac/trac/prefs/templates/prefs_pygments.html
index 95af5c4..000c4d8 100644
--- a/trac/trac/prefs/templates/prefs_pygments.html
+++ b/trac/trac/prefs/templates/prefs_pygments.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/prefs/templates/prefs_userinterface.html b/trac/trac/prefs/templates/prefs_userinterface.html
index 57d355d..be380ed 100644
--- a/trac/trac/prefs/templates/prefs_userinterface.html
+++ b/trac/trac/prefs/templates/prefs_userinterface.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2012-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/prefs/tests/__init__.py b/trac/trac/prefs/tests/__init__.py
index c73c0a2..5afb396 100644
--- a/trac/trac/prefs/tests/__init__.py
+++ b/trac/trac/prefs/tests/__init__.py
@@ -1 +1,14 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
+
 from trac.prefs.tests.functional import functionalSuite
diff --git a/trac/trac/prefs/tests/functional.py b/trac/trac/prefs/tests/functional.py
index 6a2c830..891e901 100755
--- a/trac/trac/prefs/tests/functional.py
+++ b/trac/trac/prefs/tests/functional.py
@@ -1,4 +1,17 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
+
 from trac.tests.functional import *
 
 
@@ -6,37 +19,30 @@
 class TestPreferences(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Set preferences for admin user"""
-        prefs_url = self._tester.url + "/prefs"
-        tc.follow('Preferences')
-        tc.url(prefs_url)
+        self._tester.go_to_preferences()
         tc.notfind('Your preferences have been saved.')
         tc.formvalue('userprefs', 'name', ' System Administrator ')
         tc.formvalue('userprefs', 'email', ' admin@example.com ')
         tc.submit()
         tc.find('Your preferences have been saved.')
-        tc.follow('Date & Time')
-        tc.url(prefs_url + '/datetime')
+        self._tester.go_to_preferences("Date & Time")
         tc.formvalue('userprefs', 'tz', 'GMT -10:00')
         tc.submit()
         tc.find('Your preferences have been saved.')
-        tc.follow('General')
-        tc.url(prefs_url)
+        self._tester.go_to_preferences()
         tc.notfind('Your preferences have been saved.')
         tc.find('value="System Administrator"')
         tc.find(r'value="admin@example\.com"')
-        tc.follow('Date & Time')
-        tc.url(prefs_url + '/datetime')
+        self._tester.go_to_preferences("Date & Time")
         tc.find('GMT -10:00')
 
 
 class RegressionTestRev5785(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of the fix in r5785"""
-        prefs_url = self._tester.url + "/prefs"
-        tc.follow('Preferences')
-        tc.url(prefs_url)
-        tc.follow('Logout')
-        tc.notfind(internal_error) # See [5785]
+        self._tester.go_to_preferences()
+        tc.submit('logout', 'logout')
+        tc.notfind(internal_error)  # See [5785]
         tc.follow('Login')
 
 
@@ -45,9 +51,7 @@
         """Test for regression of http://trac.edgewall.org/ticket/5765
         Unable to turn off 'Enable access keys' in Preferences
         """
-        self._tester.go_to_front()
-        tc.follow('Preferences')
-        tc.follow('Keyboard Shortcuts')
+        self._tester.go_to_preferences("Keyboard Shortcuts")
         tc.formvalue('userprefs', 'accesskeys', True)
         tc.submit()
         tc.find('name="accesskeys".*checked="checked"')
@@ -56,13 +60,102 @@
         tc.notfind('name="accesskeys".*checked="checked"')
 
 
+class RegressionTestTicket11337(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11337
+        The preferences panel will only be visible when Babel is installed
+        or for a user that has `TRAC_ADMIN`.
+        """
+        from trac.util.translation import has_babel, get_available_locales
+
+        babel_hint = "Install Babel for extended language support."
+        catalog_hint = "Message catalogs have not been compiled."
+        language_select = '<select name="language">'
+        disabled_language_select = \
+            '<select name="language" disabled="disabled" ' \
+            'title="Translations are currently unavailable">'
+
+        self._tester.go_to_preferences("Language")
+        if has_babel:
+            tc.notfind(babel_hint)
+            if get_available_locales():
+                tc.find(language_select)
+                tc.notfind(catalog_hint)
+            else:
+                tc.find(disabled_language_select)
+                tc.find(catalog_hint)
+        else:
+            tc.find(babel_hint)
+            tc.find(disabled_language_select)
+            tc.notfind(catalog_hint)
+
+        # For users without TRAC_ADMIN, the Language tab should only be
+        # present when Babel is installed
+        self._tester.go_to_preferences()
+        language_tab = '<li id="tab_language">'
+        try:
+            self._tester.logout()
+            if has_babel:
+                tc.find(language_tab)
+                tc.notfind(catalog_hint)
+            else:
+                tc.notfind(language_tab)
+        finally:
+            self._tester.login('admin')
+
+
+class RegressionTestTicket11515(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11515
+        Show a notice message with new language setting after it is changed.
+        """
+        from trac.util.translation import has_babel, get_available_locales
+
+        if not has_babel:
+            return
+        for second_locale in (locale for locale in get_available_locales()
+                                     if not locale.startswith('en_')):
+            break
+        else:
+            return
+
+        try:
+            self._tester.go_to_preferences('Language')
+            tc.formvalue('userprefs', 'language', second_locale)
+            tc.submit()
+            tc.notfind('Your preferences have been saved')
+        finally:
+            tc.formvalue('userprefs', 'language', '')  # revert to default
+            tc.submit()
+            tc.find('Your preferences have been saved')
+
+
+class RegressionTestTicket11531(FunctionalTwillTestCaseSetup):
+    """Test for regression of http://trac.edgewall.org/ticket/11531
+    PreferencesModule can be set as the default_handler."""
+    def runTest(self):
+        default_handler = self._testenv.get_config('trac', 'default_handler')
+        self._testenv.set_config('trac', 'default_handler',
+                                 'PreferencesModule')
+        try:
+            tc.go(self._tester.url)
+            tc.notfind(internal_error)
+            tc.find(r"\bPreferences\b")
+        finally:
+            self._testenv.set_config('trac', 'default_handler',
+                                     default_handler)
+
+
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
     suite.addTest(TestPreferences())
     suite.addTest(RegressionTestRev5785())
     suite.addTest(RegressionTestTicket5765())
+    suite.addTest(RegressionTestTicket11337())
+    suite.addTest(RegressionTestTicket11515())
+    suite.addTest(RegressionTestTicket11531())
     return suite
 
 
diff --git a/trac/trac/prefs/web_ui.py b/trac/trac/prefs/web_ui.py
index 3a69422..0f1100d 100644
--- a/trac/trac/prefs/web_ui.py
+++ b/trac/trac/prefs/web_ui.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2004-2009 Edgewall Software
+# Copyright (C) 2004-2014 Edgewall Software
 # Copyright (C) 2004-2005 Daniel Lundin <daniel@edgewall.com>
 # All rights reserved.
 #
@@ -17,20 +17,16 @@
 import pkg_resources
 import re
 
-try:
-    from babel.core import Locale
-except ImportError:
-    Locale = None
-
 from genshi.builder import tag
 
 from trac.core import *
 from trac.prefs.api import IPreferencePanelProvider
 from trac.util.datefmt import all_timezones, get_timezone, localtz
-from trac.util.translation import _, get_available_locales
-from trac.web import HTTPNotFound, IRequestHandler
-from trac.web.chrome import add_notice, add_stylesheet, \
-                            INavigationContributor, ITemplateProvider
+from trac.util.translation import _, Locale, deactivate,\
+                                  get_available_locales, make_activable
+from trac.web.api import HTTPNotFound, IRequestHandler
+from trac.web.chrome import INavigationContributor, ITemplateProvider, \
+                            add_notice, add_stylesheet
 
 
 class PreferencesModule(Component):
@@ -44,7 +40,7 @@
         'newsid', 'name', 'email', 'tz', 'lc_time', 'dateinfo',
         'language', 'accesskeys',
         'ui.use_symbols', 'ui.hide_help',
-        ]
+    ]
 
     # INavigationContributor methods
 
@@ -52,8 +48,8 @@
         return 'prefs'
 
     def get_navigation_items(self, req):
-        yield ('metanav', 'prefs',
-               tag.a(_('Preferences'), href=req.href.prefs()))
+        yield 'metanav', 'prefs', tag.a(_("Preferences"),
+                                        href=req.href.prefs())
 
     # IRequestHandler methods
 
@@ -68,7 +64,7 @@
         if xhr and req.method == 'POST' and 'save_prefs' in req.args:
             self._do_save_xhr(req)
 
-        panel_id = req.args['panel_id']
+        panel_id = req.args.get('panel_id')
 
         panels = []
         chosen_provider = None
@@ -79,8 +75,8 @@
                     chosen_provider = provider
                 panels.append((name, label))
         if not chosen_provider:
-            self.log.warn('Unknown preference panel %r', panel_id)
-            raise HTTPNotFound(_('Unknown preference panel'))
+            self.log.warn("Unknown preference panel %r", panel_id)
+            raise HTTPNotFound(_("Unknown preference panel"))
 
         template, data = chosen_provider.render_preference_panel(req, panel_id)
         data.update({'active_panel': panel_id, 'panels': panels})
@@ -91,14 +87,14 @@
     # IPreferencePanelProvider methods
 
     def get_preference_panels(self, req):
-        yield (None, _('General'))
-        yield ('datetime', _('Date & Time'))
-        yield ('keybindings', _('Keyboard Shortcuts'))
-        yield ('userinterface', _('User Interface'))
-        if Locale:
-            yield ('language', _('Language'))
+        yield (None, _("General"))
+        yield ('datetime', _("Date & Time"))
+        yield ('keybindings', _("Keyboard Shortcuts"))
+        yield ('userinterface', _("User Interface"))
+        if Locale or 'TRAC_ADMIN' in req.perm:
+            yield ('language', _("Language"))
         if not req.authname or req.authname == 'anonymous':
-            yield ('advanced', _('Advanced'))
+            yield ('advanced', _("Advanced"))
 
     def render_preference_panel(self, req, panel):
         if req.method == 'POST':
@@ -109,18 +105,26 @@
             req.redirect(req.href.prefs(panel or None))
 
         data = {
-            'settings': {'session': req.session, 'session_id': req.session.sid},
-            'timezones': all_timezones, 'timezone': get_timezone,
-            'localtz': localtz
+            'settings': {'session': req.session,
+                         'session_id': req.session.sid},
+            'timezones': all_timezones,
+            'timezone': get_timezone,
+            'localtz': localtz,
+            'has_babel': False
         }
 
         if Locale:
-            locales = [Locale.parse(locale)
-                       for locale in get_available_locales()]
-            languages = sorted((str(locale), locale.display_name)
-                               for locale in locales)
+            locale_ids = get_available_locales()
+            locales = [Locale.parse(locale) for locale in locale_ids]
+            # use locale identifiers from get_available_locales() instead
+            # of str(locale) to prevent storing expanded locale identifier
+            # to session, e.g. zh_Hans_CN and zh_Hant_TW, since Babel 1.0.
+            # see #11258.
+            languages = sorted((id, locale.display_name)
+                               for id, locale in zip(locale_ids, locales))
             data['locales'] = locales
             data['languages'] = languages
+            data['has_babel'] = True
 
         return 'prefs_%s.html' % (panel or 'general'), data
 
@@ -142,6 +146,7 @@
         req.send_no_content()
 
     def _do_save(self, req):
+        language = req.session.get('language')
         for field in self._form_fields:
             val = req.args.get(field, '').strip()
             if val:
@@ -157,11 +162,16 @@
             elif field in req.session and (field in req.args or
                                            field + '_cb' in req.args):
                 del req.session[field]
-        add_notice(req, _('Your preferences have been saved.'))
+        if Locale and req.session.get('language') != language:
+            # reactivate translations with new language setting when changed
+            del req.locale  # for re-negotiating locale
+            deactivate()
+            make_activable(lambda: req.locale, self.env.path)
+        add_notice(req, _("Your preferences have been saved."))
 
     def _do_load(self, req):
         if req.authname == 'anonymous':
             oldsid = req.args.get('loadsid')
             if oldsid:
                 req.session.get_session(oldsid)
-                add_notice(req, _('The session has been loaded.'))
+                add_notice(req, _("The session has been loaded."))
diff --git a/trac/trac/search/__init__.py b/trac/trac/search/__init__.py
index 3ce5891..eab74dc 100644
--- a/trac/trac/search/__init__.py
+++ b/trac/trac/search/__init__.py
@@ -1 +1,14 @@
-from trac.search.api import *
\ No newline at end of file
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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/.
+
+from trac.search.api import *
diff --git a/trac/trac/search/templates/search.html b/trac/trac/search/templates/search.html
index b91ebf6..638ff05 100644
--- a/trac/trac/search/templates/search.html
+++ b/trac/trac/search/templates/search.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -16,9 +26,6 @@
         <meta name="totalResults" content="$results.num_items"/>
         <meta name="itemsPerPage" content="$results.max_per_page"/>
     </py:if>
-    <script type="text/javascript">
-      jQuery(document).ready(function($) {$("#q").get(0).focus()});
-    </script>
   </head>
   <body>
     <div id="content" class="search">
@@ -26,7 +33,7 @@
       <h1><label for="q">Search</label></h1>
       <form id="fullsearch" action="${href.search()}" method="get">
         <p>
-          <input type="text" id="q" name="q" size="40" value="${query}" />
+          <input type="text" id="q" name="q" class="trac-autofocus" size="40" value="${query}" />
           <input type="hidden" name="noquickjump" value="1" />
           <input type="submit" value="${_('Search')}" />
         </p>
diff --git a/trac/trac/search/web_ui.py b/trac/trac/search/web_ui.py
index 997a783..b2400c8 100644
--- a/trac/trac/search/web_ui.py
+++ b/trac/trac/search/web_ui.py
@@ -21,7 +21,6 @@
 
 from trac.config import IntOption, ListOption
 from trac.core import *
-from trac.mimeview import RenderingContext
 from trac.perm import IPermissionRequestor
 from trac.search.api import ISearchSource
 from trac.util.datefmt import format_datetime, user_time
@@ -29,7 +28,7 @@
 from trac.util.presentation import Paginator
 from trac.util.text import quote_query_string
 from trac.util.translation import _
-from trac.web import IRequestHandler
+from trac.web.api import IRequestHandler
 from trac.web.chrome import (INavigationContributor, ITemplateProvider,
                              add_link, add_stylesheet, add_warning,
                              web_context)
@@ -229,7 +228,7 @@
                                         q=req.args.get('q'),
                                         page=shown_page, noquickjump=1)
             pagedata.append([page_href, None, str(shown_page),
-                             'page ' + str(shown_page)])
+                             _("Page %(num)d", num=shown_page)])
 
         fields = ['href', 'class', 'string', 'title']
         results.shown_pages = [dict(zip(fields, p)) for p in pagedata]
diff --git a/trac/trac/templates/about.html b/trac/trac/templates/about.html
index 3411b45..62f1423 100644
--- a/trac/trac/templates/about.html
+++ b/trac/trac/templates/about.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -12,7 +22,9 @@
     <script type="text/javascript">
       //<![CDATA[
       jQuery(document).ready(function ($) {
-        $("#systeminfo table").append("<tr><th>jQuery</th><td>"+$().jquery+"</td></tr>");
+        $("#systeminfo table").append("<tr><th>jQuery</th><td>"+$().jquery+"</td></tr>" +
+                                      "<tr><th>jQuery UI</th><td>"+$.ui.version+"</td></tr>" +
+                                      "<tr><th>jQuery Timepicker</th><td>"+$.timepicker.version+"</td></tr>");
       });
       //]]>
     </script>
diff --git a/trac/trac/templates/attach_file_form.html b/trac/trac/templates/attach_file_form.html
index 48d7189..9e481aa 100644
--- a/trac/trac/templates/attach_file_form.html
+++ b/trac/trac/templates/attach_file_form.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+
 Conditionally render an ''Attach File'' button.
 
 Arguments:
@@ -12,6 +21,8 @@
       py:if="alist.can_create" method="get" action="${alist.attach_href}" id="attachfile">
   <div>
     <input type="hidden" name="action" value="new" />
-    <input type="submit" name="attachfilebutton" value="${value_of('add_button_title', None) or _('Attach file')}" />
+    <input type="submit" id="attachfilebutton"
+           value="${value_of('add_button_title') or
+                    (_('Attach another file') if alist.attachments else _('Attach file'))}"/>
   </div>
 </form>
diff --git a/trac/trac/templates/attachment.html b/trac/trac/templates/attachment.html
index 6792841..689c7ef 100644
--- a/trac/trac/templates/attachment.html
+++ b/trac/trac/templates/attachment.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -59,7 +69,7 @@
             <input type="hidden" name="action" value="new" />
             <input type="hidden" name="realm" value="$parent.realm" />
             <input type="hidden" name="id" value="$parent.id" />
-            <input type="submit" value="${_('Add attachment')}" />
+            <input type="submit" class="trac-disable-on-submit" value="${_('Add attachment')}" />
             <input type="submit" name="cancel" value="${_('Cancel')}" />
           </div>
         </form>
@@ -73,8 +83,8 @@
           <form method="post" action="">
             <div id="delete">
               <input type="hidden" name="action" value="delete" />
+              <input type="submit" class="trac-disable-on-submit" value="${_('Delete attachment')}" />
               <input type="submit" name="cancel" value="${_('Cancel')}" />
-              <input type="submit" value="${_('Delete attachment')}" />
             </div>
           </form>
         </div>
@@ -83,7 +93,6 @@
       <py:when test="'list'">
         <h1><a href="${url_of(parent)}">${name_of(parent)}</a></h1>
         <py:with vars="context = context.child(parent)">
-          <?python add_button_title = _('Attach another file') ?>
           <xi:include href="list_of_attachments.html" py:with="alist = attachments"/>
         </py:with>
       </py:when>
diff --git a/trac/trac/templates/diff_div.html b/trac/trac/templates/diff_div.html
index e13c1af..76cc5e7 100644
--- a/trac/trac/templates/diff_div.html
+++ b/trac/trac/templates/diff_div.html
@@ -1,39 +1,48 @@
-<!--!
-       changes   - a list of diff items, each being a dict containing information about
-                   changes for one file:
-                     .href         - link for the title (optional)
-                     .title        - tooltip for the title link (optional)
-                     .comments     - annotation for the change (optional)
-                     .new and .old - information about the files being diffed
-                       .path         - path of the file
-                       .rev          - rev of the file (for 'sidebyside')
-                       .shortrev     - abbreviated form of rev of the file (for 'inline')
-                       .href         - link to the full file (optional)
-                     .props        - a list of property changes
-                       .name         - name of the property
-                       .diff         - rendered difference
-                       .old          - old value of the property
-                       .new          - new value for the property
-                       (both .old and .new have .name, .value and .rendered properties)
-                     .diffs        - a sequence of list of blocks
+<!--!  Copyright (C) 2006-2014 Edgewall Software
 
-                       Each block being a dict:
-                       .type         - one of 'unmod', 'add', 'rem' or 'mod'
-                       .base and .changed - information about lines from old and new content
-                         .lines              - the lines
-                         .offset             - position within the file
+  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.
 
-                     .diffs_title  - a sequence of titles for the list of blocks
-                                     Note: integrate this into .diffs for 0.12 or 1.0.
+  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/.
 
-       diff      - dict specifying diff style and options
-                     .style     - can be 'sidebyside' (4 columns) or 'inline' (3 columns)
-                     .options   - contexlines, various ignore...
+Arguments:
+ - changes   - a list of diff items, each being a dict containing information about
+              changes for one file:
+                .href         - link for the title (optional)
+                .title        - tooltip for the title link (optional)
+                .comments     - annotation for the change (optional)
+                .new and .old - information about the files being diffed
+                  .path         - path of the file
+                  .rev          - rev of the file (for 'sidebyside')
+                  .shortrev     - abbreviated form of rev of the file (for 'inline')
+                  .href         - link to the full file (optional)
+                .props        - a list of property changes
+                  .name         - name of the property
+                  .diff         - rendered difference
+                  .old          - old value of the property
+                  .new          - new value for the property
+                  (both .old and .new have .name, .value and .rendered properties)
+                .diffs        - a sequence of list of blocks
 
-       longcol  - "long" column header; e.g. 'Revision' or 'File' or '' (for 'sidebyside')
-       shortcol - "short" column header: e.g. 'r' or '' (for 'inline')
-       no_id    - skip generation of id attributes in h2 headings
+                  Each block being a dict:
+                  .type         - one of 'unmod', 'add', 'rem' or 'mod'
+                  .base and .changed - information about lines from old and new content
+                    .lines              - the lines
+                    .offset             - position within the file
 
+                .diffs_title  - a sequence of titles for the list of blocks
+                                Note: integrate this into .diffs for 0.12 or 1.0.
+
+ - diff      - dict specifying diff style and options
+                .style     - can be 'sidebyside' (4 columns) or 'inline' (3 columns)
+                .options   - contexlines, various ignore...
+
+ - longcol  - "long" column header; e.g. 'Revision' or 'File' or '' (for 'sidebyside')
+ - shortcol - "short" column header: e.g. 'r' or '' (for 'inline')
+ - no_id    - skip generation of id attributes in h2 headings
 -->
 <div xmlns="http://www.w3.org/1999/xhtml"
       xmlns:py="http://genshi.edgewall.org/"
@@ -132,11 +141,11 @@
                                      clines = block.changed.lines">
                       <py:choose test="diff.style">
                         <py:when test="'sidebyside'">
-                          <th>$from_n</th><td class="l"><span>$line</span>&nbsp;</td>
-                          <th>$to_n</th><td class="r"><span>${clines[idx] if idx &lt; len(clines) else None}</span>&nbsp;</td>
+                          <th>$from_n</th><td class="l"><span>$line</span></td>
+                          <th>$to_n</th><td class="r"><span>${clines[idx] if idx &lt; len(clines) else None}</span></td>
                         </py:when>
                         <py:when test="'inline'">
-                          <th>$from_n</th><th>$to_n</th><td class="l"><span>$line</span>&nbsp;</td>
+                          <th>$from_n</th><th>$to_n</th><td class="l"><span>$line</span></td>
                         </py:when>
                       </py:choose>
                     </py:with>
@@ -149,11 +158,11 @@
                     <py:with vars="to_n = block.changed.offset+idx+1">
                       <py:choose test="diff.style">
                         <py:when test="'sidebyside'">
-                          <th>&nbsp;</th><td class="l">&nbsp;</td>
-                          <th>$to_n</th><td class="r"><ins>$line</ins>&nbsp;</td>
+                          <th>&nbsp;</th><td class="l"></td>
+                          <th>$to_n</th><td class="r"><ins>$line</ins></td>
                         </py:when>
                         <py:when test="'inline'">
-                          <th>&nbsp;</th><th>$to_n</th><td class="r"><ins>$line</ins>&nbsp;</td>
+                          <th>&nbsp;</th><th>$to_n</th><td class="r"><ins>$line</ins></td>
                         </py:when>
                       </py:choose>
                     </py:with>
@@ -166,11 +175,11 @@
                     <py:with vars="from_n = block.base.offset+idx+1">
                       <py:choose test="diff.style">
                         <py:when test="'sidebyside'">
-                          <th>$from_n</th><td class="l"><del>$line</del>&nbsp;</td>
-                          <th>&nbsp;</th><td class="r">&nbsp;</td>
+                          <th>$from_n</th><td class="l"><del>$line</del></td>
+                          <th>&nbsp;</th><td class="r"></td>
                         </py:when>
                         <py:when test="'inline'">
-                          <th>$from_n</th><th>&nbsp;</th><td class="l"><del>$line</del>&nbsp;</td>
+                          <th>$from_n</th><th>&nbsp;</th><td class="l"><del>$line</del></td>
                         </py:when>
                       </py:choose>
                     </py:with>
@@ -184,10 +193,10 @@
                         <py:when test="len(block.base.lines) &gt;= len(block.changed.lines)">
                           <tr py:for="idx, line in enumerate(block.base.lines)">
                             <th>${block.base.offset+idx+1}</th>
-                            <td class="l"><span>$line</span>&nbsp;</td>
+                            <td class="l"><span>$line</span></td>
                             <py:with vars="within_change = idx &lt; len(block.changed.lines)">
                               <th>${block.changed.offset + idx + 1 if within_change else '&nbsp;'}</th>
-                              <td class="r"><span py:if="within_change">${block.changed.lines[idx]}</span>&nbsp;</td>
+                              <td class="r"><span py:if="within_change">${block.changed.lines[idx]}</span></td>
                             </py:with>
                           </tr>
                         </py:when>
@@ -195,10 +204,10 @@
                           <tr py:for="idx, line in enumerate(block.changed.lines)">
                             <py:with vars="within_change = idx &lt; len(block.base.lines)">
                               <th>${block.base.offset + idx + 1 if within_change else '&nbsp;'}</th>
-                              <td class="l"><span py:if="within_change">${block.base.lines[idx]}</span>&nbsp;</td>
+                              <td class="l"><span py:if="within_change">${block.base.lines[idx]}</span></td>
                             </py:with>
                             <th>${block.changed.offset + idx + 1}</th>
-                            <td class="r"><span>$line</span>&nbsp;</td>
+                            <td class="r"><span>$line</span></td>
                           </tr>
                         </py:otherwise>
                       </py:choose>
@@ -207,12 +216,12 @@
                       <!--! First show the "old" lines -->
                       <tr py:for="idx, line in enumerate(block.base.lines)"
                         class="${'first' if idx == 0 else None}">
-                        <th>${block.base.offset + idx + 1}</th><th>&nbsp;</th><td class="l"><span>$line</span>&nbsp;</td>
+                        <th>${block.base.offset + idx + 1}</th><th>&nbsp;</th><td class="l"><span>$line</span></td>
                       </tr>
                       <!--! Then show the "new" lines -->
                       <tr py:for="idx, line in enumerate(block.changed.lines)"
                         class="${'last' if idx + 1 == len(block.changed.lines) else None}">
-                        <th>&nbsp;</th><th>${block.changed.offset + idx + 1}</th><td class="r"><span>$line</span>&nbsp;</td>
+                        <th>&nbsp;</th><th>${block.changed.offset + idx + 1}</th><td class="r"><span>$line</span></td>
                       </tr>
                     </py:when>
                   </py:choose>
diff --git a/trac/trac/templates/diff_options.html b/trac/trac/templates/diff_options.html
index 6565859..950e3e6 100644
--- a/trac/trac/templates/diff_options.html
+++ b/trac/trac/templates/diff_options.html
@@ -1,6 +1,17 @@
-<!--! Add diff option fields (to be used inside a form)
+<!--!  Copyright (C) 2009-2014 Edgewall Software
 
-     `diff` the datastructure which contains diff options
+  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/.
+
+Add diff option fields (to be used inside a form)
+
+Arguments:
+ - `diff` the datastructure which contains diff options
 -->
 <div xmlns="http://www.w3.org/1999/xhtml"
     xmlns:py="http://genshi.edgewall.org/"
diff --git a/trac/trac/templates/diff_view.html b/trac/trac/templates/diff_view.html
index 3c74b8a..997bfec 100644
--- a/trac/trac/templates/diff_view.html
+++ b/trac/trac/templates/diff_view.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -18,16 +28,16 @@
         <py:when test="old_version"><i18n:msg params="old, new, name">Changes between
           <a href="${old_url or url_of(resource, version=old_version)}">Version $old_version</a> and
           <a href="${new_url or url_of(resource, version=new_version)}">Version $new_version</a> of
-          <a href="${url or url_of(resource)}">${name or name_of(resource)}</a>
+          <a href="${url or url_of(resource, version=None)}">${name or name_of(resource)}</a>
         </i18n:msg></py:when>
         <py:when test="old_version == 0"><i18n:msg params="new, name">Changes between
           <a href="${old_url or url_of(resource, version=0)}">Initial Version</a> and
           <a href="${new_url or url_of(resource, version=new_version)}">Version $new_version</a> of
-          <a href="${url or url_of(resource)}">${name or name_of(resource)}</a>
+          <a href="${url or url_of(resource, version=None)}">${name or name_of(resource)}</a>
         </i18n:msg></py:when>
         <py:otherwise><i18n:msg params="new, name">Changes from
           <a href="${new_url or url_of(resource, version=new_version)}">Version $new_version</a> of
-          <a href="${url or url_of(resource)}">${name or name_of(resource)}</a>
+          <a href="${url or url_of(resource, version=None)}">${name or name_of(resource)}</a>
         </i18n:msg></py:otherwise>
       </h1>
       <form method="post" id="prefs" action="${diff_url or url_of(resource)}">
diff --git a/trac/trac/templates/error.html b/trac/trac/templates/error.html
index 0726c42..5788aae 100644
--- a/trac/trac/templates/error.html
+++ b/trac/trac/templates/error.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -39,7 +49,9 @@
        $("#traceback pre").hide();
        $("#tbtoggle").parent().show();
 
-       $("#systeminfo").append("<tr><th>jQuery</th><td>" + $().jquery + "</td></tr>");
+       $("#systeminfo").append("<tr><th>jQuery</th><td>"+$().jquery+"</td></tr>" +
+                               "<tr><th>jQuery UI</th><td>"+$.ui.version+"</td></tr>" +
+                               "<tr><th>jQuery Timepicker</th><td>"+$.timepicker.version+"</td></tr>");
        $("#systeminfo").before("<p>User Agent: <tt>" + navigator.userAgent + "</tt></p>");
       });
     /*]]>*/</script>
@@ -48,7 +60,9 @@
         $("form.newticket textarea").each(function() {
           $(this).val($(this).val()
                              .replace(/#USER_AGENT#/m, navigator.userAgent)
-                             .replace(/#JQUERY#/m, $().jquery));
+                             .replace(/#JQUERY#/m, $().jquery)
+                             .replace(/#JQUERYUI#/m, $.ui.version)
+                             .replace(/#JQUERYTP#/m, $.timepicker.version));
         });
       });
     /*]]>*/</script>
@@ -58,6 +72,7 @@
     <input type="hidden" name="reporter" value="${get_reporter_id(req)}" />
     <input py:if="url == trac.homepage.strip('/')" type="hidden" name="version"
            value="${trac.version.split('-', 1)[0] if 'dev' in trac.version else trac.version}" />
+    <input type="hidden" name="$arg" value="$value" py:for="arg, value in tracker_args.iteritems()" />
     <input type="hidden" name="summary" value="$message" />
     <textarea name="description" rows="3" cols="10">
 ${description_en if url else description}</textarea>
@@ -72,7 +87,7 @@
         <py:when test="'TracError'">
           <h1>$title</h1>
           <py:choose test="">
-            <p py:when="istext(message)" class="message">$message</p>
+            <p py:when="not find_element(message, tag='p')" class="message">$message</p>
             <py:otherwise>$message</py:otherwise>
           </py:choose>
         </py:when>
@@ -97,13 +112,17 @@
               <p>The action that triggered the error was:</p>
               <pre>${req.method}: ${req.path_info}</pre>
             </py:when>
-            <py:otherwise>
-              <form class="newticket" method="get" action="${project.admin_href.newticket()}#">
+            <py:otherwise py:choose="">
+              <p py:when="not project.admin_href or project.admin_trac_url == '.'">
+                This is probably a local installation issue.
+              </p>
+              <form py:otherwise=""
+                    class="newticket" method="get" action="${project.admin_href.newticket()}#">
                 <p>This is probably a local installation issue.
-                  <py:if test="project.admin_href and project.admin_trac_url != '.'"><i18n:msg params="create">
+                  <i18n:msg params="create">
                     You should ${create_ticket()} a ticket at the admin Trac to report
                     the issue.
-                  </i18n:msg></py:if>
+                  </i18n:msg>
                 </p>
               </form>
               <h2>Found a bug in Trac?</h2>
@@ -139,7 +158,7 @@
                     <li class="frame" py:for="idx, frame in enumerate(frames)">
                       <a href="#frame${idx}" id="frame${idx}"><span i18n:msg="file, line, function" py:strip="">
                         <span class="file">File "${frame.filename}",
-                        line <b>${frame.lineno + 1}</b>, in</span>
+                        line <strong>${frame.lineno + 1}</strong>, in</span>
                         <var>${frame.function}</var></span>
                       </a>
                       <div py:if="frame.line" class="source" style="display: none">
diff --git a/trac/trac/templates/history_view.html b/trac/trac/templates/history_view.html
index 464b9c7..c3e2ffb 100644
--- a/trac/trac/templates/history_view.html
+++ b/trac/trac/templates/history_view.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -15,7 +25,7 @@
     <div id="content" class="ticket">
       <h1 i18n:msg="name">Change History for <a href="${url or url_of(resource)}">${name or name_of(resource)}</a></h1>
 
-      <form py:if="history" class="printableform" method="get" action="">
+      <form py:if="history" id="history" class="printableform" method="get" action="">
         <div class="buttons">
           <input type="hidden" name="action" value="${diff_action or 'diff'}" />
           <input py:for="k, v in diff_args or []" type="hidden" name="$k" value="$v"/>
diff --git a/trac/trac/templates/index.html b/trac/trac/templates/index.html
index ea1fe7b..3c43292 100644
--- a/trac/trac/templates/index.html
+++ b/trac/trac/templates/index.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/templates/layout.html b/trac/trac/templates/layout.html
index 6ded5d6..4ca8bdc 100644
--- a/trac/trac/templates/layout.html
+++ b/trac/trac/templates/layout.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -37,12 +47,18 @@
     <py:for each="script in chrome.scripts">
       ${script.prefix}<script type="${script.type}" charset="${script.charset}" src="${script.href}"></script>${script.suffix}
     </py:for>
-    <script py:if="chrome.warnings or chrome.notices" type="text/javascript">
+    <script type="text/javascript">
       jQuery(document).ready(function($) {
+        <py:if test="chrome.warnings or chrome.notices">
         $(".trac-close-msg").show().click(function () {
           $(this).closest(".system-message").hide();
           return false;
         });
+        </py:if>
+        $(".trac-autofocus").focus();
+        $(".trac-target-new").attr("target", "_blank");
+        setTimeout(function() { $(".trac-scroll").scrollToTop() }, 1);
+        $(".trac-disable-on-submit").disableOnSubmit();
       });
     </script>
     ${select("*[local-name() != 'title']|text()|comment()")}
diff --git a/trac/trac/templates/list_of_attachments.html b/trac/trac/templates/list_of_attachments.html
index afc656b..5bca713 100644
--- a/trac/trac/templates/list_of_attachments.html
+++ b/trac/trac/templates/list_of_attachments.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+
 Display a list of attachments.
 
 Arguments:
diff --git a/trac/trac/templates/macros.html b/trac/trac/templates/macros.html
index 42511ca..e98773c 100644
--- a/trac/trac/templates/macros.html
+++ b/trac/trac/templates/macros.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <div xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:xi="http://www.w3.org/2001/XInclude"
diff --git a/trac/trac/templates/page_index.html b/trac/trac/templates/page_index.html
index c28709a..7cfecb4 100644
--- a/trac/trac/templates/page_index.html
+++ b/trac/trac/templates/page_index.html
@@ -1,6 +1,17 @@
-<!--!  Display a page index.
+<!--!  Copyright (C) 2008-2014 Edgewall Software
 
-  `paginator` must be a trac.util.presentation.Paginator instance
+  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/.
+
+Display a page index.
+
+Arguments:
+ - `paginator` must be a trac.util.presentation.Paginator instance
 
   -->
 <div xmlns="http://www.w3.org/1999/xhtml"
diff --git a/trac/trac/templates/preview_file.html b/trac/trac/templates/preview_file.html
index 83c7048..a77c16b 100644
--- a/trac/trac/templates/preview_file.html
+++ b/trac/trac/templates/preview_file.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+
 Display a div for visualizing a preview of a file content.
 
 Arguments:
diff --git a/trac/trac/templates/progress_bar.html b/trac/trac/templates/progress_bar.html
index 39a70bc..86b4379 100644
--- a/trac/trac/templates/progress_bar.html
+++ b/trac/trac/templates/progress_bar.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+
 Display groups of tickets in a progress bar.
 
 Arguments:
diff --git a/trac/trac/templates/progress_bar_grouped.html b/trac/trac/templates/progress_bar_grouped.html
index 761d405..f88e65d 100644
--- a/trac/trac/templates/progress_bar_grouped.html
+++ b/trac/trac/templates/progress_bar_grouped.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2011-2014 Edgewall Software
+
+  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/.
+
 Display a table of progress bars for ticket groups
 
 Arguments:
diff --git a/trac/trac/templates/theme.html b/trac/trac/templates/theme.html
index d9fe551..0058575 100644
--- a/trac/trac/templates/theme.html
+++ b/trac/trac/templates/theme.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/test.py b/trac/trac/test.py
index 704fc98..5e2be6b 100755
--- a/trac/trac/test.py
+++ b/trac/trac/test.py
@@ -144,7 +144,8 @@
         return result
 
     def _wrapped_run(self, *args, **kwargs):
-        "Python 2.7 / unittest2 compatibility - there must be a better way..."
+        """Python 2.7 / unittest2 compatibility - there must be a better
+        way..."""
         self.setUp()
         if hasattr(self, 'fixture'):
             for test in self._tests:
@@ -153,6 +154,7 @@
         unittest.TestSuite._wrapped_run(self, *args, **kwargs)
         self.tearDown()
 
+
 class TestCaseSetup(unittest.TestCase):
     def setFixture(self, fixture):
         self.fixture = fixture
@@ -164,7 +166,7 @@
     dburi = os.environ.get('TRAC_TEST_DB_URI')
     if dburi:
         scheme, db_prop = _parse_db_str(dburi)
-        # Assume the schema 'tractest' for Postgres
+        # Assume the schema 'tractest' for PostgreSQL
         if scheme == 'postgres' and \
                 not db_prop.get('params', {}).get('schema'):
             if '?' in dburi:
@@ -176,9 +178,8 @@
 
 
 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'")
+        tables = db.get_table_names()
         for table in tables:
             db("DELETE FROM %s" % table)
         return tables
@@ -189,24 +190,24 @@
         dbname = db.schema
         if dbname:
             # reset sequences
-            # information_schema.sequences view is available in PostgreSQL 8.2+
-            # however Trac supports PostgreSQL 8.0+, uses
+            # 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)):
+            seqs = [seq for seq, in db("""
+                SELECT sequence_name
+                FROM (
+                    SELECT pg_get_serial_sequence(
+                        quote_ident(table_schema) || '.' ||
+                        quote_ident(table_name), column_name) AS sequence_name
+                    FROM information_schema.columns
+                    WHERE table_schema=%s) AS tab
+                WHERE sequence_name IS NOT NULL""", (dbname,))]
+            for seq in seqs:
                 db("ALTER SEQUENCE %s RESTART WITH 1" % seq)
             # clear tables
-            tables = db("""SELECT table_name FROM information_schema.tables
-                           WHERE table_schema=%s""", (dbname,))
+            tables = db.get_table_names()
             for table in tables:
-                db("DELETE FROM %s" % table)
+                db("DELETE FROM %s" % db.quote(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
@@ -217,12 +218,18 @@
     dbname = os.path.basename(db_prop['path'])
     if dbname:
         with env.db_transaction as db:
-            tables = db("""SELECT table_name FROM information_schema.tables
+            tables = db("""SELECT table_name, auto_increment
+                           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)
+            for table, auto_increment in tables:
+                if auto_increment is None or auto_increment == 1:
+                    # DELETE FROM is preferred to TRUNCATE TABLE, as the
+                    # auto_increment is not used or it is 1.
+                    db("DELETE FROM %s" % table)
+                else:
+                    # TRUNCATE TABLE is preferred to DELETE FROM, as we
+                    # need to reset the auto_increment in MySQL.
+                    db("TRUNCATE TABLE %s" % table)
             return tables
 
 
@@ -233,6 +240,7 @@
 
     href = abs_href = None
     global_databasemanager = None
+    required = False
 
     def __init__(self, default_data=False, enable=None, disable=None,
                  path=None, destroying=False):
@@ -242,7 +250,20 @@
                              defaults.
         :param enable: A list of component classes or name globs to
                        activate in the stub environment.
+        :param disable: A list of component classes or name globs to
+                        deactivate in the stub environment.
+        :param path: The location of the environment in the file system.
+                     No files or directories are created when specifying
+                     this parameter.
+        :param destroying: If True, the database will not be reset. This is
+                           useful for cases when the object is being
+                           constructed in order to call `destroy_db`.
         """
+        if enable is not None and not isinstance(enable, (list, tuple)):
+            raise TypeError('Keyword argument "enable" must be a list')
+        if disable is not None and not isinstance(disable, (list, tuple)):
+            raise TypeError('Keyword argument "disable" must be a list')
+
         ComponentManager.__init__(self)
         Component.__init__(self)
 
@@ -266,7 +287,7 @@
         if enable is not None:
             self.config.set('components', 'trac.*', 'disabled')
         else:
-            self.config.set('components', 'tracopt.versioncontrol.svn.*',
+            self.config.set('components', 'tracopt.versioncontrol.*',
                             'enabled')
         for name_or_class in enable or ():
             config_key = self._component_name(name_or_class)
@@ -315,28 +336,26 @@
         remove_sqlite_db = False
         try:
             with self.db_transaction as db:
-                db.rollback() # make sure there's no transaction in progress
+                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_version = self.get_version()
+        except Exception:
             # "Database not found ...",
             # "OperationalError: no such table: system" or the like
             pass
+        else:
+            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)
 
-        db = None # as we might shutdown the pool     FIXME no longer needed!
+        db = None  # as we might shutdown the pool    FIXME no longer needed!
 
         if scheme == 'sqlite' and remove_sqlite_db:
             path = db_prop['path']
@@ -354,12 +373,14 @@
                 self.global_databasemanager.shutdown()
 
         with self.db_transaction as db:
+            if scheme == 'sqlite':
+                # Speed-up tests with SQLite database
+                db("PRAGMA synchronous = OFF")
             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)
+                                      ','.join(['%s'] * len(cols))), vals)
             else:
                 db("INSERT INTO system (name, value) VALUES (%s, %s)",
                    ('database_version', str(db_default.db_version)))
@@ -370,12 +391,9 @@
         try:
             with self.db_transaction as db:
                 if scheme == 'postgres' and db.schema:
-                    db('DROP SCHEMA "%s" CASCADE' % db.schema)
+                    db('DROP SCHEMA %s CASCADE' % db.quote(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,)):
+                    for table in db.get_table_names():
                         db("DROP TABLE IF EXISTS `%s`" % table)
         except Exception:
             # "TracError: Database not found...",
@@ -383,7 +401,7 @@
             pass
         return False
 
-    # overriden
+    # overridden
 
     def is_component_enabled(self, cls):
         if self._component_name(cls).startswith('__main__.'):
@@ -410,11 +428,13 @@
 
 INCLUDE_FUNCTIONAL_TESTS = True
 
+
 def suite():
     import trac.tests
     import trac.admin.tests
     import trac.db.tests
     import trac.mimeview.tests
+    import trac.timeline.tests
     import trac.ticket.tests
     import trac.util.tests
     import trac.versioncontrol.tests
@@ -423,17 +443,17 @@
     import trac.wiki.tests
     import tracopt.mimeview.tests
     import tracopt.perm.tests
+    import tracopt.ticket.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.timeline.tests.suite())
     suite.addTest(trac.util.tests.suite())
     suite.addTest(trac.versioncontrol.tests.suite())
     suite.addTest(trac.versioncontrol.web_ui.tests.suite())
@@ -441,9 +461,12 @@
     suite.addTest(trac.wiki.tests.suite())
     suite.addTest(tracopt.mimeview.tests.suite())
     suite.addTest(tracopt.perm.tests.suite())
+    suite.addTest(tracopt.ticket.tests.suite())
     suite.addTest(tracopt.versioncontrol.git.tests.suite())
     suite.addTest(tracopt.versioncontrol.svn.tests.suite())
     suite.addTest(doctest.DocTestSuite(sys.modules[__name__]))
+    if INCLUDE_FUNCTIONAL_TESTS:
+        suite.addTest(trac.tests.functionalSuite())
 
     return suite
 
diff --git a/trac/trac/tests/__init__.py b/trac/trac/tests/__init__.py
index 67ee4f6..e641345 100644
--- a/trac/trac/tests/__init__.py
+++ b/trac/trac/tests/__init__.py
@@ -1,7 +1,20 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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 unittest
 
-from trac.tests import attachment, config, core, env, perm, resource, \
-                       wikisyntax, functional
+from trac.tests import attachment, config, core, env, perm, notification, \
+                       resource, wikisyntax, functional
 
 def suite():
     suite = unittest.TestSuite()
@@ -15,6 +28,7 @@
     suite.addTest(config.suite())
     suite.addTest(core.suite())
     suite.addTest(env.suite())
+    suite.addTest(notification.suite())
     suite.addTest(perm.suite())
     suite.addTest(resource.suite())
     suite.addTest(wikisyntax.suite())
diff --git a/trac/trac/tests/allwiki.py b/trac/trac/tests/allwiki.py
index 67a0c88c..d3e1861 100644
--- a/trac/trac/tests/allwiki.py
+++ b/trac/trac/tests/allwiki.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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 unittest
 
 import trac.tests.wikisyntax
diff --git a/trac/trac/tests/attachment.py b/trac/trac/tests/attachment.py
index 51e20ea..bf61db7 100644
--- a/trac/trac/tests/attachment.py
+++ b/trac/trac/tests/attachment.py
@@ -1,6 +1,19 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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 os.path
+from __future__ import with_statement
+
+import os
 import shutil
 from StringIO import StringIO
 import tempfile
@@ -44,8 +57,7 @@
 
     def setUp(self):
         self.env = EnvironmentStub()
-        self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
-        os.mkdir(self.env.path)
+        self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
         self.attachments_dir = os.path.join(self.env.path, 'files',
                                             'attachments')
         self.env.config.set('trac', 'permission_policies',
@@ -53,6 +65,10 @@
         self.env.config.set('attachment', 'max_size', 512)
 
         self.perm = PermissionCache(self.env)
+        with self.env.db_transaction as db:
+            db("INSERT INTO wiki (name,version) VALUES ('WikiStart',1)")
+            db("INSERT INTO wiki (name,version) VALUES ('SomePage',1)")
+            db("INSERT INTO ticket (id) VALUES (42)")
 
     def tearDown(self):
         shutil.rmtree(self.env.path)
@@ -144,7 +160,7 @@
                                       hashes['42'][0:3], hashes['42'],
                                       hashes['foo.2.txt'] + '.txt'),
                          attachment.path)
-        self.assert_(os.path.exists(attachment.path))
+        self.assertTrue(os.path.exists(attachment.path))
 
     def test_insert_outside_attachments_dir(self):
         attachment = Attachment(self.env, '../../../../../sth/private', 42)
@@ -163,8 +179,8 @@
         attachment1.delete()
         attachment2.delete()
 
-        assert not os.path.exists(attachment1.path)
-        assert not os.path.exists(attachment2.path)
+        self.assertFalse(os.path.exists(attachment1.path))
+        self.assertFalse(os.path.exists(attachment2.path))
 
         attachments = Attachment.select(self.env, 'wiki', 'SomePage')
         self.assertEqual(0, len(list(attachments)))
@@ -191,7 +207,7 @@
         self.assertEqual(2, len(list(attachments)))
         attachments = Attachment.select(self.env, 'ticket', 123)
         self.assertEqual(0, len(list(attachments)))
-        assert os.path.exists(path1) and os.path.exists(attachment2.path)
+        self.assertTrue(os.path.exists(path1) and os.path.exists(attachment2.path))
 
         attachment1.reparent('ticket', 123)
         self.assertEqual('ticket', attachment1.parent_realm)
@@ -203,19 +219,19 @@
         self.assertEqual(1, len(list(attachments)))
         attachments = Attachment.select(self.env, 'ticket', 123)
         self.assertEqual(1, len(list(attachments)))
-        assert not os.path.exists(path1) and os.path.exists(attachment1.path)
-        assert os.path.exists(attachment2.path)
+        self.assertFalse(os.path.exists(path1) and os.path.exists(attachment1.path))
+        self.assertTrue(os.path.exists(attachment2.path))
 
     def test_legacy_permission_on_parent(self):
         """Ensure that legacy action tests are done on parent.  As
         `ATTACHMENT_VIEW` maps to `TICKET_VIEW`, the `TICKET_VIEW` is tested
         against the ticket's resource."""
         attachment = Attachment(self.env, 'ticket', 42)
-        self.assert_('ATTACHMENT_VIEW' in self.perm(attachment.resource))
+        self.assertTrue('ATTACHMENT_VIEW' in self.perm(attachment.resource))
 
     def test_resource_doesnt_exist(self):
         r = Resource('wiki', 'WikiStart').child('attachment', 'file.txt')
-        self.assertEqual(False, AttachmentModule(self.env).resource_exists(r))
+        self.assertFalse(AttachmentModule(self.env).resource_exists(r))
 
     def test_resource_exists(self):
         att = Attachment(self.env, 'wiki', 'WikiStart')
@@ -232,6 +248,10 @@
         self.listener = TestResourceChangeListener(self.env)
         self.listener.resource_type = Attachment
         self.listener.callback = self.listener_callback
+        from trac.wiki.model import WikiPage
+        page = WikiPage(self.env, 'WikiStart')
+        page.text = "WikiStart"
+        page.save('user', '', '::1')
 
     def tearDown(self):
         self.env.reset_db()
@@ -277,7 +297,7 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(AttachmentTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(AttachmentTestCase))
     suite.addTest(unittest.makeSuite(
         AttachmentResourceChangeListenerTestCase, 'test'))
     return suite
diff --git a/trac/trac/tests/compat.py b/trac/trac/tests/compat.py
new file mode 100644
index 0000000..4a8b084
--- /dev/null
+++ b/trac/trac/tests/compat.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 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/.
+
+"""Some test functions since Python 2.7 to provide backwards-compatibility
+with previous versions of Python from 2.5 onward.
+"""
+
+import os
+import shutil
+import sys
+import unittest
+
+
+if not hasattr(unittest.TestCase, 'assertIs'):
+    def assertIs(self, expr1, expr2, msg=None):
+        if expr1 is not expr2:
+            raise self.failureException(msg or '%r is not %r'
+                                               % (expr1, expr2))
+    unittest.TestCase.assertIs = assertIs
+
+
+if not hasattr(unittest.TestCase, 'assertIsNot'):
+    def assertIsNot(self, expr1, expr2, msg=None):
+        if expr1 is expr2:
+            raise self.failureException(msg or '%r is %r' % (expr1, expr2))
+    unittest.TestCase.assertIsNot = assertIsNot
+
+
+if not hasattr(unittest.TestCase, 'assertIsNone'):
+    def assertIsNone(self, obj, msg=None):
+        self.assertIs(obj, None, msg)
+    unittest.TestCase.assertIsNone = assertIsNone
+
+
+if not hasattr(unittest.TestCase, 'assertIsNotNone'):
+    def assertIsNotNone(self, obj, msg=None):
+        self.assertIsNot(obj, None, msg)
+    unittest.TestCase.assertIsNotNone = assertIsNotNone
+
+
+if not hasattr(unittest.TestCase, 'assertIn'):
+    def assertIn(self, member, container, msg=None):
+        if member not in container:
+            raise self.failureException(msg or '%r not in %r' %
+                                               (member, container))
+    unittest.TestCase.assertIn = assertIn
+
+
+if not hasattr(unittest.TestCase, 'assertNotIn'):
+    def assertNotIn(self, member, container, msg=None):
+        if member in container:
+            raise self.failureException(msg or '%r in %r' %
+                                               (member, container))
+    unittest.TestCase.assertNotIn = assertNotIn
+
+
+if not hasattr(unittest.TestCase, 'assertIsInstance'):
+    def assertIsInstance(self, obj, cls, msg=None):
+        if not isinstance(obj, cls):
+            raise self.failureException(msg or '%r is not an instance of %r' %
+                                               (obj, cls))
+    unittest.TestCase.assertIsInstance = assertIsInstance
+
+
+if not hasattr(unittest.TestCase, 'assertNotIsInstance'):
+    def assertNotIsInstance(self, obj, cls, msg=None):
+        if isinstance(obj, cls):
+            raise self.failureException(msg or '%r is an instance of %r' %
+                                               (obj, cls))
+    unittest.TestCase.assertNotIsInstance = assertNotIsInstance
+
+
+def rmtree(path):
+    import errno
+    def onerror(function, path, excinfo):
+        # `os.remove` fails for a readonly file on Windows.
+        # Then, it attempts to be writable and remove.
+        if function != os.remove:
+            raise
+        e = excinfo[1]
+        if isinstance(e, OSError) and e.errno == errno.EACCES:
+            mode = os.stat(path).st_mode
+            os.chmod(path, mode | 0666)
+            function(path)
+        else:
+            raise
+    if os.name == 'nt' and isinstance(path, str):
+        # Use unicode characters in order to allow non-ansi characters
+        # on Windows.
+        path = unicode(path, sys.getfilesystemencoding())
+    shutil.rmtree(path, onerror=onerror)
diff --git a/trac/trac/tests/config.py b/trac/trac/tests/config.py
index 1025806..2441b9b 100644
--- a/trac/trac/tests/config.py
+++ b/trac/trac/tests/config.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
 #
@@ -19,8 +19,10 @@
 import time
 import unittest
 
+import trac.tests.compat
 from trac.config import *
-from trac.test import Configuration
+from trac.core import Component, Interface, implements
+from trac.test import Configuration, EnvironmentStub
 from trac.util import create_file
 
 
@@ -29,6 +31,7 @@
     def setUp(self):
         tmpdir = os.path.realpath(tempfile.gettempdir())
         self.filename = os.path.join(tmpdir, 'trac-test.ini')
+        self.env = EnvironmentStub()
         self._write([])
         self._orig_registry = Option.registry
         Option.registry = {}
@@ -46,79 +49,80 @@
 
     def test_default(self):
         config = self._read()
-        self.assertEquals('', config.get('a', 'option'))
-        self.assertEquals('value', config.get('a', 'option', 'value'))
+        self.assertEqual('', config.get('a', 'option'))
+        self.assertEqual('value', config.get('a', 'option', 'value'))
 
         class Foo(object):
             option_a = Option('a', 'option', 'value')
 
-        self.assertEquals('value', config.get('a', 'option'))
+        self.assertEqual('value', config.get('a', 'option'))
 
     def test_default_bool(self):
         config = self._read()
-        self.assertEquals(False, config.getbool('a', 'option'))
-        self.assertEquals(True, config.getbool('a', 'option', 'yes'))
-        self.assertEquals(True, config.getbool('a', 'option', 1))
+        self.assertFalse(config.getbool('a', 'option'))
+        self.assertTrue(config.getbool('a', 'option', 'yes'))
+        self.assertTrue(config.getbool('a', 'option', 1))
 
         class Foo(object):
             option_a = Option('a', 'option', 'true')
 
-        self.assertEquals(True, config.getbool('a', 'option'))
+        self.assertTrue(config.getbool('a', 'option'))
 
     def test_default_int(self):
         config = self._read()
         self.assertRaises(ConfigurationError,
                           config.getint, 'a', 'option', 'b')
-        self.assertEquals(0, config.getint('a', 'option'))
-        self.assertEquals(1, config.getint('a', 'option', '1'))
-        self.assertEquals(1, config.getint('a', 'option', 1))
+        self.assertEqual(0, config.getint('a', 'option'))
+        self.assertEqual(1, config.getint('a', 'option', '1'))
+        self.assertEqual(1, config.getint('a', 'option', 1))
 
         class Foo(object):
             option_a = Option('a', 'option', '2')
 
-        self.assertEquals(2, config.getint('a', 'option'))
+        self.assertEqual(2, config.getint('a', 'option'))
 
     def test_default_float(self):
         config = self._read()
         self.assertRaises(ConfigurationError,
                           config.getfloat, 'a', 'option', 'b')
-        self.assertEquals(0.0, config.getfloat('a', 'option'))
-        self.assertEquals(1.2, config.getfloat('a', 'option', '1.2'))
-        self.assertEquals(1.2, config.getfloat('a', 'option', 1.2))
-        self.assertEquals(1.0, config.getfloat('a', 'option', 1))
+        self.assertEqual(0.0, config.getfloat('a', 'option'))
+        self.assertEqual(1.2, config.getfloat('a', 'option', '1.2'))
+        self.assertEqual(1.2, config.getfloat('a', 'option', 1.2))
+        self.assertEqual(1.0, config.getfloat('a', 'option', 1))
 
         class Foo(object):
             option_a = Option('a', 'option', '2.5')
 
-        self.assertEquals(2.5, config.getfloat('a', 'option'))
+        self.assertEqual(2.5, config.getfloat('a', 'option'))
 
     def test_default_path(self):
         config = self._read()
         class Foo(object):
             option_a = PathOption('a', 'opt1', 'file.ini')
             option_b = PathOption('a', 'opt2', '/somewhere/file.ini')
-        self.assertEquals('file.ini', config.get('a', 'opt1'))
+        self.assertEqual('file.ini', config.get('a', 'opt1'))
         self.assertNotEquals('file.ini', config.getpath('a', 'opt1'))
         self.assertTrue(os.path.isabs(config.getpath('a', 'opt1')))
-        self.assertEquals('/somewhere/file.ini', os.path.splitdrive(
-                config.getpath('a', 'opt2'))[1].replace('\\', '/'))
-        self.assertEquals('/none.ini', os.path.splitdrive(
-                config.getpath('a', 'opt3', '/none.ini'))[1].replace('\\', '/'))
+        self.assertEqual('/somewhere/file.ini', os.path.splitdrive(
+                         config.getpath('a', 'opt2'))[1].replace('\\', '/'))
+        self.assertEqual('/none.ini', os.path.splitdrive(
+                         config.getpath('a', 'opt3',
+                                        '/none.ini'))[1].replace('\\', '/'))
         self.assertNotEquals('none.ini', config.getpath('a', 'opt3', 'none.ini'))
 
     def test_read_and_get(self):
         self._write(['[a]', 'option = x'])
         config = self._read()
-        self.assertEquals('x', config.get('a', 'option'))
-        self.assertEquals('x', config.get('a', 'option', 'y'))
-        self.assertEquals('y', config.get('b', 'option2', 'y'))
+        self.assertEqual('x', config.get('a', 'option'))
+        self.assertEqual('x', config.get('a', 'option', 'y'))
+        self.assertEqual('y', config.get('b', 'option2', 'y'))
 
     def test_read_and_get_unicode(self):
         self._write([u'[ä]', u'öption = x'])
         config = self._read()
-        self.assertEquals('x', config.get(u'ä', u'öption'))
-        self.assertEquals('x', config.get(u'ä', u'öption', 'y'))
-        self.assertEquals('y', config.get('b', u'öption2', 'y'))
+        self.assertEqual('x', config.get(u'ä', u'öption'))
+        self.assertEqual('x', config.get(u'ä', u'öption', 'y'))
+        self.assertEqual('y', config.get('b', u'öption2', 'y'))
 
     def test_read_and_getbool(self):
         self._write(['[a]', 'option = yes', 'option2 = true',
@@ -126,73 +130,74 @@
                      'option5 = 1', 'option6 = 123', 'option7 = 123.456',
                      'option8 = disabled', 'option9 = 0', 'option10 = 0.0'])
         config = self._read()
-        self.assertEquals(True, config.getbool('a', 'option'))
-        self.assertEquals(True, config.getbool('a', 'option', False))
-        self.assertEquals(True, config.getbool('a', 'option2'))
-        self.assertEquals(True, config.getbool('a', 'option3'))
-        self.assertEquals(True, config.getbool('a', 'option4'))
-        self.assertEquals(True, config.getbool('a', 'option5'))
-        self.assertEquals(True, config.getbool('a', 'option6'))
-        self.assertEquals(True, config.getbool('a', 'option7'))
-        self.assertEquals(False, config.getbool('a', 'option8'))
-        self.assertEquals(False, config.getbool('a', 'option9'))
-        self.assertEquals(False, config.getbool('a', 'option10'))
-        self.assertEquals(False, config.getbool('b', 'option_b'))
-        self.assertEquals(False, config.getbool('b', 'option_b', False))
-        self.assertEquals(False, config.getbool('b', 'option_b', 'disabled'))
+        self.assertTrue(config.getbool('a', 'option'))
+        self.assertTrue(config.getbool('a', 'option', False))
+        self.assertTrue(config.getbool('a', 'option2'))
+        self.assertTrue(config.getbool('a', 'option3'))
+        self.assertTrue(config.getbool('a', 'option4'))
+        self.assertTrue(config.getbool('a', 'option5'))
+        self.assertTrue(config.getbool('a', 'option6'))
+        self.assertTrue(config.getbool('a', 'option7'))
+        self.assertFalse(config.getbool('a', 'option8'))
+        self.assertFalse(config.getbool('a', 'option9'))
+        self.assertFalse(config.getbool('a', 'option10'))
+        self.assertFalse(config.getbool('b', 'option_b'))
+        self.assertFalse(config.getbool('b', 'option_b', False))
+        self.assertFalse(config.getbool('b', 'option_b', 'disabled'))
 
     def test_read_and_getint(self):
         self._write(['[a]', 'option = 42'])
         config = self._read()
-        self.assertEquals(42, config.getint('a', 'option'))
-        self.assertEquals(42, config.getint('a', 'option', 25))
-        self.assertEquals(0, config.getint('b', 'option2'))
-        self.assertEquals(25, config.getint('b', 'option2', 25))
-        self.assertEquals(25, config.getint('b', 'option2', '25'))
+        self.assertEqual(42, config.getint('a', 'option'))
+        self.assertEqual(42, config.getint('a', 'option', 25))
+        self.assertEqual(0, config.getint('b', 'option2'))
+        self.assertEqual(25, config.getint('b', 'option2', 25))
+        self.assertEqual(25, config.getint('b', 'option2', '25'))
 
     def test_read_and_getfloat(self):
         self._write(['[a]', 'option = 42.5'])
         config = self._read()
-        self.assertEquals(42.5, config.getfloat('a', 'option'))
-        self.assertEquals(42.5, config.getfloat('a', 'option', 25.3))
-        self.assertEquals(0, config.getfloat('b', 'option2'))
-        self.assertEquals(25.3, config.getfloat('b', 'option2', 25.3))
-        self.assertEquals(25.0, config.getfloat('b', 'option2', 25))
-        self.assertEquals(25.3, config.getfloat('b', 'option2', '25.3'))
+        self.assertEqual(42.5, config.getfloat('a', 'option'))
+        self.assertEqual(42.5, config.getfloat('a', 'option', 25.3))
+        self.assertEqual(0, config.getfloat('b', 'option2'))
+        self.assertEqual(25.3, config.getfloat('b', 'option2', 25.3))
+        self.assertEqual(25.0, config.getfloat('b', 'option2', 25))
+        self.assertEqual(25.3, config.getfloat('b', 'option2', '25.3'))
 
     def test_read_and_getlist(self):
         self._write(['[a]', 'option = foo, bar, baz'])
         config = self._read()
-        self.assertEquals(['foo', 'bar', 'baz'],
-                          config.getlist('a', 'option'))
-        self.assertEquals([],
-                          config.getlist('b', 'option2'))
-        self.assertEquals(['foo', 'bar', 'baz'],
-                    config.getlist('b', 'option2', ['foo', 'bar', 'baz']))
-        self.assertEquals(['foo', 'bar', 'baz'],
-                    config.getlist('b', 'option2', 'foo, bar, baz'))
+        self.assertEqual(['foo', 'bar', 'baz'],
+                         config.getlist('a', 'option'))
+        self.assertEqual([],
+                         config.getlist('b', 'option2'))
+        self.assertEqual(['foo', 'bar', 'baz'],
+                         config.getlist('b', 'option2',
+                                        ['foo', 'bar', 'baz']))
+        self.assertEqual(['foo', 'bar', 'baz'],
+                         config.getlist('b', 'option2', 'foo, bar, baz'))
 
     def test_read_and_getlist_sep(self):
         self._write(['[a]', 'option = foo | bar | baz'])
         config = self._read()
-        self.assertEquals(['foo', 'bar', 'baz'],
-                          config.getlist('a', 'option', sep='|'))
+        self.assertEqual(['foo', 'bar', 'baz'],
+                         config.getlist('a', 'option', sep='|'))
 
     def test_read_and_getlist_keep_empty(self):
         self._write(['[a]', 'option = ,bar,baz'])
         config = self._read()
-        self.assertEquals(['bar', 'baz'], config.getlist('a', 'option'))
-        self.assertEquals(['', 'bar', 'baz'],
-                          config.getlist('a', 'option', keep_empty=True))
+        self.assertEqual(['bar', 'baz'], config.getlist('a', 'option'))
+        self.assertEqual(['', 'bar', 'baz'],
+                         config.getlist('a', 'option', keep_empty=True))
 
     def test_read_and_getlist_false_values(self):
         config = self._read()
         values = [None, False, '', 'foo', u'', u'bar',
                   0, 0L, 0.0, 0j, 42, 43.0]
-        self.assertEquals([False, 'foo', u'bar', 0, 0L, 0.0, 0j, 42, 43.0],
-                          config.getlist('a', 'false', values))
-        self.assertEquals(values, config.getlist('a', 'false', values,
-                                                 keep_empty=True))
+        self.assertEqual([False, 'foo', u'bar', 0, 0L, 0.0, 0j, 42, 43.0],
+                         config.getlist('a', 'false', values))
+        self.assertEqual(values, config.getlist('a', 'false', values,
+                                                keep_empty=True))
 
     def test_read_and_choice(self):
         self._write(['[a]', 'option = 2', 'invalid = d'])
@@ -208,8 +213,83 @@
                 self.config = config
 
         foo = Foo()
-        self.assertEquals('2', foo.option)
-        self.assertEquals('1', foo.other)
+        self.assertEqual('2', foo.option)
+        self.assertEqual('1', foo.other)
+        self.assertRaises(ConfigurationError, getattr, foo, 'invalid')
+
+    def test_read_and_getextensionoption(self):
+        self._write(['[a]', 'option = ImplA', 'invalid = ImplB'])
+        config = self._read()
+
+        class IDummy(Interface):
+            pass
+
+        class ImplA(Component):
+            implements(IDummy)
+
+        class Foo(Component):
+            default1 = (ExtensionOption)('a', 'default1', IDummy)
+            default2 = (ExtensionOption)('a', 'default2', IDummy, 'ImplA')
+            default3 = (ExtensionOption)('a', 'default3', IDummy, 'ImplB')
+            option = (ExtensionOption)('a', 'option', IDummy)
+            option2 = (ExtensionOption)('a', 'option', IDummy, 'ImplB')
+            invalid = (ExtensionOption)('a', 'invalid', IDummy)
+
+            def __init__(self):
+                self.config = config
+
+        foo = Foo(self.env)
+        self.assertRaises(ConfigurationError, getattr, foo, 'default1')
+        self.assertIsInstance(foo.default2, ImplA)
+        self.assertRaises(ConfigurationError, getattr, foo, 'default3')
+        self.assertIsInstance(foo.option, ImplA)
+        self.assertIsInstance(foo.option2, ImplA)
+        self.assertRaises(ConfigurationError, getattr, foo, 'invalid')
+
+    def test_read_and_getorderedextensionsoption(self):
+        self._write(['[a]', 'option = ImplA, ImplB',
+                     'invalid = ImplB, ImplD'])
+        config = self._read()
+
+        class IDummy(Interface):
+            pass
+
+        class ImplA(Component):
+            implements(IDummy)
+
+        class ImplB(Component):
+            implements(IDummy)
+
+        class ImplC(Component):
+            implements(IDummy)
+
+        class Foo(Component):
+            # enclose in parentheses to avoid messages extraction
+            default1 = (OrderedExtensionsOption)('a', 'default1', IDummy,
+                                                 include_missing=False)
+            default2 = (OrderedExtensionsOption)('a', 'default2', IDummy)
+            default3 = (OrderedExtensionsOption)('a', 'default3', IDummy,
+                                                 'ImplB, ImplC',
+                                                 include_missing=False)
+            option = (OrderedExtensionsOption)('a', 'option', IDummy,
+                                               include_missing=False)
+            invalid = (OrderedExtensionsOption)('a', 'invalid', IDummy)
+
+            def __init__(self):
+                self.config = config
+
+        foo = Foo(self.env)
+        self.assertEqual([], foo.default1)
+        self.assertEqual(3, len(foo.default2))
+        self.assertIsInstance(foo.default2[0], ImplA)
+        self.assertIsInstance(foo.default2[1], ImplB)
+        self.assertIsInstance(foo.default2[2], ImplC)
+        self.assertEqual(2, len(foo.default3))
+        self.assertIsInstance(foo.default3[0], ImplB)
+        self.assertIsInstance(foo.default3[1], ImplC)
+        self.assertEqual(2, len(foo.option))
+        self.assertIsInstance(foo.option[0], ImplA)
+        self.assertIsInstance(foo.option[1], ImplB)
         self.assertRaises(ConfigurationError, getattr, foo, 'invalid')
 
     def test_getpath(self):
@@ -218,12 +298,20 @@
         config.set('a', 'path_a', os.path.join(base, 'here', 'absolute.txt'))
         config.set('a', 'path_b', 'thisdir.txt')
         config.set('a', 'path_c', os.path.join(os.pardir, 'parentdir.txt'))
-        self.assertEquals(os.path.join(base, 'here', 'absolute.txt'),
-                          config.getpath('a', 'path_a'))
-        self.assertEquals(os.path.join(base, 'thisdir.txt'),
-                          config.getpath('a', 'path_b'))
-        self.assertEquals(os.path.join(os.path.dirname(base), 'parentdir.txt'),
-                          config.getpath('a', 'path_c'))
+        self.assertEqual(os.path.join(base, 'here', 'absolute.txt'),
+                         config.getpath('a', 'path_a'))
+        self.assertEqual(os.path.join(base, 'thisdir.txt'),
+                         config.getpath('a', 'path_b'))
+        self.assertEqual(os.path.join(os.path.dirname(base), 'parentdir.txt'),
+                         config.getpath('a', 'path_c'))
+
+    def test_set_raises(self):
+        class Foo(object):
+            option = Option('a', 'option', 'value')
+
+        f = Foo()
+        self.assertRaises(AttributeError, setattr, f, 'option',
+                          Option('a', 'option2', 'value2'))
 
     def test_set_and_save(self):
         config = self._read()
@@ -233,57 +321,57 @@
         config.set(u'aä', 'option1', u"Voilà l'été") # unicode
         # Note: the following would depend on the locale.getpreferredencoding()
         # config.set('a', 'option3', "Voil\xe0 l'\xe9t\xe9") # latin-1
-        self.assertEquals('x', config.get(u'aä', u'öption0'))
-        self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option1'))
-        self.assertEquals(u"Voilà l'été", config.get(u'aä', 'option2'))
+        self.assertEqual('x', config.get(u'aä', u'öption0'))
+        self.assertEqual(u"Voilà l'été", config.get(u'aä', 'option1'))
+        self.assertEqual(u"Voilà l'été", config.get(u'aä', 'option2'))
         config.save()
 
         configfile = open(self.filename, 'r')
-        self.assertEquals(['# -*- coding: utf-8 -*-\n',
-                           '\n',
-                           '[aä]\n',
-                           "option1 = Voilà l'été\n",
-                           "option2 = Voilà l'été\n",
-                           'öption0 = x\n',
-                           # "option3 = Voilà l'été\n",
-                           '\n',
-                           '[b]\n',
-                           'öption0 = y\n',
-                           '\n'],
-                          configfile.readlines())
+        self.assertEqual(['# -*- coding: utf-8 -*-\n',
+                          '\n',
+                          '[aä]\n',
+                          "option1 = Voilà l'été\n",
+                          "option2 = Voilà l'été\n",
+                          'öption0 = x\n',
+                          # "option3 = Voilà l'été\n",
+                          '\n',
+                          '[b]\n',
+                          'öption0 = y\n',
+                          '\n'],
+                         configfile.readlines())
         configfile.close()
         config2 = Configuration(self.filename)
-        self.assertEquals('x', config2.get(u'aä', u'öption0'))
-        self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option1'))
-        self.assertEquals(u"Voilà l'été", config2.get(u'aä', 'option2'))
-        # self.assertEquals(u"Voilà l'été", config2.get('a', 'option3'))
+        self.assertEqual('x', config2.get(u'aä', u'öption0'))
+        self.assertEqual(u"Voilà l'été", config2.get(u'aä', 'option1'))
+        self.assertEqual(u"Voilà l'été", config2.get(u'aä', 'option2'))
+        # self.assertEqual(u"Voilà l'été", config2.get('a', 'option3'))
 
     def test_set_and_save_inherit(self):
         def testcb():
             config = self._read()
             config.set('a', 'option2', "Voilà l'été")  # UTF-8
             config.set('a', 'option1', u"Voilà l'été") # unicode
-            self.assertEquals('x', config.get('a', 'option'))
-            self.assertEquals(u"Voilà l'été", config.get('a', 'option1'))
-            self.assertEquals(u"Voilà l'été", config.get('a', 'option2'))
+            self.assertEqual('x', config.get('a', 'option'))
+            self.assertEqual(u"Voilà l'été", config.get('a', 'option1'))
+            self.assertEqual(u"Voilà l'été", config.get('a', 'option2'))
             config.save()
 
             configfile = open(self.filename, 'r')
-            self.assertEquals(['# -*- coding: utf-8 -*-\n',
-                               '\n',
-                               '[a]\n',
-                               "option1 = Voilà l'été\n",
-                               "option2 = Voilà l'été\n",
-                               '\n',
-                               '[inherit]\n',
-                               "file = trac-site.ini\n",
-                               '\n'],
-                              configfile.readlines())
+            self.assertEqual(['# -*- coding: utf-8 -*-\n',
+                              '\n',
+                              '[a]\n',
+                              "option1 = Voilà l'été\n",
+                              "option2 = Voilà l'été\n",
+                              '\n',
+                              '[inherit]\n',
+                              "file = trac-site.ini\n",
+                              '\n'],
+                             configfile.readlines())
             configfile.close()
             config2 = Configuration(self.filename)
-            self.assertEquals('x', config2.get('a', 'option'))
-            self.assertEquals(u"Voilà l'été", config2.get('a', 'option1'))
-            self.assertEquals(u"Voilà l'été", config2.get('a', 'option2'))
+            self.assertEqual('x', config2.get('a', 'option'))
+            self.assertEqual(u"Voilà l'été", config2.get('a', 'option1'))
+            self.assertEqual(u"Voilà l'été", config2.get('a', 'option2'))
         self._test_with_inherit(testcb)
 
     def test_simple_remove(self):
@@ -292,106 +380,106 @@
         config.get('a', 'option') # populates the cache
         config.set(u'aä', u'öption', u'öne')
         config.remove('a', 'option')
-        self.assertEquals('', config.get('a', 'option'))
+        self.assertEqual('', config.get('a', 'option'))
         config.remove(u'aä', u'öption')
-        self.assertEquals('', config.get('aä', 'öption'))
+        self.assertEqual('', config.get('aä', 'öption'))
         config.remove('a', 'option2') # shouldn't fail
         config.remove('b', 'option2') # shouldn't fail
 
     def test_sections(self):
         self._write(['[a]', 'option = x', '[b]', 'option = y'])
         config = self._read()
-        self.assertEquals(['a', 'b'], config.sections())
+        self.assertEqual(['a', 'b'], config.sections())
 
         class Foo(object):
             # enclose in parentheses to avoid messages extraction
             section_c = (ConfigSection)('c', 'Doc for c')
             option_c = Option('c', 'option', 'value')
 
-        self.assertEquals(['a', 'b', 'c'], config.sections())
+        self.assertEqual(['a', 'b', 'c'], config.sections())
         foo = Foo()
         foo.config = config
-        self.assert_(foo.section_c is config['c'])
-        self.assertEquals('value', foo.section_c.get('option'))
+        self.assertTrue(foo.section_c is config['c'])
+        self.assertEqual('value', foo.section_c.get('option'))
 
     def test_sections_unicode(self):
         self._write([u'[aä]', u'öption = x', '[b]', 'option = y'])
         config = self._read()
-        self.assertEquals([u'aä', 'b'], config.sections())
+        self.assertEqual([u'aä', 'b'], config.sections())
 
         class Foo(object):
             option_c = Option(u'cä', 'option', 'value')
 
-        self.assertEquals([u'aä', 'b', u'cä'], config.sections())
+        self.assertEqual([u'aä', 'b', u'cä'], config.sections())
 
     def test_options(self):
         self._write(['[a]', 'option = x', '[b]', 'option = y'])
         config = self._read()
-        self.assertEquals(('option', 'x'), iter(config.options('a')).next())
-        self.assertEquals(('option', 'y'), iter(config.options('b')).next())
+        self.assertEqual(('option', 'x'), iter(config.options('a')).next())
+        self.assertEqual(('option', 'y'), iter(config.options('b')).next())
         self.assertRaises(StopIteration, iter(config.options('c')).next)
-        self.assertEquals('option', iter(config['a']).next())
-        self.assertEquals('option', iter(config['b']).next())
+        self.assertEqual('option', iter(config['a']).next())
+        self.assertEqual('option', iter(config['b']).next())
         self.assertRaises(StopIteration, iter(config['c']).next)
 
         class Foo(object):
             option_a = Option('a', 'b', 'c')
 
-        self.assertEquals([('option', 'x'), ('b', 'c')],
-                                list(config.options('a')))
+        self.assertEqual([('option', 'x'), ('b', 'c')],
+                         list(config.options('a')))
 
     def test_options_unicode(self):
         self._write([u'[ä]', u'öption = x', '[b]', 'option = y'])
         config = self._read()
-        self.assertEquals((u'öption', 'x'), iter(config.options(u'ä')).next())
-        self.assertEquals(('option', 'y'), iter(config.options('b')).next())
+        self.assertEqual((u'öption', 'x'), iter(config.options(u'ä')).next())
+        self.assertEqual(('option', 'y'), iter(config.options('b')).next())
         self.assertRaises(StopIteration, iter(config.options('c')).next)
-        self.assertEquals(u'öption', iter(config['ä']).next())
+        self.assertEqual(u'öption', iter(config['ä']).next())
 
         class Foo(object):
             option_a = Option(u'ä', u'öption2', 'c')
 
-        self.assertEquals([(u'öption', 'x'), (u'öption2', 'c')],
-                                list(config.options(u'ä')))
+        self.assertEqual([(u'öption', 'x'), (u'öption2', 'c')],
+                         list(config.options(u'ä')))
 
     def test_has_option(self):
         config = self._read()
-        self.assertEquals(False, config.has_option('a', 'option'))
-        self.assertEquals(False, 'option' in config['a'])
+        self.assertFalse(config.has_option('a', 'option'))
+        self.assertFalse('option' in config['a'])
         self._write(['[a]', 'option = x'])
         config = self._read()
-        self.assertEquals(True, config.has_option('a', 'option'))
-        self.assertEquals(True, 'option' in config['a'])
+        self.assertTrue(config.has_option('a', 'option'))
+        self.assertTrue('option' in config['a'])
 
         class Foo(object):
             option_a = Option('a', 'option2', 'x2')
 
-        self.assertEquals(True, config.has_option('a', 'option2'))
+        self.assertTrue(config.has_option('a', 'option2'))
 
     def test_has_option_unicode(self):
         config = self._read()
-        self.assertEquals(False, config.has_option(u'ä', u'öption'))
-        self.assertEquals(False, u'öption' in config[u'ä'])
+        self.assertFalse(config.has_option(u'ä', u'öption'))
+        self.assertFalse(u'öption' in config[u'ä'])
         self._write([u'[ä]', u'öption = x'])
         config = self._read()
-        self.assertEquals(True, config.has_option(u'ä', u'öption'))
-        self.assertEquals(True, u'öption' in config[u'ä'])
+        self.assertTrue(config.has_option(u'ä', u'öption'))
+        self.assertTrue(u'öption' in config[u'ä'])
 
         class Foo(object):
             option_a = Option(u'ä', u'öption2', 'x2')
 
-        self.assertEquals(True, config.has_option(u'ä', u'öption2'))
+        self.assertTrue(config.has_option(u'ä', u'öption2'))
 
     def test_reparse(self):
         self._write(['[a]', 'option = x'])
         config = self._read()
-        self.assertEquals('x', config.get('a', 'option'))
+        self.assertEqual('x', config.get('a', 'option'))
         time.sleep(2) # needed because of low mtime granularity,
                       # especially on fat filesystems
 
         self._write(['[a]', 'option = y'])
         config.parse_if_needed()
-        self.assertEquals('y', config.get('a', 'option'))
+        self.assertEqual('y', config.get('a', 'option'))
 
     def test_inherit_one_level(self):
         def testcb():
@@ -401,7 +489,7 @@
             config.remove('a', 'option') # Should *not* remove option in parent
             self.assertEqual('x', config.get('a', 'option'))
             self.assertEqual([('option', 'x')], list(config.options('a')))
-            self.assertEqual(True, 'a' in config)
+            self.assertTrue('a' in config)
         self._test_with_inherit(testcb)
 
     def test_inherit_multiple(self):
@@ -441,6 +529,89 @@
             os.remove(site1)
             os.rmdir(os.path.dirname(site1))
 
+    def test_option_with_raw_default(self):
+        class Foo(object):
+            # enclose in parentheses to avoid messages extraction
+            option_none = (Option)('a', 'none', None)
+            option_blah = (Option)('a', 'blah', u'Blàh!')
+            option_true = (BoolOption)('a', 'true', True)
+            option_false = (BoolOption)('a', 'false', False)
+            option_list = (ListOption)('a', 'list', ['#cc0', 4.2, 42L, 0, None,
+                                                     True, False, None],
+                                       sep='|')
+            option_choice = (ChoiceOption)('a', 'choice', [-42, 42])
+
+        config = self._read()
+        config.set_defaults()
+        config.save()
+        with open(self.filename, 'r') as f:
+            self.assertEqual('# -*- coding: utf-8 -*-\n',            f.next())
+            self.assertEqual('\n',                                   f.next())
+            self.assertEqual('[a]\n',                                f.next())
+            self.assertEqual('blah = Blàh!\n',                       f.next())
+            self.assertEqual('choice = -42\n',                       f.next())
+            self.assertEqual('false = disabled\n',                   f.next())
+            self.assertEqual('list = #cc0|4.2|42|0||enabled|disabled|\n',
+                             f.next())
+            self.assertEqual('# none = <inherited>\n',               f.next())
+            self.assertEqual('true = enabled\n',                     f.next())
+            self.assertEqual('\n',                                   f.next())
+            self.assertRaises(StopIteration, f.next)
+
+    def test_unicode_option_with_raw_default(self):
+        class Foo(object):
+            # enclose in parentheses to avoid messages extraction
+            option_none = (Option)(u'résumé', u'nöné', None)
+            option_blah = (Option)(u'résumé', u'bláh', u'Blàh!')
+            option_true = (BoolOption)(u'résumé', u'trüé', True)
+            option_false = (BoolOption)(u'résumé', u'fálsé', False)
+            option_list = (ListOption)(u'résumé', u'liśt',
+                                       [u'#ccö', 4.2, 42L, 0, None, True,
+                                        False, None],
+                                       sep='|')
+            option_choice = (ChoiceOption)(u'résumé', u'chöicé', [-42, 42])
+
+        config = self._read()
+        config.set_defaults()
+        config.save()
+        with open(self.filename, 'r') as f:
+            self.assertEqual('# -*- coding: utf-8 -*-\n',            f.next())
+            self.assertEqual('\n',                                   f.next())
+            self.assertEqual('[résumé]\n',                           f.next())
+            self.assertEqual('bláh = Blàh!\n',                       f.next())
+            self.assertEqual('chöicé = -42\n',                       f.next())
+            self.assertEqual('fálsé = disabled\n',                   f.next())
+            self.assertEqual('liśt = #ccö|4.2|42|0||enabled|disabled|\n',
+                             f.next())
+            self.assertEqual('# nöné = <inherited>\n',               f.next())
+            self.assertEqual('trüé = enabled\n',                     f.next())
+            self.assertEqual('\n',                                   f.next())
+            self.assertRaises(StopIteration, f.next)
+
+    def test_save_changes_mtime(self):
+        """Test that each save operation changes the file modification time."""
+        class Foo(object):
+            IntOption('section', 'option', 1)
+        sconfig = self._read()
+        sconfig.set_defaults()
+        sconfig.save()
+        rconfig = self._read()
+        self.assertEqual(1, rconfig.getint('section', 'option'))
+        sconfig.set('section', 'option', 2)
+        time.sleep(1.0 - time.time() % 1.0)
+        sconfig.save()
+        rconfig.parse_if_needed()
+        self.assertEqual(2, rconfig.getint('section', 'option'))
+
+    def test_touch_changes_mtime(self):
+        """Test that each touch command changes the file modification time."""
+        config = self._read()
+        time.sleep(1.0 - time.time() % 1.0)
+        config.touch()
+        mtime = os.stat(self.filename).st_mtime
+        config.touch()
+        self.assertNotEqual(mtime, os.stat(self.filename).st_mtime)
+
     def _test_with_inherit(self, testcb):
         sitename = os.path.join(tempfile.gettempdir(), 'trac-site.ini')
         try:
@@ -454,7 +625,7 @@
 
 
 def suite():
-    return unittest.makeSuite(ConfigurationTestCase, 'test')
+    return unittest.makeSuite(ConfigurationTestCase)
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
diff --git a/trac/trac/tests/contentgen.py b/trac/trac/tests/contentgen.py
index 7d5c22a..9704d7d 100644
--- a/trac/trac/tests/contentgen.py
+++ b/trac/trac/tests/contentgen.py
@@ -1,11 +1,23 @@
-#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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 random
-
+import uuid
 
 try:
-    all_words = [x.strip() for x in open('/usr/share/dict/words').readlines() if x.strip().isalpha()]
-except Exception:
+    all_words = [x.strip() for x in open('/usr/share/dict/words').readlines()
+                           if x.strip().isalpha()]
+except IOError:
     all_words = [
         'one',
         'two',
@@ -19,37 +31,50 @@
         'ten',
     ]
 
-def random_word():
+
+def random_word(min_length=1):
     word = random.choice(all_words)
+    while len(word) < min_length:
+        word = random.choice(all_words)
     # Do not return CamelCase words
     if word[0].isupper():
         word = word.lower().capitalize()
     return word
 
+
 _random_unique_camels = []
 def random_unique_camel():
     """Returns a unique camelcase word pair"""
     while True:
-        camel = random_word().title() + random_word().title()
+        camel = random_word(2).title() + random_word(2).title()
         if not camel in _random_unique_camels:
             break
     _random_unique_camels.append(camel)
     return camel
 
+
 def random_sentence(word_count=None):
-    if word_count == None:
+    """Generates a random sentence. The first word consists of the first 8
+    characters of a uuid to ensure uniqueness.
+
+    :param word_count: number of words in the sentence
+    """
+    if word_count is None:
         word_count = random.randint(1, 20)
-    words = [random_word() for x in range(word_count)]
+    words = [random_word() for x in range(word_count - 1)]
+    words.insert(0, str(uuid.uuid1()).split('-')[0])
     return '%s.' % ' '.join(words)
 
+
 def random_paragraph(sentence_count=None):
-    if sentence_count == None:
+    if sentence_count is None:
         sentence_count = random.randint(1, 10)
     sentences = [random_sentence(random.randint(2, 15)) for x in range(sentence_count)]
     return '  '.join(sentences)
 
+
 def random_page(paragraph_count=None):
-    if paragraph_count == None:
+    if paragraph_count is None:
         paragraph_count = random.randint(1, 10)
     paragraphs = [random_paragraph(random.randint(1, 5)) for x in range(paragraph_count)]
     return '\r\n\r\n'.join(paragraphs)
diff --git a/trac/trac/tests/core.py b/trac/trac/tests/core.py
index c8b3cfb..822258b 100644
--- a/trac/trac/tests/core.py
+++ b/trac/trac/tests/core.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2005-2009 Edgewall Software
+# Copyright (C) 2005-2009 Edgewall Software
 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
 #
@@ -14,7 +14,9 @@
 #
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
+import trac.tests.compat
 from trac.core import *
+from trac.core import ComponentManager
 
 import unittest
 
@@ -51,7 +53,7 @@
         registry.
         """
         from trac.core import ComponentMeta
-        assert Component not in ComponentMeta._components
+        self.assertNotIn(Component, ComponentMeta._components)
         self.assertRaises(TracError, self.compmgr.__getitem__, Component)
 
     def test_abstract_component_not_registered(self):
@@ -62,7 +64,7 @@
         from trac.core import ComponentMeta
         class AbstractComponent(Component):
             abstract = True
-        assert AbstractComponent not in ComponentMeta._components
+        self.assertNotIn(AbstractComponent, ComponentMeta._components)
         self.assertRaises(TracError, self.compmgr.__getitem__,
                           AbstractComponent)
 
@@ -82,8 +84,8 @@
         """
         class ComponentA(Component):
             pass
-        assert self.compmgr[ComponentA]
-        assert ComponentA(self.compmgr)
+        self.assertTrue(self.compmgr[ComponentA])
+        self.assertTrue(ComponentA(self.compmgr))
 
     def test_component_identity(self):
         """
@@ -94,9 +96,9 @@
             pass
         c1 = ComponentA(self.compmgr)
         c2 = ComponentA(self.compmgr)
-        assert c1 is c2, 'Expected same component instance'
+        self.assertIs(c1, c2, 'Expected same component instance')
         c2 = self.compmgr[ComponentA]
-        assert c1 is c2, 'Expected same component instance'
+        self.assertIs(c1, c2, 'Expected same component instance')
 
     def test_component_initializer(self):
         """
@@ -212,7 +214,7 @@
             def test(self):
                 return 'x'
         tests = iter(ComponentA(self.compmgr).tests)
-        self.assertEquals('x', tests.next().test())
+        self.assertEqual('x', tests.next().test())
         self.assertRaises(StopIteration, tests.next)
 
     def test_extension_point_with_two_extensions(self):
@@ -231,7 +233,7 @@
             def test(self):
                 return 'y'
         results = [test.test() for test in ComponentA(self.compmgr).tests]
-        self.assertEquals(['x', 'y'], sorted(results))
+        self.assertEqual(['x', 'y'], sorted(results))
 
     def test_inherited_extension_point(self):
         """
@@ -246,7 +248,7 @@
             def test(self):
                 return 'x'
         tests = iter(ConcreteComponent(self.compmgr).tests)
-        self.assertEquals('x', tests.next().test())
+        self.assertEqual('x', tests.next().test())
         self.assertRaises(StopIteration, tests.next)
 
     def test_inherited_implements(self):
@@ -260,7 +262,7 @@
         class ConcreteComponent(BaseComponent):
             pass
         from trac.core import ComponentMeta
-        assert ConcreteComponent in ComponentMeta._registry.get(ITest, [])
+        self.assertIn(ConcreteComponent, ComponentMeta._registry.get(ITest, []))
 
     def test_inherited_implements_multilevel(self):
         """
@@ -276,8 +278,8 @@
         class ConcreteComponent(ChildComponent):
             pass
         from trac.core import ComponentMeta
-        assert ConcreteComponent in ComponentMeta._registry.get(ITest, [])
-        assert ConcreteComponent in ComponentMeta._registry.get(IOtherTest, [])
+        self.assertIn(ConcreteComponent, ComponentMeta._registry.get(ITest, []))
+        self.assertIn(ConcreteComponent, ComponentMeta._registry.get(IOtherTest, []))
 
     def test_component_manager_component(self):
         """
@@ -295,9 +297,9 @@
             def test(self):
                 return 'x'
         mgr = ManagerComponent('Test', 42)
-        assert id(mgr) == id(mgr[ManagerComponent])
+        self.assertEqual(id(mgr), id(mgr[ManagerComponent]))
         tests = iter(mgr.tests)
-        self.assertEquals('x', tests.next().test())
+        self.assertEqual('x', tests.next().test())
         self.assertRaises(StopIteration, tests.next)
 
     def test_component_manager_component_isolation(self):
@@ -308,69 +310,28 @@
 
         See bh:comment:5:ticket:438 and #11121
         """
-        from trac.core import ComponentManager
-        class ManagerComponent(ComponentManager, Component):
-            tests = ExtensionPoint(ITest)
-            def __init__(self, foo, bar):
-                ComponentManager.__init__(self)
-                self.foo, self.bar = foo, bar
-
-        class YetAnotherManagerComponent(ComponentManager, Component):
-            implements(ITest)
-            def __init__(self, foo, bar):
-                ComponentManager.__init__(self)
-                self.foo, self.bar = foo, bar
-
-            # ITest methods
-            def test(self):
-                return self.foo + self.bar
-
-        class ComponentA(Component):
-            tests = ExtensionPoint(ITest)
-
-        class Extender(Component):
+        class ManagerComponentA(ComponentManager, Component):
             implements(ITest)
             def test(self):
-                return 'x'
+                pass
 
-        mgr = ManagerComponent('Test', 42)
-        yamc = YetAnotherManagerComponent('y', 'z')
+        class ManagerComponentB(ManagerComponentA):
+            pass
 
-        assert yamc[ManagerComponent] is None 
-        assert mgr[YetAnotherManagerComponent] is None 
-        assert yamc[ComponentManager] is None 
-        assert self.compmgr[YetAnotherManagerComponent] is None 
-        assert mgr[ComponentManager] is None 
-        assert self.compmgr[ManagerComponent] is None 
+        class Tester(Component):
+            tests = ExtensionPoint(ITest)
 
-        self.assertTrue(any(c.__class__ is YetAnotherManagerComponent
-                            for c in ComponentA(yamc).tests))
-        self.assertFalse(any(c.__class__ is YetAnotherManagerComponent
-                             for c in ComponentA(self.compmgr).tests))
-        self.assertFalse(any(c.__class__ is YetAnotherManagerComponent
-                             for c in ComponentA(mgr).tests))
-        self.assertFalse(any(c.__class__ is ManagerComponent
-                             for c in ComponentA(yamc).tests))
-        self.assertFalse(any(c.__class__ is YetAnotherManagerComponent
-                             for c in mgr.tests))
+        mgrA = ManagerComponentA()
+        mgrB = ManagerComponentB()
 
-        results = [test.test() for test in ComponentA(yamc).tests]
-        self.assertEquals(['x', 'yz'], sorted(results))
-
-        results = [test.test() for test in ComponentA(self.compmgr).tests]
-        self.assertEquals(['x'], sorted(results))
-
-        results = [test.test() for test in ComponentA(mgr).tests]
-        self.assertEquals(['x'], sorted(results))
-        results = [test.test() for test in mgr.tests]
-        self.assertEquals(['x'], sorted(results))
+        self.assertEqual([mgrA], Tester(mgrA).tests)
+        self.assertEqual([mgrB], Tester(mgrB).tests)
 
     def test_instantiation_doesnt_enable(self):
         """
         Make sure that a component disabled by the ComponentManager is not
         implicitly enabled by instantiating it directly.
         """
-        from trac.core import ComponentManager
         class DisablingComponentManager(ComponentManager):
             def is_component_enabled(self, cls):
                 return False
@@ -378,10 +339,21 @@
             pass
         mgr = DisablingComponentManager()
         instance = ComponentA(mgr)
-        self.assertEqual(None, mgr[ComponentA])
+        self.assertIsNone(mgr[ComponentA])
+
+    def test_invalid_argument_raises(self):
+        """
+        AssertionError is raised when first argument to initializer is not a
+        ComponentManager instance.
+        """
+        class ComponentA(Component):
+            pass
+        self.assertRaises(AssertionError, Component)
+
 
 def suite():
-    return unittest.makeSuite(ComponentTestCase, 'test')
+    return unittest.makeSuite(ComponentTestCase)
+
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/tests/env.py b/trac/trac/tests/env.py
index c619c9b..41cca56 100644
--- a/trac/trac/tests/env.py
+++ b/trac/trac/tests/env.py
@@ -1,24 +1,72 @@
+# -*- 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/.
+
 from __future__ import with_statement
 
-from trac import db_default
-from trac.env import Environment
-
+from ConfigParser import RawConfigParser
 import os.path
-import unittest
-import tempfile
 import shutil
+import tempfile
+import unittest
+
+import trac.tests.compat
+from trac import db_default
+from trac.core import ComponentManager
+from trac.env import Environment
+from trac.test import EnvironmentStub
+
+
+class EnvironmentCreatedWithoutData(Environment):
+    def __init__(self, path, create=False, options=[]):
+        ComponentManager.__init__(self)
+
+        self.path = path
+        self.systeminfo = []
+        self.href = self.abs_href = None
+
+        if create:
+            self.create(options)
+        else:
+            self.verify()
+            self.setup_config()
+
+
+class EmptyEnvironmentTestCase(unittest.TestCase):
+
+    def setUp(self):
+        env_path = tempfile.mkdtemp(prefix='trac-tempenv-')
+        self.env = EnvironmentCreatedWithoutData(env_path, create=True)
+
+    def tearDown(self):
+        self.env.shutdown() # really closes the db connections
+        shutil.rmtree(self.env.path)
+
+    def test_get_version(self):
+        """Testing env.get_version"""
+        self.assertFalse(self.env.get_version())
 
 
 class EnvironmentTestCase(unittest.TestCase):
 
     def setUp(self):
-        env_path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
+        env_path = tempfile.mkdtemp(prefix='trac-tempenv-')
         self.addCleanup(self.cleanupEnvPath, env_path)
         self.env = Environment(env_path, create=True)
+        self.env.config.set('trac', 'base_url',
+                            'http://trac.edgewall.org/some/path')
+        self.env.config.save()
 
     def tearDown(self):
-        with self.env.db_query as db:
-            db.close()
         self.env.shutdown() # really closes the db connections
         shutil.rmtree(self.env.path)
 
@@ -26,33 +74,77 @@
         if os.path.exists(path):
             shutil.rmtree(path)
 
+    def test_db_exc(self):
+        db_exc = self.env.db_exc
+        self.assertTrue(hasattr(db_exc, 'IntegrityError'))
+        self.assertIs(db_exc, self.env.db_exc)
+
+    def test_abs_href(self):
+        abs_href = self.env.abs_href
+        self.assertEqual('http://trac.edgewall.org/some/path', abs_href())
+        self.assertIs(abs_href, self.env.abs_href)
+
+    def test_href(self):
+        href = self.env.href
+        self.assertEqual('/some/path', href())
+        self.assertIs(href, self.env.href)
+
     def test_get_version(self):
         """Testing env.get_version"""
-        assert self.env.get_version() == db_default.db_version
+        self.assertEqual(db_default.db_version, self.env.get_version())
+        self.assertEqual(db_default.db_version, self.env.database_version)
+        self.assertEqual(db_default.db_version, self.env.database_initial_version)
 
     def test_get_known_users(self):
         """Testing env.get_known_users"""
         with self.env.db_transaction as db:
             db.executemany("INSERT INTO session VALUES (%s,%s,0)",
-               [('123', 0),('tom', 1), ('joe', 1), ('jane', 1)])
+                [('123', 0), ('tom', 1), ('joe', 1), ('jane', 1)])
             db.executemany("INSERT INTO session_attribute VALUES (%s,%s,%s,%s)",
-               [('123', 0, 'email', 'a@example.com'),
-                ('tom', 1, 'name', 'Tom'),
-                ('tom', 1, 'email', 'tom@example.com'),
-                ('joe', 1, 'email', 'joe@example.com'),
-                ('jane', 1, 'name', 'Jane')])
+                [('123', 0, 'email', 'a@example.com'),
+                 ('tom', 1, 'name', 'Tom'),
+                 ('tom', 1, 'email', 'tom@example.com'),
+                 ('joe', 1, 'email', 'joe@example.com'),
+                 ('jane', 1, 'name', 'Jane')])
         users = {}
         for username, name, email in self.env.get_known_users():
             users[username] = (name, email)
 
-        assert not users.has_key('anonymous')
+        self.assertTrue('anonymous' not in users)
         self.assertEqual(('Tom', 'tom@example.com'), users['tom'])
         self.assertEqual((None, 'joe@example.com'), users['joe'])
         self.assertEqual(('Jane', None), users['jane'])
 
+    def test_is_component_enabled(self):
+        self.assertEqual(True, Environment.required)
+        self.assertEqual(True, self.env.is_component_enabled(Environment))
+        self.assertEqual(False, EnvironmentStub.required)
+        self.assertEqual(None, self.env.is_component_enabled(EnvironmentStub))
+
+    def test_dumped_values_in_tracini(self):
+        parser = RawConfigParser()
+        filename = self.env.config.filename
+        self.assertEqual([filename], parser.read(filename))
+        self.assertEqual('#cc0,#0c0,#0cc,#00c,#c0c,#c00',
+                         parser.get('revisionlog', 'graph_colors'))
+        self.assertEqual('disabled', parser.get('trac', 'secure_cookies'))
+
+    def test_dumped_values_in_tracini_sample(self):
+        parser = RawConfigParser()
+        filename = self.env.config.filename + '.sample'
+        self.assertEqual([filename], parser.read(filename))
+        self.assertEqual('#cc0,#0c0,#0cc,#00c,#c0c,#c00',
+                         parser.get('revisionlog', 'graph_colors'))
+        self.assertEqual('disabled', parser.get('trac', 'secure_cookies'))
+        self.assertTrue(parser.has_option('logging', 'log_format'))
+        self.assertEqual('', parser.get('logging', 'log_format'))
+
 
 def suite():
-    return unittest.makeSuite(EnvironmentTestCase,'test')
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(EnvironmentTestCase))
+    suite.addTest(unittest.makeSuite(EmptyEnvironmentTestCase))
+    return suite
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
diff --git a/trac/trac/tests/functional/__init__.py b/trac/trac/tests/functional/__init__.py
index fe2b504..f15ba04 100755
--- a/trac/trac/tests/functional/__init__.py
+++ b/trac/trac/tests/functional/__init__.py
@@ -1,4 +1,17 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
+
 """functional_tests
 
 While unittests work well for testing facets of an implementation, they fail to
@@ -42,17 +55,19 @@
  - lxml for XHTML validation (optional)
 """
 
+import exceptions
 import os
+import shutil
 import signal
+import stat
 import sys
 import time
-import shutil
-import stat
 import unittest
-import exceptions
+from datetime import datetime, timedelta
 
 import trac
-from trac.tests.functional.compat import close_fds, rmtree
+from trac.tests.compat import rmtree
+from trac.util.compat import close_fds
 
 # Handle missing twill so we can print a useful 'SKIP'
 # message.  We import subprocess first to allow customizing it on Windows
@@ -60,7 +75,7 @@
 # is allowed to load first, its (unmodified) copy will always be loaded.
 import subprocess
 
-from better_twill import twill, b, tc, ConnectError
+from trac.tests.functional.better_twill import twill, b, tc, ConnectError
 
 try:
     # This is the first indicator of whether the subversion bindings are
@@ -70,11 +85,15 @@
 except ImportError:
     has_svn = False
 
-from datetime import datetime, timedelta
+try:
+    from configobj import ConfigObj
+except ImportError:
+    ConfigObj = None
+    print "SKIP: fine-grained permission tests (ConfigObj not installed)"
 
+from trac.test import TestSetup, TestCaseSetup
 from trac.tests.contentgen import random_sentence, random_page, random_word, \
     random_unique_camel
-from trac.test import TestSetup, TestCaseSetup
 
 internal_error = 'Trac detected an internal error:'
 
@@ -112,14 +131,23 @@
             subdirectory 'testenv<portnum>'.
             """
             if port is None:
-                port = 8000 + os.getpid() % 1000
-                dirname = "testenv"
+                try:
+                    port = int(os.getenv('TRAC_TEST_PORT'))
+                except (TypeError, ValueError):
+                    pass
+
+            env_path = os.getenv('TRAC_TEST_ENV_PATH')
+            if not env_path:
+                env_name = 'testenv%s' % (port or '')
+                env_path = os.path.join(trac_source_tree, env_name)
             else:
-                dirname = "testenv%s" % port
-            dirname = os.path.join(trac_source_tree, dirname)
+                env_path += str(port or '')
+
+            if port is None:
+                port = 8000 + os.getpid() % 1000
 
             baseurl = "http://127.0.0.1:%s" % port
-            self._testenv = self.env_class(dirname, port, baseurl)
+            self._testenv = self.env_class(env_path, port, baseurl)
             self._testenv.start()
             self._tester = self.tester_class(baseurl)
             self.fixture = (self._testenv, self._tester)
@@ -137,10 +165,16 @@
 
     class FunctionalTwillTestCaseSetup(FunctionalTestCaseSetup):
         failureException = twill.errors.TwillAssertionError
+
 else:
     # We're going to have to skip the functional tests
+    class FunctionalTestSuite(TestSetup):
+        def __init__(self):
+            raise ImportError("Twill not installed")
+
     class FunctionalTwillTestCaseSetup:
         pass
+
     class FunctionalTestCaseSetup:
         pass
 
@@ -151,15 +185,33 @@
     return '(Owned by:(<[^>]*>|\\n| )*%s)' % username
 
 
+def functionalSuite():
+    suite = FunctionalTestSuite()
+    return suite
+
+
 def suite():
-    if twill:
-        from trac.tests.functional.testcases import suite
-        suite = suite()
-    else:
-        diagnostic = "SKIP: functional tests"
-        if not twill:
-            diagnostic += " (no twill installed)"
-        print diagnostic
+    try:
+        suite = functionalSuite()
+        import trac.tests.functional.testcases
+        trac.tests.functional.testcases.functionalSuite(suite)
+        import trac.versioncontrol.tests
+        trac.versioncontrol.tests.functionalSuite(suite)
+        import trac.ticket.tests
+        trac.ticket.tests.functionalSuite(suite)
+        import trac.prefs.tests
+        trac.prefs.tests.functionalSuite(suite)
+        import trac.wiki.tests
+        trac.wiki.tests.functionalSuite(suite)
+        import trac.timeline.tests
+        trac.timeline.tests.functionalSuite(suite)
+        import trac.admin.tests
+        trac.admin.tests.functionalSuite(suite)
+        # The db tests should be last since the backup test occurs there.
+        import trac.db.tests
+        trac.db.tests.functionalSuite(suite)
+    except ImportError, e:
+        print("SKIP: functional tests (%s)" % e)
         # No tests to run, provide an empty suite.
         suite = unittest.TestSuite()
     return suite
diff --git a/trac/trac/tests/functional/better_twill.py b/trac/trac/tests/functional/better_twill.py
index d181437..f9314a6 100755
--- a/trac/trac/tests/functional/better_twill.py
+++ b/trac/trac/tests/functional/better_twill.py
@@ -1,12 +1,26 @@
-#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-2014 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/.
+
 """better_twill is a small wrapper around twill to set some sane defaults and
 monkey-patch some better versions of some of twill's methods.
 It also handles twill's absense.
 """
 
 import os
-from os.path import abspath, dirname, join
 import sys
+import urllib
+import urlparse
+from os.path import abspath, dirname, join
 from pkg_resources import parse_version as pv
 try:
     from cStringIO import StringIO
@@ -36,9 +50,9 @@
 
 if twill:
     # We want Trac to generate valid html, and therefore want to test against
-    # the html as generated by Trac.  "tidy" tries to clean up broken html, and
-    # is responsible for one difficult to track down testcase failure (for
-    # #5497).  Therefore we turn it off here.
+    # the html as generated by Trac.  "tidy" tries to clean up broken html,
+    # and is responsible for one difficult to track down testcase failure
+    # (for #5497).  Therefore we turn it off here.
     twill.commands.config('use_tidy', '0')
 
     # We use a transparent proxy to access the global browser object through
@@ -90,9 +104,9 @@
                 context = data.splitlines()[max(0, entry.line - 5):
                                             entry.line + 6]
                 msg.append("\n# %s\n# URL: %s\n# Line %d, column %d\n\n%s\n"
-                    % (entry.message, entry.filename,
-                       entry.line, entry.column,
-                       "\n".join([each.decode('utf-8') for each in context])))
+                           % (entry.message, entry.filename, entry.line,
+                              entry.column, "\n".join([each.decode('utf-8')
+                                                       for each in context])))
             return "\n".join(msg).encode('ascii', 'xmlcharrefreplace')
 
         def _validate_xhtml(func_name, *args, **kwargs):
@@ -120,6 +134,8 @@
         """Write the current html to a file.  Name the file based on the
         current testcase.
         """
+        import unittest
+
         frame = sys._getframe()
         while frame:
             if frame.f_code.co_name in ('runTest', 'setUp', 'tearDown'):
@@ -127,19 +143,26 @@
                 testname = testcase.__class__.__name__
                 tracdir = testcase._testenv.tracdir
                 break
+            elif isinstance(frame.f_locals.get('self'), unittest.TestCase):
+                testcase = frame.f_locals['self']
+                testname = '%s.%s' % (testcase.__class__.__name__,
+                                      testcase._testMethodName)
+                tracdir = testcase._testenv.tracdir
+                break
             frame = frame.f_back
         else:
             # We didn't find a testcase in the stack, so we have no clue what's
             # going on.
             raise Exception("No testcase was found on the stack.  This was "
-                "really not expected, and I don't know how to handle it.")
+                            "really not expected, and I don't know how to "
+                            "handle it.")
 
         filename = os.path.join(tracdir, 'log', "%s.html" % testname)
         html_file = open(filename, 'w')
         html_file.write(b.get_html())
         html_file.close()
 
-        return filename
+        return urlparse.urljoin('file:', urllib.pathname2url(filename))
 
     # Twill isn't as helpful with errors as I'd like it to be, so we replace
     # the formvalue function.  This would be better done as a patch to Twill.
@@ -149,8 +172,8 @@
         except (twill.errors.TwillAssertionError,
                 twill.utils.ClientForm.ItemNotFoundError), e:
             filename = twill_write_html()
-            args = e.args + (filename,)
-            raise twill.errors.TwillAssertionError(*args)
+            raise twill.errors.TwillAssertionError('%s at %s' %
+                                                   (unicode(e), filename))
     tc.formvalue = better_formvalue
     tc.fv = better_formvalue
 
@@ -163,8 +186,8 @@
         if formname is not None: # enhancement to directly specify the form
             browser._browser.form = browser.get_form(formname)
         old_submit(fieldname)
-
     b.submit = better_browser_submit
+
     def better_submit(fieldname=None, formname=None):
         b.submit(fieldname, formname)
     tc.submit = better_submit
@@ -186,39 +209,41 @@
         control = b.get_form_field(form, fieldname)
 
         if not control.is_of_kind('file'):
-            raise twill.errors.TwillException('ERROR: field is not a file '
-                                              'upload field!')
+            raise twill.errors.TwillException("ERROR: field is not a file "
+                                              "upload field!")
 
         b.clicked(form, control)
         control.add_file(fp, content_type, filename)
     tc.formfile = better_formfile
 
-    # Twill's tc.find() does not provide any guidance on what we got instead of
-    # what was expected.
+    # Twill's tc.find() does not provide any guidance on what we got
+    # instead of what was expected.
     def better_find(what, flags='', tcfind=tc.find):
         try:
             tcfind(what, flags)
         except twill.errors.TwillAssertionError, e:
             filename = twill_write_html()
-            args = e.args + (filename,)
-            raise twill.errors.TwillAssertionError(*args)
+            raise twill.errors.TwillAssertionError('%s at %s' %
+                                                   (unicode(e), filename))
     tc.find = better_find
+
     def better_notfind(what, flags='', tcnotfind=tc.notfind):
         try:
             tcnotfind(what, flags)
         except twill.errors.TwillAssertionError, e:
             filename = twill_write_html()
-            args = e.args + (filename,)
-            raise twill.errors.TwillAssertionError(*args)
+            raise twill.errors.TwillAssertionError('%s at %s' %
+                                                   (unicode(e), filename))
     tc.notfind = better_notfind
+
     # Same for tc.url - no hint about what went wrong!
     def better_url(should_be, tcurl=tc.url):
         try:
             tcurl(should_be)
         except twill.errors.TwillAssertionError, e:
             filename = twill_write_html()
-            args = e.args + (filename,)
-            raise twill.errors.TwillAssertionError(*args)
+            raise twill.errors.TwillAssertionError('%s at %s' %
+                                                   (unicode(e), filename))
     tc.url = better_url
 else:
     b = tc = None
diff --git a/trac/trac/tests/functional/compat.py b/trac/trac/tests/functional/compat.py
index 3536e31..0d4ed73 100755
--- a/trac/trac/tests/functional/compat.py
+++ b/trac/trac/tests/functional/compat.py
@@ -1,18 +1,15 @@
-#!/usr/bin/python
-import os
-import shutil
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
 
+from trac.tests.compat import rmtree
 from trac.util.compat import close_fds
-
-# On Windows, shutil.rmtree doesn't remove files with the read-only
-# attribute set, so this function explicitly removes it on every error
-# before retrying.  Even on Linux, shutil.rmtree chokes on read-only
-# directories, so we use this version in all cases.
-# Fix from http://bitten.edgewall.org/changeset/521
-def rmtree(root):
-    """Catch shutil.rmtree failures on Windows when files are read-only."""
-    def _handle_error(fn, path, excinfo):
-        os.chmod(path, 0666)
-        fn(path)
-    return shutil.rmtree(root, onerror=_handle_error)
-
diff --git a/trac/trac/tests/functional/svntestenv.py b/trac/trac/tests/functional/svntestenv.py
index dd56045..3954063 100644
--- a/trac/trac/tests/functional/svntestenv.py
+++ b/trac/trac/tests/functional/svntestenv.py
@@ -1,31 +1,47 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-2014 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 re
 from subprocess import call
 
 from testenv import FunctionalTestEnvironment
-from trac.tests.functional.compat import close_fds
 from trac.tests.functional import logfile
+from trac.util.compat import close_fds
+
 
 class SvnFunctionalTestEnvironment(FunctionalTestEnvironment):
     def work_dir(self):
         return os.path.join(self.dirname, 'workdir')
 
+    def repo_path(self, filename):
+        return os.path.join(self.dirname, filename)
+
     def repo_path_for_initenv(self):
-        return os.path.join(self.dirname, 'repo')
+        return self.repo_path('repo')
 
     def create_repo(self):
         """
         Initialize a repo of the type :attr:`self.repotype`.
         """
-        if call(["svnadmin", "create", self.repo_path_for_initenv()],
-                 stdout=logfile, stderr=logfile, close_fds=close_fds):
-            raise Exception('unable to create subversion repository')
-        if call(['svn', 'co', self.repo_url(), self.work_dir()], stdout=logfile,
-                 stderr=logfile, close_fds=close_fds):
+        self.svnadmin_create()
+        if call(['svn', 'co', self.repo_url(), self.work_dir()],
+                stdout=logfile, stderr=logfile, close_fds=close_fds):
             raise Exception('Checkout from %s failed.' % self.repo_url())
 
     def destroy_repo(self):
-        """The deletion of the testenvironment will remove the repo as well."""
+        """The deletion of the test environment will remove the
+        repo as well."""
         pass
 
     def repo_url(self):
@@ -38,6 +54,18 @@
         else:
             return 'file://' + repodir
 
+    def svnadmin_create(self, filename=None):
+        """Subversion helper to create a new repository."""
+        if filename is None:
+            path = self.repo_path_for_initenv()
+        else:
+            path = self.repo_path(filename)
+        if call(["svnadmin", "create", path],
+                stdout=logfile, stderr=logfile, close_fds=close_fds):
+            raise Exception('unable to create subversion repository: %r' %
+                            path)
+        return path
+
     def svn_mkdir(self, paths, msg, username='admin'):
         """Subversion helper to create a new directory within the main
         repository.  Operates directly on the repository url, so a working
@@ -48,11 +76,12 @@
             self._testenv.svn_mkdir(["abc", "def"], "Add dirs")
 
         """
-        self.call_in_workdir(['svn', '--username=%s' % username, 'mkdir', '-m', msg]
-                + [self.repo_url() + '/' + d for d in paths])
+        self.call_in_workdir(['svn', '--username=%s' % username,
+                              'mkdir', '-m', msg]
+                             + [self.repo_url() + '/' + d for d in paths])
         self.call_in_workdir(['svn', 'update'])
 
-    def svn_add(self, filename, data):
+    def svn_add(self, filename, data, msg=None, username='admin'):
         """Subversion helper to add a file to the given path within the main
         repository.
 
@@ -67,8 +96,10 @@
         self.call_in_workdir(['svn', 'add', filename])
         environ = os.environ.copy()
         environ['LC_ALL'] = 'C'     # Force English messages in svn
-        output = self.call_in_workdir(['svn', '--username=admin', 'commit', '-m',
-                        'Add %s' % filename, filename], environ=environ)
+        msg = 'Add %s' % filename if msg is None else msg
+        output = self.call_in_workdir(['svn', '--username=%s' % username,
+                                       'commit', '-m', msg, filename],
+                                      environ=environ)
         try:
             revision = re.search(r'Committed revision ([0-9]+)\.',
                                  output).group(1)
@@ -77,3 +108,5 @@
             raise Exception(*args)
         return int(revision)
 
+    def call_in_workdir(self, args, environ=None):
+        return self.call_in_dir(self.work_dir(), args, environ)
diff --git a/trac/trac/tests/functional/testcases.py b/trac/trac/tests/functional/testcases.py
index a5edabe..6da8d44 100755
--- a/trac/trac/tests/functional/testcases.py
+++ b/trac/trac/tests/functional/testcases.py
@@ -1,17 +1,96 @@
-# -*- encoding: utf-8 -*-
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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 os
+
 from trac.tests.functional import *
+from trac.util import create_file
+
+
+class TestAttachmentNonexistentParent(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """TracError should be raised when navigating to the attachment
+        page for a nonexistent resource."""
+        self._tester.go_to_wiki('NonexistentPage')
+        tc.find("The page NonexistentPage does not exist. "
+                "You can create it here.")
+        tc.find(r"\bCreate this page\b")
+
+        tc.go(self._tester.url + '/attachment/wiki/NonexistentPage')
+        tc.find('<h1>Trac Error</h1>\s+<p class="message">'
+                'Parent resource NonexistentPage doesn\'t exist</p>')
+
+
+class TestErrorPage(FunctionalTwillTestCaseSetup):
+    """Validate the error page.
+    Defects reported to trac-hacks should use the Component defined in the
+    plugin's URL (#11434).
+    """
+    def runTest(self):
+        env = self._testenv.get_trac_environment()
+        env.config.set('components', 'RaiseExceptionPlugin.*', 'enabled')
+        env.config.save()
+        create_file(os.path.join(env.path, 'plugins',
+                                 'RaiseExceptionPlugin.py'),
+"""\
+from trac.core import Component, implements
+from trac.web.api import IRequestHandler
+
+url = None
+
+class RaiseExceptionPlugin(Component):
+    implements(IRequestHandler)
+
+    def match_request(self, req):
+        if req.path_info.startswith('/raise-exception'):
+            return True
+
+    def process_request(self, req):
+        print 'maybe?'
+        if req.args.get('report') == 'tho':
+            global url
+            url = 'http://trac-hacks.org/wiki/HelloWorldMacro'
+        raise Exception
+
+""")
+        self._testenv.restart()
+
+        try:
+            tc.go(self._tester.url + '/raise-exception')
+            tc.find(internal_error)
+            tc.find('<form class="newticket" method="get" '
+                    'action="http://trac.edgewall.org/newticket">')
+
+            tc.go(self._tester.url + '/raise-exception?report=tho')
+            tc.find(internal_error)
+            tc.find('<form class="newticket" method="get" '
+                    'action="http://trac-hacks.org/newticket">')
+            tc.find('<input type="hidden" name="component" '
+                    'value="HelloWorldMacro" />')
+        finally:
+            env.config.set('components', 'RaiseExceptionPlugin.*', 'disabled')
 
 
 class RegressionTestRev6017(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of the plugin reload fix in r6017"""
         # Setup the DeleteTicket plugin
-        plugin = open(os.path.join(self._testenv.command_cwd, 'sample-plugins',
-            'workflow', 'DeleteTicket.py')).read()
-        open(os.path.join(self._testenv.tracdir, 'plugins', 'DeleteTicket.py'),
-             'w').write(plugin)
+        plugin = open(os.path.join(self._testenv.trac_src,
+                                   'sample-plugins', 'workflow',
+                                   'DeleteTicket.py')).read()
+        open(os.path.join(self._testenv.tracdir, 'plugins',
+                          'DeleteTicket.py'), 'w').write(plugin)
         env = self._testenv.get_trac_environment()
         prevconfig = env.config.get('ticket', 'workflow')
         env.config.set('ticket', 'workflow',
@@ -30,7 +109,6 @@
             # Remove the DeleteTicket plugin
             env.config.set('ticket', 'workflow', prevconfig)
             env.config.save()
-            self._testenv.restart()
             for ext in ('py', 'pyc', 'pyo'):
                 filename = os.path.join(self._testenv.tracdir, 'plugins',
                                         'DeleteTicket.%s' % ext)
@@ -52,7 +130,8 @@
         env.log.debug("RegressionTestTicket3833 debug1")
         debug1 = traclogfile.read()
         self.assertNotEqual(debug1.find("RegressionTestTicket3833 debug1"), -1,
-            'Logging off when it should have been on.\n%r' % debug1)
+                            'Logging off when it should have been on.\n%r'
+                            % debug1)
 
 
 class RegressionTestTicket3833b(FunctionalTestCaseSetup):
@@ -75,7 +154,8 @@
         self.assertNotEqual(debug2.find("RegressionTestTicket3833 info2"), -1,
                             'Logging at info failed.\n%r' % debug2)
         self.assertEqual(debug2.find("RegressionTestTicket3833 debug2"), -1,
-            'Logging still on when it should have been off.\n%r' % debug2)
+                         'Logging still on when it should have been off.\n%r'
+                         % debug2)
 
 
 class RegressionTestTicket3833c(FunctionalTestCaseSetup):
@@ -103,12 +183,11 @@
         success = debug3.find("RegressionTestTicket3833 debug3") != -1
         if not success:
             # Ok, the testcase failed, but we really need logging enabled.
-            self._testenv.restart()
             env.log.debug("RegressionTestTicket3833 fixup3")
             fixup3 = traclogfile.read()
             message = 'Logging still off when it should have been on.\n' \
                       '%r\n%r' % (debug3, fixup3)
-        self.assert_(success, message)
+        self.assertTrue(success, message)
 
 
 class RegressionTestTicket5572(FunctionalTwillTestCaseSetup):
@@ -122,29 +201,28 @@
 class RegressionTestTicket7209(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/7209"""
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
+        ticketid = self._tester.create_ticket()
         self._tester.create_ticket()
         self._tester.add_comment(ticketid)
-        self._tester.attach_file_to_ticket(ticketid, tempfilename='hello.txt',
+        self._tester.attach_file_to_ticket(ticketid, filename='hello.txt',
                                            description='Preserved Descr')
         self._tester.go_to_ticket(ticketid)
         tc.find('Preserved Descr')
         # Now replace the existing attachment, and the description should come
         # through.
-        self._tester.attach_file_to_ticket(ticketid, tempfilename='hello.txt',
+        self._tester.attach_file_to_ticket(ticketid, filename='hello.txt',
                                            description='', replace=True)
         self._tester.go_to_ticket(ticketid)
         tc.find('Preserved Descr')
 
-        self._tester.attach_file_to_ticket(ticketid, tempfilename='blah.txt',
+        self._tester.attach_file_to_ticket(ticketid, filename='blah.txt',
                                            description='Second Attachment')
         self._tester.go_to_ticket(ticketid)
         tc.find('Second Attachment')
 
         # This one should get a new description when it's replaced
         # (Second->Other)
-        self._tester.attach_file_to_ticket(ticketid, tempfilename='blah.txt',
+        self._tester.attach_file_to_ticket(ticketid, filename='blah.txt',
                                            description='Other Attachment',
                                            replace=True)
         self._tester.go_to_ticket(ticketid)
@@ -159,10 +237,9 @@
         Upload of a file which the browsers associates a Content-Type
         of multipart/related (e.g. an .mht file) should succeed.
         """
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
+        ticketid = self._tester.create_ticket()
         self._tester.create_ticket()
-        self._tester.attach_file_to_ticket(ticketid, tempfilename='hello.mht',
+        self._tester.attach_file_to_ticket(ticketid, filename='hello.mht',
                                            content_type='multipart/related',
                                            data="""
 Well, the actual content of the file doesn't matter, the problem is
@@ -171,15 +248,6 @@
 """)
 
 
-class ErrorPageValidation(FunctionalTwillTestCaseSetup):
-    def runTest(self):
-        """Validate the error page"""
-        url = self._tester.url + '/wiki/WikiStart'
-        tc.go(url + '?version=bug')
-        tc.url(url)
-        tc.find(internal_error)
-
-
 class RegressionTestTicket3663(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Regression test for non-UTF-8 PATH_INFO (#3663)
@@ -196,14 +264,79 @@
         tc.find('Invalid URL encoding')
 
 
-def functionalSuite():
-    suite = FunctionalTestSuite()
-    return suite
+class RegressionTestTicket6318(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Regression test for non-ascii usernames (#6318)
+        """
+        # first do a logout, otherwise we might end up logged in as
+        # admin again, as this is the first thing the tester does.
+        # ... but even before that we need to make sure we're coming
+        # from a valid URL, which is not the case if we're just coming
+        # from the above test! ('/wiki/\xE9t\xE9')
+        self._tester.go_to_front()
+        self._tester.logout()
+        try:
+            # also test a regular ascii user name
+            self._testenv.adduser(u'user')
+            self._tester.login(u'user')
+            self._tester.go_to_front()
+            self._tester.logout()
+            # now test utf-8 user name
+            self._testenv.adduser(u'joé')
+            self._tester.login(u'joé')
+            self._tester.go_to_front()
+            self._tester.logout()
+            # finally restore expected 'admin' login
+            self._tester.login('admin')
+        finally:
+            self._testenv.deluser(u'joé')
 
 
-def suite():
-    suite = functionalSuite()
+class RegressionTestTicket11434(FunctionalTwillTestCaseSetup):
+    """Test for regression of http://trac.edgewall.org/ticket/11434
+    Defects reported to trac-hacks should use the Component defined in the
+    plugin's URL.
+    """
+    def runTest(self):
+        env = self._testenv.get_trac_environment()
+        env.config.set('components', 'RaiseExceptionPlugin.*', 'enabled')
+        env.config.save()
+        create_file(os.path.join(env.path, 'plugins', 'RaiseExceptionPlugin.py'),
+"""\
+from trac.core import Component, implements
+from trac.web.api import IRequestHandler
 
+url = 'http://trac-hacks.org/wiki/HelloWorldMacro'
+
+class RaiseExceptionPlugin(Component):
+    implements(IRequestHandler)
+
+    def match_request(self, req):
+        if req.path_info == '/raise-exception':
+            return True
+
+    def process_request(self, req):
+        raise Exception
+
+""")
+
+        try:
+            tc.go(self._tester.url + '/raise-exception')
+            tc.find(internal_error)
+            tc.find('<form class="newticket" method="get" '
+                    'action="http://trac-hacks.org/newticket">')
+            tc.find('<input type="hidden" name="component" '
+                    'value="HelloWorldMacro" />')
+        finally:
+            env.config.set('components', 'RaiseExceptionPlugin.*', 'disabled')
+
+
+def functionalSuite(suite=None):
+    if not suite:
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
+    suite.addTest(TestAttachmentNonexistentParent())
+    suite.addTest(TestErrorPage())
     suite.addTest(RegressionTestRev6017())
     suite.addTest(RegressionTestTicket3833a())
     suite.addTest(RegressionTestTicket3833b())
@@ -211,27 +344,11 @@
     suite.addTest(RegressionTestTicket5572())
     suite.addTest(RegressionTestTicket7209())
     suite.addTest(RegressionTestTicket9880())
-    suite.addTest(ErrorPageValidation())
     suite.addTest(RegressionTestTicket3663())
-
-    import trac.versioncontrol.tests
-    trac.versioncontrol.tests.functionalSuite(suite)
-    import trac.ticket.tests
-    trac.ticket.tests.functionalSuite(suite)
-    import trac.prefs.tests
-    trac.prefs.tests.functionalSuite(suite)
-    import trac.wiki.tests
-    trac.wiki.tests.functionalSuite(suite)
-    import trac.timeline.tests
-    trac.timeline.tests.functionalSuite(suite)
-    import trac.admin.tests
-    trac.admin.tests.functionalSuite(suite)
-    # The db tests should be last since the backup test occurs there.
-    import trac.db.tests
-    trac.db.tests.functionalSuite(suite)
-
+    suite.addTest(RegressionTestTicket6318())
+    suite.addTest(RegressionTestTicket11434())
     return suite
 
 
 if __name__ == '__main__':
-    unittest.main(defaultTest='suite')
+    unittest.main(defaultTest='functionalSuite')
diff --git a/trac/trac/tests/functional/testenv.py b/trac/trac/tests/functional/testenv.py
index 6703037..fe37f85 100755
--- a/trac/trac/tests/functional/testenv.py
+++ b/trac/trac/tests/functional/testenv.py
@@ -1,24 +1,40 @@
-#!/usr/bin/python
 # -*- coding: utf-8 -*-
 #
+# Copyright (C) 2008-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/.
+
 """Object for creating and destroying a Trac environment for testing purposes.
 Provides some Trac environment-wide utility functions, and a way to call
 :command:`trac-admin` without it being on the path."""
 
-import os
-import time
-import signal
-import sys
-import errno
 import locale
+import os
+import re
+import sys
+import time
 from subprocess import call, Popen, PIPE, STDOUT
 
 from trac.env import open_environment
 from trac.test import EnvironmentStub, get_dburi
-from trac.tests.functional.compat import rmtree
-from trac.tests.functional import logfile
+from trac.tests.compat import rmtree
+from trac.tests.functional import logfile, trac_source_tree
 from trac.tests.functional.better_twill import tc, ConnectError
-from trac.util.compat import close_fds
+from trac.util import terminate
+from trac.util.compat import close_fds, wait_for_file_mtime_change
+from trac.util.text import to_utf8
+
+try:
+    from configobj import ConfigObj
+except ImportError:
+    ConfigObj = None
 
 # TODO: refactor to support testing multiple frontends, backends
 #       (and maybe repositories and authentication).
@@ -33,6 +49,7 @@
 #       (those need to test search escaping, among many other things like long
 #       paths in browser and unicode chars being allowed/translating...)
 
+
 class FunctionalTestEnvironment(object):
     """Common location for convenience functions that work with the test
     environment on Trac.  Subclass this and override some methods if you are
@@ -47,6 +64,7 @@
     def __init__(self, dirname, port, url):
         """Create a :class:`FunctionalTestEnvironment`, see the class itself
         for parameter information."""
+        self.trac_src = trac_source_tree
         self.url = url
         self.command_cwd = os.path.normpath(os.path.join(dirname, '..'))
         self.dirname = os.path.abspath(dirname)
@@ -60,8 +78,6 @@
         self.create()
         locale.setlocale(locale.LC_ALL, '')
 
-    trac_src = '.'
-
     @property
     def dburi(self):
         dburi = get_dburi()
@@ -97,11 +113,13 @@
 
     def post_create(self, env):
         """Hook for modifying the environment after creation.  For example, to
-        set configuration like::
+        set configuration like:
+        ::
 
             def post_create(self, env):
                 env.config.set('git', 'path', '/usr/bin/git')
                 env.config.save()
+
         """
         pass
 
@@ -125,10 +143,11 @@
         if call([sys.executable,
                  os.path.join(self.trac_src, 'contrib', 'htpasswd.py'), "-c",
                  "-b", self.htpasswd, "admin", "admin"], close_fds=close_fds,
-                 cwd=self.command_cwd):
+                cwd=self.command_cwd):
             raise Exception('Unable to setup admin password')
         self.adduser('user')
-        self._tracadmin('permission', 'add', 'admin', 'TRAC_ADMIN')
+        self.adduser('joe')
+        self.grant_perm('admin', 'TRAC_ADMIN')
         # Setup Trac logging
         env = self.get_trac_environment()
         env.config.set('logging', 'log_type', 'file')
@@ -140,23 +159,89 @@
     def adduser(self, user):
         """Add a user to the environment.  The password will be set to the
         same as username."""
+        user = to_utf8(user)
         if call([sys.executable, os.path.join(self.trac_src, 'contrib',
                  'htpasswd.py'), '-b', self.htpasswd,
                  user, user], close_fds=close_fds, cwd=self.command_cwd):
             raise Exception('Unable to setup password for user "%s"' % user)
 
+    def deluser(self, user):
+        """Delete a user from the environment."""
+        user = to_utf8(user)
+        self._tracadmin('session', 'delete', user)
+        if call([sys.executable, os.path.join(self.trac_src, 'contrib',
+                 'htpasswd.py'), '-D', self.htpasswd, user],
+                close_fds=close_fds, cwd=self.command_cwd):
+            raise Exception('Unable to remove password for user "%s"' % user)
+
+    def grant_perm(self, user, perm):
+        """Grant permission(s) to specified user. A single permission may
+        be specified as a string, or multiple permissions may be
+        specified as a list or tuple of strings."""
+        if isinstance(perm, (list, tuple)):
+            self._tracadmin('permission', 'add', user, *perm)
+        else:
+            self._tracadmin('permission', 'add', user, perm)
+        # We need to force an environment reset, as this is necessary
+        # for the permission change to take effect: grant only
+        # invalidates the `DefaultPermissionStore._all_permissions`
+        # cache, but the `DefaultPermissionPolicy.permission_cache` is
+        # unaffected.
+        self.get_trac_environment().config.touch()
+
+    def revoke_perm(self, user, perm):
+        """Revoke permission(s) from specified user. A single permission
+        may be specified as a string, or multiple permissions may be
+        specified as a list or tuple of strings."""
+        if isinstance(perm, (list, tuple)):
+            self._tracadmin('permission', 'remove', user, *perm)
+        else:
+            self._tracadmin('permission', 'remove', user, perm)
+        # Force an environment reset (see grant_perm above)
+        self.get_trac_environment().config.touch()
+
+    def set_config(self, *args):
+        """Calls trac-admin to get the value for the given option
+        in `trac.ini`."""
+        self._tracadmin('config', 'set', *args)
+
+    def get_config(self, *args):
+        """Calls trac-admin to set the value for the given option
+        in `trac.ini`."""
+        return self._tracadmin('config', 'get', *args)
+
+    def remove_config(self, *args):
+        """Calls trac-admin to remove the value for the given option
+        in `trac.ini`."""
+        return self._tracadmin('config', 'remove', *args)
+
     def _tracadmin(self, *args):
         """Internal utility method for calling trac-admin"""
         proc = Popen([sys.executable, os.path.join(self.trac_src, 'trac',
-                      'admin', 'console.py'), self.tracdir]
-                      + list(args), stdout=PIPE, stderr=STDOUT,
-                      close_fds=close_fds, cwd=self.command_cwd)
-        out = proc.communicate()[0]
+                      'admin', 'console.py'), self.tracdir],
+                     stdin=PIPE, stdout=PIPE, stderr=STDOUT,
+                     close_fds=close_fds, cwd=self.command_cwd)
+        if args:
+            if any('\n' in arg for arg in args):
+                raise Exception(
+                    "trac-admin in interactive mode doesn't support "
+                    "arguments with newline characters: %r" % (args,))
+            # Don't quote first token which is sub-command name
+            input = ' '.join(('"%s"' % to_utf8(arg) if idx else arg)
+                             for idx, arg in enumerate(args))
+        else:
+            input = None
+        out = proc.communicate(input=input)[0]
         if proc.returncode:
             print(out)
             logfile.write(out)
-            raise Exception('Failed with exitcode %s running trac-admin ' \
-                            'with %r' % (proc.returncode, args))
+            raise Exception("Failed while running trac-admin with arguments %r.\n"
+                            "Exitcode: %s \n%s"
+                            % (args, proc.returncode, out))
+        else:
+            # trac-admin is started in interactive mode, so we strip away
+            # everything up to the to the interactive prompt
+            return re.split(r'\r?\nTrac \[[^]]+\]> ', out, 2)[1]
 
     def start(self):
         """Starts the webserver, and waits for it to come up."""
@@ -177,8 +262,7 @@
         server = Popen(args + options + [self.tracdir],
                        stdout=logfile, stderr=logfile,
                        close_fds=close_fds,
-                       cwd=self.command_cwd,
-                      )
+                       cwd=self.command_cwd)
         self.pid = server.pid
         # Verify that the url is ok
         timeout = 30
@@ -199,17 +283,7 @@
         FIXME: probably needs a nicer way to exit for coverage to work
         """
         if self.pid:
-            if os.name == 'nt':
-                # Untested
-                res = call(["taskkill", "/f", "/pid", str(self.pid)],
-                     stdin=PIPE, stdout=PIPE, stderr=PIPE)
-            else:
-                os.kill(self.pid, signal.SIGTERM)
-                try:
-                    os.waitpid(self.pid, 0)
-                except OSError, e:
-                    if e.errno != errno.ESRCH:
-                        raise
+            terminate(self)
 
     def restart(self):
         """Restarts the webserver"""
@@ -224,13 +298,73 @@
         """Default to no repository"""
         return "''" # needed for Python 2.3 and 2.4 on win32
 
-    def call_in_workdir(self, args, environ=None):
+    def call_in_dir(self, dir, args, environ=None):
         proc = Popen(args, stdout=PIPE, stderr=logfile,
-                     close_fds=close_fds, cwd=self.work_dir(), env=environ)
+                     close_fds=close_fds, cwd=dir, env=environ)
         (data, _) = proc.communicate()
         if proc.wait():
             raise Exception('Unable to run command %s in %s' %
-                            (args, self.work_dir()))
-
+                            (args, dir))
         logfile.write(data)
         return data
+
+    def enable_authz_permpolicy(self, authz_content, filename=None):
+        """Enables the Authz permissions policy. The `authz_content` will
+        be written to `filename`, and may be specified in a triple-quoted
+        string.::
+
+           [wiki:WikiStart@*]
+           * = WIKI_VIEW
+           [wiki:PrivatePage@*]
+           john = WIKI_VIEW
+           * = !WIKI_VIEW
+
+        `authz_content` may also be a dictionary of dictionaries specifying
+        the sections and key/value pairs of each section, however this form
+        should only be used when the order of the entries in the file is not
+        important, as the order cannot be known.::
+
+           {
+            'wiki:WikiStart@*': {'*': 'WIKI_VIEW'},
+            'wiki:PrivatePage@*': {'john': 'WIKI_VIEW', '*': '!WIKI_VIEW'},
+           }
+
+        The `filename` parameter is optional, and if omitted a filename will
+        be generated by computing a hash of `authz_content`, prefixed with
+        "authz-".
+        """
+        if not ConfigObj:
+            raise ImportError("Can't enable authz permissions policy. " +
+                              "ConfigObj not installed.")
+        if filename is None:
+            from hashlib import md5
+            filename = 'authz-' + md5(str(authz_content)).hexdigest()[0:9]
+        env = self.get_trac_environment()
+        permission_policies = env.config.get('trac', 'permission_policies')
+        env.config.set('trac', 'permission_policies',
+                       'AuthzPolicy, ' + permission_policies)
+        authz_file = self.tracdir + '/conf/' + filename
+        if isinstance(authz_content, basestring):
+            authz_content = [line.strip() for line in
+                             authz_content.strip().splitlines()]
+        authz_config = ConfigObj(authz_content, encoding='utf8',
+                                 write_empty_values=True, indent_type='')
+        authz_config.filename = authz_file
+        wait_for_file_mtime_change(authz_file)
+        authz_config.write()
+        env.config.set('authz_policy', 'authz_file', authz_file)
+        env.config.set('components', 'tracopt.perm.authz_policy.*', 'enabled')
+        env.config.save()
+
+    def disable_authz_permpolicy(self):
+        """Disables the Authz permission policy."""
+        env = self.get_trac_environment()
+        permission_policies = env.config.get('trac', 'permission_policies')
+        pp_list = [p.strip() for p in permission_policies.split(',')]
+        if 'AuthzPolicy' in pp_list:
+            pp_list.remove('AuthzPolicy')
+        permission_policies = ', '.join(pp_list)
+        env.config.set('trac', 'permission_policies', permission_policies)
+        env.config.remove('authz_policy', 'authz_file')
+        env.config.remove('components', 'tracopt.perm.authz_policy.*')
+        env.config.save()
diff --git a/trac/trac/tests/functional/tester.py b/trac/trac/tests/functional/tester.py
index 3ccd556..e655ae2 100755
--- a/trac/trac/tests/functional/tester.py
+++ b/trac/trac/tests/functional/tester.py
@@ -1,19 +1,34 @@
-#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
+
 """The :class:`FunctionalTester` object provides a higher-level interface to
 working with a Trac environment to make test cases more succinct.
 """
 
+import re
+
 from trac.tests.functional import internal_error
 from trac.tests.functional.better_twill import tc, b
 from trac.tests.contentgen import random_page, random_sentence, random_word, \
-    random_unique_camel
-from trac.util.text import unicode_quote
+                                  random_unique_camel
+from trac.util.text import to_utf8, unicode_quote
 
 try:
     from cStringIO import StringIO
 except ImportError:
     from StringIO import StringIO
 
+
 class FunctionalTester(object):
     """Provides a library of higher-level operations for interacting with a
     test environment.
@@ -34,10 +49,11 @@
 
     def login(self, username):
         """Login as the given user"""
+        username = to_utf8(username)
         tc.add_auth("", self.url, username, username)
         self.go_to_front()
         tc.find("Login")
-        tc.follow("Login")
+        tc.follow(r"\bLogin\b")
         # We've provided authentication info earlier, so this should
         # redirect back to the base url.
         tc.find("logged in as %s" % username)
@@ -47,8 +63,9 @@
 
     def logout(self):
         """Logout"""
-        tc.follow("Logout")
+        tc.submit('logout', 'logout')
         tc.notfind(internal_error)
+        tc.notfind('logged in as')
 
     def create_ticket(self, summary=None, info=None):
         """Create a new (random) ticket in the test environment.  Returns
@@ -63,10 +80,10 @@
         `summary` and `description` default to randomly-generated values.
         """
         self.go_to_front()
-        tc.follow('New Ticket')
+        tc.follow(r"\bNew Ticket\b")
         tc.notfind(internal_error)
-        if summary == None:
-            summary = random_sentence(4)
+        if summary is None:
+            summary = random_sentence(5)
         tc.formvalue('propertyform', 'field_summary', summary)
         tc.formvalue('propertyform', 'field_description', random_page())
         if info:
@@ -75,17 +92,12 @@
         tc.submit('submit')
         # we should be looking at the newly created ticket
         tc.url(self.url + '/ticket/%s' % (self.ticketcount + 1))
+        tc.notfind(internal_error)
         # Increment self.ticketcount /after/ we've verified that the ticket
         # was created so a failure does not trigger spurious later
         # failures.
         self.ticketcount += 1
 
-        # verify the ticket creation event shows up in the timeline
-        self.go_to_timeline()
-        tc.formvalue('prefs', 'ticket', True)
-        tc.submit()
-        tc.find('Ticket.*#%s.*created' % self.ticketcount)
-
         return self.ticketcount
 
     def quickjump(self, search):
@@ -94,51 +106,91 @@
         tc.submit()
         tc.notfind(internal_error)
 
-    def go_to_front(self):
-        """Go to the Trac front page"""
-        tc.go(self.url)
-        tc.url(self.url)
+    def go_to_url(self, url):
+        tc.go(url)
+        tc.url(re.escape(url))
         tc.notfind(internal_error)
 
-    def go_to_ticket(self, ticketid):
-        """Surf to the page for the given ticket ID.  Assumes ticket
-        exists."""
-        ticket_url = self.url + "/ticket/%s" % ticketid
-        tc.go(ticket_url)
-        tc.url(ticket_url)
+    def go_to_front(self):
+        """Go to the Trac front page"""
+        self.go_to_url(self.url)
 
-    def go_to_wiki(self, name):
-        """Surf to the page for the given wiki page."""
+    def go_to_ticket(self, ticketid=None):
+        """Surf to the page for the given ticket ID, or to the NewTicket page
+        if `ticketid` is not specified or is `None`. If `ticketid` is
+        specified, it assumes the ticket exists."""
+        if ticketid is not None:
+            ticket_url = self.url + '/ticket/%s' % ticketid
+        else:
+            ticket_url = self.url + '/newticket'
+        self.go_to_url(ticket_url)
+        tc.url(ticket_url + '$')
+
+    def go_to_wiki(self, name, version=None):
+        """Surf to the wiki page. By default this will be the latest version
+        of the page.
+
+        :param name: name of the wiki page.
+        :param version: version of the wiki page.
+        """
         # Used to go based on a quickjump, but if the wiki pagename isn't
         # camel case, that won't work.
         wiki_url = self.url + '/wiki/%s' % name
-        tc.go(wiki_url)
-        tc.url(wiki_url)
+        if version:
+            wiki_url += '?version=%s' % version
+        self.go_to_url(wiki_url)
 
     def go_to_timeline(self):
         """Surf to the timeline page."""
         self.go_to_front()
-        tc.follow('Timeline')
+        tc.follow(r"\bTimeline\b")
         tc.url(self.url + '/timeline')
 
+    def go_to_view_tickets(self, href='report'):
+        """Surf to the View Tickets page. By default this will be the Reports
+        page, but 'query' can be specified for the `href` argument to support
+        non-default configurations."""
+        self.go_to_front()
+        tc.follow(r"\bView Tickets\b")
+        tc.url(self.url + '/' + href.lstrip('/'))
+
     def go_to_query(self):
         """Surf to the custom query page."""
         self.go_to_front()
-        tc.follow('View Tickets')
-        tc.follow('Custom Query')
+        tc.follow(r"\bView Tickets\b")
+        tc.follow(r"\bCustom Query\b")
         tc.url(self.url + '/query')
 
-    def go_to_admin(self):
-        """Surf to the webadmin page."""
+    def go_to_admin(self, panel_label=None):
+        """Surf to the webadmin page. Continue surfing to a specific
+        admin page if `panel_label` is specified."""
         self.go_to_front()
-        tc.follow('\\bAdmin\\b')
+        tc.follow(r"\bAdmin\b")
+        tc.url(self.url + '/admin')
+        if panel_label is not None:
+            tc.follow(r"\b%s\b" % panel_label)
 
     def go_to_roadmap(self):
         """Surf to the roadmap page."""
         self.go_to_front()
-        tc.follow('\\bRoadmap\\b')
+        tc.follow(r"\bRoadmap\b")
         tc.url(self.url + '/roadmap')
 
+    def go_to_milestone(self, name):
+        """Surf to the specified milestone page. Assumes milestone exists."""
+        self.go_to_roadmap()
+        tc.follow(r"\bMilestone: %s\b" % name)
+        tc.url(self.url + '/milestone/%s' % name)
+
+    def go_to_preferences(self, panel_label=None):
+        """Surf to the preferences page. Continue surfing to a specific
+        preferences panel if `panel_label` is specified."""
+        self.go_to_front()
+        tc.follow(r"\bPreferences\b")
+        tc.url(self.url + '/prefs')
+        if panel_label is not None:
+            tc.follow(r"\b%s\b" % panel_label)
+
     def add_comment(self, ticketid, comment=None):
         """Adds a comment to the given ticket ID, assumes ticket exists."""
         self.go_to_ticket(ticketid)
@@ -152,35 +204,16 @@
         tc.url(self.url + '/ticket/%s(?:#comment:.*)?$' % ticketid)
         return comment
 
-    def attach_file_to_ticket(self, ticketid, data=None, tempfilename=None,
+    def attach_file_to_ticket(self, ticketid, data=None, filename=None,
                               description=None, replace=False,
                               content_type=None):
         """Attaches a file to the given ticket id, with random data if none is
         provided.  Assumes the ticket exists.
         """
-        if data is None:
-            data = random_page()
-        if description is None:
-            description = random_sentence()
-        if tempfilename is None:
-            tempfilename = random_word()
-
         self.go_to_ticket(ticketid)
-        # set the value to what it already is, so that twill will know we
-        # want this form.
-        tc.formvalue('attachfile', 'action', 'new')
-        tc.submit()
-        tc.url(self.url + "/attachment/ticket/" \
-               "%s/\\?action=new&attachfilebutton=Attach\\+file" % ticketid)
-        fp = StringIO(data)
-        tc.formfile('attachment', 'attachment', tempfilename,
-                    content_type=content_type, fp=fp)
-        tc.formvalue('attachment', 'description', description)
-        if replace:
-            tc.formvalue('attachment', 'replace', True)
-        tc.submit()
-        tc.url(self.url + '/attachment/ticket/%s/$' % ticketid)
-        return tempfilename
+        return self._attach_file_to_resource('ticket', ticketid, data,
+                                             filename, description,
+                                             replace, content_type)
 
     def clone_ticket(self, ticketid):
         """Create a clone of the given ticket id using the clone button."""
@@ -194,57 +227,66 @@
         tc.url(self.url + "/ticket/%s" % self.ticketcount)
         return self.ticketcount
 
-    def create_wiki_page(self, page, content=None):
-        """Creates the specified wiki page, with random content if none is
-        provided.
+    def create_wiki_page(self, name=None, content=None, comment=None):
+        """Creates a wiki page, with a random unique CamelCase name if none
+        is provided, random content if none is provided and a random comment
+        if none is provided.  Returns the name of the wiki page.
         """
-        if content == None:
+        if name is None:
+            name = random_unique_camel()
+        if content is None:
             content = random_page()
-        page_url = self.url + "/wiki/" + page
-        tc.go(page_url)
-        tc.url(page_url)
-        tc.find("The page %s does not exist." % page)
-        tc.formvalue('modifypage', 'action', 'edit')
-        tc.submit()
-        tc.url(page_url + '\\?action=edit')
+        self.go_to_wiki(name)
+        tc.find("The page %s does not exist." % name)
 
-        tc.formvalue('edit', 'text', content)
-        tc.submit('save')
-        tc.url(page_url+'$')
+        self.edit_wiki_page(name, content, comment)
 
         # verify the event shows up in the timeline
         self.go_to_timeline()
         tc.formvalue('prefs', 'wiki', True)
         tc.submit()
-        tc.find(page + ".*created")
+        tc.find(name + ".*created")
 
-    def attach_file_to_wiki(self, name, data=None, tempfilename=None):
+        self.go_to_wiki(name)
+
+        return name
+
+    def edit_wiki_page(self, name, content=None, comment=None):
+        """Edits a wiki page, with random content is none is provided.
+        and a random comment if none is provided. Returns the content.
+        """
+        if content is None:
+            content = random_page()
+        if comment is None:
+            comment = random_sentence()
+        self.go_to_wiki(name)
+        tc.formvalue('modifypage', 'action', 'edit')
+        tc.submit()
+        tc.formvalue('edit', 'text', content)
+        tc.formvalue('edit', 'comment', comment)
+        tc.submit('save')
+        page_url = self.url + '/wiki/%s' % name
+        tc.url(page_url+'$')
+
+        return content
+
+    def attach_file_to_wiki(self, name, data=None, filename=None,
+                            description=None, replace=False,
+                            content_type=None):
         """Attaches a file to the given wiki page, with random content if none
         is provided.  Assumes the wiki page exists.
         """
-        if data == None:
-            data = random_page()
-        if tempfilename is None:
-            tempfilename = random_word()
+
         self.go_to_wiki(name)
-        # set the value to what it already is, so that twill will know we
-        # want this form.
-        tc.formvalue('attachfile', 'action', 'new')
-        tc.submit()
-        tc.url(self.url + "/attachment/wiki/" \
-               "%s/\\?action=new&attachfilebutton=Attach\\+file" % name)
-        fp = StringIO(data)
-        tc.formfile('attachment', 'attachment', tempfilename, fp=fp)
-        tc.formvalue('attachment', 'description', random_sentence())
-        tc.submit()
-        tc.url(self.url + '/attachment/wiki/%s/$' % name)
-        return tempfilename
+        return self._attach_file_to_resource('wiki', name, data,
+                                             filename, description,
+                                             replace, content_type)
 
     def create_milestone(self, name=None, due=None):
         """Creates the specified milestone, with a random name if none is
         provided.  Returns the name of the milestone.
         """
-        if name == None:
+        if name is None:
             name = random_unique_camel()
         milestone_url = self.url + "/admin/ticket/milestones"
         tc.go(milestone_url)
@@ -260,32 +302,51 @@
         tc.find(name)
 
         # Make sure it's on the roadmap.
-        tc.follow('Roadmap')
+        tc.follow(r"\bRoadmap\b")
         tc.url(self.url + "/roadmap")
         tc.find('Milestone:.*%s' % name)
-        tc.follow(name)
+        tc.follow(r"\b%s\b" % name)
         tc.url('%s/milestone/%s' % (self.url, unicode_quote(name)))
         if not due:
             tc.find('No date set')
 
         return name
 
-    def create_component(self, name=None, user=None):
+    def attach_file_to_milestone(self, name, data=None, filename=None,
+                                 description=None, replace=False,
+                                 content_type=None):
+        """Attaches a file to the given milestone, with random content if none
+        is provided.  Assumes the milestone exists.
+        """
+
+        self.go_to_milestone(name)
+        return self._attach_file_to_resource('milestone', name, data,
+                                             filename, description,
+                                             replace, content_type)
+
+    def create_component(self, name=None, owner=None, description=None):
         """Creates the specified component, with a random camel-cased name if
         none is provided.  Returns the name."""
-        if name == None:
+        if name is None:
             name = random_unique_camel()
         component_url = self.url + "/admin/ticket/components"
         tc.go(component_url)
         tc.url(component_url)
         tc.formvalue('addcomponent', 'name', name)
-        if user != None:
-            tc.formvalue('addcomponent', 'owner', user)
+        if owner is not None:
+            tc.formvalue('addcomponent', 'owner', owner)
         tc.submit()
         # Verify the component appears in the component list
         tc.url(component_url)
         tc.find(name)
         tc.notfind(internal_error)
+        if description is not None:
+            tc.follow(r"\b%s\b" % name)
+            tc.formvalue('modcomp', 'description', description)
+            tc.submit('save')
+            tc.url(component_url)
+            tc.find("Your changes have been saved.")
+            tc.notfind(internal_error)
         # TODO: verify the component shows up in the newticket page
         return name
 
@@ -294,7 +355,7 @@
         ``severity``, etc). If no name is given, a unique random word is used.
         The name is returned.
         """
-        if name == None:
+        if name is None:
             name = random_unique_camel()
         priority_url = self.url + "/admin/ticket/" + kind
         tc.go(priority_url)
@@ -326,12 +387,12 @@
         """Create a new version.  The name defaults to a random camel-cased
         word if not provided."""
         version_admin = self.url + "/admin/ticket/versions"
-        if name == None:
+        if name is None:
             name = random_unique_camel()
         tc.go(version_admin)
         tc.url(version_admin)
         tc.formvalue('addversion', 'name', name)
-        if releasetime != None:
+        if releasetime is not None:
             tc.formvalue('addversion', 'time', releasetime)
         tc.submit()
         tc.url(version_admin)
@@ -342,7 +403,7 @@
     def create_report(self, title, query, description):
         """Create a new report with the given title, query, and description"""
         self.go_to_front()
-        tc.follow('View Tickets')
+        tc.follow(r"\bView Tickets\b")
         tc.formvalue('create_report', 'action', 'new') # select the right form
         tc.submit()
         tc.find('New Report')
@@ -364,3 +425,29 @@
         tc.formvalue('propertyform', 'milestone', milestone)
         tc.submit('submit')
         # TODO: verify the change occurred.
+
+    def _attach_file_to_resource(self, realm, name, data=None,
+                                 filename=None, description=None,
+                                 replace=False, content_type=None):
+        """Attaches a file to a resource. Assumes the resource exists and
+           has already been navigated to."""
+
+        if data is None:
+            data = random_page()
+        if description is None:
+            description = random_sentence()
+        if filename is None:
+            filename = random_word()
+
+        tc.submit('attachfilebutton', 'attachfile')
+        tc.url(self.url + r'/attachment/%s/%s/\?action=new$' % (realm, name))
+        fp = StringIO(data)
+        tc.formfile('attachment', 'attachment', filename,
+                    content_type=content_type, fp=fp)
+        tc.formvalue('attachment', 'description', description)
+        if replace:
+            tc.formvalue('attachment', 'replace', True)
+        tc.submit()
+        tc.url(self.url + r'/attachment/%s/%s/$' % (realm, name))
+
+        return filename
diff --git a/trac/trac/tests/notification.py b/trac/trac/tests/notification.py
index 00ea9a4..a90d82f 100644
--- a/trac/trac/tests/notification.py
+++ b/trac/trac/tests/notification.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.blot@free.fr>
 # All rights reserved.
 #
@@ -19,24 +19,30 @@
 # classes to run SMTP notification tests
 #
 
+import base64
+import os
+import quopri
+import re
 import socket
 import string
 import threading
-import re
-import base64
-import quopri
+import unittest
 
+from trac.config import ConfigurationError
+from trac.notification import SendmailEmailSender, SmtpEmailSender
+from trac.test import EnvironmentStub
 
 LF = '\n'
 CR = '\r'
-email_re = re.compile(r"([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+")
+SMTP_TEST_PORT = 7000 + os.getpid() % 1000
+email_re = re.compile(r'([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+')
 header_re = re.compile(r'^=\?(?P<charset>[\w\d\-]+)\?(?P<code>[qb])\?(?P<value>.*)\?=$')
 
 
 class SMTPServerInterface:
     """
-    A base class for the imlementation of an application specific SMTP
-    Server. Applications should subclass this and overide these
+    A base class for the implementation of an application specific SMTP
+    Server. Applications should subclass this and override these
     methods, which by default do nothing.
 
     A method is defined for each RFC821 command. For each of these
@@ -44,7 +50,7 @@
     client. The 'data' method is called after all of the client DATA
     is received.
 
-    If a method returns 'None', then a '250 OK'message is
+    If a method returns 'None', then a '250 OK' message is
     automatically sent to the client. If a subclass returns a non-null
     string then it is returned instead.
     """
@@ -67,10 +73,10 @@
     def reset(self, args):
         return None
 
+
 #
 # Some helper functions for manipulating from & to addresses etc.
 #
-
 def strip_address(address):
     """
     Strip the leading & trailing <> from an address.  Handy for
@@ -80,6 +86,7 @@
     end = string.index(address, '>')
     return address[start:end]
 
+
 def split_to(address):
     """
     Return 'address' as undressed (host, fulladdress) tuple.
@@ -88,7 +95,7 @@
     start = string.index(address, '<') + 1
     sep = string.index(address, '@') + 1
     end = string.index(address, '>')
-    return (address[sep:end], address[start:end],)
+    return address[sep:end], address[start:end]
 
 
 #
@@ -133,13 +140,13 @@
                     lump = self.socket.recv(1024)
                     if len(lump):
                         data += lump
-                        if (len(data) >= 2) and data[-2:] == '\r\n':
+                        if len(data) >= 2 and data[-2:] == '\r\n':
                             completeLine = 1
                             if self.state != SMTPServerEngine.ST_DATA:
                                 rsp, keep = self.do_command(data)
                             else:
                                 rsp = self.do_data(data)
-                                if rsp == None:
+                                if rsp is None:
                                     continue
                             self.socket.send(rsp + "\r\n")
                             if keep == 0:
@@ -171,28 +178,28 @@
             keep = 0
         elif cmd == "MAIL":
             if self.state != SMTPServerEngine.ST_HELO:
-                return ("503 Bad command sequence", 1)
+                return "503 Bad command sequence", 1
             self.state = SMTPServerEngine.ST_MAIL
             rv = self.impl.mail_from(data[5:])
         elif cmd == "RCPT":
             if (self.state != SMTPServerEngine.ST_MAIL) and \
                (self.state != SMTPServerEngine.ST_RCPT):
-                return ("503 Bad command sequence", 1)
+                return "503 Bad command sequence", 1
             self.state = SMTPServerEngine.ST_RCPT
             rv = self.impl.rcpt_to(data[5:])
         elif cmd == "DATA":
             if self.state != SMTPServerEngine.ST_RCPT:
-                return ("503 Bad command sequence", 1)
+                return "503 Bad command sequence", 1
             self.state = SMTPServerEngine.ST_DATA
             self.data_accum = ""
-            return ("354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1)
+            return "354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1
         else:
-            return ("505 Eh? WTF was that?", 1)
+            return "505 Eh? WTF was that?", 1
 
         if rv:
-            return (rv, keep)
+            return rv, keep
         else:
-            return("250 OK", keep)
+            return "250 OK", keep
 
     def do_data(self, data):
         """
@@ -227,7 +234,7 @@
         self._socket_service = None
 
     def serve(self, impl):
-        while ( self._resume ):
+        while self._resume:
             try:
                 nsd = self._socket.accept()
             except socket.error:
@@ -273,11 +280,11 @@
 
     def mail_from(self, args):
         if args.lower().startswith('from:'):
-            self.sender = strip_address(args[5:].replace('\r\n','').strip())
+            self.sender = strip_address(args[5:].replace('\r\n', '').strip())
 
     def rcpt_to(self, args):
         if args.lower().startswith('to:'):
-            rcpt = args[3:].replace('\r\n','').strip()
+            rcpt = args[3:].replace('\r\n', '').strip()
             self.recipients.append(strip_address(rcpt))
 
     def data(self, args):
@@ -300,12 +307,12 @@
     def __init__(self, port):
         self.port = port
         self.server = SMTPServer(port)
-        self.store  = SMTPServerStore()
+        self.store = SMTPServerStore()
         threading.Thread.__init__(self)
 
     def run(self):
         # run from within the SMTP server thread
-        self.server.serve(impl = self.store)
+        self.server.serve(impl=self.store)
 
     def start(self):
         # run from the main thread
@@ -356,19 +363,19 @@
     # header does not seem to be MIME-encoded
     if not mo:
         return header
-    # attempts to decode the hedear,
-    # following the specified MIME endoding and charset
+    # attempts to decode the header,
+    # following the specified MIME encoding and charset
     try:
         encoding = mo.group('code').lower()
-        if encoding  == 'q':
+        if encoding == 'q':
             val = quopri.decodestring(mo.group('value'), header=True)
         elif encoding == 'b':
             val = base64.decodestring(mo.group('value'))
         else:
-            raise AssertionError, "unsupported encoding: %s" % encoding
+            raise AssertionError("unsupported encoding: %s" % encoding)
         header = unicode(val, mo.group('charset'))
     except Exception, e:
-        raise AssertionError, e
+        raise AssertionError(e)
     return header
 
 
@@ -384,19 +391,19 @@
     # last line does not contain the final line ending
     msg += '\r\n'
     for line in msg.splitlines(True):
-        if body != None:
+        if body is not None:
             # append current line to the body
             if line[-2] == CR:
                 body += line[0:-2]
                 body += '\n'
             else:
-                raise AssertionError, "body misses CRLF: %s (0x%x)" \
-                                      % (line, ord(line[-1]))
+                raise AssertionError("body misses CRLF: %s (0x%x)"
+                                     % (line, ord(line[-1])))
         else:
             if line[-2] != CR:
                 # RFC822 requires CRLF at end of field line
-                raise AssertionError, "header field misses CRLF: %s (0x%x)" \
-                                      % (line, ord(line[-1]))
+                raise AssertionError("header field misses CRLF: %s (0x%x)"
+                                     % (line, ord(line[-1])))
             # discards CR
             line = line[0:-2]
             if line.strip() == '':
@@ -405,11 +412,11 @@
             else:
                 val = None
                 if line[0] in ' \t':
-                    # continution of the previous line
+                    # continuation of the previous line
                     if not lh:
                         # unexpected multiline
-                        raise AssertionError, \
-                             "unexpected folded line: %s" % line
+                        raise AssertionError("unexpected folded line: %s"
+                                             % line)
                     val = decode_header(line.strip(' \t'))
                     # appends the current line to the previous one
                     if not isinstance(headers[lh], tuple):
@@ -420,14 +427,52 @@
                     # splits header name from value
                     (h, v) = line.split(':', 1)
                     val = decode_header(v.strip())
-                    if headers.has_key(h):
+                    if h in headers:
                         if isinstance(headers[h], tuple):
                             headers[h] += val
                         else:
                             headers[h] = (headers[h], val)
                     else:
                         headers[h] = val
-                    # stores the last header (for multilines headers)
+                    # stores the last header (for multi-line headers)
                     lh = h
     # returns the headers and the message body
-    return (headers, body)
+    return headers, body
+
+
+class SendmailEmailSenderTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+
+    def test_sendmail_path_not_found_raises(self):
+        sender = SendmailEmailSender(self.env)
+        self.env.config.set('notification', 'sendmail_path',
+                            os.path.join(os.path.dirname(__file__),
+                                         'sendmail'))
+        self.assertRaises(ConfigurationError, sender.send,
+                          'admin@domain.com', ['foo@domain.com'], "")
+
+
+class SmtpEmailSenderTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+
+    def test_smtp_server_not_found_raises(self):
+        sender = SmtpEmailSender(self.env)
+        self.env.config.set('notification', 'smtp_server', 'localhost')
+        self.env.config.set('notification', 'smtp_port', '65536')
+        self.assertRaises(ConfigurationError, sender.send,
+                          'admin@domain.com', ['foo@domain.com'], "")
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(SendmailEmailSenderTestCase))
+    suite.addTest(unittest.makeSuite(SmtpEmailSenderTestCase))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/tests/perm.py b/trac/trac/tests/perm.py
index cbd34a4..691ea93 100644
--- a/trac/trac/tests/perm.py
+++ b/trac/trac/tests/perm.py
@@ -1,15 +1,30 @@
-from trac import perm
-from trac.core import *
-from trac.test import EnvironmentStub
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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 unittest
 
+from trac import perm
+from trac.core import *
+from trac.resource import Resource
+from trac.test import EnvironmentStub
+
 
 class DefaultPermissionStoreTestCase(unittest.TestCase):
 
     def setUp(self):
-        self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore,
-                                           perm.DefaultPermissionGroupProvider])
+        self.env = \
+            EnvironmentStub(enable=[perm.DefaultPermissionStore,
+                                    perm.DefaultPermissionGroupProvider])
         self.store = perm.DefaultPermissionStore(self.env)
 
     def tearDown(self):
@@ -21,10 +36,10 @@
             [('john', 'WIKI_MODIFY'),
              ('john', 'REPORT_ADMIN'),
              ('kate', 'TICKET_CREATE')])
-        self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
-                          sorted(self.store.get_user_permissions('john')))
-        self.assertEquals(['TICKET_CREATE'],
-                          self.store.get_user_permissions('kate'))
+        self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+                         sorted(self.store.get_user_permissions('john')))
+        self.assertEqual(['TICKET_CREATE'],
+                         self.store.get_user_permissions('kate'))
 
     def test_simple_group(self):
         self.env.db_transaction.executemany(
@@ -32,8 +47,8 @@
             [('dev', 'WIKI_MODIFY'),
              ('dev', 'REPORT_ADMIN'),
              ('john', 'dev')])
-        self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
-                          sorted(self.store.get_user_permissions('john')))
+        self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+                         sorted(self.store.get_user_permissions('john')))
 
     def test_nested_groups(self):
         self.env.db_transaction.executemany(
@@ -42,8 +57,8 @@
              ('dev', 'REPORT_ADMIN'),
              ('admin', 'dev'),
              ('john', 'admin')])
-        self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
-                          sorted(self.store.get_user_permissions('john')))
+        self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+                         sorted(self.store.get_user_permissions('john')))
 
     def test_mixed_case_group(self):
         self.env.db_transaction.executemany(
@@ -52,8 +67,8 @@
              ('Dev', 'REPORT_ADMIN'),
              ('Admin', 'Dev'),
              ('john', 'Admin')])
-        self.assertEquals(['REPORT_ADMIN', 'WIKI_MODIFY'],
-                          sorted(self.store.get_user_permissions('john')))
+        self.assertEqual(['REPORT_ADMIN', 'WIKI_MODIFY'],
+                         sorted(self.store.get_user_permissions('john')))
 
     def test_builtin_groups(self):
         self.env.db_transaction.executemany(
@@ -61,10 +76,10 @@
             [('authenticated', 'WIKI_MODIFY'),
              ('authenticated', 'REPORT_ADMIN'),
              ('anonymous', 'TICKET_CREATE')])
-        self.assertEquals(['REPORT_ADMIN', 'TICKET_CREATE', 'WIKI_MODIFY'],
-                          sorted(self.store.get_user_permissions('john')))
-        self.assertEquals(['TICKET_CREATE'],
-                          self.store.get_user_permissions('anonymous'))
+        self.assertEqual(['REPORT_ADMIN', 'TICKET_CREATE', 'WIKI_MODIFY'],
+                         sorted(self.store.get_user_permissions('john')))
+        self.assertEqual(['TICKET_CREATE'],
+                         self.store.get_user_permissions('anonymous'))
 
     def test_get_all_permissions(self):
         self.env.db_transaction.executemany(
@@ -76,7 +91,7 @@
                     ('dev', 'REPORT_ADMIN'),
                     ('john', 'dev')]
         for res in self.store.get_all_permissions():
-            self.failIf(res not in expected)
+            self.assertFalse(res not in expected)
 
 
 class TestPermissionRequestor(Component):
@@ -89,6 +104,48 @@
                 ('TEST_ADMIN', ['TEST_MODIFY'])]
 
 
+class PermissionErrorTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+
+    def test_default_message(self):
+        permission_error = perm.PermissionError()
+        self.assertEqual(None, permission_error.action)
+        self.assertEqual(None, permission_error.resource)
+        self.assertEqual(None, permission_error.env)
+        self.assertEqual("Insufficient privileges to perform this operation.",
+                         unicode(permission_error))
+        self.assertEqual("Forbidden", permission_error.title)
+        self.assertEqual(unicode(permission_error), permission_error.msg)
+
+    def test_message_specified(self):
+        message = "The message."
+        permission_error = perm.PermissionError(msg=message)
+        self.assertEqual(message, unicode(permission_error))
+
+    def test_message_from_action(self):
+        action = 'WIKI_VIEW'
+        permission_error = perm.PermissionError(action)
+        self.assertEqual(action, permission_error.action)
+        self.assertEqual(None, permission_error.resource)
+        self.assertEqual(None, permission_error.env)
+        self.assertEqual("WIKI_VIEW privileges are required to perform this "
+                         "operation. You don't have the required "
+                         "permissions.", unicode(permission_error))
+
+    def test_message_from_action_and_resource(self):
+        action = 'WIKI_VIEW'
+        resource = Resource('wiki', 'WikiStart')
+        permission_error = perm.PermissionError(action, resource, self.env)
+        self.assertEqual(action, permission_error.action)
+        self.assertEqual(resource, permission_error.resource)
+        self.assertEqual(self.env, permission_error.env)
+        self.assertEqual("WIKI_VIEW privileges are required to perform this "
+                         "operation on WikiStart. You don't have the "
+                         "required permissions.", unicode(permission_error))
+
+
 class PermissionSystemTestCase(unittest.TestCase):
 
     def setUp(self):
@@ -130,7 +187,7 @@
         expected = [('bob', 'TEST_CREATE'),
                     ('jane', 'TEST_ADMIN')]
         for res in self.perm.get_all_permissions():
-            self.failIf(res not in expected)
+            self.assertFalse(res not in expected)
 
     def test_expand_actions_iter_7467(self):
         # Check that expand_actions works with iterators (#7467)
@@ -146,6 +203,8 @@
         self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore,
                                            perm.DefaultPermissionPolicy,
                                            TestPermissionRequestor])
+        self.env.config.set('trac', 'permission_policies',
+                            'DefaultPermissionPolicy')
         self.perm_system = perm.PermissionSystem(self.env)
         # by-pass DefaultPermissionPolicy cache:
         perm.DefaultPermissionPolicy.CACHE_EXPIRY = -1
@@ -157,19 +216,20 @@
         self.env.reset_db()
 
     def test_contains(self):
-        self.assertEqual(True, 'TEST_MODIFY' in self.perm)
-        self.assertEqual(True, 'TEST_ADMIN' in self.perm)
-        self.assertEqual(False, 'TRAC_ADMIN' in self.perm)
+        self.assertTrue('TEST_MODIFY' in self.perm)
+        self.assertTrue('TEST_ADMIN' in self.perm)
+        self.assertFalse('TRAC_ADMIN' in self.perm)
 
     def test_has_permission(self):
-        self.assertEqual(True, self.perm.has_permission('TEST_MODIFY'))
-        self.assertEqual(True, self.perm.has_permission('TEST_ADMIN'))
-        self.assertEqual(False, self.perm.has_permission('TRAC_ADMIN'))
+        self.assertTrue(self.perm.has_permission('TEST_MODIFY'))
+        self.assertTrue(self.perm.has_permission('TEST_ADMIN'))
+        self.assertFalse(self.perm.has_permission('TRAC_ADMIN'))
 
     def test_require(self):
         self.perm.require('TEST_MODIFY')
         self.perm.require('TEST_ADMIN')
-        self.assertRaises(perm.PermissionError, self.perm.require, 'TRAC_ADMIN')
+        self.assertRaises(perm.PermissionError,
+                          self.perm.require, 'TRAC_ADMIN')
 
     def test_assert_permission(self):
         self.perm.assert_permission('TEST_MODIFY')
@@ -216,12 +276,14 @@
 
 
 class PermissionPolicyTestCase(unittest.TestCase):
+
     def setUp(self):
         self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore,
                                            perm.DefaultPermissionPolicy,
                                            TestPermissionPolicy,
                                            TestPermissionRequestor])
-        self.env.config.set('trac', 'permission_policies', 'TestPermissionPolicy')
+        self.env.config.set('trac', 'permission_policies',
+                            'TestPermissionPolicy')
         self.policy = TestPermissionPolicy(self.env)
         self.perm = perm.PermissionCache(self.env, 'testuser')
 
@@ -246,7 +308,8 @@
                           ('testuser', 'TEST_ADMIN'): True})
 
     def test_policy_chaining(self):
-        self.env.config.set('trac', 'permission_policies', 'TestPermissionPolicy,DefaultPermissionPolicy')
+        self.env.config.set('trac', 'permission_policies',
+                            'TestPermissionPolicy,DefaultPermissionPolicy')
         self.policy.grant('testuser', ['TEST_MODIFY'])
         system = perm.PermissionSystem(self.env)
         system.grant_permission('testuser', 'TEST_ADMIN')
@@ -259,13 +322,17 @@
         self.assertEqual(self.policy.results,
                          {('testuser', 'TEST_MODIFY'): True,
                           ('testuser', 'TEST_ADMIN'): None})
+
+
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(DefaultPermissionStoreTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(PermissionSystemTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(PermissionCacheTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(PermissionPolicyTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(DefaultPermissionStoreTestCase))
+    suite.addTest(unittest.makeSuite(PermissionErrorTestCase))
+    suite.addTest(unittest.makeSuite(PermissionSystemTestCase))
+    suite.addTest(unittest.makeSuite(PermissionCacheTestCase))
+    suite.addTest(unittest.makeSuite(PermissionPolicyTestCase))
     return suite
 
+
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/tests/resource.py b/trac/trac/tests/resource.py
index b1a6401..051f0a3 100644
--- a/trac/trac/tests/resource.py
+++ b/trac/trac/tests/resource.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2007-2009 Edgewall Software
+# Copyright (C) 2007-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -165,7 +165,7 @@
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(resource))
-    suite.addTest(unittest.makeSuite(ResourceTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ResourceTestCase))
     suite.addTest(unittest.makeSuite(NeighborhoodTestCase, 'test'))
     return suite
 
diff --git a/trac/trac/tests/wikisyntax.py b/trac/trac/tests/wikisyntax.py
index ab4170a..dafef0a 100644
--- a/trac/trac/tests/wikisyntax.py
+++ b/trac/trac/tests/wikisyntax.py
@@ -1,4 +1,18 @@
-import os
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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/.
+
+from __future__ import with_statement
+
 import shutil
 import tempfile
 import unittest
@@ -11,6 +25,7 @@
 from trac.web.href import Href
 from trac.wiki.tests import formatter
 
+
 SEARCH_TEST_CASES = u"""
 ============================== search: link resolver
 search:foo
@@ -116,12 +131,14 @@
 def attachment_setup(tc):
     import trac.ticket.api
     import trac.wiki.api
-    tc.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
-    os.mkdir(tc.env.path)
-    attachment = Attachment(tc.env, 'wiki', 'WikiStart')
-    attachment.insert('file.txt', tempfile.TemporaryFile(), 0)
+    tc.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
+    with tc.env.db_transaction as db:
+        db("INSERT INTO wiki (name,version) VALUES ('SomePage/SubPage',1)")
+        db("INSERT INTO ticket (id) VALUES (123)")
     attachment = Attachment(tc.env, 'ticket', 123)
     attachment.insert('file.txt', tempfile.TemporaryFile(), 0)
+    attachment = Attachment(tc.env, 'wiki', 'WikiStart')
+    attachment.insert('file.txt', tempfile.TemporaryFile(), 0)
     attachment = Attachment(tc.env, 'wiki', 'SomePage/SubPage')
     attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
 
@@ -188,4 +205,3 @@
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
-
diff --git a/trac/trac/ticket/__init__.py b/trac/trac/ticket/__init__.py
index 9d4a70e..8797f9a 100644
--- a/trac/trac/ticket/__init__.py
+++ b/trac/trac/ticket/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
+
 from trac.ticket.api import *
 from trac.ticket.default_workflow import *
 from trac.ticket.model import *
diff --git a/trac/trac/ticket/admin.py b/trac/trac/ticket/admin.py
index 8b89b30..6633b9c 100644
--- a/trac/trac/ticket/admin.py
+++ b/trac/trac/ticket/admin.py
@@ -15,7 +15,9 @@
 
 from datetime import datetime
 
-from trac.admin import *
+from trac.admin.api import AdminCommandError, IAdminCommandProvider, \
+                           IAdminPanelProvider, console_date_format, \
+                           console_datetime_format, get_console_locale
 from trac.core import *
 from trac.perm import PermissionSystem
 from trac.resource import ResourceNotFound
@@ -40,11 +42,10 @@
     #            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:
+        if 'TICKET_ADMIN' in req.perm('admin', 'ticket/' + self._type):
             # in global scope show only products
             # in local scope everything but products
             parent = getattr(self.env, 'parent', None)
@@ -54,7 +55,6 @@
                         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)
@@ -152,7 +152,7 @@
                         req.redirect(req.href.admin(cat, page))
 
             data = {'view': 'list',
-                    'components': model.Component.select(self.env),
+                    'components': list(model.Component.select(self.env)),
                     'default': default}
 
         if self.config.getbool('ticket', 'restrict_owner'):
@@ -175,7 +175,7 @@
         yield ('component list', '',
                'Show available components',
                None, self._do_list)
-        yield ('component add', '<name> <owner>',
+        yield ('component add', '<name> [owner]',
                'Add a new component',
                self._complete_add, self._do_add)
         yield ('component rename', '<name> <newname>',
@@ -214,7 +214,7 @@
                      for c in model.Component.select(self.env)],
                     [_('Name'), _('Owner')])
 
-    def _do_add(self, name, owner):
+    def _do_add(self, name, owner=None):
         component = model.Component(self.env)
         component.name = name
         component.owner = owner
@@ -242,21 +242,19 @@
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'MILESTONE_VIEW' in req.perm:
+        if 'MILESTONE_VIEW' in req.perm('admin', 'ticket/' + self._type):
             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')
-
+        perm = req.perm('admin', 'ticket/' + self._type)
         # Detail view?
         if milestone:
             mil = model.Milestone(self.env, milestone)
             if req.method == 'POST':
                 if req.args.get('save'):
-                    req.perm.require('MILESTONE_MODIFY')
+                    perm.require('MILESTONE_MODIFY')
                     mil.name = name = req.args.get('name')
                     mil.due = mil.completed = None
                     due = req.args.get('duedate', '')
@@ -273,7 +271,7 @@
                                             _('Invalid Completion Date'))
                     mil.description = req.args.get('description', '')
                     try:
-                        mil.update()
+                        mil.update(author=req.authname)
                     except self.env.db_exc.IntegrityError:
                         raise TracError(_('The milestone "%(name)s" already '
                                           'exists.', name=name))
@@ -290,7 +288,7 @@
             if req.method == 'POST':
                 # Add Milestone
                 if req.args.get('add') and req.args.get('name'):
-                    req.perm.require('MILESTONE_CREATE')
+                    perm.require('MILESTONE_CREATE')
                     name = req.args.get('name')
                     try:
                         mil = model.Milestone(self.env, name=name)
@@ -313,7 +311,7 @@
 
                 # Remove milestone
                 elif req.args.get('remove'):
-                    req.perm.require('MILESTONE_DELETE')
+                    perm.require('MILESTONE_DELETE')
                     sel = req.args.get('sel')
                     if not sel:
                         raise TracError(_('No milestone selected'))
@@ -357,6 +355,10 @@
     # IAdminCommandProvider methods
 
     def get_admin_commands(self):
+        hints = {
+           'datetime': get_datetime_format_hint(get_console_locale(self.env)),
+           'iso8601': get_datetime_format_hint('iso8601'),
+        }
         yield ('milestone list', '',
                "Show milestones",
                None, self._do_list)
@@ -369,20 +371,22 @@
         yield ('milestone due', '<name> <due>',
                """Set milestone due date
 
-               The <due> date must be specified in the "%s" format.
+               The <due> date must be specified in the "%(datetime)s"
+               or "%(iso8601)s" (ISO 8601) 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,
+               """ % hints,
                self._complete_name, self._do_due)
         yield ('milestone completed', '<name> <completed>',
                """Set milestone complete date
 
-               The <completed> date must be specified in the "%s" format.
+               The <completed> date must be specified in the "%(datetime)s"
+               or "%(iso8601)s" (ISO 8601) 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,
+               """ % hints,
                self._complete_name, self._do_completed)
         yield ('milestone remove', '<name>',
                "Remove milestone",
@@ -396,10 +400,11 @@
             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))
+        print_table([(m.name,
+                      format_date(m.due, console_date_format)
+                      if m.due else None,
+                      format_datetime(m.completed, console_datetime_format)
+                      if m.completed else None)
                      for m in model.Milestone.select(self.env)],
                     [_("Name"), _("Due"), _("Completed")])
 
@@ -407,23 +412,27 @@
         milestone = model.Milestone(self.env)
         milestone.name = name
         if due is not None:
-            milestone.due = parse_date(due, hint='datetime')
+            milestone.due = parse_date(due, hint='datetime',
+                                       locale=get_console_locale(self.env))
         milestone.insert()
 
     def _do_rename(self, name, newname):
         milestone = model.Milestone(self.env, name)
         milestone.name = newname
-        milestone.update()
+        milestone.update(author=getuser())
 
     def _do_due(self, name, due):
         milestone = model.Milestone(self.env, name)
-        milestone.due = due and parse_date(due, hint='datetime')
+        milestone.due = parse_date(due, hint='datetime',
+                                   locale=get_console_locale(self.env)) \
+                        if due else None
         milestone.update()
 
     def _do_completed(self, name, completed):
         milestone = model.Milestone(self.env, name)
-        milestone.completed = completed and parse_date(completed,
-                                                       hint='datetime')
+        milestone.completed = parse_date(completed, hint='datetime',
+                                         locale=get_console_locale(self.env)) \
+                              if completed else None
         milestone.update()
 
     def _do_remove(self, name):
@@ -515,7 +524,7 @@
                         req.redirect(req.href.admin(cat, page))
 
             data = {'view': 'list',
-                    'versions': model.Version.select(self.env),
+                    'versions': list(model.Version.select(self.env)),
                     'default': default}
 
         Chrome(self.env).add_jquery_ui(req)
@@ -528,6 +537,10 @@
     # IAdminCommandProvider methods
 
     def get_admin_commands(self):
+        hints = {
+           'datetime': get_datetime_format_hint(get_console_locale(self.env)),
+           'iso8601': get_datetime_format_hint('iso8601'),
+        }
         yield ('version list', '',
                "Show versions",
                None, self._do_list)
@@ -540,11 +553,12 @@
         yield ('version time', '<name> <time>',
                """Set version date
 
-               The <time> must be specified in the "%s" format. Alternatively,
-               "now" can be used to set the version date to the current time.
-               To remove the date from a version, specify an empty string
-               ("").
-               """ % console_date_format_hint,
+               The <time> must be specified in the "%(datetime)s"
+               or "%(iso8601)s" (ISO 8601) format.
+               Alternatively, "now" can be used to set the version date to
+               the current time. To remove the date from a version, specify
+               an empty string ("").
+               """ % hints,
                self._complete_name, self._do_time)
         yield ('version remove', '<name>',
                "Remove version",
@@ -559,15 +573,18 @@
 
     def _do_list(self):
         print_table([(v.name,
-                      v.time and format_date(v.time, console_date_format))
-                     for v in model.Version.select(self.env)],
+                      format_date(v.time, console_date_format)
+                      if v.time else None)
+                    for v in model.Version.select(self.env)],
                     [_("Name"), _("Time")])
 
     def _do_add(self, name, time=None):
         version = model.Version(self.env)
         version.name = name
         if time is not None:
-            version.time = time and parse_date(time, hint='datetime')
+            version.time = parse_date(time, hint='datetime',
+                                      locale=get_console_locale(self.env)) \
+                           if time else None
         version.insert()
 
     def _do_rename(self, name, newname):
@@ -577,7 +594,9 @@
 
     def _do_time(self, name, time):
         version = model.Version(self.env, name)
-        version.time = time and parse_date(time, hint='datetime')
+        version.time = parse_date(time, hint='datetime',
+                                  locale=get_console_locale(self.env)) \
+                       if time else None
         version.update()
 
     def _do_remove(self, name):
diff --git a/trac/trac/ticket/api.py b/trac/trac/ticket/api.py
index 974d93a..f591ea5 100644
--- a/trac/trac/ticket/api.py
+++ b/trac/trac/ticket/api.py
@@ -123,6 +123,15 @@
     def ticket_deleted(ticket):
         """Called when a ticket is deleted."""
 
+    def ticket_comment_modified(ticket, cdate, author, comment, old_comment):
+        """Called when a ticket comment is modified."""
+
+    def ticket_change_deleted(ticket, cdate, changes):
+        """Called when a ticket change is deleted.
+
+        `changes` is a dictionary of tuple `(oldvalue, newvalue)`
+        containing the ticket change of the fields that have changed."""
+
 
 class ITicketManipulator(Interface):
     """Miscellaneous manipulation of ticket workflow features."""
@@ -562,24 +571,44 @@
                 cnum, realm, id = elts
                 if cnum != 'description' and cnum and not cnum[0].isdigit():
                     realm, id, cnum = elts # support old comment: style
+                id = as_int(id, None)
                 resource = formatter.resource(realm, id)
         else:
             resource = formatter.resource
             cnum = target
 
-        if resource and resource.realm == 'ticket':
-            id = as_int(resource.id, None)
-            if id is not None:
-                href = "%s#comment:%s" % (formatter.href.ticket(resource.id),
-                                          cnum)
-                title = _("Comment %(cnum)s for Ticket #%(id)s", cnum=cnum,
-                          id=resource.id)
-                if 'TICKET_VIEW' in formatter.perm(resource):
-                    for status, in self.env.db_query(
-                            "SELECT status FROM ticket WHERE id=%s", (id,)):
-                        return tag.a(label, href=href, title=title,
-                                     class_=status)
-                return tag.a(label, href=href, title=title)
+        if resource and resource.id and resource.realm == 'ticket' and \
+                cnum and (all(c.isdigit() for c in cnum) or cnum == 'description'):
+            href = title = class_ = None
+            if self.resource_exists(resource):
+                from trac.ticket.model import Ticket
+                ticket = Ticket(self.env, resource.id)
+                if cnum != 'description' and not ticket.get_change(cnum):
+                    title = _("ticket comment does not exist")
+                    class_ = 'missing ticket'
+                elif 'TICKET_VIEW' in formatter.perm(resource):
+                    href = formatter.href.ticket(resource.id) + \
+                           "#comment:%s" % cnum
+                    if resource.id != formatter.resource.id:
+                        if cnum == 'description':
+                            title = _("Description for Ticket #%(id)s",
+                                      id=resource.id)
+                        else:
+                            title = _("Comment %(cnum)s for Ticket #%(id)s",
+                                      cnum=cnum, id=resource.id)
+                        class_ = ticket['status'] + ' ticket'
+                    else:
+                        title = _("Description") if cnum == 'description' \
+                                                 else _("Comment %(cnum)s",
+                                                        cnum=cnum)
+                        class_ = 'ticket'
+                else:
+                    title = _("no permission to view ticket")
+                    class_ = 'forbidden ticket'
+            else:
+                title = _("ticket does not exist")
+                class_ = 'missing ticket'
+            return tag.a(label, class_=class_, href=href, title=title)
         return label
 
     # IResourceManager methods
diff --git a/trac/trac/ticket/batch.py b/trac/trac/ticket/batch.py
index 72bb414..8dc509d 100644
--- a/trac/trac/ticket/batch.py
+++ b/trac/trac/ticket/batch.py
@@ -29,6 +29,7 @@
 from trac.web import IRequestHandler
 from trac.web.chrome import add_warning, add_script_data
 
+
 class BatchModifyModule(Component):
     """Ticket batch modification module.
 
diff --git a/trac/trac/ticket/default_workflow.py b/trac/trac/ticket/default_workflow.py
index 6b087ab..1b222a3 100644
--- a/trac/trac/ticket/default_workflow.py
+++ b/trac/trac/ticket/default_workflow.py
@@ -20,6 +20,7 @@
 
 from ConfigParser import RawConfigParser
 from StringIO import StringIO
+from functools import partial
 
 from genshi.builder import tag
 
@@ -98,10 +99,12 @@
     """Ticket action controller which provides actions according to a
     workflow defined in trac.ini.
 
-    The workflow is idefined in the `[ticket-workflow]` section of the
+    The workflow is defined in the `[ticket-workflow]` section of the
     [wiki:TracIni#ticket-workflow-section trac.ini] configuration file.
     """
 
+    implements(IEnvironmentSetupParticipant, ITicketActionController)
+
     ticket_workflow_section = ConfigSection('ticket-workflow',
         """The workflow for tickets is controlled by plugins. By default,
         there's only a `ConfigurableTicketWorkflow` component in charge.
@@ -129,7 +132,6 @@
                 self.log.warning("Ticket workflow action '%s' doesn't define "
                                  "any transitions", name)
 
-    implements(ITicketActionController, IEnvironmentSetupParticipant)
 
     # IEnvironmentSetupParticipant methods
 
@@ -226,18 +228,14 @@
         this_action = self.actions[action]
         status = this_action['newstate']
         operations = this_action['operations']
-        current_owner = ticket._old.get('owner', ticket['owner'] or '(none)')
-        if not (Chrome(self.env).show_email_addresses
-                or 'EMAIL_VIEW' in req.perm(ticket.resource)):
-            format_user = obfuscate_email_address
-        else:
-            format_user = lambda address: address
-        current_owner = format_user(current_owner)
+        current_owner = ticket._old.get('owner', ticket['owner'])
+        format_author = partial(Chrome(self.env).format_author, req)
+        formatted_current_owner = format_author(current_owner or _("(none)"))
 
         control = [] # default to nothing
         hints = []
         if 'reset_workflow' in operations:
-            control.append(tag("from invalid state "))
+            control.append(_("from invalid state"))
             hints.append(_("Current state no longer exists"))
         if 'del_owner' in operations:
             hints.append(_("The ticket will be disowned"))
@@ -245,7 +243,7 @@
             id = 'action_%s_reassign_owner' % action
             selected_owner = req.args.get(id, req.authname)
 
-            if this_action.has_key('set_owner'):
+            if 'set_owner' in this_action:
                 owners = [x.strip() for x in
                           this_action['set_owner'].split(',')]
             elif self.config.getbool('ticket', 'restrict_owner'):
@@ -255,41 +253,42 @@
             else:
                 owners = None
 
-            if owners == None:
+            if owners is None:
                 owner = req.args.get(id, req.authname)
-                control.append(tag_('to %(owner)s',
+                control.append(tag_("to %(owner)s",
                                     owner=tag.input(type='text', id=id,
                                                     name=id, value=owner)))
                 hints.append(_("The owner will be changed from "
-                               "%(current_owner)s",
-                               current_owner=current_owner))
+                               "%(current_owner)s to the specified user",
+                               current_owner=formatted_current_owner))
             elif len(owners) == 1:
                 owner = tag.input(type='hidden', id=id, name=id,
                                   value=owners[0])
-                formatted_owner = format_user(owners[0])
-                control.append(tag_('to %(owner)s ',
-                                    owner=tag(formatted_owner, owner)))
+                formatted_new_owner = format_author(owners[0])
+                control.append(tag_("to %(owner)s",
+                                    owner=tag(formatted_new_owner, owner)))
                 if ticket['owner'] != owners[0]:
                     hints.append(_("The owner will be changed from "
                                    "%(current_owner)s to %(selected_owner)s",
-                                   current_owner=current_owner,
-                                   selected_owner=formatted_owner))
+                                   current_owner=formatted_current_owner,
+                                   selected_owner=formatted_new_owner))
             else:
-                control.append(tag_('to %(owner)s', owner=tag.select(
+                control.append(tag_("to %(owner)s", owner=tag.select(
                     [tag.option(x, value=x,
                                 selected=(x == selected_owner or None))
                      for x in owners],
                     id=id, name=id)))
                 hints.append(_("The owner will be changed from "
                                "%(current_owner)s to the selected user",
-                               current_owner=current_owner))
+                               current_owner=formatted_current_owner))
         elif 'set_owner_to_self' in operations and \
                 ticket._old.get('owner', ticket['owner']) != req.authname:
             hints.append(_("The owner will be changed from %(current_owner)s "
-                           "to %(authname)s", current_owner=current_owner,
-                           authname=req.authname))
+                           "to %(authname)s",
+                           current_owner=formatted_current_owner,
+                           authname=format_author(req.authname)))
         if 'set_resolution' in operations:
-            if this_action.has_key('set_resolution'):
+            if 'set_resolution' in this_action:
                 resolutions = [x.strip() for x in
                                this_action['set_resolution'].split(',')]
             else:
@@ -302,7 +301,7 @@
             if len(resolutions) == 1:
                 resolution = tag.input(type='hidden', id=id, name=id,
                                        value=resolutions[0])
-                control.append(tag_('as %(resolution)s',
+                control.append(tag_("as %(resolution)s",
                                     resolution=tag(resolutions[0],
                                                    resolution)))
                 hints.append(_("The resolution will be set to %(name)s",
@@ -310,7 +309,7 @@
             else:
                 selected_option = req.args.get(id,
                         TicketSystem(self.env).default_resolution)
-                control.append(tag_('as %(resolution)s',
+                control.append(tag_("as %(resolution)s",
                                     resolution=tag.select(
                     [tag.option(x, value=x,
                                 selected=(x == selected_option or None))
@@ -320,13 +319,20 @@
         if 'del_resolution' in operations:
             hints.append(_("The resolution will be deleted"))
         if 'leave_status' in operations:
-            control.append(_('as %(status)s ',
+            control.append(_("as %(status)s",
                              status= ticket._old.get('status',
                                                      ticket['status'])))
+            if len(operations) == 1:
+                hints.append(_("The owner will remain %(current_owner)s",
+                               current_owner=formatted_current_owner)
+                             if current_owner else
+                             _("The ticket will remain with no owner"))
         else:
             if status != '*':
                 hints.append(_("Next status will be '%(name)s'", name=status))
-        return (this_action['name'], tag(*control), '. '.join(hints) + ".")
+        return (this_action['name'],
+                tag((' ' if i else None, c) for i, c in enumerate(control)),
+                '. '.join(hints) + '.' if hints else '')
 
     def get_ticket_changes(self, req, ticket, action):
         this_action = self.actions[action]
diff --git a/trac/trac/ticket/model.py b/trac/trac/ticket/model.py
index 29d43b3..77a6b33 100644
--- a/trac/trac/ticket/model.py
+++ b/trac/trac/ticket/model.py
@@ -524,6 +524,12 @@
 
         self._fetch_ticket(self.id)
 
+        changes = dict((field, (oldvalue, newvalue))
+                       for field, oldvalue, newvalue in fields)
+        for listener in TicketSystem(self.env).change_listeners:
+            if hasattr(listener, 'ticket_change_deleted'):
+                listener.ticket_change_deleted(self, cdate, changes)
+
     def modify_comment(self, cdate, author, comment, when=None):
         """Modify a ticket comment specified by its date, while keeping a
         history of edits.
@@ -548,8 +554,8 @@
             # Find the next edit number
             fields = db("""SELECT field FROM ticket_change
                            WHERE ticket=%%s AND time=%%s AND field %s
-                           """ % db.like(),
-                           (self.id, ts, db.like_escape('_comment') + '%'))
+                           """ % db.prefix_match(),
+                           (self.id, ts, db.prefix_match_value('_comment')))
             rev = max(int(field[8:]) for field, in fields) + 1 if fields else 0
             db("""INSERT INTO ticket_change
                     (ticket,time,author,field,oldvalue,newvalue)
@@ -563,8 +569,8 @@
                 for old_author, in db("""
                         SELECT author FROM ticket_change
                         WHERE ticket=%%s AND time=%%s AND NOT field %s LIMIT 1
-                        """ % db.like(),
-                        (self.id, ts, db.like_escape('_') + '%')):
+                        """ % db.prefix_match(),
+                        (self.id, ts, db.prefix_match_value('_'))):
                     db("""INSERT INTO ticket_change
                             (ticket,time,author,field,oldvalue,newvalue)
                           VALUES (%s,%s,%s,'comment','',%s)
@@ -580,6 +586,12 @@
 
         self.values['changetime'] = when
 
+        old_comment = old_comment or ''
+        for listener in TicketSystem(self.env).change_listeners:
+            if hasattr(listener, 'ticket_comment_modified'):
+                listener.ticket_comment_modified(self, cdate, author, comment,
+                                                 old_comment)
+
     def get_comment_history(self, cnum=None, cdate=None, db=None):
         """Retrieve the edit history of a comment identified by its number or
         date.
@@ -607,8 +619,8 @@
                 for author0, last_comment in db("""
                         SELECT author, newvalue FROM ticket_change
                         WHERE ticket=%%s AND time=%%s AND NOT field %s LIMIT 1
-                        """ % db.like(),
-                        (self.id, ts0, db.like_escape('_') + '%')):
+                        """ % db.prefix_match(),
+                        (self.id, ts0, db.prefix_match_value('_'))):
                     break
                 else:
                     return
@@ -617,8 +629,8 @@
             rows = db("""SELECT field, author, oldvalue, newvalue
                          FROM ticket_change
                          WHERE ticket=%%s AND time=%%s AND field %s
-                         """ % db.like(),
-                         (self.id, ts0, db.like_escape('_comment') + '%'))
+                         """ % db.prefix_match(),
+                         (self.id, ts0, db.prefix_match_value('_comment')))
             rows = sorted((int(field[8:]), author, old, new)
                           for field, author, old, new in rows)
             history = []
@@ -670,8 +682,8 @@
                 for author, in db("""
                         SELECT author FROM ticket_change
                         WHERE ticket=%%s AND time=%%s AND NOT field %s LIMIT 1
-                        """ % db.like(),
-                        (self.id, ts, db.like_escape('_') + '%')):
+                        """ % db.prefix_match(),
+                        (self.id, ts, db.prefix_match_value('_'))):
                     break
             return (ts, author, comment)
 
@@ -1040,23 +1052,20 @@
     def delete(self, retarget_to=None, author=None, db=None):
         """Delete the milestone.
 
+        :since 1.0.2: the `retarget_to` and `author` parameters are
+                      deprecated and will be removed in Trac 1.3.1. Tickets
+                      should be moved to another milestone by calling
+                      `move_tickets` before `delete`.
+
         :since 1.0: the `db` parameter is no longer needed and will be removed
         in version 1.1.1
         """
         with self.env.db_transaction as db:
             self.env.log.info("Deleting milestone %s", self.name)
             db("DELETE FROM milestone WHERE name=%s", (self.name,))
-
-            # Retarget/reset tickets associated with this milestone
-            now = datetime.now(utc)
-            tkt_ids = [int(row[0]) for row in
-                       db("SELECT id FROM ticket WHERE milestone=%s",
-                          (self.name,))]
-            for tkt_id in tkt_ids:
-                ticket = Ticket(self.env, tkt_id, db)
-                ticket['milestone'] = retarget_to
-                comment = "Milestone %s deleted" % self.name # don't translate
-                ticket.save_changes(author, comment, now)
+            Attachment.delete_all(self.env, 'milestone', self.name)
+            # Don't translate ticket comment (comment:40:ticket:5658)
+            self.move_tickets(retarget_to, author, "Milestone deleted")
             self._old['name'] = None
             del self.cache.milestones
             TicketSystem(self.env).reset_ticket_fields()
@@ -1088,7 +1097,7 @@
             listener.milestone_created(self)
         ResourceSystem(self.env).resource_created(self)
 
-    def update(self, db=None):
+    def update(self, db=None, author=None):
         """Update the milestone.
 
         :since 1.0: the `db` parameter is no longer needed and will be removed
@@ -1100,34 +1109,66 @@
 
         old = self._old.copy()
         with self.env.db_transaction as db:
-            old_name = old['name']
-            self.env.log.info("Updating milestone '%s'", self.name)
+            if self.name != old['name']:
+                # Update milestone field in tickets
+                self.move_tickets(self.name, author, "Milestone renamed")
+                TicketSystem(self.env).reset_ticket_fields()
+                # Reparent attachments
+                Attachment.reparent_all(self.env, 'milestone', old['name'],
+                                        'milestone', self.name)
+
+            self.env.log.info("Updating milestone '%s'", old['name'])
             db("""UPDATE milestone
                   SET name=%s, due=%s, completed=%s, description=%s
                   WHERE name=%s
                   """, (self.name, to_utimestamp(self.due),
                         to_utimestamp(self.completed),
-                        self.description, old_name))
+                        self.description, old['name']))
             self.checkin()
 
-            if self.name != old_name:
-                # Update milestone field in tickets
-                self.env.log.info("Updating milestone field of all tickets "
-                                  "associated with milestone '%s'", self.name)
-                db("UPDATE ticket SET milestone=%s WHERE milestone=%s",
-                   (self.name, old_name))
-                TicketSystem(self.env).reset_ticket_fields()
-
-                # Reparent attachments
-                Attachment.reparent_all(self.env, 'milestone', old_name,
-                                        'milestone', self.name)
-
         old_values = dict((k, v) for k, v in old.iteritems()
                           if getattr(self, k) != v)
         for listener in TicketSystem(self.env).milestone_change_listeners:
             listener.milestone_changed(self, old_values)
         ResourceSystem(self.env).resource_changed(self, old_values)
 
+    def move_tickets(self, new_milestone, author, comment=None,
+                     exclude_closed=False):
+        """Move tickets associated with this milestone to another
+        milestone.
+
+        :param new_milestone: milestone to which the tickets are moved
+        :param author: author of the change
+        :param comment: comment that is inserted into moved tickets. The
+                        string should not be translated.
+        :param exclude_closed: whether tickets with status closed should be
+                               excluded
+
+        :return: a list of ids of tickets that were moved
+        """
+        # Check if milestone exists, but if the milestone is being renamed
+        # the new milestone won't exist in the cache yet so skip the test
+        if new_milestone and new_milestone != self.name:
+            if not self.cache.fetchone(new_milestone):
+                raise ResourceNotFound(
+                    _("Milestone %(name)s does not exist.",
+                      name=new_milestone), _("Invalid milestone name"))
+        now = datetime.now(utc)
+        with self.env.db_transaction as db:
+            sql = "SELECT id FROM ticket WHERE milestone=%s"
+            if exclude_closed:
+                sql += " AND status != 'closed'"
+            tkt_ids = [int(row[0]) for row in db(sql, (self._old['name'],))]
+            if tkt_ids:
+                self.env.log.info("Moving tickets associated with milestone "
+                                  "'%s' to milestone '%s'", self._old['name'],
+                                  new_milestone)
+                for tkt_id in tkt_ids:
+                    ticket = Ticket(self.env, tkt_id)
+                    ticket['milestone'] = new_milestone
+                    ticket.save_changes(author, comment, now)
+        return tkt_ids
+
     @classmethod
     def select(cls, env, include_completed=True, db=None):
         """
diff --git a/trac/trac/ticket/notification.py b/trac/trac/ticket/notification.py
index 2de8ec9..62fdc94 100644
--- a/trac/trac/ticket/notification.py
+++ b/trac/trac/ticket/notification.py
@@ -26,8 +26,10 @@
 from trac.config import *
 from trac.notification import NotifyEmail
 from trac.ticket.api import TicketSystem
+from trac.ticket.model import Ticket
 from trac.util.datefmt import to_utimestamp
-from trac.util.text import obfuscate_email_address, text_width, wrap
+from trac.util.text import obfuscate_email_address, shorten_line, \
+                           text_width, wrap
 from trac.util.translation import deactivate, reactivate
 
 
@@ -57,7 +59,7 @@
         ''(since 0.11)''""")
 
     batch_subject_template = Option('notification', 'batch_subject_template',
-                                     '$prefix Batch modify: $tickets_descr',
+                                    '$prefix Batch modify: $tickets_descr',
         """Like ticket_subject_template but for batch modifications.
 
         By default, the template is `$prefix Batch modify: $tickets_descr`.
@@ -73,60 +75,70 @@
         US-ASCII characters.  This is expected by CJK users. ''(since
         0.12.2)''""")
 
-def get_ticket_notification_recipients(env, config, tktid, prev_cc):
-    notify_reporter = config.getbool('notification', 'always_notify_reporter')
-    notify_owner = config.getbool('notification', 'always_notify_owner')
-    notify_updater = config.getbool('notification', 'always_notify_updater')
 
-    ccrecipients = prev_cc
-    torecipients = []
-    with env.db_query as db:
-        # Harvest email addresses from the cc, reporter, and owner fields
-        for row in db("SELECT cc, reporter, owner FROM ticket WHERE id=%s",
-                      (tktid,)):
-            if row[0]:
-                ccrecipients += row[0].replace(',', ' ').split()
-            reporter = row[1]
-            owner = row[2]
-            if notify_reporter:
-                torecipients.append(row[1])
-            if notify_owner:
-                torecipients.append(row[2])
-            break
+def get_ticket_notification_recipients(env, config, tktid, prev_cc=None,
+                                       modtime=None):
+    """Returns notifications recipients.
 
-        # Harvest email addresses from the author field of ticket_change(s)
-        if notify_updater:
-            for author, ticket in db("""
-                    SELECT DISTINCT author, ticket FROM ticket_change
-                    WHERE ticket=%s
-                    """, (tktid,)):
-                torecipients.append(author)
+    :since 1.0.2: the `config` parameter is no longer used.
+    :since 1.0.2: the `prev_cc` parameter is deprecated.
+    """
+    section = env.config['notification']
+    always_notify_reporter = section.getbool('always_notify_reporter')
+    always_notify_owner = section.getbool('always_notify_owner')
+    always_notify_updater = section.getbool('always_notify_updater')
 
-        # Suppress the updater from the recipients
-        updater = None
-        for updater, in db("""
-                SELECT author FROM ticket_change WHERE ticket=%s
-                ORDER BY time DESC LIMIT 1
-                """, (tktid,)):
-            break
-        else:
-            for updater, in db("SELECT reporter FROM ticket WHERE id=%s",
-                               (tktid,)):
-                break
+    cc_recipients = set(prev_cc or [])
+    to_recipients = set()
+    tkt = Ticket(env, tktid)
 
-        if not notify_updater:
-            filter_out = True
-            if notify_reporter and (updater == reporter):
-                filter_out = False
-            if notify_owner and (updater == owner):
-                filter_out = False
-            if filter_out:
-                torecipients = [r for r in torecipients
-                                if r and r != updater]
-        elif updater:
-            torecipients.append(updater)
+    # CC field is stored as comma-separated string. Parse to list.
+    to_list = lambda cc: cc.replace(',', ' ').split()
 
-    return (torecipients, ccrecipients, reporter, owner)
+    # Backward compatibility
+    if not modtime:
+        modtime = tkt['changetime']
+
+    # Harvest email addresses from the cc, reporter, and owner fields
+    if tkt['cc']:
+        cc_recipients.update(to_list(tkt['cc']))
+    if always_notify_reporter:
+        to_recipients.add(tkt['reporter'])
+    if always_notify_owner:
+        to_recipients.add(tkt['owner'])
+
+    # Harvest email addresses from the author field of ticket_change(s)
+    if always_notify_updater:
+        for author, ticket in env.db_query("""
+                SELECT DISTINCT author, ticket FROM ticket_change
+                WHERE ticket=%s
+                """, (tktid, )):
+            to_recipients.add(author)
+
+    # Harvest previous owner and cc list
+    author = None
+    for changelog in tkt.get_changelog(modtime):
+        author, field, old = changelog[1:4]
+        if field == 'owner' and always_notify_owner:
+            to_recipients.add(old)
+        elif field == 'cc':
+            cc_recipients.update(to_list(old))
+
+    # Suppress the updater from the recipients if necessary
+    updater = author or tkt['reporter']
+    if not always_notify_updater:
+        filter_out = True
+        if always_notify_reporter and updater == tkt['reporter']:
+            filter_out = False
+        if always_notify_owner and updater == tkt['owner']:
+            filter_out = False
+        if filter_out:
+            to_recipients.discard(updater)
+    elif updater:
+        to_recipients.add(updater)
+
+    return list(to_recipients), list(cc_recipients), \
+           tkt['reporter'], tkt['owner']
 
 
 class TicketNotifyEmail(NotifyEmail):
@@ -141,7 +153,6 @@
 
     def __init__(self, env):
         NotifyEmail.__init__(self, env)
-        self.prev_cc = []
         ambiguous_char_width = env.config.get('notification',
                                               'ambiguous_char_width',
                                               'single')
@@ -219,7 +230,6 @@
                                           self.ambiwidth) + '\n'
                         if chgcc:
                             changes_body += chgcc
-                        self.prev_cc += self.parse_cc(old) if old else []
                     else:
                         if field in ['owner', 'reporter']:
                             old = self.obfuscate_email(old)
@@ -306,13 +316,13 @@
                 width_l = self.COLS - width_r - 1
         sep = width_l * '-' + '+' + width_r * '-'
         txt = sep + '\n'
-        cell_tmp = [u'', u'']
+        vals_lr = ([], [])
         big = []
         i = 0
         width_lr = [width_l, width_r]
         for f in [f for f in fields if f['name'] != 'description']:
             fname = f['name']
-            if not tkt.values.has_key(fname):
+            if fname not in tkt.values:
                 continue
             fval = tkt[fname] or ''
             if fname in ['owner', 'reporter']:
@@ -324,15 +334,36 @@
                 # __str__ method won't be called.
                 str_tmp = u'%s:  %s' % (f['label'], unicode(fval))
                 idx = i % 2
-                cell_tmp[idx] += wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
-                                      (width[2 * idx]
-                                       - self.get_text_width(f['label'])
-                                       + 2 * idx) * ' ',
-                                      2 * ' ', '\n', self.ambiwidth)
-                cell_tmp[idx] += '\n'
+                initial_indent = ' ' * (width[2 * idx] -
+                                        self.get_text_width(f['label']) +
+                                        2 * idx)
+                wrapped = wrap(str_tmp, width_lr[idx] - 2 + 2 * idx,
+                               initial_indent, '  ', '\n', self.ambiwidth)
+                vals_lr[idx].append(wrapped.splitlines())
                 i += 1
-        cell_l = cell_tmp[0].splitlines()
-        cell_r = cell_tmp[1].splitlines()
+        if len(vals_lr[0]) > len(vals_lr[1]):
+            vals_lr[1].append([])
+
+        cell_l = []
+        cell_r = []
+        for i in xrange(len(vals_lr[0])):
+            vals_l = vals_lr[0][i]
+            vals_r = vals_lr[1][i]
+            vals_diff = len(vals_l) - len(vals_r)
+            diff = len(cell_l) - len(cell_r)
+            if diff > 0:
+                # add padding to right side if needed
+                if vals_diff < 0:
+                    diff += vals_diff
+                cell_r.extend([''] * max(diff, 0))
+            elif diff < 0:
+                # add padding to left side if needed
+                if vals_diff > 0:
+                    diff += vals_diff
+                cell_l.extend([''] * max(-diff, 0))
+            cell_l.extend(vals_l)
+            cell_r.extend(vals_r)
+
         for i in range(max(len(cell_l), len(cell_r))):
             if i >= len(cell_l):
                 cell_l.append(width_l * ' ')
@@ -383,9 +414,9 @@
         return template.generate(**data).render('text', encoding=None).strip()
 
     def get_recipients(self, tktid):
-        (torecipients, ccrecipients, reporter, owner) = \
-            get_ticket_notification_recipients(self.env, self.config,
-                tktid, self.prev_cc)
+        torecipients, ccrecipients, reporter, owner = \
+            get_ticket_notification_recipients(self.env, self.config, tktid,
+                                               modtime=self.modtime)
         self.reporter = reporter
         self.owner = owner
         return (torecipients, ccrecipients)
@@ -425,6 +456,7 @@
         else:
             return obfuscate_email_address(text)
 
+
 class BatchTicketNotifyEmail(NotifyEmail):
     """Notification of ticket batch modifications."""
 
@@ -443,7 +475,6 @@
 
     def _notify(self, tickets, new_values, comment, action, author):
         self.tickets = tickets
-        changes_body = ''
         self.reporter = ''
         self.owner = ''
         changes_descr = '\n'.join(['%s to %s' % (prop, val)
@@ -475,16 +506,15 @@
             'tickets_descr': tickets_descr,
             'env': self.env,
         }
-
-        return template.generate(**data).render('text', encoding=None).strip()
+        subj = template.generate(**data).render('text', encoding=None).strip()
+        return shorten_line(subj)
 
     def get_recipients(self, tktids):
-        alltorecipients = []
-        allccrecipients = []
+        alltorecipients = set()
+        allccrecipients = set()
         for t in tktids:
-            (torecipients, ccrecipients, reporter, owner) = \
-                get_ticket_notification_recipients(self.env, self.config,
-                    t, [])
-            alltorecipients.extend(torecipients)
-            allccrecipients.extend(ccrecipients)
-        return (list(set(alltorecipients)), list(set(allccrecipients)))
+            torecipients, ccrecipients, reporter, owner = \
+                get_ticket_notification_recipients(self.env, self.config, t)
+            alltorecipients.update(torecipients)
+            allccrecipients.update(ccrecipients)
+        return list(alltorecipients), list(allccrecipients)
diff --git a/trac/trac/ticket/query.py b/trac/trac/ticket/query.py
index 3f12978..a267467 100644
--- a/trac/trac/ticket/query.py
+++ b/trac/trac/ticket/query.py
@@ -32,14 +32,15 @@
 from trac.mimeview.api import IContentConverter, Mimeview
 from trac.resource import Resource
 from trac.ticket.api import TicketSystem
-from trac.ticket.model import Milestone, group_milestones, Ticket
+from trac.ticket.model import Milestone, group_milestones
 from trac.util import Ranges, as_bool
+from trac.util.compat import any
 from trac.util.datefmt import format_date, format_datetime, from_utimestamp, \
                               parse_date, to_timestamp, to_utimestamp, utc, \
                               user_time
 from trac.util.presentation import Paginator
 from trac.util.text import empty, shorten_line, quote_query_string
-from trac.util.translation import _, tag_, cleandoc_
+from trac.util.translation import _, tag_, cleandoc_, ngettext
 from trac.web import arg_list_to_args, parse_arg_list, IRequestHandler
 from trac.web.href import Href
 from trac.web.chrome import (INavigationContributor, Chrome,
@@ -434,11 +435,12 @@
 
         enum_columns = ('resolution', 'priority', 'severity')
         # Build the list of actual columns to query
-        cols = self.cols[:]
+        cols = []
         def add_cols(*args):
             for col in args:
                 if not col in cols:
                     cols.append(col)
+        add_cols(*self.cols)  # remove duplicated cols
         if self.group and not self.group in cols:
             add_cols(self.group)
         if self.rows:
@@ -456,14 +458,20 @@
                                          if c not in custom_fields]))
         sql.append(",priority.value AS priority_value")
         for k in [db.quote(k) for k in cols if k in custom_fields]:
-            sql.append(",%s.value AS %s" % (k, k))
-        sql.append("\nFROM ticket AS t")
+            sql.append(",t.%s AS %s" % (k, k))
 
-        # Join with ticket_custom table as necessary
-        for k in [k for k in cols if k in custom_fields]:
-            qk = db.quote(k)
-            sql.append("\n  LEFT OUTER JOIN ticket_custom AS %s ON " \
-                       "(id=%s.ticket AND %s.name='%s')" % (qk, qk, qk, k))
+        # Use subquery of ticket_custom table as necessary
+        if any(k in custom_fields for k in cols):
+            sql.append('\nFROM (\n  SELECT ' +
+                       ','.join('t.%s AS %s' % (c, c)
+                                for c in cols if c not in custom_fields))
+            sql.extend(",\n  (SELECT c.value FROM ticket_custom c "
+                       "WHERE c.ticket=t.id AND c.name='%s') AS %s"
+                       % (k, db.quote(k))
+                       for k in cols if k in custom_fields)
+            sql.append("\n  FROM ticket AS t) AS t")
+        else:
+            sql.append("\nFROM ticket AS t")
 
         # Join with the enum table for proper sorting
         for col in [c for c in enum_columns
@@ -490,7 +498,7 @@
             if name not in custom_fields:
                 col = 't.' + name
             else:
-                col = '%s.value' % db.quote(name)
+                col = 't.' + db.quote(name)
             value = value[len(mode) + neg:]
 
             if name in self.time_fields:
@@ -579,11 +587,11 @@
                         if a == b:
                             ids.append(str(a))
                         else:
-                            id_clauses.append('id BETWEEN %s AND %s')
+                            id_clauses.append('t.id BETWEEN %s AND %s')
                             args.append(a)
                             args.append(b)
                     if ids:
-                        id_clauses.append('id IN (%s)' % (','.join(ids)))
+                        id_clauses.append('t.id IN (%s)' % (','.join(ids)))
                     if id_clauses:
                         clauses.append('%s(%s)' % ('NOT 'if neg else '',
                                                    ' OR '.join(id_clauses)))
@@ -592,7 +600,7 @@
                     if k not in custom_fields:
                         col = 't.' + k
                     else:
-                        col = '%s.value' % db.quote(k)
+                        col = 't.' + db.quote(k)
                     clauses.append("COALESCE(%s,'') %sIN (%s)"
                                    % (col, 'NOT ' if neg else '',
                                       ','.join(['%s' for val in v])))
@@ -633,7 +641,7 @@
             if name in enum_columns:
                 col = name + '.value'
             elif name in custom_fields:
-                col = '%s.value' % db.quote(name)
+                col = 't.' + db.quote(name)
             else:
                 col = 't.' + name
             desc = ' DESC' if desc else ''
@@ -716,11 +724,17 @@
         cols = self.get_columns()
         labels = TicketSystem(self.env).get_ticket_field_labels()
         wikify = set(f['name'] for f in self.fields
-                     if f['type'] == 'text' and f.get('format') == 'wiki')
+                     if f['type'] == 'text' and
+                        f.get('format') == 'wiki')
+        wikifyblock = set(f['name'] for f in self.fields
+                          if f['type'] == 'textarea' and
+                             f.get('format') == 'wiki')
+        wikifyblock.add('description')
 
         headers = [{
             'name': col, 'label': labels.get(col, _('Ticket')),
             'wikify': col in wikify,
+            'wikifyblock': col in wikifyblock,
             'href': self.get_href(context.href, order=col,
                                   desc=(col == self.order and not self.desc))
         } for col in cols]
@@ -872,7 +886,8 @@
     def get_navigation_items(self, req):
         from trac.ticket.report import ReportModule
         if 'TICKET_VIEW' in req.perm and \
-                not self.env.is_component_enabled(ReportModule):
+                not (self.env.is_component_enabled(ReportModule) and
+                     'REPORT_VIEW' in req.perm):
             yield ('mainnav', 'tickets',
                    tag.a(_('View Tickets'), href=req.href.query()))
 
@@ -883,6 +898,9 @@
 
     def process_request(self, req):
         req.perm.assert_permission('TICKET_VIEW')
+        report_id = req.args.get('report')
+        if report_id:
+            req.perm('report', report_id).assert_permission('REPORT_VIEW')
 
         constraints = self._get_constraints(req)
         args = req.args
@@ -937,7 +955,7 @@
         max = args.get('max')
         if max is None and format in ('csv', 'tab'):
             max = 0 # unlimited unless specified explicitly
-        query = Query(self.env, req.args.get('report'),
+        query = Query(self.env, report_id,
                       constraints, cols, args.get('order'),
                       'desc' in args, args.get('group'),
                       'groupdesc' in args, 'verbose' in args,
@@ -1151,7 +1169,7 @@
                 values = []
                 for col in cols:
                     value = result[col]
-                    if col in ('cc', 'reporter'):
+                    if col in ('cc', 'owner', 'reporter'):
                         value = Chrome(self.env).format_emails(
                                     context.child(ticket), value)
                     elif col in query.time_fields:
@@ -1215,7 +1233,7 @@
     can be included in field values by escaping them with a backslash (`\`).
 
     Groups of field constraints to be OR-ed together can be separated by a
-    litteral `or` argument.
+    literal `or` argument.
 
     In addition to filters, several other named parameters can be used
     to control how the results are presented. All of them are optional.
@@ -1253,6 +1271,9 @@
     The `rows` parameter can be used to specify which field(s) should
     be viewed as a row, e.g. `rows=description|summary`
 
+    The `col` parameter can be used to specify which fields should
+    be viewed as columns. For '''table''' format only.
+
     For compatibility with Trac 0.10, if there's a last positional parameter
     given to the macro, it will be used to specify the `format`.
     Also, using "&" as a field separator still works (except for `order`)
@@ -1313,8 +1334,10 @@
 
         if format == 'count':
             cnt = query.count(req)
-            return tag.span(cnt, title='%d tickets for which %s' %
-                            (cnt, query_string), class_='query_count')
+            title = ngettext("%(num)d ticket for which %(query)s",
+                             "%(num)d tickets for which %(query)s",
+                             cnt, query=query_string)
+            return tag.span(cnt, title=title, class_='query_count')
 
         tickets = query.execute(req)
 
@@ -1336,14 +1359,21 @@
             add_stylesheet(req, 'common/css/roadmap.css')
 
             def query_href(extra_args, group_value = None):
-                q = Query.from_string(self.env, query_string)
+                q = query_string + ''.join('&%s=%s' % (kw, v)
+                                           for kw in extra_args
+                                           if kw not in ['group', 'status']
+                                           for v in extra_args[kw])
+                q = Query.from_string(self.env, q)
+                args = {}
                 if q.group:
-                    extra_args[q.group] = group_value
-                    q.group = None
+                    args[q.group] = group_value
+                q.group = extra_args.get('group')
+                if 'status' in extra_args:
+                    args['status'] = extra_args['status']
                 for constraint in q.constraints:
-                    constraint.update(extra_args)
+                    constraint.update(args)
                 if not q.constraints:
-                    q.constraints.append(extra_args)
+                    q.constraints.append(args)
                 return q.get_href(formatter.context)
             chrome = Chrome(self.env)
             tickets = apply_ticket_permissions(self.env, req, tickets)
diff --git a/trac/trac/ticket/report.py b/trac/trac/ticket/report.py
index ed9bfdd..e5c27d0 100644
--- a/trac/trac/ticket/report.py
+++ b/trac/trac/ticket/report.py
@@ -43,7 +43,6 @@
 from trac.wiki import IWikiSyntaxProvider, WikiParser
 
 
-
 SORT_COLUMN = '@SORT_COLUMN@'
 LIMIT_OFFSET = '@LIMIT_OFFSET@'
 
@@ -63,9 +62,11 @@
     | \([^()]+\)                   # parenthesis group
 ''', re.MULTILINE | re.VERBOSE)
 
+
 def _expand_with_space(m):
     return ' ' * len(m.group(0))
 
+
 def sql_skeleton(sql):
     """Strip an SQL query to leave only its toplevel structure.
 
@@ -89,6 +90,7 @@
 
 _order_by_re = re.compile(r'ORDER\s+BY', re.MULTILINE)
 
+
 def split_sql(sql, clause_re, skel=None):
     """Split an SQL query according to a toplevel clause regexp.
 
@@ -107,7 +109,6 @@
         return sql, '' # no single clause separator
 
 
-
 class ReportModule(Component):
 
     implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
@@ -148,13 +149,15 @@
             return True
 
     def process_request(self, req):
-        req.perm.require('REPORT_VIEW')
-
         # did the user ask for any special report?
         id = int(req.args.get('id', -1))
-        action = req.args.get('action', 'view')
+        if id != -1:
+            req.perm('report', id).require('REPORT_VIEW')
+        else:
+            req.perm.require('REPORT_VIEW')
 
         data = {}
+        action = req.args.get('action', 'view')
         if req.method == 'POST':
             if action == 'new':
                 self._do_create(req)
@@ -164,7 +167,7 @@
                 self._do_save(req, id)
         elif action in ('copy', 'edit', 'new'):
             template = 'report_edit.html'
-            data = self._render_editor(req, id, action=='copy')
+            data = self._render_editor(req, id, action == 'copy')
             Chrome(self.env).add_wiki_toolbars(req)
         elif action == 'delete':
             template = 'report_delete.html'
@@ -183,17 +186,19 @@
             if content_type: # i.e. alternate format
                 return template, data, content_type
 
+        from trac.ticket.query import QueryModule
+        show_query_link = 'TICKET_VIEW' in req.perm and \
+                          self.env.is_component_enabled(QueryModule)
+
         if id != -1 or action == 'new':
             add_ctxtnav(req, _('Available Reports'), href=req.href.report())
             add_link(req, 'up', req.href.report(), _('Available Reports'))
-        else:
+        elif show_query_link:
             add_ctxtnav(req, _('Available Reports'))
 
         # Kludge: only show link to custom query if the query module
         # is actually enabled
-        from trac.ticket.query import QueryModule
-        if 'TICKET_VIEW' in req.perm and \
-                self.env.is_component_enabled(QueryModule):
+        if show_query_link:
             add_ctxtnav(req, _('Custom Query'), href=req.href.query())
             data['query_href'] = req.href.query()
             data['saved_query_href'] = req.session.get('query_href')
@@ -224,7 +229,7 @@
         req.redirect(req.href.report(report_id))
 
     def _do_delete(self, req, id):
-        req.perm.require('REPORT_DELETE')
+        req.perm('report', id).require('REPORT_DELETE')
 
         if 'cancel' in req.args:
             req.redirect(req.href.report(id))
@@ -235,7 +240,7 @@
 
     def _do_save(self, req, id):
         """Save report changes to the database"""
-        req.perm.require('REPORT_MODIFY')
+        req.perm('report', id).require('REPORT_MODIFY')
 
         if 'cancel' not in req.args:
             title = req.args.get('title', '')
@@ -249,29 +254,18 @@
         req.redirect(req.href.report(id))
 
     def _render_confirm_delete(self, req, id):
-        req.perm.require('REPORT_DELETE')
+        req.perm('report', id).require('REPORT_DELETE')
 
-        for title, in self.env.db_query("""
-                SELECT title FROM report WHERE id=%s
-                """, (id,)):
-            return {'title': _("Delete Report {%(num)s} %(title)s", num=id,
-                               title=title),
-                    'action': 'delete',
-                    'report': {'id': id, 'title': title}}
-        else:
-            raise TracError(_("Report {%(num)s} does not exist.", num=id),
-                            _("Invalid Report Number"))
+        title = self.get_report(id)[0]
+        return {'title': _("Delete Report {%(num)s} %(title)s", num=id,
+                           title=title),
+                'action': 'delete',
+                'report': {'id': id, 'title': title}}
 
     def _render_editor(self, req, id, copy):
         if id != -1:
-            req.perm.require('REPORT_MODIFY')
-            for title, description, query in self.env.db_query(
-                    "SELECT title, description, query FROM report WHERE id=%s",
-                    (id,)):
-                break
-            else:
-                raise TracError(_("Report {%(num)s} does not exist.", num=id),
-                                _("Invalid Report Number"))
+            req.perm('report', id).require('REPORT_MODIFY')
+            title, description, query = self.get_report(id)
         else:
             req.perm.require('REPORT_CREATE')
             title = description = query = ''
@@ -306,6 +300,8 @@
                 SELECT id, title, description FROM report ORDER BY %s %s
                 """ % ('title' if sort == 'title' else 'id',
                        '' if asc else 'DESC'))
+        rows = [(id, title, description) for id, title, description in rows
+                if 'REPORT_VIEW' in req.perm('report', id)]
 
         if format == 'rss':
             data = {'rows': rows}
@@ -344,13 +340,7 @@
 
     def _render_view(self, req, id):
         """Retrieve the report results and pre-process them for rendering."""
-        for title, sql, description in self.env.db_query("""
-                SELECT title, query, description from report WHERE id=%s
-                """, (id,)):
-            break
-        else:
-            raise ResourceNotFound(_("Report {%(num)s} does not exist.",
-                                     num=id), _("Invalid Report Number"))
+        title, description, sql = self.get_report(id)
         try:
             args = self.get_var_args(req)
         except ValueError, e:
@@ -393,7 +383,7 @@
         title = '{%i} %s' % (id, title)
 
         report_resource = Resource('report', id)
-        req.perm.require('REPORT_VIEW', report_resource)
+        req.perm(report_resource).require('REPORT_VIEW')
         context = web_context(req, report_resource)
 
         page = int(req.args.get('page', '1'))
@@ -436,13 +426,13 @@
                                                 offset)
 
         if len(res) == 2:
-             e, sql = res
-             data['message'] = \
-                 tag_("Report execution failed: %(error)s %(sql)s",
-                      error=tag.pre(exception_to_unicode(e)),
-                      sql=tag(tag.hr(),
-                              tag.pre(sql, style="white-space: pre")))
-             return 'report_view.html', data, None
+            e, sql = res
+            data['message'] = \
+                tag_("Report execution failed: %(error)s %(sql)s",
+                     error=tag.pre(exception_to_unicode(e)),
+                     sql=tag(tag.hr(),
+                             tag.pre(sql, style="white-space: pre")))
+            return 'report_view.html', data, None
 
         cols, results, num_items, missing_args, limit_offset = res
         need_paginator = limit > 0 and limit_offset
@@ -469,8 +459,8 @@
             fields = ['href', 'class', 'string', 'title']
             paginator.shown_pages = [dict(zip(fields, p)) for p in pagedata]
             paginator.current_page = {'href': None, 'class': 'current',
-                                    'string': str(paginator.page + 1),
-                                    'title': None}
+                                      'string': str(paginator.page + 1),
+                                      'title': None}
             numrows = paginator.num_items
 
         # Place retrieved columns in groups, according to naming conventions
@@ -609,7 +599,7 @@
         if format == 'rss':
             data['email_map'] = chrome.get_email_map()
             data['context'] = web_context(req, report_resource,
-                                                   absurls=True)
+                                          absurls=True)
             return 'report.rss', data, 'application/rss+xml'
         elif format == 'csv':
             filename = 'report_%s.csv' % id if id else 'report.csv'
@@ -629,7 +619,7 @@
                      _('Comma-delimited Text'), 'text/plain')
             add_link(req, 'alternate', report_href(format='tab', page=p),
                      _('Tab-delimited Text'), 'text/plain')
-            if 'REPORT_SQL_VIEW' in req.perm:
+            if 'REPORT_SQL_VIEW' in req.perm('report', id):
                 add_link(req, 'alternate',
                          req.href.report(id=id, format='sql'),
                          _('SQL Query'), 'text/plain')
@@ -692,6 +682,9 @@
             try:
                 cursor.execute(count_sql, args)
             except Exception, e:
+                self.log.warn('Exception caught while executing report: %r, '
+                              'args %r%s', count_sql, args,
+                              exception_to_unicode(e, traceback=True))
                 return e, count_sql
             num_items = cursor.fetchone()[0]
 
@@ -701,6 +694,9 @@
             try:
                 cursor.execute(colnames_sql, args)
             except Exception, e:
+                self.log.warn('Exception caught while executing report: %r, '
+                              'args %r%s', colnames_sql, args,
+                              exception_to_unicode(e, traceback=True))
                 return e, colnames_sql
             cols = get_column_names(cursor)
 
@@ -724,7 +720,7 @@
                 sql = sql.replace(SORT_COLUMN, sort_col or '1')
             elif sort_col:
                 # Method 2: automagically insert sort_col (and __group__
-                # before it, if __group__ was specified) as first criterions
+                # before it, if __group__ was specified) as first criteria
                 if '__group__' in cols:
                     order_by.append('__group__ ASC')
                 order_by.append(sort_col)
@@ -752,6 +748,9 @@
         try:
             cursor.execute(sql, args)
         except Exception, e:
+            self.log.warn('Exception caught while executing report: %r, args '
+                          '%r%s',
+                          sql, args, exception_to_unicode(e, traceback=True))
             if order_by or limit_offset:
                 add_notice(req, _("Hint: if the report failed due to automatic"
                                   " modification of the ORDER BY clause or the"
@@ -766,6 +765,20 @@
         cols = get_column_names(cursor)
         return cols, rows, num_items, missing_args, limit_offset
 
+    def get_report(self, id):
+        try:
+            number = int(id)
+        except (ValueError, TypeError):
+            pass
+        else:
+            for title, description, sql in self.env.db_query("""
+                    SELECT title, description, query from report WHERE id=%s
+                    """, (number,)):
+                return title, description, sql
+
+        raise ResourceNotFound(_("Report {%(num)s} does not exist.", num=id),
+                               _("Invalid Report Number"))
+
     def get_var_args(self, req):
         # reuse somehow for #9574 (wiki vars)
         report_args = {}
@@ -874,7 +887,7 @@
         raise RequestDone
 
     def _send_sql(self, req, id, title, description, sql):
-        req.perm.require('REPORT_SQL_VIEW')
+        req.perm('report', id).require('REPORT_SQL_VIEW')
 
         out = StringIO()
         out.write('-- ## %s: %s ## --\n\n' % (id, title.encode('utf-8')))
@@ -901,8 +914,8 @@
         yield ('report', self._format_link)
 
     def get_wiki_syntax(self):
-        yield (r"!?\{(?P<it_report>%s\s*)[0-9]+\}" % \
-                                                WikiParser.INTERTRAC_SCHEME,
+        yield (r"!?\{(?P<it_report>%s\s*)[0-9]+\}" %
+                   WikiParser.INTERTRAC_SCHEME,
                lambda x, y, z: self._format_link(x, 'report', y[1:-1], y, z))
 
     def _format_link(self, formatter, ns, target, label, fullmatch=None):
@@ -910,6 +923,16 @@
                                                          fullmatch)
         if intertrac:
             return intertrac
-        report, args, fragment = formatter.split_link(target)
-        return tag.a(label, href=formatter.href.report(report) + args,
-                     class_='report')
+        id, args, fragment = formatter.split_link(target)
+        try:
+            self.get_report(id)
+        except ResourceNotFound:
+            return tag.a(label, class_='missing report',
+                         title=_("report does not exist"))
+        else:
+            if 'REPORT_VIEW' in formatter.req.perm('report', id):
+                return tag.a(label, href=formatter.href.report(id) + args,
+                             class_='report')
+            else:
+                return tag.a(label, class_='forbidden report',
+                             title=_("no permission to view report"))
diff --git a/trac/trac/ticket/roadmap.py b/trac/trac/ticket/roadmap.py
index 44736d4..a55aab5 100644
--- a/trac/trac/ticket/roadmap.py
+++ b/trac/trac/ticket/roadmap.py
@@ -31,12 +31,13 @@
 from trac.resource import *
 from trac.search import ISearchSource, search_to_regexps, shorten_result
 from trac.util import as_bool
-from trac.util.datefmt import parse_date, utc, to_utimestamp, to_datetime, \
+from trac.util.datefmt import parse_date, utc, pretty_timedelta, to_datetime, \
                               get_datetime_format_hint, format_date, \
                               format_datetime, from_utimestamp, user_time
-from trac.util.text import CRLF
+from trac.util.text import CRLF, exception_to_unicode, to_unicode
 from trac.util.translation import _, tag_
 from trac.ticket.api import TicketSystem
+from trac.ticket.batch import BatchTicketNotifyEmail
 from trac.ticket.model import Milestone, MilestoneCache, Ticket, \
                               group_milestones
 from trac.timeline.api import ITimelineEventProvider
@@ -349,6 +350,13 @@
                 group_names = field['options']
                 if field.get('optional'):
                     group_names.insert(0, '')
+            elif field.get('custom'):
+                group_names = [name for name, in env.db_query("""
+                    SELECT DISTINCT COALESCE(c.value, '') FROM ticket_custom c
+                    WHERE c.name=%s ORDER BY COALESCE(c.value, '')
+                    """, (by, ))]
+                if '' not in group_names:
+                    group_names.insert(0, '')
             else:
                 group_names = [name for name, in env.db_query("""
                     SELECT DISTINCT COALESCE(%s, '') FROM ticket
@@ -415,7 +423,7 @@
         return req.path_info == '/roadmap'
 
     def process_request(self, req):
-        req.perm.require('MILESTONE_VIEW')
+        req.perm.require('ROADMAP_VIEW')
 
         show = req.args.getlist('show')
         if 'all' in show:
@@ -671,7 +679,7 @@
             action = 'edit' # rather than 'new' so that it works for POST/save
 
         if req.method == 'POST':
-            if req.args.has_key('cancel'):
+            if 'cancel' in req.args:
                 if milestone.exists:
                     req.redirect(req.href.milestone(milestone.name))
                 else:
@@ -695,12 +703,33 @@
     def _do_delete(self, req, milestone):
         req.perm(milestone.resource).require('MILESTONE_DELETE')
 
-        retarget_to = None
-        if req.args.has_key('retarget'):
-            retarget_to = req.args.get('target') or None
-        milestone.delete(retarget_to, req.authname)
+        retarget_to = req.args.get('target') or None
+        # Don't translate ticket comment (comment:40:ticket:5658)
+        retargeted_tickets = \
+            milestone.move_tickets(retarget_to, req.authname,
+                "Ticket retargeted after milestone deleted")
+        milestone.delete(author=req.authname)
         add_notice(req, _('The milestone "%(name)s" has been deleted.',
                           name=milestone.name))
+        if retargeted_tickets:
+            add_notice(req, _('The tickets associated with milestone '
+                              '"%(name)s" have been retargeted to milestone '
+                              '"%(retarget)s".', name=milestone.name,
+                              retarget=retarget_to))
+            new_values = {'milestone': retarget_to}
+            comment = _("Tickets retargeted after milestone deleted")
+            tn = BatchTicketNotifyEmail(self.env)
+            try:
+                tn.notify(retargeted_tickets, new_values, comment, None,
+                          req.authname)
+            except Exception, e:
+                self.log.error("Failure sending notification on ticket batch "
+                               "change: %s", exception_to_unicode(e))
+                add_warning(req, tag_("The changes have been saved, but an "
+                                      "error occurred while sending "
+                                      "notifications: %(message)s",
+                                      message=to_unicode(e)))
+
         req.redirect(req.href.roadmap())
 
     def _do_save(self, req, milestone):
@@ -722,7 +751,7 @@
             milestone.due = None
 
         completed = req.args.get('completeddate', '')
-        retarget_to = req.args.get('target')
+        retarget_to = req.args.get('target') or None
 
         # Instead of raising one single error, check all the constraints and
         # let the user fix them by going back to edit mode showing the warnings
@@ -764,15 +793,31 @@
 
         # -- actually save changes
         if milestone.exists:
-            milestone.update()
-            # eventually retarget opened tickets associated with the milestone
-            if 'retarget' in req.args and completed:
-                self.env.db_transaction("""
-                    UPDATE ticket SET milestone=%s
-                    WHERE milestone=%s and status != 'closed'
-                    """, (retarget_to, old_name))
-                self.log.info("Tickets associated with milestone %s "
-                              "retargeted to %s" % (old_name, retarget_to))
+            milestone.update(author=req.authname)
+            if completed and 'retarget' in req.args:
+                comment = req.args.get('comment', '')
+                retargeted_tickets = \
+                    milestone.move_tickets(retarget_to, req.authname,
+                                           comment, exclude_closed=True)
+                add_notice(req, _('The open tickets associated with '
+                                  'milestone "%(name)s" have been retargeted '
+                                  'to milestone "%(retarget)s".',
+                                  name=milestone.name, retarget=retarget_to))
+                new_values = {'milestone': retarget_to}
+                comment = comment or \
+                          _("Open tickets retargeted after milestone closed")
+                tn = BatchTicketNotifyEmail(self.env)
+                try:
+                    tn.notify(retargeted_tickets, new_values, comment, None,
+                              req.authname)
+                except Exception, e:
+                    self.log.error("Failure sending notification on ticket "
+                                   "batch change: %s",
+                                   exception_to_unicode(e))
+                    add_warning(req, tag_("The changes have been saved, but "
+                                          "an error occurred while sending "
+                                          "notifications: %(message)s",
+                                          message=to_unicode(e)))
         else:
             milestone.insert()
 
@@ -816,6 +861,9 @@
                 'TICKET_ADMIN' in req.perm)
         else:
             req.perm(milestone.resource).require('MILESTONE_CREATE')
+            if milestone.name:
+                add_notice(req, _("Milestone %(name)s does not exist. You can"
+                                  " create it here.", name=milestone.name))
 
         chrome = Chrome(self.env)
         chrome.add_jquery_ui(req)
@@ -910,7 +958,7 @@
     def _render_link(self, context, name, label, extra=''):
         try:
             milestone = Milestone(self.env, name)
-        except TracError:
+        except ResourceNotFound:
             milestone = None
         # Note: the above should really not be needed, `Milestone.exists`
         # should simply be false if the milestone doesn't exist in the db
@@ -918,9 +966,29 @@
         href = context.href.milestone(name)
         if milestone and milestone.exists:
             if 'MILESTONE_VIEW' in context.perm(milestone.resource):
+                title = None
+                if hasattr(context, 'req'):
+                    if milestone.is_completed:
+                        title = _(
+                            'Completed %(duration)s ago (%(date)s)',
+                            duration=pretty_timedelta(milestone.completed),
+                            date=user_time(context.req, format_datetime,
+                                           milestone.completed))
+                    elif milestone.is_late:
+                        title = _('%(duration)s late (%(date)s)',
+                                  duration=pretty_timedelta(milestone.due),
+                                  date=user_time(context.req, format_datetime,
+                                                 milestone.due))
+                    elif milestone.due:
+                        title = _('Due in %(duration)s (%(date)s)',
+                                  duration=pretty_timedelta(milestone.due),
+                                  date=user_time(context.req, format_datetime,
+                                                 milestone.due))
+                    else:
+                        title = _('No date set')
                 closed = 'closed ' if milestone.is_completed else ''
                 return tag.a(label, class_='%smilestone' % closed,
-                             href=href + extra)
+                             href=href + extra, title=title)
         elif 'MILESTONE_CREATE' in context.perm('milestone', name):
             return tag.a(label, class_='missing milestone', href=href + extra,
                          rel='nofollow')
@@ -972,7 +1040,7 @@
         milestone_realm = Resource('milestone')
         for name, due, completed, description \
                 in MilestoneCache(self.env).milestones.itervalues():
-            if any(r.search(description) or r.search(name)
+            if all(r.search(description) or r.search(name)
                    for r in term_regexps):
                 milestone = milestone_realm(id=name)
                 if 'MILESTONE_VIEW' in req.perm(milestone):
diff --git a/trac/trac/admin/templates/admin_components.html b/trac/trac/ticket/templates/admin_components.html
similarity index 76%
rename from trac/trac/admin/templates/admin_components.html
rename to trac/trac/ticket/templates/admin_components.html
index 6a0556a..e489366 100644
--- a/trac/trac/admin/templates/admin_components.html
+++ b/trac/trac/ticket/templates/admin_components.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -11,7 +21,7 @@
   </head>
 
   <body>
-    <h2>Manage Components</h2>
+    <h2>Manage Components <span py:if="view == 'list'" class="trac-count">(${len(components)})</span></h2>
 
     <py:def function="owner_field(default_owner='', br_after_label=False)">
       <div class="field">
@@ -34,26 +44,24 @@
         <fieldset>
           <legend>Modify Component:</legend>
           <div class="field">
-            <label>Name:<br /><input type="text" name="name" value="$component.name"/></label>
+            <label>Name:<br /><input type="text" name="name" class="trac-autofocus" value="$component.name" /></label>
           </div>
           ${owner_field(component.owner, True)}
           <div class="field">
             <fieldset>
               <label for="description" i18n:msg="">
-                Description (you may use
-                <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a>
-                here):
+                Description: (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here)
               </label>
               <p>
-                <textarea id="description" name="description" class="wikitext trac-resizable"
+                <textarea id="description" name="description" class="wikitext trac-fullwidth trac-resizable"
                           rows="6" cols="60">
 $component.description</textarea>
               </p>
             </fieldset>
           </div>
           <div class="buttons">
+            <input type="submit" name="save" class="trac-disable-on-submit" value="${_('Save')}"/>
             <input type="submit" name="cancel" value="${_('Cancel')}" />
-            <input type="submit" name="save" value="${_('Save')}" />
           </div>
         </fieldset>
       </form>
@@ -67,7 +75,7 @@
             </div>
             ${owner_field()}
             <div class="buttons">
-              <input type="submit" name="add" value="${_('Add')}"/>
+              <input type="submit" name="add" class="trac-disable-on-submit" value="${_('Add')}"/>
             </div>
           </fieldset>
         </form>
@@ -95,8 +103,8 @@
               </tbody>
             </table>
             <div class="buttons">
-              <input type="submit" name="remove" value="${_('Remove selected items')}" />
               <input type="submit" name="apply" value="${_('Apply changes')}" />
+              <input type="submit" name="remove" class="trac-disable-on-submit" value="${_('Remove selected items')}"/>
             </div>
             <p class="help">
               You can remove all items from this list to completely hide this
diff --git a/trac/trac/admin/templates/admin_enums.html b/trac/trac/ticket/templates/admin_enums.html
similarity index 74%
rename from trac/trac/admin/templates/admin_enums.html
rename to trac/trac/ticket/templates/admin_enums.html
index 5354f5d..7e1c202 100644
--- a/trac/trac/admin/templates/admin_enums.html
+++ b/trac/trac/ticket/templates/admin_enums.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -11,18 +21,21 @@
   </head>
 
   <body>
-    <h2 i18n:msg="label_plural">Manage $label_plural</h2>
+    <h2>
+      <i18n:msg params="label_plural">Manage $label_plural</i18n:msg>
+      <span py:if="view == 'list'" class="trac-count">(${len(enums)})</span>
+    </h2>
 
     <py:choose test="view">
       <form py:when="'detail'" class="mod" id="modenum" method="post" action="">
         <fieldset>
-          <legend i18n:msg="label_singular">Modify $label_singular</legend>
+          <legend i18n:msg="label_singular">Modify $label_singular:</legend>
           <div class="field">
-            <label>Name: <input type="text" name="name" value="${enum.name}" /></label>
+            <label>Name: <input type="text" name="name" class="trac-autofocus" value="${enum.name}" /></label>
           </div>
           <div class="buttons">
+            <input type="submit" name="save" class="trac-disable-on-submit" value="${_('Save')}"/>
             <input type="submit" name="cancel" value="${_('Cancel')}"/>
-            <input type="submit" name="save" value="${_('Save')}"/>
           </div>
         </fieldset>
       </form>
@@ -30,12 +43,12 @@
       <py:otherwise>
         <form class="addnew" id="addenum" method="post" action="">
           <fieldset>
-            <legend i18n:msg="label_singular">Add $label_singular</legend>
+            <legend i18n:msg="label_singular">Add $label_singular:</legend>
             <div class="field">
               <label>Name: <input type="text" name="name" id="name"/></label>
             </div>
             <div class="buttons">
-              <input type="submit" name="add" value="${_('Add')}"/>
+              <input type="submit" name="add" class="trac-disable-on-submit" value="${_('Add')}"/>
             </div>
           </fieldset>
         </form>
@@ -66,8 +79,8 @@
               </tbody>
             </table>
             <div class="buttons">
-              <input type="submit" name="remove" value="${_('Remove selected items')}" />
               <input type="submit" name="apply" value="${_('Apply changes')}" />
+              <input type="submit" name="remove" class="trac-disable-on-submit" value="${_('Remove selected items')}" />
             </div>
             <p class="help">
               You can remove all items from this list to completely hide this
diff --git a/trac/trac/admin/templates/admin_milestones.html b/trac/trac/ticket/templates/admin_milestones.html
similarity index 72%
rename from trac/trac/admin/templates/admin_milestones.html
rename to trac/trac/ticket/templates/admin_milestones.html
index a2faabe..a3129ab 100644
--- a/trac/trac/admin/templates/admin_milestones.html
+++ b/trac/trac/ticket/templates/admin_milestones.html
@@ -1,31 +1,49 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:xi="http://www.w3.org/2001/XInclude"
       xmlns:py="http://genshi.edgewall.org/"
-      xmlns:i18n="http://genshi.edgewall.org/i18n">
+      xmlns:i18n="http://genshi.edgewall.org/i18n"
+      py:with="perm = req.perm('admin', 'ticket/milestones');
+               can_create = 'MILESTONE_CREATE' in perm;
+               can_modify = 'MILESTONE_MODIFY' in perm;
+               can_remove = 'MILESTONE_DELETE' in perm">
   <xi:include href="admin.html" />
   <head>
     <title>Milestones</title>
-    <script type="text/javascript">/*<![CDATA[*/
+    <script type="text/javascript"
+            py:if="view == 'detail' and can_modify or
+                   view == 'list' and can_create">
       jQuery(document).ready(function($) {
         $("#duedate").datetimepicker();
         $("#completeddate").datetimepicker();
       });
-    /*]]>*/</script>
+    </script>
   </head>
 
   <body>
-    <h2>Manage Milestones</h2>
+    <h2>Manage Milestones <span py:if="view == 'list'" class="trac-count">(${len(milestones)})</span></h2>
 
     <py:choose test="view">
       <form py:when="'detail'" class="mod" method="post" id="modifymilestone" action=""
-            py:with="readonly = 'MILESTONE_MODIFY' not in req.perm or None">
+            py:with="disabled = 'disabled' if not can_modify else None;
+                     readonly = 'readonly' if not can_modify else None">
         <fieldset>
           <legend>Modify Milestone:</legend>
           <div class="field">
-            <label>Name:<br /> <input type="text" name="name" value="$milestone.name" readonly="${readonly}"/></label>
+            <label>Name:<br /> <input type="text" name="name" class="trac-autofocus"
+                                      value="$milestone.name" readonly="${readonly}" /></label>
           </div>
           <div class="field">
             <label>Due:<br />
@@ -38,14 +56,14 @@
           <div class="field">
             <label>
               <input type="checkbox" id="completed" name="completed"
-                     checked="${milestone.completed or None}" disabled="${readonly}"/>
+                     checked="${milestone.completed or None}" disabled="${disabled}"/>
               Completed:<br />
             </label>
             <label>
               <input type="text" id="completeddate" name="completeddate"
                      size="${len(datetime_hint)}"
                      value="${format_datetime(milestone.completed)}" readonly="${readonly}"
-                     title="${_('Format: %(datehint)s', datehint=datetime_hint)}"/>
+                     title="${_('Format: %(datehint)s', datehint=datetime_hint)}" />
               <span class="hint" i18n:msg="datehint">Format: $datetime_hint</span>
             </label>
             <script type="text/javascript">
@@ -61,24 +79,24 @@
           <div class="field">
             <fieldset>
               <label for="description" i18n:msg="">
-                Description (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here):
+                Description: (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here)
               </label>
               <p>
-              <textarea id="description" name="description" class="wikitext trac-resizable"
+              <textarea id="description" name="description" class="wikitext trac-fullwidth trac-resizable"
                         rows="6" cols="60" readonly="${readonly}">
 ${milestone.description}</textarea>
               </p>
             </fieldset>
           </div>
           <div class="buttons">
+            <input type="submit" name="save" value="${_('Save')}" class="trac-disable-on-submit" disabled="${disabled}"/>
             <input type="submit" name="cancel" value="${_('Cancel')}"/>
-            <input type="submit" name="save" value="${_('Save')}" disabled="${readonly}"/>
           </div>
         </fieldset>
       </form>
 
       <py:otherwise>
-        <form class="addnew" id="addmilestone" method="post" action="" py:if="'MILESTONE_CREATE' in req.perm">
+        <form class="addnew" id="addmilestone" method="post" action="" py:if="can_create">
           <fieldset>
             <legend>Add Milestone:</legend>
             <div class="field">
@@ -93,14 +111,13 @@
               </label>
             </div>
             <div class="buttons">
-              <input type="submit" name="add" value="${_('Add')}" />
+              <input type="submit" name="add" class="trac-disable-on-submit" value="${_('Add')}"/>
             </div>
           </fieldset>
         </form>
 
         <py:choose>
-          <form id="milestone_table" method="post" action=""
-                py:when="milestones" py:with="can_remove = 'MILESTONE_DELETE' in req.perm">
+          <form id="milestone_table" method="post" action="" py:when="milestones">
             <table class="listing" id="millist">
               <thead>
                 <tr><th class="sel" py:if="can_remove">&nbsp;</th>
@@ -128,8 +145,8 @@
               </tr></tbody>
             </table>
             <div class="buttons">
-              <input type="submit" name="remove" value="${_('Remove selected items')}" py:if="can_remove" />
               <input type="submit" name="apply" value="${_('Apply changes')}" />
+              <input type="submit" name="remove" class="trac-disable-on-submit" value="${_('Remove selected items')}" py:if="can_remove"/>
             </div>
             <p class="help">
               You can remove all items from this list to completely hide this
@@ -137,7 +154,7 @@
             </p>
           </form>
 
-          <p py:otherwise="" class="hint">
+          <p py:otherwise="" class="help">
             As long as you don't add any items to the list, this field
             will remain completely hidden from the user interface.
           </p>
diff --git a/trac/trac/admin/templates/admin_versions.html b/trac/trac/ticket/templates/admin_versions.html
similarity index 76%
rename from trac/trac/admin/templates/admin_versions.html
rename to trac/trac/ticket/templates/admin_versions.html
index 0350502..7decbfd 100644
--- a/trac/trac/admin/templates/admin_versions.html
+++ b/trac/trac/ticket/templates/admin_versions.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -8,15 +18,15 @@
   <xi:include href="admin.html" />
   <head>
     <title>Versions</title>
-    <script type="text/javascript">/*<![CDATA[*/
+    <script type="text/javascript">
       jQuery(document).ready(function($) {
         $("#releaseddate").datetimepicker();
       });
-    /*]]>*/</script>
+    </script>
   </head>
 
   <body>
-    <h2>Manage Versions</h2>
+    <h2>Manage Versions <span py:if="view == 'list'" class="trac-count">(${len(versions)})</span></h2>
 
     <py:choose test="view">
       <form py:when="'detail'" class="mod" id="modifyversion" method="post" action="">
@@ -24,11 +34,11 @@
           <legend>Modify Version:</legend>
           <div class="field">
             <label>Name:<br />
-              <input type="text" name="name" value="${version.name}" />
+              <input type="text" name="name" class="trac-autofocus" value="${version.name}" />
             </label>
           </div>
           <div class="field">
-            <label>Date:<br />
+            <label>Released:<br />
               <input type="text" id="releaseddate" name="time" size="${len(datetime_hint)}"
                      value="${format_datetime(version.time)}"
                      title="${_('Format: %(datehint)s', datehint=datetime_hint)}" />
@@ -38,17 +48,17 @@
           <div class="field">
             <fieldset>
               <label for="description" i18n:msg="">
-                Description (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here):
+                Description: (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here)
               </label>
               <p>
-                <textarea id="description" name="description" class="wikitext trac-resizable" rows="6" cols="60">
+                <textarea id="description" name="description" class="wikitext trac-fullwidth trac-resizable" rows="6" cols="60">
 $version.description</textarea>
               </p>
             </fieldset>
           </div>
           <div class="buttons">
+            <input type="submit" name="save" class="trac-disable-on-submit" value="${_('Save')}"/>
             <input type="submit" name="cancel" value="${_('Cancel')}"/>
-            <input type="submit" name="save" value="${_('Save')}"/>
           </div>
         </fieldset>
       </form>
@@ -70,7 +80,7 @@
               </label>
             </div>
             <div class="buttons">
-              <input type="submit" name="add" value="${_('Add')}" />
+              <input type="submit" name="add" class="trac-disable-on-submit" value="${_('Add')}" />
             </div>
           </fieldset>
         </form>
@@ -96,8 +106,8 @@
               </tbody>
             </table>
             <div class="buttons">
-              <input type="submit" name="remove" value="${_('Remove selected items')}" />
               <input type="submit" name="apply" value="${_('Apply changes')}" />
+              <input type="submit" name="remove" class="trac-disable-on-submit" value="${_('Remove selected items')}" />
             </div>
             <p class="help">
               You can remove all items from this list to completely hide this
diff --git a/trac/trac/ticket/templates/batch_modify.html b/trac/trac/ticket/templates/batch_modify.html
index 0d09162..be5ada2 100644
--- a/trac/trac/ticket/templates/batch_modify.html
+++ b/trac/trac/ticket/templates/batch_modify.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2012-2014 Edgewall Software
+
+  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/.
+-->
 <form xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:i18n="http://genshi.edgewall.org/i18n"
@@ -12,7 +22,7 @@
         <label for="batchmod_value_comment">Comment:</label>
       </th>
       <td class="fullrow"><textarea
-          id="batchmod_value_comment" name="batchmod_value_comment" cols="70" rows="5"/>
+          id="batchmod_value_comment" name="batchmod_value_comment" class="trac-fullwidth" cols="70" rows="5"/>
       </td>
     </tr>
 
@@ -54,7 +64,7 @@
   <div>
     <input type="hidden" name="selected_tickets" value=""/>
     <input type="hidden" name="query_href" value="${query_href}"/>
-    <input type="submit" id="batchmod_submit" name="batchmod_submit" value="${_('Change tickets')}" />
+    <input type="submit" id="batchmod_submit" name="batchmod_submit" class="trac-disable-on-submit" value="${_('Change tickets')}" />
   </div>
 
 </fieldset>
diff --git a/trac/trac/ticket/templates/milestone_delete.html b/trac/trac/ticket/templates/milestone_delete.html
index dc293d5..b9b4f6f 100644
--- a/trac/trac/ticket/templates/milestone_delete.html
+++ b/trac/trac/ticket/templates/milestone_delete.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -10,11 +20,6 @@
     <title i18n:msg="name">Delete Milestone ${milestone.name}</title>
     <link rel="stylesheet" type="text/css"
           href="${chrome.htdocs_location}css/roadmap.css" />
-    <script type="text/javascript">
-      jQuery(document).ready(function($) {
-        $("#retarget").click(function(){ $("#target").enable(this.checked) });
-      });
-    </script>
   </head>
 
   <body>
@@ -25,7 +30,6 @@
       <div>
         <input type="hidden" name="action" value="delete" />
         <p><strong>Are you sure you want to delete this milestone?</strong></p>
-        <input type="checkbox" id="retarget" name="retarget" checked="checked" />
         <label for="target">Retarget associated tickets to milestone</label>
         <select name="target" id="target">
           <option value="">None</option>
@@ -37,8 +41,8 @@
         </select>
       </div>
       <div class="buttons">
+        <input type="submit" id="delete" class="trac-disable-on-submit" value="${_('Delete milestone')}" />
         <input type="submit" name="cancel" value="${_('Cancel')}" />
-        <input type="submit" value="${_('Delete milestone')}" />
       </div>
     </form>
 
diff --git a/trac/trac/ticket/templates/milestone_edit.html b/trac/trac/ticket/templates/milestone_edit.html
index 07b6ccf..1371d11 100644
--- a/trac/trac/ticket/templates/milestone_edit.html
+++ b/trac/trac/ticket/templates/milestone_edit.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -26,13 +36,14 @@
           var retarget = $("#retarget");
           retarget.enable(checked);
           $("#target").enable(checked && retarget.checked());
+          $("#retarget-comment").enable(checked && retarget.checked());
         }
         $("#completed").click(updateCompletedDate);
         updateCompletedDate();
         $("#retarget").click(function(){
           $("#target").enable(this.checked);
+          $("#retarget-comment").enable(this.checked);
         });
-        $("#name").get(0).focus()
         $("#duedate").datetimepicker();
         $("#completeddate").datetimepicker();
       });
@@ -51,7 +62,8 @@
           <input type="hidden" name="id" value="${milestone.name}" />
           <input type="hidden" name="action" value="edit" />
           <label>Name of the milestone:<br />
-            <input type="text" id="name" name="name" size="32" value="${milestone.name or req.args.get('name')}" />
+            <input type="text" id="name" name="name" class="trac-autofocus" size="32"
+                   value="${milestone.name or req.args.get('name')}" />
           </label>
         </div>
         <fieldset>
@@ -91,20 +103,28 @@
                           value="${milestone.name}" py:content="milestone.name"></option>
                 </optgroup>
               </select>
+              <br/>
+              <label for="retarget-comment">Comment:</label>
+              <!--! Don't translate ticket comment (comment:40:ticket:5658) -->
+              <input type="text" id="retarget-comment" name="comment" size="40"
+                     value="Ticket retargeted after milestone closed" />
             </py:if>
           </div>
         </fieldset>
         <div class="field">
           <fieldset>
-            <label for="description" i18n:msg="">Description (you may use <a tabindex="42"
-                   href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here):</label>
-            <p><textarea id="description" name="description" class="wikitext trac-resizable" rows="10" cols="78">
+            <label for="description" i18n:msg="">
+              Description: (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here)
+            </label>
+            <p><textarea id="description" name="description" class="wikitext trac-fullwidth trac-resizable" rows="10" cols="78">
 ${milestone.description}</textarea></p>
           </fieldset>
         </div>
         <div class="buttons" py:choose="milestone.exists">
-          <input py:when="True" type="submit" value="${_('Submit changes')}" />
-          <input py:otherwise="" type="submit" value="${_('Add milestone')}" />
+          <input py:when="True" type="submit" name="save"
+                 value="${_('Submit changes')}" class="trac-disable-on-submit" />
+          <input py:otherwise="" type="submit" name="add"
+                 value="${_('Add milestone')}" />
           <input type="submit" name="cancel" value="${_('Cancel')}" />
         </div>
       </form>
diff --git a/trac/trac/ticket/templates/milestone_view.html b/trac/trac/ticket/templates/milestone_view.html
index 9989690..7b73931 100644
--- a/trac/trac/ticket/templates/milestone_view.html
+++ b/trac/trac/ticket/templates/milestone_view.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/ticket/templates/query.html b/trac/trac/ticket/templates/query.html
index 4f8e682..b6e9830 100644
--- a/trac/trac/ticket/templates/query.html
+++ b/trac/trac/ticket/templates/query.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -223,7 +233,7 @@
       </form>
 
       <xi:include href="query_results.html" />
-      <xi:include py:if="batch_modify" href="batch_modify.html" />
+      <xi:include py:if="tickets and batch_modify" href="batch_modify.html" />
 
       <div id="trac-report-buttons" class="buttons"
            py:with="edit = report_resource and 'REPORT_MODIFY' in perm(report_resource);
diff --git a/trac/trac/ticket/templates/query_results.html b/trac/trac/ticket/templates/query_results.html
index 4bba241..747da1f 100644
--- a/trac/trac/ticket/templates/query_results.html
+++ b/trac/trac/ticket/templates/query_results.html
@@ -1,17 +1,26 @@
-<!--!
-       groups    - a dict, where:
-                     key       - is the value shared by all results in this group
-                     value     - is the list of corresponding tickets
+<!--!  Copyright (C) 2006-2014 Edgewall Software
 
-       headers   - a sequence of header structure:
-                     .name     - field name for this header
-                     .label    - what to display for this header
+  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.
 
-       fields    - dict of field name to field structure:
-                     .label    - field label
+  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/.
 
-       query     - the actual Query instance used to perform the query
+Arguments:
+ - groups    - a dict, where:
+                key       - is the value shared by all results in this group
+                value     - is the list of corresponding tickets
 
+ - headers   - a sequence of header structure:
+                .name     - field name for this header
+                .label    - what to display for this header
+
+ - fields    - dict of field name to field structure:
+                .label    - field label
+
+ - query     - the actual Query instance used to perform the query
 -->
 <div xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
@@ -74,7 +83,8 @@
                   <py:with vars="name = header.name; value = result[name]">
                     <td py:when="name == 'id'" class="id"><a href="$result.href" title="View ticket"
                         class="${classes(closed=result.status == 'closed')}">#$result.id</a></td>
-                    <td py:otherwise="" class="$name" py:choose="">
+                    <td py:otherwise="" class="$name"
+                        xml:space="${'preserve' if header.wikifyblock else None}" py:choose="">
                       <a py:when="name == 'summary'" href="$result.href" title="View ticket">$value</a>
                       <py:when test="isinstance(value, datetime)">${pretty_dateinfo(value, dateonly=True)}</py:when>
                       <py:when test="name == 'reporter'">${authorinfo(value)}</py:when>
@@ -82,6 +92,7 @@
                       <py:when test="name == 'owner' and value">${authorinfo(value)}</py:when>
                       <py:when test="name == 'milestone'"><a py:if="value" title="View milestone" href="${href.milestone(value)}">${value}</a></py:when>
                       <py:when test="header.wikify">${wiki_to_oneliner(ticket_context, value)}</py:when>
+                      <py:when test="header.wikifyblock">${wiki_to_html(ticket_context, value)}</py:when>
                       <py:otherwise>$value</py:otherwise>
                     </td>
                   </py:with>
diff --git a/trac/trac/ticket/templates/report_delete.html b/trac/trac/ticket/templates/report_delete.html
index 030487a..1e95ba1 100644
--- a/trac/trac/ticket/templates/report_delete.html
+++ b/trac/trac/ticket/templates/report_delete.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -12,14 +22,14 @@
 
   <body>
     <div id="content" class="report">
-      <h1>$report.title</h1>
+      <h1>{$report.id} $report.title</h1>
       <form action="${href.report()}" method="post">
         <p><strong>Are you sure you want to delete this report?</strong></p>
         <div class="buttons">
           <input type="hidden" name="id" value="$report.id"/>
           <input type="hidden" name="action" value="delete" />
+          <input type="submit" class="trac-disable-on-submit" value="${_('Delete report')}"/>
           <input type="submit" name="cancel" value="${_('Cancel')}"/>
-          <input type="submit" value="${_('Delete report')}"/>
         </div>
       </form>
 
diff --git a/trac/trac/ticket/templates/report_edit.html b/trac/trac/ticket/templates/report_edit.html
index 9e5ced9..a70a72d 100644
--- a/trac/trac/ticket/templates/report_edit.html
+++ b/trac/trac/ticket/templates/report_edit.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -11,38 +21,45 @@
   </head>
 
   <body>
-    <div id="content" class="report">
+    <div id="content" class="report edit"
+         py:with="new_report = action == 'new'">
 
-      <h1>${_('New Report') if action == 'new' else report.title}</h1>
-      <form action="${href.report(report.id)}" method="post" id="edit_report">
-        <div>
+      <h1 py:choose="">
+        <py:when test="new_report">New Report</py:when>
+        <py:otherwise>$report.title</py:otherwise>
+      </h1>
+      <form id="edit_report" method="post" action="${href.report(report.id)}">
+        <fieldset>
+          <legend py:choose="">
+            <py:when test="new_report">Create Report:</py:when>
+            <py:otherwise>Modify Report:</py:otherwise>
+          </legend>
           <input type="hidden" name="action" value="$action" />
           <div class="field">
-            <label for="title">Report Title:</label><br />
-            <input type="text" id="title" name="title" value="$report.title" size="50"/><br />
+            <label for="title">Title:</label>
+            <input type="text" id="title" name="title" class="trac-fullwidth trac-autofocus" value="$report.title" />
           </div>
           <div class="field">
             <label for="description" i18n:msg="">
               Description: (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here)
             </label>
-            <br />
-            <textarea id="description" name="description" class="wikitext trac-resizable" rows="10" cols="78">
+            <textarea id="description" name="description" class="wikitext trac-fullwidth trac-resizable" rows="10" cols="78">
 $report.description</textarea>
           </div>
           <div class="field">
             <div class="system-message" py:if="error">
               <strong>Error:</strong> $error
             </div>
-            <label for="query" i18n:msg="">Query for Report: (can be either SQL or, if starting with <tt>query:</tt>,
+            <label for="query" i18n:msg="">Query: (can be either SQL or, if starting with <tt>query:</tt>,
               a <a tabindex="42" href="${href.wiki('TracQuery') + '#QueryLanguage'}">TracQuery</a> expression)
-            </label><br />
-            <textarea id="query" name="query" class="trac-resizable" cols="85" rows="20">
+            </label>
+            <textarea id="query" name="query" class="trac-fullwidth trac-resizable" rows="20" cols="78">
 $report.sql</textarea>
           </div>
-          <div class="buttons">
-            <input type="submit" value="${_('Save report')}"/>
-            <input type="submit" name="cancel" value="${_('Cancel')}"/>
-          </div>
+        </fieldset>
+        <div class="buttons">
+          <input type="submit" class="trac-disable-on-submit" value="${_('Save report')}" />
+          <input type="submit" name="cancel" value="${_('Cancel')}" />
         </div>
       </form>
 
diff --git a/trac/trac/ticket/templates/report_list.html b/trac/trac/ticket/templates/report_list.html
index c8db59e..14d0197 100644
--- a/trac/trac/ticket/templates/report_list.html
+++ b/trac/trac/ticket/templates/report_list.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2009-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -96,7 +106,7 @@
               </a></h3>
             <span class="foldable" />
             <div py:if="description" class="description" xml:space="preserve">
-              ${wiki_to_html(context, description)}
+              ${wiki_to_html(context.child('report', id), description)}
             </div>
           </div>
         </py:when>
diff --git a/trac/trac/ticket/templates/report_view.html b/trac/trac/ticket/templates/report_view.html
index 25ab3d3..ee1f1cb 100644
--- a/trac/trac/ticket/templates/report_view.html
+++ b/trac/trac/ticket/templates/report_view.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/ticket/templates/roadmap.html b/trac/trac/ticket/templates/roadmap.html
index f25ca16..97430f2 100644
--- a/trac/trac/ticket/templates/roadmap.html
+++ b/trac/trac/ticket/templates/roadmap.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -60,7 +70,7 @@
             </py:choose>
             <xi:include href="progress_bar.html" py:if="mstats.stats.count"
                         py:with="stats = mstats.stats; interval_hrefs = mstats.interval_hrefs;
-                                 stats_href = mstats.stats_href"/>
+                                 stats_href = mstats.stats_href" />
           </div>
 
           <div class="description" xml:space="preserve">
@@ -71,10 +81,10 @@
       </div>
 
       <div py:if="'MILESTONE_CREATE' in perm" class="buttons">
-       <form method="get" action="${href.milestone()}"><div>
-        <input type="hidden" name="action" value="new" />
-        <input type="submit" value="${_('Add new milestone')}" />
-       </div></form>
+        <form id="add" method="get" action="${href.milestone()}"><div>
+          <input type="hidden" name="action" value="new" />
+          <input type="submit" value="${_('Add new milestone')}" />
+        </div></form>
       </div>
 
       <div id="help" i18n:msg=""><strong>Note:</strong> See
diff --git a/trac/trac/ticket/templates/ticket.html b/trac/trac/ticket/templates/ticket.html
index def8013..768a7cd 100644
--- a/trac/trac/ticket/templates/ticket.html
+++ b/trac/trac/ticket/templates/ticket.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2007-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -97,17 +107,11 @@
           comment.toggle(comment.children().length != 0);
         }, "#changelog .trac-loading");
         /*]]>*/
-        <py:if test="preview_mode">
-        $("#trac-add-comment").scrollToTop();
-        </py:if>
       </py:when>
       <py:otherwise>
         $("#propertyform").autoSubmit({preview: '1'}, function(data, reply) {
           $('#ticket').replaceWith(reply);
         }, "#ticket .trac-loading");
-        <py:if test="not preview_mode">
-        $("#field-summary").focus();
-        </py:if>
       </py:otherwise>
       });
     </script>
@@ -182,7 +186,8 @@
             action="${href.ticket(ticket.id) + '#trac-add-comment' if ticket.exists
                       else href.newticket() + '#ticket'}">
         <!--! Add comment -->
-        <div py:if="ticket.exists and can_append" id="trac-add-comment" class="field">
+        <div py:if="ticket.exists and can_append" id="trac-add-comment"
+             class="${classes('field', 'trac-scroll' if preview_mode else None)}">
           <h3 class="foldable" id="edit">Add Comment</h3>
           <div>
             <div id="trac-edit-warning" class="warning system-message"
@@ -199,7 +204,7 @@
                 <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a>
                 here.
               </label>
-              <textarea id="comment" name="comment" class="wikitext trac-resizable" rows="10" cols="78">
+              <textarea id="comment" name="comment" class="wikitext trac-fullwidth trac-resizable" rows="10" cols="78">
 ${comment}</textarea>
             </fieldset>
           </div>
@@ -216,37 +221,35 @@
                   <py:otherwise>Properties</py:otherwise>
               </legend>
               <table>
+                <col class="th" /><col class="td" /><col class="th" /><col class="td" />
                 <tr py:if="can_modify or can_create">
                   <th><label for="field-summary">Summary:</label></th>
                   <td class="fullrow" colspan="3">
                     <input type="text" id="field-summary" name="field_summary"
-                           value="$ticket.summary" size="70" />
+                           class="${'trac-autofocus' if not ticket.exists and not preview_mode else None}"
+                           value="$ticket.summary" />
                   </td>
                 </tr>
-                <py:if test="only_for_admin">
-                  <tr>
-                    <th><label for="field-reporter">Reporter:</label></th>
-                    <td class="fullrow" colspan="3">
-                      <input type="text" id="field-reporter" name="field_reporter"
-                             value="${ticket.reporter}" size="70" />
-                    </td>
-                  </tr>
-                </py:if>
-                <py:if test="can_edit or can_create">
-                  <tr>
-                    <th><label for="field-description">Description:</label></th>
-                    <td class="fullrow" colspan="3">
-                      <fieldset>
-                        <label for="field-description" id="field-description-help" i18n:msg="">You may use
-                          <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here.
-                        </label>
-                        <textarea id="field-description" name="field_description"
-                                  class="wikitext trac-resizable" rows="10" cols="68">
+                <tr py:if="only_for_admin">
+                  <th><label for="field-reporter">Reporter:</label></th>
+                  <td class="fullrow" colspan="3">
+                    <input type="text" id="field-reporter" name="field_reporter"
+                           value="${ticket.reporter}" />
+                  </td>
+                </tr>
+                <tr py:if="can_edit or can_create">
+                  <th><label for="field-description">Description:</label></th>
+                  <td class="fullrow" colspan="3">
+                    <fieldset>
+                      <label for="field-description" id="field-description-help" i18n:msg="">You may use
+                        <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here.
+                      </label>
+                      <textarea id="field-description" name="field_description"
+                                class="wikitext trac-fullwidth trac-resizable" rows="10" cols="68">
 ${ticket.description}</textarea>
-                      </fieldset>
-                    </td>
-                  </tr>
-                </py:if>
+                    </fieldset>
+                  </td>
+                </tr>
                 <tr py:for="row in group(fields, 2, lambda f: f.type != 'textarea')"
                     py:if="can_modify or can_create"
                     py:with="fullrow = len(row) == 1">
@@ -361,7 +364,7 @@
               </tr>
             </table>
             <p py:if="author_id == 'anonymous'" class="hint">
-              <i18n:msg>E-mail address and user name can be saved in the <a href="${href.prefs()}">Preferences</a>.</i18n:msg>
+              <i18n:msg>E-mail address and user name can be saved in the <a href="${href.prefs()}" class="trac-target-new">Preferences</a>.</i18n:msg>
             </p>
           </fieldset>
         </div>
@@ -385,7 +388,7 @@
             <input type="hidden" name="replyto" value="${replyto}" />
           </py:if>
           <input type="submit" name="preview" value="${_('Preview')}" accesskey="r" />&nbsp;
-          <input type="submit" name="submit" value="${_('Submit changes') if ticket.exists else _('Create ticket')}" />
+          <input type="submit" name="submit" value="${_('Submit changes') if ticket.exists else _('Create ticket')}" class="trac-disable-on-submit" />
         </div>
 
       </form>
diff --git a/trac/trac/ticket/templates/ticket_box.html b/trac/trac/ticket/templates/ticket_box.html
index d5d0cb1..c7c6fd1 100644
--- a/trac/trac/ticket/templates/ticket_box.html
+++ b/trac/trac/ticket/templates/ticket_box.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+
 Ticket Box (ticket fields along with description).
 
 Arguments:
diff --git a/trac/trac/ticket/templates/ticket_change.html b/trac/trac/ticket/templates/ticket_change.html
index 7b9ea36..e5ecbea 100644
--- a/trac/trac/ticket/templates/ticket_change.html
+++ b/trac/trac/ticket/templates/ticket_change.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+
 Render a ticket comment.
 
 Arguments:
@@ -106,7 +115,7 @@
   <form py:if="show_editor" id="trac-comment-editor" method="post"
         action="${href.ticket(ticket.id) + '#comment:%d' % cnum}">
     <div>
-      <textarea name="edited_comment" class="wikitext trac-resizable" rows="10" cols="78">
+      <textarea name="edited_comment" class="wikitext trac-fullwidth trac-resizable" rows="10" cols="78">
 ${edited_comment if edited_comment is not None else change.comment}</textarea>
       <input type="hidden" name="cnum_edit" value="${cnum}"/>
     </div>
diff --git a/trac/trac/ticket/templates/ticket_preview.html b/trac/trac/ticket/templates/ticket_preview.html
index 08e38bd..57efef8 100644
--- a/trac/trac/ticket/templates/ticket_preview.html
+++ b/trac/trac/ticket/templates/ticket_preview.html
@@ -1,4 +1,13 @@
-<!--!
+<!--!  Copyright (C) 2011-2014 Edgewall Software
+
+  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/.
+
 Render data relevant to automatic ticket preview.
 -->
 <html xmlns="http://www.w3.org/1999/xhtml"
diff --git a/trac/trac/ticket/tests/__init__.py b/trac/trac/ticket/tests/__init__.py
index dc5b3aa..a434c34 100644
--- a/trac/trac/ticket/tests/__init__.py
+++ b/trac/trac/ticket/tests/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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 doctest
 import unittest
 
diff --git a/trac/trac/ticket/tests/api.py b/trac/trac/ticket/tests/api.py
index 618a944..fd2bc94 100644
--- a/trac/trac/ticket/tests/api.py
+++ b/trac/trac/ticket/tests/api.py
@@ -1,7 +1,20 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
+
 from trac.perm import PermissionCache, PermissionSystem
+from trac.test import EnvironmentStub, Mock
 from trac.ticket.api import TicketSystem, ITicketFieldProvider
 from trac.ticket.model import Ticket
-from trac.test import EnvironmentStub, Mock
 from trac.core import implements, Component
 
 import unittest
@@ -174,7 +187,8 @@
 
 
 def suite():
-    return unittest.makeSuite(TicketSystemTestCase, 'test')
+    return unittest.makeSuite(TicketSystemTestCase)
+
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/ticket/tests/batch.py b/trac/trac/ticket/tests/batch.py
index e129651..b09eec3 100644
--- a/trac/trac/ticket/tests/batch.py
+++ b/trac/trac/ticket/tests/batch.py
@@ -1,11 +1,29 @@
-from trac.perm import PermissionCache
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012-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/.
+
+from __future__ import with_statement
+
+import unittest
+from datetime import datetime, timedelta
+
+from trac.perm import DefaultPermissionPolicy, DefaultPermissionStore,\
+                      PermissionCache
 from trac.test import Mock, EnvironmentStub
 from trac.ticket import api, default_workflow, web_ui
 from trac.ticket.batch import BatchModifyModule
 from trac.ticket.model import Ticket
 from trac.util.datefmt import utc
-
-import unittest
+from trac.web.chrome import web_context
 
 
 class BatchModifyTestCase(unittest.TestCase):
@@ -13,8 +31,10 @@
     def setUp(self):
         self.env = EnvironmentStub(default_data=True,
             enable=[default_workflow.ConfigurableTicketWorkflow,
-                    web_ui.TicketModule, 
-                    api.TicketSystem])
+                    DefaultPermissionPolicy, DefaultPermissionStore,
+                    web_ui.TicketModule, api.TicketSystem])
+        self.env.config.set('trac', 'permission_policies',
+                            'DefaultPermissionPolicy')
         self.req = Mock(href=self.env.href, authname='anonymous', tz=utc)
         self.req.session = {}
         self.req.perm = PermissionCache(self.env)
@@ -220,8 +240,6 @@
         batch._save_ticket_changes(self.req, selected_tickets, {}, '',
                                    'embiggen')
 
-        ticket = Ticket(self.env, int(first_ticket_id))
-        changes = ticket.get_changelog()
         self.assertFieldChanged(first_ticket_id, 'status', 'big')
         self.assertFieldChanged(second_ticket_id, 'status', 'big')
 
@@ -242,15 +260,56 @@
         batch._save_ticket_changes(self.req, selected_tickets, {}, '',
                                    'buckify')
 
-        ticket = Ticket(self.env, int(first_ticket_id))
-        changes = ticket.get_changelog()
         self.assertFieldChanged(first_ticket_id, 'owner', 'buck')
         self.assertFieldChanged(second_ticket_id, 'owner', 'buck')
 
+    def test_timeline_events(self):
+        """Regression test for #11288"""
+        tktmod = web_ui.TicketModule(self.env)
+        now = datetime.now(utc)
+        start = now - timedelta(hours=1)
+        stop = now + timedelta(hours=1)
+        events = tktmod.get_timeline_events(self.req, start, stop,
+                                            ['ticket_details'])
+        self.assertEqual(True, all(ev[0] != 'batchmodify' for ev in events))
+
+        ids = []
+        for i in xrange(20):
+            ticket = Ticket(self.env)
+            ticket['summary'] = 'Ticket %d' % i
+            ids.append(ticket.insert())
+        ids.sort()
+        new_values = {'summary': 'batch updated ticket',
+                      'owner': 'ticket11288', 'reporter': 'ticket11288'}
+        batch = BatchModifyModule(self.env)
+        batch._save_ticket_changes(self.req, ids, new_values, '', 'leave')
+        # shuffle ticket_change records
+        with self.env.db_transaction as db:
+            rows = db('SELECT * FROM ticket_change')
+            db.execute('DELETE FROM ticket_change')
+            rows = rows[0::4] + rows[1::4] + rows[2::4] + rows[3::4]
+            db.executemany('INSERT INTO ticket_change VALUES (%s)' %
+                           ','.join(('%s',) * len(rows[0])),
+                           rows)
+
+        events = tktmod.get_timeline_events(self.req, start, stop,
+                                            ['ticket_details'])
+        events = [ev for ev in events if ev[0] == 'batchmodify']
+        self.assertEqual(1, len(events))
+        batch_ev = events[0]
+        self.assertEqual('anonymous', batch_ev[2])
+        self.assertEqual(ids, sorted(batch_ev[3][0]))
+        self.assertEqual('updated', batch_ev[3][1])
+
+        context = web_context(self.req)
+        self.assertEqual(
+            self.req.href.query(id=','.join(str(t) for t in ids)),
+            tktmod.render_timeline_event(context, 'url', batch_ev))
+
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(BatchModifyTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(BatchModifyTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/ticket/tests/conversion.py b/trac/trac/ticket/tests/conversion.py
index d1b0d5d..ac215eb 100644
--- a/trac/trac/ticket/tests/conversion.py
+++ b/trac/trac/ticket/tests/conversion.py
@@ -1,11 +1,24 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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 os
 import unittest
 
 from trac import __version__ as TRAC_VERSION
-from trac.test import EnvironmentStub, Mock
+from trac.mimeview.api import Mimeview
+from trac.test import EnvironmentStub, Mock, MockPerm
 from trac.ticket.model import Ticket
 from trac.ticket.web_ui import TicketModule
-from trac.mimeview.api import Mimeview
 from trac.web.href import Href
 
 
@@ -37,6 +50,16 @@
         ticket.insert()
         return ticket
 
+    def _create_a_ticket_with_email(self):
+        ticket = Ticket(self.env)
+        ticket['owner'] = 'joe@example.org'
+        ticket['reporter'] = 'santa@example.org'
+        ticket['cc'] = 'cc1, cc2@example.org'
+        ticket['summary'] = 'Foo'
+        ticket['description'] = 'Bar'
+        ticket.insert()
+        return ticket
+
     def test_conversions(self):
         conversions = self.mimeview.get_supported_conversions(
             'trac.ticket.Ticket')
@@ -61,6 +84,26 @@
                           'keywords,cc\r\n1,Foo,santa,,Bar,,,\r\n',
                           'text/csv;charset=utf-8', 'csv'), csv)
 
+    def test_csv_conversion_with_obfuscation(self):
+        ticket = self._create_a_ticket_with_email()
+        csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket',
+                                            ticket, 'csv')
+        self.assertEqual(
+            ('\xef\xbb\xbf'
+             'id,summary,reporter,owner,description,status,keywords,cc\r\n'
+             '1,Foo,santa@…,joe@…,Bar,,,cc1 cc2@…\r\n',
+             'text/csv;charset=utf-8', 'csv'),
+            csv)
+        self.req.perm = MockPerm()
+        csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket',
+                                            ticket, 'csv')
+        self.assertEqual(
+            ('\xef\xbb\xbf'
+             'id,summary,reporter,owner,description,status,keywords,cc\r\n'
+             '1,Foo,santa@example.org,joe@example.org,Bar,,,'
+             'cc1 cc2@example.org\r\n',
+             'text/csv;charset=utf-8', 'csv'),
+            csv)
 
     def test_tab_conversion(self):
         ticket = self._create_a_ticket()
@@ -72,6 +115,29 @@
                           'text/tab-separated-values;charset=utf-8', 'tsv'),
                          csv)
 
+    def test_tab_conversion_with_obfuscation(self):
+        ticket = self._create_a_ticket_with_email()
+        csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket',
+                                            ticket, 'tab')
+        self.assertEqual(
+            ('\xef\xbb\xbf'
+             'id\tsummary\treporter\towner\tdescription\tstatus\tkeywords\t'
+             'cc\r\n'
+             '1\tFoo\tsanta@…\tjoe@…\tBar\t\t\tcc1 cc2@…\r\n',
+             'text/tab-separated-values;charset=utf-8', 'tsv'),
+            csv)
+        self.req.perm = MockPerm()
+        csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket',
+                                            ticket, 'tab')
+        self.assertEqual(
+            ('\xef\xbb\xbf'
+             'id\tsummary\treporter\towner\tdescription\tstatus\tkeywords\t'
+             'cc\r\n'
+             '1\tFoo\tsanta@example.org\tjoe@example.org\tBar\t\t\t'
+             'cc1 cc2@example.org\r\n',
+             'text/tab-separated-values;charset=utf-8', 'tsv'),
+            csv)
+
     def test_rss_conversion(self):
         ticket = self._create_a_ticket()
         content, mimetype, ext = self.mimeview.convert_content(
@@ -94,7 +160,8 @@
 
 
 def suite():
-    return unittest.makeSuite(TicketConversionTestCase, 'test')
+    return unittest.makeSuite(TicketConversionTestCase)
+
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/ticket/tests/functional.py b/trac/trac/ticket/tests/functional.py
index 8163f6e..dfb2d48 100755
--- a/trac/trac/ticket/tests/functional.py
+++ b/trac/trac/ticket/tests/functional.py
@@ -1,24 +1,89 @@
-#!/usr/bin/python
+#!/usr/bin/env python
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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 os
 import re
 
 from datetime import datetime, timedelta
 
+from trac.admin.tests.functional import AuthorizationTestCaseSetup
 from trac.test import locale_en
 from trac.tests.functional import *
+from trac.util import create_file
 from trac.util.datefmt import utc, localtz, format_date, format_datetime
+from trac.util.text import to_utf8
+
+try:
+    from configobj import ConfigObj
+except ImportError:
+    ConfigObj = None
 
 
 class TestTickets(FunctionalTwillTestCaseSetup):
     def runTest(self):
-        """Create a ticket, comment on it, and attach a file"""
+        """Create a ticket and comment on it."""
         # TODO: this should be split into multiple tests
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.create_ticket()
-        self._tester.add_comment(ticketid)
-        self._tester.attach_file_to_ticket(ticketid)
+        id = self._tester.create_ticket()
+        self._tester.add_comment(id)
+
+
+class TestTicketMaxSummarySize(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test `[ticket] max_summary_size` option.
+        http://trac.edgewall.org/ticket/11472"""
+        prev_max_summary_size = \
+            self._testenv.get_config('ticket', 'max_summary_size')
+        short_summary = "abcdefghijklmnopqrstuvwxyz"
+        long_summary = short_summary + "."
+        max_summary_size = len(short_summary)
+        warning_message = r"Ticket summary is too long \(must be less " \
+                          r"than %s characters\)" % max_summary_size
+        self._testenv.set_config('ticket', 'max_summary_size',
+                                 str(max_summary_size))
+        try:
+            self._tester.create_ticket(short_summary)
+            tc.find(short_summary)
+            tc.notfind(warning_message)
+            self._tester.go_to_front()
+            tc.follow(r"\bNew Ticket\b")
+            tc.notfind(internal_error)
+            tc.url(self._tester.url + '/newticket')
+            tc.formvalue('propertyform', 'field_summary', long_summary)
+            tc.submit('submit')
+            tc.url(self._tester.url + '/newticket')
+            tc.find(warning_message)
+        finally:
+            self._testenv.set_config('ticket', 'max_summary_size',
+                                     prev_max_summary_size)
+
+
+class TestTicketAddAttachment(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Add attachment to a ticket. Test that the attachment button
+        reads 'Attach file' when no files have been attached, and 'Attach
+        another file' when there are existing attachments.
+        Feature added in http://trac.edgewall.org/ticket/10281"""
+        id = self._tester.create_ticket()
+        tc.find("Attach file")
+        filename = self._tester.attach_file_to_ticket(id)
+
+        self._tester.go_to_ticket(id)
+        tc.find("Attach another file")
+        tc.find('Attachments <span class="trac-count">\(1\)</span>')
+        tc.find(filename)
+        tc.find('Download all attachments as:\s+<a rel="nofollow" '
+                'href="/zip-attachment/ticket/%s/">.zip</a>' % id)
 
 
 class TestTicketPreview(FunctionalTwillTestCaseSetup):
@@ -51,27 +116,72 @@
         tc.find('ticket not yet created')
 
 
+class TestTicketManipulator(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        plugin_name = self.__class__.__name__
+        env = self._testenv.get_trac_environment()
+        env.config.set('components', plugin_name + '.*', 'enabled')
+        env.config.save()
+        create_file(os.path.join(env.path, 'plugins', plugin_name + '.py'),
+"""\
+from genshi.builder import tag
+from trac.core import Component, implements
+from trac.ticket.api import ITicketManipulator
+from trac.util.translation import tag_
+
+
+class TicketManipulator(Component):
+    implements(ITicketManipulator)
+
+    def prepare_ticket(self, req, ticket, fields, actions):
+        pass
+
+    def validate_ticket(self, req, ticket):
+        field = 'reporter'
+        yield None, tag_("A ticket with the summary %(summary)s"
+                         " already exists.",
+                          summary=tag.em("Testing ticket manipulator"))
+        yield field, tag_("The ticket %(field)s is %(status)s.",
+                          field=tag.strong(field),
+                          status=tag.em("invalid"))
+""")
+        self._testenv.restart()
+
+        try:
+            self._tester.go_to_front()
+            tc.follow("New Ticket")
+            tc.formvalue('propertyform', 'field-description',
+                         "Testing ticket manipulator")
+            tc.submit('submit')
+            tc.url(self._tester.url + '/newticket$')
+            tc.find("A ticket with the summary <em>Testing ticket "
+                    "manipulator</em> already exists.")
+            tc.find("The ticket field 'reporter' is invalid: The"
+                    " ticket <strong>reporter</strong> is <em>invalid</em>.")
+        finally:
+            env.config.set('components', plugin_name + '.*', 'disabled')
+            env.config.save()
+
+
 class TestTicketAltFormats(FunctionalTestCaseSetup):
     def runTest(self):
         """Download ticket in alternative formats"""
         summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(summary)
         for format in ['Comma-delimited Text', 'Tab-delimited Text',
                        'RSS Feed']:
             tc.follow(format)
             content = b.get_html()
             if content.find(summary) < 0:
-                raise AssertionError('Summary missing from %s format' % format)
+                raise AssertionError('Summary missing from %s format'
+                                     % format)
             tc.back()
 
 
 class TestTicketCSVFormat(FunctionalTestCaseSetup):
     def runTest(self):
         """Download ticket in CSV format"""
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket()
         tc.follow('Comma-delimited Text')
         csv = b.get_html()
         if not csv.startswith('\xef\xbb\xbfid,summary,'): # BOM
@@ -80,24 +190,20 @@
 
 class TestTicketTabFormat(FunctionalTestCaseSetup):
     def runTest(self):
-        """Download ticket in Tab-delimitted format"""
-        summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        """Download ticket in Tab-delimited format"""
+        self._tester.create_ticket()
         tc.follow('Tab-delimited Text')
         tab = b.get_html()
         if not tab.startswith('\xef\xbb\xbfid\tsummary\t'): # BOM
-            raise AssertionError('Bad tab delimitted format')
+            raise AssertionError('Bad tab delimited format')
 
 
 class TestTicketRSSFormat(FunctionalTestCaseSetup):
     def runTest(self):
         """Download ticket in RSS format"""
         summary = random_sentence(5)
-        ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(summary)
         # Make a number of changes to exercise all of the RSS feed code
-        self._tester.go_to_ticket(ticketid)
         tc.formvalue('propertyform', 'comment', random_sentence(3))
         tc.formvalue('propertyform', 'field-type', 'task')
         tc.formvalue('propertyform', 'description', summary + '\n\n' +
@@ -119,7 +225,7 @@
     def runTest(self):
         """Test ticket search"""
         summary = random_sentence(4)
-        ticketid = self._tester.create_ticket(summary)
+        self._tester.create_ticket(summary)
         self._tester.go_to_front()
         tc.follow('Search')
         tc.formvalue('fullsearch', 'ticket', True)
@@ -135,7 +241,7 @@
         # Create a summary containing only unique words
         summary = ' '.join([random_word() + '_TestNonTicketSearch'
                             for i in range(5)])
-        ticketid = self._tester.create_ticket(summary)
+        self._tester.create_ticket(summary)
         self._tester.go_to_front()
         tc.follow('Search')
         tc.formvalue('fullsearch', 'ticket', False)
@@ -150,9 +256,14 @@
         """Test ticket history"""
         summary = random_sentence(5)
         ticketid = self._tester.create_ticket(summary)
-        comment = random_sentence(5)
-        self._tester.add_comment(ticketid, comment=comment)
+        comment = self._tester.add_comment(ticketid)
         self._tester.go_to_ticket(ticketid)
+        tc.find(r'<a [^>]+>\bModify\b</a>')
+        tc.find(r"\bAttach file\b")
+        tc.find(r"\bAdd Comment\b")
+        tc.find(r"\bModify Ticket\b")
+        tc.find(r"\bPreview\b")
+        tc.find(r"\bSubmit changes\b")
         url = b.get_url()
         tc.go(url + '?version=0')
         tc.find('at <[^>]*>*Initial Version')
@@ -162,19 +273,23 @@
         tc.find('at <[^>]*>*Version 1')
         tc.find(summary)
         tc.find(comment)
+        tc.notfind(r'<a [^>]+>\bModify\b</a>')
+        tc.notfind(r"\bAttach file\b")
+        tc.notfind(r"\bAdd Comment\b")
+        tc.notfind(r"\bModify Ticket\b")
+        tc.notfind(r"\bPreview\b")
+        tc.notfind(r"\bSubmit changes\b")
 
 
 class TestTicketHistoryDiff(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test ticket history (diff)"""
-        name = 'TestTicketHistoryDiff'
-        ticketid = self._tester.create_ticket(name)
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket()
         tc.formvalue('propertyform', 'description', random_sentence(6))
         tc.submit('submit')
         tc.find('Description<[^>]*>\\s*modified \\(<[^>]*>diff', 's')
         tc.follow('diff')
-        tc.find('Changes\\s*between\\s*<[^>]*>Initial Version<[^>]*>\\s*and' \
+        tc.find('Changes\\s*between\\s*<[^>]*>Initial Version<[^>]*>\\s*and'
                 '\\s*<[^>]*>Version 1<[^>]*>\\s*of\\s*<[^>]*>Ticket #' , 's')
 
 
@@ -214,14 +329,63 @@
         tc.find('class="missing">Next Ticket &rarr;')
 
 
+class TestTicketQueryLinksQueryModuleDisabled(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Ticket query links should not be present when the QueryModule
+        is disabled."""
+        def enable_query_module(enable):
+            self._tester.go_to_admin('Plugins')
+            tc.formvalue('edit-plugin-trac', 'component',
+                         'trac.ticket.query.QueryModule')
+            tc.formvalue('edit-plugin-trac', 'enable',
+                         '%strac.ticket.query.QueryModule'
+                         % ('+' if enable else '-'))
+            tc.submit()
+            tc.find("The following component has been %s:"
+                    ".*QueryModule.*\(trac\.ticket\.query\.\*\)"
+                    % ("enabled" if enable else "disabled"))
+        props = {'cc': 'user1, user2',
+                 'component': 'component1',
+                 'keywords': 'kw1, kw2',
+                 'milestone': 'milestone1',
+                 'owner': 'user',
+                 'priority': 'major',
+                 'reporter': 'admin',
+                 'version': '2.0'}
+        tid = self._tester.create_ticket(info=props)
+        milestone_cell = \
+            r'<td headers="h_milestone">\s*' \
+            r'<a class="milestone" href="/milestone/%(milestone)s" ' \
+            r'title=".*">\s*%(milestone)s\s*</a>\s*</td>'\
+            % {'milestone': props['milestone']}
+        try:
+            for field, value in props.iteritems():
+                if field != 'milestone':
+                    links = r', '.join(r'<a href="/query.*>%s</a>'
+                                       % v.strip() for v in value.split(','))
+                    tc.find(r'<td headers="h_%s"( class="searchable")?>'
+                            r'\s*%s\s*</td>' % (field, links))
+                else:
+                    tc.find(milestone_cell)
+            enable_query_module(False)
+            self._tester.go_to_ticket(tid)
+            for field, value in props.iteritems():
+                if field != 'milestone':
+                    tc.find(r'<td headers="h_%s"( class="searchable")?>'
+                            r'\s*%s\s*</td>' % (field, value))
+                else:
+                    tc.find(milestone_cell)
+        finally:
+            enable_query_module(True)
+
+
 class TestTicketQueryOrClause(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test ticket query with an or clauses"""
         count = 3
-        ticket_ids = [self._tester.create_ticket(
-                        summary='TestTicketQueryOrClause%s' % i,
-                        info={'keywords': str(i)})
-                      for i in range(count)]
+        [self._tester.create_ticket(summary='TestTicketQueryOrClause%s' % i,
+                                    info={'keywords': str(i)})
+         for i in range(count)]
         self._tester.go_to_query()
         tc.formvalue('query', '0_owner', '')
         tc.submit('rm_filter_0_owner_0')
@@ -233,7 +397,7 @@
         tc.formvalue('query', '1_keywords', '2')
         tc.submit('update')
         tc.notfind('TestTicketQueryOrClause0')
-        for i in [1, 2]:
+        for i in (1, 2):
             tc.find('TestTicketQueryOrClause%s' % i)
 
 
@@ -249,11 +413,8 @@
         env.config.set('ticket-custom', 'newfield.format', '')
         env.config.save()
 
-        self._testenv.restart()
         val = "%s %s" % (random_unique_camel(), random_word())
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % val)
 
 
@@ -269,11 +430,8 @@
         env.config.set('ticket-custom', 'newfield.format', '')
         env.config.save()
 
-        self._testenv.restart()
         val = "%s %s" % (random_unique_camel(), random_word())
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % val)
 
 
@@ -290,13 +448,10 @@
         env.config.set('ticket-custom', 'newfield.format', 'wiki')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         wiki = '<a [^>]*>%s\??</a> %s' % (word1, word2)
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % wiki)
 
@@ -313,13 +468,10 @@
         env.config.set('ticket-custom', 'newfield.format', 'wiki')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         wiki = '<p>\s*<a [^>]*>%s\??</a> %s<br />\s*</p>' % (word1, word2)
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % wiki)
 
@@ -338,13 +490,10 @@
         env.config.set('ticket-custom', 'newfield.format', 'reference')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         query = 'status=!closed&amp;newfield=%s\+%s' % (word1, word2)
         querylink = '<a href="/query\?%s">%s</a>' % (query, val)
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % querylink)
@@ -364,13 +513,10 @@
         env.config.set('ticket-custom', 'newfield.format', 'list')
         env.config.save()
 
-        self._testenv.restart()
         word1 = random_unique_camel()
         word2 = random_word()
         val = "%s %s" % (word1, word2)
-        ticketid = self._tester.create_ticket(summary=random_sentence(3),
-                                              info={'newfield': val})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket(info={'newfield': val})
         query1 = 'status=!closed&amp;newfield=~%s' % word1
         query2 = 'status=!closed&amp;newfield=~%s' % word2
         querylink1 = '<a href="/query\?%s">%s</a>' % (query1, word1)
@@ -392,9 +538,7 @@
         env.config.set('ticket-custom', 'newfield.format', 'list')
         env.config.save()
 
-        self._testenv.restart()
-        ticketid = self._tester.create_ticket(summary=random_sentence(3))
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket()
 
         word1 = random_unique_camel()
         word2 = random_word()
@@ -431,7 +575,7 @@
         tc.find('<td headers="h_newfield"[^>]*>\s*%s\s*</td>' % querylinks)
 
 
-class TestTimelineTicketDetails(FunctionalTwillTestCaseSetup):
+class TestTicketTimeline(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test ticket details on timeline"""
         env = self._testenv.get_trac_environment()
@@ -439,14 +583,18 @@
         env.config.save()
         summary = random_sentence(5)
         ticketid = self._tester.create_ticket(summary)
-        self._tester.go_to_ticket(ticketid)
         self._tester.add_comment(ticketid)
+
         self._tester.go_to_timeline()
+        tc.formvalue('prefs', 'ticket', True)
+        tc.submit()
+        tc.find('Ticket.*#%s.*created' % ticketid)
         tc.formvalue('prefs', 'ticket_details', True)
         tc.submit()
         htmltags = '(<[^>]*>)*'
-        tc.find('Ticket ' + htmltags + '#' + str(ticketid) + htmltags + ' \\(' +
-                summary + '\\) updated\\s+by\\s+' + htmltags + 'admin', 's')
+        tc.find('Ticket ' + htmltags + '#' + str(ticketid) + htmltags +
+                ' \\(' + summary.split()[0] +
+                ' [^\\)]+\\) updated\\s+by\\s+' + htmltags + 'admin', 's')
 
 
 class TestAdminComponent(FunctionalTwillTestCaseSetup):
@@ -455,10 +603,17 @@
         self._tester.create_component()
 
 
+class TestAdminComponentAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Components
+        panel."""
+        self.test_authorization('/admin/ticket/components', 'TICKET_ADMIN',
+                                "Manage Components")
+
 class TestAdminComponentDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate component"""
-        name = "DuplicateMilestone"
+        name = "DuplicateComponent"
         self._tester.create_component(name)
         component_url = self._tester.url + "/admin/ticket/components"
         tc.go(component_url)
@@ -521,12 +676,44 @@
         tc.notfind(desc)
 
 
+class TestAdminComponentNoneDefined(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """The table should be hidden and help text shown when there are no
+        components defined (#11103)."""
+        from trac.ticket import model
+        env = self._testenv.get_trac_environment()
+        components = list(model.Component.select(env))
+        self._tester.go_to_admin()
+        tc.follow(r"\bComponents\b")
+
+        try:
+            for comp in components:
+                tc.formvalue('component_table', 'sel', comp.name)
+            tc.submit('remove')
+            tc.notfind('<table class="listing" id="complist">')
+            tc.find("As long as you don't add any items to the list, this "
+                    "field[ \t\n]*will remain completely hidden from the "
+                    "user interface.")
+        finally:
+            for comp in components:
+                self._tester.create_component(comp.name, comp.owner,
+                                              comp.description)
+
+
 class TestAdminMilestone(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create milestone"""
         self._tester.create_milestone()
 
 
+class TestAdminMilestoneAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Milestone
+        panel."""
+        self.test_authorization('/admin/ticket/milestones', 'TICKET_ADMIN',
+                                "Manage Milestones")
+
+
 class TestAdminMilestoneSpace(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create milestone with a space"""
@@ -590,7 +777,8 @@
         """Admin milestone duedate"""
         name = "DueMilestone"
         duedate = datetime.now(tz=utc)
-        duedate_string = format_datetime(duedate, tzinfo=utc, locale=locale_en)
+        duedate_string = format_datetime(duedate, tzinfo=utc,
+                                         locale=locale_en)
         self._tester.create_milestone(name, due=duedate_string)
         tc.find(duedate_string)
 
@@ -609,13 +797,40 @@
         tc.follow(name)
         tc.url(milestone_url + '/' + name)
         duedate = datetime.now(tz=utc)
-        duedate_string = format_datetime(duedate, tzinfo=utc, locale=locale_en)
+        duedate_string = format_datetime(duedate, tzinfo=utc,
+                                         locale=locale_en)
         tc.formvalue('modifymilestone', 'due', duedate_string)
         tc.submit('save')
         tc.url(milestone_url + '$')
         tc.find(name + '(<[^>]*>|\\s)*'+ duedate_string, 's')
 
 
+class TestAdminMilestoneDetailRename(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Admin rename milestone"""
+        name1 = self._tester.create_milestone()
+        name2 = random_unique_camel()
+        tid = self._tester.create_ticket(info={'milestone': name1})
+        milestone_url = self._tester.url + '/admin/ticket/milestones'
+
+        self._tester.go_to_url(milestone_url)
+        tc.follow(name1)
+        tc.url(milestone_url + '/' + name1)
+        tc.formvalue('modifymilestone', 'name', name2)
+        tc.submit('save')
+
+        tc.find(r"Your changes have been saved\.")
+        tc.find(r"\b%s\b" % name2)
+        tc.notfind(r"\b%s\b" % name1)
+        self._tester.go_to_ticket(tid)
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': name2})
+        tc.find('<strong class="trac-field-milestone">Milestone</strong>'
+                '[ \t\n]+changed from <em>%s</em> to <em>%s</em>'
+                % (name1, name2))
+        tc.find("Milestone renamed")
+
+
 class TestAdminMilestoneCompleted(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin milestone completed"""
@@ -642,7 +857,7 @@
         tc.follow(name)
         tc.url(milestone_url + '/' + name)
         tc.formvalue('modifymilestone', 'completed', True)
-        cdate = datetime.now(tz=utc) + timedelta(days=1)
+        cdate = datetime.now(tz=utc) + timedelta(days=2)
         cdate_string = format_date(cdate, tzinfo=localtz, locale=locale_en)
         tc.formvalue('modifymilestone', 'completeddate', cdate_string)
         tc.submit('save')
@@ -657,12 +872,22 @@
         """Admin remove milestone"""
         name = "MilestoneRemove"
         self._tester.create_milestone(name)
-        milestone_url = self._tester.url + "/admin/ticket/milestones"
+        tid = self._tester.create_ticket(info={'milestone': name})
+        milestone_url = self._tester.url + '/admin/ticket/milestones'
+
         tc.go(milestone_url)
         tc.formvalue('milestone_table', 'sel', name)
         tc.submit('remove')
+
         tc.url(milestone_url + '$')
         tc.notfind(name)
+        self._tester.go_to_ticket(tid)
+        tc.find('<th id="h_milestone" class="missing">'
+                '[ \t\n]*Milestone:[ \t\n]*</th>')
+        tc.find('<strong class="trac-field-milestone">Milestone'
+                '</strong>[ \t\n]*<em>%s</em>[ \t\n]*deleted'
+                % name)
+        tc.find("Milestone deleted")
 
 
 class TestAdminMilestoneRemoveMulti(FunctionalTwillTestCaseSetup):
@@ -717,6 +942,14 @@
         self._tester.create_priority()
 
 
+class TestAdminPriorityAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Priority
+        panel."""
+        self.test_authorization('/admin/ticket/priority', 'TICKET_ADMIN',
+                                "Manage Priorities")
+
+
 class TestAdminPriorityDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate priority"""
@@ -846,8 +1079,10 @@
         tc.url(priority_url + '$')
         tc.find(name + '1')
         tc.find(name + '2')
-        tc.formvalue('enumtable', 'value_%s' % (max_priority + 1), str(max_priority + 2))
-        tc.formvalue('enumtable', 'value_%s' % (max_priority + 2), str(max_priority + 1))
+        tc.formvalue('enumtable',
+                     'value_%s' % (max_priority + 1), str(max_priority + 2))
+        tc.formvalue('enumtable',
+                     'value_%s' % (max_priority + 2), str(max_priority + 1))
         tc.submit('apply')
         tc.url(priority_url + '$')
         # Verify that their order has changed.
@@ -873,6 +1108,14 @@
         self._tester.create_resolution()
 
 
+class TestAdminResolutionAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Resolutions
+        panel."""
+        self.test_authorization('/admin/ticket/resolution', 'TICKET_ADMIN',
+                                "Manage Resolutions")
+
+
 class TestAdminResolutionDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate resolution"""
@@ -888,6 +1131,14 @@
         self._tester.create_severity()
 
 
+class TestAdminSeverityAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Severities
+        panel."""
+        self.test_authorization('/admin/ticket/severity', 'TICKET_ADMIN',
+                                "Manage Severities")
+
+
 class TestAdminSeverityDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate severity"""
@@ -903,6 +1154,14 @@
         self._tester.create_type()
 
 
+class TestAdminTypeAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Ticket Types
+        panel."""
+        self.test_authorization('/admin/ticket/type', 'TICKET_ADMIN',
+                                "Manage Ticket Types")
+
+
 class TestAdminTypeDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate type"""
@@ -918,6 +1177,13 @@
         self._tester.create_version()
 
 
+class TestAdminVersionAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Versions panel."""
+        self.test_authorization('/admin/ticket/versions', 'TICKET_ADMIN',
+                                "Manage Versions")
+
+
 class TestAdminVersionDuplicates(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create duplicate version"""
@@ -965,7 +1231,8 @@
         tc.formvalue('modifyversion', 'time', '')
         tc.submit('save')
         tc.url(version_admin + '$')
-        tc.find(name + '(<[^>]*>|\\s)*<[^>]* name="default" value="%s"' % name, 's')
+        tc.find(name + '(<[^>]*>|\\s)*<[^>]* name="default" value="%s"'
+                % name, 's')
 
 
 class TestAdminVersionDetailCancel(FunctionalTwillTestCaseSetup):
@@ -1114,6 +1381,191 @@
                 'file[.]ext [(]WikiStart[)]</a>')
 
 
+class TestMilestone(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Create a milestone."""
+        self._tester.go_to_roadmap()
+        tc.submit(formname='add')
+        tc.url(self._tester.url + '/milestone\?action=new')
+        name = random_unique_camel()
+        due = format_datetime(datetime.now(tz=utc) + timedelta(hours=1),
+                              tzinfo=localtz, locale=locale_en)
+        tc.formvalue('edit', 'name', name)
+        tc.formvalue('edit', 'due', True)
+        tc.formvalue('edit', 'duedate', due)
+        tc.submit('add')
+        tc.url(self._tester.url + '/milestone/' + name + '$')
+        tc.find(r'<h1>Milestone %s</h1>' % name)
+        tc.find(due)
+        self._tester.create_ticket(info={'milestone': name})
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="Due in .+ (.+)">%(name)s</a>'
+                % {'name': name})
+
+
+class TestMilestoneAddAttachment(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Add attachment to a milestone. Test that the attachment
+        button reads 'Attach file' when no files have been attached, and
+        'Attach another file' when there are existing attachments.
+        Feature added in http://trac.edgewall.org/ticket/10281."""
+        name = self._tester.create_milestone()
+        self._tester.go_to_milestone(name)
+        tc.find("Attach file")
+        filename = self._tester.attach_file_to_milestone(name)
+
+        self._tester.go_to_milestone(name)
+        tc.find("Attach another file")
+        tc.find('Attachments <span class="trac-count">\(1\)</span>')
+        tc.find(filename)
+        tc.find('Download all attachments as:\s+<a rel="nofollow" '
+                'href="/zip-attachment/milestone/%s/">.zip</a>' % name)
+
+
+class TestMilestoneClose(FunctionalTwillTestCaseSetup):
+    """Close a milestone and verify that tickets are retargeted
+    to the selected milestone"""
+    def runTest(self):
+        name = self._tester.create_milestone()
+        retarget_to = self._tester.create_milestone()
+        tid1 = self._tester.create_ticket(info={'milestone': name})
+        tid2 = self._tester.create_ticket(info={'milestone': name})
+        tc.formvalue('propertyform', 'action', 'resolve')
+        tc.formvalue('propertyform',
+                     'action_resolve_resolve_resolution', 'fixed')
+        tc.submit('submit')
+
+        self._tester.go_to_milestone(name)
+        completed = format_datetime(datetime.now(tz=utc) - timedelta(hours=1),
+                                    tzinfo=localtz, locale=locale_en)
+        tc.submit(formname='editmilestone')
+        tc.formvalue('edit', 'completed', True)
+        tc.formvalue('edit', 'completeddate', completed)
+        tc.formvalue('edit', 'target', retarget_to)
+        tc.submit('save')
+
+        tc.url(self._tester.url + '/milestone/%s$' % name)
+        tc.find('The open tickets associated with milestone "%s" '
+                'have been retargeted to milestone "%s".'
+                % (name, retarget_to))
+        tc.find("Completed")
+        self._tester.go_to_ticket(tid1)
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': retarget_to})
+        tc.find('changed from <em>%s</em> to <em>%s</em>'
+                % (name, retarget_to))
+        tc.find("Ticket retargeted after milestone closed")
+        self._tester.go_to_ticket(tid2)
+        tc.find('<a class="closed milestone" href="/milestone/%(name)s" '
+                'title="Completed .+ ago (.+)">%(name)s</a>'
+                % {'name': name})
+        tc.notfind('changed from <em>%s</em> to <em>%s</em>'
+                   % (name, retarget_to))
+        tc.notfind("Ticket retargeted after milestone closed")
+
+
+class TestMilestoneDelete(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Delete a milestone and verify that tickets are retargeted
+        to the selected milestone."""
+        def delete_milestone(name, retarget_to=None, tid=None):
+            self._tester.go_to_milestone(name)
+            tc.submit(formname='deletemilestone')
+            if retarget_to is not None:
+                tc.formvalue('edit', 'target', retarget_to)
+            tc.submit('delete', formname='edit')
+
+            tc.url(self._tester.url + '/roadmap')
+            tc.find('The milestone "%s" has been deleted.' % name)
+            tc.notfind('Milestone:.*%s' % name)
+            if retarget_to is not None:
+                tc.find('Milestone:.*%s' % retarget_to)
+            retarget_notice = 'The tickets associated with milestone "%s" ' \
+                              'have been retargeted to milestone "%s".' \
+                              % (name, str(retarget_to))
+            if tid is not None:
+                tc.find(retarget_notice)
+                self._tester.go_to_ticket(tid)
+                tc.find('Changed[ \t\n]+<a .*>\d+ seconds? ago</a>'
+                        '[ \t\n]+by admin')
+                if retarget_to is not None:
+                    tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                            'title="No date set">%(name)s</a>'
+                            % {'name': retarget_to})
+                    tc.find('<strong class="trac-field-milestone">Milestone'
+                            '</strong>[ \t\n]+changed from <em>%s</em> to '
+                            '<em>%s</em>' % (name, retarget_to))
+                else:
+                    tc.find('<th id="h_milestone" class="missing">'
+                            '[ \t\n]*Milestone:[ \t\n]*</th>')
+                    tc.find('<strong class="trac-field-milestone">Milestone'
+                            '</strong>[ \t\n]*<em>%s</em>[ \t\n]*deleted'
+                            % name)
+                tc.find("Ticket retargeted after milestone deleted")
+            else:
+                tc.notfind(retarget_notice)
+
+        # No tickets associated with milestone to be retargeted
+        name = self._tester.create_milestone()
+        delete_milestone(name)
+
+        # Don't select a milestone to retarget to
+        name = self._tester.create_milestone()
+        tid = self._tester.create_ticket(info={'milestone': name})
+        delete_milestone(name, tid=tid)
+
+        # Select a milestone to retarget to
+        name = self._tester.create_milestone()
+        retarget_to = self._tester.create_milestone()
+        tid = self._tester.create_ticket(info={'milestone': name})
+        delete_milestone(name, retarget_to, tid)
+
+        # Just navigate to the page and select cancel
+        name = self._tester.create_milestone()
+        tid = self._tester.create_ticket(info={'milestone': name})
+
+        self._tester.go_to_milestone(name)
+        tc.submit(formname='deletemilestone')
+        tc.submit('cancel', formname='edit')
+
+        tc.url(self._tester.url + '/milestone/%s' % name)
+        tc.notfind('The milestone "%s" has been deleted.' % name)
+        tc.notfind('The tickets associated with milestone "%s" '
+                   'have been retargeted to milestone' % name)
+        self._tester.go_to_ticket(tid)
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': name})
+        tc.notfind('<strong class="trac-field-milestone">Milestone</strong>'
+                   '[ \t\n]*<em>%s</em>[ \t\n]*deleted' % name)
+        tc.notfind("Ticket retargeted after milestone deleted<br />")
+
+
+class TestMilestoneRename(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Rename a milestone and verify that the rename is shown in the
+        change history for the associated tickets."""
+        name = self._tester.create_milestone()
+        new_name = random_unique_camel()
+        tid = self._tester.create_ticket(info={'milestone': name})
+
+        self._tester.go_to_milestone(name)
+        tc.submit(formname='editmilestone')
+        tc.formvalue('edit', 'name', new_name)
+        tc.submit('save')
+
+        tc.url(self._tester.url + '/milestone/' + new_name)
+        tc.find("Your changes have been saved.")
+        tc.find(r"<h1>Milestone %s</h1>" % new_name)
+        self._tester.go_to_ticket(tid)
+        tc.find('Changed[ \t\n]+<a .*>\d+ seconds? ago</a>[ \t\n]+by admin')
+        tc.find('<a class="milestone" href="/milestone/%(name)s" '
+                'title="No date set">%(name)s</a>' % {'name': new_name})
+        tc.find('<strong class="trac-field-milestone">Milestone</strong>'
+                '[ \t\n]+changed from <em>%s</em> to <em>%s</em>'
+                % (name, new_name))
+        tc.find("Milestone renamed")
+
+
 class RegressionTestRev5665(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Admin create version without release time (r5665)"""
@@ -1128,7 +1580,6 @@
         env.config.set('ticket-custom', 'custfield.label', 'Custom Field')
         env.config.save()
         try:
-            self._testenv.restart()
             self._tester.go_to_query()
             tc.find('<label>( |\\n)*<input[^<]*value="custfield"'
                     '[^<]*/>( |\\n)*Custom Field( |\\n)*</label>', 's')
@@ -1136,23 +1587,21 @@
             pass
             #env.config.set('ticket', 'restrict_owner', 'no')
             #env.config.save()
-            #self._testenv.restart()
 
 
 class RegressionTestTicket4447(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/4447"""
-        ticketid = self._tester.create_ticket(summary="Hello World")
-
         env = self._testenv.get_trac_environment()
         env.config.set('ticket-custom', 'newfield', 'text')
         env.config.set('ticket-custom', 'newfield.label',
                        'Another Custom Field')
         env.config.save()
-        self._testenv.restart()
-        self._tester.go_to_ticket(ticketid)
+
+        ticketid = self._tester.create_ticket(summary="Hello World")
         self._tester.add_comment(ticketid)
-        tc.notfind('deleted')
+        tc.notfind('<strong class="trac-field-newfield">Another Custom Field'
+                   '</strong>[ \t\n]+<em></em>[ \t\n]+deleted')
         tc.notfind('set to')
 
 
@@ -1163,25 +1612,27 @@
         env.config.set('ticket', 'restrict_owner', 'yes')
         env.config.save()
         try:
-            self._testenv.restart()
             # Make sure 'user' has logged in.
             self._tester.go_to_front()
             self._tester.logout()
             self._tester.login('user')
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('joe')
+            self._tester.go_to_front()
             self._tester.logout()
             self._tester.login('admin')
-            ticket_id = self._tester.create_ticket()
-            self._tester.go_to_ticket(ticket_id)
+            self._tester.create_ticket()
             tc.formvalue('propertyform', 'action', 'reassign')
             tc.find('reassign_reassign_owner')
-            tc.formvalue('propertyform', 'action_reassign_reassign_owner', 'user')
+            tc.formvalue('propertyform', 'action_reassign_reassign_owner',
+                         'user')
             tc.submit('submit')
         finally:
             # Undo the config change for now since this (failing)
             # regression test causes problems for later tests.
             env.config.set('ticket', 'restrict_owner', 'no')
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket4630b(FunctionalTestCaseSetup):
@@ -1195,7 +1646,7 @@
         users = perm.get_users_with_permission('TRAC_ADMIN')
         self.assertEqual(users, ['admin'])
         users = perm.get_users_with_permission('TICKET_MODIFY')
-        self.assertEqual(users, ['admin', 'user'])
+        self.assertEqual(sorted(users), ['admin', 'joe', 'user'])
 
 
 class RegressionTestTicket5022(FunctionalTwillTestCaseSetup):
@@ -1217,14 +1668,13 @@
         env = self._testenv.get_trac_environment()
         env.config.set('ticket', 'restrict_owner', 'yes')
         env.config.save()
-        self._testenv.restart()
 
         self._tester.go_to_front()
         self._tester.logout()
 
         test_users = ['alice', 'bob', 'jane', 'john', 'charlie', 'alan',
                       'zorro']
-        # Apprently it takes a sec for the new user to be recognized by the
+        # Apparently it takes a sec for the new user to be recognized by the
         # environment.  So we add all the users, then log in as the users
         # in a second loop.  This should be faster than adding a sleep(1)
         # between the .adduser and .login steps.
@@ -1232,17 +1682,17 @@
             self._testenv.adduser(user)
         for user in test_users:
             self._tester.login(user)
+            self._tester.go_to_front()
             self._tester.logout()
 
         self._tester.login('admin')
 
-        ticketid = self._tester.create_ticket("regression test 5394a")
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5394a")
 
         options = 'id="action_reassign_reassign_owner">' + \
             ''.join(['<option[^>]*>%s</option>' % user for user in
-                     sorted(test_users + ['admin', 'user'])])
-        tc.find(options, 's')
+                     sorted(test_users + ['admin', 'joe', 'user'])])
+        tc.find(to_utf8(options), 's')
         # We don't have a good way to fully delete a user from the Trac db.
         # Once we do, we may want to cleanup our list of users here.
 
@@ -1285,8 +1735,7 @@
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 a
         Open ticket, component changed, owner not changed"""
-        ticketid = self._tester.create_ticket("regression test 5497a")
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497a")
         tc.formvalue('propertyform', 'field-component', 'regression5497')
         tc.submit('submit')
         tc.find(regex_owned_by('user'))
@@ -1295,11 +1744,11 @@
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 b
         Open ticket, component changed, owner changed"""
-        ticketid = self._tester.create_ticket("regression test 5497b")
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497b")
         tc.formvalue('propertyform', 'field-component', 'regression5497')
         tc.formvalue('propertyform', 'action', 'reassign')
-        tc.formvalue('propertyform', 'action_reassign_reassign_owner', 'admin')
+        tc.formvalue('propertyform', 'action_reassign_reassign_owner',
+                     'admin')
         tc.submit('submit')
         tc.notfind(regex_owned_by('user'))
         tc.find(regex_owned_by('admin'))
@@ -1308,18 +1757,17 @@
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 c
         New ticket, component changed, owner not changed"""
-        ticketid = self._tester.create_ticket("regression test 5497c",
-            {'component':'regression5497'})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497c",
+                                   {'component':'regression5497'})
         tc.find(regex_owned_by('user'))
 
 class RegressionTestTicket5497d(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5497 d
         New ticket, component changed, owner changed"""
-        ticketid = self._tester.create_ticket("regression test 5497d",
-            {'component':'regression5497', 'owner':'admin'})
-        self._tester.go_to_ticket(ticketid)
+        self._tester.create_ticket("regression test 5497d",
+                                   {'component':'regression5497',
+                                    'owner':'admin'})
         tc.find(regex_owned_by('admin'))
 
 
@@ -1328,15 +1776,16 @@
         """Test for regression of http://trac.edgewall.org/ticket/5602"""
         # Create a set of tickets, and assign them all to a milestone
         milestone = self._tester.create_milestone()
-        ids = [self._tester.create_ticket() for x in range(5)]
-        [self._tester.ticket_set_milestone(x, milestone) for x in ids]
+        ids = [self._tester.create_ticket(info={'milestone': milestone})
+               for x in range(5)]
         # Need a ticket in each state: new, assigned, accepted, closed,
         # reopened
         # leave ids[0] as new
         # make ids[1] be assigned
         self._tester.go_to_ticket(ids[1])
         tc.formvalue('propertyform', 'action', 'reassign')
-        tc.formvalue('propertyform', 'action_reassign_reassign_owner', 'admin')
+        tc.formvalue('propertyform', 'action_reassign_reassign_owner',
+                     'admin')
         tc.submit('submit')
         # make ids[2] be accepted
         self._tester.go_to_ticket(ids[2])
@@ -1345,12 +1794,14 @@
         # make ids[3] be closed
         self._tester.go_to_ticket(ids[3])
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('submit')
         # make ids[4] be reopened
         self._tester.go_to_ticket(ids[4])
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('submit')
         # FIXME: we have to wait a second to avoid "IntegrityError: columns
         # ticket, time, field are not unique"
@@ -1380,9 +1831,10 @@
 class RegressionTestTicket5687(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/5687"""
+        self._tester.go_to_front()
         self._tester.logout()
         self._tester.login('user')
-        ticketid = self._tester.create_ticket()
+        self._tester.create_ticket()
         self._tester.logout()
         self._tester.login('admin')
 
@@ -1406,10 +1858,10 @@
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/6048"""
         # Setup the DeleteTicket plugin
-        plugin = open(os.path.join(self._testenv.command_cwd, 'sample-plugins',
+        plugin = open(os.path.join(self._testenv.trac_src, 'sample-plugins',
                                    'workflow', 'DeleteTicket.py')).read()
-        open(os.path.join(self._testenv.tracdir, 'plugins', 'DeleteTicket.py'),
-             'w').write(plugin)
+        open(os.path.join(self._testenv.tracdir, 'plugins',
+                          'DeleteTicket.py'), 'w').write(plugin)
         env = self._testenv.get_trac_environment()
         prevconfig = env.config.get('ticket', 'workflow')
         env.config.set('ticket', 'workflow',
@@ -1418,8 +1870,7 @@
         env = self._testenv.get_trac_environment() # reload environment
 
         # Create a ticket and delete it
-        ticket_id = self._tester.create_ticket(
-            summary='RegressionTestTicket6048')
+        ticket_id = self._tester.create_ticket('RegressionTestTicket6048')
         # (Create a second ticket so that the ticket id does not get reused
         # and confuse the tester object.)
         self._tester.create_ticket(summary='RegressionTestTicket6048b')
@@ -1454,10 +1905,7 @@
         env.config.save()
 
         try:
-            self._testenv.restart()
-
-            ticket_id = self._tester.create_ticket("RegressionTestTicket6747")
-            self._tester.go_to_ticket(ticket_id)
+            self._tester.create_ticket("RegressionTestTicket6747")
             tc.find("a_specified_owner")
             tc.notfind("a_specified_owneras")
 
@@ -1468,7 +1916,6 @@
                            'set_resolution')
             env.config.remove('ticket-workflow', 'resolve.set_owner')
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket6879a(FunctionalTwillTestCaseSetup):
@@ -1479,10 +1926,10 @@
         be those for the close status.
         """
         # create a ticket, then preview resolving the ticket twice
-        ticket_id = self._tester.create_ticket("RegressionTestTicket6879 a")
-        self._tester.go_to_ticket(ticket_id)
+        self._tester.create_ticket("RegressionTestTicket6879 a")
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('preview')
         tc.formvalue('propertyform', 'action', 'resolve')
         tc.submit('preview')
@@ -1496,10 +1943,10 @@
         be those for the close status.
         """
         # create a ticket, then preview resolving the ticket twice
-        ticket_id = self._tester.create_ticket("RegressionTestTicket6879 b")
-        self._tester.go_to_ticket(ticket_id)
+        self._tester.create_ticket("RegressionTestTicket6879 b")
         tc.formvalue('propertyform', 'action', 'resolve')
-        tc.formvalue('propertyform', 'action_resolve_resolve_resolution', 'fixed')
+        tc.formvalue('propertyform', 'action_resolve_resolve_resolution',
+                     'fixed')
         tc.submit('preview')
         tc.formvalue('propertyform', 'action', 'resolve')
         tc.submit('submit')
@@ -1510,7 +1957,7 @@
         """Test for regression of http://trac.edgewall.org/ticket/6912 a"""
         try:
             self._tester.create_component(name='RegressionTestTicket6912a',
-                                          user='')
+                                          owner='')
         except twill.utils.ClientForm.ItemNotFoundError, e:
             raise twill.errors.TwillAssertionError(e)
 
@@ -1519,7 +1966,7 @@
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/6912 b"""
         self._tester.create_component(name='RegressionTestTicket6912b',
-                                      user='admin')
+                                      owner='admin')
         tc.follow('RegressionTestTicket6912b')
         try:
             tc.formvalue('modcomp', 'owner', '')
@@ -1532,7 +1979,8 @@
 
 class RegressionTestTicket7821group(FunctionalTwillTestCaseSetup):
     def runTest(self):
-        """Test for regression of http://trac.edgewall.org/ticket/7821 group"""
+        """Test for regression of http://trac.edgewall.org/ticket/7821 group.
+        """
         env = self._testenv.get_trac_environment()
         saved_default_query = env.config.get('query', 'default_query')
         default_query = 'status!=closed&order=status&group=status&max=42' \
@@ -1541,7 +1989,6 @@
         env.config.set('query', 'default_query', default_query)
         env.config.save()
         try:
-            self._testenv.restart()
             self._tester.create_ticket('RegressionTestTicket7821 group')
             self._tester.go_to_query()
             # $USER
@@ -1573,7 +2020,6 @@
         finally:
             env.config.set('query', 'default_query', saved_default_query)
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket7821var(FunctionalTwillTestCaseSetup):
@@ -1587,7 +2033,6 @@
         env.config.set('ticket', 'restrict_owner', 'no')
         env.config.save()
         try:
-            self._testenv.restart()
             self._tester.create_ticket('RegressionTestTicket7821 var')
             self._tester.go_to_query()
             # $USER in default_query
@@ -1606,7 +2051,6 @@
             env.config.set('query', 'default_query', saved_default_query)
             env.config.set('ticket', 'restrict_owner', saved_restrict_owner)
             env.config.save()
-            self._testenv.restart()
 
 
 class RegressionTestTicket8247(FunctionalTwillTestCaseSetup):
@@ -1627,7 +2071,7 @@
         tc.find('<strong class="trac-field-milestone">Milestone</strong>'
                 '[ \n\t]*<em>%s</em> deleted' % name)
         tc.find('Changed <a.* ago</a> by admin')
-        tc.notfind('anonymous')
+        tc.notfind('</a> ago by anonymous')
 
 
 class RegressionTestTicket8861(FunctionalTwillTestCaseSetup):
@@ -1665,23 +2109,197 @@
 class RegressionTestTicket9981(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/9981"""
-        ticketid = self._tester.create_ticket()
-        self._tester.add_comment(ticketid)
+        tid1 = self._tester.create_ticket()
+        self._tester.add_comment(tid1)
         tc.formvalue('propertyform', 'action', 'resolve')
         tc.submit('submit')
-        comment = '[ticket:%s#comment:1]' % ticketid
-        self._tester.add_comment(ticketid, comment=comment)
-        self._tester.go_to_ticket(ticketid)
-        tc.find('class="closed ticket".*ticket/%s#comment:1"' % ticketid)
+        tid2 = self._tester.create_ticket()
+        comment = '[comment:1:ticket:%s]' % tid1
+        self._tester.add_comment(tid2, comment)
+        self._tester.go_to_ticket(tid2)
+        tc.find('<a class="closed ticket"[ \t\n]+'
+                'href="/ticket/%(num)s#comment:1"[ \t\n]+'
+                'title="Comment 1 for Ticket #%(num)s"' % {'num': tid1})
+
+
+class RegressionTestTicket11028(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11028"""
+        self._tester.go_to_roadmap()
+
+        try:
+            # Check that a milestone is found on the roadmap,
+            # even for anonymous
+            tc.find('<a href="/milestone/milestone1">[ \n\t]*'
+                    'Milestone: <em>milestone1</em>[ \n\t]*</a>')
+            self._tester.logout()
+            tc.find('<a href="/milestone/milestone1">[ \n\t]*'
+                    'Milestone: <em>milestone1</em>[ \n\t]*</a>')
+
+            # Check that no milestones are found on the roadmap when
+            # MILESTONE_VIEW is revoked
+            self._testenv.revoke_perm('anonymous', 'MILESTONE_VIEW')
+            tc.reload()
+            tc.notfind('Milestone: <em>milestone\d+</em>')
+
+            # Check that roadmap can't be viewed without ROADMAP_VIEW
+
+            self._testenv.revoke_perm('anonymous', 'ROADMAP_VIEW')
+            self._tester.go_to_url(self._tester.url + '/roadmap')
+            tc.find('<h1>Error: Forbidden</h1>')
+        finally:
+            # Restore state prior to test execution
+            self._tester.login('admin')
+            self._testenv.grant_perm('anonymous',
+                                     ('ROADMAP_VIEW', 'MILESTONE_VIEW'))
+
+
+class RegressionTestTicket11152(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11152"""
+        # Check that "View Tickets" mainnav entry links to the report page
+        self._tester.go_to_view_tickets()
+
+        # Check that "View Tickets" mainnav entry links to the query page
+        # when the user doesn't have REPORT_VIEW, and that the mainnav entry
+        # is not present when the user doesn't have TICKET_VIEW.
+        try:
+            self._tester.logout()
+            self._testenv.revoke_perm('anonymous', 'REPORT_VIEW')
+            self._tester.go_to_view_tickets('query')
+
+            self._testenv.revoke_perm('anonymous', 'TICKET_VIEW')
+            self._tester.go_to_front()
+            tc.notfind('\\bView Tickets\\b')
+        finally:
+            self._testenv.grant_perm('anonymous',
+                                     ('REPORT_VIEW', 'TICKET_VIEW'))
+            self._tester.login('admin')
+
+        # Disable the ReportModule component and check that "View Tickets"
+        # mainnav entry links to the `/query` page.
+        env = self._testenv.get_trac_environment()
+        env.config.set('components', 'trac.ticket.report.ReportModule',
+                       'disabled')
+        env.config.save()
+
+        try:
+            self._tester.go_to_view_tickets('query')
+        finally:
+            env.config.remove('components', 'trac.ticket.report.ReportModule')
+            env.config.save()
+
+        # Disable the QueryModule component and check that "View Tickets"
+        # mainnav entry links to the `/report` page
+        env.config.set('components', 'trac.ticket.query.QueryModule',
+                       'disabled')
+        env.config.save()
+
+        try:
+            self._tester.go_to_view_tickets('report')
+            tc.notfind('<li class="last first">Available Reports</li>')
+        finally:
+            env.config.remove('components', 'trac.ticket.query.QueryModule')
+            env.config.save()
+
+
+class RegressionTestTicket11176(FunctionalTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11176
+        Fine-grained permission checks should be enforced on the Report list
+        page, the report pages and query pages."""
+        self._testenv.enable_authz_permpolicy("""
+            [report:1]
+            anonymous = REPORT_VIEW
+            [report:2]
+            anonymous = REPORT_VIEW
+            [report:*]
+            anonymous =
+        """)
+        self._tester.go_to_front()
+        self._tester.logout()
+        self._tester.go_to_view_tickets()
+        try:
+            # Check that permissions are enforced on the report list page
+            tc.find(r'<a title="View report" '
+                    r'href="/report/1">[ \n\t]*<em>\{1\}</em>')
+            tc.find(r'<a title="View report" '
+                    r'href="/report/2">[ \n\t]*<em>\{2\}</em>')
+            for report_num in range(3, 9):
+                tc.notfind(r'<a title="View report" '
+                           r'href="/report/%(num)s">[ \n\t]*'
+                           r'<em>\{%(num)s\}</em>' % {'num': report_num})
+            # Check that permissions are enforced on the report pages
+            tc.go(self._tester.url + '/report/1')
+            tc.find(r'<h1>\{1\} Active Tickets[ \n\t]*'
+                    r'(<span class="numrows">\(\d+ matches\)</span>)?'
+                    r'[ \n\t]*</h1>')
+            tc.go(self._tester.url + '/report/2')
+            tc.find(r'<h1>\{2\} Active Tickets by Version[ \n\t]*'
+                    r'(<span class="numrows">\(\d+ matches\)</span>)?'
+                    r'[ \n\t]*</h1>')
+            for report_num in range(3, 9):
+                tc.go(self._tester.url + '/report/%d' % report_num)
+                tc.find(r'<h1>Error: Forbidden</h1>')
+            # Check that permissions are enforced on the query pages
+            tc.go(self._tester.url + '/query?report=1')
+            tc.find(r'<h1>Active Tickets '
+                    r'<span class="numrows">\(\d+ matches\)</span></h1>')
+            tc.go(self._tester.url + '/query?report=2')
+            tc.find(r'<h1>Active Tickets by Version '
+                    r'<span class="numrows">\(\d+ matches\)</span></h1>')
+            for report_num in range(3, 9):
+                tc.go(self._tester.url + '/query?report=%d' % report_num)
+                tc.find(r'<h1>Error: Forbidden</h1>')
+        finally:
+            self._tester.login('admin')
+            self._testenv.disable_authz_permpolicy()
+
+
+class RegressionTestTicket11590(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11590"""
+        report_id = self._tester.create_report('#11590', 'SELECT 1',
+                                               '[./ this report]')
+        self._tester.go_to_view_tickets()
+        tc.notfind(internal_error)
+        tc.find('<a class="report" href="[^>"]*?/report/%s">this report</a>' %
+                report_id)
+
+
+class RegressionTestTicket11618(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11618
+        fix for malformed `readonly="True"` attribute in milestone admin page
+        """
+        name = "11618Milestone"
+        self._tester.create_milestone(name)
+        try:
+            self._testenv.grant_perm('user', 'TICKET_ADMIN')
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('user')
+            tc.go(self._tester.url + "/admin/ticket/milestones/" + name)
+            tc.notfind('No administration panels available')
+            tc.find(' readonly="readonly"')
+            tc.notfind(' readonly="True"')
+        finally:
+            self._testenv.revoke_perm('user', 'TICKET_ADMIN')
+            self._tester.go_to_front()
+            self._tester.logout()
+            self._tester.login('admin')
 
 
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
     suite.addTest(TestTickets())
+    suite.addTest(TestTicketMaxSummarySize())
+    suite.addTest(TestTicketAddAttachment())
     suite.addTest(TestTicketPreview())
     suite.addTest(TestTicketNoSummary())
+    suite.addTest(TestTicketManipulator())
     suite.addTest(TestTicketAltFormats())
     suite.addTest(TestTicketCSVFormat())
     suite.addTest(TestTicketTabFormat())
@@ -1691,6 +2309,7 @@
     suite.addTest(TestTicketHistory())
     suite.addTest(TestTicketHistoryDiff())
     suite.addTest(TestTicketQueryLinks())
+    suite.addTest(TestTicketQueryLinksQueryModuleDisabled())
     suite.addTest(TestTicketQueryOrClause())
     suite.addTest(TestTicketCustomFieldTextNoFormat())
     suite.addTest(TestTicketCustomFieldTextWikiFormat())
@@ -1699,19 +2318,23 @@
     suite.addTest(TestTicketCustomFieldTextReferenceFormat())
     suite.addTest(TestTicketCustomFieldTextListFormat())
     suite.addTest(RegressionTestTicket10828())
-    suite.addTest(TestTimelineTicketDetails())
+    suite.addTest(TestTicketTimeline())
     suite.addTest(TestAdminComponent())
+    suite.addTest(TestAdminComponentAuthorization())
     suite.addTest(TestAdminComponentDuplicates())
     suite.addTest(TestAdminComponentRemoval())
     suite.addTest(TestAdminComponentNonRemoval())
     suite.addTest(TestAdminComponentDefault())
     suite.addTest(TestAdminComponentDetail())
+    suite.addTest(TestAdminComponentNoneDefined())
     suite.addTest(TestAdminMilestone())
+    suite.addTest(TestAdminMilestoneAuthorization())
     suite.addTest(TestAdminMilestoneSpace())
     suite.addTest(TestAdminMilestoneDuplicates())
     suite.addTest(TestAdminMilestoneDetail())
     suite.addTest(TestAdminMilestoneDue())
     suite.addTest(TestAdminMilestoneDetailDue())
+    suite.addTest(TestAdminMilestoneDetailRename())
     suite.addTest(TestAdminMilestoneCompleted())
     suite.addTest(TestAdminMilestoneCompletedFuture())
     suite.addTest(TestAdminMilestoneRemove())
@@ -1719,6 +2342,7 @@
     suite.addTest(TestAdminMilestoneNonRemoval())
     suite.addTest(TestAdminMilestoneDefault())
     suite.addTest(TestAdminPriority())
+    suite.addTest(TestAdminPriorityAuthorization())
     suite.addTest(TestAdminPriorityModify())
     suite.addTest(TestAdminPriorityRemove())
     suite.addTest(TestAdminPriorityRemoveMulti())
@@ -1728,12 +2352,16 @@
     suite.addTest(TestAdminPriorityRenumber())
     suite.addTest(TestAdminPriorityRenumberDup())
     suite.addTest(TestAdminResolution())
+    suite.addTest(TestAdminResolutionAuthorization())
     suite.addTest(TestAdminResolutionDuplicates())
     suite.addTest(TestAdminSeverity())
+    suite.addTest(TestAdminSeverityAuthorization())
     suite.addTest(TestAdminSeverityDuplicates())
     suite.addTest(TestAdminType())
+    suite.addTest(TestAdminTypeAuthorization())
     suite.addTest(TestAdminTypeDuplicates())
     suite.addTest(TestAdminVersion())
+    suite.addTest(TestAdminVersionAuthorization())
     suite.addTest(TestAdminVersionDuplicates())
     suite.addTest(TestAdminVersionDetail())
     suite.addTest(TestAdminVersionDetailTime())
@@ -1744,6 +2372,11 @@
     suite.addTest(TestAdminVersionDefault())
     suite.addTest(TestNewReport())
     suite.addTest(TestReportRealmDecoration())
+    suite.addTest(TestMilestone())
+    suite.addTest(TestMilestoneAddAttachment())
+    suite.addTest(TestMilestoneClose())
+    suite.addTest(TestMilestoneDelete())
+    suite.addTest(TestMilestoneRename())
     suite.addTest(RegressionTestRev5665())
     suite.addTest(RegressionTestRev5994())
 
@@ -1773,6 +2406,14 @@
     suite.addTest(RegressionTestTicket8861())
     suite.addTest(RegressionTestTicket9084())
     suite.addTest(RegressionTestTicket9981())
+    suite.addTest(RegressionTestTicket11028())
+    suite.addTest(RegressionTestTicket11152())
+    suite.addTest(RegressionTestTicket11590())
+    suite.addTest(RegressionTestTicket11618())
+    if ConfigObj:
+        suite.addTest(RegressionTestTicket11176())
+    else:
+        print "SKIP: RegressionTestTicket11176 (ConfigObj not installed)"
 
     return suite
 
diff --git a/trac/trac/ticket/tests/model.py b/trac/trac/ticket/tests/model.py
index bda9481..9b74295 100644
--- a/trac/trac/ticket/tests/model.py
+++ b/trac/trac/ticket/tests/model.py
@@ -1,23 +1,37 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
+
 from __future__ import with_statement
 
 from datetime import datetime, timedelta
-import os.path
 from StringIO import StringIO
 import tempfile
 import shutil
 import unittest
 
+import trac.tests.compat
 from trac import core
 from trac.attachment import Attachment
 from trac.core import TracError, implements
 from trac.resource import ResourceNotFound
+from trac.test import EnvironmentStub
 from trac.ticket.model import (
     Ticket, Component, Milestone, Priority, Type, Version
 )
+from trac.ticket.roadmap import MilestoneModule
 from trac.ticket.api import (
     IMilestoneChangeListener, ITicketChangeListener, TicketSystem
 )
-from trac.test import EnvironmentStub
 from trac.tests.resource import TestResourceChangeListener
 from trac.util.datefmt import from_utimestamp, to_utimestamp, utc
 
@@ -41,6 +55,36 @@
         self.action = 'deleted'
         self.ticket = ticket
 
+    # the listener has no ticket_comment_modified and ticket_change_deleted
+
+
+class TestTicketChangeListener_2(core.Component):
+    implements(ITicketChangeListener)
+
+    def ticket_created(self, ticket):
+        pass
+
+    def ticket_changed(self, ticket, comment, author, old_values):
+        pass
+
+    def ticket_deleted(self, ticket):
+        pass
+
+    def ticket_comment_modified(self, ticket, cdate, author, comment,
+                                old_comment):
+        self.action = 'comment_modified'
+        self.ticket = ticket
+        self.cdate = cdate
+        self.author = author
+        self.comment = comment
+        self.old_comment = old_comment
+
+    def ticket_change_deleted(self, ticket, cdate, changes):
+        self.action = 'change_deleted'
+        self.ticket = ticket
+        self.cdate = cdate
+        self.changes = changes
+
 
 class TicketTestCase(unittest.TestCase):
 
@@ -118,9 +162,9 @@
         log = ticket3.get_changelog()
         self.assertEqual(len(log), 3)
         ok_vals = ['foo', 'summary', 'comment']
-        self.failUnless(log[0][2] in ok_vals)
-        self.failUnless(log[1][2] in ok_vals)
-        self.failUnless(log[2][2] in ok_vals)
+        self.assertIn(log[0][2], ok_vals)
+        self.assertIn(log[1][2], ok_vals)
+        self.assertIn(log[2][2], ok_vals)
 
     def test_create_ticket_5(self):
         ticket3 = self._modify_a_ticket()
@@ -157,7 +201,7 @@
         ticket.save_changes()
 
         for change in ticket.get_changelog():
-            self.assertEqual(None, change[1])
+            self.assertIsNone(change[1])
 
     def test_comment_with_whitespace_only_is_not_saved(self):
         ticket = Ticket(self.env)
@@ -310,7 +354,7 @@
         self.assertEqual('john', ticket['reporter'])
 
         # An unknown field
-        assert ticket['bar'] is None
+        self.assertIsNone(ticket['bar'])
 
         # Custom field
         self.assertEqual('bar', ticket['foo'])
@@ -599,6 +643,19 @@
             self.assertEqual((i, t[i], 'joe (%d)' % i,
                              'Comment 1 (%d)' % i), history[i])
 
+    def test_change_listener_comment_modified(self):
+        listener = TestTicketChangeListener_2(self.env)
+        ticket = Ticket(self.env, self.id)
+        ticket.modify_comment(cdate=self.t2, author='jack',
+                              comment='New Comment 2', when=datetime.now(utc))
+
+        self.assertEqual('comment_modified', listener.action)
+        self.assertEqual(ticket, listener.ticket)
+        self.assertEqual(self.t2, listener.cdate)
+        self.assertEqual('jack', listener.author)
+        self.assertEqual('New Comment 2', listener.comment)
+        self.assertEqual('Comment 2', listener.old_comment)
+
 
 class TicketCommentDeleteTestCase(TicketCommentTestCase):
 
@@ -632,8 +689,8 @@
         ticket.delete_change(cnum=4, when=t)
         self.assertEqual('a, b', ticket['keywords'])
         self.assertEqual('change3', ticket['foo'])
-        self.assertEqual(None, ticket.get_change(cnum=4))
-        self.assertNotEqual(None, ticket.get_change(cnum=3))
+        self.assertIsNone(ticket.get_change(cnum=4))
+        self.assertIsNotNone(ticket.get_change(cnum=3))
         self.assertEqual(t, ticket.time_changed)
 
     def test_delete_last_comment_when_custom_field_gone(self):
@@ -651,13 +708,13 @@
         ticket.delete_change(cnum=4, when=t)
         self.assertEqual('a, b', ticket['keywords'])
         # 'foo' is no longer defined for the ticket
-        self.assertEqual(None, ticket['foo'])
+        self.assertIsNone(ticket['foo'])
         # however, 'foo=change3' is still in the database
         self.assertEqual([('change3',)], self.env.db_query("""
             SELECT value FROM ticket_custom WHERE ticket=%s AND name='foo'
             """, (self.id,)))
-        self.assertEqual(None, ticket.get_change(cnum=4))
-        self.assertNotEqual(None, ticket.get_change(cnum=3))
+        self.assertIsNone(ticket.get_change(cnum=4))
+        self.assertIsNotNone(ticket.get_change(cnum=3))
         self.assertEqual(t, ticket.time_changed)
 
     def test_delete_last_comment_by_date(self):
@@ -668,8 +725,8 @@
         ticket.delete_change(cdate=self.t4, when=t)
         self.assertEqual('a, b', ticket['keywords'])
         self.assertEqual('change3', ticket['foo'])
-        self.assertEqual(None, ticket.get_change(cdate=self.t4))
-        self.assertNotEqual(None, ticket.get_change(cdate=self.t3))
+        self.assertIsNone(ticket.get_change(cdate=self.t4))
+        self.assertIsNotNone(ticket.get_change(cdate=self.t3))
         self.assertEqual(t, ticket.time_changed)
 
     def test_delete_mid_comment(self):
@@ -680,7 +737,7 @@
             foo=dict(author='joe', old='change3', new='change4'))
         t = datetime.now(utc)
         ticket.delete_change(cnum=3, when=t)
-        self.assertEqual(None, ticket.get_change(cnum=3))
+        self.assertIsNone(ticket.get_change(cnum=3))
         self.assertEqual('a', ticket['keywords'])
         self.assertChange(ticket, 4, self.t4, 'joe',
             comment=dict(author='joe', old='4', new='Comment 4'),
@@ -696,7 +753,7 @@
             foo=dict(author='joe', old='change3', new='change4'))
         t = datetime.now(utc)
         ticket.delete_change(cdate=self.t3, when=t)
-        self.assertEqual(None, ticket.get_change(cdate=self.t3))
+        self.assertIsNone(ticket.get_change(cdate=self.t3))
         self.assertEqual('a', ticket['keywords'])
         self.assertChange(ticket, 4, self.t4, 'joe',
             comment=dict(author='joe', old='4', new='Comment 4'),
@@ -718,7 +775,7 @@
             keywords=dict(author='joe', old='1, 2', new='a'),
             foo=dict(author='joe', old='change3', new='change4'))
         ticket.delete_change(3)
-        self.assertEqual(None, ticket.get_change(3))
+        self.assertIsNone(ticket.get_change(3))
         self.assertEqual('a', ticket['keywords'])
         self.assertChange(ticket, 4, self.t4, 'joe',
             comment=dict(author='joe', old='4', new='Comment 4'),
@@ -734,6 +791,25 @@
         ticket.delete_change(1, when=t)
         self.assertEqual(t, ticket.time_changed)
 
+    def test_ticket_change_deleted(self):
+        listener = TestTicketChangeListener_2(self.env)
+        ticket = Ticket(self.env, self.id)
+
+        ticket.delete_change(cdate=self.t3, when=datetime.now(utc))
+        self.assertEqual('change_deleted', listener.action)
+        self.assertEqual(ticket, listener.ticket)
+        self.assertEqual(self.t3, listener.cdate)
+        self.assertEqual(dict(keywords=('a, b, c', 'a, b'),
+                              foo=('change2', 'change3')),
+                         listener.changes)
+
+        ticket.delete_change(cnum=2, when=datetime.now(utc))
+        self.assertEqual('change_deleted', listener.action)
+        self.assertEqual(ticket, listener.ticket)
+        self.assertEqual(self.t2, listener.cdate)
+        self.assertEqual(dict(owner=('john', 'jack'),
+                              foo=('change 1', 'change2')),
+                         listener.changes)
 
 class EnumTestCase(unittest.TestCase):
 
@@ -752,14 +828,14 @@
         prio = Priority(self.env)
         prio.name = 'foo'
         prio.insert()
-        self.assertEqual(True, prio.exists)
+        self.assertTrue(prio.exists)
 
     def test_priority_insert_with_value(self):
         prio = Priority(self.env)
         prio.name = 'bar'
         prio.value = 100
         prio.insert()
-        self.assertEqual(True, prio.exists)
+        self.assertTrue(prio.exists)
 
     def test_priority_update(self):
         prio = Priority(self.env, 'major')
@@ -772,7 +848,7 @@
         prio = Priority(self.env, 'major')
         self.assertEqual('3', prio.value)
         prio.delete()
-        self.assertEqual(False, prio.exists)
+        self.assertFalse(prio.exists)
         self.assertRaises(TracError, Priority, self.env, 'major')
         prio = Priority(self.env, 'minor')
         self.assertEqual('3', prio.value)
@@ -807,8 +883,9 @@
 
     def setUp(self):
         self.env = EnvironmentStub(default_data=True)
-        self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
-        os.mkdir(self.env.path)
+        self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
+        self.created_at = datetime(2001, 1, 1, tzinfo=utc)
+        self.updated_at = self.created_at + timedelta(seconds=1)
 
     def tearDown(self):
         shutil.rmtree(self.env.path)
@@ -820,12 +897,25 @@
             setattr(milestone, k, v)
         return milestone
 
+    def _insert_ticket(self, when=None, **kwargs):
+        ticket = Ticket(self.env)
+        for name, value in kwargs.iteritems():
+            ticket[name] = value
+        ticket.insert(when or self.created_at)
+        return ticket
+
+    def _update_ticket(self, ticket, author=None, comment=None, when=None,
+                       **kwargs):
+        for name, value in kwargs.iteritems():
+            ticket[name] = value
+        ticket.save_changes(author, comment, when or self.updated_at)
+
     def test_new_milestone(self):
         milestone = Milestone(self.env)
-        self.assertEqual(False, milestone.exists)
-        self.assertEqual(None, milestone.name)
-        self.assertEqual(None, milestone.due)
-        self.assertEqual(None, milestone.completed)
+        self.assertFalse(milestone.exists)
+        self.assertIsNone(milestone.name)
+        self.assertIsNone(milestone.due)
+        self.assertIsNone(milestone.completed)
         self.assertEqual('', milestone.description)
 
     def test_new_milestone_empty_name(self):
@@ -834,20 +924,20 @@
         milestone being correctly detected as non-existent.
         """
         milestone = Milestone(self.env, '')
-        self.assertEqual(False, milestone.exists)
-        self.assertEqual(None, milestone.name)
-        self.assertEqual(None, milestone.due)
-        self.assertEqual(None, milestone.completed)
+        self.assertFalse(milestone.exists)
+        self.assertIsNone(milestone.name)
+        self.assertIsNone(milestone.due)
+        self.assertIsNone(milestone.completed)
         self.assertEqual('', milestone.description)
 
     def test_existing_milestone(self):
         self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
 
         milestone = Milestone(self.env, 'Test')
-        self.assertEqual(True, milestone.exists)
+        self.assertTrue(milestone.exists)
         self.assertEqual('Test', milestone.name)
-        self.assertEqual(None, milestone.due)
-        self.assertEqual(None, milestone.completed)
+        self.assertIsNone(milestone.due)
+        self.assertIsNone(milestone.completed)
         self.assertEqual('', milestone.description)
 
     def test_create_and_update_milestone(self):
@@ -868,35 +958,119 @@
             WHERE name='Test'
             """))
 
+    def test_move_tickets(self):
+        self.env.db_transaction.executemany(
+            "INSERT INTO milestone (name) VALUES (%s)",
+            [('Test',), ('Testing',)])
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
+        milestone = Milestone(self.env, 'Test')
+        milestone.move_tickets('Testing', 'anonymous', 'Move tickets')
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Testing', tkt1['milestone'])
+        self.assertEqual('Testing', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+
+    def test_move_tickets_exclude_closed(self):
+        self.env.db_transaction.executemany(
+            "INSERT INTO milestone (name) VALUES (%s)",
+            [('Test',), ('Testing',)])
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
+        milestone = Milestone(self.env, 'Test')
+        milestone.move_tickets('Testing', 'anonymous', 'Move tickets',
+                               exclude_closed=True)
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Testing', tkt1['milestone'])
+        self.assertEqual('Test', tkt2['milestone'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+        self.assertEqual(self.updated_at, tkt2['changetime'])
+
+    def test_move_tickets_target_doesnt_exist(self):
+        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        milestone = Milestone(self.env, 'Test')
+        self.assertRaises(ResourceNotFound, milestone.move_tickets,
+                          'Testing', 'anonymous')
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Test', tkt1['milestone'])
+        self.assertEqual('Test', tkt2['milestone'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+        self.assertNotEqual(self.updated_at, tkt2['changetime'])
+
     def test_create_milestone_without_name(self):
         milestone = Milestone(self.env)
         self.assertRaises(TracError, milestone.insert)
 
     def test_delete_milestone(self):
         self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
-
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
         milestone = Milestone(self.env, 'Test')
         milestone.delete()
-        self.assertEqual(False, milestone.exists)
+        self.assertFalse(milestone.exists)
         self.assertEqual([],
             self.env.db_query("SELECT * FROM milestone WHERE name='Test'"))
 
-    def test_delete_milestone_retarget_tickets(self):
-        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('', tkt1['milestone'])
+        self.assertEqual('', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
 
-        tkt1 = Ticket(self.env)
-        tkt1.populate({'summary': 'Foo', 'milestone': 'Test'})
-        tkt1.insert()
-        tkt2 = Ticket(self.env)
-        tkt2.populate({'summary': 'Bar', 'milestone': 'Test'})
-        tkt2.insert()
+    def test_delete_milestone_with_attachment(self):
+        milestone = Milestone(self.env)
+        milestone.name = 'MilestoneWithAttachment'
+        milestone.insert()
+        
+        attachment = Attachment(self.env, 'milestone', milestone.name)
+        attachment.insert('foo.txt', StringIO(), 0, 1)
 
-        milestone = Milestone(self.env, 'Test')
-        milestone.delete(retarget_to='Other')
+        milestone.delete()
         self.assertEqual(False, milestone.exists)
 
-        self.assertEqual('Other', Ticket(self.env, tkt1.id)['milestone'])
-        self.assertEqual('Other', Ticket(self.env, tkt2.id)['milestone'])
+        attachments = Attachment.select(self.env, 'milestone', milestone.name)
+        self.assertRaises(StopIteration, attachments.next)
+
+    def test_delete_milestone_retarget_tickets(self):
+        self.env.db_transaction.executemany(
+            "INSERT INTO milestone (name) VALUES (%s)",
+            [('Test',), ('Other',)])
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
+        milestone = Milestone(self.env, 'Test')
+        milestone.delete(retarget_to='Other')
+        self.assertFalse(milestone.exists)
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Other', tkt1['milestone'])
+        self.assertEqual('Other', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
 
     def test_update_milestone(self):
         self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
@@ -920,23 +1094,6 @@
         milestone.name = None
         self.assertRaises(TracError, milestone.update)
 
-    def test_update_milestone_update_tickets(self):
-        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
-
-        tkt1 = Ticket(self.env)
-        tkt1.populate({'summary': 'Foo', 'milestone': 'Test'})
-        tkt1.insert()
-        tkt2 = Ticket(self.env)
-        tkt2.populate({'summary': 'Bar', 'milestone': 'Test'})
-        tkt2.insert()
-
-        milestone = Milestone(self.env, 'Test')
-        milestone.name = 'Testing'
-        milestone.update()
-
-        self.assertEqual('Testing', Ticket(self.env, tkt1.id)['milestone'])
-        self.assertEqual('Testing', Ticket(self.env, tkt2.id)['milestone'])
-
     def test_rename_milestone(self):
         milestone = Milestone(self.env)
         milestone.name = 'OldName'
@@ -958,6 +1115,24 @@
         self.assertEqual('foo.txt', attachments.next().filename)
         self.assertRaises(StopIteration, attachments.next)
 
+    def test_rename_milestone_retarget_tickets(self):
+        self.env.db_transaction("INSERT INTO milestone (name) VALUES ('Test')")
+        tkt1 = self._insert_ticket(status='new', summary='Foo',
+                                   milestone='Test')
+        tkt2 = self._insert_ticket(status='new', summary='Bar',
+                                   milestone='Test')
+        self._update_ticket(tkt2, status='closed', resolution='fixed')
+        milestone = Milestone(self.env, 'Test')
+        milestone.name = 'Testing'
+        milestone.update()
+
+        tkt1 = Ticket(self.env, tkt1.id)
+        tkt2 = Ticket(self.env, tkt2.id)
+        self.assertEqual('Testing', tkt1['milestone'])
+        self.assertEqual('Testing', tkt2['milestone'])
+        self.assertEqual(tkt1['changetime'], tkt2['changetime'])
+        self.assertNotEqual(self.updated_at, tkt1['changetime'])
+
     def test_select_milestones(self):
         self.env.db_transaction.executemany(
             "INSERT INTO milestone (name) VALUES (%s)",
@@ -965,9 +1140,9 @@
 
         milestones = list(Milestone.select(self.env))
         self.assertEqual('1.0', milestones[0].name)
-        assert milestones[0].exists
+        self.assertTrue(milestones[0].exists)
         self.assertEqual('2.0', milestones[1].name)
-        assert milestones[1].exists
+        self.assertTrue(milestones[1].exists)
 
     def test_change_listener_created(self):
         listener = TestMilestoneChangeListener(self.env)
@@ -999,10 +1174,10 @@
         listener = TestMilestoneChangeListener(self.env)
         milestone = self._create_milestone(name='Milestone 1')
         milestone.insert()
-        self.assertEqual(True, milestone.exists)
+        self.assertTrue(milestone.exists)
         milestone.delete()
         self.assertEqual('Milestone 1', milestone.name)
-        self.assertEqual(False, milestone.exists)
+        self.assertFalse(milestone.exists)
         self.assertEqual('deleted', listener.action)
         self.assertEqual(milestone, listener.milestone)
 
@@ -1180,13 +1355,13 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(TicketTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TicketCommentEditTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TicketCommentDeleteTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(EnumTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(MilestoneTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ComponentTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(VersionTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(TicketTestCase))
+    suite.addTest(unittest.makeSuite(TicketCommentEditTestCase))
+    suite.addTest(unittest.makeSuite(TicketCommentDeleteTestCase))
+    suite.addTest(unittest.makeSuite(EnumTestCase))
+    suite.addTest(unittest.makeSuite(MilestoneTestCase))
+    suite.addTest(unittest.makeSuite(ComponentTestCase))
+    suite.addTest(unittest.makeSuite(VersionTestCase))
     suite.addTest(unittest.makeSuite(
         ComponentResourceChangeListenerTestCase, 'test'))
     suite.addTest(unittest.makeSuite(
diff --git a/trac/trac/ticket/tests/notification.py b/trac/trac/ticket/tests/notification.py
index 8c466b0..23c7f75 100644
--- a/trac/trac/ticket/tests/notification.py
+++ b/trac/trac/ticket/tests/notification.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.blot@free.fr>
 # All rights reserved.
 #
@@ -16,25 +16,200 @@
 # (lsmithson@open-networks.co.uk) extensible Python SMTP Server
 #
 
-from trac.util.datefmt import utc
-from trac.ticket.model import Ticket
-from trac.ticket.notification import TicketNotifyEmail
-from trac.test import EnvironmentStub, Mock, MockPerm
-from trac.tests.notification import SMTPThreadedServer, parse_smtp_message, \
-                                    smtp_address
-
 import base64
-from datetime import datetime
 import os
 import quopri
 import re
 import unittest
+from datetime import datetime
 
-SMTP_TEST_PORT = 7000 + os.getpid() % 1000
+import trac.tests.compat
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.tests.notification import SMTP_TEST_PORT, SMTPThreadedServer,\
+                                    parse_smtp_message
+from trac.ticket.model import Ticket
+from trac.ticket.notification import TicketNotifyEmail
+from trac.ticket.web_ui import TicketModule
+from trac.util.datefmt import utc
+
 MAXBODYWIDTH = 76
 notifysuite = None
 
 
+class RecipientTestCase(unittest.TestCase):
+    """Notification test cases for email recipients."""
+
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        self.env.config.set('project', 'name', 'TracTest')
+        self.env.config.set('notification', 'smtp_enabled', 'true')
+        self.env.config.set('notification', 'smtp_port', str(SMTP_TEST_PORT))
+
+    def tearDown(self):
+        notifysuite.tear_down()
+        self.env.reset_db()
+
+    def test_no_recipients(self):
+        """No recipient case"""
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'anonymous'
+        ticket['summary'] = 'Foo'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        sender = notifysuite.smtpd.get_sender()
+        message = notifysuite.smtpd.get_message()
+        self.assertEqual(0, len(recipients))
+        self.assertIsNone(sender)
+        self.assertIsNone(message)
+
+    def test_new_ticket_recipients(self):
+        """Report and CC list should be in recipient list for new tickets."""
+        always_cc = ('joe.user@example.net', 'joe.bar@example.net')
+        ticket_cc = ('joe.user@example.com', 'joe.bar@example.org')
+        self.env.config.set('notification', 'smtp_always_cc',
+                            ', '.join(always_cc))
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe.bar@example.org'
+        ticket['owner'] = 'joe.user@example.net'
+        ticket['cc'] = ' '.join(ticket_cc)
+        ticket['summary'] = 'New ticket recipients'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        for r in always_cc + ticket_cc + \
+                (ticket['owner'], ticket['reporter']):
+            self.assertIn(r, recipients)
+
+    def test_cc_only(self):
+        """Notification w/o explicit recipients but Cc: (#3101)"""
+        always_cc = ('joe.user@example.net', 'joe.bar@example.net')
+        self.env.config.set('notification', 'smtp_always_cc',
+                            ', '.join(always_cc))
+        ticket = Ticket(self.env)
+        ticket['summary'] = 'Foo'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        for r in always_cc:
+            self.assertIn(r, recipients)
+
+    def test_always_notify_updater(self):
+        """The `always_notify_updater` option."""
+        def _test_updater(enabled):
+            self.env.config.set('notification', 'always_notify_updater',
+                                enabled)
+            ticket = Ticket(self.env)
+            ticket['reporter'] = 'joe.user@example.org'
+            ticket['summary'] = u'This is a súmmäry'
+            ticket.insert()
+            now = datetime.now(utc)
+            ticket.save_changes('joe.bar2@example.com', 'This is a change',
+                                when=now)
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, newticket=False, modtime=now)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertEqual(1, len(recipients))
+                self.assertIn('joe.bar2@example.com', recipients)
+            else:
+                self.assertEqual(0, len(recipients))
+                self.assertNotIn('joe.bar2@example.com', recipients)
+
+        # Validate with and without a default domain
+        for enable in False, True:
+            _test_updater(enable)
+
+    def test_always_notify_owner(self):
+        """The `always_notify_owner` option."""
+        def _test_reporter(enabled):
+            self.env.config.set('notification', 'always_notify_owner',
+                                enabled)
+            self.env.config.set('notification', 'always_notify_updater',
+                                'false')
+            ticket = Ticket(self.env)
+            ticket['summary'] = 'Foo'
+            ticket['reporter'] = u'joe@example.org'
+            ticket['owner'] = u'jim@example.org'
+            ticket.insert()
+            now = datetime.now(utc)
+            ticket.save_changes('joe@example.org', 'this is my comment',
+                                when=now)
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, newticket=True, modtime=now)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertEqual(1, len(recipients))
+                self.assertEqual('jim@example.org', recipients[0])
+            else:
+                self.assertEqual(0, len(recipients))
+
+        for enable in False, True:
+            _test_reporter(enable)
+
+    def test_always_notify_reporter(self):
+        """Notification to reporter w/ updater option disabled (#3780)"""
+        def _test_reporter(enabled):
+            self.env.config.set('notification', 'always_notify_updater',
+                                'false')
+            self.env.config.set('notification', 'always_notify_reporter',
+                                enabled)
+            ticket = Ticket(self.env)
+            ticket['summary'] = 'Foo'
+            ticket['reporter'] = u'joe@example.org'
+            ticket.insert()
+            now = datetime.now(utc)
+            ticket.save_changes('joe@example.org', 'this is my comment',
+                                when=now)
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, newticket=True, modtime=now)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertEqual(1, len(recipients))
+                self.assertEqual('joe@example.org', recipients[0])
+            else:
+                self.assertEqual(0, len(recipients))
+
+        for enable in False, True:
+            _test_reporter(enable)
+
+    def test_no_duplicates(self):
+        """Email addresses should be found only once in the recipient list."""
+        self.env.config.set('notification', 'smtp_always_cc',
+                            'joe.user@example.com')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe.user@example.com'
+        ticket['owner'] = 'joe.user@example.com'
+        ticket['cc'] = 'joe.user@example.com'
+        ticket['summary'] = 'No duplicates'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        self.assertEqual(1, len(recipients))
+        self.assertIn('joe.user@example.com', recipients)
+
+    def test_long_forms(self):
+        """Long forms of SMTP email addresses 'Display Name <address>'"""
+        self.env.config.set('notification', 'always_notify_owner', True)
+        ticket = Ticket(self.env)
+        ticket['reporter'] = '"Joe" <joe.user@example.com>'
+        ticket['owner'] = 'Joe <joe.user@example.net>'
+        ticket['cc'] = 'Joe < joe.user@example.org >'
+        ticket['summary'] = 'Long form'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        self.assertEqual(3, len(recipients))
+        self.assertIn('joe.user@example.com', recipients)
+        self.assertIn('joe.user@example.net', recipients)
+        self.assertIn('joe.user@example.org', recipients)
+
+
 class NotificationTestCase(unittest.TestCase):
     """Notification test cases that send email over SMTP"""
 
@@ -48,7 +223,7 @@
                             'joe.user@example.net, joe.bar@example.net')
         self.env.config.set('notification', 'use_public_cc', 'true')
         self.env.config.set('notification', 'smtp_port', str(SMTP_TEST_PORT))
-        self.env.config.set('notification', 'smtp_server','localhost')
+        self.env.config.set('notification', 'smtp_server', 'localhost')
         self.req = Mock(href=self.env.href, abs_href=self.env.abs_href, tz=utc,
                         perm=MockPerm())
 
@@ -57,85 +232,28 @@
         notifysuite.tear_down()
         self.env.reset_db()
 
-    def test_recipients(self):
-        """To/Cc recipients"""
-        ticket = Ticket(self.env)
-        ticket['reporter'] = '"Joe User" < joe.user@example.org >'
-        ticket['owner']    = 'joe.user@example.net'
-        ticket['cc']       = 'joe.user@example.com, joe.bar@example.org, ' \
-                             'joe.bar@example.net'
-        ticket['summary'] = 'Foo'
-        ticket.insert()
-        tn = TicketNotifyEmail(self.env)
-        tn.notify(ticket, newticket=True)
-        recipients = notifysuite.smtpd.get_recipients()
-        # checks there is no duplicate in the recipient list
-        rcpts = []
-        for r in recipients:
-            self.failIf(r in rcpts)
-            rcpts.append(r)
-        # checks that all cc recipients have been notified
-        cc_list = self.env.config.get('notification', 'smtp_always_cc')
-        cc_list = "%s, %s" % (cc_list, ticket['cc'])
-        for r in cc_list.replace(',', ' ').split():
-            self.failIf(r not in recipients)
-        # checks that owner has been notified
-        self.failIf(smtp_address(ticket['owner']) not in recipients)
-        # checks that reporter has been notified
-        self.failIf(smtp_address(ticket['reporter']) not in recipients)
-
-    def test_no_recipient(self):
-        """No recipient case"""
-        self.env.config.set('notification', 'smtp_always_cc', '')
-        ticket = Ticket(self.env)
-        ticket['reporter'] = 'anonymous'
-        ticket['summary'] = 'Foo'
-        ticket.insert()
-        tn = TicketNotifyEmail(self.env)
-        tn.notify(ticket, newticket=True)
-        sender = notifysuite.smtpd.get_sender()
-        recipients = notifysuite.smtpd.get_recipients()
-        message = notifysuite.smtpd.get_message()
-        # checks that no message has been sent
-        self.failIf(recipients)
-        self.failIf(sender)
-        self.failIf(message)
-
-    def test_cc_only(self):
-        """Notification w/o explicit recipients but Cc: (#3101)"""
-        ticket = Ticket(self.env)
-        ticket['summary'] = 'Foo'
-        ticket.insert()
-        tn = TicketNotifyEmail(self.env)
-        tn.notify(ticket, newticket=True)
-        recipients = notifysuite.smtpd.get_recipients()
-        # checks that all cc recipients have been notified
-        cc_list = self.env.config.get('notification', 'smtp_always_cc')
-        for r in cc_list.replace(',', ' ').split():
-            self.failIf(r not in recipients)
-
     def test_structure(self):
         """Basic SMTP message structure (headers, body)"""
         ticket = Ticket(self.env)
         ticket['reporter'] = '"Joe User" <joe.user@example.org>'
-        ticket['owner']    = 'joe.user@example.net'
-        ticket['cc']       = 'joe.user@example.com, joe.bar@example.org, ' \
-                             'joe.bar@example.net'
+        ticket['owner'] = 'joe.user@example.net'
+        ticket['cc'] = 'joe.user@example.com, joe.bar@example.org, ' \
+                       'joe.bar@example.net'
         ticket['summary'] = 'This is a summary'
         ticket.insert()
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # checks for header existence
-        self.failIf(not headers)
-        # checks for body existance
-        self.failIf(not body)
+        self.assertTrue(headers)
+        # checks for body existence
+        self.assertTrue(body)
         # checks for expected headers
-        self.failIf('Date' not in headers)
-        self.failIf('Subject' not in headers)
-        self.failIf('Message-ID' not in headers)
-        self.failIf('From' not in headers)
+        self.assertIn('Date', headers)
+        self.assertIn('Subject', headers)
+        self.assertIn('Message-ID', headers)
+        self.assertIn('From', headers)
 
     def test_date(self):
         """Date format compliance (RFC822)
@@ -148,7 +266,7 @@
         date_re = re.compile(date_str)
         # python time module does not detect incorrect time values
         days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
-        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', \
+        months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                   'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
         tz = ['UT', 'GMT', 'EST', 'EDT', 'CST', 'CDT', 'MST', 'MDT',
               'PST', 'PDT']
@@ -159,24 +277,23 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
-        self.failIf('Date' not in headers)
+        headers, body = parse_smtp_message(message)
+        self.assertIn('Date', headers)
         mo = date_re.match(headers['Date'])
-        self.failIf(not mo)
+        self.assertTrue(mo)
         if mo.group('day'):
-            self.failIf(mo.group('day') not in days)
-        self.failIf(int(mo.group('dm')) not in range(1, 32))
-        self.failIf(mo.group('month') not in months)
-        self.failIf(int(mo.group('hour')) not in range(0, 24))
+            self.assertIn(mo.group('day'), days)
+        self.assertIn(int(mo.group('dm')), range(1, 32))
+        self.assertIn(mo.group('month'), months)
+        self.assertIn(int(mo.group('hour')), range(0, 24))
         if mo.group('tz'):
-            self.failIf(mo.group('tz') not in tz)
+            self.assertIn(mo.group('tz'), tz)
 
     def test_bcc_privacy(self):
         """Visibility of recipients"""
-        def run_bcc_feature(public):
+        def run_bcc_feature(public_cc):
             # CC list should be private
-            self.env.config.set('notification', 'use_public_cc',
-                                'true' if public else 'false')
+            self.env.config.set('notification', 'use_public_cc', public_cc)
             self.env.config.set('notification', 'smtp_always_bcc',
                                 'joe.foobar@example.net')
             ticket = Ticket(self.env)
@@ -186,15 +303,15 @@
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=True)
             message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
-            if public:
+            headers, body = parse_smtp_message(message)
+            if public_cc:
                 # Msg should have a To list
-                self.failIf('To' not in headers)
+                self.assertIn('To', headers)
                 # Extract the list of 'To' recipients from the message
                 to = [rcpt.strip() for rcpt in headers['To'].split(',')]
             else:
                 # Msg should not have a To list
-                self.failIf('To' in headers)
+                self.assertNotIn('To', headers)
                 # Extract the list of 'To' recipients from the message
                 to = []
             # Extract the list of 'Cc' recipients from the message
@@ -207,20 +324,20 @@
             for rcpt in cclist:
                 # Each recipient of the 'Cc' list should appear
                 # in the 'Cc' header
-                self.failIf(rcpt not in cc)
+                self.assertIn(rcpt, cc)
                 # Check the message has actually been sent to the recipients
-                self.failIf(rcpt not in rcptlist)
+                self.assertIn(rcpt, rcptlist)
             # Build the list of the expected 'Bcc' recipients
             bccrcpt = self.env.config.get('notification', 'smtp_always_bcc')
             bcclist = [bccr.strip() for bccr in bccrcpt.split(',')]
             for rcpt in bcclist:
                 # Check none of the 'Bcc' recipients appears
                 # in the 'To' header
-                self.failIf(rcpt in to)
+                self.assertNotIn(rcpt, to)
                 # Check the message has actually been sent to the recipients
-                self.failIf(rcpt not in rcptlist)
-        run_bcc_feature(True)
-        run_bcc_feature(False)
+                self.assertIn(rcpt, rcptlist)
+        for public in False, True:
+            run_bcc_feature(public)
 
     def test_short_login(self):
         """Email addresses without a FQDN"""
@@ -233,31 +350,30 @@
             # send a notification even if other addresses are not valid
             self.env.config.set('notification', 'smtp_always_cc',
                                 'joe.bar@example.net')
-            if enabled:
-                self.env.config.set('notification', 'use_short_addr', 'true')
+            self.env.config.set('notification', 'use_short_addr', enabled)
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=True)
             message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
+            headers, body = parse_smtp_message(message)
             # Msg should not have a 'To' header
             if not enabled:
-                self.failIf('To' in headers)
+                self.assertNotIn('To', headers)
             else:
                 tolist = [addr.strip() for addr in headers['To'].split(',')]
             # Msg should have a 'Cc' field
-            self.failIf('Cc' not in headers)
+            self.assertIn('Cc', headers)
             cclist = [addr.strip() for addr in headers['Cc'].split(',')]
             if enabled:
                 # Msg should be delivered to the reporter
-                self.failIf(ticket['reporter'] not in tolist)
+                self.assertIn(ticket['reporter'], tolist)
             else:
                 # Msg should not be delivered to joeuser
-                self.failIf(ticket['reporter'] in cclist)
+                self.assertNotIn(ticket['reporter'], cclist)
             # Msg should still be delivered to the always_cc list
-            self.failIf(self.env.config.get('notification',
-                        'smtp_always_cc') not in cclist)
+            self.assertIn(self.env.config.get('notification',
+                                              'smtp_always_cc'), cclist)
         # Validate with and without the short addr option enabled
-        for enable in [False, True]:
+        for enable in False, True:
             _test_short_login(enable)
 
     def test_default_domain(self):
@@ -282,21 +398,21 @@
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=True)
             message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
+            headers, body = parse_smtp_message(message)
             # Msg should always have a 'Cc' field
-            self.failIf('Cc' not in headers)
+            self.assertIn('Cc', headers)
             cclist = [addr.strip() for addr in headers['Cc'].split(',')]
-            self.failIf('joewithdom@example.com' not in cclist)
-            self.failIf('joe.bar@example.net' not in cclist)
-            if not enabled:
-                self.failIf(len(cclist) != 2)
-                self.failIf('joenodom' in cclist)
+            self.assertIn('joewithdom@example.com', cclist)
+            self.assertIn('joe.bar@example.net', cclist)
+            if enabled:
+                self.assertEqual(3, len(cclist))
+                self.assertIn('joenodom@example.org', cclist)
             else:
-                self.failIf(len(cclist) != 3)
-                self.failIf('joenodom@example.org' not in cclist)
+                self.assertEqual(2, len(cclist))
+                self.assertNotIn('joenodom', cclist)
 
         # Validate with and without a default domain
-        for enable in [False, True]:
+        for enable in False, True:
             _test_default_domain(enable)
 
     def test_email_map(self):
@@ -317,15 +433,15 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Msg should always have a 'To' field
-        self.failIf('To' not in headers)
+        self.assertIn('To', headers)
         tolist = [addr.strip() for addr in headers['To'].split(',')]
         # 'To' list should have been resolved to the real email address
-        self.failIf('user-joe@example.com' not in tolist)
-        self.failIf('user-jim@example.com' not in tolist)
-        self.failIf('joeuser' in tolist)
-        self.failIf('jim@domain' in tolist)
+        self.assertIn('user-joe@example.com', tolist)
+        self.assertIn('user-jim@example.com', tolist)
+        self.assertNotIn('joeuser', tolist)
+        self.assertNotIn('jim@domain', tolist)
 
     def test_from_author(self):
         """Using the reporter or change author as the notification sender"""
@@ -346,7 +462,7 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"Joe User" <user-joe@example.com>', headers['From'])
         # Ticket change uses the change author
         ticket['summary'] = 'Modified summary'
@@ -354,7 +470,7 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"Jim User" <user-jim@example.com>', headers['From'])
         # Known author without name uses e-mail address only
         ticket['summary'] = 'Final summary'
@@ -362,7 +478,7 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('user-noname@example.com', headers['From'])
         # Known author without e-mail uses smtp_from and smtp_from_name
         ticket['summary'] = 'Other summary'
@@ -370,7 +486,7 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"My Trac" <trac@example.com>', headers['From'])
         # Unknown author with name and e-mail address
         ticket['summary'] = 'Some summary'
@@ -378,7 +494,7 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"Test User" <test@example.com>', headers['From'])
         # Unknown author with e-mail address only
         ticket['summary'] = 'Some summary'
@@ -386,7 +502,7 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('test@example.com', headers['From'])
         # Unknown author uses smtp_from and smtp_from_name
         ticket['summary'] = 'Better summary'
@@ -394,7 +510,7 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=False, modtime=ticket['changetime'])
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         self.assertEqual('"My Trac" <trac@example.com>', headers['From'])
 
     def test_ignore_domains(self):
@@ -412,16 +528,16 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Msg should always have a 'To' field
-        self.failIf('To' not in headers)
+        self.assertIn('To', headers)
         tolist = [addr.strip() for addr in headers['To'].split(',')]
         # 'To' list should not contain addresses with non-SMTP domains
-        self.failIf('kerberos@example.com' in tolist)
-        self.failIf('kerberos@example.org' in tolist)
+        self.assertNotIn('kerberos@example.com', tolist)
+        self.assertNotIn('kerberos@example.org', tolist)
         # 'To' list should have been resolved to the actual email address
-        self.failIf('kerb@example.net' not in tolist)
-        self.failIf(len(tolist) != 1)
+        self.assertIn('kerb@example.net', tolist)
+        self.assertEqual(1, len(tolist))
 
     def test_admit_domains(self):
         """SMTP domain inclusion"""
@@ -436,16 +552,16 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Msg should always have a 'To' field
-        self.failIf('Cc' not in headers)
+        self.assertIn('Cc', headers)
         cclist = [addr.strip() for addr in headers['Cc'].split(',')]
         # 'Cc' list should contain addresses with SMTP included domains
-        self.failIf('joe.user@localdomain' not in cclist)
-        self.failIf('joe.user@server' not in cclist)
+        self.assertIn('joe.user@localdomain', cclist)
+        self.assertIn('joe.user@server', cclist)
         # 'Cc' list should not contain non-FQDN domains
-        self.failIf('joe.user@unknown' in cclist)
-        self.failIf(len(cclist) != 2+2)
+        self.assertNotIn('joe.user@unknown', cclist)
+        self.assertEqual(4, len(cclist))
 
     def test_multiline_header(self):
         """Encoded headers split into multiple lines"""
@@ -458,11 +574,11 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         # Discards the project name & ticket number
         subject = headers['Subject']
         summary = subject[subject.find(':')+2:]
-        self.failIf(ticket['summary'] != summary)
+        self.assertEqual(ticket['summary'], summary)
 
     def test_mimebody_b64(self):
         """MIME Base64/utf-8 encoding"""
@@ -472,8 +588,7 @@
         ticket['summary'] = u'This is a long enough summary to cause Trac ' \
                             u'to generate a multi-line (2 lines) súmmäry'
         ticket.insert()
-        self._validate_mimebody((base64, 'base64', 'utf-8'), \
-                                ticket, True)
+        self._validate_mimebody((base64, 'base64', 'utf-8'), ticket, True)
 
     def test_mimebody_qp(self):
         """MIME QP/utf-8 encoding"""
@@ -493,8 +608,7 @@
         ticket['reporter'] = 'joe.user'
         ticket['summary'] = u'This is a summary'
         ticket.insert()
-        self._validate_mimebody((None, '7bit', 'utf-8'), \
-                                ticket, True)
+        self._validate_mimebody((None, '7bit', 'utf-8'), ticket, True)
 
     def test_mimebody_none_8bit(self):
         """MIME None encoding resulting in 8bit"""
@@ -503,8 +617,7 @@
         ticket['reporter'] = 'joe.user'
         ticket['summary'] = u'This is a summary for Jöe Usèr'
         ticket.insert()
-        self._validate_mimebody((None, '8bit', 'utf-8'), \
-                                ticket, True)
+        self._validate_mimebody((None, '8bit', 'utf-8'), ticket, True)
 
     def test_md5_digest(self):
         """MD5 digest w/ non-ASCII recipient address (#3491)"""
@@ -518,118 +631,77 @@
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
+        self.assertEqual('joe.user@example.org', headers['To'])
 
-    def test_updater(self):
-        """No-self-notification option"""
-        def _test_updater(disable):
-            if disable:
-                self.env.config.set('notification', 'always_notify_updater',
-                                    'false')
+    def test_previous_cc_list(self):
+        """Members removed from CC list receive notifications"""
+        ticket = Ticket(self.env)
+        ticket['summary'] = 'Foo'
+        ticket['cc'] = 'joe.user1@example.net'
+        ticket.insert()
+        ticket['cc'] = 'joe.user2@example.net'
+        now = datetime.now(utc)
+        ticket.save_changes('joe.bar@example.com', 'Removed from cc', now)
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=False, modtime=now)
+        recipients = notifysuite.smtpd.get_recipients()
+        self.assertIn('joe.user1@example.net', recipients)
+        self.assertIn('joe.user2@example.net', recipients)
+
+    def test_previous_owner(self):
+        """Previous owner is notified when ticket is reassigned (#2311)
+           if always_notify_owner is set to True"""
+        def _test_owner(enabled):
+            self.env.config.set('notification', 'always_notify_owner', enabled)
             ticket = Ticket(self.env)
-            ticket['reporter'] = 'joe.user@example.org'
-            ticket['summary'] = u'This is a súmmäry'
-            ticket['cc'] = 'joe.bar@example.com'
+            ticket['summary'] = 'Foo'
+            ticket['owner'] = prev_owner = 'joe.user1@example.net'
             ticket.insert()
-            ticket['component'] = 'dummy'
+            ticket['owner'] = new_owner = 'joe.user2@example.net'
             now = datetime.now(utc)
-            ticket.save_changes('joe.bar2@example.com', 'This is a change',
-                                when=now)
+            ticket.save_changes('joe.bar@example.com', 'Changed owner', now)
             tn = TicketNotifyEmail(self.env)
             tn.notify(ticket, newticket=False, modtime=now)
-            message = notifysuite.smtpd.get_message()
-            (headers, body) = parse_smtp_message(message)
-            # checks for header existence
-            self.failIf(not headers)
-            # checks for updater in the 'To' recipient list
-            self.failIf('To' not in headers)
-            tolist = [addr.strip() for addr in headers['To'].split(',')]
-            if disable:
-                self.failIf('joe.bar2@example.com' in tolist)
+            recipients = notifysuite.smtpd.get_recipients()
+            if enabled:
+                self.assertIn(prev_owner, recipients)
+                self.assertIn(new_owner, recipients)
             else:
-                self.failIf('joe.bar2@example.com' not in tolist)
+                self.assertNotIn(prev_owner, recipients)
+                self.assertNotIn(new_owner, recipients)
 
-        # Validate with and without a default domain
-        for disable in [False, True]:
-            _test_updater(disable)
-
-    def test_updater_only(self):
-        """Notification w/ updater, w/o any other recipient (#4188)"""
-        self.env.config.set('notification', 'always_notify_owner', 'false')
-        self.env.config.set('notification', 'always_notify_reporter', 'false')
-        self.env.config.set('notification', 'always_notify_updater', 'true')
-        self.env.config.set('notification', 'smtp_always_cc', '')
-        self.env.config.set('notification', 'smtp_always_bcc', '')
-        self.env.config.set('notification', 'use_public_cc', 'false')
-        self.env.config.set('notification', 'use_short_addr', 'false')
-        self.env.config.set('notification', 'smtp_replyto',
-                            'joeuser@example.net')
-        ticket = Ticket(self.env)
-        ticket['summary'] = 'Foo'
-        ticket.insert()
-        ticket['summary'] = 'Bar'
-        ticket['component'] = 'New value'
-        ticket.save_changes('joe@example.com', 'this is my comment')
-        tn = TicketNotifyEmail(self.env)
-        tn.notify(ticket, newticket=True)
-        recipients = notifysuite.smtpd.get_recipients()
-        self.failIf(recipients is None)
-        self.failIf(len(recipients) != 1)
-        self.failIf(recipients[0] != 'joe@example.com')
-
-    def test_updater_is_reporter(self):
-        """Notification to reporter w/ updater option disabled (#3780)"""
-        self.env.config.set('notification', 'always_notify_owner', 'false')
-        self.env.config.set('notification', 'always_notify_reporter', 'true')
-        self.env.config.set('notification', 'always_notify_updater', 'false')
-        self.env.config.set('notification', 'smtp_always_cc', '')
-        self.env.config.set('notification', 'smtp_always_bcc', '')
-        self.env.config.set('notification', 'use_public_cc', 'false')
-        self.env.config.set('notification', 'use_short_addr', 'false')
-        self.env.config.set('notification', 'smtp_replyto',
-                            'joeuser@example.net')
-        ticket = Ticket(self.env)
-        ticket['summary'] = 'Foo'
-        ticket['reporter'] = u'joe@example.org'
-        ticket.insert()
-        ticket['summary'] = 'Bar'
-        ticket['component'] = 'New value'
-        ticket.save_changes('joe@example.org', 'this is my comment')
-        tn = TicketNotifyEmail(self.env)
-        tn.notify(ticket, newticket=True)
-        recipients = notifysuite.smtpd.get_recipients()
-        self.failIf(recipients is None)
-        self.failIf(len(recipients) != 1)
-        self.failIf(recipients[0] != 'joe@example.org')
+        for enable in False, True:
+            _test_owner(enable)
 
     def _validate_mimebody(self, mime, ticket, newtk):
         """Body of a ticket notification message"""
-        (mime_decoder, mime_name, mime_charset) = mime
+        mime_decoder, mime_name, mime_charset = mime
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=newtk)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
-        self.failIf('MIME-Version' not in headers)
-        self.failIf('Content-Type' not in headers)
-        self.failIf('Content-Transfer-Encoding' not in headers)
-        self.failIf(not re.compile(r"1.\d").match(headers['MIME-Version']))
+        headers, body = parse_smtp_message(message)
+        self.assertIn('MIME-Version', headers)
+        self.assertIn('Content-Type', headers)
+        self.assertIn('Content-Transfer-Encoding', headers)
+        self.assertTrue(re.compile(r"1.\d").match(headers['MIME-Version']))
         type_re = re.compile(r'^text/plain;\scharset="([\w\-\d]+)"$')
         charset = type_re.match(headers['Content-Type'])
-        self.failIf(not charset)
+        self.assertTrue(charset)
         charset = charset.group(1)
-        self.assertEqual(charset, mime_charset)
+        self.assertEqual(mime_charset, charset)
         self.assertEqual(headers['Content-Transfer-Encoding'], mime_name)
         # checks the width of each body line
         for line in body.splitlines():
-            self.failIf(len(line) > MAXBODYWIDTH)
-        # attempts to decode the body, following the specified MIME endoding
+            self.assertTrue(len(line) <= MAXBODYWIDTH)
+        # attempts to decode the body, following the specified MIME encoding
         # and charset
         try:
             if mime_decoder:
                 body = mime_decoder.decodestring(body)
             body = unicode(body, charset)
         except Exception, e:
-            raise AssertionError, e
+            raise AssertionError(e)
         # now processes each line of the body
         bodylines = body.splitlines()
         # body starts with one of more summary lines, first line is prefixed
@@ -637,25 +709,25 @@
         # finds the banner after the summary
         banner_delim_re = re.compile(r'^\-+\+\-+$')
         bodyheader = []
-        while ( not banner_delim_re.match(bodylines[0]) ):
+        while not banner_delim_re.match(bodylines[0]):
             bodyheader.append(bodylines.pop(0))
         # summary should be present
-        self.failIf(not bodyheader)
+        self.assertTrue(bodyheader)
         # banner should not be empty
-        self.failIf(not bodylines)
+        self.assertTrue(bodylines)
         # extracts the ticket ID from the first line
-        (tknum, bodyheader[0]) = bodyheader[0].split(' ', 1)
-        self.assertEqual(tknum[0], '#')
+        tknum, bodyheader[0] = bodyheader[0].split(' ', 1)
+        self.assertEqual('#', tknum[0])
         try:
             tkid = int(tknum[1:-1])
-            self.assertEqual(tkid, 1)
+            self.assertEqual(1, tkid)
         except ValueError:
-            raise AssertionError, "invalid ticket number"
-        self.assertEqual(tknum[-1], ':')
+            raise AssertionError("invalid ticket number")
+        self.assertEqual(':', tknum[-1])
         summary = ' '.join(bodyheader)
         self.assertEqual(summary, ticket['summary'])
         # now checks the banner contents
-        self.failIf(not banner_delim_re.match(bodylines[0]))
+        self.assertTrue(banner_delim_re.match(bodylines[0]))
         banner = True
         footer = None
         props = {}
@@ -667,11 +739,11 @@
             if banner:
                 # parse banner and fill in a property dict
                 properties = line.split('|')
-                self.assertEqual(len(properties), 2)
+                self.assertEqual(2, len(properties))
                 for prop in properties:
                     if prop.strip() == '':
                         continue
-                    (k, v) = prop.split(':')
+                    k, v = prop.split(':')
                     props[k.strip().lower()] = v.strip()
             # detect footer marker (weak detection)
             if not footer:
@@ -679,10 +751,10 @@
                     footer = 0
                     continue
             # check footer
-            if footer != None:
+            if footer is not None:
                 footer += 1
                 # invalid footer detection
-                self.failIf(footer > 3)
+                self.assertTrue(footer <= 3)
                 # check ticket link
                 if line[:11] == 'Ticket URL:':
                     ticket_link = self.env.abs_href.ticket(ticket.id)
@@ -693,12 +765,13 @@
         xlist = ['summary', 'description', 'comment', 'time', 'changetime']
         # check banner content (field exists, msg value matches ticket value)
         for p in [prop for prop in ticket.values.keys() if prop not in xlist]:
-            self.failIf(not props.has_key(p))
+            self.assertIn(p, props)
             # Email addresses might be obfuscated
             if '@' in ticket[p] and '@' in props[p]:
-                self.failIf(props[p].split('@')[0] != ticket[p].split('@')[0])
+                self.assertEqual(props[p].split('@')[0],
+                                 ticket[p].split('@')[0])
             else:
-                self.failIf(props[p] != ticket[p])
+                self.assertEqual(props[p], ticket[p])
 
     def test_props_format_ambiwidth_single(self):
         self.env.config.set('notification', 'mime_encoding', 'none')
@@ -827,7 +900,7 @@
       Type:  defect                              |     Status:  new
   Priority:  major                               |  Milestone:  milestone1
  Component:  Lorem ipsum dolor sit amet,         |    Version:  2.0
-  consectetur adipisicing elit, sed do eiusmod   |   Keywords:
+  consectetur adipisicing elit, sed do eiusmod   |
   tempor incididunt ut labore et dolore magna    |
   aliqua. Ut enim ad minim veniam, quis nostrud  |
   exercitation ullamco laboris nisi ut aliquip   |
@@ -837,7 +910,7 @@
   Excepteur sint occaecat cupidatat non          |
   proident, sunt in culpa qui officia deserunt   |
   mollit anim id est laborum.                    |
-Resolution:  fixed                               |"""
+Resolution:  fixed                               |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_leftside_unicode(self):
@@ -865,10 +938,10 @@
       Type:  defect                              |     Status:  new
   Priority:  major                               |  Milestone:  milestone1
  Component:  Trac は BSD ライセンスのもとで配布  |    Version:  2.0
-  されています。[1:]このライセンスの全文は、配   |   Keywords:
+  されています。[1:]このライセンスの全文は、配   |
   布ファイルに含まれている [3:COPYING] ファイル  |
   と同じものが[2:オンライン]で参照できます。     |
-Resolution:  fixed                               |"""
+Resolution:  fixed                               |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_rightside(self):
@@ -893,7 +966,7 @@
                               u'culpa qui officia deserunt mollit anim id ' \
                               u'est laborum.'
         ticket['component'] = u'component1'
-        ticket['version'] = u'2.0'
+        ticket['version'] = u'2.0 Standard and International Edition'
         ticket['resolution'] = u'fixed'
         ticket['keywords'] = u''
         ticket.insert()
@@ -901,8 +974,8 @@
   Reporter:  anonymous   |      Owner:  somebody
       Type:  defect      |     Status:  new
   Priority:  major       |  Milestone:  Lorem ipsum dolor sit amet,
- Component:  component1  |  consectetur adipisicing elit, sed do eiusmod
-Resolution:  fixed       |  tempor incididunt ut labore et dolore magna
+                         |  consectetur adipisicing elit, sed do eiusmod
+                         |  tempor incididunt ut labore et dolore magna
                          |  aliqua. Ut enim ad minim veniam, quis nostrud
                          |  exercitation ullamco laboris nisi ut aliquip ex
                          |  ea commodo consequat. Duis aute irure dolor in
@@ -911,8 +984,9 @@
                          |  occaecat cupidatat non proident, sunt in culpa
                          |  qui officia deserunt mollit anim id est
                          |  laborum.
-                         |    Version:  2.0
-                         |   Keywords:"""
+ Component:  component1  |    Version:  2.0 Standard and International
+                         |  Edition
+Resolution:  fixed       |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_rightside_unicode(self):
@@ -937,10 +1011,10 @@
   Reporter:  anonymous   |      Owner:  somebody
       Type:  defect      |     Status:  new
   Priority:  major       |  Milestone:  Trac 在经过修改的BSD协议下发布。
- Component:  component1  |  [1:]协议的完整文本可以[2:在线查看]也可在发布版
-Resolution:  fixed       |  的 [3:COPYING] 文件中找到。
-                         |    Version:  2.0
-                         |   Keywords:"""
+                         |  [1:]协议的完整文本可以[2:在线查看]也可在发布版
+                         |  的 [3:COPYING] 文件中找到。
+ Component:  component1  |    Version:  2.0
+Resolution:  fixed       |   Keywords:"""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_bothsides(self):
@@ -964,31 +1038,33 @@
                               u'occaecat cupidatat non proident, sunt in ' \
                               u'culpa qui officia deserunt mollit anim id ' \
                               u'est laborum.'
-        ticket['component'] = ticket['milestone']
+        ticket['component'] = (u'Lorem ipsum dolor sit amet, consectetur '
+                               u'adipisicing elit, sed do eiusmod tempor '
+                               u'incididunt ut labore et dolore magna aliqua.')
         ticket['version'] = u'2.0'
         ticket['resolution'] = u'fixed'
-        ticket['keywords'] = u''
+        ticket['keywords'] = u'Ut enim ad minim veniam, ....'
         ticket.insert()
         formatted = """\
   Reporter:  anonymous               |      Owner:  somebody
       Type:  defect                  |     Status:  new
   Priority:  major                   |  Milestone:  Lorem ipsum dolor sit
- Component:  Lorem ipsum dolor sit   |  amet, consectetur adipisicing elit,
-  amet, consectetur adipisicing      |  sed do eiusmod tempor incididunt ut
-  elit, sed do eiusmod tempor        |  labore et dolore magna aliqua. Ut
-  incididunt ut labore et dolore     |  enim ad minim veniam, quis nostrud
-  magna aliqua. Ut enim ad minim     |  exercitation ullamco laboris nisi
-  veniam, quis nostrud exercitation  |  ut aliquip ex ea commodo consequat.
-  ullamco laboris nisi ut aliquip    |  Duis aute irure dolor in
-  ex ea commodo consequat. Duis      |  reprehenderit in voluptate velit
-  aute irure dolor in reprehenderit  |  esse cillum dolore eu fugiat nulla
-  in voluptate velit esse cillum     |  pariatur. Excepteur sint occaecat
-  dolore eu fugiat nulla pariatur.   |  cupidatat non proident, sunt in
-  Excepteur sint occaecat cupidatat  |  culpa qui officia deserunt mollit
-  non proident, sunt in culpa qui    |  anim id est laborum.
-  officia deserunt mollit anim id    |    Version:  2.0
-  est laborum.                       |   Keywords:
-Resolution:  fixed                   |"""
+                                     |  amet, consectetur adipisicing elit,
+                                     |  sed do eiusmod tempor incididunt ut
+                                     |  labore et dolore magna aliqua. Ut
+                                     |  enim ad minim veniam, quis nostrud
+                                     |  exercitation ullamco laboris nisi
+                                     |  ut aliquip ex ea commodo consequat.
+                                     |  Duis aute irure dolor in
+                                     |  reprehenderit in voluptate velit
+                                     |  esse cillum dolore eu fugiat nulla
+ Component:  Lorem ipsum dolor sit   |  pariatur. Excepteur sint occaecat
+  amet, consectetur adipisicing      |  cupidatat non proident, sunt in
+  elit, sed do eiusmod tempor        |  culpa qui officia deserunt mollit
+  incididunt ut labore et dolore     |  anim id est laborum.
+  magna aliqua.                      |    Version:  2.0
+Resolution:  fixed                   |   Keywords:  Ut enim ad minim
+                                     |  veniam, ...."""
         self._validate_props_format(formatted, ticket)
 
     def test_props_format_wrap_bothsides_unicode(self):
@@ -1011,7 +1087,7 @@
                               u'に含まれている[3:CОPYING]ファイ' \
                               u'ルと同じものが[2:オンライン]で' \
                               u'参照できます。'
-        ticket['version'] = u'2.0'
+        ticket['version'] = u'2.0 International Edition'
         ticket['resolution'] = u'fixed'
         ticket['keywords'] = u''
         ticket.insert()
@@ -1022,17 +1098,74 @@
  Component:  Trac は BSD ライセンス  |  议下发布。[1:]协议的完整文本可以[2:
   のもとで配布されています。[1:]こ   |  在线查看]也可在发布版的 [3:COPYING]
   のライセンスの全文は、※配布ファ   |  文件中找到。
-  イルに含まれている[3:CОPYING]フ   |    Version:  2.0
-  ァイルと同じものが[2:オンライン]   |   Keywords:
+  イルに含まれている[3:CОPYING]フ   |    Version:  2.0 International
+  ァイルと同じものが[2:オンライン]   |  Edition
   で参照できます。                   |
-Resolution:  fixed                   |"""
+Resolution:  fixed                   |   Keywords:"""
+        self._validate_props_format(formatted, ticket)
+
+    def test_props_format_wrap_ticket_10283(self):
+        self.env.config.set('notification', 'mime_encoding', 'none')
+        for name, value in (('blockedby', 'text'),
+                            ('blockedby.label', 'Blocked by'),
+                            ('blockedby.order', '6'),
+                            ('blocking', 'text'),
+                            ('blocking.label', 'Blocking'),
+                            ('blocking.order', '5'),
+                            ('deployment', 'text'),
+                            ('deployment.label', 'Deployment state'),
+                            ('deployment.order', '1'),
+                            ('nodes', 'text'),
+                            ('nodes.label', 'Related nodes'),
+                            ('nodes.order', '3'),
+                            ('privacy', 'text'),
+                            ('privacy.label', 'Privacy sensitive'),
+                            ('privacy.order', '2'),
+                            ('sensitive', 'text'),
+                            ('sensitive.label', 'Security sensitive'),
+                            ('sensitive.order', '4')):
+            self.env.config.set('ticket-custom', name, value)
+
+        ticket = Ticket(self.env)
+        ticket['summary'] = u'This is a summary'
+        ticket['reporter'] = u'anonymous'
+        ticket['owner'] = u'somebody'
+        ticket['type'] = u'defect'
+        ticket['status'] = u'closed'
+        ticket['priority'] = u'normal'
+        ticket['milestone'] = u'iter_01'
+        ticket['component'] = u'XXXXXXXXXXXXXXXXXXXXXXXXXX'
+        ticket['resolution'] = u'fixed'
+        ticket['keywords'] = u''
+        ticket['deployment'] = ''
+        ticket['privacy'] = '0'
+        ticket['nodes'] = 'XXXXXXXXXX'
+        ticket['sensitive'] = '0'
+        ticket['blocking'] = ''
+        ticket['blockedby'] = ''
+        ticket.insert()
+
+        formatted = """\
+          Reporter:  anonymous                   |             Owner:
+                                                 |  somebody
+              Type:  defect                      |            Status:
+                                                 |  closed
+          Priority:  normal                      |         Milestone:
+                                                 |  iter_01
+         Component:  XXXXXXXXXXXXXXXXXXXXXXXXXX  |        Resolution:
+                                                 |  fixed
+          Keywords:                              |  Deployment state:
+ Privacy sensitive:  0                           |     Related nodes:
+                                                 |  XXXXXXXXXX
+Security sensitive:  0                           |          Blocking:
+        Blocked by:                              |"""
         self._validate_props_format(formatted, ticket)
 
     def _validate_props_format(self, expected, ticket):
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
         message = notifysuite.smtpd.get_message()
-        (headers, body) = parse_smtp_message(message)
+        headers, body = parse_smtp_message(message)
         bodylines = body.splitlines()
         # Extract ticket properties
         delim_re = re.compile(r'^\-+\+\-+$')
@@ -1052,7 +1185,7 @@
         ticket.insert()
         tn = TicketNotifyEmail(self.env)
         tn.notify(ticket, newticket=True)
-        self.assertNotEqual(None, notifysuite.smtpd.get_message())
+        self.assertIsNotNone(notifysuite.smtpd.get_message())
         self.assertEqual('My Summary', ticket['summary'])
         self.assertEqual('Some description', ticket['description'])
         valid_fieldnames = set([f['name'] for f in ticket.fields])
@@ -1069,6 +1202,52 @@
         tn.ticket = ticket
         tn.get_message_id('foo')
 
+    def test_mime_meta_characters_in_from_header(self):
+        """MIME encoding with meta characters in From header"""
+
+        self.env.config.set('notification', 'smtp_from', 'trac@example.com')
+        self.env.config.set('notification', 'mime_encoding', 'base64')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joeuser'
+        ticket['summary'] = 'This is a summary'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+
+        def notify(from_name):
+            self.env.config.set('notification', 'smtp_from_name', from_name)
+            tn.notify(ticket, newticket=True)
+            message = notifysuite.smtpd.get_message()
+            headers, body = parse_smtp_message(message)
+            return message, headers, body
+
+        message, headers, body = notify(u'Träc')
+        self.assertEqual(r'"=?utf-8?b?VHLDpGM=?=" <trac@example.com>',
+                         headers['From'])
+        message, headers, body = notify(u'Trac\\')
+        self.assertEqual(r'"Trac\\" <trac@example.com>', headers['From'])
+        message, headers, body = notify(u'Trac"')
+        self.assertEqual(r'"Trac\"" <trac@example.com>', headers['From'])
+        message, headers, body = notify(u'=?utf-8?b?****?=')
+        self.assertEqual('"=?utf-8?b?PT91dGYtOD9iPyoqKio/PQ==?=" '
+                         '<trac@example.com>', headers['From'])
+
+    def test_mime_meta_characters_in_subject_header(self):
+        """MIME encoding with meta characters in Subject header"""
+
+        self.env.config.set('notification', 'smtp_from', 'trac@example.com')
+        self.env.config.set('notification', 'mime_encoding', 'base64')
+        summary = u'=?utf-8?q?****?='
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joeuser'
+        ticket['summary'] = summary
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, newticket=True)
+        message = notifysuite.smtpd.get_message()
+        headers, body = parse_smtp_message(message)
+        self.assertIn('\nSubject: =?utf-8?b?', message)  # is mime-encoded
+        self.assertEqual(summary,
+                         re.split(r' #[0-9]+: ', headers['Subject'], 1)[1])
 
 
 class NotificationTestSuite(unittest.TestSuite):
@@ -1079,18 +1258,20 @@
         unittest.TestSuite.__init__(self)
         self.smtpd = SMTPThreadedServer(SMTP_TEST_PORT)
         self.smtpd.start()
-        self.addTest(unittest.makeSuite(NotificationTestCase, 'test'))
+        self.addTest(unittest.makeSuite(RecipientTestCase))
+        self.addTest(unittest.makeSuite(NotificationTestCase))
         self.remaining = self.countTestCases()
 
     def tear_down(self):
         """Reset the local SMTP test server"""
         self.smtpd.cleanup()
-        self.remaining = self.remaining-1
+        self.remaining -= 1
         if self.remaining > 0:
             return
         # stop the SMTP test server when all tests have been completed
         self.smtpd.stop()
 
+
 def suite():
     global notifysuite
     if not notifysuite:
@@ -1098,4 +1279,4 @@
     return notifysuite
 
 if __name__ == '__main__':
-    unittest.TextTestRunner(verbosity=2).run(suite())
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/ticket/tests/query.py b/trac/trac/ticket/tests/query.py
index 9431627..b30cff6 100644
--- a/trac/trac/ticket/tests/query.py
+++ b/trac/trac/ticket/tests/query.py
@@ -1,9 +1,24 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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/.
+
 from trac.test import Mock, EnvironmentStub, MockPerm, locale_en
+from trac.ticket.model import Ticket
 from trac.ticket.query import Query, QueryModule, TicketQueryMacro
 from trac.util.datefmt import utc
 from trac.web.chrome import web_context
 from trac.web.href import Href
 from trac.wiki.formatter import LinkFormatter
+from trac.wiki.tests import formatter
 
 import unittest
 import difflib
@@ -257,12 +272,14 @@
         sql, args = query.get_sql()
         foo = self.env.get_read_db().quote('foo')
         self.assertEqualSQL(sql,
-"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value,%s.value AS %s
-FROM ticket AS t
-  LEFT OUTER JOIN ticket_custom AS %s ON (id=%s.ticket AND %s.name='foo')
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value,t.%s AS %s
+FROM (
+  SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,
+  (SELECT c.value FROM ticket_custom c WHERE c.ticket=t.id AND c.name='foo') AS %s
+  FROM ticket AS t) AS t
   LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
-WHERE ((COALESCE(%s.value,'')=%%s))
-ORDER BY COALESCE(t.id,0)=0,t.id""" % ((foo,) * 6))
+WHERE ((COALESCE(t.%s,'')=%%s))
+ORDER BY COALESCE(t.id,0)=0,t.id""" % ((foo,) * 4))
         self.assertEqual(['something'], args)
         tickets = query.execute(self.req)
 
@@ -272,15 +289,77 @@
         sql, args = query.get_sql()
         foo = self.env.get_read_db().quote('foo')
         self.assertEqualSQL(sql,
-"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value,%s.value AS %s
-FROM ticket AS t
-  LEFT OUTER JOIN ticket_custom AS %s ON (id=%s.ticket AND %s.name='foo')
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value,t.%s AS %s
+FROM (
+  SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,
+  (SELECT c.value FROM ticket_custom c WHERE c.ticket=t.id AND c.name='foo') AS %s
+  FROM ticket AS t) AS t
   LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
-ORDER BY COALESCE(%s.value,'')='',%s.value,COALESCE(t.id,0)=0,t.id""" %
-        ((foo,) * 7))
+ORDER BY COALESCE(t.%s,'')='',t.%s,COALESCE(t.id,0)=0,t.id""" %
+        ((foo,) * 5))
         self.assertEqual([], args)
         tickets = query.execute(self.req)
 
+    def test_constrained_by_id_ranges(self):
+        query = Query.from_string(self.env, 'id=42,44,51-55&order=id')
+        sql, args = query.get_sql()
+        self.assertEqualSQL(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE ((t.id BETWEEN %s AND %s OR t.id IN (42,44)))
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([51, 55], args)
+
+    def test_constrained_by_id_and_custom_field(self):
+        self.env.config.set('ticket-custom', 'foo', 'text')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe'
+        ticket['summary'] = 'Foo'
+        ticket['foo'] = 'blah'
+        ticket.insert()
+
+        query = Query.from_string(self.env, 'id=%d-42&foo=blah' % ticket.id)
+        tickets = query.execute(self.req)
+        self.assertEqual(1, len(tickets))
+        self.assertEqual(ticket.id, tickets[0]['id'])
+
+        query = Query.from_string(self.env, 'id=%d,42&foo=blah' % ticket.id)
+        tickets = query.execute(self.req)
+        self.assertEqual(1, len(tickets))
+        self.assertEqual(ticket.id, tickets[0]['id'])
+
+        query = Query.from_string(self.env, 'id=%d,42,43-84&foo=blah' %
+                                            ticket.id)
+        tickets = query.execute(self.req)
+        self.assertEqual(1, len(tickets))
+        self.assertEqual(ticket.id, tickets[0]['id'])
+
+    def test_too_many_custom_fields(self):
+        fields = ['col_%02d' % i for i in xrange(100)]
+        for f in fields:
+            self.env.config.set('ticket-custom', f, 'text')
+
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe'
+        ticket['summary'] = 'Foo'
+        for idx, f in enumerate(fields):
+            ticket[f] = '%d.%s' % (idx, f)
+        ticket.insert()
+
+        string = 'col_00=0.col_00&order=id&col=id&col=reporter&col=summary' + \
+                 ''.join('&col=' + f for f in fields)
+        query = Query.from_string(self.env, string)
+        tickets = query.execute(self.req)
+        self.assertEqual(ticket.id, tickets[0]['id'])
+        self.assertEqual('joe', tickets[0]['reporter'])
+        self.assertEqual('Foo', tickets[0]['summary'])
+        self.assertEqual('0.col_00', tickets[0]['col_00'])
+        self.assertEqual('99.col_99', tickets[0]['col_99'])
+
+        query = Query.from_string(self.env, 'col_00=notfound')
+        self.assertEqual([], query.execute(self.req))
+
     def test_constrained_by_multiple_owners(self):
         query = Query.from_string(self.env, 'owner=someone|someone_else',
                                   order='id')
@@ -503,6 +582,31 @@
         self.assertEqual('\xef\xbb\xbfcol1\r\n"value, needs escaped"\r\n',
                          content)
 
+    def test_csv_obfuscation(self):
+        class NoEmailView(MockPerm):
+            def has_permission(self, action, realm_or_resource=None, id=False,
+                               version=False):
+                return action != 'EMAIL_VIEW'
+            __contains__ = has_permission
+
+        query = Mock(get_columns=lambda: ['owner', 'reporter', 'cc'],
+                     execute=lambda r: [{'id': 1,
+                                         'owner': 'joe@example.org',
+                                         'reporter': 'foo@example.org',
+                                         'cc': 'cc1@example.org, cc2'}],
+                     time_fields=['time', 'changetime'])
+        req = Mock(href=self.env.href, perm=NoEmailView())
+        content, mimetype = QueryModule(self.env).export_csv(req, query)
+        self.assertEqual(u'\uFEFFowner,reporter,cc\r\n'
+                         u'joe@…,foo@…,"cc1@…, cc2"\r\n',
+                         content.decode('utf-8'))
+        req = Mock(href=self.env.href, perm=MockPerm())
+        content, mimetype = QueryModule(self.env).export_csv(req, query)
+        self.assertEqual(
+            u'\uFEFFowner,reporter,cc\r\n'
+            u'joe@example.org,foo@example.org,"cc1@example.org, cc2"\r\n',
+            content.decode('utf-8'))
+
     def test_template_data(self):
         req = Mock(href=self.env.href, perm=MockPerm(), authname='anonymous',
                    tz=None, locale=None)
@@ -576,12 +680,267 @@
                            dict(col='status|summary', max='0', order='id'),
                            'list')
 
+QUERY_TEST_CASES = u"""
+============================== TicketQuery(format=progress)
+[[TicketQuery(format=progress)]]
+------------------------------
+<p>
+</p><div class="trac-progress">
+
+  <table xmlns="http://www.w3.org/1999/xhtml" class="progress">
+    <tr>
+      <td class="closed" style="width: 33%">
+        <a href="/query?status=closed&amp;group=resolution&amp;max=0&amp;order=time" title="1/3 closed"></a>
+      </td><td class="open" style="width: 67%">
+        <a href="/query?status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;max=0&amp;order=id" title="2/3 active"></a>
+      </td>
+    </tr>
+  </table>
+
+  <p class="percent">33%</p>
+
+  <p class="legend">
+    <span class="first interval">
+      <a href="/query?max=0&amp;order=id">Total number of tickets: 3</a>
+    </span>
+    <span class="interval">
+      - <a href="/query?status=closed&amp;group=resolution&amp;max=0&amp;order=time">closed: 1</a>
+    </span><span class="interval">
+      - <a href="/query?status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;max=0&amp;order=id">active: 2</a>
+    </span>
+  </p>
+</div><p>
+</p>
+------------------------------
+============================== TicketQuery(reporter=santa, format=progress)
+[[TicketQuery(reporter=santa, format=progress)]]
+------------------------------
+<p>
+</p><div class="trac-progress">
+
+  <table xmlns="http://www.w3.org/1999/xhtml" class="progress">
+    <tr>
+      <td class="closed" style="display: none">
+        <a href="/query?status=closed&amp;reporter=santa&amp;group=resolution&amp;max=0&amp;order=time" title="0/1 closed"></a>
+      </td><td class="open" style="width: 100%">
+        <a href="/query?status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;reporter=santa&amp;max=0&amp;order=id" title="1/1 active"></a>
+      </td>
+    </tr>
+  </table>
+
+  <p class="percent">0%</p>
+
+  <p class="legend">
+    <span class="first interval">
+      <a href="/query?reporter=santa&amp;max=0&amp;order=id">Total number of tickets: 1</a>
+    </span>
+    <span class="interval">
+      - <a href="/query?status=closed&amp;reporter=santa&amp;group=resolution&amp;max=0&amp;order=time">closed: 0</a>
+    </span><span class="interval">
+      - <a href="/query?status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;reporter=santa&amp;max=0&amp;order=id">active: 1</a>
+    </span>
+  </p>
+</div><p>
+</p>
+------------------------------
+============================== TicketQuery(reporter=santa&or&owner=santa, format=progress)
+[[TicketQuery(reporter=santa&or&owner=santa, format=progress)]]
+------------------------------
+<p>
+</p><div class="trac-progress">
+
+  <table xmlns="http://www.w3.org/1999/xhtml" class="progress">
+    <tr>
+      <td class="closed" style="width: 50%">
+        <a href="/query?status=closed&amp;reporter=santa&amp;or&amp;owner=santa&amp;status=closed&amp;group=resolution&amp;max=0&amp;order=time" title="1/2 closed"></a>
+      </td><td class="open" style="width: 50%">
+        <a href="/query?status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;reporter=santa&amp;or&amp;owner=santa&amp;status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;max=0&amp;order=id" title="1/2 active"></a>
+      </td>
+    </tr>
+  </table>
+
+  <p class="percent">50%</p>
+
+  <p class="legend">
+    <span class="first interval">
+      <a href="/query?reporter=santa&amp;or&amp;owner=santa&amp;max=0&amp;order=id">Total number of tickets: 2</a>
+    </span>
+    <span class="interval">
+      - <a href="/query?status=closed&amp;reporter=santa&amp;or&amp;owner=santa&amp;status=closed&amp;group=resolution&amp;max=0&amp;order=time">closed: 1</a>
+    </span><span class="interval">
+      - <a href="/query?status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;reporter=santa&amp;or&amp;owner=santa&amp;status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;max=0&amp;order=id">active: 1</a>
+    </span>
+  </p>
+</div><p>
+</p>
+------------------------------
+============================== TicketQuery(format=progress, group=project)
+[[TicketQuery(format=progress, group=project)]]
+------------------------------
+<p>
+</p><div class="trac-groupprogress">
+  <table xmlns="http://www.w3.org/1999/xhtml" summary="Ticket completion status for each project">
+    <tr>
+      <th scope="row">
+        <i><a href="/query?project=&amp;max=0&amp;order=id">(none)</a></i>
+
+
+      </th>
+      <td>
+
+
+  <table class="progress" style="width: 40%">
+    <tr>
+      <td class="closed" style="display: none">
+        <a href="/query?project=&amp;status=closed&amp;group=resolution&amp;max=0&amp;order=time" title="0/1 closed"></a>
+      </td><td class="open" style="width: 100%">
+        <a href="/query?project=&amp;status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;max=0&amp;order=id" title="1/1 active"></a>
+      </td>
+    </tr>
+  </table>
+
+  <p class="percent">0 / 1</p>
+
+
+
+      </td>
+    </tr><tr>
+      <th scope="row">
+
+
+        <a href="/query?project=xmas&amp;max=0&amp;order=id">xmas</a>
+      </th>
+      <td>
+
+
+  <table class="progress" style="width: 80%">
+    <tr>
+      <td class="closed" style="width: 50%">
+        <a href="/query?project=xmas&amp;status=closed&amp;group=resolution&amp;max=0&amp;order=time" title="1/2 closed"></a>
+      </td><td class="open" style="width: 50%">
+        <a href="/query?project=xmas&amp;status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;max=0&amp;order=id" title="1/2 active"></a>
+      </td>
+    </tr>
+  </table>
+
+  <p class="percent">1 / 2</p>
+
+
+
+      </td>
+    </tr>
+  </table>
+</div><p>
+</p>
+------------------------------
+============================== TicketQuery(reporter=santa, format=progress, group=project)
+[[TicketQuery(reporter=santa, format=progress, group=project)]]
+------------------------------
+<p>
+</p><div class="trac-groupprogress">
+  <table xmlns="http://www.w3.org/1999/xhtml" summary="Ticket completion status for each project">
+    <tr>
+      <th scope="row">
+
+
+        <a href="/query?project=xmas&amp;reporter=santa&amp;max=0&amp;order=id">xmas</a>
+      </th>
+      <td>
+
+
+  <table class="progress" style="width: 80%">
+    <tr>
+      <td class="closed" style="display: none">
+        <a href="/query?project=xmas&amp;status=closed&amp;reporter=santa&amp;group=resolution&amp;max=0&amp;order=time" title="0/1 closed"></a>
+      </td><td class="open" style="width: 100%">
+        <a href="/query?project=xmas&amp;status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;reporter=santa&amp;max=0&amp;order=id" title="1/1 active"></a>
+      </td>
+    </tr>
+  </table>
+
+  <p class="percent">0 / 1</p>
+
+
+
+      </td>
+    </tr>
+  </table>
+</div><p>
+</p>
+------------------------------
+============================== TicketQuery(reporter=santa&or&owner=santa, format=progress, group=project)
+[[TicketQuery(reporter=santa&or&owner=santa, format=progress, group=project)]]
+------------------------------
+<p>
+</p><div class="trac-groupprogress">
+  <table xmlns="http://www.w3.org/1999/xhtml" summary="Ticket completion status for each project">
+    <tr>
+      <th scope="row">
+
+
+        <a href="/query?project=xmas&amp;reporter=santa&amp;or&amp;owner=santa&amp;project=xmas&amp;max=0&amp;order=id">xmas</a>
+      </th>
+      <td>
+
+
+  <table class="progress" style="width: 80%">
+    <tr>
+      <td class="closed" style="width: 50%">
+        <a href="/query?project=xmas&amp;status=closed&amp;reporter=santa&amp;or&amp;owner=santa&amp;project=xmas&amp;status=closed&amp;group=resolution&amp;max=0&amp;order=time" title="1/2 closed"></a>
+      </td><td class="open" style="width: 50%">
+        <a href="/query?project=xmas&amp;status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;reporter=santa&amp;or&amp;owner=santa&amp;project=xmas&amp;status=assigned&amp;status=new&amp;status=accepted&amp;status=reopened&amp;max=0&amp;order=id" title="1/2 active"></a>
+      </td>
+    </tr>
+  </table>
+
+  <p class="percent">1 / 2</p>
+
+
+
+      </td>
+    </tr>
+  </table>
+</div><p>
+</p>
+------------------------------
+"""
+
+def ticket_setup(tc):
+    tc.env.config.set('ticket-custom', 'project', 'text')
+    ticket = Ticket(tc.env)
+    ticket.values.update({'reporter': 'santa',
+                          'summary': 'This is the summary',
+                          'status': 'new',
+                          'project': 'xmas'})
+    ticket.insert()
+    ticket = Ticket(tc.env)
+    ticket.values.update({'owner': 'elf',
+                          'summary': 'This is another summary',
+                          'status': 'assigned'})
+    ticket.insert()
+    ticket = Ticket(tc.env)
+    ticket.values.update({'owner': 'santa',
+                          'summary': 'This is th third summary',
+                          'status': 'closed',
+                          'project': 'xmas'})
+    ticket.insert()
+
+    tc.env.config.set('milestone-groups', 'closed.status', 'closed')
+    tc.env.config.set('milestone-groups', 'closed.query_args', 'group=resolution,order=time')
+    tc.env.config.set('milestone-groups', 'closed.overall_completion', 'true')
+    tc.env.config.set('milestone-groups', 'active.status', '*')
+    tc.env.config.set('milestone-groups', 'active.css_class', 'open')
+
+def ticket_teardown(tc):
+    tc.env.reset_db()
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(QueryTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(QueryLinksTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TicketQueryMacroTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(QueryTestCase))
+    suite.addTest(unittest.makeSuite(QueryLinksTestCase))
+    suite.addTest(unittest.makeSuite(TicketQueryMacroTestCase))
+    suite.addTest(formatter.suite(QUERY_TEST_CASES, ticket_setup, __file__,
+                                  ticket_teardown))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/ticket/tests/report.py b/trac/trac/ticket/tests/report.py
index de8f88f..eeee0d4 100644
--- a/trac/trac/ticket/tests/report.py
+++ b/trac/trac/ticket/tests/report.py
@@ -1,16 +1,35 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2014 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 __future__ import with_statement
 
 import doctest
-
-from trac.db.mysql_backend import MySQLConnection
-from trac.ticket.report import ReportModule
-from trac.test import EnvironmentStub, Mock
-from trac.web.api import Request, RequestDone
-import trac
+from datetime import datetime, timedelta
 
 import unittest
 from StringIO import StringIO
 
+import trac.tests.compat
+from trac.db.mysql_backend import MySQLConnection
+from trac.ticket.model import Ticket
+from trac.ticket.report import ReportModule
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.util.datefmt import utc
+from trac.web.api import Request, RequestDone
+from trac.web.href import Href
+import trac
+
+
 class MockMySQLConnection(MySQLConnection):
     def __init__(self):
         pass
@@ -99,11 +118,409 @@
                          'type=r%C3%A9sum%C3%A9&report=' + str(id),
                          headers_sent['Location'])
 
+    def test_quoted_id_with_var(self):
+        req = Mock(base_path='', chrome={}, args={}, session={},
+                   abs_href=Href('/'), href=Href('/'), locale='',
+                   perm=MockPerm(), authname=None, tz=None)
+        db = self.env.get_read_db()
+        name = """%s"`'%%%?"""
+        sql = 'SELECT 1 AS %s, $USER AS user' % db.quote(name)
+        rv = self.report_module.execute_paginated_report(req, db, 1, sql,
+                                                         {'USER': 'joe'})
+        self.assertEqual(5, len(rv), repr(rv))
+        cols, results, num_items, missing_args, limit_offset = rv
+        self.assertEqual([name, 'user'], cols)
+        self.assertEqual([(1, 'joe')], results)
+        self.assertEqual([], missing_args)
+        self.assertEqual(None, limit_offset)
+
+
+class ExecuteReportTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        self.req = Mock(base_path='', chrome={}, args={}, session={},
+                        abs_href=Href('/'), href=Href('/'), locale='',
+                        perm=MockPerm(), authname=None, tz=None)
+        self.report_module = ReportModule(self.env)
+
+    def tearDown(self):
+        self.env.reset_db()
+
+    def _insert_ticket(self, when=None, **kwargs):
+        ticket = Ticket(self.env)
+        for name, value in kwargs.iteritems():
+            ticket[name] = value
+        ticket['status'] = 'new'
+        ticket.insert(when=when)
+        return ticket
+
+    def _save_ticket(self, ticket, author=None, comment=None, when=None,
+                     **kwargs):
+        if when is None:
+            when = ticket['changetime'] + timedelta(microseconds=1)
+        for name, value in kwargs.iteritems():
+            ticket[name] = value
+        return ticket.save_changes(author=author, comment=comment, when=when)
+
+    def _execute_report(self, id, args=None):
+        mod = self.report_module
+        req = self.req
+        with self.env.db_query as db:
+            title, description, sql = mod.get_report(id)
+            return mod.execute_paginated_report(req, db, id, sql, args or {})
+
+    def _generate_tickets(self, columns, data, attrs):
+        with self.env.db_transaction as db:
+            tickets = []
+            when = datetime(2014, 1, 1, 0, 0, 0, 0, utc)
+            for idx, line in enumerate(data.splitlines()):
+                line = line.strip()
+                if not line or line.startswith('#'):
+                    continue
+                values = line.split()
+                assert len(columns) == len(values), 'Line %d' % (idx + 1)
+                summary = ' '.join(values)
+                values = map(lambda v: None if v == 'None' else v, values)
+                d = attrs.copy()
+                d['summary'] = summary
+                d.update(zip(columns, values))
+
+                status = None
+                if 'status' in d:
+                    status = d.pop('status')
+                ticket = self._insert_ticket(when=when, status='new', **d)
+                if status != 'new':
+                    self._save_ticket(ticket, status=status,
+                                      when=when + timedelta(microseconds=1))
+                tickets.append(ticket)
+                when += timedelta(seconds=1)
+            return tickets
+
+    REPORT_1_DATA = """\
+        # status    priority
+        new         minor
+        new         major
+        new         critical
+        closed      minor
+        closed      major
+        closed      critical"""
+
+    def test_report_1_active_tickets(self):
+        attrs = dict(reporter='joe', component='component1', version='1.0',
+                     milestone='milestone1', type='defect', owner='joe')
+        self._generate_tickets(('status', 'priority'), self.REPORT_1_DATA,
+                               attrs)
+
+        rv = self._execute_report(1)
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        idx_summary = cols.index('summary')
+        self.assertEqual(['new critical',
+                          'new major',
+                          'new minor'],
+                         [r[idx_summary] for r in results])
+        idx_color = cols.index('__color__')
+        self.assertEqual(set(('2', '3', '4')),
+                         set(r[idx_color] for r in results))
+
+    REPORT_2_DATA = """\
+        # status    version     priority
+        new         2.0         minor
+        new         2.0         critical
+        new         1.0         minor
+        new         1.0         critical
+        new         None        minor
+        new         None        critical
+        closed      2.0         minor
+        closed      2.0         critical
+        closed      1.0         minor
+        closed      1.0         critical
+        closed      None        minor
+        closed      None        critical"""
+
+    def test_report_2_active_tickets_by_version(self):
+        attrs = dict(reporter='joe', component='component1',
+                     milestone='milestone1', type='defect', owner='joe')
+        self._generate_tickets(('status', 'version', 'priority'),
+                                self.REPORT_2_DATA, attrs)
+
+        rv = self._execute_report(2)
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        idx_summary = cols.index('summary')
+        self.assertEqual(['new 1.0 critical',
+                          'new 1.0 minor',
+                          'new 2.0 critical',
+                          'new 2.0 minor',
+                          'new None critical',
+                          'new None minor'],
+                         [r[idx_summary] for r in results])
+        idx_color = cols.index('__color__')
+        self.assertEqual(set(('2', '4')),
+                         set(r[idx_color] for r in results))
+        idx_group = cols.index('__group__')
+        self.assertEqual(set(('1.0', '2.0', None)),
+                         set(r[idx_group] for r in results))
+
+    REPORT_3_DATA = """\
+        # status    milestone   priority
+        new         milestone3  minor
+        new         milestone3  major
+        new         milestone1  minor
+        new         milestone1  major
+        new         None        minor
+        new         None        major
+        closed      milestone3  minor
+        closed      milestone3  major
+        closed      milestone1  minor
+        closed      milestone1  major
+        closed      None        minor
+        closed      None        major"""
+
+    def test_report_3_active_tickets_by_milestone(self):
+        attrs = dict(reporter='joe', component='component1', version='1.0',
+                     type='defect', owner='joe')
+        self._generate_tickets(('status', 'milestone', 'priority'),
+                                self.REPORT_3_DATA, attrs)
+
+        rv = self._execute_report(3)
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        idx_summary = cols.index('summary')
+        self.assertEqual(['new milestone1 major',
+                          'new milestone1 minor',
+                          'new milestone3 major',
+                          'new milestone3 minor',
+                          'new None major',
+                          'new None minor'],
+                         [r[idx_summary] for r in results])
+        idx_color = cols.index('__color__')
+        self.assertEqual(set(('3', '4')),
+                         set(r[idx_color] for r in results))
+        idx_group = cols.index('__group__')
+        self.assertEqual(set(('Milestone milestone1', 'Milestone milestone3',
+                              None)),
+                         set(r[idx_group] for r in results))
+
+    REPORT_4_DATA = """\
+        # status    owner   priority
+        new         john    trivial
+        new         john    blocker
+        new         jack    trivial
+        new         jack    blocker
+        new         foo     trivial
+        new         foo     blocker
+        accepted    john    trivial
+        accepted    john    blocker
+        accepted    jack    trivial
+        accepted    jack    blocker
+        accepted    foo     trivial
+        accepted    foo     blocker
+        closed      john    trivial
+        closed      john    blocker
+        closed      jack    trivial
+        closed      jack    blocker
+        closed      foo     trivial
+        closed      foo     blocker"""
+
+    def _test_active_tickets_by_owner(self, report_id, full_description=False):
+        attrs = dict(reporter='joe', component='component1',
+                     milestone='milestone1', version='1.0', type='defect')
+        self._generate_tickets(('status', 'owner', 'priority'),
+                                self.REPORT_4_DATA, attrs)
+
+        rv = self._execute_report(report_id)
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        idx_summary = cols.index('summary')
+        self.assertEqual(['accepted foo blocker',
+                          'accepted foo trivial',
+                          'accepted jack blocker',
+                          'accepted jack trivial',
+                          'accepted john blocker',
+                          'accepted john trivial'],
+                         [r[idx_summary] for r in results])
+        idx_color = cols.index('__color__')
+        self.assertEqual(set(('1', '5')),
+                         set(r[idx_color] for r in results))
+        idx_group = cols.index('__group__')
+        self.assertEqual(set(('jack', 'john', 'foo')),
+                         set(r[idx_group] for r in results))
+        if full_description:
+            self.assertNotIn('_description', cols)
+            self.assertIn('_description_', cols)
+        else:
+            self.assertNotIn('_description_', cols)
+            self.assertIn('_description', cols)
+
+    def test_report_4_active_tickets_by_owner(self):
+        self._test_active_tickets_by_owner(4, full_description=False)
+
+    def test_report_5_active_tickets_by_owner_full_description(self):
+        self._test_active_tickets_by_owner(5, full_description=True)
+
+    REPORT_6_DATA = """\
+        # status    milestone  priority owner
+        new         milestone4 trivial  john
+        new         milestone4 critical jack
+        new         milestone2 trivial  jack
+        new         milestone2 critical john
+        new         None       trivial  john
+        new         None       critical jack
+        closed      milestone4 trivial  jack
+        closed      milestone4 critical john
+        closed      milestone2 trivial  john
+        closed      milestone2 critical jack
+        closed      None       trivial  jack
+        closed      None       critical john"""
+
+    def test_report_6_all_tickets_by_milestone(self):
+        attrs = dict(reporter='joe', component='component1', version='1.0',
+                     type='defect')
+        self._generate_tickets(('status', 'milestone', 'priority', 'owner'),
+                                self.REPORT_6_DATA, attrs)
+
+        rv = self._execute_report(6, {'USER': 'john'})
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        idx_summary = cols.index('summary')
+        self.assertEqual(['new milestone4 critical jack',
+                          'new milestone4 trivial john',
+                          'closed milestone4 critical john',
+                          'closed milestone4 trivial jack',
+                          'new milestone2 critical john',
+                          'new milestone2 trivial jack',
+                          'closed milestone2 critical jack',
+                          'closed milestone2 trivial john',
+                          'new None critical jack',
+                          'new None trivial john',
+                          'closed None critical john',
+                          'closed None trivial jack'],
+                         [r[idx_summary] for r in results])
+        idx_style = cols.index('__style__')
+        self.assertEqual('color: #777; background: #ddd; border-color: #ccc;',
+                         results[2][idx_style])  # closed and owned
+        self.assertEqual('color: #777; background: #ddd; border-color: #ccc;',
+                         results[3][idx_style])  # closed and not owned
+        self.assertEqual('font-weight: bold',
+                         results[1][idx_style])  # not closed and owned
+        self.assertEqual(None,
+                         results[0][idx_style])  # not closed and not owned
+        idx_color = cols.index('__color__')
+        self.assertEqual(set(('2', '5')),
+                         set(r[idx_color] for r in results))
+        idx_group = cols.index('__group__')
+        self.assertEqual(set(('milestone2', 'milestone4', None)),
+                         set(r[idx_group] for r in results))
+
+    REPORT_7_DATA = """\
+        # status    owner   reporter    priority
+        accepted    john    foo         minor
+        accepted    john    foo         critical
+        accepted    foo     foo         major
+        new         john    foo         minor
+        new         john    foo         blocker
+        new         foo     foo         major
+        closed      john    foo         major
+        closed      foo     foo         major
+        new         foo     foo         major
+        new         foo     john        trivial
+        new         foo     john        major
+        closed      foo     foo         major
+        closed      foo     john        major
+        new         foo     bar         major
+        new         bar     foo         major"""
+
+    def test_report_7_my_tickets(self):
+        attrs = dict(component='component1', milestone='milestone1',
+                     version='1.0', type='defect')
+        tickets = self._generate_tickets(
+            ('status', 'owner', 'reporter', 'priority'), self.REPORT_7_DATA,
+            attrs)
+
+        rv = self._execute_report(7, {'USER': 'john'})
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        idx_summary = cols.index('summary')
+        self.assertEqual(['accepted john foo critical',
+                          'accepted john foo minor',
+                          'new john foo blocker',
+                          'new john foo minor',
+                          'new foo john major',
+                          'new foo john trivial'],
+                         [r[idx_summary] for r in results])
+        idx_group = cols.index('__group__')
+        self.assertEqual(set(('Accepted', 'Owned', 'Reported')),
+                         set(r[idx_group] for r in results))
+
+        self._save_ticket(tickets[-1], author='john', comment='commented')
+        rv = self._execute_report(7, {'USER': 'john'})
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        self.assertEqual(7, len(results))
+        self.assertEqual('new bar foo major', results[-1][idx_summary])
+        self.assertEqual(set(('Accepted', 'Owned', 'Reported', 'Commented')),
+                         set(r[idx_group] for r in results))
+
+        rv = self._execute_report(7, {'USER': 'blah <blah@example.org>'})
+        cols, results, num_items, missing_args, limit_offset = rv
+        self.assertEqual(0, len(results))
+
+        self._save_ticket(tickets[-1], author='blah <blah@example.org>',
+                          comment='from anonymous')
+        rv = self._execute_report(7, {'USER': 'blah <blah@example.org>'})
+        cols, results, num_items, missing_args, limit_offset = rv
+        self.assertEqual(1, len(results))
+        self.assertEqual('new bar foo major', results[0][idx_summary])
+        self.assertEqual('Commented', results[0][idx_group])
+
+    REPORT_8_DATA = """\
+        # status    owner   priority
+        new         foo     minor
+        new         foo     critical
+        new         john    minor
+        new         john    critical
+        closed      john    major
+        closed      foo     major"""
+
+    def test_report_8_active_tickets_mine_first(self):
+        attrs = dict(component='component1', milestone='milestone1',
+                     version='1.0', type='defect')
+        tickets = self._generate_tickets(('status', 'owner', 'priority'),
+                                         self.REPORT_8_DATA, attrs)
+
+        rv = self._execute_report(8, {'USER': 'john'})
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        idx_summary = cols.index('summary')
+        self.assertEqual(['new john critical',
+                          'new john minor',
+                          'new foo critical',
+                          'new foo minor'],
+                         [r[idx_summary] for r in results])
+        idx_group = cols.index('__group__')
+        self.assertEqual('My Tickets', results[1][idx_group])
+        self.assertEqual('Active Tickets', results[2][idx_group])
+
+        rv = self._execute_report(8, {'USER': 'anonymous'})
+        cols, results, num_items, missing_args, limit_offset = rv
+
+        self.assertEqual(['new foo critical',
+                          'new john critical',
+                          'new foo minor',
+                          'new john minor'],
+                         [r[idx_summary] for r in results])
+        idx_group = cols.index('__group__')
+        self.assertEqual(['Active Tickets'],
+                         sorted(set(r[idx_group] for r in results)))
+
 
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(trac.ticket.report))
-    suite.addTest(unittest.makeSuite(ReportTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ReportTestCase))
+    suite.addTest(unittest.makeSuite(ExecuteReportTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/ticket/tests/roadmap.py b/trac/trac/ticket/tests/roadmap.py
index c5aa4ce..c04746a 100644
--- a/trac/trac/ticket/tests/roadmap.py
+++ b/trac/trac/ticket/tests/roadmap.py
@@ -1,62 +1,78 @@
-from trac.test import EnvironmentStub
-from trac.ticket.roadmap import *
-from trac.core import ComponentManager
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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 unittest
 
+from trac.core import ComponentManager
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.tests.contentgen import random_sentence
+from trac.ticket.roadmap import *
+from trac.web.tests.api import RequestHandlerPermissionsTestCaseBase
+
+
 class TicketGroupStatsTestCase(unittest.TestCase):
 
     def setUp(self):
         self.stats = TicketGroupStats('title', 'units')
 
     def test_init(self):
-        self.assertEquals('title', self.stats.title, 'title incorrect')
-        self.assertEquals('units', self.stats.unit, 'unit incorrect')
-        self.assertEquals(0, self.stats.count, 'count not zero')
-        self.assertEquals(0, len(self.stats.intervals), 'intervals not empty')
+        self.assertEqual('title', self.stats.title, 'title incorrect')
+        self.assertEqual('units', self.stats.unit, 'unit incorrect')
+        self.assertEqual(0, self.stats.count, 'count not zero')
+        self.assertEqual(0, len(self.stats.intervals), 'intervals not empty')
 
     def test_add_iterval(self):
         self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0)
         self.stats.refresh_calcs()
-        self.assertEquals(3, self.stats.count, 'count not incremented')
+        self.assertEqual(3, self.stats.count, 'count not incremented')
         int = self.stats.intervals[0]
-        self.assertEquals('intTitle', int['title'], 'title incorrect')
-        self.assertEquals(3, int['count'], 'count incorrect')
-        self.assertEquals({'k1': 'v1'}, int['qry_args'], 'query args incorrect')
-        self.assertEquals('css', int['css_class'], 'css class incorrect')
-        self.assertEquals(100, int['percent'], 'percent incorrect')
+        self.assertEqual('intTitle', int['title'], 'title incorrect')
+        self.assertEqual(3, int['count'], 'count incorrect')
+        self.assertEqual({'k1': 'v1'}, int['qry_args'], 'query args incorrect')
+        self.assertEqual('css', int['css_class'], 'css class incorrect')
+        self.assertEqual(100, int['percent'], 'percent incorrect')
         self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0)
         self.stats.refresh_calcs()
-        self.assertEquals(50, int['percent'], 'percent not being updated')
+        self.assertEqual(50, int['percent'], 'percent not being updated')
 
     def test_add_interval_no_prog(self):
         self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0)
         self.stats.add_interval('intTitle', 5, {'k1': 'v1'}, 'css', 0)
         self.stats.refresh_calcs()
         interval = self.stats.intervals[1]
-        self.assertEquals(0, self.stats.done_count, 'count added for no prog')
-        self.assertEquals(0, self.stats.done_percent, 'percent incremented')
+        self.assertEqual(0, self.stats.done_count, 'count added for no prog')
+        self.assertEqual(0, self.stats.done_percent, 'percent incremented')
 
     def test_add_interval_prog(self):
         self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0)
         self.stats.add_interval('intTitle', 1, {'k1': 'v1'}, 'css', 1)
         self.stats.refresh_calcs()
-        self.assertEquals(4, self.stats.count, 'count not incremented')
-        self.assertEquals(1, self.stats.done_count, 'count not added to prog')
-        self.assertEquals(25, self.stats.done_percent, 'done percent not incr')
+        self.assertEqual(4, self.stats.count, 'count not incremented')
+        self.assertEqual(1, self.stats.done_count, 'count not added to prog')
+        self.assertEqual(25, self.stats.done_percent, 'done percent not incr')
 
     def test_add_interval_fudging(self):
         self.stats.add_interval('intTitle', 3, {'k1': 'v1'}, 'css', 0)
         self.stats.add_interval('intTitle', 5, {'k1': 'v1'}, 'css', 1)
         self.stats.refresh_calcs()
-        self.assertEquals(8, self.stats.count, 'count not incremented')
-        self.assertEquals(5, self.stats.done_count, 'count not added to prog')
-        self.assertEquals(62, self.stats.done_percent,
-                          'done percnt not fudged downward')
-        self.assertEquals(62, self.stats.intervals[1]['percent'],
-                          'interval percent not fudged downward')
-        self.assertEquals(38, self.stats.intervals[0]['percent'],
-                          'interval percent not fudged upward')
+        self.assertEqual(8, self.stats.count, 'count not incremented')
+        self.assertEqual(5, self.stats.done_count, 'count not added to prog')
+        self.assertEqual(62, self.stats.done_percent,
+                         'done percnt not fudged downward')
+        self.assertEqual(62, self.stats.intervals[1]['percent'],
+                         'interval percent not fudged downward')
+        self.assertEqual(38, self.stats.intervals[0]['percent'],
+                         'interval percent not fudged upward')
 
 
 class DefaultTicketGroupStatsProviderTestCase(unittest.TestCase):
@@ -96,43 +112,116 @@
         self.env.reset_db()
 
     def test_stats(self):
-        self.assertEquals(self.stats.title, 'ticket status', 'title incorrect')
-        self.assertEquals(self.stats.unit, 'tickets', 'unit incorrect')
-        self.assertEquals(2, len(self.stats.intervals), 'more than 2 intervals')
+        self.assertEqual(self.stats.title, 'ticket status', 'title incorrect')
+        self.assertEqual(self.stats.unit, 'tickets', 'unit incorrect')
+        self.assertEqual(2, len(self.stats.intervals), 'more than 2 intervals')
 
     def test_closed_interval(self):
         closed = self.stats.intervals[0]
-        self.assertEquals('closed', closed['title'], 'closed title incorrect')
-        self.assertEquals('closed', closed['css_class'], 'closed class incorrect')
-        self.assertEquals(True, closed['overall_completion'],
-                          'closed should contribute to overall completion')
-        self.assertEquals({'status': ['closed'], 'group': ['resolution']},
-                          closed['qry_args'], 'qry_args incorrect')
-        self.assertEquals(1, closed['count'], 'closed count incorrect')
-        self.assertEquals(33, closed['percent'], 'closed percent incorrect')
+        self.assertEqual('closed', closed['title'], 'closed title incorrect')
+        self.assertEqual('closed', closed['css_class'], 'closed class incorrect')
+        self.assertTrue(closed['overall_completion'],
+                        'closed should contribute to overall completion')
+        self.assertEqual({'status': ['closed'], 'group': ['resolution']},
+                         closed['qry_args'], 'qry_args incorrect')
+        self.assertEqual(1, closed['count'], 'closed count incorrect')
+        self.assertEqual(33, closed['percent'], 'closed percent incorrect')
 
     def test_open_interval(self):
         open = self.stats.intervals[1]
-        self.assertEquals('active', open['title'], 'open title incorrect')
-        self.assertEquals('open', open['css_class'], 'open class incorrect')
-        self.assertEquals(False, open['overall_completion'],
-                          "open shouldn't contribute to overall completion")
-        self.assertEquals({'status':
-                           [u'assigned', u'new', u'accepted', u'reopened']},
-                          open['qry_args'], 'qry_args incorrect')
-        self.assertEquals(2, open['count'], 'open count incorrect')
-        self.assertEquals(67, open['percent'], 'open percent incorrect')
+        self.assertEqual('active', open['title'], 'open title incorrect')
+        self.assertEqual('open', open['css_class'], 'open class incorrect')
+        self.assertFalse(open['overall_completion'],
+                         "open shouldn't contribute to overall completion")
+        self.assertEqual({'status':
+                          [u'assigned', u'new', u'accepted', u'reopened']},
+                         open['qry_args'], 'qry_args incorrect')
+        self.assertEqual(2, open['count'], 'open count incorrect')
+        self.assertEqual(67, open['percent'], 'open percent incorrect')
+
+
+class MilestoneModuleTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.req = Mock(href=self.env.href, perm=MockPerm())
+        self.mmodule = MilestoneModule(self.env)
+        self.terms = ['MilestoneAlpha', 'MilestoneBeta', 'MilestoneGamma']
+        for term in self.terms + [' '.join(self.terms)]:
+            m = Milestone(self.env)
+            m.name = term
+            m.due = datetime.now(utc)
+            m.description = random_sentence()
+            m.insert()
+
+    def tearDown(self):
+        self.env.reset_db()
+
+    def test_get_search_filters(self):
+        filters = self.mmodule.get_search_filters(self.req)
+        filters = list(filters)
+        self.assertEqual(1, len(filters))
+        self.assertEqual(2, len(filters[0]))
+        self.assertEqual('milestone', filters[0][0])
+        self.assertEqual('Milestones', filters[0][1])
+
+    def test_get_search_results_milestone_not_in_filters(self):
+        results = self.mmodule.get_search_results(self.req, self.terms, [])
+        self.assertEqual([], list(results))
+
+    def test_get_search_results_matches_all_terms(self):
+        milestone = Milestone(self.env, ' '.join(self.terms))
+        results = self.mmodule.get_search_results(self.req, self.terms,
+                                                  ['milestone'])
+        results = list(results)
+        self.assertEqual(1, len(results))
+        self.assertEqual(5, len(results[0]))
+        self.assertEqual('/trac.cgi/milestone/' +
+                         milestone.name.replace(' ', '%20'),
+                         results[0][0])
+        self.assertEqual('Milestone ' + milestone.name, results[0][1])
+        self.assertEqual(milestone.due, results[0][2])
+        self.assertEqual('', results[0][3])
+        self.assertEqual(milestone.description, results[0][4])
+
+
+class MilestoneModulePermissionsTestCase(RequestHandlerPermissionsTestCaseBase):
+
+    def setUp(self):
+        super(MilestoneModulePermissionsTestCase, self).setUp(MilestoneModule)
+
+    def test_milestone_notfound_with_milestone_create(self):
+        self.grant_perm('anonymous', 'MILESTONE_VIEW')
+        self.grant_perm('anonymous', 'MILESTONE_CREATE')
+
+        req = self.create_request(path_info='/milestone/milestone5')
+        res = self.process_request(req)
+
+        self.assertEqual('milestone_edit.html', res[0])
+        self.assertEqual('milestone5', res[1]['milestone'].name)
+        self.assertEqual("Milestone milestone5 does not exist. You can"
+                         " create it here.", req.chrome['notices'][0])
+
+    def test_milestone_notfound_without_milestone_create(self):
+        self.grant_perm('anonymous', 'MILESTONE_VIEW')
+
+        req = self.create_request(path_info='/milestone/milestone5')
+
+        self.assertRaises(ResourceNotFound, self.process_request, req)
 
 
 def in_tlist(ticket, list):
     return len([t for t in list if t['id'] == ticket.id]) > 0
 
+
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(TicketGroupStatsTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(DefaultTicketGroupStatsProviderTestCase,
-                                      'test'))
+    suite.addTest(unittest.makeSuite(TicketGroupStatsTestCase))
+    suite.addTest(unittest.makeSuite(DefaultTicketGroupStatsProviderTestCase))
+    suite.addTest(unittest.makeSuite(MilestoneModuleTestCase))
+    suite.addTest(unittest.makeSuite(MilestoneModulePermissionsTestCase))
     return suite
 
+
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
diff --git a/trac/trac/ticket/tests/wikisyntax.py b/trac/trac/ticket/tests/wikisyntax.py
index 4c71a45..936c5c4 100644
--- a/trac/trac/ticket/tests/wikisyntax.py
+++ b/trac/trac/ticket/tests/wikisyntax.py
@@ -1,11 +1,28 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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 unittest
+from datetime import datetime, timedelta
 
-from trac.ticket.model import Ticket
-from trac.ticket.roadmap import Milestone
+from trac.test import locale_en
+from trac.ticket.query import QueryModule
+from trac.ticket.report import ReportModule
+from trac.ticket.roadmap import RoadmapModule
+from trac.ticket.model import Milestone, Ticket
+from trac.util.datefmt import format_datetime, pretty_timedelta, utc
 from trac.wiki.tests import formatter
 
+
 TICKET_TEST_CASES = u"""
 ============================== ticket: link resolver
 ticket:1
@@ -109,6 +126,9 @@
 """ # "
 
 def ticket_setup(tc):
+    config = tc.env.config
+    config.set('ticket-custom', 'custom1', 'text')
+    config.save()
     ticket = Ticket(tc.env)
     ticket.values.update({'reporter': 'santa',
                           'summary': 'This is the summary',
@@ -116,6 +136,9 @@
     ticket.insert()
 
 def ticket_teardown(tc):
+    config = tc.env.config
+    config.remove('ticket-custom', 'custom1')
+    config.save()
     tc.env.reset_db()
 
 
@@ -127,7 +150,7 @@
 ------------------------------
 <p>
 <a class="report" href="/report/1">{1}</a>, <a class="report" href="/report/2">{2}</a>
-<a class="report" href="/report/12">{12}</a>, {abc}
+<a class="missing report" title="report does not exist">{12}</a>, {abc}
 </p>
 ------------------------------
 ============================== escaping the above
@@ -146,6 +169,14 @@
 </p>
 ------------------------------
 &amp;#1; &amp;#23;
+============================== report link with non-digits
+report:blah
+------------------------------
+<p>
+<a class="missing report" title="report does not exist">report:blah</a>
+</p>
+------------------------------
+<a class="missing report" title="report does not exist">report:blah</a>
 ============================== InterTrac for reports
 trac:report:1
 [trac:report:1 Trac r1]
@@ -175,7 +206,16 @@
 """ # '
 
 def report_setup(tc):
-    pass # TBD
+    def create_report(tc, id):
+        tc.env.db_transaction("""
+            INSERT INTO report (id,title,query,description)
+            VALUES (%s,%s,'SELECT 1','')""", (id, 'Report %s' % id))
+    create_report(tc, 1)
+    create_report(tc, 2)
+
+
+dt_past = datetime.now(utc) - timedelta(days=1)
+dt_future = datetime.now(utc) + timedelta(days=1)
 
 
 MILESTONE_TEST_CASES = u"""
@@ -183,11 +223,15 @@
 milestone:foo
 [milestone:boo Milestone Boo]
 [milestone:roo Milestone Roo]
+[milestone:woo Milestone Woo]
+[milestone:zoo Milestone Zoo]
 ------------------------------
 <p>
 <a class="missing milestone" href="/milestone/foo" rel="nofollow">milestone:foo</a>
-<a class="milestone" href="/milestone/boo">Milestone Boo</a>
-<a class="closed milestone" href="/milestone/roo">Milestone Roo</a>
+<a class="milestone" href="/milestone/boo" title="No date set">Milestone Boo</a>
+<a class="closed milestone" href="/milestone/roo" title="Completed %(dt_past)s ago (%(datestr_past)s)">Milestone Roo</a>
+<a class="milestone" href="/milestone/woo" title="Due in %(dt_future)s (%(datestr_future)s)">Milestone Woo</a>
+<a class="milestone" href="/milestone/zoo" title="%(dt_past)s late (%(datestr_past)s)">Milestone Zoo</a>
 </p>
 ------------------------------
 ============================== milestone: link resolver + arguments
@@ -196,23 +240,35 @@
 ------------------------------
 <p>
 <a class="missing milestone" href="/milestone/?action=new" rel="nofollow">milestone:?action=new</a>
-<a class="milestone" href="/milestone/boo#KnownIssues">Known Issues for 1.0</a>
+<a class="milestone" href="/milestone/boo#KnownIssues" title="No date set">Known Issues for 1.0</a>
 </p>
 ------------------------------
-""" #"
+""" % {'dt_past': pretty_timedelta(dt_past),
+       'dt_future': pretty_timedelta(dt_future),
+       'datestr_past': format_datetime(dt_past, locale=locale_en, tzinfo=utc),
+       'datestr_future': format_datetime(dt_future, locale=locale_en,
+                                         tzinfo=utc)} #"
 
 def milestone_setup(tc):
-    from datetime import datetime
-    from trac.util.datefmt import utc
     boo = Milestone(tc.env)
     boo.name = 'boo'
     boo.completed = boo.due = None
     boo.insert()
     roo = Milestone(tc.env)
     roo.name = 'roo'
-    roo.completed = datetime.now(utc)
+    roo.completed = dt_past
     roo.due = None
     roo.insert()
+    woo = Milestone(tc.env)
+    woo.name = 'woo'
+    woo.completed = None
+    woo.due = dt_future
+    woo.insert()
+    zoo = Milestone(tc.env)
+    zoo.name = 'zoo'
+    zoo.completed = None
+    zoo.due = dt_past
+    zoo.insert()
 
 def milestone_teardown(tc):
     tc.env.reset_db()
@@ -299,7 +355,7 @@
 New tickets: [[TicketQuery(status=new, format=count)]]
 ------------------------------
 <p>
-New tickets: <span class="query_count" title="1 tickets for which status=new&amp;max=0&amp;order=id">1</span>
+New tickets: <span class="query_count" title="1 ticket for which status=new&amp;max=0&amp;order=id">1</span>
 </p>
 ------------------------------
 ============================== TicketQuery macro: one result, compact form
@@ -309,6 +365,20 @@
 New tickets: <span><a class="new" href="/ticket/1" title="This is the summary">#1</a></span>
 </p>
 ------------------------------
+============================== TicketQuery macro: duplicated fields
+New tickets: [[TicketQuery(status=new, format=compact, col=summary|status|status)]]
+------------------------------
+<p>
+New tickets: <span><a class="new" href="/ticket/1" title="This is the summary">#1</a></span>
+</p>
+------------------------------
+============================== TicketQuery macro: duplicated custom fields
+New tickets: [[TicketQuery(status=new, format=compact, col=summary|custom1|custom1)]]
+------------------------------
+<p>
+New tickets: <span><a class="new" href="/ticket/1" title="This is the summary">#1</a></span>
+</p>
+------------------------------
 """
 
 QUERY2_TEST_CASES = u"""
@@ -353,25 +423,88 @@
 
 COMMENT_TEST_CASES = u"""
 ============================== comment: link resolver (deprecated)
-comment:ticket:123:2 (deprecated)
-[comment:ticket:123:2 see above] (deprecated)
-[comment:ticket:123:description see descr] (deprecated)
+comment:ticket:1:1 (deprecated)
+[comment:ticket:1:1 see above] (deprecated)
+comment:ticket:1:description (deprecated)
+[comment:ticket:1:description see descr] (deprecated)
+comment:ticket:2:1 (deprecated)
+comment:ticket:2:3 (deprecated)
+comment:ticket:3:1 (deprecated)
+comment:tiket:2:1 (deprecated)
+comment:ticket:two:1 (deprecated)
+comment:ticket:2:1a (deprecated)
+comment:ticket:2:one (deprecated)
+comment:ticket:1: (deprecated)
+comment:ticket::2 (deprecated)
+comment:ticket:: (deprecated)
 ------------------------------
 <p>
-<a href="/ticket/123#comment:2" title="Comment 2 for Ticket #123">comment:ticket:123:2</a> (deprecated)
-<a href="/ticket/123#comment:2" title="Comment 2 for Ticket #123">see above</a> (deprecated)
-<a href="/ticket/123#comment:description" title="Comment description for Ticket #123">see descr</a> (deprecated)
+<a class="new ticket" href="/ticket/1#comment:1" title="Comment 1 for Ticket #1">comment:ticket:1:1</a> (deprecated)
+<a class="new ticket" href="/ticket/1#comment:1" title="Comment 1 for Ticket #1">see above</a> (deprecated)
+<a class="new ticket" href="/ticket/1#comment:description" title="Description for Ticket #1">comment:ticket:1:description</a> (deprecated)
+<a class="new ticket" href="/ticket/1#comment:description" title="Description for Ticket #1">see descr</a> (deprecated)
+<a class="ticket" href="/ticket/2#comment:1" title="Comment 1">comment:ticket:2:1</a> (deprecated)
+<a class="missing ticket" title="ticket comment does not exist">comment:ticket:2:3</a> (deprecated)
+<a class="missing ticket" title="ticket does not exist">comment:ticket:3:1</a> (deprecated)
+comment:tiket:2:1 (deprecated)
+comment:ticket:two:1 (deprecated)
+comment:ticket:2:1a (deprecated)
+comment:ticket:2:one (deprecated)
+comment:ticket:1: (deprecated)
+comment:ticket::2 (deprecated)
+comment:ticket:: (deprecated)
 </p>
 ------------------------------
 ============================== comment: link resolver
-comment:2:ticket:123
-[comment:2:ticket:123 see above]
-[comment:description:ticket:123 see descr]
+comment:1
+[comment:1 see above]
+comment:description
+[comment:description see descr]
+comment:
+comment:one
+comment:1a
 ------------------------------
 <p>
-<a href="/ticket/123#comment:2" title="Comment 2 for Ticket #123">comment:2:ticket:123</a>
-<a href="/ticket/123#comment:2" title="Comment 2 for Ticket #123">see above</a>
-<a href="/ticket/123#comment:description" title="Comment description for Ticket #123">see descr</a>
+<a class="ticket" href="/ticket/2#comment:1" title="Comment 1">comment:1</a>
+<a class="ticket" href="/ticket/2#comment:1" title="Comment 1">see above</a>
+<a class="ticket" href="/ticket/2#comment:description" title="Description">comment:description</a>
+<a class="ticket" href="/ticket/2#comment:description" title="Description">see descr</a>
+comment:
+comment:one
+comment:1a
+</p>
+------------------------------
+============================== comment: link resolver with ticket number
+comment:1:ticket:1
+[comment:1:ticket:1 see above]
+comment:description:ticket:1
+[comment:description:ticket:1 see descr]
+comment:1:ticket:2
+comment:3:ticket:2
+comment:1:ticket:3
+comment:2:tiket:1
+comment:1:ticket:two
+comment:one:ticket:1
+comment:1a:ticket:1
+comment:ticket:1
+comment:2:ticket:
+comment::ticket:
+------------------------------
+<p>
+<a class="new ticket" href="/ticket/1#comment:1" title="Comment 1 for Ticket #1">comment:1:ticket:1</a>
+<a class="new ticket" href="/ticket/1#comment:1" title="Comment 1 for Ticket #1">see above</a>
+<a class="new ticket" href="/ticket/1#comment:description" title="Description for Ticket #1">comment:description:ticket:1</a>
+<a class="new ticket" href="/ticket/1#comment:description" title="Description for Ticket #1">see descr</a>
+<a class="ticket" href="/ticket/2#comment:1" title="Comment 1">comment:1:ticket:2</a>
+<a class="missing ticket" title="ticket comment does not exist">comment:3:ticket:2</a>
+<a class="missing ticket" title="ticket does not exist">comment:1:ticket:3</a>
+comment:2:tiket:1
+comment:1:ticket:two
+comment:one:ticket:1
+comment:1a:ticket:1
+comment:ticket:1
+comment:2:ticket:
+comment::ticket:
 </p>
 ------------------------------
 """ # "
@@ -385,6 +518,24 @@
 # As it's a problem with a temp workaround, I think there's no need
 # to fix it for now.
 
+def comment_setup(tc):
+    ticket1 = Ticket(tc.env)
+    ticket1.values.update({'reporter': 'santa',
+                            'summary': 'This is the summary for ticket 1',
+                            'status': 'new'})
+    ticket1.insert()
+    ticket1.save_changes(comment='This is the comment for ticket 1')
+    ticket2 = Ticket(tc.env)
+    ticket2.values.update({'reporter': 'claws',
+                           'summary': 'This is the summary for ticket 2',
+                           'status': 'closed'})
+    ticket2.insert()
+    ticket2.save_changes(comment='This is the comment for ticket 2')
+
+def comment_teardown(tc):
+    tc.env.reset_db()
+
+
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(formatter.suite(TICKET_TEST_CASES, ticket_setup, __file__,
@@ -396,9 +547,9 @@
                                   ticket_teardown))
     suite.addTest(formatter.suite(QUERY2_TEST_CASES, query2_setup, __file__,
                                   query2_teardown))
-    suite.addTest(formatter.suite(COMMENT_TEST_CASES, file=__file__))
+    suite.addTest(formatter.suite(COMMENT_TEST_CASES, comment_setup, __file__,
+                                  comment_teardown, ('ticket', 2)))
     return suite
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
-
diff --git a/trac/trac/ticket/web_ui.py b/trac/trac/ticket/web_ui.py
index e3f1ada..42b2533 100644
--- a/trac/trac/ticket/web_ui.py
+++ b/trac/trac/ticket/web_ui.py
@@ -38,20 +38,18 @@
 from trac.ticket.model import Milestone, Ticket, group_milestones
 from trac.ticket.notification import TicketNotifyEmail
 from trac.timeline.api import ITimelineEventProvider
-from trac.util import as_bool, as_int, get_reporter_id
+from trac.util import as_bool, as_int, get_reporter_id, lazy
 from trac.util.datefmt import (
     format_datetime, from_utimestamp, to_utimestamp, utc
 )
+from trac.util.html import to_fragment
 from trac.util.text import (
-    exception_to_unicode, empty, obfuscate_email_address, shorten_line,
-    to_unicode
+    exception_to_unicode, empty, obfuscate_email_address, shorten_line
 )
 from trac.util.presentation import separated
-from trac.util.translation import _, tag_, tagn_, N_, gettext, ngettext
+from trac.util.translation import _, tag_, tagn_, N_, ngettext
 from trac.versioncontrol.diff import get_diff_options, diff_blocks
-from trac.web import (
-    IRequestHandler, RequestDone, arg_list_to_args, parse_arg_list
-)
+from trac.web import IRequestHandler, arg_list_to_args, parse_arg_list
 from trac.web.chrome import (
     Chrome, INavigationContributor, ITemplateProvider,
     add_ctxtnav, add_link, add_notice, add_script, add_script_data,
@@ -77,12 +75,14 @@
         open / close operations (''since 0.9'').""")
 
     max_description_size = IntOption('ticket', 'max_description_size', 262144,
-        """Don't accept tickets with a too big description.
-        (''since 0.11'').""")
+        """Maximum allowed description size in characters.
+        (//since 0.11//).""")
 
     max_comment_size = IntOption('ticket', 'max_comment_size', 262144,
-        """Don't accept tickets with a too big comment.
-        (''since 0.11.2'')""")
+        """Maximum allowed comment size in characters. (//since 0.11.2//).""")
+
+    max_summary_size = IntOption('ticket', 'max_summary_size', 262144,
+        """Maximum allowed summary size in characters. (//since 1.0.2//).""")
 
     timeline_newticket_formatter = Option('timeline', 'newticket_formatter',
                                           'oneliner',
@@ -123,11 +123,11 @@
             return getattr(TicketSystem(self.env), name)
         raise AttributeError("TicketModule has no attribute '%s'" % name)
 
-    @property
+    @lazy
     def must_preserve_newlines(self):
         preserve_newlines = self.preserve_newlines
         if preserve_newlines == 'default':
-            preserve_newlines = self.env.get_version(initial=True) >= 21 # 0.11
+            preserve_newlines = self.env.database_initial_version >= 21 # 0.11
         return as_bool(preserve_newlines)
 
     # IContentConverter methods
@@ -296,7 +296,7 @@
                     FROM ticket_change tc
                         INNER JOIN ticket t ON t.id = tc.ticket
                             AND tc.time>=%s AND tc.time<=%s
-                    ORDER BY tc.time
+                    ORDER BY tc.time, tc.ticket
                     """ % (ts_start, ts_stop)):
                 if not (oldvalue or newvalue):
                     # ignore empty change corresponding to custom field
@@ -307,7 +307,7 @@
                         ev = produce_event(data, status, fields, comment,
                                            cid)
                         if ev:
-                             yield (ev, data[1])
+                            yield (ev, data[1])
                     status, fields, comment, cid = 'edit', {}, '', None
                     data = (id, t, author, type, summary, None)
                 if field == 'comment':
@@ -1130,7 +1130,7 @@
         for f in fields:
             name = f['name']
             value = ticket.values.get(name, '')
-            if name in ('cc', 'reporter'):
+            if name in ('cc', 'owner', 'reporter'):
                 value = Chrome(self.env).format_emails(context, value, ' ')
             elif name in ticket.time_fields:
                 value = format_datetime(value, '%Y-%m-%d %H:%M:%S',
@@ -1241,8 +1241,9 @@
                 value = ticket[name]
                 if value:
                     if value not in field['options']:
-                        add_warning(req, '"%s" is not a valid value for '
-                                    'the %s field.' % (value, name))
+                        add_warning(req, _('"%(value)s" is not a valid value '
+                                           'for the %(name)s field.',
+                                           value=value, name=name))
                         valid = False
                 elif not field.get('optional', False):
                     add_warning(req, _("field %(name)s must be set",
@@ -1263,6 +1264,13 @@
                                num=self.max_comment_size))
             valid = False
 
+        # Validate summary length
+        if len(ticket['summary']) > self.max_summary_size:
+            add_warning(req, _("Ticket summary is too long (must be less "
+                               "than %(num)s characters)",
+                               num=self.max_summary_size))
+            valid = False
+
         # Validate comment numbering
         try:
             # replyto must be 'description' or a number
@@ -1278,9 +1286,9 @@
             for field, message in manipulator.validate_ticket(req, ticket):
                 valid = False
                 if field:
-                    add_warning(req, _("The ticket field '%(field)s' is "
-                                       "invalid: %(message)s",
-                                       field=field, message=message))
+                    add_warning(req, tag_("The ticket field '%(field)s'"
+                                          " is invalid: %(message)s",
+                                          field=field, message=message))
                 else:
                     add_warning(req, message)
         return valid
@@ -1295,9 +1303,9 @@
         except Exception, e:
             self.log.error("Failure sending notification on creation of "
                     "ticket #%s: %s", ticket.id, exception_to_unicode(e))
-            add_warning(req, _("The ticket has been created, but an error "
-                               "occurred while sending notifications: "
-                               "%(message)s", message=to_unicode(e)))
+            add_warning(req, tag_("The ticket has been created, but an error "
+                                  "occurred while sending notifications: "
+                                  "%(message)s", message=to_fragment(e)))
 
         # Redirect the user to the newly created ticket or add attachment
         ticketref=tag.a('#', ticket.id, href=req.href.ticket(ticket.id))
@@ -1341,7 +1349,7 @@
                 add_warning(req, tag_("The %(change)s has been saved, but an "
                                       "error occurred while sending "
                                       "notifications: %(message)s",
-                                      change=change, message=to_unicode(e)))
+                                      change=change, message=to_fragment(e)))
                 fragment = ''
 
         # After saving the changes, apply the side-effects.
@@ -1401,6 +1409,9 @@
 
     def _query_link(self, req, name, value, text=None):
         """Return a link to /query with the appropriate name and value"""
+        from trac.ticket.query import QueryModule
+        if not self.env.is_component_enabled(QueryModule):
+            return text or value
         default_query = self.ticketlink_query.lstrip('?')
         args = arg_list_to_args(parse_arg_list(default_query))
         args[name] = value
@@ -1410,7 +1421,9 @@
 
     def _query_link_words(self, context, name, value):
         """Splits a list of words and makes a query link to each separately"""
-        if not isinstance(value, basestring): # None or other non-splitable
+        from trac.ticket.query import QueryModule
+        if not (isinstance(value, basestring) and  # None or other non-splitable
+                self.env.is_component_enabled(QueryModule)):
             return value
         default_query = self.ticketlink_query.startswith('?') and \
                         self.ticketlink_query[1:] or self.ticketlink_query
diff --git a/trac/trac/timeline/__init__.py b/trac/trac/timeline/__init__.py
index a0966cd..8bb8e7f 100644
--- a/trac/trac/timeline/__init__.py
+++ b/trac/trac/timeline/__init__.py
@@ -1 +1,14 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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/.
+
 from trac.timeline.api import *
diff --git a/trac/trac/timeline/api.py b/trac/trac/timeline/api.py
index 47d7eb5..a58f401 100644
--- a/trac/trac/timeline/api.py
+++ b/trac/trac/timeline/api.py
@@ -69,5 +69,3 @@
                       the 'url'
         :param event: the event tuple, as returned by `get_timeline_events`
         """
-
-
diff --git a/trac/trac/timeline/templates/timeline.html b/trac/trac/timeline/templates/timeline.html
index 957222f..b73ca5c 100644
--- a/trac/trac/timeline/templates/timeline.html
+++ b/trac/trac/timeline/templates/timeline.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/timeline/templates/timeline.rss b/trac/trac/timeline/templates/timeline.rss
index cce29f9..2a7afbc 100644
--- a/trac/trac/timeline/templates/timeline.rss
+++ b/trac/trac/timeline/templates/timeline.rss
@@ -6,9 +6,7 @@
     <title>${project.name}</title>
     <link>${abs_href.timeline()}</link>
     <description>Trac Timeline</description>
-    <language>${req.locale and \
-                '%s-%s' % (req.locale.language, req.locale.territory) or \
-                'en-US'}</language>
+    <language>${str(locale).replace('_', '-') if locale else 'en-US'}</language>
     <generator>Trac ${trac.version}</generator>
     <image py:if="chrome.logo.src_abs">
       <title>${project.name}</title>
diff --git a/trac/trac/timeline/tests/__init__.py b/trac/trac/timeline/tests/__init__.py
index d48e5f6..1319ea5 100644
--- a/trac/trac/timeline/tests/__init__.py
+++ b/trac/trac/timeline/tests/__init__.py
@@ -1 +1,28 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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 unittest
+
+from trac.timeline.tests import web_ui
+from trac.timeline.tests import wikisyntax
 from trac.timeline.tests.functional import functionalSuite
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(web_ui.suite())
+    suite.addTest(wikisyntax.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/timeline/tests/functional.py b/trac/trac/timeline/tests/functional.py
index e5b3c7a..fcd5b42 100755
--- a/trac/trac/timeline/tests/functional.py
+++ b/trac/trac/timeline/tests/functional.py
@@ -1,4 +1,17 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
+
 from trac.tests.functional import *
 
 
@@ -29,8 +42,8 @@
 
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
     suite.addTest(RegressionTestRev5883())
     return suite
 
diff --git a/trac/trac/timeline/tests/web_ui.py b/trac/trac/timeline/tests/web_ui.py
new file mode 100644
index 0000000..7034ff7
--- /dev/null
+++ b/trac/trac/timeline/tests/web_ui.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 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 datetime import datetime, timedelta
+
+from trac.test import EnvironmentStub, Mock, MockPerm, locale_en
+from trac.timeline.web_ui import TimelineModule
+from trac.util.datefmt import (
+    format_date, format_datetime, format_time, pretty_timedelta, utc,
+)
+from trac.util.html import plaintext
+from trac.web.chrome import Chrome
+from trac.web.href import Href
+
+
+class PrettyDateinfoTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.req = Mock(href=Href('/'), abs_href=Href('http://example.org/'),
+                        authname='anonymous', tz=utc, locale=locale_en,
+                        lc_time=locale_en, chrome={}, perm=MockPerm(),
+                        session={})
+
+    def tearDown(self):
+        self.env.reset_db()
+
+    def _format_chrome(self, d, format, dateonly):
+        data = Chrome(self.env).populate_data(self.req, {})
+        return plaintext(data['pretty_dateinfo'](d, format=format,
+                                                 dateonly=dateonly))
+
+    def _format_timeline(self, d, format, dateonly):
+        data = Chrome(self.env).populate_data(self.req, {})
+        TimelineModule(self.env) \
+            .post_process_request(self.req, 'timeline.html', data, None)
+        return plaintext(data['pretty_dateinfo'](d, format=format,
+                                                 dateonly=dateonly))
+
+    def test_relative(self):
+        t = datetime.now(utc) - timedelta(days=1)
+        label = '%s ago' % pretty_timedelta(t)
+        self.assertEqual(label, self._format_chrome(t, 'relative', False))
+        self.assertEqual(label, self._format_timeline(t, 'relative', False))
+
+    def test_relative_dateonly(self):
+        t = datetime.now(utc) - timedelta(days=1)
+        label = pretty_timedelta(t)
+        self.assertEqual(label, self._format_chrome(t, 'relative', True))
+        self.assertEqual(label, self._format_timeline(t, 'relative', True))
+
+    def test_absolute(self):
+        t = datetime.now(utc) - timedelta(days=1)
+        label = 'on %s at %s' % \
+                (format_date(t, locale=locale_en, tzinfo=utc),
+                 format_time(t, locale=locale_en, tzinfo=utc))
+        self.assertEqual(label, self._format_chrome(t, 'absolute', False))
+        self.assertEqual(label, self._format_timeline(t, 'absolute', False))
+
+    def test_absolute_dateonly(self):
+        t = datetime.now(utc) - timedelta(days=1)
+        label = format_datetime(t, locale=locale_en, tzinfo=utc)
+        self.assertEqual(label, self._format_chrome(t, 'absolute', True))
+        self.assertEqual(label, self._format_timeline(t, 'absolute', True))
+
+    def test_absolute_iso8601(self):
+        t = datetime(2014, 1, 28, 2, 30, 44, 0, utc)
+        label = 'at 2014-01-28T02:30:44Z'
+        self.req.lc_time = 'iso8601'
+        self.assertEqual(label, self._format_chrome(t, 'absolute', False))
+        self.assertEqual(label, self._format_timeline(t, 'absolute', False))
+
+    def test_absolute_iso8601_dateonly(self):
+        t = datetime(2014, 1, 28, 2, 30, 44, 0, utc)
+        label = '2014-01-28T02:30:44Z'
+        self.req.lc_time = 'iso8601'
+        self.assertEqual(label, self._format_chrome(t, 'absolute', True))
+        self.assertEqual(label, self._format_timeline(t, 'absolute', True))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(PrettyDateinfoTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/timeline/tests/wikisyntax.py b/trac/trac/timeline/tests/wikisyntax.py
new file mode 100644
index 0000000..81c963d
--- /dev/null
+++ b/trac/trac/timeline/tests/wikisyntax.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 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 time
+import unittest
+
+from trac.timeline.web_ui import TimelineModule
+from trac.wiki.tests import formatter
+
+TIMELINE_TEST_CASES = u"""
+============================== timeline: link resolver
+timeline:2008-01-29
+timeline:2008-01-29T15:48
+timeline:2008-01-29T15:48Z
+timeline:2008-01-29T16:48+01
+timeline:2008-01-0A
+timeline:@datestr_libc@
+------------------------------
+<p>
+<a class="timeline" href="/timeline?from=2008-01-29T00%3A00%3A00Z" title="See timeline at 2008-01-29T00:00:00Z">timeline:2008-01-29</a>
+<a class="timeline" href="/timeline?from=2008-01-29T15%3A48%3A00Z&amp;precision=minutes" title="See timeline at 2008-01-29T15:48:00Z">timeline:2008-01-29T15:48</a>
+<a class="timeline" href="/timeline?from=2008-01-29T15%3A48%3A00Z&amp;precision=minutes" title="See timeline at 2008-01-29T15:48:00Z">timeline:2008-01-29T15:48Z</a>
+<a class="timeline" href="/timeline?from=2008-01-29T15%3A48%3A00Z&amp;precision=seconds" title="See timeline at 2008-01-29T15:48:00Z">timeline:2008-01-29T16:48+01</a>
+<a class="timeline missing" title="&#34;2008-01-0A&#34; is an invalid date, or the date format is not known. Try &#34;YYYY-MM-DDThh:mm:ss±hh:mm&#34; instead.">timeline:2008-01-0A</a>
+<a class="timeline missing" title="&#34;@datestr_libc@&#34; is an invalid date, or the date format is not known. Try &#34;YYYY-MM-DDThh:mm:ss±hh:mm&#34; instead.">timeline:@datestr_libc@</a>
+</p>
+------------------------------
+"""
+
+
+def suite():
+    suite = unittest.TestSuite()
+    datestr_libc = time.strftime('%x', (2013, 10, 24, 0, 0, 0, 0, 0, -1))
+    suite.addTest(formatter.suite(TIMELINE_TEST_CASES.replace('@datestr_libc@',
+                                                              datestr_libc),
+                                  file=__file__))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/timeline/web_ui.py b/trac/trac/timeline/web_ui.py
index a47b4bd..1de5692 100644
--- a/trac/trac/timeline/web_ui.py
+++ b/trac/trac/timeline/web_ui.py
@@ -326,11 +326,11 @@
                 elif len(time) >= 2:
                     precision = 'hours'
             try:
-                return self.get_timeline_link(formatter.req,
-                                              parse_date(path, utc),
-                                              label, precision, query, fragment)
+                dt = parse_date(path, utc, locale='iso8601', hint='iso8601')
+                return self.get_timeline_link(formatter.req, dt, label,
+                                              precision, query, fragment)
             except TracError, e:
-                return tag.a(label, title=to_unicode(e.message),
+                return tag.a(label, title=to_unicode(e),
                              class_='timeline missing')
         yield ('timeline', link_resolver)
 
@@ -395,7 +395,7 @@
                        name=tag.tt(ep.__class__.__name__),
                        kinds=', '.join('"%s"' % ep_kinds[f] for f in
                                        current_filters & ep_filters)),
-                  tag.b(exception_to_unicode(exc)), class_='message'),
+                  tag.strong(exception_to_unicode(exc)), class_='message'),
             tag.p(tag_("You may want to see the %(other_events)s from the "
                        "Timeline or notify your Trac administrator about the "
                        "error (detailed information was written to the log).",
diff --git a/trac/trac/upgrades/db10.py b/trac/trac/upgrades/db10.py
index 5d1b6bf..f269f3f 100644
--- a/trac/trac/upgrades/db10.py
+++ b/trac/trac/upgrades/db10.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 sql = [
 #-- Make the node_change table contain more information, and force a resync
 """DROP TABLE revision;""",
@@ -19,6 +32,7 @@
 );"""
 ]
 
+
 def do_upgrade(env, ver, cursor):
     for s in sql:
         cursor.execute(s)
diff --git a/trac/trac/upgrades/db11.py b/trac/trac/upgrades/db11.py
index 043ab18..0067885 100644
--- a/trac/trac/upgrades/db11.py
+++ b/trac/trac/upgrades/db11.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 sql = [
 #-- Remove empty values from the milestone list
 """DELETE FROM milestone WHERE COALESCE(name,'')='';""",
diff --git a/trac/trac/upgrades/db12.py b/trac/trac/upgrades/db12.py
index 9302508..eefc7b4 100644
--- a/trac/trac/upgrades/db12.py
+++ b/trac/trac/upgrades/db12.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 sql = [
 #-- Some anonymous session might have been left over
 """DELETE FROM session WHERE username='anonymous';""",
diff --git a/trac/trac/upgrades/db13.py b/trac/trac/upgrades/db13.py
index e2ef896..c6890d0 100644
--- a/trac/trac/upgrades/db13.py
+++ b/trac/trac/upgrades/db13.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 sql = [
 #-- Add ticket_type to 'ticket', remove the unused 'url' column
 """CREATE TEMPORARY TABLE ticket_old AS SELECT * FROM ticket;""",
diff --git a/trac/trac/upgrades/db14.py b/trac/trac/upgrades/db14.py
index 0fe6888..04b88a1 100644
--- a/trac/trac/upgrades/db14.py
+++ b/trac/trac/upgrades/db14.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 sql = [
 """CREATE TEMPORARY TABLE node_change_old AS SELECT * FROM node_change;""",
 """DROP TABLE node_change;""",
diff --git a/trac/trac/upgrades/db15.py b/trac/trac/upgrades/db15.py
index e6acf24..cc81b51 100644
--- a/trac/trac/upgrades/db15.py
+++ b/trac/trac/upgrades/db15.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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 trac.db import Table, Column, Index, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db16.py b/trac/trac/upgrades/db16.py
index 9c1bf11..b6b5ac1 100644
--- a/trac/trac/upgrades/db16.py
+++ b/trac/trac/upgrades/db16.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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 trac.db import Table, Column, Index
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db17.py b/trac/trac/upgrades/db17.py
index 9230d8f..5807308 100644
--- a/trac/trac/upgrades/db17.py
+++ b/trac/trac/upgrades/db17.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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.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 trac.db import Table, Column, Index, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db18.py b/trac/trac/upgrades/db18.py
index 3bc6f4c..97f9635 100644
--- a/trac/trac/upgrades/db18.py
+++ b/trac/trac/upgrades/db18.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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.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 trac.db import Table, Column, Index, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
@@ -61,4 +74,3 @@
 
     cursor.execute("DROP TABLE session_old")
     cursor.execute("DROP TABLE ticket_change_old")
-
diff --git a/trac/trac/upgrades/db19.py b/trac/trac/upgrades/db19.py
index 09d7563..7e335c9 100644
--- a/trac/trac/upgrades/db19.py
+++ b/trac/trac/upgrades/db19.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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.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 trac.db import Table, Column, Index, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db20.py b/trac/trac/upgrades/db20.py
index c01d81c..b42b36b 100644
--- a/trac/trac/upgrades/db20.py
+++ b/trac/trac/upgrades/db20.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-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.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 trac.versioncontrol.cache import CACHE_YOUNGEST_REV
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db21.py b/trac/trac/upgrades/db21.py
index d25b32c..ac29688 100644
--- a/trac/trac/upgrades/db21.py
+++ b/trac/trac/upgrades/db21.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-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.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/.
+
 
 def do_upgrade(env, ver, cursor):
     """Upgrade the reports to better handle the new workflow capabilities"""
diff --git a/trac/trac/upgrades/db22.py b/trac/trac/upgrades/db22.py
index 3ca3ff8..ca72f85 100644
--- a/trac/trac/upgrades/db22.py
+++ b/trac/trac/upgrades/db22.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-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.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 trac.db import Table, Column, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db23.py b/trac/trac/upgrades/db23.py
index 2eb9222..070986c 100644
--- a/trac/trac/upgrades/db23.py
+++ b/trac/trac/upgrades/db23.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-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.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 trac.db import Table, Column, Index, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db24.py b/trac/trac/upgrades/db24.py
index 7f993ad..2c2632a 100644
--- a/trac/trac/upgrades/db24.py
+++ b/trac/trac/upgrades/db24.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2009-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.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 trac.db import Table, Column, Index, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db25.py b/trac/trac/upgrades/db25.py
index 39eee61..a4e513f 100644
--- a/trac/trac/upgrades/db25.py
+++ b/trac/trac/upgrades/db25.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010-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.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 trac.db import DatabaseManager
 
 
diff --git a/trac/trac/upgrades/db26.py b/trac/trac/upgrades/db26.py
index fa38bfe..b36b7e0 100644
--- a/trac/trac/upgrades/db26.py
+++ b/trac/trac/upgrades/db26.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2010-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.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/.
+
 
 def do_upgrade(env, ver, cursor):
     """Zero-pad Subversion revision numbers in the cache."""
diff --git a/trac/trac/upgrades/db27.py b/trac/trac/upgrades/db27.py
index 3d1dede..1bbdd04 100644
--- a/trac/trac/upgrades/db27.py
+++ b/trac/trac/upgrades/db27.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012-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.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 trac.db import Table, Column, DatabaseManager
 
 def do_upgrade(env, ver, cursor):
diff --git a/trac/trac/upgrades/db28.py b/trac/trac/upgrades/db28.py
index 4079285..4769ae7 100644
--- a/trac/trac/upgrades/db28.py
+++ b/trac/trac/upgrades/db28.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2012 Edgewall Software
+# Copyright (C) 2012-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
diff --git a/trac/trac/upgrades/db29.py b/trac/trac/upgrades/db29.py
index d933f75..bf5f70d 100644
--- a/trac/trac/upgrades/db29.py
+++ b/trac/trac/upgrades/db29.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2012 Edgewall Software
+# Copyright (C) 2012-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
diff --git a/trac/trac/upgrades/db3.py b/trac/trac/upgrades/db3.py
index 4276edc..c9a3ac6 100644
--- a/trac/trac/upgrades/db3.py
+++ b/trac/trac/upgrades/db3.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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.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/.
+
 sql = """
 CREATE TABLE attachment (
          type            text,
diff --git a/trac/trac/upgrades/db4.py b/trac/trac/upgrades/db4.py
index a2a47e5..bc8bcce 100644
--- a/trac/trac/upgrades/db4.py
+++ b/trac/trac/upgrades/db4.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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.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/.
+
 sql = [
 """CREATE TABLE session (
          sid             text,
diff --git a/trac/trac/upgrades/db5.py b/trac/trac/upgrades/db5.py
index 472a30c..1b23062 100644
--- a/trac/trac/upgrades/db5.py
+++ b/trac/trac/upgrades/db5.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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.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/.
+
 sql = [
 #-- Add unique id, descr to 'milestone'
 """CREATE TEMPORARY TABLE milestone_old AS SELECT * FROM milestone;""",
diff --git a/trac/trac/upgrades/db6.py b/trac/trac/upgrades/db6.py
index 16a17c5..ff9db2c 100644
--- a/trac/trac/upgrades/db6.py
+++ b/trac/trac/upgrades/db6.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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.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/.
+
 sql = """
 CREATE TABLE ticket_custom (
        ticket               integer,
@@ -9,4 +22,3 @@
 
 def do_upgrade(env, ver, cursor):
     cursor.execute(sql)
-
diff --git a/trac/trac/upgrades/db7.py b/trac/trac/upgrades/db7.py
index 5982d74..52b3f32 100644
--- a/trac/trac/upgrades/db7.py
+++ b/trac/trac/upgrades/db7.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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.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/.
+
 sql = [
 #-- Add readonly flag to 'wiki'
 """CREATE TEMPORARY TABLE wiki_old AS SELECT * FROM wiki;""",
diff --git a/trac/trac/upgrades/db8.py b/trac/trac/upgrades/db8.py
index 18621c4..87acc59 100644
--- a/trac/trac/upgrades/db8.py
+++ b/trac/trac/upgrades/db8.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 import time
 
 d = {'now':time.time()}
@@ -17,6 +30,7 @@
 SELECT name,time,descr FROM milestone_old WHERE time > %(now)s;""" % d
 ]
 
+
 def do_upgrade(env, ver, cursor):
     for s in sql:
         cursor.execute(s)
diff --git a/trac/trac/upgrades/db9.py b/trac/trac/upgrades/db9.py
index d644ba1..490fe36 100644
--- a/trac/trac/upgrades/db9.py
+++ b/trac/trac/upgrades/db9.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 import time
 
 sql = [
@@ -17,6 +30,7 @@
 """DROP TABLE session_old;"""
 ]
 
+
 def do_upgrade(env, ver, cursor):
     for s in sql:
         cursor.execute(s)
diff --git a/trac/trac/util/__init__.py b/trac/trac/util/__init__.py
index db45296..60c5b8e 100644
--- a/trac/trac/util/__init__.py
+++ b/trac/trac/util/__init__.py
@@ -20,6 +20,7 @@
 from __future__ import with_statement
 
 import errno
+import functools
 import inspect
 from itertools import izip, tee
 import locale
@@ -29,11 +30,13 @@
 import re
 import shutil
 import sys
+import struct
 import tempfile
 import time
 from urllib import quote, unquote, urlencode
 
 from .compat import any, md5, sha1, sorted
+from .datefmt import to_datetime, to_timestamp, utc
 from .text import exception_to_unicode, to_unicode, getpreferredencoding
 
 # -- req, session and web utils
@@ -255,6 +258,68 @@
             path = '%s.%d%s' % (parts[0], idx, parts[1])
 
 
+def create_zipinfo(filename, mtime=None, dir=False, executable=False, symlink=False,
+                   comment=None):
+    """Create a instance of `ZipInfo`.
+
+    :param filename: file name of the entry
+    :param mtime: modified time of the entry
+    :param dir: if `True`, the entry is a directory
+    :param executable: if `True`, the entry is a executable file
+    :param symlink: if `True`, the entry is a symbolic link
+    :param comment: comment of the entry
+    """
+    from zipfile import ZipInfo, ZIP_DEFLATED, ZIP_STORED
+    zipinfo = ZipInfo()
+
+    # The general purpose bit flag 11 is used to denote
+    # UTF-8 encoding for path and comment. Only set it for
+    # non-ascii files for increased portability.
+    # See http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+    if any(ord(c) >= 128 for c in filename):
+        zipinfo.flag_bits |= 0x0800
+    zipinfo.filename = filename.encode('utf-8')
+
+    if mtime is not None:
+        mtime = to_datetime(mtime, utc)
+        zipinfo.date_time = mtime.utctimetuple()[:6]
+        # The "extended-timestamp" extra field is used for the
+        # modified time of the entry in unix time. It avoids
+        # extracting wrong modified time if non-GMT timezone.
+        # See http://www.opensource.apple.com/source/zip/zip-6/unzip/unzip
+        #     /proginfo/extra.fld
+        zipinfo.extra += struct.pack(
+            '<hhBl',
+            0x5455,                 # extended-timestamp extra block type
+            1 + 4,                  # size of this block
+            1,                      # modification time is present
+            to_timestamp(mtime))    # time of last modification
+
+    # external_attr is 4 bytes in size. The high order two
+    # bytes represent UNIX permission and file type bits,
+    # while the low order two contain MS-DOS FAT file
+    # attributes, most notably bit 4 marking directories.
+    if dir:
+        if not zipinfo.filename.endswith('/'):
+            zipinfo.filename += '/'
+        zipinfo.compress_type = ZIP_STORED
+        zipinfo.external_attr = 040755 << 16L       # permissions drwxr-xr-x
+        zipinfo.external_attr |= 0x10               # MS-DOS directory flag
+    else:
+        zipinfo.compress_type = ZIP_DEFLATED
+        zipinfo.external_attr = 0644 << 16L         # permissions -r-wr--r--
+        if executable:
+            zipinfo.external_attr |= 0755 << 16L    # -rwxr-xr-x
+        if symlink:
+            zipinfo.compress_type = ZIP_STORED
+            zipinfo.external_attr |= 0120000 << 16L # symlink file type
+
+    if comment:
+        zipinfo.comment = comment.encode('utf-8')
+
+    return zipinfo
+
+
 class NaivePopen:
     """This is a deadlock-safe version of popen that returns an object with
     errorlevel, out (a string) and err (a string).
@@ -299,6 +364,39 @@
                 os.remove(errfile)
 
 
+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
+        try:
+            os.kill(process.pid, signal.SIGTERM)
+        except OSError, e:
+            # If the process has already finished and has not been
+            # waited for, killing it raises an ESRCH error on Cygwin
+            import errno
+            if e.errno != errno.ESRCH:
+                raise
+
+    if sys.platform == 'win32':
+        return terminate_win(process)
+    return terminate_nix(process)
+
+
 def makedirs(path, overwrite=False):
     """Create as many directories as necessary to make `path` exist.
 
@@ -513,7 +611,7 @@
         return __import__(module_name, globals(), locals(), [])
     except Exception, e:
         for modname in sys.modules.copy():
-            if not already_imported.has_key(modname):
+            if modname not in already_imported:
                 del(sys.modules[modname])
         raise e
 
@@ -611,11 +709,33 @@
     """
     import types
     if isinstance(dist, types.ModuleType):
+        def has_resource(dist, resource_name):
+            if dist.location.endswith('.egg'):  # installed by easy_install
+                return dist.has_resource(resource_name)
+            if dist.has_metadata('installed-files.txt'):  # installed by pip
+                resource_name = os.path.normpath('../' + resource_name)
+                return any(resource_name == os.path.normpath(name)
+                           for name
+                           in dist.get_metadata_lines('installed-files.txt'))
+            if dist.has_metadata('SOURCES.txt'):
+                resource_name = os.path.normpath(resource_name)
+                return any(resource_name == os.path.normpath(name)
+                           for name in dist.get_metadata_lines('SOURCES.txt'))
+            toplevel = resource_name.split('/')[0]
+            if dist.has_metadata('top_level.txt'):
+                return toplevel in dist.get_metadata_lines('top_level.txt')
+            return dist.key == toplevel.lower()
         module = dist
         module_path = get_module_path(module)
+        resource_name = module.__name__.replace('.', '/')
+        if os.path.basename(module.__file__) in ('__init__.py', '__init__.pyc',
+                                                 '__init__.pyo'):
+            resource_name += '/__init__.py'
+        else:
+            resource_name += '.py'
         for dist in find_distributions(module_path, only=True):
             if os.path.isfile(module_path) or \
-                   dist.key == module.__name__.lower():
+                    has_resource(dist, resource_name):
                 break
         else:
             return {}
@@ -639,6 +759,20 @@
             info[normalize(attr)] = err
     return info
 
+
+def warn_setuptools_issue(out=None):
+    if not out:
+        out = sys.stderr
+    import setuptools
+    from pkg_resources import parse_version as parse
+    if parse('5.4') <= parse(setuptools.__version__) < parse('5.7') and \
+            not os.environ.get('PKG_RESOURCES_CACHE_ZIP_MANIFESTS'):
+        out.write("Warning: Detected setuptools version %s. The environment "
+                  "variable 'PKG_RESOURCES_CACHE_ZIP_MANIFESTS' must be set "
+                  "to avoid significant performance degradation.\n"
+                  % setuptools.__version__)
+
+
 # -- crypto utils
 
 try:
@@ -983,18 +1117,30 @@
 
 
 class lazy(object):
-    """A lazily-evaluated attribute"""
+    """A lazily-evaluated attribute.
+
+    :since: 1.0
+    """
 
     def __init__(self, fn):
         self.fn = fn
+        functools.update_wrapper(self, fn)
 
     def __get__(self, instance, owner):
         if instance is None:
             return self
+        if self.fn.__name__ in instance.__dict__:
+            return instance.__dict__[self.fn.__name__]
         result = self.fn(instance)
-        setattr(instance, self.fn.__name__, result)
+        instance.__dict__[self.fn.__name__] = result
         return result
 
+    def __set__(self, instance, value):
+        instance.__dict__[self.fn.__name__] = value
+
+    def __delete__(self, instance):
+        del instance.__dict__[self.fn.__name__]
+
 
 # -- algorithmic utilities
 
diff --git a/trac/trac/util/compat.py b/trac/trac/util/compat.py
index 4b47b9d..2724a87 100644
--- a/trac/trac/util/compat.py
+++ b/trac/trac/util/compat.py
@@ -17,7 +17,9 @@
 previous versions of Python from 2.5 onward.
 """
 
+import math
 import os
+import time
 
 # Import symbols previously defined here, kept around so that plugins importing
 # them don't suddenly stop working
@@ -95,3 +97,19 @@
             while lines and not lines[0]:
                 lines.pop(0)
             return '\n'.join(lines)
+
+
+def wait_for_file_mtime_change(filename):
+    """This function is typically called before a file save operation,
+     waiting if necessary for the file modification time to change. The
+     purpose is to avoid successive file updates going undetected by the
+     caching mechanism that depends on a change in the file modification
+     time to know when the file should be reparsed."""
+    try:
+        mtime = os.stat(filename).st_mtime
+        os.utime(filename, None)
+        while mtime == os.stat(filename).st_mtime:
+            time.sleep(1e-3)
+            os.utime(filename, None)
+    except OSError:
+        pass  # file doesn't exist (yet)
diff --git a/trac/trac/util/daemon.py b/trac/trac/util/daemon.py
index 2924a73..d823247 100644
--- a/trac/trac/util/daemon.py
+++ b/trac/trac/util/daemon.py
@@ -19,6 +19,7 @@
 import signal
 import sys
 
+
 def daemonize(pidfile=None, progname=None, stdin='/dev/null',
               stdout='/dev/null', stderr='/dev/null', umask=022):
     """Fork a daemon process."""
diff --git a/trac/trac/util/datefmt.py b/trac/trac/util/datefmt.py
index 7bbabc2..7058c0e 100644
--- a/trac/trac/util/datefmt.py
+++ b/trac/trac/util/datefmt.py
@@ -26,8 +26,11 @@
 
 try:
     import babel
+except ImportError:
+    babel = None
+else:
     from babel import Locale
-    from babel.core import LOCALE_ALIASES
+    from babel.core import LOCALE_ALIASES, UnknownLocaleError
     from babel.dates import (
         format_datetime as babel_format_datetime,
         format_date as babel_format_date,
@@ -36,12 +39,10 @@
         get_time_format, get_month_names,
         get_period_names, get_day_names
     )
-except ImportError:
-    babel = None
 
 from trac.core import TracError
 from trac.util.text import to_unicode, getpreferredencoding
-from trac.util.translation import _, ngettext, get_available_locales
+from trac.util.translation import _, ngettext
 
 # Date/time utilities
 
@@ -54,12 +55,14 @@
 
     ``t`` is converted using the following rules:
 
-     - If ``t`` is already a `datetime` object,
-       - if it is timezone-"naive", it is localized to ``tzinfo``
-       - if it is already timezone-aware, ``t`` is mapped to the given
-         timezone (`datetime.datetime.astimezone`)
-     - If ``t`` is None, the current time will be used.
-     - If ``t`` is a number, it is interpreted as a timestamp.
+    * If ``t`` is already a `datetime` object,
+
+     * if it is timezone-"naive", it is localized to ``tzinfo``
+     * if it is already timezone-aware, ``t`` is mapped to the given
+       timezone (`datetime.datetime.astimezone`)
+
+    * If ``t`` is None, the current time will be used.
+    * If ``t`` is a number, it is interpreted as a timestamp.
 
     Any other input will trigger a `TypeError`.
 
@@ -87,6 +90,8 @@
                     timedelta(seconds=frac + 1)
         else:
             dt = datetime.fromtimestamp(t, tz)
+    else:
+        dt = None
     if dt:
         return tz.normalize(dt)
     raise TypeError('expecting datetime, int, long, float, or None; got %s' %
@@ -154,51 +159,45 @@
     'date': {'short': '%x', 'medium': '%x', 'long': '%x', 'full': '%x'},
     'time': {'short': '%H:%M', 'medium': '%X', 'long': '%X', 'full': '%X'},
 }
-_ISO8601_FORMATS = {
-    'datetime': {
-        '%x %X': 'iso8601', '%x': 'iso8601date', '%X': 'iso8601time',
-        'short': '%Y-%m-%dT%H:%M', 'medium': '%Y-%m-%dT%H:%M:%S',
-        'long': 'iso8601', 'full': 'iso8601',
-        'iso8601': 'iso8601', None: 'iso8601'},
-    'date': {
-        '%x %X': 'iso8601', '%x': 'iso8601date', '%X': 'iso8601time',
-        'short': 'iso8601date', 'medium': 'iso8601date',
-        'long': 'iso8601date', 'full': 'iso8601date',
-        'iso8601': 'iso8601date', None: 'iso8601date'},
-    'time': {
-        '%x %X': 'iso8601', '%x': 'iso8601date', '%X': 'iso8601time',
-        'short': '%H:%M', 'medium': '%H:%M:%S',
-        'long': 'iso8601time', 'full': 'iso8601time',
-        'iso8601': 'iso8601time', None: 'iso8601time'},
-}
 _STRFTIME_HINTS = {'%x %X': 'datetime', '%x': 'date', '%X': 'time'}
 
 def _format_datetime_without_babel(t, format):
-    normalize_Z = False
-    if format.lower().startswith('iso8601'):
-        if 'date' in format:
-            format = '%Y-%m-%d'
-        elif 'time' in format:
-            format = '%H:%M:%S%z'
-            normalize_Z = True
-        else:
-            format = '%Y-%m-%dT%H:%M:%S%z'
-            normalize_Z = True
     text = t.strftime(str(format))
-    if normalize_Z:
-        text = text.replace('+0000', 'Z')
-        if not text.endswith('Z'):
-            text = text[:-2] + ":" + text[-2:]
     encoding = getlocale(LC_TIME)[1] or getpreferredencoding() \
                or sys.getdefaultencoding()
     return unicode(text, encoding, 'replace')
 
+def _format_datetime_iso8601(t, format, hint):
+    if format != 'full':
+        t = t.replace(microsecond=0)
+    text = t.isoformat()  # YYYY-MM-DDThh:mm:ss.SSSSSS±hh:mm
+    if format == 'short':
+        text = text[:16]  # YYYY-MM-DDThh:mm
+    elif format == 'medium':
+        text = text[:19]  # YYYY-MM-DDThh:mm:ss
+    elif text.endswith('+00:00'):
+        text = text[:-6] + 'Z'
+    if hint == 'date':
+        text = text.split('T', 1)[0]
+    elif hint == 'time':
+        text = text.split('T', 1)[1]
+    return unicode(text, 'ascii')
+
 def _format_datetime(t, format, tzinfo, locale, hint):
     t = to_datetime(t, tzinfo or localtz)
 
-    if (format in ('iso8601', 'iso8601date', 'iso8601time') or
-        locale == 'iso8601'):
-        format = _ISO8601_FORMATS[hint].get(format, format)
+    if format == 'iso8601':
+        return _format_datetime_iso8601(t, 'long', hint)
+    if format in ('iso8601date', 'iso8601time'):
+        return _format_datetime_iso8601(t, 'long', format[7:])
+    if locale == 'iso8601':
+        if format is None:
+            format = 'long'
+        elif format in _STRFTIME_HINTS:
+            hint = _STRFTIME_HINTS[format]
+            format = 'long'
+        if format in ('short', 'medium', 'long', 'full'):
+            return _format_datetime_iso8601(t, format, hint)
         return _format_datetime_without_babel(t, format)
 
     if babel and locale:
@@ -254,11 +253,17 @@
     if babel and locale:
         format = get_date_format('medium', locale=locale)
         return format.pattern
+    return _libc_get_date_format_hint()
 
+def _libc_get_date_format_hint(format=None):
     t = datetime(1999, 10, 29, tzinfo=utc)
     tmpl = format_date(t, tzinfo=utc)
-    return tmpl.replace('1999', 'YYYY', 1).replace('99', 'YY', 1) \
-               .replace('10', 'MM', 1).replace('29', 'DD', 1)
+    units = [('1999', 'YYYY'), ('99', 'YY'), ('10', 'MM'), ('29', 'dd')]
+    if format:
+        units = [(unit[0], '%(' + unit[1] + ')s') for unit in units]
+    for unit in units:
+        tmpl = tmpl.replace(unit[0], unit[1], 1)
+    return tmpl
 
 def get_datetime_format_hint(locale=None):
     """Present the default format used by `format_datetime` in a human readable
@@ -274,16 +279,22 @@
         format = get_datetime_format('medium', locale=locale)
         return format.replace('{0}', time_pattern) \
                      .replace('{1}', date_pattern)
+    return _libc_get_datetime_format_hint()
 
+def _libc_get_datetime_format_hint(format=None):
     t = datetime(1999, 10, 29, 23, 59, 58, tzinfo=utc)
     tmpl = format_datetime(t, tzinfo=utc)
     ampm = format_time(t, '%p', tzinfo=utc)
+    units = []
     if ampm:
-        tmpl = tmpl.replace(ampm, 'a', 1)
-    return tmpl.replace('1999', 'YYYY', 1).replace('99', 'YY', 1) \
-               .replace('10', 'MM', 1).replace('29', 'DD', 1) \
-               .replace('23', 'hh', 1).replace('11', 'hh', 1) \
-               .replace('59', 'mm', 1).replace('58', 'ss', 1)
+        units.append((ampm, 'a'))
+    units.extend([('1999', 'YYYY'), ('99', 'YY'), ('10', 'MM'), ('29', 'dd'),
+                  ('23', 'hh'), ('11', 'hh'), ('59', 'mm'), ('58', 'ss')])
+    if format:
+        units = [(unit[0], '%(' + unit[1] + ')s') for unit in units]
+    for unit in units:
+        tmpl = tmpl.replace(unit[0], unit[1], 1)
+    return tmpl
 
 def get_month_names_jquery_ui(req):
     """Get the month names for the jQuery UI datepicker library"""
@@ -372,6 +383,19 @@
     if locale == 'iso8601':
         return 1 # Monday
     if babel and locale:
+        if not locale.territory:
+            # search first locale which has the same `langauge` and territory
+            # in preferred languages
+            for l in req.languages:
+                l = l.replace('-', '_').lower()
+                if l.startswith(locale.language.lower() + '_'):
+                    try:
+                        l = Locale.parse(l)
+                        if l.territory:
+                            locale = l
+                            break
+                    except UnknownLocaleError:
+                        pass
         if not locale.territory and locale.language in LOCALE_ALIASES:
             locale = Locale.parse(LOCALE_ALIASES[locale.language])
         return (locale.first_week_day + 1) % 7
@@ -437,6 +461,21 @@
 
     return None
 
+def _libc_parse_date(text, tzinfo):
+    for format in ('%x %X', '%x, %X', '%X %x', '%X, %x', '%x', '%c',
+                   '%b %d, %Y'):
+        try:
+            tm = time.strptime(text, format)
+            dt = tzinfo.localize(datetime(*tm[0:6]))
+            return tzinfo.normalize(dt)
+        except ValueError:
+            continue
+    try:
+        return _i18n_parse_date(text, tzinfo, None)
+    except ValueError:
+        pass
+    return
+
 def parse_date(text, tzinfo=None, locale=None, hint='date'):
     tzinfo = tzinfo or localtz
     text = text.strip()
@@ -446,24 +485,25 @@
         if babel and locale:
             dt = _i18n_parse_date(text, tzinfo, locale)
         else:
-            for format in ['%x %X', '%x, %X', '%X %x', '%X, %x', '%x', '%c',
-                           '%b %d, %Y']:
-                try:
-                    tm = time.strptime(text, format)
-                    dt = tzinfo.localize(datetime(*tm[0:6]))
-                    dt = tzinfo.normalize(dt)
-                    break
-                except ValueError:
-                    continue
+            dt = _libc_parse_date(text, tzinfo)
     if dt is None:
         dt = _parse_relative_time(text, tzinfo)
     if dt is None:
-        hint = {'datetime': get_datetime_format_hint,
-                'date': get_date_format_hint
-               }.get(hint, lambda(l): hint)(locale)
-        raise TracError(_('"%(date)s" is an invalid date, or the date format '
-                          'is not known. Try "%(hint)s" instead.',
-                          date=text, hint=hint), _('Invalid Date'))
+        formatted_hint = {
+            'datetime': get_datetime_format_hint,
+            'date': get_date_format_hint,
+            'iso8601': lambda l: get_datetime_format_hint('iso8601'),
+        }.get(hint, lambda(l): hint)(locale)
+        if hint != 'iso8601':
+            msg = _('"%(date)s" is an invalid date, or the date format '
+                    'is not known. Try "%(hint)s" or "%(isohint)s" instead.',
+                    date=text, hint=formatted_hint,
+                    isohint=get_datetime_format_hint('iso8601'))
+        else:
+            msg = _('"%(date)s" is an invalid date, or the date format '
+                    'is not known. Try "%(hint)s" instead.',
+                    date=text, hint=formatted_hint)
+        raise TracError(msg, _('Invalid Date'))
     # Make sure we can convert it to a timestamp and back - fromtimestamp()
     # may raise ValueError if larger than platform C localtime() or gmtime()
     try:
@@ -483,15 +523,17 @@
         'm': ('m',),
         's': ('s',),
     }
-    regexp = [r'[0-9]+']
 
-    date_format = get_date_format('medium', locale=locale)
-    time_format = get_time_format('medium', locale=locale)
-    datetime_format = get_datetime_format('medium', locale=locale)
-    formats = (
-        datetime_format.replace('{0}', time_format.format) \
-                       .replace('{1}', date_format.format),
-        date_format.format)
+    if locale is None:
+        formats = (_libc_get_datetime_format_hint(format=True),
+                   _libc_get_date_format_hint(format=True))
+    else:
+        date_format = get_date_format('medium', locale=locale)
+        time_format = get_time_format('medium', locale=locale)
+        datetime_format = get_datetime_format('medium', locale=locale)
+        formats = (datetime_format.replace('{0}', time_format.format) \
+                                  .replace('{1}', date_format.format),
+                   date_format.format)
 
     orders = []
     for format in formats:
@@ -503,49 +545,64 @@
                     order.append((idx, key))
                     break
         order.sort()
-        order = dict((key, idx) for idx, (_, key) in enumerate(order))
-        orders.append(order)
+        orders.append(dict((key, idx) for idx, (_, key) in enumerate(order)))
 
-    month_names = {
-        'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6,
-        'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12,
-    }
-    if formats[0].find('%(MMM)s') != -1:
-        for width in ('wide', 'abbreviated'):
-            names = get_month_names(width, locale=locale)
-            for num, name in names.iteritems():
-                name = name.lower()
-                month_names[name] = num
-    regexp.extend(month_names.iterkeys())
-
+    # always allow using English names regardless of locale
+    month_names = dict(zip(('jan', 'feb', 'mar', 'apr', 'may', 'jun',
+                            'jul', 'aug', 'sep', 'oct', 'nov', 'dec',),
+                           xrange(1, 13)))
     period_names = {'am': 'am', 'pm': 'pm'}
-    if formats[0].find('%(a)s') != -1:
-        names = get_period_names(locale=locale)
-        for period, name in names.iteritems():
-            name = name.lower()
-            period_names[name] = period
-    regexp.extend(period_names.iterkeys())
+
+    if locale is None:
+        for num in xrange(1, 13):
+            t = datetime(1999, num, 1, tzinfo=utc)
+            names = format_date(t, '%b\t%B', utc).split('\t')
+            month_names.update((name.lower(), num) for name in names
+                               if str(num) not in name)
+        for num, period in ((11, 'am'), (23, 'pm')):
+            t = datetime(1999, 1, 1, num, tzinfo=utc)
+            name = format_datetime(t, '%p', utc)
+            if name:
+                period_names[name.lower()] = period
+    else:
+        if formats[0].find('%(MMM)s') != -1:
+            for width in ('wide', 'abbreviated'):
+                names = get_month_names(width, locale=locale)
+                month_names.update((name.lower(), num)
+                                   for num, name in names.iteritems())
+        if formats[0].find('%(a)s') != -1:
+            names = get_period_names(locale=locale)
+            period_names.update((name.lower(), period)
+                                for period, name in names.iteritems()
+                                if period in ('am', 'pm'))
+
+    regexp = ['[0-9]+']
+    regexp.extend(re.escape(name) for name in month_names)
+    regexp.extend(re.escape(name) for name in period_names)
 
     return {
         'orders': orders,
-        'regexp': re.compile('(%s)' % '|'.join(regexp),
-                             re.IGNORECASE | re.UNICODE),
+        'regexp': re.compile('(%s)' % '|'.join(regexp), re.IGNORECASE),
         'month_names': month_names,
         'period_names': period_names,
     }
 
-_I18N_PARSE_DATE_PATTERNS = dict(map(lambda l: (l, False),
-                                     get_available_locales()))
+_I18N_PARSE_DATE_PATTERNS = {}
+_I18N_PARSE_DATE_PATTERNS_LIBC = {}
 
 def _i18n_parse_date(text, tzinfo, locale):
-    locale = Locale.parse(locale)
-    key = str(locale)
-    pattern = _I18N_PARSE_DATE_PATTERNS.get(key)
-    if pattern is False:
-        pattern = _i18n_parse_date_pattern(locale)
-        _I18N_PARSE_DATE_PATTERNS[key] = pattern
+    if locale is None:
+        key = getlocale(LC_TIME)[0]
+        patterns = _I18N_PARSE_DATE_PATTERNS_LIBC
+    else:
+        locale = Locale.parse(locale)
+        key = str(locale)
+        patterns = _I18N_PARSE_DATE_PATTERNS
+
+    pattern = patterns.get(key)
     if pattern is None:
-        return None
+        pattern = _i18n_parse_date_pattern(locale)
+        patterns[key] = pattern
 
     regexp = pattern['regexp']
     period_names = pattern['period_names']
@@ -749,67 +806,103 @@
 
     @classmethod
     def _initialize(cls):
-        cls._std_tz = cls(False)
         cls._std_offset = timedelta(seconds=-time.timezone)
+        cls._std_tz = cls(cls._std_offset)
         if time.daylight:
-            cls._dst_tz = cls(True)
             cls._dst_offset = timedelta(seconds=-time.altzone)
+            cls._dst_tz = cls(cls._dst_offset)
         else:
-            cls._dst_tz = cls._std_tz
             cls._dst_offset = cls._std_offset
+            cls._dst_tz = cls._std_tz
         cls._dst_diff = cls._dst_offset - cls._std_offset
 
-    def __init__(self, is_dst=None):
-        self.is_dst = is_dst
+    def __init__(self, offset=None):
+        self._offset = offset
 
     def __str__(self):
-        offset = self.utcoffset(datetime.now())
-        secs = offset.days * 3600 * 24 + offset.seconds
-        hours, rem = divmod(abs(secs), 3600)
-        return 'UTC%c%02d:%02d' % ('-' if secs < 0 else '+', hours, rem / 60)
+        return self._tzname_offset(self.utcoffset(datetime.now()))
 
     def __repr__(self):
-        if self.is_dst is None:
+        if self._offset is None:
             return '<LocalTimezone "%s" %s "%s" %s>' % \
                    (time.tzname[False], self._std_offset,
                     time.tzname[True], self._dst_offset)
-        if self.is_dst:
-            offset = self._dst_offset
+        return '<LocalTimezone "%s" %s>' % (self._tzname(), self._offset)
+
+    def _tzname(self):
+        if self is self._std_tz:
+            return time.tzname[False]
+        elif self is self._dst_tz:
+            return time.tzname[True]
+        elif self._offset is not None:
+            return self._tzname_offset(self._offset)
         else:
-            offset = self._std_offset
-        return '<LocalTimezone "%s" %s>' % (time.tzname[self.is_dst], offset)
+            return '%s, %s' % time.tzname
+
+    def _tzname_offset(self, offset):
+        secs = offset.days * 3600 * 24 + offset.seconds
+        hours, rem = divmod(abs(secs), 3600)
+        return 'UTC%c%02d:%02d' % ('+-'[secs < 0], hours, rem / 60)
+
+    def _tzinfo(self, dt, is_dst=False):
+        tzinfo = dt.tzinfo
+        if isinstance(tzinfo, LocalTimezone) and tzinfo._offset is not None:
+            return tzinfo
+
+        base_tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
+                   dt.weekday(), 0)
+        local_tt = [None, None]
+        for idx in (0, 1):
+            try:
+                local_tt[idx] = time.localtime(time.mktime(base_tt + (idx,)))
+            except (ValueError, OverflowError):
+                pass
+        if local_tt[0] is local_tt[1] is None:
+            return self._std_tz
+
+        std_correct = local_tt[0] and local_tt[0].tm_isdst == 0
+        dst_correct = local_tt[1] and local_tt[1].tm_isdst == 1
+        if is_dst is None and std_correct is dst_correct:
+            if std_correct:
+                raise ValueError('Ambiguous time "%s"' % dt)
+            if not std_correct:
+                raise ValueError('Non existent time "%s"' % dt)
+        tt = None
+        if std_correct is dst_correct is True:
+            tt = local_tt[bool(is_dst)]
+        elif std_correct is True:
+            tt = local_tt[0]
+        elif dst_correct is True:
+            tt = local_tt[1]
+        if tt:
+            utc_ts = to_timestamp(datetime(tzinfo=utc, *tt[:6]))
+            tz_offset = timedelta(seconds=utc_ts - time.mktime(tt))
+        else:
+            dt = dt.replace(tzinfo=utc)
+            utc_ts = to_timestamp(dt)
+            dt -= timedelta(hours=6)
+            tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
+                  dt.weekday(), 0, -1)
+            tz_offset = timedelta(seconds=utc_ts - time.mktime(tt) - 6 * 3600)
+
+        # if UTC offset doesn't match timezone offset, create a
+        # LocalTimezone instance with the UTC offset (#11563)
+        if tz_offset == self._std_offset:
+            tz = self._std_tz
+        elif tz_offset == self._dst_offset:
+            tz = self._dst_tz
+        else:
+            tz = LocalTimezone(tz_offset)
+        return tz
 
     def _is_dst(self, dt, is_dst=False):
-        if self.is_dst is not None:
-            return self.is_dst
-
-        tt = (dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
-              dt.weekday(), 0)
-        try:
-            std_tt = time.localtime(time.mktime(tt + (0,)))
-            dst_tt = time.localtime(time.mktime(tt + (1,)))
-        except (ValueError, OverflowError):
-            return False
-
-        std_correct = std_tt.tm_isdst == 0
-        dst_correct = dst_tt.tm_isdst == 1
-        if std_correct is dst_correct:
-            if is_dst is None:
-                if std_correct is True:
-                    raise ValueError('Ambiguous time "%s"' % dt)
-                if std_correct is False:
-                    raise ValueError('Non existent time "%s"' % dt)
-            return is_dst
-        if std_correct:
-            return False
-        if dst_correct:
+        tz = self._tzinfo(dt, is_dst)
+        if tz is self._dst_tz:
             return True
+        return False
 
     def utcoffset(self, dt):
-        if self._is_dst(dt):
-            return self._dst_offset
-        else:
-            return self._std_offset
+        return self._tzinfo(dt)._offset
 
     def dst(self, dt):
         if self._is_dst(dt):
@@ -818,16 +911,12 @@
             return _zero
 
     def tzname(self, dt):
-        return time.tzname[self._is_dst(dt)]
+        return self._tzinfo(dt)._tzname()
 
     def localize(self, dt, is_dst=False):
         if dt.tzinfo is not None:
             raise ValueError('Not naive datetime (tzinfo is already set)')
-        if self._is_dst(dt, is_dst):
-            tz = self._dst_tz
-        else:
-            tz = self._std_tz
-        return dt.replace(tzinfo=tz)
+        return dt.replace(tzinfo=self._tzinfo(dt, is_dst))
 
     def normalize(self, dt, is_dst=False):
         if dt.tzinfo is None:
@@ -839,12 +928,22 @@
     def fromutc(self, dt):
         if dt.tzinfo is None or dt.tzinfo is not self:
             raise ValueError('fromutc: dt.tzinfo is not self')
-        tt = time.localtime(to_timestamp(dt.replace(tzinfo=utc)))
-        if tt.tm_isdst > 0:
+        dt = dt.replace(tzinfo=utc)
+        try:
+            tt = time.localtime(to_timestamp(dt))
+        except ValueError:
+            return dt.replace(tzinfo=self._std_tz) + self._std_offset
+        # if UTC offset from localtime() doesn't match timezone offset,
+        # create a LocalTimezone instance with the UTC offset (#11563)
+        new_dt = datetime(*(tt[:6] + (dt.microsecond, utc)))
+        tz_offset = new_dt - dt
+        if tz_offset == self._std_offset:
+            tz = self._std_tz
+        elif tz_offset == self._dst_offset:
             tz = self._dst_tz
         else:
-            tz = self._std_tz
-        return datetime(microsecond=dt.microsecond, tzinfo=tz, *tt[0:6])
+            tz = LocalTimezone(tz_offset)
+        return new_dt.replace(tzinfo=tz)
 
 
 utc = FixedOffset(0, 'UTC')
diff --git a/trac/trac/util/dist.py b/trac/trac/util/dist.py
index 0d6132e..623df0a 100644
--- a/trac/trac/util/dist.py
+++ b/trac/trac/util/dist.py
@@ -17,4 +17,3 @@
     from trac.dist import extract_javascript_script
 except ImportError:
     pass
-
diff --git a/trac/trac/util/html.py b/trac/trac/util/html.py
index 42aef8f..c345bd1 100644
--- a/trac/trac/util/html.py
+++ b/trac/trac/util/html.py
@@ -16,12 +16,19 @@
 
 from genshi import Markup, HTML, escape, unescape
 from genshi.core import stripentities, striptags, START, END
-from genshi.builder import Element, ElementFactory, Fragment
+from genshi.builder import Element, ElementFactory, Fragment, tag
 from genshi.filters.html import HTMLSanitizer
 from genshi.input import ParseError
+try:
+    from babel.support import LazyProxy
+except ImportError:
+    LazyProxy = None
 
-__all__ = ['escape', 'unescape', 'html', 'plaintext', 'find_element',
-           'TracHTMLSanitizer', 'Deuglifier', 'FormTokenInjector']
+from trac.core import TracError
+from trac.util.text import to_unicode
+
+__all__ = ['Deuglifier', 'FormTokenInjector', 'TracHTMLSanitizer', 'escape',
+           'find_element', 'html', 'plaintext', 'to_fragment', 'unescape']
 
 
 class TracHTMLSanitizer(HTMLSanitizer):
@@ -39,15 +46,17 @@
         'background', 'background-attachment', 'background-color',
         'background-image', 'background-position', 'background-repeat',
         'border', 'border-bottom', 'border-bottom-color',
-        'border-bottom-style', 'border-bottom-width', 'border-collapse',
-        'border-color', 'border-left', 'border-left-color',
-        'border-left-style', 'border-left-width', 'border-right',
-        'border-right-color', 'border-right-style', 'border-right-width',
-        'border-spacing', 'border-style', 'border-top', 'border-top-color',
+        'border-bottom-style', 'border-bottom-left-radius',
+        'border-bottom-right-radius', 'border-bottom-width',
+        'border-collapse', 'border-color', 'border-left', 'border-left-color',
+        'border-left-style', 'border-left-width', 'border-radius',
+        'border-right', 'border-right-color', 'border-right-style',
+        'border-right-width', 'border-spacing', 'border-style', 'border-top',
+        'border-top-color', 'border-top-left-radius', 'border-top-right-radius',
         'border-top-style', 'border-top-width', 'border-width', 'bottom',
         'caption-side', 'clear', 'clip', 'color', 'content',
-        'counter-increment', 'counter-reset', 'cursor', 'direction', 'display',
-        'empty-cells', 'float', 'font', 'font-family', 'font-size',
+        'counter-increment', 'counter-reset', 'cursor', 'direction',
+        'display', 'empty-cells', 'float', 'font', 'font-family', 'font-size',
         'font-style', 'font-variant', 'font-weight', 'height', 'left',
         'letter-spacing', 'line-height', 'list-style', 'list-style-image',
         'list-style-position', 'list-style-type', 'margin', 'margin-bottom',
@@ -297,18 +306,20 @@
     return text
 
 
-def find_element(frag, attr=None, cls=None):
-    """Return the first element in the fragment having the given attribute or
-    class, using a preorder depth-first search.
+def find_element(frag, attr=None, cls=None, tag=None):
+    """Return the first element in the fragment having the given attribute,
+    class or tag, using a preorder depth-first search.
     """
     if isinstance(frag, Element):
         if attr is not None and attr in frag.attrib:
             return frag
         if cls is not None and cls in frag.attrib.get('class', '').split():
             return frag
+        if tag is not None and tag == frag.tag:
+            return frag
     if isinstance(frag, Fragment):
         for child in frag.children:
-            elt = find_element(child, attr, cls)
+            elt = find_element(child, attr, cls, tag)
             if elt is not None:
                 return elt
 
@@ -328,3 +339,15 @@
                 yield event
         else:
             yield event
+
+
+def to_fragment(input):
+    """Convert input to a `Fragment` object."""
+
+    if isinstance(input, TracError):
+        input = input.message
+    if LazyProxy and isinstance(input, LazyProxy):
+        input = input.value
+    if isinstance(input, Fragment):
+        return input
+    return tag(to_unicode(input))
diff --git a/trac/trac/util/tests/__init__.py b/trac/trac/util/tests/__init__.py
index 375cb28..369e816 100644
--- a/trac/trac/util/tests/__init__.py
+++ b/trac/trac/util/tests/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2009 Edgewall Software
+# Copyright (C) 2006-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -15,13 +15,18 @@
 
 import doctest
 import os.path
+import pkg_resources
 import random
 import re
+import sys
 import tempfile
 import unittest
 
+import trac
+import trac.tests.compat
 from trac import util
-from trac.util.tests import concurrency, datefmt, presentation, text, html
+from trac.util.tests import concurrency, datefmt, presentation, text, \
+                            translation, html
 
 
 class AtomicFileTestCase(unittest.TestCase):
@@ -38,7 +43,7 @@
     def test_non_existing(self):
         with util.AtomicFile(self.path) as f:
             f.write('test content')
-        self.assertEqual(True, f.closed)
+        self.assertTrue(f.closed)
         self.assertEqual('test content', util.read_file(self.path))
 
     def test_existing(self):
@@ -46,7 +51,7 @@
         self.assertEqual('Some content', util.read_file(self.path))
         with util.AtomicFile(self.path) as f:
             f.write('Some new content')
-        self.assertEqual(True, f.closed)
+        self.assertTrue(f.closed)
         self.assertEqual('Some new content', util.read_file(self.path))
 
     if util.can_rename_open_file:
@@ -56,8 +61,8 @@
             with open(self.path) as rf:
                 with util.AtomicFile(self.path) as f:
                     f.write('Replaced content')
-            self.assertEqual(True, rf.closed)
-            self.assertEqual(True, f.closed)
+            self.assertTrue(rf.closed)
+            self.assertTrue(f.closed)
             self.assertEqual('Replaced content', util.read_file(self.path))
 
     # FIXME: It is currently not possible to make this test pass on all
@@ -70,18 +75,18 @@
         self.path = os.path.join(tempfile.gettempdir(), u'träc-témpfilè')
         with util.AtomicFile(self.path) as f:
             f.write('test content')
-        self.assertEqual(True, f.closed)
+        self.assertTrue(f.closed)
         self.assertEqual('test content', util.read_file(self.path))
 
 
 class PathTestCase(unittest.TestCase):
 
     def assert_below(self, path, parent):
-        self.assert_(util.is_path_below(path.replace('/', os.sep),
-                                        parent.replace('/', os.sep)))
+        self.assertTrue(util.is_path_below(path.replace('/', os.sep),
+                                           parent.replace('/', os.sep)))
 
     def assert_not_below(self, path, parent):
-        self.assert_(not util.is_path_below(path.replace('/', os.sep),
+        self.assertFalse(util.is_path_below(path.replace('/', os.sep),
                                             parent.replace('/', os.sep)))
 
     def test_is_path_below(self):
@@ -93,8 +98,8 @@
         self.assert_not_below('/svn/project2/sub/repos', '/svn/project1')
         self.assert_not_below('/svn/project1/../project2/repos',
                               '/svn/project1')
-        self.assert_(util.is_path_below('repos', os.path.join(os.getcwd())))
-        self.assert_(not util.is_path_below('../sub/repos',
+        self.assertTrue(util.is_path_below('repos', os.path.join(os.getcwd())))
+        self.assertFalse(util.is_path_below('../sub/repos',
                                             os.path.join(os.getcwd())))
 
 
@@ -168,19 +173,118 @@
                          "type(s) for +: 'int' and 'str')>", sr)
 
 
+class SetuptoolsUtilsTestCase(unittest.TestCase):
+
+    def test_get_module_path(self):
+        self.assertEqual(util.get_module_path(trac),
+                         util.get_module_path(util))
+
+    def test_get_pkginfo_trac(self):
+        pkginfo = util.get_pkginfo(trac)
+        self.assertEqual(trac.__version__, pkginfo.get('version'))
+        self.assertNotEqual({}, pkginfo)
+
+    def test_get_pkginfo_non_toplevel(self):
+        from trac import core
+        import tracopt
+        pkginfo = util.get_pkginfo(trac)
+        self.assertEqual(pkginfo, util.get_pkginfo(util))
+        self.assertEqual(pkginfo, util.get_pkginfo(core))
+        self.assertEqual(pkginfo, util.get_pkginfo(tracopt))
+
+    def test_get_pkginfo_genshi(self):
+        try:
+            import genshi
+            import genshi.core
+            dist = pkg_resources.get_distribution('Genshi')
+        except:
+            pass
+        else:
+            pkginfo = util.get_pkginfo(genshi)
+            self.assertNotEqual({}, pkginfo)
+            self.assertEqual(pkginfo, util.get_pkginfo(genshi.core))
+
+    def test_get_pkginfo_babel(self):
+        try:
+            import babel
+            import babel.core
+            dist = pkg_resources.get_distribution('Babel')
+        except:
+            pass
+        else:
+            pkginfo = util.get_pkginfo(babel)
+            self.assertNotEqual({}, pkginfo)
+            self.assertEqual(pkginfo, util.get_pkginfo(babel.core))
+
+    def test_get_pkginfo_mysqldb(self):
+        # MySQLdb's package name is "MySQL-Python"
+        try:
+            import MySQLdb
+            import MySQLdb.cursors
+            dist = pkg_resources.get_distribution('MySQL-Python')
+            dist.get_metadata('top_level.txt')
+        except:
+            pass
+        else:
+            pkginfo = util.get_pkginfo(MySQLdb)
+            self.assertNotEqual({}, pkginfo)
+            self.assertEqual(pkginfo, util.get_pkginfo(MySQLdb.cursors))
+
+    def test_get_pkginfo_psycopg2(self):
+        # python-psycopg2 deb package doesn't provide SOURCES.txt and
+        # top_level.txt
+        try:
+            import psycopg2
+            import psycopg2.extensions
+            dist = pkg_resources.get_distribution('psycopg2')
+        except:
+            pass
+        else:
+            pkginfo = util.get_pkginfo(psycopg2)
+            self.assertNotEqual({}, pkginfo)
+            self.assertEqual(pkginfo, util.get_pkginfo(psycopg2.extensions))
+
+
+class LazyClass(object):
+    @util.lazy
+    def f(self):
+        return object()
+
+
+class LazyTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.obj = LazyClass()
+
+    def test_lazy_get(self):
+        f = self.obj.f
+        self.assertTrue(self.obj.f is f)
+
+    def test_lazy_set(self):
+        self.obj.f = 2
+        self.assertEqual(2, self.obj.f)
+
+    def test_lazy_del(self):
+        f = self.obj.f
+        del self.obj.f
+        self.assertFalse(self.obj.f is f)
+
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(AtomicFileTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(PathTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(RandomTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ContentDispositionTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(SafeReprTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(AtomicFileTestCase))
+    suite.addTest(unittest.makeSuite(PathTestCase))
+    suite.addTest(unittest.makeSuite(RandomTestCase))
+    suite.addTest(unittest.makeSuite(ContentDispositionTestCase))
+    suite.addTest(unittest.makeSuite(SafeReprTestCase))
+    suite.addTest(unittest.makeSuite(SetuptoolsUtilsTestCase))
+    suite.addTest(unittest.makeSuite(LazyTestCase))
     suite.addTest(concurrency.suite())
     suite.addTest(datefmt.suite())
     suite.addTest(presentation.suite())
     suite.addTest(doctest.DocTestSuite(util))
     suite.addTest(text.suite())
+    suite.addTest(translation.suite())
     suite.addTest(html.suite())
     return suite
 
diff --git a/trac/trac/util/tests/concurrency.py b/trac/trac/util/tests/concurrency.py
index f047766..fdc3264 100644
--- a/trac/trac/util/tests/concurrency.py
+++ b/trac/trac/util/tests/concurrency.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2010-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -37,7 +37,7 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(ThreadLocalTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ThreadLocalTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/util/tests/datefmt.py b/trac/trac/util/tests/datefmt.py
index 3a4688c..3acead5 100644
--- a/trac/trac/util/tests/datefmt.py
+++ b/trac/trac/util/tests/datefmt.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2007-2009 Edgewall Software
+# Copyright (C) 2007-2013 Edgewall Software
 # Copyright (C) 2007 Matt Good <trac@matt-good.net>
 # All rights reserved.
 #
@@ -15,23 +15,21 @@
 # Author: Matt Good <trac@matt-good.net>
 
 import datetime
+import locale
 import os
 import time
 import unittest
 
+import trac.tests.compat
 from trac.core import TracError
-from trac.util import datefmt, translation
+from trac.util import datefmt
 
 try:
-    import pytz
-except ImportError:
-    pytz = None
-try:
     from babel import Locale
 except ImportError:
     Locale = None
 
-if pytz is None:
+if datefmt.pytz is None:
     PytzTestCase = None
 else:
     class PytzTestCase(unittest.TestCase):
@@ -99,19 +97,6 @@
             dt = datefmt.to_datetime(t, tz)
             self.assertEqual(datetime.timedelta(0, 7200), dt.utcoffset())
 
-        def test_parse_date_across_dst_boundary(self):
-            tz = datefmt.get_timezone('Europe/Zurich')
-            # DST start - 31 March, 02:00
-            format = '%Y-%m-%d %H:%M:%S %Z%z'
-            expected = '2002-03-31 03:30:00 CEST+0200'
-            # iso8601
-            t = datefmt.parse_date('2002-03-31T02:30:00', tz)
-            self.assertEqual(expected, t.strftime(format))
-            # strptime
-            t = datetime.datetime(2002, 3, 31, 2, 30)
-            t = datefmt.parse_date(t.strftime('%x %X'), tz)
-            self.assertEqual(expected, t.strftime(format))
-
         def test_to_datetime_astimezone(self):
             tz = datefmt.get_timezone('Europe/Paris')
             t = datetime.datetime(2012, 3, 25, 2, 15, tzinfo=datefmt.utc)
@@ -121,11 +106,11 @@
         def test_to_datetime_tz_from_naive_datetime_is_localtz(self):
             t = datetime.datetime(2012, 3, 25, 2, 15)
             dt = datefmt.to_datetime(t)
-            self.assert_(isinstance(dt.tzinfo, datefmt.LocalTimezone))
+            self.assertIsInstance(dt.tzinfo, datefmt.LocalTimezone)
 
         def test_to_datetime_tz_from_now_is_localtz(self):
             dt = datefmt.to_datetime(None)
-            self.assert_(isinstance(dt.tzinfo, datefmt.LocalTimezone))
+            self.assertIsInstance(dt.tzinfo, datefmt.LocalTimezone)
 
 
 class ParseISO8601TestCase(unittest.TestCase):
@@ -205,7 +190,7 @@
         t = datetime.datetime(2012, 10, 11, 2, 40, 57, 0, datefmt.localtz)
         dt = datefmt.parse_date('2012-10-11T02:40:57')
         self.assertEqual(t, dt)
-        self.assert_(isinstance(dt.tzinfo, datefmt.LocalTimezone))
+        self.assertIsInstance(dt.tzinfo, datefmt.LocalTimezone)
 
     def test_iso8601_naive_tz_used_tzinfo_arg(self):
         tz = datefmt.timezone('GMT +1:00')
@@ -221,7 +206,7 @@
         self.assertEqual(datetime.timedelta(hours=-9, minutes=-30),
                          dt.utcoffset())
 
-    if pytz:
+    if datefmt.pytz:
         def test_iso8601_naive_tz_normalize_non_existent_time(self):
             t = datetime.datetime(2012, 3, 25, 1, 15, 57, 0, datefmt.utc)
             tz = datefmt.timezone('Europe/Paris')
@@ -238,6 +223,128 @@
             self.assertEqual(2, dt.hour)
             self.assertEqual(datetime.timedelta(hours=1), dt.utcoffset())
 
+    def test_hint_iso8601(self):
+        def validate(locale=None):
+            try:
+                datefmt.parse_date('2001-0a-01', locale=locale, hint='iso8601')
+                raise self.failureException('TracError not raised')
+            except TracError, e:
+                self.assertIn(u'"YYYY-MM-DDThh:mm:ss±hh:mm"', unicode(e))
+
+        validate(locale=None)
+        validate(locale='iso8601')
+        if Locale:
+            validate(locale=Locale.parse('en_US'))
+
+
+class ParseDateWithoutBabelTestCase(unittest.TestCase):
+
+    if os.name != 'nt':
+        locales = {}
+    else:
+        # LCID: http://msdn.microsoft.com/en-us/goglobal/bb964664.aspx
+        # NLS: http://msdn.microsoft.com/en-us/goglobal/bb896001.aspx
+        ref_time = time.gmtime(123456)
+        locales = {
+            'en_US.UTF8': ('English_United States', '1/2/1970 10:17:36 AM'),
+            'en_GB.UTF8': ('English_United Kingdom', '02/01/1970 10:17:36'),
+            'fr_FR.UTF8': ('French_France', '02/01/1970 10:17:36'),
+            'ja_JP.UTF8': ('Japanese_Japan', '1970/01/02 10:17:36'),
+            'zh_CN.UTF8': ("Chinese_People's Republic of China",
+                           '1970/1/2 10:17:36')
+        }
+
+    def setUp(self):
+        rv = locale.getlocale(locale.LC_TIME)
+        self._orig_locale = rv if rv[0] else 'C'
+
+    def tearDown(self):
+        locale.setlocale(locale.LC_ALL, self._orig_locale)
+
+    def _setlocale(self, id):
+        try:
+            mapped, ref_strftime = self.locales.get(id, (id, None))
+            locale.setlocale(locale.LC_ALL, mapped)
+            return (ref_strftime is None or
+                    ref_strftime == time.strftime('%x %X', self.ref_time))
+        except locale.Error:
+            return False
+
+    def test_parse_date_libc(self):
+        tz = datefmt.timezone('GMT +2:00')
+        expected = datetime.datetime(2010, 8, 28, 13, 45, 56, 0, tz)
+        expected_minute = datetime.datetime(2010, 8, 28, 13, 45, 0, 0, tz)
+        expected_date = datetime.datetime(2010, 8, 28, 0, 0, 0, 0, tz)
+
+        self.assertTrue(self._setlocale('C'))
+        self.assertEqual(expected,
+                         datefmt.parse_date('08/28/10 13:45:56', tz))
+        self.assertEqual(expected_minute,
+                         datefmt.parse_date('08/28/10 13:45', tz))
+        self.assertEqual(expected_date, datefmt.parse_date('08/28/10', tz))
+        self.assertEqual(expected_minute,
+                         datefmt.parse_date('28 Aug 2010 1:45 pm', tz))
+
+        if self._setlocale('en_US.UTF8'):
+            self.assertEqual(expected,
+                             datefmt.parse_date('Aug 28, 2010 1:45:56 PM', tz))
+            self.assertEqual(expected,
+                             datefmt.parse_date('8 28, 2010 1:45:56 PM', tz))
+            self.assertEqual(expected,
+                             datefmt.parse_date('28 Aug 2010 1:45:56 PM', tz))
+            self.assertEqual(expected,
+                             datefmt.parse_date('28 Aug 2010 PM 1:45:56', tz))
+            self.assertEqual(expected,
+                             datefmt.parse_date('28 Aug 2010 13:45:56', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('28 Aug 2010 PM 1:45', tz))
+            self.assertEqual(expected_date,
+                             datefmt.parse_date('28 Aug 2010', tz))
+
+        if self._setlocale('en_GB.UTF8'):
+            self.assertEqual(expected,
+                             datefmt.parse_date('28 Aug 2010 13:45:56', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('28 Aug 2010 PM 1:45', tz))
+            self.assertEqual(expected_date,
+                             datefmt.parse_date('28 Aug 2010', tz))
+
+        if self._setlocale('fr_FR.UTF8'):
+            self.assertEqual(expected,
+                             datefmt.parse_date(u'28 août 2010 13:45:56', tz))
+            self.assertEqual(expected,
+                             datefmt.parse_date(u'août 28 2010 13:45:56', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date(u'août 28 2010 13:45', tz))
+            self.assertEqual(expected_date,
+                             datefmt.parse_date(u'août 28 2010', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('Aug 28 2010 1:45 pm', tz))
+
+        if self._setlocale('ja_JP.UTF8'):
+            self.assertEqual(expected,
+                             datefmt.parse_date('2010/08/28 13:45:56', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('2010/08/28 13:45', tz))
+            self.assertEqual(expected_date,
+                             datefmt.parse_date('2010/08/28', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('2010/Aug/28 1:45 pm', tz))
+
+        if self._setlocale('zh_CN.UTF8'):
+            self.assertEqual(expected,
+                             datefmt.parse_date(u'2010-8-28 下午01:45:56', tz))
+            self.assertEqual(expected,
+                             datefmt.parse_date(u'2010-8-28 01:45:56下午', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date(u'2010-8-28 下午01:45', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date(u'2010-8-28 01:45下午', tz))
+            self.assertEqual(expected_date,
+                             datefmt.parse_date('2010-8-28', tz))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('2010-Aug-28 01:45 pm', tz))
+
 
 class ParseRelativeDateTestCase(unittest.TestCase):
 
@@ -417,7 +524,7 @@
         self.assertEqual(datetime.datetime(2012, 3, 25, 3, 14, 59, tzinfo=tz),
                          datefmt._parse_relative_time('last second', tz, now))
 
-    if pytz:
+    if datefmt.pytz:
         def test_time_interval_across_dst(self):
             tz = datefmt.timezone('Europe/Paris')
             now = datefmt.to_datetime(datetime.datetime(2012, 3, 25, 3, 0, 41),
@@ -458,9 +565,9 @@
         datefmt.parse_date('2038-01-19T03:14:07Z')
         try:
             datefmt.parse_date('9999-12-31T23:59:59-12:00')
-            raise AssertionError('TracError not raised')
+            raise self.failureException('TracError not raised')
         except TracError, e:
-            self.assert_('is outside valid range' in unicode(e))
+            self.assertIn('is outside valid range', unicode(e))
 
     def test_min_timestamp(self):
         if os.name != 'nt':
@@ -472,9 +579,9 @@
             datefmt.parse_date('1970-01-01T00:00:00Z')
         try:
             datefmt.parse_date('0001-01-01T00:00:00+14:00')
-            raise AssertionError('TracError not raised')
+            raise self.failureException('TracError not raised')
         except TracError, e:
-            self.assert_('is outside valid range' in unicode(e))
+            self.assertIn('is outside valid range', unicode(e))
 
 
 class DateFormatTestCase(unittest.TestCase):
@@ -523,6 +630,10 @@
         self.assertEqual(datefmt.to_datetime(23L, tz), expected)
         self.assertEqual(datefmt.to_datetime(23.0, tz), expected)
 
+    def test_to_datetime_typeerror(self):
+        self.assertRaises(TypeError, datefmt.to_datetime, 'blah')
+        self.assertRaises(TypeError, datefmt.to_datetime, u'bl\xe1h')
+
     def test_format_datetime_utc(self):
         t = datetime.datetime(1970, 1, 1, 1, 0, 23, 0, datefmt.utc)
         expected = '1970-01-01T01:00:23Z'
@@ -559,6 +670,21 @@
         self.assertEqual(datefmt.format_time(t, 'iso8601', gmt01),
                          expected.split('T')[1])
 
+    def test_format_iso8601_before_1900(self):
+        t = datetime.datetime(1899, 12, 30, 23, 58, 59, 123456, datefmt.utc)
+        self.assertEqual('1899-12-30T23:58:59Z',
+                         datefmt.format_datetime(t, 'iso8601', datefmt.utc))
+        self.assertEqual('1899-12-30',
+                         datefmt.format_datetime(t, 'iso8601date',
+                                                 datefmt.utc))
+        self.assertEqual('1899-12-30',
+                         datefmt.format_date(t, 'iso8601', datefmt.utc))
+        self.assertEqual('23:58:59Z',
+                         datefmt.format_datetime(t, 'iso8601time',
+                                                 datefmt.utc))
+        self.assertEqual('23:58:59Z',
+                         datefmt.format_time(t, 'iso8601', datefmt.utc))
+
     def test_format_date_accepts_date_instances(self):
         a_date = datetime.date(2009, 8, 20)
         self.assertEqual('2009-08-20',
@@ -660,38 +786,68 @@
                          datefmt.format_time(t, 'medium', tz, 'iso8601'))
         self.assertEqual('2010-08-28T11:45:56',
                          datefmt.format_datetime(t, 'medium', tz, 'iso8601'))
-        for f in ('long', 'full'):
-            self.assertEqual('11:45:56+02:00',
-                             datefmt.format_time(t, f, tz, 'iso8601'))
-            self.assertEqual('2010-08-28T11:45:56+02:00',
-                             datefmt.format_datetime(t, f, tz, 'iso8601'))
+        self.assertEqual('11:45:56+02:00',
+                         datefmt.format_time(t, 'long', tz, 'iso8601'))
+        self.assertEqual('2010-08-28T11:45:56+02:00',
+                         datefmt.format_datetime(t, 'long', tz, 'iso8601'))
+        self.assertEqual('11:45:56.123456+02:00',
+                         datefmt.format_time(t, 'full', tz, 'iso8601'))
+        self.assertEqual('2010-08-28T11:45:56.123456+02:00',
+                         datefmt.format_datetime(t, 'full', tz, 'iso8601'))
+
+    def test_with_babel_format_before_1900(self):
+        tz = datefmt.timezone('GMT +2:00')
+        t = datetime.datetime(1899, 8, 28, 11, 45, 56, 123456, tz)
+        for f in ('short', 'medium', 'long', 'full'):
+            self.assertEqual('1899-08-28',
+                             datefmt.format_date(t, f, tz, 'iso8601'))
+        self.assertEqual('11:45',
+                         datefmt.format_time(t, 'short', tz, 'iso8601'))
+        self.assertEqual('1899-08-28T11:45',
+                         datefmt.format_datetime(t, 'short', tz, 'iso8601'))
+        self.assertEqual('11:45:56',
+                         datefmt.format_time(t, 'medium', tz, 'iso8601'))
+        self.assertEqual('1899-08-28T11:45:56',
+                         datefmt.format_datetime(t, 'medium', tz, 'iso8601'))
+        self.assertEqual('11:45:56+02:00',
+                         datefmt.format_time(t, 'long', tz, 'iso8601'))
+        self.assertEqual('1899-08-28T11:45:56+02:00',
+                         datefmt.format_datetime(t, 'long', tz, 'iso8601'))
+        self.assertEqual('11:45:56.123456+02:00',
+                         datefmt.format_time(t, 'full', tz, 'iso8601'))
+        self.assertEqual('1899-08-28T11:45:56.123456+02:00',
+                         datefmt.format_datetime(t, 'full', tz, 'iso8601'))
 
     def test_hint(self):
         try:
             datefmt.parse_date('***', locale='iso8601', hint='date')
+            raise self.failureException('TracError not raised')
         except TracError, e:
-            self.assert_('"YYYY-MM-DD"' in unicode(e))
+            self.assertIn('"YYYY-MM-DD"', unicode(e))
         try:
             datefmt.parse_date('***', locale='iso8601', hint='datetime')
+            raise self.failureException('TracError not raised')
         except TracError, e:
-            self.assert_(u'"YYYY-MM-DDThh:mm:ss±hh:mm"' in unicode(e))
+            self.assertIn(u'"YYYY-MM-DDThh:mm:ss±hh:mm"', unicode(e))
         try:
             datefmt.parse_date('***', locale='iso8601', hint='foobar')
+            raise self.failureException('TracError not raised')
         except TracError, e:
-            self.assert_('"foobar"' in unicode(e))
+            self.assertIn('"foobar"', unicode(e))
 
 
 if Locale is None:
     I18nDateFormatTestCase = None
 else:
     class I18nDateFormatTestCase(unittest.TestCase):
+
         def test_i18n_format_datetime(self):
             tz = datefmt.timezone('GMT +2:00')
             t = datetime.datetime(2010, 8, 28, 11, 45, 56, 123456, datefmt.utc)
             en_US = Locale.parse('en_US')
-            self.assertEqual('Aug 28, 2010 1:45:56 PM',
-                             datefmt.format_datetime(t, tzinfo=tz,
-                                                     locale=en_US))
+            self.assertIn(datefmt.format_datetime(t, tzinfo=tz, locale=en_US),
+                          ('Aug 28, 2010 1:45:56 PM',
+                           'Aug 28, 2010, 1:45:56 PM'))  # CLDR 23
             en_GB = Locale.parse('en_GB')
             self.assertEqual('28 Aug 2010 13:45:56',
                              datefmt.format_datetime(t, tzinfo=tz,
@@ -706,9 +862,9 @@
             self.assertEqual(u'13:45:56 28-08-2010',
                              datefmt.format_datetime(t, tzinfo=tz, locale=vi))
             zh_CN = Locale.parse('zh_CN')
-            self.assertEqual(u'2010-8-28 下午01:45:56',
-                             datefmt.format_datetime(t, tzinfo=tz,
-                                                     locale=zh_CN))
+            self.assertIn(datefmt.format_datetime(t, tzinfo=tz, locale=zh_CN),
+                          (u'2010-8-28 下午01:45:56',
+                           u'2010年8月28日 下午1:45:56'))
 
         def test_i18n_format_date(self):
             tz = datefmt.timezone('GMT +2:00')
@@ -729,8 +885,8 @@
             self.assertEqual(u'07-08-2010',
                              datefmt.format_date(t, tzinfo=tz, locale=vi))
             zh_CN = Locale.parse('zh_CN')
-            self.assertEqual(u'2010-8-7',
-                             datefmt.format_date(t, tzinfo=tz, locale=zh_CN))
+            self.assertIn(datefmt.format_date(t, tzinfo=tz, locale=zh_CN),
+                          (u'2010-8-7', u'2010年8月7日'))
 
         def test_i18n_format_time(self):
             tz = datefmt.timezone('GMT +2:00')
@@ -752,8 +908,8 @@
                              datefmt.format_time(t, tzinfo=tz, locale=ja))
             self.assertEqual('13:45:56',
                              datefmt.format_time(t, tzinfo=tz, locale=vi))
-            self.assertEqual(u'下午01:45:56',
-                             datefmt.format_time(t, tzinfo=tz, locale=zh_CN))
+            self.assertIn(datefmt.format_time(t, tzinfo=tz, locale=zh_CN),
+                          (u'下午01:45:56', u'下午1:45:56'))
 
         def test_i18n_datetime_hint(self):
             en_US = Locale.parse('en_US')
@@ -763,18 +919,19 @@
             vi = Locale.parse('vi')
             zh_CN = Locale.parse('zh_CN')
 
-            self.assert_(datefmt.get_datetime_format_hint(en_US)
-                         in ('MMM d, yyyy h:mm:ss a', 'MMM d, y h:mm:ss a'))
-            self.assert_(datefmt.get_datetime_format_hint(en_GB)
-                         in ('d MMM yyyy HH:mm:ss', 'd MMM y HH:mm:ss'))
-            self.assert_(datefmt.get_datetime_format_hint(fr)
-                         in ('d MMM yyyy HH:mm:ss', 'd MMM y HH:mm:ss'))
-            self.assertEqual('yyyy/MM/dd H:mm:ss',
-                             datefmt.get_datetime_format_hint(ja))
-            self.assertEqual('HH:mm:ss dd-MM-yyyy',
-                             datefmt.get_datetime_format_hint(vi))
-            self.assertEqual('yyyy-M-d ahh:mm:ss',
-                             datefmt.get_datetime_format_hint(zh_CN))
+            self.assertIn(datefmt.get_datetime_format_hint(en_US),
+                          ('MMM d, yyyy h:mm:ss a', 'MMM d, y h:mm:ss a',
+                           'MMM d, y, h:mm:ss a'))
+            self.assertIn(datefmt.get_datetime_format_hint(en_GB),
+                          ('d MMM yyyy HH:mm:ss', 'd MMM y HH:mm:ss'))
+            self.assertIn(datefmt.get_datetime_format_hint(fr),
+                          ('d MMM yyyy HH:mm:ss', 'd MMM y HH:mm:ss'))
+            self.assertIn(datefmt.get_datetime_format_hint(ja),
+                          ('yyyy/MM/dd H:mm:ss', 'y/MM/dd H:mm:ss'))
+            self.assertIn(datefmt.get_datetime_format_hint(vi),
+                          ('HH:mm:ss dd-MM-yyyy', 'HH:mm:ss dd-MM-y'))
+            self.assertIn(datefmt.get_datetime_format_hint(zh_CN),
+                          ('yyyy-M-d ahh:mm:ss', u'y年M月d日 ah:mm:ss'))
 
         def test_i18n_date_hint(self):
             en_US = Locale.parse('en_US')
@@ -784,18 +941,18 @@
             vi = Locale.parse('vi')
             zh_CN = Locale.parse('zh_CN')
 
-            self.assert_(datefmt.get_date_format_hint(en_US)
-                         in ('MMM d, yyyy', 'MMM d, y'))
-            self.assert_(datefmt.get_date_format_hint(en_GB)
-                         in ('d MMM yyyy', 'd MMM y'))
-            self.assert_(datefmt.get_date_format_hint(fr)
-                         in ('d MMM yyyy', 'd MMM y'))
-            self.assertEqual('yyyy/MM/dd',
-                             datefmt.get_date_format_hint(ja))
-            self.assertEqual('dd-MM-yyyy',
-                             datefmt.get_date_format_hint(vi))
-            self.assertEqual('yyyy-M-d',
-                             datefmt.get_date_format_hint(zh_CN))
+            self.assertIn(datefmt.get_date_format_hint(en_US),
+                          ('MMM d, yyyy', 'MMM d, y'))
+            self.assertIn(datefmt.get_date_format_hint(en_GB),
+                          ('d MMM yyyy', 'd MMM y'))
+            self.assertIn(datefmt.get_date_format_hint(fr),
+                          ('d MMM yyyy', 'd MMM y'))
+            self.assertIn(datefmt.get_date_format_hint(ja),
+                          ('yyyy/MM/dd', 'y/MM/dd'))
+            self.assertIn(datefmt.get_date_format_hint(vi),
+                          ('dd-MM-yyyy', 'dd-MM-y'))
+            self.assertIn(datefmt.get_date_format_hint(zh_CN),
+                          ('yyyy-M-d', u'y年M月d日'))
 
         def test_i18n_parse_date_iso8609(self):
             tz = datefmt.timezone('GMT +2:00')
@@ -864,16 +1021,22 @@
             self.assertEqual(expected_minute,
                              datefmt.parse_date(u'août 28 2010 13:45', tz,
                                                 fr))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('Aug 28 2010 1:45 PM', tz, fr))
 
             self.assertEqual(expected,
                              datefmt.parse_date('2010/08/28 13:45:56', tz, ja))
             self.assertEqual(expected_minute,
                              datefmt.parse_date('2010/08/28 13:45', tz, ja))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('2010/Aug/28 1:45 PM', tz, ja))
 
             self.assertEqual(expected,
                              datefmt.parse_date('13:45:56 28-08-2010', tz, vi))
             self.assertEqual(expected_minute,
                              datefmt.parse_date('13:45 28-08-2010', tz, vi))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('1:45PM 28-Aug-2010', tz, vi))
 
             self.assertEqual(expected,
                              datefmt.parse_date(u'2010-8-28 下午01:45:56',
@@ -887,6 +1050,9 @@
             self.assertEqual(expected_minute,
                              datefmt.parse_date(u'2010-8-28 01:45下午', tz,
                                                 zh_CN))
+            self.assertEqual(expected_minute,
+                             datefmt.parse_date('2010-Aug-28 01:45PM', tz,
+                                                zh_CN))
 
         def test_i18n_parse_date_datetime_meridiem(self):
             tz = datefmt.timezone('GMT +2:00')
@@ -945,20 +1111,27 @@
                              datefmt.parse_date(u'2010-8-28', tz, zh_CN))
 
         def test_i18n_parse_date_roundtrip(self):
+            from pkg_resources import resource_listdir
+            locales = sorted(dirname
+                             for dirname in resource_listdir('trac', 'locale')
+                             if '.' not in dirname)
+
             tz = datefmt.timezone('GMT +2:00')
             t = datetime.datetime(2010, 8, 28, 11, 45, 56, 123456, datefmt.utc)
-            expected = datetime.datetime(2010, 8, 28, 13, 45, 56, 0, tz)
+            tz_t = datetime.datetime(2010, 8, 28, 13, 45, 56, 0, tz)
 
-            for locale in translation.get_available_locales():
+            for locale in locales:
                 locale = Locale.parse(locale)
                 formatted = datefmt.format_datetime(t, tzinfo=tz,
                                                     locale=locale)
 
                 actual = datefmt.parse_date(formatted, tz, locale)
-                self.assertEqual(expected, actual,
-                                 '%r != %r (%r)' % (expected, actual, locale))
+                self.assertEqual(tz_t, actual,
+                                 '%r != %r (%r %r)' % (tz_t, actual, formatted,
+                                                       locale))
+                self.assertEqual(tz_t.isoformat(), actual.isoformat())
 
-                actual = datefmt.format_datetime(expected, tzinfo=tz,
+                actual = datefmt.format_datetime(tz_t, tzinfo=tz,
                                                  locale=locale)
                 self.assertEqual(formatted, actual,
                                  '%r != %r (%r)' % (formatted, actual, locale))
@@ -966,12 +1139,12 @@
         def test_format_compatibility(self):
             tz = datefmt.timezone('GMT +2:00')
             t = datetime.datetime(2010, 8, 28, 11, 45, 56, 123456, datefmt.utc)
-            tz_t = datetime.datetime(2010, 8, 28, 13, 45, 56, 123456, tz)
             en_US = Locale.parse('en_US')
 
             # Converting default format to babel's format
-            self.assertEqual('Aug 28, 2010 1:45:56 PM',
-                             datefmt.format_datetime(t, '%x %X', tz, en_US))
+            self.assertIn(datefmt.format_datetime(t, '%x %X', tz, en_US),
+                          ('Aug 28, 2010 1:45:56 PM',
+                           'Aug 28, 2010, 1:45:56 PM'))  # CLDR 23
             self.assertEqual('Aug 28, 2010',
                              datefmt.format_datetime(t, '%x', tz, en_US))
             self.assertEqual('1:45:56 PM',
@@ -1162,6 +1335,29 @@
         self.assertEqual('2011-10-30T02:45:42.123456+01:00',
                          dt.astimezone(datefmt.localtz).isoformat())
 
+    def test_astimezone_invalid_range_on_gmt01(self):
+        self._tzset('GMT-1')
+
+        # 1899-12-30T23:59:58+00:00 is -0x83ac4e92 for time_t, out of range
+        # for 32-bit signed integer
+        dt = datetime.datetime(1899, 12, 30, 23, 59, 58, 123456, datefmt.utc)
+        self.assertEqual('1899-12-31T00:59:58.123456+01:00',
+                         dt.astimezone(datefmt.localtz).isoformat())
+        dt = datetime.datetime(1899, 12, 30, 23, 59, 58, 123456,
+                               datefmt.localtz)
+        self.assertEqual('1899-12-30T22:59:58.123456+00:00',
+                         dt.astimezone(datefmt.utc).isoformat())
+
+        # 2040-12-31T23:59:58+00:00 is 0x858c84ee for time_t, out of range for
+        # 32-bit signed integer
+        dt = datetime.datetime(2040, 12, 31, 23, 59, 58, 123456, datefmt.utc)
+        self.assertEqual('2041-01-01T00:59:58.123456+01:00',
+                         dt.astimezone(datefmt.localtz).isoformat())
+        dt = datetime.datetime(2040, 12, 31, 23, 59, 58, 123456,
+                               datefmt.localtz)
+        self.assertEqual('2040-12-31T22:59:58.123456+00:00',
+                         dt.astimezone(datefmt.utc).isoformat())
+
     def test_arithmetic_localized_non_existent_time(self):
         self._tzset('Europe/Paris')
         t = datetime.datetime(2012, 3, 25, 1, 15, 42, 123456)
@@ -1231,7 +1427,6 @@
     def test_arithmetic_not_localized_normalized_non_existent_time(self):
         self._tzset('Europe/Paris')
         t = datetime.datetime(2012, 3, 25, 1, 15, 42, 123456, datefmt.localtz)
-        t_utc = t.replace(tzinfo=datefmt.utc)
         t1 = t
         self.assertEqual('2012-03-25T01:15:42.123456+01:00', t1.isoformat())
         t2 = datefmt.localtz.normalize(t1 + datetime.timedelta(hours=1))
@@ -1245,7 +1440,6 @@
     def test_arithmetic_not_localized_normalized_ambiguous_time(self):
         self._tzset('Europe/Paris')
         t = datetime.datetime(2011, 10, 30, 1, 45, 42, 123456, datefmt.localtz)
-        t_utc = t.replace(tzinfo=datefmt.utc)
         t1 = t
         self.assertEqual('2011-10-30T01:45:42.123456+02:00', t1.isoformat())
         t2 = datefmt.localtz.normalize(t1 + datetime.timedelta(hours=1))
@@ -1259,6 +1453,235 @@
         self.assertEqual(datetime.timedelta(hours=1), t3 - t2)
         self.assertEqual(datetime.timedelta(hours=1), t4 - t3)
 
+    def test_london_between_1968_and_1971(self):
+        self._tzset('Europe/London')
+        # -1:00 (DST end) at 1967-10-29 03:00
+        ts = datefmt.to_timestamp(datetime.datetime(1967, 10, 30,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('1967-10-30T00:00:00+00:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # +1:00 (DST start) at 1968-02-18 02:00
+        ts = datefmt.to_timestamp(datetime.datetime(1968, 2, 19,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('1968-02-19T01:00:00+01:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # No DST between 1968-02-18 02:00 and 1971-10-31 03:00
+        ts = datefmt.to_timestamp(datetime.datetime(1970, 1, 1, 0, 0, 23,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('1970-01-01T01:00:23+01:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # -1:00 (TZ change) at 1971-10-31 03:00
+        t = datefmt.to_datetime(datetime.datetime(1971, 10, 31, 1, 30),
+                                datefmt.localtz)
+        delta = datetime.timedelta(hours=1)
+        self.assertEqual('1971-10-31T01:30:00+01:00', t.isoformat())
+        t = datefmt.to_datetime(t + delta, datefmt.localtz)
+        self.assertEqual('1971-10-31T02:30:00+01:00', t.isoformat())
+        t = datefmt.to_datetime(t + delta, datefmt.localtz)
+        self.assertEqual('1971-10-31T02:30:00+00:00', t.isoformat())
+        t = datefmt.to_datetime(t + delta, datefmt.localtz)
+        self.assertEqual('1971-10-31T03:30:00+00:00', t.isoformat())
+
+        ts = datefmt.to_timestamp(datetime.datetime(1971, 11, 1,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('1971-11-01T00:00:00+00:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+
+    def test_guatemala_dst_in_2006(self):
+        self._tzset('America/Guatemala')
+        # No DST before 2006-04-30 00:00
+        ts = datefmt.to_timestamp(datetime.datetime(2006, 4, 29,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('2006-04-28T18:00:00-06:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # +1:00 (DST start) at 2006-04-30 00:00
+        ts = datefmt.to_timestamp(datetime.datetime(2006, 8, 1,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('2006-07-31T19:00:00-05:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # -1:00 (DST end) at 2006-10-01 00:00
+        ts = datefmt.to_timestamp(datetime.datetime(2006, 10, 2,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('2006-10-01T18:00:00-06:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # No DST after 2006-10-01 00:00
+
+    def test_venezuela_in_2007(self):
+        self._tzset('America/Caracas')
+        ts = datefmt.to_timestamp(datetime.datetime(2007, 12, 8,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('2007-12-07T20:00:00-04:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # -0:30 (TZ change) at 2007-12-09 03:00
+        ts = datefmt.to_timestamp(datetime.datetime(2007, 12, 10,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('2007-12-09T19:30:00-04:30',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+
+    def test_lord_howe_island_in_198x(self):
+        self._tzset('Australia/Lord_Howe')
+        ts = datefmt.to_timestamp(datetime.datetime(1985, 3, 1,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('1985-03-01T11:30:00+11:30',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        # -1:00 (DST end) at 1985-03-03 02:00
+        ts = datefmt.to_timestamp(datetime.datetime(1985, 8, 1,
+                                                    tzinfo=datefmt.utc))
+        self.assertEqual('1985-08-01T10:30:00+10:30',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+        ts = datefmt.to_timestamp(datetime.datetime(1985, 11, 1,
+                                                    tzinfo=datefmt.utc))
+        # +0:30 (DST start) at 1985-10-27 02:00
+        self.assertEqual('1985-11-01T11:00:00+11:00',
+                         datefmt.to_datetime(ts, datefmt.localtz).isoformat())
+
+    def _compare_pytz_arithmetic(self, tz, dt_naive):
+        """Compare arithmetic timezone-aware datetime between localtz and
+        pytz's timezone"""
+        localtz = datefmt.localtz
+        delta = datetime.timedelta(minutes=20)
+        n = datetime.timedelta(hours=3).seconds / delta.seconds
+        # create timezone-aware datetime instances
+        dt_localtz = datefmt.to_datetime(dt_naive - delta * n, localtz)
+        dt_tz = datefmt.to_datetime(dt_naive - delta * n, tz)
+        # compare datetime instances between -3 hours and +3 hours
+        for i in xrange(n * 2 + 1):
+            self.assertEqual(dt_tz, dt_localtz)
+            self.assertEqual(dt_tz.isoformat(), dt_localtz.isoformat())
+            dt_localtz = datefmt.to_datetime(dt_localtz + delta, localtz)
+            dt_tz = datefmt.to_datetime(dt_tz + delta, tz)
+
+    def _compare_pytz_localize_and_normalize(self, tz, dt_naive):
+        """Compare localize() and normalize() of LocalTimezone and pytz's
+        timezone"""
+        localtz = datefmt.localtz
+        delta = datetime.timedelta(minutes=20)
+        n = datetime.timedelta(hours=3).seconds / delta.seconds
+        dt_naive -= delta * n
+        # compare localize and normalize with naive datetime
+        # between -3 hours and +3 hours
+        for i in xrange(n * 2 + 1):
+            dt_localtz = localtz.localize(dt_naive)
+            dt_tz = tz.localize(dt_naive)
+            self.assertEqual(dt_tz, dt_localtz,
+                             '%r != %r (%r)' % (dt_tz, dt_localtz, dt_naive))
+            self.assertEqual(dt_tz.isoformat(), dt_localtz.isoformat(),
+                             '%r != %r (%r)' % (dt_tz.isoformat(),
+                                                dt_localtz.isoformat(),
+                                                dt_naive))
+            dt_localtz = localtz.normalize(localtz.localize(dt_naive))
+            dt_tz = tz.normalize(tz.localize(dt_naive))
+            self.assertEqual(dt_tz, dt_localtz,
+                             '%r != %r (%r)' % (dt_tz, dt_localtz, dt_naive))
+            self.assertEqual(dt_tz.isoformat(), dt_localtz.isoformat(),
+                             '%r != %r (%r)' % (dt_tz.isoformat(),
+                                                dt_localtz.isoformat(),
+                                                dt_naive))
+            dt_naive += delta
+
+    def _compare_pytz(self, tz, value, localize=True):
+        if isinstance(value, basestring):
+            value = datefmt.parse_date(value + 'Z', datefmt.utc)
+        dt_naive = value.replace(tzinfo=None)
+        self._compare_pytz_arithmetic(tz, dt_naive)
+        # `localize()` differs one of pytz's timezone when backward timezone
+        # change
+        if localize:
+            self._compare_pytz_localize_and_normalize(tz, dt_naive)
+
+    if datefmt.pytz:
+        def test_pytz_choibalsan(self):
+            tz = datefmt.timezone('Asia/Choibalsan')
+            self._tzset('Asia/Choibalsan')
+            self._compare_pytz(tz, '1977-01-01T00:00')  # No DST
+            self._compare_pytz(tz, '1978-01-01T01:00')  # +1:00 (TZ change)
+            self._compare_pytz(tz, '1978-01-01T02:00')  #       (TZ change)
+            self._compare_pytz(tz, '1982-04-01T00:00')  # No DST
+            self._compare_pytz(tz, '1983-04-01T00:00')  # +2:00 (TZ change)
+            self._compare_pytz(tz, '1983-04-01T02:00')  #       (TZ change)
+            self._compare_pytz(tz, '1983-10-01T00:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '2006-03-25T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '2006-09-30T02:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '2007-07-01T00:00')  # No DST in 2007
+            self._compare_pytz(tz, '2008-03-30T23:00',  #       (TZ change)
+                               localize=False)
+            self._compare_pytz(tz, '2008-03-31T00:00',  # -1:00 (TZ change)
+                               localize=False)
+            self._compare_pytz(tz, '2009-07-01T00:00')  # No DST
+
+        def test_pytz_guatemala(self):
+            tz = datefmt.timezone('America/Guatemala')
+            self._tzset('America/Guatemala')
+            self._compare_pytz(tz, '2005-07-01T00:00')  # No DST
+            self._compare_pytz(tz, '2006-04-30T00:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '2006-10-01T00:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '2007-07-01T00:00')  # No DST
+
+        def test_pytz_london(self):
+            tz = datefmt.timezone('Europe/London')
+            self._tzset('Europe/London')
+            self._compare_pytz(tz, '1968-02-18T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '1971-10-31T02:00',  #       (TZ change)
+                               localize=False)
+            self._compare_pytz(tz, '1971-10-31T03:00',  # -1:00 (TZ change)
+                               localize=False)
+            self._compare_pytz(tz, '1972-03-19T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '1972-10-29T03:00')  # -1:00 (DST end)
+
+        def test_pytz_lord_howe_island(self):
+            tz = datefmt.timezone('Australia/Lord_Howe')
+            self._tzset('Australia/Lord_Howe')
+            self._compare_pytz(tz, '1980-07-01T00:00')  # No DST
+            self._compare_pytz(tz, '1981-03-01T00:00')  # +0:30 (TZ change)
+            self._compare_pytz(tz, '1981-03-01T00:30')  #       (TZ change)
+            self._compare_pytz(tz, '1981-10-25T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '1985-03-03T02:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '1985-10-27T02:00')  # +0:30 (DST start)
+            self._compare_pytz(tz, '1986-03-16T02:00')  # -0:30 (DST end)
+
+        def test_pytz_moscow(self):
+            tz = datefmt.timezone('Europe/Moscow')
+            self._tzset('Europe/Moscow')
+            self._compare_pytz(tz, '1991-09-29T03:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '1992-01-19T02:00')  # +1:00 (TZ change)
+            self._compare_pytz(tz, '1992-01-19T03:00')  #       (TZ change)
+            self._compare_pytz(tz, '1992-03-28T23:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '1992-09-26T23:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '2010-03-28T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '2010-10-31T03:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '2011-03-27T02:00')  # +1:00 (TZ change)
+            self._compare_pytz(tz, '2011-03-27T03:00')  #       (TZ change)
+            self._compare_pytz(tz, '2011-10-31T03:00')  # No DST
+
+        def test_pytz_paris(self):
+            tz = datefmt.timezone('Europe/Paris')
+            self._tzset('Europe/Paris')
+            self._compare_pytz(tz, '1975-07-01T01:00')  # No DST
+            self._compare_pytz(tz, '1976-03-28T01:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '1976-09-26T01:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '2009-03-29T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '2009-10-25T03:00')  # -1:00 (DST end)
+
+        def test_pytz_tokyo(self):
+            tz = datefmt.timezone('Asia/Tokyo')
+            self._tzset('Asia/Tokyo')
+            self._compare_pytz(tz, '1947-07-01T02:00')  # No DST
+            self._compare_pytz(tz, '1948-05-02T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '1948-09-11T02:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '1949-04-03T02:00')  # +1:00 (DST start)
+            self._compare_pytz(tz, '1949-09-10T02:00')  # -1:00 (DST end)
+            self._compare_pytz(tz, '1950-07-01T02:00')  # No DST
+
+        def test_pytz_venezuela(self):
+            tz = datefmt.timezone('America/Caracas')
+            self._tzset('America/Caracas')
+            self._compare_pytz(tz, '2006-07-01T00:00')  # No DST
+            self._compare_pytz(tz, '2007-12-09T02:30',  #       (TZ change)
+                               localize=False)
+            self._compare_pytz(tz, '2007-12-09T03:00',  # -0:30 (TZ change)
+                               localize=False)
+            self._compare_pytz(tz, '2008-07-01T00:00')  # No DST
+
 
 class LocalTimezoneStrTestCase(unittest.TestCase):
 
@@ -1282,17 +1705,18 @@
 def suite():
     suite = unittest.TestSuite()
     if PytzTestCase:
-        suite.addTest(unittest.makeSuite(PytzTestCase, 'test'))
+        suite.addTest(unittest.makeSuite(PytzTestCase))
     else:
         print "SKIP: utils/tests/datefmt.py (no pytz installed)"
     suite.addTest(unittest.makeSuite(DateFormatTestCase))
     suite.addTest(unittest.makeSuite(UTimestampTestCase))
     suite.addTest(unittest.makeSuite(ISO8601TestCase))
     if I18nDateFormatTestCase:
-        suite.addTest(unittest.makeSuite(I18nDateFormatTestCase, 'test'))
+        suite.addTest(unittest.makeSuite(I18nDateFormatTestCase))
     else:
         print "SKIP: utils/tests/datefmt.py (no babel installed)"
     suite.addTest(unittest.makeSuite(ParseISO8601TestCase))
+    suite.addTest(unittest.makeSuite(ParseDateWithoutBabelTestCase))
     suite.addTest(unittest.makeSuite(ParseRelativeDateTestCase))
     suite.addTest(unittest.makeSuite(ParseDateValidRangeTestCase))
     suite.addTest(unittest.makeSuite(HttpDateTestCase))
diff --git a/trac/trac/util/tests/html.py b/trac/trac/util/tests/html.py
index cfb5fac..a3f650e 100644
--- a/trac/trac/util/tests/html.py
+++ b/trac/trac/util/tests/html.py
@@ -1,9 +1,24 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-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 unittest
+from genshi.builder import Element, Fragment, tag
 from genshi.input import HTML
 
-from trac.util.html import TracHTMLSanitizer
+import trac.tests.compat
+from trac.core import TracError
+from trac.util.html import TracHTMLSanitizer, find_element, to_fragment
+from trac.util.translation import gettext, tgettext
 
 
 class TracHTMLSanitizerTestCase(unittest.TestCase):
@@ -141,9 +156,95 @@
         self.assertEqual('<div>XSS</div>', unicode(html | TracHTMLSanitizer()))
 
 
+class FindElementTestCase(unittest.TestCase):
+    def test_find_element_with_tag(self):
+        frag = tag(tag.p('Paragraph with a ',
+                   tag.a('link', href='http://www.edgewall.org'),
+                   ' and some ', tag.strong('strong text')))
+        self.assertIsNotNone(find_element(frag, tag='p'))
+        self.assertIsNotNone(find_element(frag, tag='a'))
+        self.assertIsNotNone(find_element(frag, tag='strong'))
+        self.assertIsNone(find_element(frag, tag='input'))
+        self.assertIsNone(find_element(frag, tag='textarea'))
+
+
+class ToFragmentTestCase(unittest.TestCase):
+
+    def test_unicode(self):
+        rv = to_fragment('blah')
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('blah', unicode(rv))
+
+    def test_fragment(self):
+        rv = to_fragment(tag('blah'))
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('blah', unicode(rv))
+
+    def test_element(self):
+        rv = to_fragment(tag.p('blah'))
+        self.assertEqual(Element, type(rv))
+        self.assertEqual('<p>blah</p>', unicode(rv))
+
+    def test_tracerror(self):
+        rv = to_fragment(TracError('blah'))
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('blah', unicode(rv))
+
+    def test_tracerror_with_fragment(self):
+        message = tag('Powered by ',
+                      tag.a('Trac', href='http://trac.edgewall.org/'))
+        rv = to_fragment(TracError(message))
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('Powered by <a href="http://trac.edgewall.org/">Trac'
+                         '</a>', unicode(rv))
+
+    def test_tracerror_with_element(self):
+        message = tag.p('Powered by ',
+                        tag.a('Trac', href='http://trac.edgewall.org/'))
+        rv = to_fragment(TracError(message))
+        self.assertEqual(Element, type(rv))
+        self.assertEqual('<p>Powered by <a href="http://trac.edgewall.org/">'
+                         'Trac</a></p>', unicode(rv))
+
+    def test_error(self):
+        rv = to_fragment(ValueError('invalid literal for int(): blah'))
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('invalid literal for int(): blah', unicode(rv))
+
+    def test_gettext(self):
+        rv = to_fragment(gettext('%(size)s bytes', size=0))
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('0 bytes', unicode(rv))
+
+    def test_tgettext(self):
+        rv = to_fragment(tgettext('Back to %(parent)s',
+                                  parent=tag.a('WikiStart',
+                                               href='http://localhost/')))
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('Back to <a href="http://localhost/">WikiStart</a>',
+                         unicode(rv))
+
+    def test_tracerror_with_gettext(self):
+        e = TracError(gettext('%(size)s bytes', size=0))
+        rv = to_fragment(e)
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('0 bytes', unicode(rv))
+
+    def test_tracerror_with_tgettext(self):
+        e = TracError(tgettext('Back to %(parent)s',
+                               parent=tag.a('WikiStart',
+                                            href='http://localhost/')))
+        rv = to_fragment(e)
+        self.assertEqual(Fragment, type(rv))
+        self.assertEqual('Back to <a href="http://localhost/">WikiStart</a>',
+                         unicode(rv))
+
+
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(TracHTMLSanitizerTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(TracHTMLSanitizerTestCase))
+    suite.addTest(unittest.makeSuite(FindElementTestCase))
+    suite.addTest(unittest.makeSuite(ToFragmentTestCase))
     return suite
 
 
diff --git a/trac/trac/util/tests/presentation.py b/trac/trac/util/tests/presentation.py
index d1ff873..13b864a 100644
--- a/trac/trac/util/tests/presentation.py
+++ b/trac/trac/util/tests/presentation.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2006-2009 Edgewall Software
+# Copyright (C) 2006-2013 Edgewall Software
 # Copyright (C) 2006 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
 #
@@ -32,16 +32,19 @@
                          presentation.to_json("a ' single quote"))
         self.assertEqual(r'"\u003cb\u003e\u0026\u003c/b\u003e"',
                          presentation.to_json('<b>&</b>'))
+        self.assertEqual(r'"\n\r\u2028\u2029"',
+                         presentation.to_json(u'\x0a\x0d\u2028\u2029'))
 
     def test_compound_types(self):
         self.assertEqual('[1,2,[true,false]]',
                          presentation.to_json([1, 2, [True, False]]))
         self.assertEqual(r'{"one":1,"other":[null,0],'
                          r'''"three":[3,"\u0026\u003c\u003e'"],'''
-                         r'"two":2}',
+                         r'"two":2,"\u2028\n":"\u2029\r"}',
                          presentation.to_json({"one": 1, "two": 2,
                                                "other": [None, 0],
-                                               "three": [3, "&<>'"]}))
+                                               "three": [3, "&<>'"],
+                                               u"\u2028\x0a": u"\u2029\x0d"}))
 
 
 def suite():
diff --git a/trac/trac/util/tests/text.py b/trac/trac/util/tests/text.py
index eda9179..fb24c92 100644
--- a/trac/trac/util/tests/text.py
+++ b/trac/trac/util/tests/text.py
@@ -1,31 +1,46 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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 os
+import socket
 import unittest
 from StringIO import StringIO
 
+import trac.tests.compat
 from trac.util.text import empty, expandtabs, fix_eol, javascript_quote, \
-                           to_js_string, normalize_whitespace, to_unicode, \
-                           text_width, print_table, unicode_quote, \
-                           unicode_quote_plus, unicode_unquote, \
-                           unicode_urlencode, wrap, quote_query_string, \
-                           unicode_to_base64, unicode_from_base64, \
-                           strip_line_ws, stripws, levenshtein_distance
+                           levenshtein_distance, normalize_whitespace, \
+                           print_table, quote_query_string, shorten_line, \
+                           strip_line_ws, stripws, text_width, \
+                           to_js_string, to_unicode, unicode_from_base64, \
+                           unicode_quote, unicode_quote_plus, \
+                           unicode_to_base64, unicode_unquote, \
+                           unicode_urlencode, wrap
 
 
 class ToUnicodeTestCase(unittest.TestCase):
     def test_explicit_charset(self):
         uc = to_unicode('\xc3\xa7', 'utf-8')
-        assert isinstance(uc, unicode)
+        self.assertIsInstance(uc, unicode)
         self.assertEquals(u'\xe7', uc)
 
     def test_explicit_charset_with_replace(self):
         uc = to_unicode('\xc3', 'utf-8')
-        assert isinstance(uc, unicode)
+        self.assertIsInstance(uc, unicode)
         self.assertEquals(u'\xc3', uc)
 
     def test_implicit_charset(self):
         uc = to_unicode('\xc3\xa7')
-        assert isinstance(uc, unicode)
+        self.assertIsInstance(uc, unicode)
         self.assertEquals(u'\xe7', uc)
 
     def test_from_exception_using_unicode_args(self):
@@ -33,26 +48,52 @@
         try:
             raise ValueError, '%s is not a number.' % u
         except ValueError, e:
-            self.assertEquals(u'\uB144 is not a number.', to_unicode(e))
+            self.assertEqual(u'\uB144 is not a number.', to_unicode(e))
 
     def test_from_exception_using_str_args(self):
         u = u'Das Ger\xe4t oder die Ressource ist belegt'
         try:
             raise ValueError, u.encode('utf-8')
         except ValueError, e:
-            self.assertEquals(u, to_unicode(e))
+            self.assertEqual(u, to_unicode(e))
+
+    def test_from_windows_error(self):
+        try:
+            os.stat('non/existent/file.txt')
+        except OSError, e:
+            uc = to_unicode(e)
+            self.assertIsInstance(uc, unicode, uc)
+            self.assertTrue(uc.startswith('[Error '), uc)
+            self.assertIn(e.strerror.decode('mbcs'), uc)
+
+    def test_from_socket_error(self):
+        for res in socket.getaddrinfo('127.0.0.1', 65536, 0,
+                                      socket.SOCK_STREAM):
+            af, socktype, proto, canonname, sa = res
+            s = socket.socket(af, socktype, proto)
+            try:
+                s.connect(sa)
+            except socket.error, e:
+                uc = to_unicode(e)
+                self.assertIsInstance(uc, unicode, uc)
+                if hasattr(e, 'strerror'):
+                    self.assertIn(e.strerror.decode('mbcs'), uc)
+
+    if os.name != 'nt':
+        del test_from_windows_error
+        del test_from_socket_error
 
 
 class ExpandtabsTestCase(unittest.TestCase):
     def test_empty(self):
         x = expandtabs('', ignoring='\0')
-        self.assertEquals('', x)
+        self.assertEqual('', x)
     def test_ingoring(self):
         x = expandtabs('\0\t', ignoring='\0')
-        self.assertEquals('\0        ', x)
+        self.assertEqual('\0        ', x)
     def test_tabstops(self):
-        self.assertEquals('        ', expandtabs('       \t'))
-        self.assertEquals('                ', expandtabs('\t\t'))
+        self.assertEqual('        ', expandtabs('       \t'))
+        self.assertEqual('                ', expandtabs('\t\t'))
 
 
 class JavascriptQuoteTestCase(unittest.TestCase):
@@ -65,6 +106,8 @@
                          javascript_quote('\x02\x1e'))
         self.assertEqual(r'\u0026\u003c\u003e',
                          javascript_quote('&<>'))
+        self.assertEqual(r'\u2028\u2029',
+                         javascript_quote(u'\u2028\u2029'))
 
 
 class ToJsStringTestCase(unittest.TestCase):
@@ -81,6 +124,8 @@
                          to_js_string(''))
         self.assertEqual('""',
                          to_js_string(None))
+        self.assertEqual(r'"\u2028\u2029"',
+                         to_js_string(u'\u2028\u2029'))
 
 
 class UnicodeQuoteTestCase(unittest.TestCase):
@@ -310,17 +355,17 @@
 
 class StripwsTestCase(unittest.TestCase):
     def test_stripws(self):
-        self.assertEquals(u'stripws',
-                          stripws(u' \u200b\t\u3000stripws \u200b\t\u2008'))
-        self.assertEquals(u'stripws \u3000\t',
-                          stripws(u'\u200b\t\u2008 stripws \u3000\t',
-                                  trailing=False))
-        self.assertEquals(u' \t\u3000stripws',
-                          stripws(u' \t\u3000stripws \u200b\t\u2008',
-                                  leading=False))
-        self.assertEquals(u' \t\u3000stripws \u200b\t\u2008',
-                          stripws(u' \t\u3000stripws \u200b\t\u2008',
-                                  leading=False, trailing=False))
+        self.assertEqual(u'stripws',
+                         stripws(u' \u200b\t\u3000stripws \u200b\t\u2008'))
+        self.assertEqual(u'stripws \u3000\t',
+                         stripws(u'\u200b\t\u2008 stripws \u3000\t',
+                                 trailing=False))
+        self.assertEqual(u' \t\u3000stripws',
+                         stripws(u' \t\u3000stripws \u200b\t\u2008',
+                                 leading=False))
+        self.assertEqual(u' \t\u3000stripws \u200b\t\u2008',
+                         stripws(u' \t\u3000stripws \u200b\t\u2008',
+                                 leading=False, trailing=False))
 
 
 
@@ -333,22 +378,41 @@
         self.assertEqual(0, levenshtein_distance('milestone', 'milestone'))
 
 
+class ShortenLineTestCase(unittest.TestCase):
+
+    def test_less_than_maxlen(self):
+        text = '123456789'
+        self.assertEqual(text, shorten_line(text, 10))
+
+    def test_equalto_maxlen(self):
+        text = '1234567890'
+        self.assertEqual(text, shorten_line(text, 10))
+
+    def test_greater_than_maxlen(self):
+        text = 'word word word word'
+        self.assertEqual('word word ...', shorten_line(text, 15))
+        text = 'abcdefghij'
+        self.assertEqual('abcde ...', shorten_line(text, 9))
+
+
+
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(ToUnicodeTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ExpandtabsTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(UnicodeQuoteTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(JavascriptQuoteTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ToJsStringTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(QuoteQueryStringTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(WhitespaceTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(TextWidthTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(PrintTableTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(WrapTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(FixEolTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(UnicodeBase64TestCase, 'test'))
-    suite.addTest(unittest.makeSuite(StripwsTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(LevenshteinDistanceTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ToUnicodeTestCase))
+    suite.addTest(unittest.makeSuite(ExpandtabsTestCase))
+    suite.addTest(unittest.makeSuite(UnicodeQuoteTestCase))
+    suite.addTest(unittest.makeSuite(JavascriptQuoteTestCase))
+    suite.addTest(unittest.makeSuite(ToJsStringTestCase))
+    suite.addTest(unittest.makeSuite(QuoteQueryStringTestCase))
+    suite.addTest(unittest.makeSuite(WhitespaceTestCase))
+    suite.addTest(unittest.makeSuite(TextWidthTestCase))
+    suite.addTest(unittest.makeSuite(PrintTableTestCase))
+    suite.addTest(unittest.makeSuite(WrapTestCase))
+    suite.addTest(unittest.makeSuite(FixEolTestCase))
+    suite.addTest(unittest.makeSuite(UnicodeBase64TestCase))
+    suite.addTest(unittest.makeSuite(StripwsTestCase))
+    suite.addTest(unittest.makeSuite(LevenshteinDistanceTestCase))
+    suite.addTest(unittest.makeSuite(ShortenLineTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/util/tests/translation.py b/trac/trac/util/tests/translation.py
new file mode 100644
index 0000000..517c952
--- /dev/null
+++ b/trac/trac/util/tests/translation.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 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 shutil
+import tempfile
+import unittest
+from pkg_resources import resource_exists, resource_filename
+try:
+    import babel
+except ImportError:
+    babel = None
+    locale_identifiers = lambda: ()
+else:
+    try:
+        from babel.localedata import locale_identifiers
+    except ImportError:
+        from babel.localedata import list as locale_identifiers
+
+from trac.test import EnvironmentStub
+from trac.util import translation
+
+
+class TranslationsProxyTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
+
+    def tearDown(self):
+        translation.deactivate()
+        self.env.reset_db()
+        shutil.rmtree(self.env.path)
+
+    def _get_locale_dir(self):
+        return resource_filename('trac', 'locale')
+
+    def _get_available_locales(self):
+        return sorted(locale
+                      for locale in translation.get_available_locales()
+                      if resource_exists('trac',
+                                         'locale/%s/LC_MESSAGES/messages.mo'
+                                         % locale))
+
+    def test_activate(self):
+        locales = self._get_available_locales()
+        if locales:
+            translation.activate(locales[0], self.env.path)
+
+    def test_activate_unavailable_locale(self):
+        unavailables = sorted(set(locale_identifiers()) -
+                              set(translation.get_available_locales())) or \
+                       ('en_US',)
+        locale_dir = self._get_locale_dir()
+        translation.add_domain('catalog1', self.env.path, locale_dir)
+        translation.add_domain('catalog2', self.env.path, locale_dir)
+        translation.activate(unavailables[0], self.env.path)
+
+    def test_activate_with_non_existent_catalogs(self):
+        locales = self._get_available_locales()
+        if locales:
+            locale_dir = self._get_locale_dir()
+            translation.add_domain('catalog1', self.env.path, locale_dir)
+            translation.add_domain('catalog2', self.env.path, locale_dir)
+            translation.activate(locales[0], self.env.path)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TranslationsProxyTestCase))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/util/text.py b/trac/trac/util/text.py
index 4903e5c..269b488 100644
--- a/trac/trac/util/text.py
+++ b/trac/trac/util/text.py
@@ -61,6 +61,13 @@
         except UnicodeDecodeError:
             return unicode(text, 'latin1')
     elif isinstance(text, Exception):
+        if os.name == 'nt' and isinstance(text, (OSError, IOError)):
+            # the exception might have a localized error string encoded with
+            # ANSI codepage if OSError and IOError on Windows
+            try:
+                return unicode(str(text), 'mbcs')
+            except UnicodeError:
+                pass
         # two possibilities for storing unicode strings in exception data:
         try:
             # custom __str__ method on the exception (e.g. PermissionError)
@@ -131,10 +138,10 @@
 
 _js_quote = {'\\': '\\\\', '"': '\\"', '\b': '\\b', '\f': '\\f',
              '\n': '\\n', '\r': '\\r', '\t': '\\t', "'": "\\'"}
-for i in range(0x20) + [ord(c) for c in '&<>']:
-    _js_quote.setdefault(chr(i), '\\u%04x' % i)
-_js_quote_re = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t\'&<>]')
-_js_string_re = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t&<>]')
+for i in range(0x20) + [ord(c) for c in u'&<>\u2028\u2029']:
+    _js_quote.setdefault(unichr(i), '\\u%04x' % i)
+_js_quote_re = re.compile(ur'[\x00-\x1f\\"\b\f\n\r\t\'&<>\u2028\u2029]')
+_js_string_re = re.compile(ur'[\x00-\x1f\\"\b\f\n\r\t&<>\u2028\u2029]')
 
 
 def javascript_quote(text):
@@ -221,24 +228,27 @@
 
 
 def to_utf8(text, charset='latin1'):
-    """Convert a string to UTF-8, assuming the encoding is either UTF-8, ISO
-    Latin-1, or as specified by the optional `charset` parameter.
+    """Convert a string to an UTF-8 `str` object.
 
-    .. deprecated :: 0.10
-       You should use `unicode` strings only.
+    If the input is not an `unicode` object, we assume the encoding is
+    already UTF-8, ISO Latin-1, or as specified by the optional
+    *charset* parameter.
     """
-    try:
-        # Do nothing if it's already utf-8
-        u = unicode(text, 'utf-8')
-        return text
-    except UnicodeError:
+    if isinstance(text, unicode):
+        u = text
+    else:
         try:
-            # Use the user supplied charset if possible
-            u = unicode(text, charset)
+            # Do nothing if it's already utf-8
+            u = unicode(text, 'utf-8')
+            return text
         except UnicodeError:
-            # This should always work
-            u = unicode(text, 'latin1')
-        return u.encode('utf-8')
+            try:
+                # Use the user supplied charset if possible
+                u = unicode(text, charset)
+            except UnicodeError:
+                # This should always work
+                u = unicode(text, 'latin1')
+    return u.encode('utf-8')
 
 
 class unicode_passwd(unicode):
@@ -402,17 +412,19 @@
 
 
 def shorten_line(text, maxlen=75):
-    """Truncates content to at most `maxlen` characters.
+    """Truncates `text` to length less than or equal to `maxlen` characters.
 
     This tries to be (a bit) clever and attempts to find a proper word
     boundary for doing so.
     """
-    if len(text or '') < maxlen:
+    if len(text or '') <= maxlen:
         return text
-    cut = max(text.rfind(' ', 0, maxlen), text.rfind('\n', 0, maxlen))
+    suffix = ' ...'
+    maxtextlen = maxlen - len(suffix)
+    cut = max(text.rfind(' ', 0, maxtextlen), text.rfind('\n', 0, maxtextlen))
     if cut < 0:
-        cut = maxlen
-    return text[:cut] + ' ...'
+        cut = maxtextlen
+    return text[:cut] + suffix
 
 
 class UnicodeTextWrapper(textwrap.TextWrapper):
diff --git a/trac/trac/util/translation.py b/trac/trac/util/translation.py
index 0868b38..bb49e25 100644
--- a/trac/trac/util/translation.py
+++ b/trac/trac/util/translation.py
@@ -146,17 +146,18 @@
                 self._activate_failed = True
                 return
             t = Translations.load(locale_dir, locale or 'en_US')
-            if not t or t.__class__ is NullTranslations:
+            if not isinstance(t, Translations):
                 t = self._null_translations
             else:
-                t.add(Translations.load(locale_dir, locale or 'en_US',
-                                        'tracini'))
+                self._add(t, Translations.load(locale_dir, locale or 'en_US',
+                                               'tracini'))
                 if env_path:
                     with self._plugin_domains_lock:
                         domains = self._plugin_domains.get(env_path, {})
                         domains = domains.items()
                     for domain, dirname in domains:
-                        t.add(Translations.load(dirname, locale, domain))
+                        self._add(t, Translations.load(dirname, locale,
+                                                       domain))
             self._current.translations = t
             self._activate_failed = False
 
@@ -184,6 +185,12 @@
             return self._current.translations is not None \
                    or self._activate_failed
 
+        # Internal methods
+
+        def _add(self, t, translations):
+            if isinstance(translations, Translations):
+                t.add(translations)
+
         # Delegated methods
 
         def __getattr__(self, name):
@@ -335,21 +342,37 @@
         translations are available.
         """
         try:
-            return [dirname for dirname
-                    in pkg_resources.resource_listdir('trac', 'locale')
-                    if '.' not in dirname]
+            locales = [dirname for dirname
+                       in pkg_resources.resource_listdir('trac', 'locale')
+                       if '.' not in dirname
+                       and pkg_resources.resource_exists(
+                        'trac', 'locale/%s/LC_MESSAGES/messages.mo' % dirname)]
+            return locales
         except Exception:
             return []
 
     def get_negotiated_locale(preferred_locales):
         def normalize(locale_ids):
             return [id.replace('-', '_') for id in locale_ids if id]
-        return Locale.negotiate(normalize(preferred_locales),
-                                normalize(get_available_locales()))
+        available_locales = get_available_locales()
+        if 'en_US' not in available_locales:
+            available_locales.append('en_US')
+        locale = Locale.negotiate(normalize(preferred_locales),
+                                  normalize(available_locales))
+        if locale and str(locale) not in available_locales:
+            # The list of get_available_locales() must include locale
+            # identifier from str(locale), but zh_* don't be included after
+            # Babel 1.0. Avoid expanding zh_* to zh_Hans_CN and zh_Hant_TW
+            # to clear "script" property of Locale instance. See #11258.
+            locale._data  # load localedata before clear script property
+            locale.script = None
+            assert str(locale) in available_locales
+        return locale
 
     has_babel = True
 
 except ImportError: # fall back on 0.11 behavior, i18n functions are no-ops
+    Locale = None
     gettext = _ = gettext_noop
     dgettext = dgettext_noop
     ngettext = ngettext_noop
diff --git a/trac/trac/versioncontrol/__init__.py b/trac/trac/versioncontrol/__init__.py
index a42873e..4411a5d 100644
--- a/trac/trac/versioncontrol/__init__.py
+++ b/trac/trac/versioncontrol/__init__.py
@@ -1 +1,14 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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 trac.versioncontrol.api import *
diff --git a/trac/trac/versioncontrol/admin.py b/trac/trac/versioncontrol/admin.py
index a001239..a3eb311 100644
--- a/trac/trac/versioncontrol/admin.py
+++ b/trac/trac/versioncontrol/admin.py
@@ -176,13 +176,11 @@
     # IAdminPanelProvider methods
 
     def get_admin_panels(self, req):
-        if 'VERSIONCONTROL_ADMIN' in req.perm:
+        if 'VERSIONCONTROL_ADMIN' in req.perm('admin', 'versioncontrol/repository'):
             yield ('versioncontrol', _('Version Control'), 'repository',
                    _('Repositories'))
 
     def render_admin_panel(self, req, category, page, path_info):
-        req.perm.require('VERSIONCONTROL_ADMIN')
-
         # Retrieve info for all repositories
         rm = RepositoryManager(self.env)
         all_repos = rm.get_all_repositories()
@@ -202,39 +200,42 @@
                 elif db_provider and req.args.get('save'):
                     # Modify repository
                     changes = {}
+                    valid = True
                     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 'dir' in changes \
-                            and not self._check_dir(req, changes['dir']):
-                        changes = {}
-                    if changes:
+                    if 'dir' in changes and not \
+                            self._check_dir(req, changes['dir']):
+                        valid = False
+                    if valid and changes:
                         db_provider.modify_repository(reponame, changes)
                         add_notice(req, _('Your changes have been saved.'))
-                    name = req.args.get('name')
-                    resync = tag.tt('trac-admin $ENV repository resync "%s"'
-                                    % (name or '(default)'))
-                    if 'dir' in changes:
-                        msg = tag_('You should now run %(resync)s to '
-                                   'synchronize Trac with the repository.',
-                                   resync=resync)
-                        add_notice(req, msg)
-                    elif 'type' in changes:
-                        msg = tag_('You may have to run %(resync)s to '
-                                   'synchronize Trac with the repository.',
-                                   resync=resync)
-                        add_notice(req, msg)
-                    if name and name != path_info and not 'alias' in info:
-                        cset_added = tag.tt('trac-admin $ENV changeset '
-                                            'added "%s" $REV'
-                                            % (name or '(default)'))
-                        msg = tag_('You will need to update your post-commit '
-                                   'hook to call %(cset_added)s with the new '
-                                   'repository name.', cset_added=cset_added)
-                        add_notice(req, msg)
-                    if changes:
+                        name = req.args.get('name')
+                        resync = tag.tt('trac-admin $ENV repository resync '
+                                        '"%s"' % (name or '(default)'))
+                        if 'dir' in changes:
+                            msg = tag_('You should now run %(resync)s to '
+                                       'synchronize Trac with the repository.',
+                                       resync=resync)
+                            add_notice(req, msg)
+                        elif 'type' in changes:
+                            msg = tag_('You may have to run %(resync)s to '
+                                       'synchronize Trac with the repository.',
+                                       resync=resync)
+                            add_notice(req, msg)
+                        if name and name != path_info and not 'alias' in info:
+                            cset_added = tag.tt('trac-admin $ENV changeset '
+                                                'added "%s" $REV'
+                                                % (name or '(default)'))
+                            msg = tag_('You will need to update your '
+                                       'post-commit hook to call '
+                                       '%(cset_added)s with the new '
+                                       'repository name.',
+                                       cset_added=cset_added)
+                            add_notice(req, msg)
+                    if valid:
                         req.redirect(req.href.admin(category, page))
 
             Chrome(self.env).add_wiki_toolbars(req)
@@ -253,7 +254,12 @@
                         add_warning(req, _('Missing arguments to add a '
                                            'repository.'))
                     elif self._check_dir(req, dir):
-                        db_provider.add_repository(name, dir, type_)
+                        try:
+                            db_provider.add_repository(name, dir, type_)
+                        except self.env.db_exc.IntegrityError:
+                            name = name or '(default)'
+                            raise TracError(_('The repository "%(name)s" '
+                                              'already exists.', name=name))
                         name = name or '(default)'
                         add_notice(req, _('The repository "%(name)s" has been '
                                           'added.', name=name))
@@ -277,7 +283,12 @@
                     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)
+                        try:
+                            db_provider.add_alias(name, alias)
+                        except self.env.db_exc.IntegrityError:
+                            raise TracError(_('The alias "%(name)s" already '
+                                              'exists.',
+                                              name=name or '(default)'))
                         add_notice(req, _('The alias "%(name)s" has been '
                                           'added.', name=name or '(default)'))
                         req.redirect(req.href.admin(category, page))
diff --git a/trac/trac/versioncontrol/api.py b/trac/trac/versioncontrol/api.py
index dca3667..faf2d52 100644
--- a/trac/trac/versioncontrol/api.py
+++ b/trac/trac/versioncontrol/api.py
@@ -24,7 +24,7 @@
 from trac.core import *
 from trac.resource import IResourceManager, Resource, ResourceNotFound
 from trac.util.concurrency import threading
-from trac.util.text import printout, to_unicode
+from trac.util.text import printout, to_unicode, exception_to_unicode
 from trac.util.translation import _
 from trac.web.api import IRequestFilter
 
@@ -250,9 +250,18 @@
         """Modify attributes of a repository."""
         if is_default(reponame):
             reponame = ''
+        new_reponame = changes.get('name', reponame)
+        if is_default(new_reponame):
+            new_reponame = ''
         rm = RepositoryManager(self.env)
         with self.env.db_transaction as db:
             id = rm.get_repository_id(reponame)
+            if reponame != new_reponame:
+                if db("""SELECT id FROM repository WHERE name='name' AND
+                         value=%s""", (new_reponame,)):
+                    raise TracError(_('The repository "%(name)s" already '
+                                      'exists.',
+                                      name=new_reponame or '(default)'))
             for (k, v) in changes.iteritems():
                 if k not in self.repository_attrs:
                     continue
@@ -353,7 +362,22 @@
                         _("Can't synchronize with repository \"%(name)s\" "
                           "(%(error)s). Look in the Trac log for more "
                           "information.", name=reponame or '(default)',
-                          error=to_unicode(e.message)))
+                          error=to_unicode(e)))
+                except Exception, e:
+                    add_warning(req,
+                        _("Failed to sync with repository \"%(name)s\": "
+                          "%(error)s; repository information may be out of "
+                          "date. Look in the Trac log for more information "
+                          "including mitigation strategies.",
+                          name=reponame or '(default)', error=to_unicode(e)))
+                    self.log.error(
+                        "Failed to sync with repository \"%s\"; You may be "
+                        "able to reduce the impact of this issue by "
+                        "configuring [trac] repository_sync_per_request; see "
+                        "http://trac.edgewall.org/wiki/TracRepositoryAdmin"
+                        "#ExplicitSync for more detail: %s",
+                        reponame or '(default)',
+                        exception_to_unicode(e, traceback=True))
                 self.log.info("Synchronized '%s' repository in %0.2f seconds",
                               reponame or '(default)', time.time() - start)
         return handler
@@ -400,6 +424,8 @@
             return _('%(kind)s %(id)s%(at_version)s%(in_repo)s',
                      kind=kind, id=id, at_version=version, in_repo=in_repo)
         elif resource.realm == 'repository':
+            if not resource.id:
+                return _("Default repository")
             return _("Repository %(repo)s", repo=resource.id)
 
     def get_resource_url(self, resource, href, **kwargs):
@@ -502,8 +528,8 @@
 
         This will create and save a new id if none is found.
 
-        \note: this should probably be renamed as we're dealing
-               exclusively with *db* repository ids here.
+        Note: this should probably be renamed as we're dealing
+              exclusively with *db* repository ids here.
         """
         with self.env.db_transaction as db:
             for id, in db(
@@ -631,8 +657,8 @@
         The supported events are the names of the methods defined in the
         `IRepositoryChangeListener` interface.
         """
-        self.log.debug("Event %s on %s for changesets %r",
-                       event, reponame, revs)
+        self.log.debug("Event %s on repository '%s' for changesets %r",
+                       event, reponame or '(default)', revs)
 
         # Notify a repository by name, and all repositories with the same
         # base, or all repositories by base or by repository dir
@@ -667,8 +693,12 @@
                         repos.sync_changeset(rev)
                         changeset = repos.get_changeset(rev)
                     except NoSuchChangeset:
+                        self.log.debug(
+                            "No changeset '%s' found in repository '%s'. "
+                            "Skipping subscribers for event %s",
+                            rev, repos.reponame or '(default)', event)
                         continue
-                self.log.debug("Event %s on %s for revision %s",
+                self.log.debug("Event %s on repository '%s' for revision '%s'",
                                event, repos.reponame or '(default)', rev)
                 for listener in self.change_listeners:
                     getattr(listener, event)(repos, changeset, *args)
@@ -998,6 +1028,23 @@
         """
         raise NotImplementedError
 
+    def get_processed_content(self, keyword_substitution=True, eol_hint=None):
+        """Return a stream for reading the content of the node, with some
+        standard processing applied.
+
+        :param keyword_substitution: if `True`, meta-data keywords
+            present in the content like ``$Rev$`` are substituted
+            (which keyword are substituted and how they are
+            substituted is backend specific)
+
+        :param eol_hint: which style of line ending is expected if
+            `None` was explicitly specified for the file itself in
+            the version control backend (for example in Subversion,
+            if it was set to ``'native'``).  It can be `None`,
+            ``'LF'``, ``'CR'`` or ``'CRLF'``.
+        """
+        return self.get_content()
+
     def get_entries(self):
         """Generator that yields the immediate child entries of a directory.
 
diff --git a/trac/trac/versioncontrol/cache.py b/trac/trac/versioncontrol/cache.py
index 48bc736..a3e2729 100644
--- a/trac/trac/versioncontrol/cache.py
+++ b/trac/trac/versioncontrol/cache.py
@@ -101,7 +101,7 @@
                       """, (to_utimestamp(cset.date), cset.author,
                             cset.message, self.id, srev))
             else:
-                self._insert_changeset(db, rev, cset)
+                self._insert_changeset(db, cset.rev, cset)
         return old_cset
 
     @cached('_metadata_id')
@@ -115,62 +115,10 @@
 
     def sync(self, feedback=None, clean=False):
         if clean:
-            self.log.info("Cleaning cache")
-            with self.env.db_transaction as db:
-                db("DELETE FROM revision WHERE repos=%s",
-                   (self.id,))
-                db("DELETE FROM node_change WHERE repos=%s",
-                   (self.id,))
-                db.executemany("DELETE FROM repository WHERE id=%s AND name=%s",
-                               [(self.id, k) for k in CACHE_METADATA_KEYS])
-                db.executemany("""
-                      INSERT INTO repository (id, name, value)
-                      VALUES (%s, %s, %s)
-                      """, [(self.id, k, '') for k in CACHE_METADATA_KEYS])
-                del self.metadata
+            self.remove_cache()
 
         metadata = self.metadata
-
-        with self.env.db_transaction as db:
-            invalidate = False
-
-            # -- check that we're populating the cache for the correct
-            #    repository
-            repository_dir = metadata.get(CACHE_REPOSITORY_DIR)
-            if repository_dir:
-                # directory part of the repo name can vary on case insensitive
-                # fs
-                if os.path.normcase(repository_dir) \
-                        != os.path.normcase(self.name):
-                    self.log.info("'repository_dir' has changed from %r to %r",
-                                  repository_dir, self.name)
-                    raise TracError(_("The repository directory has changed, "
-                                      "you should resynchronize the "
-                                      "repository with: trac-admin $ENV "
-                                      "repository resync '%(reponame)s'",
-                                      reponame=self.reponame or '(default)'))
-            elif repository_dir is None: #
-                self.log.info('Storing initial "repository_dir": %s',
-                              self.name)
-                db("""INSERT INTO repository (id, name, value)
-                      VALUES (%s, %s, %s)
-                      """, (self.id, CACHE_REPOSITORY_DIR, self.name))
-                invalidate = True
-            else: # 'repository_dir' cleared by a resync
-                self.log.info('Resetting "repository_dir": %s', self.name)
-                db("UPDATE repository SET value=%s WHERE id=%s AND name=%s",
-                   (self.name, self.id, CACHE_REPOSITORY_DIR))
-                invalidate = True
-
-            # -- insert a 'youngeset_rev' for the repository if necessary
-            if metadata.get(CACHE_YOUNGEST_REV) is None:
-                db("""INSERT INTO repository (id, name, value)
-                      VALUES (%s, %s, %s)
-                      """, (self.id, CACHE_YOUNGEST_REV, ''))
-                invalidate = True
-
-            if invalidate:
-                del self.metadata
+        self.save_metadata(metadata)
 
         # -- retrieve the youngest revision in the repository and the youngest
         #    revision cached so far
@@ -263,6 +211,65 @@
                 if feedback:
                     feedback(youngest)
 
+    def remove_cache(self):
+        """Remove the repository cache."""
+        self.log.info("Cleaning cache")
+        with self.env.db_transaction as db:
+            db("DELETE FROM revision WHERE repos=%s",
+               (self.id,))
+            db("DELETE FROM node_change WHERE repos=%s",
+               (self.id,))
+            db.executemany("DELETE FROM repository WHERE id=%s AND name=%s",
+                           [(self.id, k) for k in CACHE_METADATA_KEYS])
+            db.executemany("""
+                  INSERT INTO repository (id, name, value)
+                  VALUES (%s, %s, %s)
+                  """, [(self.id, k, '') for k in CACHE_METADATA_KEYS])
+            del self.metadata
+
+    def save_metadata(self, metadata):
+        """Save the repository metadata."""
+        with self.env.db_transaction as db:
+            invalidate = False
+
+            # -- check that we're populating the cache for the correct
+            #    repository
+            repository_dir = metadata.get(CACHE_REPOSITORY_DIR)
+            if repository_dir:
+                # directory part of the repo name can vary on case insensitive
+                # fs
+                if os.path.normcase(repository_dir) \
+                        != os.path.normcase(self.name):
+                    self.log.info("'repository_dir' has changed from %r to %r",
+                                  repository_dir, self.name)
+                    raise TracError(_("The repository directory has changed, "
+                                      "you should resynchronize the "
+                                      "repository with: trac-admin $ENV "
+                                      "repository resync '%(reponame)s'",
+                                      reponame=self.reponame or '(default)'))
+            elif repository_dir is None: #
+                self.log.info('Storing initial "repository_dir": %s',
+                              self.name)
+                db("""INSERT INTO repository (id, name, value)
+                      VALUES (%s, %s, %s)
+                      """, (self.id, CACHE_REPOSITORY_DIR, self.name))
+                invalidate = True
+            else: # 'repository_dir' cleared by a resync
+                self.log.info('Resetting "repository_dir": %s', self.name)
+                db("UPDATE repository SET value=%s WHERE id=%s AND name=%s",
+                   (self.name, self.id, CACHE_REPOSITORY_DIR))
+                invalidate = True
+
+            # -- insert a 'youngeset_rev' for the repository if necessary
+            if metadata.get(CACHE_YOUNGEST_REV) is None:
+                db("""INSERT INTO repository (id, name, value)
+                      VALUES (%s, %s, %s)
+                      """, (self.id, CACHE_YOUNGEST_REV, ''))
+                invalidate = True
+
+            if invalidate:
+                del self.metadata
+
     def _insert_changeset(self, db, rev, cset):
         srev = self.db_rev(rev)
         # 1. Attempt to resync the 'revision' table.  In case of
@@ -310,9 +317,64 @@
             return [int(rev) for rev, in db("""
                     SELECT DISTINCT rev FROM node_change
                     WHERE repos=%%s AND rev>=%%s AND rev<=%%s
-                      AND (path=%%s OR path %s)""" % db.like(),
+                      AND (path=%%s OR path %s)""" % db.prefix_match(),
                     (self.id, sfirst, slast, path,
-                     db.like_escape(path + '/') + '%'))]
+                     db.prefix_match_value(path + '/')))]
+
+    def _get_changed_revs(self, node_infos):
+        if not node_infos:
+            return {}
+
+        node_infos = [(node, self.normalize_rev(first)) for node, first
+                                                        in node_infos]
+        sfirst = self.db_rev(min(first for node, first in node_infos))
+        slast = self.db_rev(max(node.rev for node, first in node_infos))
+        path_infos = dict((node.path, (node, first)) for node, first
+                                                     in node_infos)
+        path_revs = dict((node.path, []) for node, first in node_infos)
+
+        db = self.env.get_read_db()
+        cursor = db.cursor()
+        prefix_match = db.prefix_match()
+
+        # Prevent "too many SQL variables" since max number of parameters is
+        # 999 on SQLite. No limitation on PostgreSQL and MySQL.
+        idx = 0
+        delta = (999 - 3) // 5
+        while idx < len(node_infos):
+            subset = node_infos[idx:idx + delta]
+            idx += delta
+            count = len(subset)
+
+            holders = ','.join(('%s',) * count)
+            query = """\
+                SELECT DISTINCT
+                  rev, (CASE WHEN path IN (%s) THEN path %s END) AS path
+                FROM node_change
+                WHERE repos=%%s AND rev>=%%s AND rev<=%%s AND (path IN (%s) %s)
+                """ % \
+                (holders,
+                 ' '.join(('WHEN path ' + prefix_match + ' THEN %s',) * count),
+                 holders,
+                 ' '.join(('OR path ' + prefix_match,) * count))
+            args = []
+            args.extend(node.path for node, first in subset)
+            for node, first in subset:
+                args.append(db.prefix_match_value(node.path + '/'))
+                args.append(node.path)
+            args.extend((self.id, sfirst, slast))
+            args.extend(node.path for node, first in subset)
+            args.extend(db.prefix_match_value(node.path + '/')
+                        for node, first in subset)
+            cursor.execute(query, args)
+
+            for srev, path in cursor:
+                rev = self.rev_db(srev)
+                node, first = path_infos[path]
+                if first <= rev <= node.rev:
+                    path_revs[path].append(rev)
+
+        return path_revs
 
     def has_node(self, path, rev=None):
         return self.repos.has_node(path, self.normalize_rev(rev))
@@ -324,10 +386,16 @@
         return self.rev_db(self.metadata.get(CACHE_YOUNGEST_REV))
 
     def previous_rev(self, rev, path=''):
-        if self.has_linear_changesets:
-            return self._next_prev_rev('<', rev, path)
-        else:
-            return self.repos.previous_rev(self.normalize_rev(rev), path)
+        # Hitting the repository directly is faster than searching the
+        # database.  When there is a long stretch of inactivity on a file (in
+        # particular, when a file is added late in the history) the database
+        # query can take a very long time to determine that there is no
+        # previous revision in the node_changes table.  However, the repository
+        # will have a datastructure that will allow it to find the previous
+        # version of a node fairly directly.
+        #if self.has_linear_changesets:
+        #    return self._next_prev_rev('<', rev, path)
+        return self.repos.previous_rev(self.normalize_rev(rev), path)
 
     def next_rev(self, rev, path=''):
         if self.has_linear_changesets:
@@ -346,8 +414,8 @@
             if path:
                 path = path.lstrip('/')
                 # changes on path itself or its children
-                sql += " AND (path=%s OR path " + db.like()
-                args.extend((path, db.like_escape(path + '/') + '%'))
+                sql += " AND (path=%s OR path " + db.prefix_match()
+                args.extend((path, db.prefix_match_value(path + '/')))
                 # deletion of path ancestors
                 components = path.lstrip('/').split('/')
                 parents = ','.join(('%s',) * len(components))
@@ -361,6 +429,12 @@
             for rev, in db(sql, args):
                 return int(rev)
 
+    def parent_revs(self, rev):
+        if self.has_linear_changesets:
+            return Repository.parent_revs(self, rev)
+        else:
+            return self.repos.parent_revs(rev)
+
     def rev_older_than(self, rev1, rev2):
         return self.repos.rev_older_than(self.normalize_rev(rev1),
                                          self.normalize_rev(rev2))
diff --git a/trac/trac/versioncontrol/templates/admin_repositories.html b/trac/trac/versioncontrol/templates/admin_repositories.html
index 05210ec..395d788 100644
--- a/trac/trac/versioncontrol/templates/admin_repositories.html
+++ b/trac/trac/versioncontrol/templates/admin_repositories.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2009-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -11,7 +21,7 @@
   </head>
 
   <body py:with="sorted_repos = sorted(repositories.iteritems(), key=lambda item: item[0].lower())">
-    <h2>Manage Repositories</h2>
+    <h2>Manage Repositories <span py:if="view == 'list'" class="trac-count">(${len(repositories)})</span></h2>
 
     <py:def function="type_field(editable, multiline=False, selected=None)">
       <div class="field">
@@ -39,7 +49,8 @@
 
     <py:choose test="view">
       <form py:when="'detail'" py:with="info = repositories[reponame]" class="mod" id="trac-modrepos" method="post" action="">
-        <fieldset py:choose="" py:with="readonly = not info.editable or None">
+        <fieldset py:choose="" py:with="disabled = 'disabled' if not info.editable else None;
+                                        readonly = 'readonly' if not info.editable else None">
           <legend py:when="info.editable">Modify Repository:</legend>
           <legend py:otherwise="">View Repository:</legend>
           <p py:if="not info.editable" class="hint" i18n:msg=""><strong>Note:</strong>
@@ -47,7 +58,8 @@
             and cannot be edited on this page.
           </p>
           <div class="field">
-            <label>Name:<br/><input type="text" name="name" value="$info.name" readonly="$readonly"/></label>
+            <label>Name:<br/><input type="text" name="name" class="trac-autofocus"
+                                    value="$info.name" readonly="$readonly" /></label>
           </div>
           <py:choose>
             <py:when test="'alias' in info">
@@ -64,25 +76,26 @@
             </py:otherwise>
           </py:choose>
           <div class="field">
-            <label><input type="checkbox" name="hidden" value="1" checked="${info.hidden or None}" disabled="$readonly"/>
+            <label><input type="checkbox" name="hidden" value="1" checked="${info.hidden or None}"
+                          disabled="${not info.editable or None}"/>
               Hide from repository index
             </label>
           </div>
           <div class="field">
             <fieldset>
               <label for="description" i18n:msg="">
-                Description (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here):
+                Description: (you may use <a tabindex="42" href="${href.wiki('WikiFormatting')}">WikiFormatting</a> here)
               </label>
               <p>
-                <textarea id="description" name="description" class="wikitext trac-resizable"
+                <textarea id="description" name="description" class="wikitext trac-fullwidth trac-resizable"
                           rows="6" cols="60" readonly="$readonly">
 $info.description</textarea>
               </p>
             </fieldset>
           </div>
           <div class="buttons">
+            <input py:if="info.editable" type="submit" name="save" class="trac-disable-on-submit" value="${_('Save')}"/>
             <input type="submit" name="cancel" value="${_('Cancel')}"/>
-            <input py:if="info.editable" type="submit" name="save" value="${_('Save')}"/>
           </div>
         </fieldset>
       </form>
@@ -99,7 +112,7 @@
               <label>Directory: <input type="text" name="dir"/></label>
             </div>
             <div class="buttons">
-              <input type="submit" name="add_repos" value="${_('Add')}"/>
+              <input type="submit" name="add_repos" class="trac-disable-on-submit" value="${_('Add')}"/>
             </div>
           </fieldset>
         </form>
@@ -113,12 +126,12 @@
             </div>
             ${alias_field(True)}
             <div class="buttons">
-              <input type="submit" name="add_alias" value="${_('Add')}"/>
+              <input type="submit" name="add_alias" class="trac-disable-on-submit" value="${_('Add')}"/>
             </div>
           </fieldset>
         </form>
 
-        <form id="trac-repository_table" method="post" action="">
+        <form py:if="sorted_repos" id="trac-repository_table" method="post" action="">
           <table class="listing" id="trac-reposlist">
             <thead>
               <tr><th class="sel">&nbsp;</th>
@@ -142,7 +155,7 @@
           </table>
           <div class="buttons">
             <input type="submit" name="refresh" value="${_('Refresh')}"/>
-            <input type="submit" name="remove" value="${_('Remove selected items')}"/>
+            <input type="submit" name="remove" class="trac-disable-on-submit" value="${_('Remove selected items')}"/>
           </div>
         </form>
       </py:otherwise>
diff --git a/trac/trac/versioncontrol/templates/browser.html b/trac/trac/versioncontrol/templates/browser.html
index c80a6ce..d6f138c 100644
--- a/trac/trac/versioncontrol/templates/browser.html
+++ b/trac/trac/versioncontrol/templates/browser.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -96,7 +106,7 @@
           </form>
         </div>
 
-        <div class="trac-tags" py:with="changeset = repos.get_changeset(repos.normalize_rev(stickyrev))">
+        <div class="trac-tags" py:if="changeset">
           <span py:for="branch, head in changeset.get_branches()" py:if="branch not in ('default', 'master')"
                 class="branch${' head' if head else ''}"
                 title="${_('Branch head') if head else _('Branch')}">${branch}</span>
diff --git a/trac/trac/versioncontrol/templates/changeset.html b/trac/trac/versioncontrol/templates/changeset.html
index 2471c94..f216814 100644
--- a/trac/trac/versioncontrol/templates/changeset.html
+++ b/trac/trac/versioncontrol/templates/changeset.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/versioncontrol/templates/diff_form.html b/trac/trac/versioncontrol/templates/diff_form.html
index ca1ea53..18ba121 100644
--- a/trac/trac/versioncontrol/templates/diff_form.html
+++ b/trac/trac/versioncontrol/templates/diff_form.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/versioncontrol/templates/dir_entries.html b/trac/trac/versioncontrol/templates/dir_entries.html
index a88be05..f2eece6 100644
--- a/trac/trac/versioncontrol/templates/dir_entries.html
+++ b/trac/trac/versioncontrol/templates/dir_entries.html
@@ -1,4 +1,15 @@
-<!--! Template for generating rows corresponding to directory entries -->
+<!--!  Copyright (C) 2007-2014 Edgewall Software
+
+  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/.
+
+Template for generating rows corresponding to directory entries
+-->
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:py="http://genshi.edgewall.org/"
       xmlns:xi="http://www.w3.org/2001/XInclude" py:strip="">
diff --git a/trac/trac/versioncontrol/templates/dirlist_thead.html b/trac/trac/versioncontrol/templates/dirlist_thead.html
index 39c8cf5..20588d6 100644
--- a/trac/trac/versioncontrol/templates/dirlist_thead.html
+++ b/trac/trac/versioncontrol/templates/dirlist_thead.html
@@ -1,4 +1,15 @@
-<!--! Template snippet for a standard table header for a dirlist -->
+<!--!  Copyright (C) 2008-2014 Edgewall Software
+
+  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/.
+
+Template snippet for a standard table header for a dirlist
+-->
 <html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:py="http://genshi.edgewall.org/"
     xmlns:xi="http://www.w3.org/2001/XInclude" py:strip="">
diff --git a/trac/trac/versioncontrol/templates/path_links.html b/trac/trac/versioncontrol/templates/path_links.html
index cb40d1d..61b358b 100644
--- a/trac/trac/versioncontrol/templates/path_links.html
+++ b/trac/trac/versioncontrol/templates/path_links.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2008-2014 Edgewall Software
+
+  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/.
+-->
 <div xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/" py:strip="">
   <!--!  Display a sequence of path components.
diff --git a/trac/trac/versioncontrol/templates/repository_index.html b/trac/trac/versioncontrol/templates/repository_index.html
index 3617164..3800891 100644
--- a/trac/trac/versioncontrol/templates/repository_index.html
+++ b/trac/trac/versioncontrol/templates/repository_index.html
@@ -1,4 +1,15 @@
-<!--! Template snippet for a table of repositories -->
+<!--!  Copyright (C) 2008-2014 Edgewall Software
+
+  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/.
+
+Template snippet for a table of repositories
+-->
 <html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:py="http://genshi.edgewall.org/"
     xmlns:xi="http://www.w3.org/2001/XInclude" py:strip="">
@@ -11,11 +22,11 @@
         <tr class="${'odd' if idx % 2 else 'even'}">
           <td class="name">
             <em py:strip="not err">
-              <b py:strip="repoinfo.alias != ''">
+              <strong py:strip="repoinfo.alias != ''">
                 <a class="dir" title="View Root Directory"
                    href="${href.browser(repos.reponame if repos else reponame,
                                         order=order if order != 'name' else None, desc=desc)}">$reponame</a>
-              </b>
+              </strong>
             </em>
           </td>
           <td class="size">
diff --git a/trac/trac/versioncontrol/templates/revisionlog.html b/trac/trac/versioncontrol/templates/revisionlog.html
index 606a8f4..1d80615 100644
--- a/trac/trac/versioncontrol/templates/revisionlog.html
+++ b/trac/trac/versioncontrol/templates/revisionlog.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/versioncontrol/templates/sortable_th.html b/trac/trac/versioncontrol/templates/sortable_th.html
index 4902223..bd44262 100644
--- a/trac/trac/versioncontrol/templates/sortable_th.html
+++ b/trac/trac/versioncontrol/templates/sortable_th.html
@@ -1,4 +1,14 @@
-<!--! Snippet for a <th> corresponding to a sortable column.
+<!--!  Copyright (C) 2008-2014 Edgewall Software
+
+  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/.
+
+Snippet for a <th> corresponding to a sortable column.
 
     Expects the following variables to be set specifically:
 
diff --git a/trac/trac/versioncontrol/tests/__init__.py b/trac/trac/versioncontrol/tests/__init__.py
index e130e62..d4896ba 100644
--- a/trac/trac/versioncontrol/tests/__init__.py
+++ b/trac/trac/versioncontrol/tests/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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 unittest
 
 from trac.versioncontrol.tests import cache, diff, svn_authz, api
diff --git a/trac/trac/versioncontrol/tests/api.py b/trac/trac/versioncontrol/tests/api.py
index 5f93d37..fff5007 100644
--- a/trac/trac/versioncontrol/tests/api.py
+++ b/trac/trac/versioncontrol/tests/api.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 #
+# Copyright (C) 2007-2013 Edgewall Software
 # Copyright (C) 2007 CommProve, Inc. <eli.carter@commprove.com>
 # All rights reserved.
 #
@@ -27,40 +28,40 @@
                                     None)
 
     def test_raise_NotImplementedError_close(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.close)
+        self.assertRaises(NotImplementedError, self.repo_base.close)
 
     def test_raise_NotImplementedError_get_changeset(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.get_changeset, 1)
+        self.assertRaises(NotImplementedError, self.repo_base.get_changeset, 1)
 
     def test_raise_NotImplementedError_get_node(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.get_node, 'path')
+        self.assertRaises(NotImplementedError, self.repo_base.get_node, 'path')
 
     def test_raise_NotImplementedError_get_oldest_rev(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.get_oldest_rev)
+        self.assertRaises(NotImplementedError, self.repo_base.get_oldest_rev)
 
     def test_raise_NotImplementedError_get_youngest_rev(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.get_youngest_rev)
+        self.assertRaises(NotImplementedError, self.repo_base.get_youngest_rev)
 
     def test_raise_NotImplementedError_previous_rev(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.previous_rev, 1)
+        self.assertRaises(NotImplementedError, self.repo_base.previous_rev, 1)
 
     def test_raise_NotImplementedError_next_rev(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.next_rev, 1)
+        self.assertRaises(NotImplementedError, self.repo_base.next_rev, 1)
 
     def test_raise_NotImplementedError_rev_older_than(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.rev_older_than, 1, 2)
+        self.assertRaises(NotImplementedError, self.repo_base.rev_older_than, 1, 2)
 
     def test_raise_NotImplementedError_get_path_history(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.get_path_history, 'path')
+        self.assertRaises(NotImplementedError, self.repo_base.get_path_history, 'path')
 
     def test_raise_NotImplementedError_normalize_path(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.normalize_path, 'path')
+        self.assertRaises(NotImplementedError, self.repo_base.normalize_path, 'path')
 
     def test_raise_NotImplementedError_normalize_rev(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.normalize_rev, 1)
+        self.assertRaises(NotImplementedError, self.repo_base.normalize_rev, 1)
 
     def test_raise_NotImplementedError_get_changes(self):
-        self.failUnlessRaises(NotImplementedError, self.repo_base.get_changes, 'path', 1, 'path', 2)
+        self.assertRaises(NotImplementedError, self.repo_base.get_changes, 'path', 1, 'path', 2)
 
 
 class ResourceManagerTestCase(unittest.TestCase):
@@ -110,13 +111,19 @@
         self.assertEqual('/trac.cgi/browser/testrepo',
                          get_resource_url(self.env, res, self.env.href))
 
+        res = Resource('repository', '')  # default repository
+        self.assertEqual('Default repository',
+                         get_resource_description(self.env, res))
+        self.assertEqual('/trac.cgi/browser',
+                         get_resource_url(self.env, res, self.env.href))
+
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(ApiTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ResourceManagerTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(ApiTestCase))
+    suite.addTest(unittest.makeSuite(ResourceManagerTestCase))
     return suite
 
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/versioncontrol/tests/cache.py b/trac/trac/versioncontrol/tests/cache.py
index 438a47f..33292c1 100644
--- a/trac/trac/versioncontrol/tests/cache.py
+++ b/trac/trac/versioncontrol/tests/cache.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
 #
@@ -18,6 +18,7 @@
 
 from datetime import datetime
 
+import trac.tests.compat
 from trac.test import EnvironmentStub, Mock
 from trac.util.datefmt import to_utimestamp, utc
 from trac.versioncontrol import Repository, Changeset, Node, NoSuchChangeset
@@ -81,9 +82,9 @@
         cache.sync()
 
         with self.env.db_query as db:
-            self.assertEquals([], db(
+            self.assertEqual([], db(
                 "SELECT rev, time, author, message FROM revision"))
-            self.assertEquals(0, db("SELECT COUNT(*) FROM node_change")[0][0])
+            self.assertEqual(0, db("SELECT COUNT(*) FROM node_change")[0][0])
 
     def test_initial_sync(self):
         t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
@@ -101,17 +102,17 @@
 
         with self.env.db_query as db:
             rows = db("SELECT rev, time, author, message FROM revision")
-            self.assertEquals(len(rows), 2)
-            self.assertEquals(('0', to_utimestamp(t1), '', ''), rows[0])
-            self.assertEquals(('1', to_utimestamp(t2), 'joe', 'Import'),
-                              rows[1])
+            self.assertEqual(len(rows), 2)
+            self.assertEqual(('0', to_utimestamp(t1), '', ''), rows[0])
+            self.assertEqual(('1', to_utimestamp(t2), 'joe', 'Import'),
+                             rows[1])
             rows = db("""
                 SELECT rev, path, node_type, change_type, base_path, base_rev
                 FROM node_change""")
-            self.assertEquals(len(rows), 2)
-            self.assertEquals(('1', 'trunk', 'D', 'A', None, None), rows[0])
-            self.assertEquals(('1', 'trunk/README', 'F', 'A', None, None),
-                              rows[1])
+            self.assertEqual(len(rows), 2)
+            self.assertEqual(('1', 'trunk', 'D', 'A', None, None), rows[0])
+            self.assertEqual(('1', 'trunk/README', 'F', 'A', None, None),
+                             rows[1])
 
     def test_update_sync(self):
         t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
@@ -137,10 +138,10 @@
         cache.sync()
 
         with self.env.db_query as db:
-            self.assertEquals([(to_utimestamp(t3), 'joe', 'Update')],
+            self.assertEqual([(to_utimestamp(t3), 'joe', 'Update')],
                 db("SELECT time, author, message FROM revision WHERE rev='2'"))
-            self.assertEquals([('trunk/README', 'F', 'E', 'trunk/README',
-                                '1')],
+            self.assertEqual([('trunk/README', 'F', 'E', 'trunk/README',
+                               '1')],
                     db("""SELECT path, node_type, change_type, base_path,
                                  base_rev
                           FROM node_change WHERE rev='2'"""))
@@ -175,20 +176,20 @@
         rows = self.env.db_query("""
             SELECT time, author, message FROM revision ORDER BY rev
             """)
-        self.assertEquals(3, len(rows))
-        self.assertEquals((to_utimestamp(t1), 'joe', '**empty**'), rows[0])
-        self.assertEquals((to_utimestamp(t2), 'joe', 'Initial Import'),
-                          rows[1])
-        self.assertEquals((to_utimestamp(t3), 'joe', 'Update'), rows[2])
+        self.assertEqual(3, len(rows))
+        self.assertEqual((to_utimestamp(t1), 'joe', '**empty**'), rows[0])
+        self.assertEqual((to_utimestamp(t2), 'joe', 'Initial Import'),
+                         rows[1])
+        self.assertEqual((to_utimestamp(t3), 'joe', 'Update'), rows[2])
 
         rows = self.env.db_query("""
             SELECT rev, path, node_type, change_type, base_path, base_rev
             FROM node_change ORDER BY rev, path""")
-        self.assertEquals(3, len(rows))
-        self.assertEquals(('1', 'trunk', 'D', 'A', None, None), rows[0])
-        self.assertEquals(('1', 'trunk/README', 'F', 'A', None, None), rows[1])
-        self.assertEquals(('2', 'trunk/README', 'F', 'E', 'trunk/README', '1'),
-                          rows[2])
+        self.assertEqual(3, len(rows))
+        self.assertEqual(('1', 'trunk', 'D', 'A', None, None), rows[0])
+        self.assertEqual(('1', 'trunk/README', 'F', 'A', None, None), rows[1])
+        self.assertEqual(('2', 'trunk/README', 'F', 'E', 'trunk/README', '1'),
+                         rows[2])
 
     def test_sync_changeset(self):
         t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
@@ -215,9 +216,9 @@
 
         rows = self.env.db_query(
                 "SELECT time, author, message FROM revision ORDER BY rev")
-        self.assertEquals(2, len(rows))
-        self.assertEquals((to_utimestamp(t1), 'joe', '**empty**'), rows[0])
-        self.assertEquals((to_utimestamp(t2), 'joe', 'Import'), rows[1])
+        self.assertEqual(2, len(rows))
+        self.assertEqual((to_utimestamp(t1), 'joe', '**empty**'), rows[0])
+        self.assertEqual((to_utimestamp(t2), 'joe', 'Import'), rows[1])
 
     def test_sync_changeset_if_not_exists(self):
         t = [
@@ -260,7 +261,7 @@
         cache.sync()
         self.assertRaises(NoSuchChangeset, cache.get_changeset, 2)
 
-        self.assertEqual(None, cache.sync_changeset(2))
+        self.assertIsNone(cache.sync_changeset(2))
         cset = cache.get_changeset(2)
         self.assertEqual('john', cset.author)
         self.assertEqual('Created directories', cset.message)
@@ -275,12 +276,46 @@
 
         rows = self.env.db_query(
                 "SELECT time,author,message FROM revision ORDER BY rev")
-        self.assertEquals(4, len(rows))
-        self.assertEquals((to_utimestamp(t[0]), 'joe', '**empty**'), rows[0])
-        self.assertEquals((to_utimestamp(t[1]), 'joe', 'Import'), rows[1])
-        self.assertEquals((to_utimestamp(t[2]), 'john', 'Created directories'),
-                          rows[2])
-        self.assertEquals((to_utimestamp(t[3]), 'joe', 'Add COPYING'), rows[3])
+        self.assertEqual(4, len(rows))
+        self.assertEqual((to_utimestamp(t[0]), 'joe', '**empty**'), rows[0])
+        self.assertEqual((to_utimestamp(t[1]), 'joe', 'Import'), rows[1])
+        self.assertEqual((to_utimestamp(t[2]), 'john', 'Created directories'),
+                         rows[2])
+        self.assertEqual((to_utimestamp(t[3]), 'joe', 'Add COPYING'), rows[3])
+
+    def test_sync_changeset_with_string_rev(self):  # ticket:11660
+
+        class MockCachedRepository(CachedRepository):
+            def db_rev(self, rev):
+                return '%010d' % rev
+
+        t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
+        t2 = datetime(2002, 1, 1, 1, 1, 1, 0, utc)
+        repos = self.get_repos(get_changeset=lambda x: changesets[int(x)],
+                               youngest_rev=1)
+        changesets = [
+            Mock(Changeset, repos, 0, 'empty', 'joe', t1,
+                 get_changes=lambda: []),
+            Mock(Changeset, repos, 1, 'first', 'joe', t2,
+                 get_changes=lambda: []),
+            ]
+        cache = MockCachedRepository(self.env, repos, self.log)
+
+        cache.sync_changeset('0')   # not cached yet
+        cache.sync_changeset(u'1')  # not cached yet
+        rows = self.env.db_query(
+            "SELECT rev,author FROM revision ORDER BY rev")
+        self.assertEqual(2, len(rows))
+        self.assertEquals(('0000000000', 'joe'), rows[0])
+        self.assertEquals(('0000000001', 'joe'), rows[1])
+
+        cache.sync_changeset(u'0')  # cached
+        cache.sync_changeset('1')   # cached
+        rows = self.env.db_query(
+            "SELECT rev,author FROM revision ORDER BY rev")
+        self.assertEqual(2, len(rows))
+        self.assertEquals(('0000000000', 'joe'), rows[0])
+        self.assertEquals(('0000000001', 'joe'), rows[1])
 
     def test_get_changes(self):
         t1 = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
@@ -307,7 +342,8 @@
 
 
 def suite():
-    return unittest.makeSuite(CacheTestCase, 'test')
+    return unittest.makeSuite(CacheTestCase)
+
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/versioncontrol/tests/diff.py b/trac/trac/versioncontrol/tests/diff.py
index 933f668..68e6e47 100644
--- a/trac/trac/versioncontrol/tests/diff.py
+++ b/trac/trac/versioncontrol/tests/diff.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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/.
+
 from trac.versioncontrol import diff
 
 import unittest
@@ -159,55 +172,55 @@
         """Make sure that the escape calls leave quotes along, we don't need
         to escape them."""
         changes = diff.diff_blocks(['ab'], ['a"b'])
-        self.assertEquals(len(changes), 1)
+        self.assertEqual(len(changes), 1)
         blocks = changes[0]
-        self.assertEquals(len(blocks), 1)
+        self.assertEqual(len(blocks), 1)
         block = blocks[0]
-        self.assertEquals(block['type'], 'mod')
-        self.assertEquals(str(block['base']['lines'][0]), 'a<del></del>b')
-        self.assertEquals(str(block['changed']['lines'][0]), 'a<ins>"</ins>b')
+        self.assertEqual(block['type'], 'mod')
+        self.assertEqual(str(block['base']['lines'][0]), 'a<del></del>b')
+        self.assertEqual(str(block['changed']['lines'][0]), 'a<ins>"</ins>b')
 
     def test_whitespace_marked_up1(self):
         """Regression test for #5795"""
         changes = diff.diff_blocks(['*a'], [' *a'])
         block = changes[0][0]
-        self.assertEquals(block['type'], 'mod')
-        self.assertEquals(str(block['base']['lines'][0]), '<del></del>*a')
-        self.assertEquals(str(block['changed']['lines'][0]),
-                          '<ins>&nbsp;</ins>*a')
+        self.assertEqual(block['type'], 'mod')
+        self.assertEqual(str(block['base']['lines'][0]), '<del></del>*a')
+        self.assertEqual(str(block['changed']['lines'][0]),
+                         '<ins>&nbsp;</ins>*a')
 
     def test_whitespace_marked_up2(self):
         """Related to #5795"""
         changes = diff.diff_blocks(['   a'], ['   b'])
         block = changes[0][0]
-        self.assertEquals(block['type'], 'mod')
-        self.assertEquals(str(block['base']['lines'][0]),
-                          '&nbsp; &nbsp;<del>a</del>')
-        self.assertEquals(str(block['changed']['lines'][0]),
-                          '&nbsp; &nbsp;<ins>b</ins>')
+        self.assertEqual(block['type'], 'mod')
+        self.assertEqual(str(block['base']['lines'][0]),
+                         '&nbsp; &nbsp;<del>a</del>')
+        self.assertEqual(str(block['changed']['lines'][0]),
+                         '&nbsp; &nbsp;<ins>b</ins>')
 
     def test_whitespace_marked_up3(self):
         """Related to #5795"""
         changes = diff.diff_blocks(['a   '], ['b   '])
         block = changes[0][0]
-        self.assertEquals(block['type'], 'mod')
-        self.assertEquals(str(block['base']['lines'][0]),
-                          '<del>a</del>&nbsp; &nbsp;')
-        self.assertEquals(str(block['changed']['lines'][0]),
-                          '<ins>b</ins>&nbsp; &nbsp;')
+        self.assertEqual(block['type'], 'mod')
+        self.assertEqual(str(block['base']['lines'][0]),
+                         '<del>a</del>&nbsp; &nbsp;')
+        self.assertEqual(str(block['changed']['lines'][0]),
+                         '<ins>b</ins>&nbsp; &nbsp;')
 
     def test_expandtabs_works_right(self):
         """Regression test for #4557"""
         changes = diff.diff_blocks(['aa\tb'], ['aaxb'])
         block = changes[0][0]
-        self.assertEquals(block['type'], 'mod')
-        self.assertEquals(str(block['base']['lines'][0]),
-                          'aa<del>&nbsp; &nbsp; &nbsp; </del>b')
-        self.assertEquals(str(block['changed']['lines'][0]),
-                          'aa<ins>x</ins>b')
+        self.assertEqual(block['type'], 'mod')
+        self.assertEqual(str(block['base']['lines'][0]),
+                         'aa<del>&nbsp; &nbsp; &nbsp; </del>b')
+        self.assertEqual(str(block['changed']['lines'][0]),
+                         'aa<ins>x</ins>b')
 
 def suite():
-    return unittest.makeSuite(DiffTestCase, 'test')
+    return unittest.makeSuite(DiffTestCase)
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/trac/trac/versioncontrol/tests/functional.py b/trac/trac/versioncontrol/tests/functional.py
index d2d893e..25268d2 100755
--- a/trac/trac/versioncontrol/tests/functional.py
+++ b/trac/trac/versioncontrol/tests/functional.py
@@ -1,7 +1,31 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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 tempfile
+
+from trac.admin.tests.functional import AuthorizationTestCaseSetup
 from trac.tests.functional import *
 
 
+class TestAdminRepositoryAuthorization(AuthorizationTestCaseSetup):
+    def runTest(self):
+        """Check permissions required to access the Version Control
+        Repositories panel."""
+        self.test_authorization('/admin/versioncontrol/repository',
+                                'VERSIONCONTROL_ADMIN', "Manage Repositories")
+
+
 class TestEmptySvnRepo(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Check empty repository"""
@@ -114,6 +138,53 @@
         tc.find(components, 's')
 
 
+class RegressionTestTicket11186(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11186
+        TracError should be raised when repository with name already exists
+        """
+        self._tester.go_to_admin()
+        tc.follow("\\bRepositories\\b")
+        tc.url(self._tester.url + '/admin/versioncontrol/repository')
+        name = random_word()
+        tc.formvalue('trac-addrepos', 'name', name)
+        tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % name)
+        tc.submit()
+        tc.find('The repository "%s" has been added.' % name)
+        tc.formvalue('trac-addrepos', 'name', name)
+        tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % name)
+        tc.submit()
+        tc.find('The repository "%s" already exists.' % name)
+        tc.notfind(internal_error)
+
+
+class RegressionTestTicket11186Alias(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11186 alias
+        TracError should be raised when repository alias with name already
+        exists
+        """
+        self._tester.go_to_admin()
+        tc.follow("\\bRepositories\\b")
+        tc.url(self._tester.url + '/admin/versioncontrol/repository')
+        word = random_word()
+        target = '%s_repos' % word
+        name = '%s_alias' % word
+        tc.formvalue('trac-addrepos', 'name', target)
+        tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % target)
+        tc.submit()
+        tc.find('The repository "%s" has been added.' % target)
+        tc.formvalue('trac-addalias', 'name', name)
+        tc.formvalue('trac-addalias', 'alias', target)
+        tc.submit()
+        tc.find('The alias "%s" has been added.' % name)
+        tc.formvalue('trac-addalias', 'name', name)
+        tc.formvalue('trac-addalias', 'alias', target)
+        tc.submit()
+        tc.find('The alias "%s" already exists.' % name)
+        tc.notfind(internal_error)
+
+
 class RegressionTestRev5877(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of the source browser fix in r5877"""
@@ -121,16 +192,169 @@
         tc.notfind(internal_error)
 
 
+class RegressionTestTicket11194(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11194
+        TracError should be raised when repository with name already exists
+        """
+        self._tester.go_to_admin()
+        tc.follow("\\bRepositories\\b")
+        tc.url(self._tester.url + '/admin/versioncontrol/repository')
+
+        word = random_word()
+        names = ['%s_%d' % (word, n) for n in xrange(3)]
+        tc.formvalue('trac-addrepos', 'name', names[0])
+        tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % names[0])
+        tc.submit()
+        tc.notfind(internal_error)
+
+        tc.formvalue('trac-addrepos', 'name', names[1])
+        tc.formvalue('trac-addrepos', 'dir', '/var/svn/%s' % names[1])
+        tc.submit()
+        tc.notfind(internal_error)
+
+        tc.follow('\\b' + names[1] + '\\b')
+        tc.url(self._tester.url + '/admin/versioncontrol/repository/' + names[1])
+        tc.formvalue('trac-modrepos', 'name', names[2])
+        tc.submit('save')
+        tc.notfind(internal_error)
+        tc.url(self._tester.url + '/admin/versioncontrol/repository')
+
+        tc.follow('\\b' + names[2] + '\\b')
+        tc.url(self._tester.url + '/admin/versioncontrol/repository/' + names[2])
+        tc.formvalue('trac-modrepos', 'name', names[0])
+        tc.submit('save')
+        tc.find('The repository "%s" already exists.' % names[0])
+        tc.notfind(internal_error)
+
+
+class RegressionTestTicket11346(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11346
+        fix for log: link with revision ranges included oldest wrongly
+        showing HEAD revision
+        """
+        # create new 3 revisions
+        self._testenv.svn_mkdir(['ticket11346'], '')
+        for i in (1, 2):
+            rev = self._testenv.svn_add('ticket11346/file%d.txt' % i, '')
+        tc.go(self._tester.url + '/log?revs=1-2')
+        tc.find('@1')
+        tc.find('@2')
+        tc.notfind('@3')
+        tc.notfind('@%d' % rev)
+
+
+class RegressionTestTicket11355(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11355
+        Save with no changes should redirect back to the repository listing.
+        """
+        # Add a repository
+        self._tester.go_to_admin("Repositories")
+        name = random_unique_camel()
+        dir = os.path.join(tempfile.gettempdir(), name.lower())
+        tc.formvalue('trac-addrepos', 'name', name)
+        tc.formvalue('trac-addrepos', 'dir', dir)
+        tc.submit('add_repos')
+        tc.find('The repository "%s" has been added.' % name)
+
+        # Save unmodified form and redirect back to listing page
+        tc.follow(r"\b%s\b" % name)
+        tc.url(self._tester.url + '/admin/versioncontrol/repository/' + name)
+        tc.submit('save', formname='trac-modrepos')
+        tc.url(self._tester.url + '/admin/versioncontrol/repository')
+        tc.find("Your changes have been saved.")
+
+        # Warning is added when repository dir is not an absolute path
+        tc.follow(r"\b%s\b" % name)
+        tc.url(self._tester.url + '/admin/versioncontrol/repository/' + name)
+        tc.formvalue('trac-modrepos', 'dir', os.path.basename(dir))
+        tc.submit('save')
+        tc.url(self._tester.url + '/admin/versioncontrol/repository/' + name)
+        tc.find('The repository directory must be an absolute path.')
+
+
+class RegressionTestTicket11438(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11438
+        fix for log: link with revision ranges included "head" keyword
+        """
+        rev = self._testenv.svn_mkdir(['ticket11438'], '')
+        rev = self._testenv.svn_add('ticket11438/file1.txt', '')
+        rev = self._testenv.svn_add('ticket11438/file2.txt', '')
+        tc.go(self._tester.url + '/intertrac/log:@%d:head' % (rev - 1))
+        tc.url(self._tester.url + r'/log/\?revs=' + str(rev - 1) + '%3Ahead')
+        tc.notfind('@%d' % (rev + 1))
+        tc.find('@%d' % rev)
+        tc.find('@%d' % (rev - 1))
+        tc.notfind('@%d' % (rev - 2))
+
+
+class RegressionTestTicket11584(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11584
+        don't raise NoSuchChangeset for empty repository if no "rev" parameter
+        """
+        repo_path = self._testenv.svnadmin_create('repo-t11584')
+
+        self._tester.go_to_admin()
+        tc.follow("\\bRepositories\\b")
+        tc.url(self._tester.url + '/admin/versioncontrol/repository')
+
+        tc.formvalue('trac-addrepos', 'name', 't11584')
+        tc.formvalue('trac-addrepos', 'dir', repo_path)
+        tc.submit()
+        tc.notfind(internal_error)
+        self._testenv._tracadmin('repository', 'sync', 't11584')
+
+        browser_url = self._tester.url + '/browser/t11584'
+        tc.go(browser_url)
+        tc.url(browser_url)
+        tc.notfind('Error: No such changeset')
+
+
+class RegressionTestTicket11618(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11618
+        fix for malformed `readonly="True"` attribute in repository admin.
+        """
+        env = self._testenv.get_trac_environment()
+        env.config.set('repositories', 't11618.dir',
+                       self._testenv.repo_path_for_initenv())
+        env.config.save()
+        try:
+            self._tester.go_to_admin()
+            tc.follow(r'\bRepositories\b')
+            tc.url(self._tester.url + '/admin/versioncontrol/repository')
+            tc.follow(r'\bt11618\b')
+            tc.url(self._tester.url + '/admin/versioncontrol/repository/t11618')
+            tc.notfind(' readonly="True"')
+            tc.find(' readonly="readonly"')
+        finally:
+            env.config.remove('repositories', 't11618.dir')
+            env.config.save()
+
+
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
+    suite.addTest(TestAdminRepositoryAuthorization())
+    suite.addTest(RegressionTestTicket11355())
     if has_svn:
         suite.addTest(TestEmptySvnRepo())
         suite.addTest(TestRepoCreation())
         suite.addTest(TestRepoBrowse())
         suite.addTest(TestNewFileLog())
         suite.addTest(RegressionTestTicket5819())
+        suite.addTest(RegressionTestTicket11186())
+        suite.addTest(RegressionTestTicket11186Alias())
+        suite.addTest(RegressionTestTicket11194())
+        suite.addTest(RegressionTestTicket11346())
+        suite.addTest(RegressionTestTicket11438())
+        suite.addTest(RegressionTestTicket11584())
+        suite.addTest(RegressionTestTicket11618())
         suite.addTest(RegressionTestRev5877())
     else:
         print "SKIP: versioncontrol/tests/functional.py (no svn bindings)"
diff --git a/trac/trac/versioncontrol/tests/svn_authz.py b/trac/trac/versioncontrol/tests/svn_authz.py
index 4ee5d46..f98b3b3 100644
--- a/trac/trac/versioncontrol/tests/svn_authz.py
+++ b/trac/trac/versioncontrol/tests/svn_authz.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -390,12 +390,10 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(AuthzParserTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(AuthzSourcePolicyTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(AuthzParserTestCase))
+    suite.addTest(unittest.makeSuite(AuthzSourcePolicyTestCase))
     return suite
 
 
 if __name__ == '__main__':
-    runner = unittest.TextTestRunner()
-    runner.run(suite())
-
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/versioncontrol/web_ui/__init__.py b/trac/trac/versioncontrol/web_ui/__init__.py
index 7038764..25df3bd 100644
--- a/trac/trac/versioncontrol/web_ui/__init__.py
+++ b/trac/trac/versioncontrol/web_ui/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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 trac.versioncontrol.web_ui.browser import *
 from trac.versioncontrol.web_ui.changeset import *
 from trac.versioncontrol.web_ui.log import *
diff --git a/trac/trac/versioncontrol/web_ui/browser.py b/trac/trac/versioncontrol/web_ui/browser.py
index 88fd8c6..4d2d067 100644
--- a/trac/trac/versioncontrol/web_ui/browser.py
+++ b/trac/trac/versioncontrol/web_ui/browser.py
@@ -24,15 +24,14 @@
 from trac.config import ListOption, BoolOption, Option
 from trac.core import *
 from trac.mimeview.api import IHTMLPreviewAnnotator, Mimeview, is_binary
-from trac.perm import IPermissionRequestor
+from trac.perm import IPermissionRequestor, PermissionError
 from trac.resource import Resource, ResourceNotFound
 from trac.util import as_bool, embedded_numbers
-from trac.util.compat import cleandoc
 from trac.util.datefmt import http_date, to_datetime, utc
 from trac.util.html import escape, Markup
 from trac.util.text import exception_to_unicode, shorten_line
 from trac.util.translation import _, cleandoc_
-from trac.web import IRequestHandler, RequestDone
+from trac.web.api import IRequestHandler, RequestDone
 from trac.web.chrome import (INavigationContributor, add_ctxtnav, add_link,
                              add_script, add_stylesheet, prevnext_nav,
                              web_context)
@@ -296,7 +295,8 @@
 
     def get_navigation_items(self, req):
         rm = RepositoryManager(self.env)
-        if 'BROWSER_VIEW' in req.perm and rm.get_real_repositories():
+        if any(repos.is_viewable(req.perm) for repos
+                                           in rm.get_real_repositories()):
             yield ('mainnav', 'browser',
                    tag.a(_('Browse Source'), href=req.href.browser()))
 
@@ -327,8 +327,6 @@
             return True
 
     def process_request(self, req):
-        req.perm.require('BROWSER_VIEW')
-
         presel = req.args.get('preselected')
         if presel and (presel + '/').startswith(req.href.browser() + '/'):
             req.redirect(presel)
@@ -337,8 +335,9 @@
         rev = req.args.get('rev', '')
         if rev.lower() in ('', 'head'):
             rev = None
+        format = req.args.get('format')
         order = req.args.get('order', 'name').lower()
-        desc = req.args.has_key('desc')
+        desc = 'desc' in req.args
         xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest'
 
         rm = RepositoryManager(self.env)
@@ -365,6 +364,7 @@
         # Find node for the requested path/rev
         context = web_context(req)
         node = None
+        changeset = None
         display_rev = lambda rev: rev
         if repos:
             try:
@@ -377,6 +377,12 @@
             except NoSuchChangeset, e:
                 raise ResourceNotFound(e.message,
                                        _('Invalid changeset number'))
+            if node:
+                try:
+                    # use changeset instance to retrieve branches and tags
+                    changeset = repos.get_changeset(node.rev)
+                except NoSuchChangeset:
+                    pass
 
             context = context.child(repos.resource.child('source', path,
                                                    version=rev_or_latest))
@@ -391,13 +397,25 @@
             repo_data = self._render_repository_index(
                                         context, all_repositories, order, desc)
         if node:
+            if not node.is_viewable(req.perm):
+                raise PermissionError('BROWSER_VIEW' if node.isdir else
+                                      'FILE_VIEW', node.resource, self.env)
             if node.isdir:
+                if format in ('zip',): # extension point here...
+                    self._render_zip(req, context, repos, node, rev)
+                    # not reached
                 dir_data = self._render_dir(req, repos, node, rev, order, desc)
             elif node.isfile:
                 file_data = self._render_file(req, context, repos, node, rev)
 
         if not repos and not (repo_data and repo_data['repositories']):
-            raise ResourceNotFound(_("No node %(path)s", path=path))
+            # If no viewable repositories, check permission instead of
+            # repos.is_viewable()
+            req.perm.require('BROWSER_VIEW')
+            if show_index:
+                raise ResourceNotFound(_("No viewable repositories"))
+            else:
+                raise ResourceNotFound(_("No node %(path)s", path=path))
 
         quickjump_data = properties_data = None
         if node and not xhr:
@@ -409,7 +427,7 @@
             'context': context, 'reponame': reponame, 'repos': repos,
             'repoinfo': all_repositories.get(reponame or ''),
             'path': path, 'rev': node and node.rev, 'stickyrev': rev,
-            'display_rev': display_rev,
+            'display_rev': display_rev, 'changeset': changeset,
             'created_path': node and node.created_path,
             'created_rev': node and node.created_rev,
             'properties': properties_data,
@@ -500,6 +518,10 @@
                 continue
             try:
                 repos = rm.get_repository(reponame)
+            except TracError, err:
+                entry = (reponame, repoinfo, None, None,
+                         exception_to_unicode(err), None)
+            else:
                 if repos:
                     if not repos.is_viewable(context.perm):
                         continue
@@ -518,10 +540,7 @@
                              raw_href)
                 else:
                     entry = (reponame, repoinfo, None, None, u"\u2013", None)
-            except TracError, err:
-                entry = (reponame, repoinfo, None, None,
-                         exception_to_unicode(err), None)
-            if entry[-1] is not None:   # Check permission in case of error
+            if entry[4] is not None:  # Check permission in case of error
                 root = Resource('repository', reponame).child('source', '/')
                 if 'BROWSER_VIEW' not in context.perm(root):
                     continue
@@ -620,13 +639,35 @@
                                    timerange.to_seconds(timerange.oldest)),
                 }
 
+    def _iter_nodes(self, node):
+        stack = [node]
+        while stack:
+            node = stack.pop()
+            yield node
+            if node.isdir:
+                stack.extend(sorted(node.get_entries(),
+                                    key=lambda x: x.name,
+                                    reverse=True))
+
+    def _render_zip(self, req, context, repos, root_node, rev=None):
+        if not self.is_path_downloadable(repos, root_node.path):
+            raise TracError(_("Path not available for download"))
+        req.perm(context.resource).require('FILE_VIEW')
+        root_path = root_node.path.rstrip('/')
+        if root_path:
+            archive_name = root_node.name
+        else:
+            archive_name = repos.reponame or 'repository'
+        filename = '%s-%s.zip' % (archive_name, root_node.rev)
+        render_zip(req, filename, repos, root_node, self._iter_nodes)
+
     def _render_file(self, req, context, repos, node, rev=None):
         req.perm(node.resource).require('FILE_VIEW')
 
         mimeview = Mimeview(self.env)
 
         # MIME type detection
-        content = node.get_content()
+        content = node.get_processed_content()
         chunk = content.read(CHUNK_SIZE)
         mime_type = node.content_type
         if not mime_type or mime_type == 'application/octet-stream':
@@ -639,7 +680,6 @@
             req.send_response(200)
             req.send_header('Content-Type',
                             'text/plain' if format == 'txt' else mime_type)
-            req.send_header('Content-Length', node.content_length)
             req.send_header('Last-Modified', http_date(node.last_modified))
             if rev is None:
                 req.send_header('Pragma', 'no-cache')
@@ -652,11 +692,12 @@
                 req.send_header('Content-Disposition', 'attachment')
             req.end_headers()
 
-            while 1:
-                if not chunk:
-                    raise RequestDone
-                req.write(chunk)
-                chunk = content.read(CHUNK_SIZE)
+            def chunks():
+                c = chunk
+                while c:
+                    yield c
+                    c = content.read(CHUNK_SIZE)
+            raise RequestDone(chunks())
         else:
             # The changeset corresponding to the last change on `node`
             # is more interesting than the `rev` changeset.
@@ -678,7 +719,7 @@
             self.log.debug("Rendering preview of node %s@%s with mime-type %s"
                            % (node.name, str(rev), mime_type))
 
-            del content # the remainder of that content is not needed
+            content = None # the remainder of that content is not needed
 
             add_stylesheet(req, 'common/css/code.css')
 
@@ -686,7 +727,8 @@
             annotate = req.args.get('annotate')
             if annotate:
                 annotations.insert(0, annotate)
-            preview_data = mimeview.preview_data(context, node.get_content(),
+            preview_data = mimeview.preview_data(context,
+                                                 node.get_processed_content(),
                                                  node.get_content_length(),
                                                  mime_type, node.created_path,
                                                  raw_href,
@@ -704,18 +746,19 @@
         if node is not None and node.isfile:
             return href.export(rev or 'HEAD', repos.reponame or None,
                                node.path)
-        path = npath = '' if node is None else node.path.strip('/')
-        if repos.reponame:
-            path = (repos.reponame + '/' + npath).rstrip('/')
-        if any(fnmatchcase(path, p.strip('/'))
-               for p in self.downloadable_paths):
-            return href.changeset(rev or repos.youngest_rev,
-                                  repos.reponame or None, npath,
-                                  old=rev, old_path=repos.reponame or '/',
-                                  format='zip')
+        path = '' if node is None else node.path.strip('/')
+        if self.is_path_downloadable(repos, path):
+            return href.browser(repos.reponame or None, path,
+                                rev=rev or repos.youngest_rev, format='zip')
 
     # public methods
 
+    def is_path_downloadable(self, repos, path):
+        if repos.reponame:
+            path = repos.reponame + '/' + path
+        return any(fnmatchcase(path, dp.strip('/'))
+                   for dp in self.downloadable_paths)
+
     def render_properties(self, mode, context, props):
         """Prepare rendering of a collection of properties."""
         return filter(None, [self.render_property(name, mode, context, props)
diff --git a/trac/trac/versioncontrol/web_ui/changeset.py b/trac/trac/versioncontrol/web_ui/changeset.py
index e33bd1f..097fe81 100644
--- a/trac/trac/versioncontrol/web_ui/changeset.py
+++ b/trac/trac/versioncontrol/web_ui/changeset.py
@@ -20,6 +20,7 @@
 
 from __future__ import with_statement
 
+from functools import partial
 from itertools import groupby
 import os
 import posixpath
@@ -39,12 +40,13 @@
 from trac.util.datefmt import from_utimestamp, pretty_timedelta
 from trac.util.text import exception_to_unicode, to_unicode, \
                            unicode_urlencode, shorten_line, CRLF
-from trac.util.translation import _, ngettext
+from trac.util.translation import _, ngettext, tag_
 from trac.versioncontrol.api import RepositoryManager, Changeset, Node, \
                                     NoSuchChangeset
 from trac.versioncontrol.diff import get_diff_options, diff_blocks, \
                                      unified_diff
 from trac.versioncontrol.web_ui.browser import BrowserModule
+from trac.versioncontrol.web_ui.util import render_zip
 from trac.web import IRequestHandler, RequestDone
 from trac.web.chrome import (Chrome, INavigationContributor, add_ctxtnav,
                              add_link, add_script, add_stylesheet,
@@ -100,7 +102,7 @@
         unidiff = '--- \n+++ \n' + \
                   '\n'.join(unified_diff(old.splitlines(), new.splitlines(),
                                          options.get('contextlines', 3)))
-        return tag.li('Property ', tag.strong(name),
+        return tag.li(tag_("Property %(name)s", name=tag.strong(name)),
                       Mimeview(self.env).render(old_context, 'text/x-diff',
                                                 unidiff))
 
@@ -213,9 +215,9 @@
         req.perm.require('CHANGESET_VIEW')
 
         # -- retrieve arguments
-        full_new_path = new_path = req.args.get('new_path')
+        new_path = req.args.get('new_path')
         new = req.args.get('new')
-        full_old_path = old_path = req.args.get('old_path')
+        old_path = req.args.get('old_path')
         old = req.args.get('old')
         reponame = req.args.get('reponame')
 
@@ -252,14 +254,14 @@
 
         # -- normalize and check for special case
         try:
-            new_path = repos.normalize_path(new_path)
             new = repos.normalize_rev(new)
-            full_new_path = '/' + pathjoin(repos.reponame, new_path)
-            old_path = repos.normalize_path(old_path or new_path)
             old = repos.normalize_rev(old or new)
-            full_old_path = '/' + pathjoin(repos.reponame, old_path)
         except NoSuchChangeset, e:
-            raise ResourceNotFound(e.message, _('Invalid Changeset Number'))
+            raise ResourceNotFound(e.message, _("Invalid Changeset Number"))
+        new_path = repos.normalize_path(new_path)
+        old_path = repos.normalize_path(old_path or new_path)
+        full_new_path = '/' + pathjoin(repos.reponame, new_path)
+        full_old_path = '/' + pathjoin(repos.reponame, old_path)
 
         if old_path == new_path and old == new: # revert to Changeset
             old_path = old = None
@@ -268,7 +270,7 @@
         diff_opts = diff_data['options']
 
         # -- setup the `chgset` and `restricted` flags, see docstring above.
-        chgset = not old and not old_path
+        chgset = not old and old_path is None
         if chgset:
             restricted = new_path not in ('', '/') # (subset or not)
         else:
@@ -305,7 +307,7 @@
                 new = repos.youngest_rev
             elif not old:
                 old = repos.youngest_rev
-            if not old_path:
+            if old_path is None:
                 old_path = new_path
             data = {'old_path': old_path, 'old_rev': old,
                     'new_path': new_path, 'new_rev': new}
@@ -339,15 +341,14 @@
                 if restricted:
                     filename = 'diff-%s-from-%s-to-%s' \
                                   % (rpath, old, new)
-                elif old_path == '/': # special case for download (#238)
-                    filename = '%s-%s' % (rpath, old)
                 else:
                     filename = 'diff-from-%s-%s-to-%s-%s' \
                                % (old_path.replace('/','_'), old, rpath, new)
             if format == 'diff':
                 self._render_diff(req, filename, repos, data)
             elif format == 'zip':
-                self._render_zip(req, filename, repos, data)
+                render_zip(req, filename + '.zip', repos, None,
+                           partial(self._zip_iter_nodes, req, repos, data))
 
         # -- HTML format
         self._render_html(req, repos, chgset, restricted, xhr, data)
@@ -753,47 +754,16 @@
         req.write(diff_str)
         raise RequestDone
 
-    def _render_zip(self, req, filename, repos, data):
-        """ZIP archive containing all the added and/or modified files."""
-        req.send_response(200)
-        req.send_header('Content-Type', 'application/zip')
-        req.send_header('Content-Disposition',
-                        content_disposition('attachment', filename + '.zip'))
-
-        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED as compression
-
-        buf = StringIO()
-        zipfile = ZipFile(buf, 'w', compression)
+    def _zip_iter_nodes(self, req, repos, data, root_node):
+        """Node iterator yielding all the added and/or modified files."""
         for old_node, new_node, kind, change in repos.get_changes(
             new_path=data['new_path'], new_rev=data['new_rev'],
             old_path=data['old_path'], old_rev=data['old_rev']):
             if (kind == Node.FILE or kind == Node.DIRECTORY) and \
                     change != Changeset.DELETE \
                     and new_node.is_viewable(req.perm):
-                zipinfo = ZipInfo()
-                # Note: unicode filenames are not supported by zipfile.
-                # UTF-8 is not supported by all Zip tools either,
-                # but as some do, UTF-8 is the best option here.
-                zipinfo.filename = new_node.path.strip('/').encode('utf-8')
-                zipinfo.flag_bits |= 0x800 # filename is encoded with utf-8
-                zipinfo.date_time = new_node.last_modified.utctimetuple()[:6]
-                zipinfo.compress_type = compression
-                # setting zipinfo.external_attr is needed since Python 2.5
-                if new_node.isfile:
-                    zipinfo.external_attr = 0644 << 16L
-                    content = new_node.get_content().read()
-                elif new_node.isdir:
-                    zipinfo.filename += '/'
-                    zipinfo.external_attr = 040755 << 16L
-                    content = ''
-                zipfile.writestr(zipinfo, content)
-        zipfile.close()
+                yield new_node
 
-        zip_str = buf.getvalue()
-        req.send_header("Content-Length", len(zip_str))
-        req.end_headers()
-        req.write(zip_str)
-        raise RequestDone
 
     def title_for_diff(self, data):
         # TRANSLATOR: 'latest' (revision)
@@ -1205,7 +1175,7 @@
                                if repos.is_viewable(req.perm))
 
             elem = tag.ul(
-                [tag.li(tag.b(path) if isdir else path)
+                [tag.li(tag.strong(path) if isdir else path)
                  for (isdir, name, path) in sorted(entries, key=kind_order)
                  if name.lower().startswith(prefix)])
 
diff --git a/trac/trac/versioncontrol/web_ui/log.py b/trac/trac/versioncontrol/web_ui/log.py
index 56d3040..9e4d3cc 100644
--- a/trac/trac/versioncontrol/web_ui/log.py
+++ b/trac/trac/versioncontrol/web_ui/log.py
@@ -28,8 +28,7 @@
 from trac.util import Ranges
 from trac.util.text import to_unicode, wrap
 from trac.util.translation import _
-from trac.versioncontrol.api import (RepositoryManager, Changeset,
-                                     NoSuchChangeset)
+from trac.versioncontrol.api import Changeset, RepositoryManager
 from trac.versioncontrol.web_ui.changeset import ChangesetModule
 from trac.versioncontrol.web_ui.util import *
 from trac.web import IRequestHandler
@@ -90,8 +89,12 @@
         reponame, repos, path = rm.get_repository_by_path(path)
 
         if not repos:
-            raise ResourceNotFound(_("Repository '%(repo)s' not found",
-                                   repo=reponame))
+            if path == '/':
+                raise TracError(_("No repository specified and no default"
+                                  " repository configured."))
+            else:
+                raise ResourceNotFound(_("Repository '%(repo)s' not found",
+                                         repo=reponame or path.strip('/')))
 
         if reponame != repos.reponame:  # Redirect alias
             qs = req.query_string
@@ -106,11 +109,11 @@
         revranges = None
         if revs:
             try:
-                revranges = Ranges(revs)
+                revranges = self._normalize_ranges(repos, path, revs)
                 rev = revranges.b
             except ValueError:
                 pass
-        rev = unicode(repos.normalize_rev(rev))
+        rev = repos.normalize_rev(rev)
         display_rev = repos.display_rev
 
         # The `history()` method depends on the mode:
@@ -139,25 +142,27 @@
                         node_history = list(node.get_history(2))
                         p, rev, chg = node_history[0]
                         if repos.rev_older_than(rev, a):
-                            break # simply skip, no separator
+                            break  # simply skip, no separator
                         if 'CHANGESET_VIEW' in req.perm(cset_resource(id=rev)):
                             if expected_next_item:
                                 # check whether we're continuing previous range
                                 np, nrev, nchg = expected_next_item
-                                if rev != nrev: # no, we need a separator
+                                if rev != nrev:  # no, we need a separator
                                     yield (np, nrev, None)
                             yield node_history[0]
-                        prevpath = node_history[-1][0] # follow copy
-                        b = repos.previous_rev(rev)
                         if len(node_history) > 1:
                             expected_next_item = node_history[-1]
+                            prevpath = expected_next_item[0]  # follow copy
+                            b = expected_next_item[1]
                         else:
                             expected_next_item = None
+                            break  # no more older revisions
                 if expected_next_item:
                     yield (expected_next_item[0], expected_next_item[1], None)
         else:
             show_graph = path == '/' and not verbose \
                          and not repos.has_linear_changesets
+
             def history():
                 node = get_existing_node(req, repos, path, rev)
                 for h in node.get_history():
@@ -192,7 +197,7 @@
                     break
                 elif mode == 'path_history':
                     depth -= 1
-            if old_chg is None: # separator entry
+            if old_chg is None:  # separator entry
                 stop_limit = limit
             else:
                 count += 1
@@ -200,13 +205,15 @@
             if count >= stop_limit:
                 break
             previous_path = old_path
-        if info == []:
+        if not info:
             node = get_existing_node(req, repos, path, rev)
             if repos.rev_older_than(stop_rev, node.created_rev):
                 # FIXME: we should send a 404 error here
                 raise TracError(_("The file or directory '%(path)s' doesn't "
-                    "exist at revision %(rev)s or at any previous revision.",
-                    path=path, rev=display_rev(rev)), _('Nonexistent path'))
+                                  "exist at revision %(rev)s or at any "
+                                  "previous revision.", path=path,
+                                  rev=display_rev(rev)),
+                                _('Nonexistent path'))
 
         # Generate graph data
         graph = {}
@@ -231,7 +238,7 @@
             return req.href.log(repos.reponame or None, path, **params)
 
         if format in ('rss', 'changelog'):
-            info = [i for i in info if i['change']] # drop separators
+            info = [i for i in info if i['change']]  # drop separators
             if info and count > limit:
                 del info[-1]
         elif info and count >= limit:
@@ -245,8 +252,9 @@
                 older_revisions_href = make_log_href(next_path, rev=next_rev,
                                                      revs=next_revranges)
                 add_link(req, 'next', older_revisions_href,
-                    _('Revision Log (restarting at %(path)s, rev. %(rev)s)',
-                    path=next_path, rev=display_rev(next_rev)))
+                         _('Revision Log (restarting at %(path)s, rev. '
+                           '%(rev)s)', path=next_path,
+                           rev=display_rev(next_rev)))
             # only show fully 'limit' results, use `change == None` as a marker
             info[-1]['change'] = None
 
@@ -275,11 +283,11 @@
             'reponame': repos.reponame or None, 'repos': repos,
             'path': path, 'rev': rev, 'stop_rev': stop_rev,
             'display_rev': display_rev, 'revranges': revranges,
-            'mode': mode, 'verbose': verbose, 'limit' : limit,
+            'mode': mode, 'verbose': verbose, 'limit': limit,
             'items': info, 'changes': changes, 'extra_changes': extra_changes,
             'graph': graph,
-            'wiki_format_messages':
-            self.config['changeset'].getbool('wiki_format_messages')
+            'wiki_format_messages': self.config['changeset']
+                                    .getbool('wiki_format_messages')
         }
 
         if format == 'changelog':
@@ -294,8 +302,8 @@
         item_ranges = []
         range = []
         for item in info:
-            if item['change'] is None: # separator
-                if range: # start new range
+            if item['change'] is None:  # separator
+                if range:  # start new range
                     range.append(item)
                     item_ranges.append(range)
                     range = []
@@ -320,7 +328,8 @@
                  'application/rss+xml', 'rss')
         changelog_href = make_log_href(path, format='changelog', revs=revs,
                                        stop_rev=stop_rev)
-        add_link(req, 'alternate', changelog_href, _('ChangeLog'), 'text/plain')
+        add_link(req, 'alternate', changelog_href, _('ChangeLog'),
+                 'text/plain')
 
         add_ctxtnav(req, _('View Latest Revision'),
                     href=req.href.browser(repos.reponame or None, path))
@@ -388,23 +397,27 @@
                     repos = rm.get_repository(reponame)
 
             if repos:
-                revranges = None
-                if any(c for c in ':-,' if c in revs):
-                    revranges = self._normalize_ranges(repos, path, revs)
-                    revs = None
                 if 'LOG_VIEW' in formatter.perm:
+                    revranges = None
+                    if any(c in revs for c in ':-,'):
+                        try:
+                            # try to parse into integer rev ranges
+                            revranges = Ranges(revs.replace(':', '-'),
+                                               reorder=True)
+                            revs = str(revranges)
+                        except ValueError:
+                            revranges = self._normalize_ranges(repos, path,
+                                                               revs)
                     if revranges:
                         href = formatter.href.log(repos.reponame or None,
                                                   path or '/',
-                                                  revs=str(revranges))
+                                                  revs=revs)
                     else:
-                        try:
-                            rev = repos.normalize_rev(revs)
-                        except NoSuchChangeset:
-                            rev = None
+                        repos.normalize_rev(revs)  # verify revision
                         href = formatter.href.log(repos.reponame or None,
-                                                  path or '/', rev=rev)
-                    if query and (revranges or revs):
+                                                  path or '/',
+                                                  rev=revs or None)
+                    if query and '?' in href:
                         query = '&' + query[1:]
                     return tag.a(label, class_='source',
                                  href=href + query + fragment)
@@ -420,17 +433,22 @@
     LOG_LINK_RE = re.compile(r"([^@:]*)[@:]%s?" % REV_RANGE)
 
     def _normalize_ranges(self, repos, path, revs):
-        ranges = revs.replace(':', '-')
         try:
             # fast path; only numbers
-            return Ranges(ranges, reorder=True)
+            return Ranges(revs.replace(':', '-'), reorder=True)
         except ValueError:
             # slow path, normalize each rev
-            splitted_ranges = re.split(r'([-,])', ranges)
+            ranges = []
+            for range in revs.split(','):
+                try:
+                    a, b = range.replace(':', '-').split('-')
+                    range = (a, b)
+                except ValueError:
+                    range = (range,)
+                ranges.append('-'.join(str(repos.normalize_rev(r))
+                                       for r in range))
+            ranges = ','.join(ranges)
             try:
-                revs = [repos.normalize_rev(r) for r in splitted_ranges[::2]]
-            except NoSuchChangeset:
+                return Ranges(ranges)
+            except ValueError:
                 return None
-            seps = splitted_ranges[1::2] + ['']
-            ranges = ''.join([str(rev)+sep for rev, sep in zip(revs, seps)])
-            return Ranges(ranges)
diff --git a/trac/trac/versioncontrol/web_ui/tests/__init__.py b/trac/trac/versioncontrol/web_ui/tests/__init__.py
index 196c3b6..3d7da1a 100644
--- a/trac/trac/versioncontrol/web_ui/tests/__init__.py
+++ b/trac/trac/versioncontrol/web_ui/tests/__init__.py
@@ -1,9 +1,27 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2014 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/.
+
 import unittest
 
-from trac.versioncontrol.web_ui.tests import wikisyntax
+from trac.versioncontrol.web_ui.tests import browser, changeset, log, \
+                                             wikisyntax
+
 
 def suite():
     suite = unittest.TestSuite()
+    suite.addTest(browser.suite())
+    suite.addTest(changeset.suite())
+    suite.addTest(log.suite())
     suite.addTest(wikisyntax.suite())
     return suite
 
diff --git a/trac/trac/versioncontrol/web_ui/tests/browser.py b/trac/trac/versioncontrol/web_ui/tests/browser.py
new file mode 100644
index 0000000..d0beec9
--- /dev/null
+++ b/trac/trac/versioncontrol/web_ui/tests/browser.py
@@ -0,0 +1,379 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 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/.
+
+import unittest
+from datetime import datetime
+from cStringIO import StringIO
+
+import trac.tests.compat
+from trac.core import Component, TracError, implements
+from trac.perm import PermissionError
+from trac.resource import ResourceNotFound
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.util.datefmt import utc
+from trac.versioncontrol.api import (
+    Changeset, DbRepositoryProvider, IRepositoryConnector, Node, NoSuchNode,
+    Repository, RepositoryManager)
+from trac.versioncontrol.web_ui.browser import BrowserModule
+from trac.web.tests.api import RequestHandlerPermissionsTestCaseBase
+from tracopt.perm.authz_policy import ConfigObj
+
+
+class MockRepositoryConnector(Component):
+
+    implements(IRepositoryConnector)
+
+    def get_supported_types(self):
+        yield 'mock', 8
+
+    def get_repository(self, repos_type, repos_dir, params):
+        def get_changeset(rev):
+            return Mock(Changeset, repos, rev, 'message', 'author',
+                        datetime(2001, 1, 1, tzinfo=utc))
+
+        def get_node(path, rev):
+            if 'missing' in path:
+                raise NoSuchNode(path, rev)
+            kind = Node.FILE if 'file' in path else Node.DIRECTORY
+            node = Mock(Node, repos, path, rev, kind,
+                        created_path=path, created_rev=rev,
+                        get_entries=lambda: iter([]),
+                        get_properties=lambda: {},
+                        get_content=lambda: StringIO('content'),
+                        get_content_length=lambda: 7,
+                        get_content_type=lambda: 'application/octet-stream')
+            return node
+
+        if params['name'] == 'raise':
+            raise TracError("")
+        else:
+            repos = Mock(Repository, params['name'], params, self.log,
+                         get_youngest_rev=lambda: 1,
+                         get_changeset=get_changeset,
+                         get_node=get_node,
+                         previous_rev=lambda rev, path='': None,
+                         next_rev=lambda rev, path='': None)
+        return repos
+
+
+class BrowserModulePermissionsTestCase(RequestHandlerPermissionsTestCaseBase):
+
+    authz_policy = """\
+[repository:*allow*@*/source:*deny*]
+anonymous = !BROWSER_VIEW, !FILE_VIEW
+
+[repository:*deny*@*/source:*allow*]
+anonymous = BROWSER_VIEW, FILE_VIEW
+
+[repository:*allow*@*]
+anonymous = BROWSER_VIEW, FILE_VIEW
+
+[repository:*deny*@*]
+anonymous = !BROWSER_VIEW, !FILE_VIEW
+
+"""
+
+    def setUp(self):
+        super(BrowserModulePermissionsTestCase, self).setUp(BrowserModule)
+        provider = DbRepositoryProvider(self.env)
+        provider.add_repository('(default)', '/', 'mock')
+        provider.add_repository('allow', '/', 'mock')
+        provider.add_repository('deny', '/', 'mock')
+        provider.add_repository('raise', '/', 'mock')
+
+    def tearDown(self):
+        RepositoryManager(self.env).reload_repositories()
+        super(BrowserModulePermissionsTestCase, self).tearDown()
+
+    def test_get_navigation_items_with_browser_view(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW')
+        provider = DbRepositoryProvider(self.env)
+        req = self.create_request(path_info='/')
+        self.assertEqual('browser', self.get_navigation_items(req).next()[1])
+
+        provider.remove_repository('allow')
+        self.assertEqual('browser', self.get_navigation_items(req).next()[1])
+
+        provider.remove_repository('deny')
+        self.assertEqual('browser', self.get_navigation_items(req).next()[1])
+
+        provider.remove_repository('(default)')
+        self.assertEqual([], list(self.get_navigation_items(req)))
+
+    def test_get_navigation_items_without_browser_view(self):
+        provider = DbRepositoryProvider(self.env)
+        req = self.create_request(path_info='/')
+        self.assertEqual('browser', self.get_navigation_items(req).next()[1])
+
+        provider.remove_repository('(default)')
+        self.assertEqual('browser', self.get_navigation_items(req).next()[1])
+
+        provider.remove_repository('deny')
+        self.assertEqual('browser', self.get_navigation_items(req).next()[1])
+
+        provider.remove_repository('allow')
+        self.assertEqual([], list(self.get_navigation_items(req)))
+
+    def test_repository_with_browser_view(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW')
+
+        req = self.create_request(path_info='/browser/')
+        rv = self.process_request(req)
+        self.assertEqual('', rv[1]['repos'].name)
+
+        req = self.create_request(path_info='/browser/allow')
+        rv = self.process_request(req)
+        self.assertEqual('allow', rv[1]['repos'].name)
+
+        req = self.create_request(path_info='/browser/deny')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('BROWSER_VIEW', e.action)
+            self.assertEqual('source', e.resource.realm)
+            self.assertEqual('/', e.resource.id)
+            self.assertEqual('repository', e.resource.parent.realm)
+            self.assertEqual('deny', e.resource.parent.id)
+
+        DbRepositoryProvider(self.env).remove_repository('(default)')
+        req = self.create_request(path_info='/browser/')
+        rv = self.process_request(req)
+        self.assertEqual(None, rv[1]['repos'])
+
+        req = self.create_request(path_info='/browser/blah-blah-file')
+        try:
+            self.process_request(req)
+            self.fail('ResourceNotFound not raised')
+        except ResourceNotFound, e:
+            self.assertEqual('No node blah-blah-file', unicode(e))
+
+    def test_repository_without_browser_view(self):
+        req = self.create_request(path_info='/browser/')
+        rv = self.process_request(req)
+        # cannot view default repository but don't raise PermissionError
+        self.assertEqual(None, rv[1]['repos'])
+
+        req = self.create_request(path_info='/browser/allow')
+        rv = self.process_request(req)
+        self.assertEqual('allow', rv[1]['repos'].name)
+
+        req = self.create_request(path_info='/browser/deny')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('BROWSER_VIEW', e.action)
+            self.assertEqual('source', e.resource.realm)
+            self.assertEqual('/', e.resource.id)
+            self.assertEqual('repository', e.resource.parent.realm)
+            self.assertEqual('deny', e.resource.parent.id)
+
+        DbRepositoryProvider(self.env).remove_repository('(default)')
+        req = self.create_request(path_info='/browser/')
+        rv = self.process_request(req)
+        self.assertEqual(None, rv[1]['repos'])
+
+        req = self.create_request(path_info='/browser/blah-blah-file')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('BROWSER_VIEW', e.action)
+            self.assertEqual(None, e.resource)
+
+    def test_node_with_file_view(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW', 'FILE_VIEW')
+
+        req = self.create_request(path_info='/browser/file')
+        rv = self.process_request(req)
+        self.assertEqual('', rv[1]['repos'].name)
+        self.assertEqual('file', rv[1]['path'])
+
+        req = self.create_request(path_info='/browser/allow-file')
+        rv = self.process_request(req)
+        self.assertEqual('', rv[1]['repos'].name)
+        self.assertEqual('allow-file', rv[1]['path'])
+
+        req = self.create_request(path_info='/browser/deny-file')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('FILE_VIEW', e.action)
+            self.assertEqual('source', e.resource.realm)
+            self.assertEqual('deny-file', e.resource.id)
+            self.assertEqual('repository', e.resource.parent.realm)
+            self.assertEqual('', e.resource.parent.id)
+
+    def test_node_in_allowed_repos_with_file_view(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW', 'FILE_VIEW')
+
+        req = self.create_request(path_info='/browser/allow/file')
+        rv = self.process_request(req)
+        self.assertEqual('allow', rv[1]['repos'].name)
+        self.assertEqual('file', rv[1]['path'])
+
+        req = self.create_request(path_info='/browser/allow/allow-file')
+        rv = self.process_request(req)
+        self.assertEqual('allow', rv[1]['repos'].name)
+        self.assertEqual('allow-file', rv[1]['path'])
+
+        req = self.create_request(path_info='/browser/allow/deny-file')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('FILE_VIEW', e.action)
+            self.assertEqual('source', e.resource.realm)
+            self.assertEqual('deny-file', e.resource.id)
+            self.assertEqual('repository', e.resource.parent.realm)
+            self.assertEqual('allow', e.resource.parent.id)
+
+    def test_node_in_denied_repos_with_file_view(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW', 'FILE_VIEW')
+
+        req = self.create_request(path_info='/browser/deny/allow-file')
+        rv = self.process_request(req)
+        self.assertEqual('deny', rv[1]['repos'].name)
+        self.assertEqual('allow-file', rv[1]['path'])
+
+        for path in ('file', 'deny-file'):
+            req = self.create_request(path_info='/browser/deny/' + path)
+            try:
+                self.process_request(req)
+                self.fail('PermissionError not raised (path: %r)' % path)
+            except PermissionError, e:
+                self.assertEqual('FILE_VIEW', e.action)
+                self.assertEqual('source', e.resource.realm)
+                self.assertEqual(path, e.resource.id)
+                self.assertEqual('repository', e.resource.parent.realm)
+                self.assertEqual('deny', e.resource.parent.id)
+
+    def test_missing_node_with_browser_view(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW')
+        req = self.create_request(path_info='/browser/allow/missing')
+        self.assertRaises(ResourceNotFound, self.process_request, req)
+        req = self.create_request(path_info='/browser/deny/missing')
+        self.assertRaises(ResourceNotFound, self.process_request, req)
+        req = self.create_request(path_info='/browser/missing')
+        self.assertRaises(ResourceNotFound, self.process_request, req)
+
+    def test_missing_node_without_browser_view(self):
+        req = self.create_request(path_info='/browser/allow/missing')
+        self.assertRaises(ResourceNotFound, self.process_request, req)
+        req = self.create_request(path_info='/browser/deny/missing')
+        self.assertRaises(ResourceNotFound, self.process_request, req)
+        req = self.create_request(path_info='/browser/missing')
+        self.assertRaises(ResourceNotFound, self.process_request, req)
+
+    def test_repository_index_with_hidden_default_repos(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW', 'FILE_VIEW')
+        provider = DbRepositoryProvider(self.env)
+        provider.modify_repository('(default)', {'hidden': 'enabled'})
+        req = self.create_request(path_info='/browser/')
+        template, data, content_type = self.process_request(req)
+        self.assertEqual(None, data['repos'])
+        repo_data = data['repo']  # for repository index
+        self.assertEqual('allow', repo_data['repositories'][0][0])
+        self.assertEqual('raise', repo_data['repositories'][1][0])
+        self.assertEqual(2, len(repo_data['repositories']))
+
+    def test_node_in_hidden_default_repos(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW', 'FILE_VIEW')
+        provider = DbRepositoryProvider(self.env)
+        provider.modify_repository('(default)', {'hidden': 'enabled'})
+        req = self.create_request(path_info='/browser/blah-blah-file')
+        template, data, content_type = self.process_request(req)
+        self.assertEqual('', data['reponame'])
+        self.assertEqual('blah-blah-file', data['path'])
+
+    def test_no_viewable_repositories_with_browser_view(self):
+        self.grant_perm('anonymous', 'BROWSER_VIEW')
+        provider = DbRepositoryProvider(self.env)
+
+        provider.remove_repository('allow')
+        provider.remove_repository('(default)')
+        provider.remove_repository('raise')
+
+        req = self.create_request(path_info='/browser/')
+        try:
+            self.process_request(req)
+            self.fail('ResourceNotFound not raised')
+        except ResourceNotFound, e:
+            self.assertEqual('No viewable repositories', unicode(e))
+        req = self.create_request(path_info='/browser/allow/')
+        try:
+            self.process_request(req)
+            self.fail('ResourceNotFound not raised')
+        except ResourceNotFound, e:
+            self.assertEqual('No node allow', unicode(e))
+        req = self.create_request(path_info='/browser/deny/')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('BROWSER_VIEW', e.action)
+            self.assertEqual('source', e.resource.realm)
+            self.assertEqual('/', e.resource.id)
+            self.assertEqual('repository', e.resource.parent.realm)
+            self.assertEqual('deny', e.resource.parent.id)
+
+        provider.remove_repository('deny')
+        req = self.create_request(path_info='/browser/')
+        try:
+            self.process_request(req)
+            self.fail('ResourceNotFound not raised')
+        except ResourceNotFound, e:
+            self.assertEqual('No viewable repositories', unicode(e))
+        req = self.create_request(path_info='/browser/deny/')
+        try:
+            self.process_request(req)
+            self.fail('ResourceNotFound not raised')
+        except ResourceNotFound, e:
+            self.assertEqual('No node deny', unicode(e))
+
+    def test_no_viewable_repositories_without_browser_view(self):
+        provider = DbRepositoryProvider(self.env)
+        provider.remove_repository('allow')
+        req = self.create_request(path_info='/browser/')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('BROWSER_VIEW', e.action)
+            self.assertEqual(None, e.resource)
+        provider.remove_repository('deny')
+        provider.remove_repository('(default)')
+        req = self.create_request(path_info='/browser/')
+        try:
+            self.process_request(req)
+            self.fail('PermissionError not raised')
+        except PermissionError, e:
+            self.assertEqual('BROWSER_VIEW', e.action)
+            self.assertEqual(None, e.resource)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    if ConfigObj:
+        suite.addTest(unittest.makeSuite(BrowserModulePermissionsTestCase))
+    else:
+        print("SKIP: %s.%s (no configobj installed)" %
+              (BrowserModulePermissionsTestCase.__module__,
+               BrowserModulePermissionsTestCase.__name__))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/versioncontrol/web_ui/tests/changeset.py b/trac/trac/versioncontrol/web_ui/tests/changeset.py
new file mode 100644
index 0000000..b7b322e
--- /dev/null
+++ b/trac/trac/versioncontrol/web_ui/tests/changeset.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 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/.
+
+import unittest
+
+from trac.core import TracError
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.versioncontrol.web_ui.changeset import ChangesetModule
+
+
+class ChangesetModuleTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.cm = ChangesetModule(self.env)
+
+    def test_default_repository_not_configured(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11599."""
+        req = Mock(perm=MockPerm(), args={'new_path': '/'},
+                   get_header=lambda self: None)
+        self.assertRaises(TracError, self.cm.process_request, req)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(ChangesetModuleTestCase))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/versioncontrol/web_ui/tests/log.py b/trac/trac/versioncontrol/web_ui/tests/log.py
new file mode 100644
index 0000000..e94e8bb
--- /dev/null
+++ b/trac/trac/versioncontrol/web_ui/tests/log.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 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/.
+
+import unittest
+
+from trac.core import TracError
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.versioncontrol.web_ui.log import LogModule
+
+
+class LogModuleTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.lm = LogModule(self.env)
+
+    def test_default_repository_not_configured(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11599."""
+        req = Mock(perm=MockPerm(), args={'new_path': '/'})
+        self.assertRaises(TracError, self.lm.process_request, req)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(LogModuleTestCase))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/versioncontrol/web_ui/tests/wikisyntax.py b/trac/trac/versioncontrol/web_ui/tests/wikisyntax.py
index 6d649c6..da46218 100644
--- a/trac/trac/versioncontrol/web_ui/tests/wikisyntax.py
+++ b/trac/trac/versioncontrol/web_ui/tests/wikisyntax.py
@@ -1,28 +1,45 @@
-# -*- encoding: utf-8 -*-
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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.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/.
 
 import unittest
 
 from trac.test import Mock
-from trac.versioncontrol import NoSuchChangeset, NoSuchNode
 from trac.versioncontrol.api import *
 from trac.versioncontrol.web_ui import *
 from trac.wiki.tests import formatter
 
 
+YOUNGEST_REV = 200
+
+
 def _get_changeset(rev):
     if rev == '1':
         return Mock(message="start", is_viewable=lambda perm: True)
     else:
         raise NoSuchChangeset(rev)
 
+
 def _normalize_rev(rev):
+    if rev is None or rev in ('', 'head'):
+        return YOUNGEST_REV
     try:
-        return int(rev)
+        nrev = int(rev)
+        if nrev <= YOUNGEST_REV:
+            return nrev
     except ValueError:
-        if rev == 'head':
-            return '200'
-        else:
-            raise NoSuchChangeset(rev)
+        pass
+    raise NoSuchChangeset(rev)
+
 
 def _get_node(path, rev=None):
     if path == 'foo':
@@ -34,12 +51,14 @@
         return Mock(path=path, rev=rev, isfile=True,
                     is_viewable=lambda resource: True)
 
+
 def _get_repository(reponame):
-    return Mock(reponame=reponame, youngest_rev='200',
+    return Mock(reponame=reponame, youngest_rev=YOUNGEST_REV,
                 get_changeset=_get_changeset,
                 normalize_rev=_normalize_rev,
                 get_node=_get_node)
 
+
 def repository_setup(tc):
     setattr(tc.env, 'get_repository', _get_repository)
     setattr(RepositoryManager(tc.env), 'get_repository', _get_repository)
@@ -177,24 +196,47 @@
 ============================== log: link resolver
 log:@12
 log:trunk
+log:trunk@head
 log:trunk@12
 log:trunk@12:23
 log:trunk@12-23
 log:trunk:12:23
 log:trunk:12-23
+log:trunk@12:head
 log:trunk:12-head
-log:trunk:12@23 (bad, but shouldn't error out)
+log:trunk:12@23
 ------------------------------
 <p>
 <a class="source" href="/log/?rev=12">log:@12</a>
 <a class="source" href="/log/trunk">log:trunk</a>
+<a class="source" href="/log/trunk?rev=head">log:trunk@head</a>
 <a class="source" href="/log/trunk?rev=12">log:trunk@12</a>
 <a class="source" href="/log/trunk?revs=12-23">log:trunk@12:23</a>
 <a class="source" href="/log/trunk?revs=12-23">log:trunk@12-23</a>
 <a class="source" href="/log/trunk?revs=12-23">log:trunk:12:23</a>
 <a class="source" href="/log/trunk?revs=12-23">log:trunk:12-23</a>
-<a class="source" href="/log/trunk?revs=12-200">log:trunk:12-head</a>
-<a class="source" href="/log/trunk">log:trunk:12@23</a> (bad, but shouldn't error out)
+<a class="source" href="/log/trunk?revs=12%3Ahead">log:trunk@12:head</a>
+<a class="source" href="/log/trunk?revs=12-head">log:trunk:12-head</a>
+<a class="missing source" title="No changeset 12@23 in the repository">log:trunk:12@23</a>
+</p>
+------------------------------
+============================== log: link resolver with missing revisions
+log:@4242
+log:@4242-4243
+log:@notfound
+log:@deadbeef:deadbef0
+log:trunk@4243
+log:trunk@notfound
+[4242:4243]
+------------------------------
+<p>
+<a class="missing source" title="No changeset 4242 in the repository">log:@4242</a>
+<a class="source" href="/log/?revs=4242-4243">log:@4242-4243</a>
+<a class="missing source" title="No changeset notfound in the repository">log:@notfound</a>
+<a class="missing source" title="No changeset deadbeef in the repository">log:@deadbeef:deadbef0</a>
+<a class="missing source" title="No changeset 4243 in the repository">log:trunk@4243</a>
+<a class="missing source" title="No changeset notfound in the repository">log:trunk@notfound</a>
+<a class="source" href="/log/?revs=4242-4243">[4242:4243]</a>
 </p>
 ------------------------------
 ============================== log: link resolver + query
@@ -214,6 +256,21 @@
 <a class="source" href="/log/trunk?revs=10-20&amp;verbose=yes&amp;format=changelog">[10:20/trunk?verbose=yes&amp;format=changelog]</a>
 </p>
 ------------------------------
+============================== log: link resolver + invalid ranges
+log:@10-20-30
+log:@10,20-30,40-50-60
+log:@10:20:30
+[10-20-30]
+[10:20:30]
+------------------------------
+<p>
+<a class="missing source" title="No changeset 10-20-30 in the repository">log:@10-20-30</a>
+<a class="missing source" title="No changeset 40-50-60 in the repository">log:@10,20-30,40-50-60</a>
+<a class="missing source" title="No changeset 10:20:30 in the repository">log:@10:20:30</a>
+[10-20-30]
+[10:20:30]
+</p>
+------------------------------
 ============================== Multiple Log ranges
 r12:20,25,35:56,68,69,100-120
 [12:20,25,35:56,68,69,100-120]
diff --git a/trac/trac/versioncontrol/web_ui/util.py b/trac/trac/versioncontrol/web_ui/util.py
index 0276a17..7f24f95 100644
--- a/trac/trac/versioncontrol/web_ui/util.py
+++ b/trac/trac/versioncontrol/web_ui/util.py
@@ -16,17 +16,21 @@
 # Author: Jonas Borgström <jonas@edgewall.com>
 #         Christian Boos <cboos@edgewall.org>
 
+from StringIO import StringIO
 from itertools import izip
+from zipfile import ZipFile, ZIP_DEFLATED
 
 from genshi.builder import tag
 
 from trac.resource import ResourceNotFound
-from trac.util.datefmt import datetime, utc
+from trac.util import content_disposition, create_zipinfo
+from trac.util.datefmt import datetime, http_date, utc
 from trac.util.translation import tag_, _
 from trac.versioncontrol.api import Changeset, NoSuchNode, NoSuchChangeset
+from trac.web.api import RequestDone
 
 __all__ = ['get_changes', 'get_path_links', 'get_existing_node',
-           'get_allowed_node', 'make_log_graph']
+           'get_allowed_node', 'make_log_graph', 'render_zip']
 
 
 def get_changes(repos, revs, log=None):
@@ -166,3 +170,63 @@
     except StopIteration:
         pass
     return threads, vertices, columns
+
+
+def render_zip(req, filename, repos, root_node, iter_nodes):
+    """Send a ZIP file containing the data corresponding to the `nodes`
+    iterable.
+
+    :type root_node: `~trac.versioncontrol.api.Node`
+    :param root_node: optional ancestor for all the *nodes*
+
+    :param iter_nodes: callable taking the optional *root_node* as input
+                       and generating the `~trac.versioncontrol.api.Node`
+                       for which the content should be added into the zip.
+    """
+    req.send_response(200)
+    req.send_header('Content-Type', 'application/zip')
+    req.send_header('Content-Disposition',
+                    content_disposition('inline', filename))
+    if root_node:
+        req.send_header('Last-Modified', http_date(root_node.last_modified))
+        root_path = root_node.path.rstrip('/')
+    else:
+        root_path = ''
+    if root_path:
+        root_path += '/'
+        root_name = root_node.name + '/'
+    else:
+        root_name = ''
+    root_len = len(root_path)
+
+    buf = StringIO()
+    zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
+    for node in iter_nodes(root_node):
+        if node is root_node:
+            continue
+        path = node.path.strip('/')
+        assert path.startswith(root_path)
+        path = root_name + path[root_len:]
+        kwargs = {'mtime': node.last_modified}
+        data = None
+        if node.isfile:
+            data = node.get_processed_content(eol_hint='CRLF').read()
+            properties = node.get_properties()
+            # Subversion specific
+            if 'svn:special' in properties and data.startswith('link '):
+                data = data[5:]
+                kwargs['symlink'] = True
+            if 'svn:executable' in properties:
+                kwargs['executable'] = True
+        elif node.isdir and path:
+            kwargs['dir'] = True
+            data = ''
+        if data is not None:
+            zipfile.writestr(create_zipinfo(path, **kwargs), data)
+    zipfile.close()
+
+    zip_str = buf.getvalue()
+    req.send_header("Content-Length", len(zip_str))
+    req.end_headers()
+    req.write(zip_str)
+    raise RequestDone
diff --git a/trac/trac/web/__init__.py b/trac/trac/web/__init__.py
index 12bfc6a..7971872 100644
--- a/trac/trac/web/__init__.py
+++ b/trac/trac/web/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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/.
+
 # Workaround for http://bugs.python.org/issue6763 and
 # http://bugs.python.org/issue5853 thread issues
 import mimetypes
diff --git a/trac/trac/web/_fcgi.py b/trac/trac/web/_fcgi.py
index 1677d40..fbb2313 100644
--- a/trac/trac/web/_fcgi.py
+++ b/trac/trac/web/_fcgi.py
@@ -1150,7 +1150,7 @@
 
         Set multithreaded to False if your application is not MT-safe.
         """
-        if kw.has_key('handler'):
+        if 'handler' in kw:
             del kw['handler'] # Doesn't make sense to let this through
         super(WSGIServer, self).__init__(**kw)
 
@@ -1278,9 +1278,9 @@
 
     def _sanitizeEnv(self, environ):
         """Ensure certain values are present, if required by WSGI."""
-        if not environ.has_key('SCRIPT_NAME'):
+        if 'SCRIPT_NAME' not in environ:
             environ['SCRIPT_NAME'] = ''
-        if not environ.has_key('PATH_INFO'):
+        if 'PATH_INFO' not in environ:
             environ['PATH_INFO'] = ''
 
         # If any of these are missing, it probably signifies a broken
@@ -1289,7 +1289,7 @@
                              ('SERVER_NAME', 'localhost'),
                              ('SERVER_PORT', '80'),
                              ('SERVER_PROTOCOL', 'HTTP/1.0')]:
-            if not environ.has_key(name):
+            if name not in environ:
                 environ['wsgi.errors'].write('%s: missing FastCGI param %s '
                                              'required by WSGI!\n' %
                                              (self.__class__.__name__, name))
diff --git a/trac/trac/web/api.py b/trac/trac/web/api.py
index a2c0a3d..6777b1c 100644
--- a/trac/trac/web/api.py
+++ b/trac/trac/web/api.py
@@ -23,19 +23,23 @@
 import new
 import mimetypes
 import os
+import re
 import socket
 from StringIO import StringIO
 import sys
 import urlparse
 
+from genshi.builder import Fragment
 from trac.core import Interface, TracError
+from trac.perm import PermissionError
 from trac.util import get_last_traceback, unquote
 from trac.util.datefmt import http_date, localtz
-from trac.util.text import empty, to_unicode
+from trac.util.text import empty, exception_to_unicode, to_unicode
 from trac.util.translation import _
 from trac.web.href import Href
 from trac.web.wsgi import _FileWrapper
 
+
 class IAuthenticator(Interface):
     """Extension point interface for components that can provide the name
     of the remote user."""
@@ -129,7 +133,7 @@
 class HTTPException(Exception):
 
     def __init__(self, detail, *args):
-        if isinstance(detail, TracError):
+        if isinstance(detail, (TracError, PermissionError)):
             self.detail = detail.message
             self.reason = detail.title
         else:
@@ -139,6 +143,35 @@
         Exception.__init__(self, '%s %s (%s)' % (self.code, self.reason,
                                                  self.detail))
 
+    @property
+    def message(self):
+        # The message is based on the e.detail, which can be an Exception
+        # object, but not a TracError one: when creating HTTPException,
+        # a TracError.message is directly assigned to e.detail
+        if isinstance(self.detail, Exception): # not a TracError or PermissionError
+            message = exception_to_unicode(self.detail)
+        elif isinstance(self.detail, Fragment): # TracError or PermissionError markup
+            message = self.detail
+        else:
+            message = to_unicode(self.detail)
+        return message
+
+    @property
+    def title(self):
+        try:
+            # We first try to get localized error messages here, but we
+            # should ignore secondary errors if the main error was also
+            # due to i18n issues
+            title = _("Error")
+            if self.reason:
+                if title.lower() in self.reason.lower():
+                    title = self.reason
+                else:
+                    title = _("Error: %(message)s", message=self.reason)
+        except Exception:
+            title = "Error"
+        return title
+
     @classmethod
     def subclass(cls, name, code):
         """Create a new Exception class representing a HTTP status code."""
@@ -243,6 +276,10 @@
     """Marker exception that indicates whether request processing has completed
     and a response was sent.
     """
+    iterable = None
+
+    def __init__(self, iterable=None):
+        self.iterable = iterable
 
 
 class Cookie(SimpleCookie):
@@ -356,7 +393,9 @@
 
         Will be `None` if the user has not logged in using HTTP authentication.
         """
-        return self.environ.get('REMOTE_USER')
+        user = self.environ.get('REMOTE_USER')
+        if user is not None:
+            return to_unicode(user)
 
     @property
     def scheme(self):
@@ -405,11 +444,12 @@
         `value` must either be an `unicode` string or can be converted to one
         (e.g. numbers, ...)
         """
-        if name.lower() == 'content-type':
+        lower_name = name.lower()
+        if lower_name == 'content-type':
             ctpos = value.find('charset=')
             if ctpos >= 0:
                 self._outcharset = value[ctpos + 8:].strip()
-        elif name.lower() == 'content-length':
+        elif lower_name == 'content-length':
             self._content_length = int(value)
         self._outheaders.append((name, unicode(value).encode('utf-8')))
 
@@ -442,7 +482,7 @@
             extra = m.hexdigest()
         etag = 'W/"%s/%s/%s"' % (self.authname, http_date(datetime), extra)
         inm = self.get_header('If-None-Match')
-        if (not inm or inm != etag):
+        if not inm or inm != etag:
             self.send_header('ETag', etag)
         else:
             self.send_response(304)
@@ -472,10 +512,12 @@
             scheme, host = urlparse.urlparse(self.base_url)[:2]
             url = urlparse.urlunparse((scheme, host, url, None, None, None))
 
-        # Workaround #10382, IE6+ bug when post and redirect with hash
-        if status == 303 and '#' in url and \
-                ' MSIE ' in self.environ.get('HTTP_USER_AGENT', ''):
-            url = url.replace('#', '#__msie303:')
+        # Workaround #10382, IE6-IE9 bug when post and redirect with hash
+        if status == 303 and '#' in url:
+            match = re.search(' MSIE ([0-9]+)',
+                              self.environ.get('HTTP_USER_AGENT', ''))
+            if match and int(match.group(1)) < 10:
+                url = url.replace('#', '#__msie303:')
 
         self.send_header('Location', url)
         self.send_header('Content-Type', 'text/plain')
@@ -601,19 +643,17 @@
     def write(self, data):
         """Write the given data to the response body.
 
-        `data` *must* be a `str` string, encoded with the charset
-        which has been specified in the ''Content-Type'' header
-        or 'utf-8' otherwise.
+        *data* **must** be a `str` string, encoded with the charset
+        which has been specified in the ``'Content-Type'`` header
+        or UTF-8 otherwise.
 
-        Note that the ''Content-Length'' header must have been specified.
-        Its value either corresponds to the length of `data`, or, if there
-        are multiple calls to `write`, to the cumulated length of the `data`
-        arguments.
+        Note that when the ``'Content-Length'`` header is specified,
+        its value either corresponds to the length of *data*, or, if
+        there are multiple calls to `write`, to the cumulative length
+        of the *data* arguments.
         """
         if not self._write:
             self.end_headers()
-        if not hasattr(self, '_content_length'):
-            raise RuntimeError("No Content-Length header set")
         if isinstance(data, unicode):
             raise ValueError("Can't send unicode content")
         try:
@@ -621,6 +661,12 @@
         except (IOError, socket.error), e:
             if e.args[0] in (errno.EPIPE, errno.ECONNRESET, 10053, 10054):
                 raise RequestDone
+            # Note that mod_wsgi raises an IOError with only a message
+            # if the client disconnects
+            if 'mod_wsgi.version' in self.environ and \
+               e.args[0] in ('failed to write data',
+                             'client connection closed'):
+                raise RequestDone
             raise
 
     # Internal methods
@@ -700,7 +746,7 @@
             # server name and port
             default_port = {'http': 80, 'https': 443}
             if self.server_port and self.server_port != \
-                   default_port[self.scheme]:
+                    default_port[self.scheme]:
                 host = '%s:%d' % (self.server_name, self.server_port)
             else:
                 host = self.server_name
diff --git a/trac/trac/web/auth.py b/trac/trac/web/auth.py
index 8929659..5148901 100644
--- a/trac/trac/web/auth.py
+++ b/trac/trac/web/auth.py
@@ -86,7 +86,7 @@
         authname = None
         if req.remote_user:
             authname = req.remote_user
-        elif req.incookie.has_key('trac_auth'):
+        elif 'trac_auth' in req.incookie:
             authname = self._get_name_for_cookie(req,
                                                  req.incookie['trac_auth'])
 
@@ -108,7 +108,10 @@
             yield ('metanav', 'login', _('logged in as %(user)s',
                                          user=req.authname))
             yield ('metanav', 'logout',
-                   tag.a(_('Logout'), href=req.href.logout()))
+                   tag.form(tag.div(tag.button(_('Logout'),
+                                               name='logout', type='submit')),
+                            action=req.href.logout(), method='post',
+                            id='logout', class_='trac-logout'))
         else:
             yield ('metanav', 'login',
                    tag.a(_('Login'), href=req.href.login()))
@@ -155,8 +158,9 @@
         if self.ignore_case:
             remote_user = remote_user.lower()
 
-        assert req.authname in ('anonymous', remote_user), \
-               _('Already logged in as %(user)s.', user=req.authname)
+        if req.authname not in ('anonymous', remote_user):
+            raise TracError(_('Already logged in as %(user)s.',
+                              user=req.authname))
 
         with self.env.db_transaction as db:
             # Delete cookies older than 10 days
@@ -192,6 +196,8 @@
         Simply deletes the corresponding record from the auth_cookie
         table.
         """
+        if req.method != 'POST':
+            return
         if req.authname == 'anonymous':
             # Not logged in
             return
@@ -429,12 +435,12 @@
                          'nc', 'cnonce']
         # Invalid response?
         for key in required_keys:
-            if not auth.has_key(key):
+            if key not in auth:
                 self.send_auth_request(environ, start_response)
                 return None
         # Unknown user?
         self.check_reload()
-        if not self.hash.has_key(auth['username']):
+        if auth['username'] not in self.hash:
             self.send_auth_request(environ, start_response)
             return None
 
diff --git a/trac/trac/web/chrome.py b/trac/trac/web/chrome.py
index ebd680e..4be1e8a 100644
--- a/trac/trac/web/chrome.py
+++ b/trac/trac/web/chrome.py
@@ -48,8 +48,8 @@
 from trac.env import IEnvironmentSetupParticipant, ISystemInfoProvider
 from trac.mimeview.api import RenderingContext, get_mimetype
 from trac.resource import *
-from trac.util import compat, get_reporter_id, presentation, get_pkginfo, \
-                      pathjoin, translation
+from trac.util import compat, get_reporter_id, html, presentation, \
+                      get_pkginfo, pathjoin, translation
 from trac.util.html import escape, plaintext
 from trac.util.text import pretty_size, obfuscate_email_address, \
                            shorten_line, unicode_quote_plus, to_unicode, \
@@ -135,42 +135,30 @@
     """Add a link to a style sheet to the chrome info so that it gets included
     in the generated HTML page.
 
-    If the filename is absolute (i.e. starts with a slash), the generated link
-    will be based off the application root path. If it is relative, the link
-    will be based off the `/chrome/` path.
+    If `filename` is a network-path reference (i.e. starts with a protocol
+    or `//`), the return value will not be modified. If `filename` is absolute
+    (i.e. starts with `/`), the generated link will be based off the
+    application root path. If it is relative, the link will be based off the
+    `/chrome/` path.
     """
-    if filename.startswith(('http://', 'https://')):
-        href = filename
-    elif filename.startswith('common/') and 'htdocs_location' in req.chrome:
-        href = Href(req.chrome['htdocs_location'])(filename[7:])
-    else:
-        href = req.href
-        if not filename.startswith('/'):
-            href = href.chrome
-        href = href(filename)
+    href = _chrome_resource_path(req, filename)
     add_link(req, 'stylesheet', href, mimetype=mimetype, media=media)
 
 def add_script(req, filename, mimetype='text/javascript', charset='utf-8',
                ie_if=None):
     """Add a reference to an external javascript file to the template.
 
-    If the filename is absolute (i.e. starts with a slash), the generated link
-    will be based off the application root path. If it is relative, the link
-    will be based off the `/chrome/` path.
+    If `filename` is a network-path reference (i.e. starts with a protocol
+    or `//`), the return value will not be modified. If `filename` is absolute
+    (i.e. starts with `/`), the generated link will be based off the
+    application root path. If it is relative, the link will be based off the
+    `/chrome/` path.
     """
     scriptset = req.chrome.setdefault('scriptset', set())
     if filename in scriptset:
         return False # Already added that script
 
-    if filename.startswith(('http://', 'https://')):
-        href = filename
-    elif filename.startswith('common/') and 'htdocs_location' in req.chrome:
-        href = Href(req.chrome['htdocs_location'])(filename[7:])
-    else:
-        href = req.href
-        if not filename.startswith('/'):
-            href = href.chrome
-        href = href(filename)
+    href = _chrome_resource_path(req, filename)
     script = {'href': href, 'type': mimetype, 'charset': charset,
               'prefix': Markup('<!--[if %s]>' % ie_if) if ie_if else None,
               'suffix': Markup('<![endif]-->') if ie_if else None}
@@ -190,7 +178,7 @@
     script_data.update(kwargs)
 
 def add_javascript(req, filename):
-    """:deprecated: use `add_script` instead."""
+    """:deprecated: since 0.10, use `add_script` instead."""
     add_script(req, filename, mimetype='text/javascript')
 
 def add_warning(req, msg, *args):
@@ -286,6 +274,8 @@
                      name)
     :return: a new rendering context
     :rtype: `RenderingContext`
+
+    :since: version 1.0
     """
     if req:
         href = req.abs_href if absurls else req.href
@@ -311,6 +301,24 @@
     return link
 
 
+def _chrome_resource_path(req, filename):
+    """Get the path for a chrome resource given its `filename`.
+
+    If `filename` is a network-path reference (i.e. starts with a protocol
+    or `//`), the return value will not be modified. If `filename` is absolute
+    (i.e. starts with `/`), the generated link will be based off the
+    application root path. If it is relative, the link will be based off the
+    `/chrome/` path.
+    """
+    if filename.startswith(('http://', 'https://', '//')):
+        return filename
+    elif filename.startswith('common/') and 'htdocs_location' in req.chrome:
+        return Href(req.chrome['htdocs_location'])(filename[7:])
+    else:
+        href = req.href if filename.startswith('/') else req.href.chrome
+        return href(filename)
+
+
 def _save_messages(req, url, permanent):
     """Save warnings and notices in case of redirect, so that they can
     be displayed after the redirect."""
@@ -475,6 +483,10 @@
         """Make `<textarea>` fields resizable. Requires !JavaScript.
         (''since 0.12'')""")
 
+    wiki_toolbars = BoolOption('trac', 'wiki_toolbars', 'true',
+        """Add a simple toolbar on top of Wiki `<textarea>`s.
+        (''since 1.0.2'')""")
+
     auto_preview_timeout = FloatOption('trac', 'auto_preview_timeout', 2.0,
         """Inactivity timeout in seconds after which the automatic wiki preview
         triggers an update. This option can contain floating-point values. The
@@ -491,8 +503,8 @@
 
     templates = None
 
-    # default doctype for 'text/html' output
-    default_html_doctype = DocType.XHTML_STRICT
+    # DocType for 'text/html' output
+    html_doctype = DocType.XHTML_STRICT
 
     # A dictionary of default context data for templates
     _default_context_data = {
@@ -505,6 +517,7 @@
         'dgettext': translation.dgettext,
         'dngettext': translation.dngettext,
         'first_last': presentation.first_last,
+        'find_element': html.find_element,
         'get_reporter_id': get_reporter_id,
         'gettext': translation.gettext,
         'group': presentation.group,
@@ -606,11 +619,13 @@
         for provider in self.template_providers:
             for dir in [os.path.normpath(dir[1]) for dir
                         in provider.get_htdocs_dirs() or []
-                        if dir[0] == prefix]:
+                        if dir[0] == prefix and dir[1]]:
                 dirs.append(dir)
                 path = os.path.normpath(os.path.join(dir, filename))
-                assert os.path.commonprefix([dir, path]) == dir
-                if os.path.isfile(path):
+                if os.path.commonprefix([dir, path]) != dir:
+                    raise TracError(_("Invalid chrome path %(path)s.",
+                                      path=filename))
+                elif os.path.isfile(path):
                     req.send_file(path, get_mimetype(path))
 
         self.log.warning('File %s not found in any of %s', filename, dirs)
@@ -679,7 +694,7 @@
            getattr(handler.__class__, 'jquery_noconflict', False):
             add_script(req, 'common/js/noconflict.js')
         add_script(req, 'common/js/babel.js')
-        if req.locale is not None:
+        if req.locale is not None and str(req.locale) != 'en_US':
             add_script(req, 'common/js/messages/%s.js' % req.locale)
         add_script(req, 'common/js/trac.js')
         add_script(req, 'common/js/search.js')
@@ -864,7 +879,14 @@
                 format = req.session.get('dateinfo',
                                          self.default_dateinfo_format)
             if format == 'absolute':
-                label = absolute
+                if dateonly:
+                    label = absolute
+                elif req.lc_time == 'iso8601':
+                    label = _("at %(iso8601)s", iso8601=absolute)
+                else:
+                    label = _("on %(date)s at %(time)s",
+                              date=user_time(req, format_date, date),
+                              time=user_time(req, format_time, date))
                 title = _("%(relativetime)s ago", relativetime=relative)
             else:
                 label = _("%(relativetime)s ago", relativetime=relative) \
@@ -952,9 +974,10 @@
         """Render the `filename` using the `data` for the context.
 
         The `content_type` argument is used to choose the kind of template
-        used (`NewTextTemplate` if `'text/plain'`, `MarkupTemplate` otherwise),
-        and tweak the rendering process. Doctype for `'text/html'` can be
-        specified by setting the default_html_doctype (default is XHTML Strict)
+        used (`NewTextTemplate` if `'text/plain'`, `MarkupTemplate`
+        otherwise), and tweak the rendering process. Doctype for `'text/html'`
+        can be specified by setting the `html_doctype` attribute (default
+        is `XHTML_STRICT`)
 
         When `fragment` is specified, the (filtered) Genshi stream is
         returned.
@@ -994,8 +1017,9 @@
             stream.render('text', out=buffer, encoding='utf-8')
             return buffer.getvalue()
 
-        doctype = {'text/html': Chrome.default_html_doctype}.get(content_type)
-        if doctype:
+        doctype = None
+        if content_type == 'text/html':
+            doctype = self.html_doctype
             if req.form_token:
                 stream |= self._add_form_token(req.form_token)
             if not int(req.session.get('accesskeys', 0)):
@@ -1102,7 +1126,8 @@
 
     def add_wiki_toolbars(self, req):
         """Add wiki toolbars to `<textarea class="wikitext">` fields."""
-        add_script(req, 'common/js/wikitoolbar.js')
+        if self.wiki_toolbars:
+            add_script(req, 'common/js/wikitoolbar.js')
         self.add_textarea_grips(req)
 
     def add_auto_preview(self, req):
@@ -1129,8 +1154,10 @@
             'first_week_day': get_first_week_day_jquery_ui(req),
             'timepicker_separator': 'T' if is_iso8601 else ' ',
             'show_timezone': is_iso8601,
+            # default timezone must be included
             'timezone_list': get_timezone_list_jquery_ui() \
-                             if is_iso8601 else [],
+                             if is_iso8601 \
+                             else [{'value': 'Z', 'label': '+00:00'}],
             'timezone_iso8601': is_iso8601,
         })
         add_script(req, 'common/js/jquery-ui-i18n.js')
@@ -1170,4 +1197,3 @@
     def _stream_location(self, stream):
         for kind, data, pos in stream:
             return pos
-
diff --git a/trac/trac/web/main.py b/trac/trac/web/main.py
index f19cfbf..89d7d3c 100644
--- a/trac/trac/web/main.py
+++ b/trac/trac/web/main.py
@@ -28,7 +28,7 @@
 import re
 import sys
 
-from genshi.builder import Fragment, tag
+from genshi.builder import tag
 from genshi.output import DocType
 from genshi.template import TemplateLoader
 
@@ -41,14 +41,14 @@
 from trac.perm import PermissionCache, PermissionError
 from trac.resource import ResourceNotFound
 from trac.util import arity, get_frame_info, get_last_traceback, hex_entropy, \
-                      read_file, safe_repr, translation
+                      read_file, safe_repr, translation, warn_setuptools_issue
 from trac.util.concurrency import threading
 from trac.util.datefmt import format_datetime, localtz, timezone, user_time
 from trac.util.text import exception_to_unicode, shorten_line, to_unicode
 from trac.util.translation import _, get_negotiated_locale, has_babel, \
                                   safefmt, tag_
 from trac.web.api import *
-from trac.web.chrome import Chrome
+from trac.web.chrome import Chrome, add_notice, add_warning
 from trac.web.href import Href
 from trac.web.session import Session
 
@@ -132,11 +132,18 @@
 
     def authenticate(self, req):
         for authenticator in self.authenticators:
-            authname = authenticator.authenticate(req)
+            try:
+                authname = authenticator.authenticate(req)
+            except TracError, e:
+                self.log.error("Can't authenticate using %s: %s",
+                               authenticator.__class__.__name__,
+                               exception_to_unicode(e, traceback=True))
+                add_warning(req, _("Authentication error. "
+                                   "Please contact your administrator."))
+                break  # don't fallback to other authenticators
             if authname:
                 return authname
-        else:
-            return 'anonymous'
+        return 'anonymous'
 
     def dispatch(self, req):
         """Find a registered handler that matches the request and let
@@ -175,8 +182,8 @@
                             chosen_handler = self.default_handler
                     # pre-process any incoming request, whether a handler
                     # was found or not
-                    chosen_handler = self._pre_process_request(req,
-                                                            chosen_handler)
+                    chosen_handler = \
+                        self._pre_process_request(req, chosen_handler)
                 except TracError, e:
                     raise HTTPInternalError(e)
                 if not chosen_handler:
@@ -249,7 +256,7 @@
                                    exception_to_unicode(e, traceback=True))
                 raise err[0], err[1], err[2]
         except PermissionError, e:
-            raise HTTPForbidden(to_unicode(e))
+            raise HTTPForbidden(e)
         except ResourceNotFound, e:
             raise HTTPNotFound(e)
         except TracError, e:
@@ -277,7 +284,8 @@
             default = self.env.config.get('trac', 'default_language', '')
             negotiated = get_negotiated_locale([preferred, default] +
                                                req.languages)
-            self.log.debug("Negotiated locale: %s -> %s", preferred, negotiated)
+            self.log.debug("Negotiated locale: %s -> %s", preferred,
+                           negotiated)
             return negotiated
 
     def _get_lc_time(self, req):
@@ -306,7 +314,7 @@
         If the the user does not have a `trac_form_token` cookie a new
         one is generated.
         """
-        if req.incookie.has_key('trac_form_token'):
+        if 'trac_form_token' in req.incookie:
             return req.incookie['trac_form_token'].value
         else:
             req.outcookie['trac_form_token'] = hex_entropy(24)
@@ -340,6 +348,8 @@
                 f.post_process_request(req, *(None,)*extra_arg_count)
         return resp
 
+
+_warn_setuptools = False
 _slashes_re = re.compile(r'/+')
 
 
@@ -350,6 +360,11 @@
     :param start_response: the WSGI callback for starting the response
     """
 
+    global _warn_setuptools
+    if _warn_setuptools is False:
+        _warn_setuptools = True
+        warn_setuptools_issue(out=environ.get('wsgi.errors'))
+
     # SCRIPT_URL is an Apache var containing the URL before URL rewriting
     # has been applied, so we can use it to reconstruct logical SCRIPT_NAME
     script_url = environ.get('SCRIPT_URL')
@@ -467,7 +482,7 @@
 
     # fixup env.abs_href if `[trac] base_url` was not specified
     if env and not env.abs_href.base:
-        env._abs_href = req.abs_href
+        env.abs_href = req.abs_href
 
     try:
         if not env and env_error:
@@ -475,12 +490,12 @@
         try:
             dispatcher = RequestDispatcher(env)
             dispatcher.dispatch(req)
-        except RequestDone:
-            pass
-        resp = req._response or []
+        except RequestDone, req_done:
+            resp = req_done.iterable
+        resp = resp or req._response or []
     except HTTPException, e:
         _send_user_error(req, env, e)
-    except Exception, e:
+    except Exception:
         send_internal_error(env, req, sys.exc_info())
     return resp
 
@@ -489,40 +504,19 @@
     # See trac/web/api.py for the definition of HTTPException subclasses.
     if env:
         env.log.warn('[%s] %s' % (req.remote_addr, exception_to_unicode(e)))
-    try:
-        # We first try to get localized error messages here, but we
-        # should ignore secondary errors if the main error was also
-        # due to i18n issues
-        title = _('Error')
-        if e.reason:
-            if title.lower() in e.reason.lower():
-                title = e.reason
-            else:
-                title = _('Error: %(message)s', message=e.reason)
-    except Exception:
-        title = 'Error'
-    # The message is based on the e.detail, which can be an Exception
-    # object, but not a TracError one: when creating HTTPException,
-    # a TracError.message is directly assigned to e.detail
-    if isinstance(e.detail, Exception): # not a TracError
-        message = exception_to_unicode(e.detail)
-    elif isinstance(e.detail, Fragment): # markup coming from a TracError
-        message = e.detail
-    else:
-        message = to_unicode(e.detail)
-    data = {'title': title, 'type': 'TracError', 'message': message,
+    data = {'title': e.title, 'type': 'TracError', 'message': e.message,
             'frames': [], 'traceback': None}
     if e.code == 403 and req.authname == 'anonymous':
         # TRANSLATOR: ... not logged in, you may want to 'do so' now (link)
         do_so = tag.a(_("do so"), href=req.href.login())
-        req.chrome['notices'].append(
-            tag_("You are currently not logged in. You may want to "
-                 "%(do_so)s now.", do_so=do_so))
+        add_notice(req, tag_("You are currently not logged in. You may want "
+                             "to %(do_so)s now.", do_so=do_so))
     try:
         req.send_error(sys.exc_info(), status=e.code, env=env, data=data)
     except RequestDone:
         pass
 
+
 def send_internal_error(env, req, exc_info):
     if env:
         env.log.error("Internal Server Error: %s",
@@ -539,6 +533,7 @@
         pass
 
     tracker = default_tracker
+    tracker_args = {}
     if has_admin and not isinstance(exc_info[1], MemoryError):
         # Collect frame and plugin information
         frames = get_frame_info(exc_info[2])
@@ -558,13 +553,18 @@
                     tracker = info['trac']
                 elif info.get('home_page', '').startswith(th):
                     tracker = th
+                    plugin_name = info.get('home_page', '').rstrip('/') \
+                                                           .split('/')[-1]
+                    tracker_args = {'component': plugin_name}
 
     def get_description(_):
         if env and has_admin:
             sys_info = "".join("|| '''`%s`''' || `%s` ||\n"
                                % (k, v.replace('\n', '` [[br]] `'))
                                for k, v in env.get_systeminfo())
-            sys_info += "|| '''`jQuery`''' || `#JQUERY#` ||\n"
+            sys_info += "|| '''`jQuery`''' || `#JQUERY#` ||\n" \
+                        "|| '''`jQuery UI`''' || `#JQUERYUI#` ||\n" \
+                        "|| '''`jQuery Timepicker`''' || `#JQUERYTP#` ||\n"
             enabled_plugins = "".join("|| '''`%s`''' || `%s` ||\n"
                                       % (p['name'], p['version'] or _('N/A'))
                                       for p in plugins)
@@ -608,9 +608,10 @@
             'traceback': traceback, 'frames': frames,
             'shorten_line': shorten_line, 'repr': safe_repr,
             'plugins': plugins, 'faulty_plugins': faulty_plugins,
-            'tracker': tracker,
+            'tracker': tracker, 'tracker_args': tracker_args,
             'description': description, 'description_en': description_en}
 
+    Chrome(env).add_jquery_ui(req)
     try:
         req.send_error(exc_info, status=500, env=env, data=data)
     except RequestDone:
@@ -702,7 +703,7 @@
         paths = [path[:-1] for path in paths if path[-1] == '/'
                  and not any(fnmatch.fnmatch(path[:-1], pattern)
                              for pattern in ignore_patterns)]
-        env_paths.extend(os.path.join(env_parent_dir, project) \
+        env_paths.extend(os.path.join(env_parent_dir, project)
                          for project in paths)
     envs = {}
     for env_path in env_paths:
diff --git a/trac/trac/web/session.py b/trac/trac/web/session.py
index 6ccd078..70ae38b 100644
--- a/trac/trac/web/session.py
+++ b/trac/trac/web/session.py
@@ -23,19 +23,20 @@
 import sys
 import time
 
-from trac.admin.api import console_date_format
+from trac.admin.api import console_date_format, get_console_locale
 from trac.core import TracError, Component, implements
 from trac.util import hex_entropy
 from trac.util.text import print_table
 from trac.util.translation import _
-from trac.util.datefmt import format_date, parse_date, to_datetime, \
-                              to_timestamp
+from trac.util.datefmt import get_datetime_format_hint, format_date, \
+                              parse_date, to_datetime, to_timestamp
 from trac.admin.api import IAdminCommandProvider, AdminCommandError
 
 UPDATE_INTERVAL = 3600 * 24 # Update session last_visit time stamp after 1 day
 PURGE_AGE = 3600 * 24 * 90 # Purge session after 90 days idle
 COOKIE_KEY = 'trac_session'
 
+
 # Note: as we often manipulate both the `session` and the
 #       `session_attribute` tables, there's a possibility of table
 #       deadlocks (#9705). We try to prevent them to happen by always
@@ -198,14 +199,14 @@
         super(Session, self).__init__(env, None)
         self.req = req
         if req.authname == 'anonymous':
-            if not req.incookie.has_key(COOKIE_KEY):
+            if COOKIE_KEY not in req.incookie:
                 self.sid = hex_entropy(24)
                 self.bake_cookie()
             else:
                 sid = req.incookie[COOKIE_KEY].value
                 self.get_session(sid)
         else:
-            if req.incookie.has_key(COOKIE_KEY):
+            if COOKIE_KEY in req.incookie:
                 sid = req.incookie[COOKIE_KEY].value
                 self.promote_session(sid)
             self.get_session(req.authname, authenticated=True)
@@ -313,6 +314,10 @@
     implements(IAdminCommandProvider)
 
     def get_admin_commands(self):
+        hints = {
+           'datetime': get_datetime_format_hint(get_console_locale(self.env)),
+           'iso8601': get_datetime_format_hint('iso8601'),
+        }
         yield ('session list', '[sid[:0|1]] [...]',
                """List the name and email for the given sids
 
@@ -353,10 +358,11 @@
                self._complete_delete, self._do_delete)
 
         yield ('session purge', '<age>',
-               """Purge all anonymous sessions older than the given age
+               """Purge anonymous sessions older than the given age or date
 
                Age may be specified as a relative time like "90 days ago", or
-               in YYYYMMDD format.""",
+               as a date in the "%(datetime)s" or "%(iso8601)s" (ISO 8601)
+               format.""" % hints,
                None, self._do_purge)
 
     def _split_sid(self, sid):
@@ -469,7 +475,8 @@
                         """, (sid, authenticated))
 
     def _do_purge(self, age):
-        when = parse_date(age)
+        when = parse_date(age, hint='datetime',
+                          locale=get_console_locale(self.env))
         with self.env.db_transaction as db:
             ts = to_timestamp(when)
             db("""
diff --git a/trac/trac/web/tests/__init__.py b/trac/trac/web/tests/__init__.py
index e3426e6..be80605 100644
--- a/trac/trac/web/tests/__init__.py
+++ b/trac/trac/web/tests/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
diff --git a/trac/trac/web/tests/api.py b/trac/trac/web/tests/api.py
index 98c1388..bbff34a 100644
--- a/trac/trac/web/tests/api.py
+++ b/trac/trac/web/tests/api.py
@@ -1,22 +1,110 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
 
-from trac.test import Mock
-from trac.web.api import Request, RequestDone, parse_arg_list
-
-from StringIO import StringIO
+import os.path
+import shutil
+import sys
+import tempfile
 import unittest
+from StringIO import StringIO
+
+import trac.tests.compat
+from trac import perm
+from trac.core import TracError
+from trac.test import EnvironmentStub, Mock, MockPerm, locale_en
+from trac.util import create_file
+from trac.util.datefmt import utc
+from trac.util.text import shorten_line
+from trac.web.api import Request, RequestDone, parse_arg_list
+from tracopt.perm.authz_policy import AuthzPolicy
+
+
+class RequestHandlerPermissionsTestCaseBase(unittest.TestCase):
+
+    authz_policy = None
+
+    def setUp(self, module_class):
+        self.path = tempfile.mkdtemp(prefix='trac-')
+        if self.authz_policy is not None:
+            self.authz_file = os.path.join(self.path, 'authz_policy.conf')
+            create_file(self.authz_file, self.authz_policy)
+            self.env = EnvironmentStub(enable=['trac.*', AuthzPolicy],
+                                       path=self.path)
+            self.env.config.set('authz_policy', 'authz_file', self.authz_file)
+            self.env.config.set('trac', 'permission_policies',
+                                'AuthzPolicy, DefaultPermissionPolicy')
+        else:
+            self.env = EnvironmentStub(path=self.path)
+        self.req_handler = module_class(self.env)
+
+    def tearDown(self):
+        self.env.reset_db()
+        shutil.rmtree(self.path)
+
+    def create_request(self, authname='anonymous', **kwargs):
+        kw = {'perm': perm.PermissionCache(self.env, authname), 'args': {},
+              'href': self.env.href, 'abs_href': self.env.abs_href,
+              'tz': utc, 'locale': None, 'lc_time': locale_en,
+              'chrome': {'notices': [], 'warnings': []},
+              'method': None, 'get_header': lambda v: None}
+        kw.update(kwargs)
+        return Mock(**kw)
+
+    def get_navigation_items(self, req):
+        return self.req_handler.get_navigation_items(req)
+
+    def grant_perm(self, username, *actions):
+        permsys = perm.PermissionSystem(self.env)
+        for action in actions:
+            permsys.grant_permission(username, action)
+
+    def process_request(self, req):
+        self.assertTrue(self.req_handler.match_request(req))
+        return self.req_handler.process_request(req)
+
+
+def _make_environ(scheme='http', server_name='example.org',
+                  server_port=80, method='GET', script_name='/trac',
+                  **kwargs):
+    environ = {'wsgi.url_scheme': scheme, 'wsgi.input': StringIO(''),
+               'REQUEST_METHOD': method, 'SERVER_NAME': server_name,
+               'SERVER_PORT': server_port, 'SCRIPT_NAME': script_name}
+    environ.update(kwargs)
+    return environ
+
+
+def _make_req(environ, start_response, args={}, arg_list=(), authname='admin',
+              form_token='A' * 40, chrome={'links': {}, 'scripts': []},
+              perm=MockPerm(), session={}, tz=utc, locale=None, **kwargs):
+    req = Request(environ, start_response)
+    req.args = args
+    req.arg_list = arg_list
+    req.authname = authname
+    req.form_token = form_token
+    req.chrome = chrome
+    req.perm = perm
+    req.session = session
+    req.tz = tz
+    req.locale = locale
+    for name, value in kwargs.iteritems():
+        setattr(req, name, value)
+    return req
 
 
 class RequestTestCase(unittest.TestCase):
 
-    def _make_environ(self, scheme='http', server_name='example.org',
-                      server_port=80, method='GET', script_name='/trac',
-                      **kwargs):
-        environ = {'wsgi.url_scheme': scheme, 'wsgi.input': StringIO(''),
-                   'REQUEST_METHOD': method, 'SERVER_NAME': server_name,
-                   'SERVER_PORT': server_port, 'SCRIPT_NAME': script_name}
-        environ.update(kwargs)
-        return environ
+    def _make_environ(self, *args, **kwargs):
+        return _make_environ(*args, **kwargs)
 
     def test_base_url(self):
         environ = self._make_environ()
@@ -97,10 +185,6 @@
         def start_response(status, headers):
             return write
         environ = self._make_environ(method='HEAD')
-        req = Request(environ, start_response)
-        req.send_header('Content-Type', 'text/plain;charset=utf-8')
-        # we didn't set Content-Length, so we get a RuntimeError for that
-        self.assertRaises(RuntimeError, req.write, u'Föö')
 
         req = Request(environ, start_response)
         req.send_header('Content-Type', 'text/plain;charset=utf-8')
@@ -146,6 +230,118 @@
         self.assertEqual('bar', req.args['action'])
 
 
+class SendErrorTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+
+    def tearDown(self):
+        self.env.reset_db()
+
+    def test_trac_error(self):
+        content = self._send_error(error_klass=TracError)
+        self.assertIn('<p class="message">Oops!</p>', content)
+        self.assertNotIn('<strong>Trac detected an internal error:</strong>',
+                         content)
+        self.assertNotIn('There was an internal error in Trac.', content)
+
+    def test_internal_error_for_non_admin(self):
+        content = self._send_error(perm={})
+        self.assertIn('There was an internal error in Trac.', content)
+        self.assertIn('<p>To that end, you could', content)
+        self.assertNotIn('This is probably a local installation issue.',
+                         content)
+        self.assertNotIn('<h2>Found a bug in Trac?</h2>', content)
+
+    def test_internal_error_with_admin_trac_for_non_admin(self):
+        content = self._send_error(perm={},
+                                   admin_trac_url='http://example.org/admin')
+        self.assertIn('There was an internal error in Trac.', content)
+        self.assertIn('<p>To that end, you could', content)
+        self.assertIn(' action="http://example.org/admin/newticket#"', content)
+        self.assertNotIn('This is probably a local installation issue.',
+                         content)
+        self.assertNotIn('<h2>Found a bug in Trac?</h2>', content)
+
+    def test_internal_error_without_admin_trac_for_non_admin(self):
+        content = self._send_error(perm={}, admin_trac_url='')
+        self.assertIn('There was an internal error in Trac.', content)
+        self.assertNotIn('<p>To that end, you could', content)
+        self.assertNotIn('This is probably a local installation issue.',
+                         content)
+        self.assertNotIn('<h2>Found a bug in Trac?</h2>', content)
+
+    def test_internal_error_for_admin(self):
+        content = self._send_error()
+        self.assertNotIn('There was an internal error in Trac.', content)
+        self.assertIn('This is probably a local installation issue.', content)
+        self.assertNotIn('a ticket at the admin Trac to report', content)
+        self.assertIn('<h2>Found a bug in Trac?</h2>', content)
+        self.assertIn('<p>Otherwise, please', content)
+        self.assertIn(' action="http://example.org/tracker/newticket"',
+                      content)
+
+    def test_internal_error_with_admin_trac_for_admin(self):
+        content = self._send_error(admin_trac_url='http://example.org/admin')
+        self.assertNotIn('There was an internal error in Trac.', content)
+        self.assertIn('This is probably a local installation issue.', content)
+        self.assertIn('a ticket at the admin Trac to report', content)
+        self.assertIn(' action="http://example.org/admin/newticket#"', content)
+        self.assertIn('<h2>Found a bug in Trac?</h2>', content)
+        self.assertIn('<p>Otherwise, please', content)
+        self.assertIn(' action="http://example.org/tracker/newticket"',
+                      content)
+
+    def test_internal_error_without_admin_trac_for_admin(self):
+        content = self._send_error(admin_trac_url='')
+        self.assertNotIn('There was an internal error in Trac.', content)
+        self.assertIn('This is probably a local installation issue.', content)
+        self.assertNotIn('a ticket at the admin Trac to report', content)
+        self.assertIn('<h2>Found a bug in Trac?</h2>', content)
+        self.assertIn('<p>Otherwise, please', content)
+        self.assertIn(' action="http://example.org/tracker/newticket"',
+                      content)
+
+    def _send_error(self, admin_trac_url='.', perm=None,
+                    error_klass=ValueError):
+        self.env.config.set('project', 'admin_trac_url', admin_trac_url)
+        self.assertEquals(admin_trac_url, self.env.project_admin_trac_url)
+
+        content = StringIO()
+        result = {'status': None, 'headers': []}
+        def write(data):
+            content.write(data)
+        def start_response(status, headers, exc_info=None):
+            result['status'] = status
+            result['headers'].extend(headers)
+            return write
+        environ = _make_environ()
+        req = _make_req(environ, start_response)
+        try:
+            raise error_klass('Oops!')
+        except:
+            exc_info = sys.exc_info()
+        data = {'title': 'Internal Error',
+                'type': ('internal', 'TracError')[error_klass is TracError],
+                'message': 'Oops!', 'traceback': None, 'frames': [],
+                'shorten_line': shorten_line,
+                'plugins': [], 'faulty_plugins': [],
+                'tracker': 'http://example.org/tracker', 'tracker_args': {},
+                'description': '', 'description_en': '',
+                'get_systeminfo': lambda: ()}
+        if perm is not None:
+            data['perm'] = perm
+
+        self.assertRaises(RequestDone, req.send_error, exc_info, env=self.env,
+                          data=data)
+        content = content.getvalue().decode('utf-8')
+        self.assertIn('<!DOCTYPE ', content)
+        self.assertEquals('500', result['status'].split()[0])
+        self.assertIn(('Content-Type', 'text/html;charset=utf-8'),
+                      result['headers'])
+        return content
+
+
 class ParseArgListTestCase(unittest.TestCase):
 
     def test_qs_str(self):
@@ -169,9 +365,11 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(RequestTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(ParseArgListTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(RequestTestCase))
+    suite.addTest(unittest.makeSuite(SendErrorTestCase))
+    suite.addTest(unittest.makeSuite(ParseArgListTestCase))
     return suite
 
+
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/web/tests/auth.py b/trac/trac/web/tests/auth.py
index f7a3c06..f7149f7 100644
--- a/trac/trac/web/tests/auth.py
+++ b/trac/trac/web/tests/auth.py
@@ -1,7 +1,19 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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 os
 
+import trac.tests.compat
 from trac.core import TracError
 from trac.test import EnvironmentStub, Mock
 from trac.web.auth import BasicAuthentication, LoginModule
@@ -24,7 +36,7 @@
         req = Mock(incookie=Cookie(), href=Href('/trac.cgi'),
                    remote_addr='127.0.0.1', remote_user=None,
                    base_path='/trac.cgi')
-        self.assertEqual(None, self.module.authenticate(req))
+        self.assertIsNone(self.module.authenticate(req))
 
     def test_unknown_cookie_access(self):
         incookie = Cookie()
@@ -33,7 +45,7 @@
                    incookie=incookie, outcookie=Cookie(),
                    remote_addr='127.0.0.1', remote_user=None,
                    base_path='/trac.cgi')
-        self.assertEqual(None, self.module.authenticate(req))
+        self.assertIsNone(self.module.authenticate(req))
 
     def test_known_cookie_access(self):
         self.env.db_transaction("""
@@ -46,7 +58,7 @@
                    href=Href('/trac.cgi'), base_path='/trac.cgi',
                    remote_addr='127.0.0.1', remote_user=None)
         self.assertEqual('john', self.module.authenticate(req))
-        self.failIf('auth_cookie' in req.outcookie)
+        self.assertNotIn('auth_cookie', req.outcookie)
 
     def test_known_cookie_ip_check_enabled(self):
         self.env.config.set('trac', 'check_auth_ip', 'yes')
@@ -60,8 +72,8 @@
                    incookie=incookie, outcookie=outcookie,
                    remote_addr='192.168.0.100', remote_user=None,
                    base_path='/trac.cgi')
-        self.assertEqual(None, self.module.authenticate(req))
-        self.failIf('trac_auth' not in req.outcookie)
+        self.assertIsNone(self.module.authenticate(req))
+        self.assertIn('trac_auth', req.outcookie)
 
     def test_known_cookie_ip_check_disabled(self):
         self.env.config.set('trac', 'check_auth_ip', 'no')
@@ -75,7 +87,7 @@
                    href=Href('/trac.cgi'), base_path='/trac.cgi',
                    remote_addr='192.168.0.100', remote_user=None)
         self.assertEqual('john', self.module.authenticate(req))
-        self.failIf('auth_cookie' in req.outcookie)
+        self.assertNotIn('auth_cookie', req.outcookie)
 
     def test_login(self):
         outcookie = Cookie()
@@ -87,10 +99,10 @@
                    authname='john', base_path='/trac.cgi')
         self.module._do_login(req)
 
-        assert outcookie.has_key('trac_auth'), '"trac_auth" Cookie not set'
+        self.assertIn('trac_auth', outcookie, '"trac_auth" Cookie not set')
         auth_cookie = outcookie['trac_auth'].value
 
-        self.assertEquals([('john', '127.0.0.1')], self.env.db_query(
+        self.assertEqual([('john', '127.0.0.1')], self.env.db_query(
             "SELECT name, ipnr FROM auth_cookie WHERE cookie=%s",
             (auth_cookie,)))
 
@@ -108,9 +120,9 @@
                    authname='anonymous', base_path='/trac.cgi')
         self.module._do_login(req)
 
-        assert outcookie.has_key('trac_auth'), '"trac_auth" Cookie not set'
+        self.assertIn('trac_auth', outcookie, '"trac_auth" Cookie not set')
         auth_cookie = outcookie['trac_auth'].value
-        self.assertEquals([('john', '127.0.0.1')], self.env.db_query(
+        self.assertEqual([('john', '127.0.0.1')], self.env.db_query(
             "SELECT name, ipnr FROM auth_cookie WHERE cookie=%s",
             (auth_cookie,)))
 
@@ -140,7 +152,7 @@
         req = Mock(incookie=incookie, authname='john',
                    href=Href('/trac.cgi'), base_path='/trac.cgi',
                    remote_addr='127.0.0.1', remote_user='tom')
-        self.assertRaises(AssertionError, self.module._do_login, req)
+        self.assertRaises(TracError, self.module._do_login, req)
 
     def test_logout(self):
         self.env.db_transaction("""
@@ -151,20 +163,38 @@
         outcookie = Cookie()
         req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
                    incookie=incookie, outcookie=outcookie,
-                   remote_addr='127.0.0.1', remote_user=None, authname='john',
-                   base_path='/trac.cgi')
+                   remote_addr='127.0.0.1', remote_user=None,
+                   authname='john', method='POST', base_path='/trac.cgi')
         self.module._do_logout(req)
-        self.failIf('trac_auth' not in outcookie)
-        self.failIf(self.env.db_query(
+        self.assertIn('trac_auth', outcookie)
+        self.assertFalse(self.env.db_query(
             "SELECT name, ipnr FROM auth_cookie WHERE name='john'"))
 
     def test_logout_not_logged_in(self):
         req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
                    incookie=Cookie(), outcookie=Cookie(),
                    remote_addr='127.0.0.1', remote_user=None,
-                   authname='anonymous', base_path='/trac.cgi')
+                   authname='anonymous', method='POST', base_path='/trac.cgi')
         self.module._do_logout(req) # this shouldn't raise an error
 
+    def test_logout_protect(self):
+        self.env.db_transaction("""
+            INSERT INTO auth_cookie (cookie, name, ipnr)
+            VALUES ('123', 'john', '127.0.0.1')""")
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        outcookie = Cookie()
+        req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
+                   incookie=incookie, outcookie=outcookie,
+                   remote_addr='127.0.0.1', remote_user=None,
+                   authname='john', method='GET', base_path='/trac.cgi')
+        self.module._do_logout(req)
+        self.assertNotIn('trac_auth', outcookie)
+        self.assertEqual(
+            [('john', '127.0.0.1')],
+            self.env.db_query("SELECT name, ipnr FROM auth_cookie "
+                              "WHERE cookie='123'"))
+
 
 class BasicAuthenticationTestCase(unittest.TestCase):
     def setUp(self):
@@ -175,23 +205,24 @@
         self.auth = None
 
     def test_crypt(self):
-        self.assert_(self.auth.test('crypt', 'crypt'))
-        self.assert_(not self.auth.test('crypt', 'other'))
+        self.assertTrue(self.auth.test('crypt', 'crypt'))
+        self.assertFalse(self.auth.test('crypt', 'other'))
 
     def test_md5(self):
-        self.assert_(self.auth.test('md5', 'md5'))
-        self.assert_(not self.auth.test('md5', 'other'))
+        self.assertTrue(self.auth.test('md5', 'md5'))
+        self.assertFalse(self.auth.test('md5', 'other'))
 
     def test_sha(self):
-        self.assert_(self.auth.test('sha', 'sha'))
-        self.assert_(not self.auth.test('sha', 'other'))
+        self.assertTrue(self.auth.test('sha', 'sha'))
+        self.assertFalse(self.auth.test('sha', 'other'))
 
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(LoginModuleTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(BasicAuthenticationTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(LoginModuleTestCase))
+    suite.addTest(unittest.makeSuite(BasicAuthenticationTestCase))
     return suite
 
+
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/web/tests/cgi_frontend.py b/trac/trac/web/tests/cgi_frontend.py
index 770cdf0..c96c8a8 100644
--- a/trac/trac/web/tests/cgi_frontend.py
+++ b/trac/trac/web/tests/cgi_frontend.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
+
 #from trac.web.cgi_frontend import CGIRequest
 
 import unittest
@@ -8,7 +21,8 @@
 
 
 def suite():
-    return unittest.makeSuite(CGIRequestTestCase, 'test')
+    return unittest.makeSuite(CGIRequestTestCase)
+
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/web/tests/chrome.py b/trac/trac/web/tests/chrome.py
index 07ba90e..b6d4267 100644
--- a/trac/trac/web/tests/chrome.py
+++ b/trac/trac/web/tests/chrome.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2013 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -11,23 +11,31 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://trac.edgewall.org/log/.
 
-from trac.core import Component, implements
-from trac.test import EnvironmentStub
+import os
+import shutil
+import tempfile
+import unittest
+
+import trac.tests.compat
+from trac.core import Component, TracError, implements
+from trac.test import EnvironmentStub, locale_en
 from trac.tests.contentgen import random_sentence
+from trac.util import create_file
 from trac.web.chrome import (
     Chrome, INavigationContributor, add_link, add_meta, add_notice, add_script,
     add_script_data, add_stylesheet, add_warning)
 from trac.web.href import Href
 
-import unittest
 
 class Request(object):
     locale = None
+    args = {}
     def __init__(self, **kwargs):
         self.chrome = {}
         for k, v in kwargs.items():
             setattr(self, k, v)
 
+
 class ChromeTestCase(unittest.TestCase):
 
     def setUp(self):
@@ -76,14 +84,24 @@
         add_script(req, 'common/js/trac.js')
         add_script(req, 'common/js/trac.js')
         add_script(req, 'http://example.com/trac.js')
+        add_script(req, '//example.com/trac.js')
+        add_script(req, '/dynamic.js')
+        add_script(req, 'plugin/js/plugin.js')
         scripts = req.chrome['scripts']
-        self.assertEqual(2, len(scripts))
+        self.assertEqual(5, len(scripts))
         self.assertEqual('text/javascript', scripts[0]['type'])
         self.assertEqual('/trac.cgi/chrome/common/js/trac.js',
                          scripts[0]['href'])
         self.assertEqual('text/javascript', scripts[1]['type'])
         self.assertEqual('http://example.com/trac.js',
                          scripts[1]['href'])
+        self.assertEqual('text/javascript', scripts[2]['type'])
+        self.assertEqual('//example.com/trac.js',
+                         scripts[2]['href'])
+        self.assertEqual('/trac.cgi/dynamic.js',
+                         scripts[3]['href'])
+        self.assertEqual('/trac.cgi/chrome/plugin/js/plugin.js',
+                         scripts[4]['href'])
 
     def test_add_script_data(self):
         req = Request(href=Href('/trac.cgi'))
@@ -97,14 +115,24 @@
         add_stylesheet(req, 'common/css/trac.css')
         add_stylesheet(req, 'common/css/trac.css')
         add_stylesheet(req, 'https://example.com/trac.css')
+        add_stylesheet(req, '//example.com/trac.css')
+        add_stylesheet(req, '/dynamic.css')
+        add_stylesheet(req, 'plugin/css/plugin.css')
         links = req.chrome['links']['stylesheet']
-        self.assertEqual(2, len(links))
+        self.assertEqual(5, len(links))
         self.assertEqual('text/css', links[0]['type'])
         self.assertEqual('/trac.cgi/chrome/common/css/trac.css',
                          links[0]['href'])
         self.assertEqual('text/css', links[1]['type'])
         self.assertEqual('https://example.com/trac.css',
                          links[1]['href'])
+        self.assertEqual('text/css', links[2]['type'])
+        self.assertEqual('//example.com/trac.css',
+                         links[2]['href'])
+        self.assertEqual('/trac.cgi/dynamic.css',
+                         links[3]['href'])
+        self.assertEqual('/trac.cgi/chrome/plugin/css/plugin.css',
+                         links[4]['href'])
 
     def test_add_stylesheet_media(self):
         req = Request(base_path='/trac.cgi', href=Href('/trac.cgi'))
@@ -152,8 +180,8 @@
         # Verify that no logo data is put in the HDF if no logo is configured
         self.env.config.set('header_logo', 'src', '')
         info = Chrome(self.env).prepare_request(req)
-        assert 'src' not in info['logo']
-        assert 'src_abs' not in info['logo']
+        self.assertNotIn('src', info['logo'])
+        self.assertNotIn('src_abs', info['logo'])
 
         # Test with a relative path to the logo image
         self.env.config.set('header_logo', 'src', 'foo.png')
@@ -204,8 +232,8 @@
         # No icon set in config, so no icon links
         self.env.config.set('project', 'icon', '')
         links = chrome.prepare_request(req)['links']
-        assert 'icon' not in links
-        assert 'shortcut icon' not in links
+        self.assertNotIn('icon', links)
+        self.assertNotIn('shortcut icon', links)
 
         # Relative URL for icon config option
         self.env.config.set('project', 'icon', 'foo.ico')
@@ -304,9 +332,60 @@
         self.assertEqual('test1', items[0]['name'])
         self.assertEqual('test2', items[1]['name'])
 
+    def test_add_jquery_ui_timezone_list_has_z(self):
+        chrome = Chrome(self.env)
+
+        req = Request(href=Href('/trac.cgi'), lc_time='iso8601')
+        chrome.add_jquery_ui(req)
+        self.assertIn({'value': 'Z', 'label': '+00:00'},
+                      req.chrome['script_data']['jquery_ui']['timezone_list'])
+
+        req = Request(href=Href('/trac.cgi'), lc_time=locale_en)
+        chrome.add_jquery_ui(req)
+        self.assertIn({'value': 'Z', 'label': '+00:00'},
+                      req.chrome['script_data']['jquery_ui']['timezone_list'])
+
+
+class ChromeTestCase2(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(path=tempfile.mkdtemp())
+        self.chrome = Chrome(self.env)
+
+    def tearDown(self):
+        shutil.rmtree(self.env.path)
+
+    def test_malicious_filename_raises(self):
+        req = Request(path_info='/chrome/site/../conf/trac.ini')
+        self.assertTrue(self.chrome.match_request(req))
+        self.assertRaises(TracError, self.chrome.process_request, req)
+
+    def test_empty_shared_htdocs_dir_raises_file_not_found(self):
+        req = Request(path_info='/chrome/shared/trac_logo.png')
+        self.assertEqual('', self.chrome.shared_htdocs_dir)
+        self.assertTrue(self.chrome.match_request(req))
+        from trac.web.api import HTTPNotFound
+        self.assertRaises(HTTPNotFound, self.chrome.process_request, req)
+
+    def test_shared_htdocs_dir_file_is_found(self):
+        from trac.web.api import RequestDone
+        def send_file(path, mimetype):
+            raise RequestDone
+        req = Request(path_info='/chrome/shared/trac_logo.png',
+                      send_file=send_file)
+        shared_htdocs_dir = os.path.join(self.env.path, 'chrome', 'shared')
+        os.makedirs(shared_htdocs_dir)
+        create_file(os.path.join(shared_htdocs_dir, 'trac_logo.png'))
+        self.env.config.set('inherit', 'htdocs_dir', shared_htdocs_dir)
+        self.assertTrue(self.chrome.match_request(req))
+        self.assertRaises(RequestDone, self.chrome.process_request, req)
+
 
 def suite():
-    return unittest.makeSuite(ChromeTestCase, 'test')
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(ChromeTestCase))
+    suite.addTest(unittest.makeSuite(ChromeTestCase2))
+    return suite
 
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
diff --git a/trac/trac/web/tests/href.py b/trac/trac/web/tests/href.py
index 04aed9b..d6ef1aa 100644
--- a/trac/trac/web/tests/href.py
+++ b/trac/trac/web/tests/href.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005-2007 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
 #
@@ -15,6 +15,7 @@
 import doctest
 import unittest
 
+import trac.tests.compat
 import trac.web.href
 
 
@@ -36,9 +37,9 @@
         self.assertEqual('/base/sub/other/', href('sub', 'other', ''))
         self.assertEqual('/base/with%20special%26chars',
                          href('with special&chars'))
-        assert href('page', param='value', other='other value', more=None) in [
+        self.assertIn(href('page', param='value', other='other value', more=None), [
             '/base/page?param=value&other=other+value',
-            '/base/page?other=other+value&param=value']
+            '/base/page?other=other+value&param=value'])
         self.assertEqual('/base/page?param=multiple&param=values',
                          href('page', param=['multiple', 'values']))
 
@@ -73,9 +74,10 @@
         self.assertEqual('/sub/other/', href('sub', 'other', ''))
         self.assertEqual('/with%20special%26chars',
                          href('with special&chars'))
-        assert href('page', param='value', other='other value', more=None) in [
-            '/page?param=value&other=other+value',
-            '/page?other=other+value&param=value']
+        self.assertIn(
+            href('page', param='value', other='other value', more=None),
+            ['/page?param=value&other=other+value',
+             '/page?other=other+value&param=value'])
         self.assertEqual('/page?param=multiple&param=values',
                          href('page', param=['multiple', 'values']))
 
@@ -96,9 +98,9 @@
                          href(param=MyList(['test', 'other'])))
         self.assertEqual('/base?param=test&param=other',
                          href(param=MyTuple(['test', 'other'])))
-        assert href(MyDict(param='value', other='other value')) in [
+        self.assertIn(href(MyDict(param='value', other='other value')), [
             '/base?param=value&other=other+value',
-            '/base?other=other+value&param=value']
+            '/base?other=other+value&param=value'])
         self.assertEqual('/base?param=value&other=other+value',
                          href(MyList([('param', 'value'), ('other', 'other value')])))
         self.assertEqual('/base?param=value&other=other+value',
@@ -108,7 +110,7 @@
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(doctest.DocTestSuite(trac.web.href))
-    suite.addTest(unittest.makeSuite(HrefTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(HrefTestCase))
     return suite
 
 if __name__ == '__main__':
diff --git a/trac/trac/web/tests/main.py b/trac/trac/web/tests/main.py
index a335b77..2a00208 100644
--- a/trac/trac/web/tests/main.py
+++ b/trac/trac/web/tests/main.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2010 Edgewall Software
+# Copyright (C) 2010-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -11,12 +11,83 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://trac.edgewall.org/log/.
 
-from trac.util import create_file
-from trac.web.main import get_environments
-
+import os.path
 import tempfile
 import unittest
-import os.path
+
+from trac.core import Component, ComponentMeta, TracError, implements
+from trac.test import EnvironmentStub, Mock
+from trac.util import create_file
+from trac.web.auth import IAuthenticator
+from trac.web.main import RequestDispatcher, get_environments
+
+
+class AuthenticateTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(disable=['trac.web.auth.LoginModule'])
+        self.request_dispatcher = RequestDispatcher(self.env)
+        self.req = Mock(chrome={'warnings': []})
+        # Make sure we have no external components hanging around in the
+        # component registry
+        self.old_registry = ComponentMeta._registry
+        ComponentMeta._registry = {}
+
+    def tearDown(self):
+        # Restore the original component registry
+        ComponentMeta._registry = self.old_registry
+
+    def test_authenticate_returns_first_successful(self):
+        class SuccessfulAuthenticator1(Component):
+            implements(IAuthenticator)
+            def authenticate(self, req):
+                return 'user1'
+        class SuccessfulAuthenticator2(Component):
+            implements(IAuthenticator)
+            def authenticate(self, req):
+                return 'user2'
+        self.assertEqual(2, len(self.request_dispatcher.authenticators))
+        self.assertIsInstance(self.request_dispatcher.authenticators[0],
+                              SuccessfulAuthenticator1)
+        self.assertIsInstance(self.request_dispatcher.authenticators[1],
+                              SuccessfulAuthenticator2)
+        self.assertEqual('user1',
+                         self.request_dispatcher.authenticate(self.req))
+
+    def test_authenticate_skips_unsuccessful(self):
+        class UnsuccessfulAuthenticator(Component):
+            implements(IAuthenticator)
+            def authenticate(self, req):
+                return None
+        class SuccessfulAuthenticator(Component):
+            implements(IAuthenticator)
+            def authenticate(self, req):
+                return 'user'
+        self.assertEqual(2, len(self.request_dispatcher.authenticators))
+        self.assertIsInstance(self.request_dispatcher.authenticators[0],
+                              UnsuccessfulAuthenticator)
+        self.assertIsInstance(self.request_dispatcher.authenticators[1],
+                              SuccessfulAuthenticator)
+        self.assertEqual('user',
+                         self.request_dispatcher.authenticate(self.req))
+
+    def test_authenticate_raises(self):
+        class RaisingAuthenticator(Component):
+            implements(IAuthenticator)
+            def authenticate(self, req):
+                raise TracError("Bad attempt")
+        class SuccessfulAuthenticator(Component):
+            implements(IAuthenticator)
+            def authenticate(self, req):
+                return 'user'
+        self.assertEqual(2, len(self.request_dispatcher.authenticators))
+        self.assertIsInstance(self.request_dispatcher.authenticators[0],
+                              RaisingAuthenticator)
+        self.assertIsInstance(self.request_dispatcher.authenticators[1],
+                              SuccessfulAuthenticator)
+        self.assertEqual('anonymous',
+                         self.request_dispatcher.authenticate(self.req))
+        self.assertEqual(1, len(self.req.chrome['warnings']))
 
 
 class EnvironmentsTestCase(unittest.TestCase):
@@ -51,32 +122,33 @@
                     for project in projects)
 
     def test_default_tracignore(self):
-        self.assertEquals(self.env_paths(['mydir1', 'mydir2']),
-                          get_environments(self.environ))
+        self.assertEqual(self.env_paths(['mydir1', 'mydir2']),
+                         get_environments(self.environ))
 
     def test_empty_tracignore(self):
         create_file(self.tracignore)
-        self.assertEquals(self.env_paths(['mydir1', 'mydir2', '.hidden_dir']),
-                          get_environments(self.environ))
+        self.assertEqual(self.env_paths(['mydir1', 'mydir2', '.hidden_dir']),
+                         get_environments(self.environ))
 
     def test_qmark_pattern_tracignore(self):
         create_file(self.tracignore, 'mydir?')
-        self.assertEquals(self.env_paths(['.hidden_dir']),
-                          get_environments(self.environ))
+        self.assertEqual(self.env_paths(['.hidden_dir']),
+                         get_environments(self.environ))
 
     def test_star_pattern_tracignore(self):
         create_file(self.tracignore, 'my*\n.hidden_dir')
-        self.assertEquals({}, get_environments(self.environ))
+        self.assertEqual({}, get_environments(self.environ))
 
     def test_combined_tracignore(self):
         create_file(self.tracignore, 'my*i?1\n\n#mydir2')
-        self.assertEquals(self.env_paths(['mydir2', '.hidden_dir']),
-                          get_environments(self.environ))
+        self.assertEqual(self.env_paths(['mydir2', '.hidden_dir']),
+                         get_environments(self.environ))
 
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(EnvironmentsTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(AuthenticateTestCase))
+    suite.addTest(unittest.makeSuite(EnvironmentsTestCase))
     return suite
 
 
diff --git a/trac/trac/web/tests/session.py b/trac/trac/web/tests/session.py
index b80bc38..b94cd76 100644
--- a/trac/trac/web/tests/session.py
+++ b/trac/trac/web/tests/session.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
+
 from __future__ import with_statement
 
 from Cookie import SimpleCookie as Cookie
@@ -5,6 +18,7 @@
 from datetime import datetime
 import unittest
 
+import trac.tests.compat
 from trac.test import EnvironmentStub, Mock
 from trac.web.session import DetachedSession, Session, PURGE_AGE, \
                              UPDATE_INTERVAL, SessionAdmin
@@ -88,8 +102,8 @@
         req = Mock(authname='anonymous', base_path='/', incookie=incookie,
                    outcookie=outcookie)
         session = Session(self.env, req)
-        self.assertEquals('123456', session.sid)
-        self.failIf(outcookie.has_key('trac_session'))
+        self.assertEqual('123456', session.sid)
+        self.assertNotIn('trac_session', outcookie)
 
     def test_authenticated_session(self):
         """
@@ -105,7 +119,7 @@
         self.assertEqual('john', session.sid)
         session['foo'] = 'bar'
         session.save()
-        self.assertEquals(0, outcookie['trac_session']['expires'])
+        self.assertEqual(0, outcookie['trac_session']['expires'])
 
     def test_session_promotion(self):
         """
@@ -351,7 +365,7 @@
         req = Mock(authname='anonymous', base_path='/', incookie=incookie,
                    outcookie=Cookie())
         session = Session(self.env, req)
-        self.assert_('foo' not in session)
+        self.assertTrue('foo' not in session)
         session['foo'] = 'baz'
         session.save()
 
@@ -572,8 +586,8 @@
 
 
 def suite():
-    return unittest.makeSuite(SessionTestCase, 'test')
+    return unittest.makeSuite(SessionTestCase)
 
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='suite')
diff --git a/trac/trac/web/tests/wikisyntax.py b/trac/trac/web/tests/wikisyntax.py
index 5be3df8..89cd3c7 100644
--- a/trac/trac/web/tests/wikisyntax.py
+++ b/trac/trac/web/tests/wikisyntax.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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 unittest
 
 from trac.wiki.tests import formatter
diff --git a/trac/trac/web/wsgi.py b/trac/trac/web/wsgi.py
index f4f895f..395d534 100644
--- a/trac/trac/web/wsgi.py
+++ b/trac/trac/web/wsgi.py
@@ -85,6 +85,7 @@
 
         self.headers_set = []
         self.headers_sent = []
+        self.use_chunked = False
 
     def run(self, application):
         """Start the gateway with the given WSGI application."""
@@ -98,8 +99,8 @@
                 for chunk in response:
                     if chunk:
                         self._write(chunk)
-                if not self.headers_sent:
-                    self._write('')
+                if not self.headers_sent or self.use_chunked:
+                    self._write('') # last chunk '\r\n0\r\n' if use_chunked
         finally:
             if hasattr(response, 'close'):
                 response.close()
@@ -193,9 +194,15 @@
 
     def finish(self):
         """We need to help the garbage collector a little."""
-        BaseHTTPRequestHandler.finish(self)
-        self.wfile = None
-        self.rfile = None
+        try:
+            BaseHTTPRequestHandler.finish(self)
+        except (IOError, socket.error), e:
+            # ignore an exception if client disconnects
+            if e.args[0] not in (errno.EPIPE, errno.ECONNRESET, 10053, 10054):
+                raise
+        finally:
+            self.wfile = None
+            self.rfile = None
 
 
 class WSGIServerGateway(WSGIGateway):
@@ -212,12 +219,27 @@
 
         try:
             if not self.headers_sent:
+                # Worry at the last minute about Content-Length. If not
+                # yet set, use either chunked encoding or close connection
                 status, headers = self.headers_sent = self.headers_set
+                if any(n.lower() == 'content-length' for n, v in headers):
+                    self.use_chunked = False
+                else:
+                    self.use_chunked = (
+                        self.environ['SERVER_PROTOCOL'] >= 'HTTP/1.1' and
+                        self.handler.protocol_version >= 'HTTP/1.1')
+                    if self.use_chunked:
+                        headers.append(('Transfer-Encoding', 'chunked'))
+                    else:
+                        headers.append(('Connection', 'close'))
                 self.handler.send_response(int(status[:3]))
                 for name, value in headers:
                     self.handler.send_header(name, value)
                 self.handler.end_headers()
-            self.handler.wfile.write(data)
+            if self.use_chunked:
+                self.handler.wfile.write('%x\r\n%s\r\n' % (len(data), data))
+            else:
+                self.handler.wfile.write(data)
         except (IOError, socket.error), e:
             if e.args[0] in (errno.EPIPE, errno.ECONNRESET, 10053, 10054):
                 # client disconnect
diff --git a/trac/trac/wiki/__init__.py b/trac/trac/wiki/__init__.py
index 41c7476..ba2a0ed 100644
--- a/trac/trac/wiki/__init__.py
+++ b/trac/trac/wiki/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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.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 trac.wiki.api import *
 from trac.wiki.formatter import *
 from trac.wiki.intertrac import *
diff --git a/trac/trac/wiki/default-pages/InterMapTxt b/trac/trac/wiki/default-pages/InterMapTxt
index 15c9305..2305368 100644
--- a/trac/trac/wiki/default-pages/InterMapTxt
+++ b/trac/trac/wiki/default-pages/InterMapTxt
@@ -38,22 +38,53 @@
 == Prefix Definitions ==
 
 {{{
-PEP     http://www.python.org/peps/pep-$1.html    # Python Enhancement Proposal 
+PEP     http://www.python.org/dev/peps/pep-$1/    # Python Enhancement Proposal 
 PythonBug    http://bugs.python.org/issue$1       # Python Issue #$1
 Python-issue http://bugs.python.org/issue$1       # Python Issue #$1
 
 Trac-ML  http://thread.gmane.org/gmane.comp.version-control.subversion.trac.general/ # Message $1 in Trac Mailing List
 trac-dev http://thread.gmane.org/gmane.comp.version-control.subversion.trac.devel/   # Message $1 in Trac Development Mailing List
 
+apidoc http://www.edgewall.org/docs/trac-trunk/html/$1.html # $1 in the API documentation for Trac
+apiref http://www.edgewall.org/docs/trac-trunk/epydoc/$1.html # $1 in the Epydoc API reference for Trac
+
+bitten   http://bitten.edgewall.org/intertrac/    # Bitten's Trac
+
 Mercurial http://www.selenic.com/mercurial/wiki/index.cgi/ # the wiki for the Mercurial distributed SCM
+hg        http://www.selenic.com/hg/rev/$1?rev=$2          # Changeset $1 $2 in Mercurial repository
+hg-issue  http://mercurial.selenic.com/bts/issue           # Issue $1 in Mercurial BTS
 
 RFC       http://tools.ietf.org/html/rfc$1          # IETF's RFC $1
 ISO       http://en.wikipedia.org/wiki/ISO_         # ISO Standard $1 in Wikipedia
 kb        http://support.microsoft.com/kb/$1/en-us/ # Article $1 in Microsoft's Knowledge Base
 
+pypi        http://pypi.python.org/pypi/   # $1 package in the Python Package Index
+CheeseShop  http://pypi.python.org/pypi/           # $1 package in the Python Package Index
+peak        http://peak.telecommunity.com/DevCenter/     # $1 in Python Enterprise Application Kit's Wiki
+setuptools-issue http://bugs.python.org/setuptools/issue # issue$1 in legacy Setuptools tracker
+pypa-setuptools-issue https://bitbucket.org/pypa/setuptools/issue/ # issue #$1 in BitBucket Setuptools tracker
+
+SQLite      http://www.sqlite.org/cvstrac/wiki?p=$1     # $1 page in the CvsTrac for SQLite
+SQLiteTkt   http://www.sqlite.org/cvstrac/tktview?tn=$1 # Ticket $1 in the CvsTrac for SQLite
+
+mysql-bugs  http://bugs.mysql.com/bug.php?id=  # Bug #$1 in MySQL's bug database
+mysql-issue http://bugs.mysql.com/bug.php?id=  # Bug #$1 in MySQL's bug database
+
+MODPYTHON          http://issues.apache.org/jira/browse/MODPYTHON- # Issue $1 in mod_python's JIRA instance
+mod-python-issue   http://issues.apache.org/jira/browse/MODPYTHON- # Issue $1 in mod_python's JIRA instance
+
+SvnWiki     http://www.orcaware.com/svn/wiki/                        # Subversion Wiki
+svnissue    http://subversion.tigris.org/issues/show_bug.cgi?id=     # Subversion issue #$1
+svn-issue   http://subversion.tigris.org/issues/show_bug.cgi?id=     # Subversion issue #$1
+svncset     http://svn.collab.net/viewvc/svn?view=revision&revision= # Subversion [$1]
+
+mod-wsgi    http://code.google.com/p/modwsgi/wiki/                 # mod_wsgi Wiki on Google Code
+mod-wsgi-issue  http://code.google.com/p/modwsgi/issues/detail?id= # mod_wsgi Issue Tracker on Google Code
+
 chromium-issue  http://code.google.com/p/chromium/issues/detail?id=
 
 Django      http://code.djangoproject.com/intertrac/ # Django's Trac
+AgileTrac   http://www.agile-trac.org/intertrac/     # Plugin adding Iterations to Trac
 
 CreoleWiki   http://wikicreole.org/wiki/
 Creole1Wiki  http://wikicreole.org/wiki/
diff --git a/trac/trac/wiki/default-pages/InterTrac b/trac/trac/wiki/default-pages/InterTrac
index ad3ebb1..3aaa3af 100644
--- a/trac/trac/wiki/default-pages/InterTrac
+++ b/trac/trac/wiki/default-pages/InterTrac
@@ -42,8 +42,7 @@
 This configuration has to be done in the TracIni file, `[intertrac]` section.
 
 Example configuration:
-{{{
-...
+{{{#!ini
 [intertrac]
 # -- Example of setting up an alias:
 t = trac
@@ -66,7 +65,7 @@
    ([trac:r3526 r3526] to be precise), then it doesn't know how to dispatch an InterTrac 
    link, and it's up to the local Trac to prepare the correct link. 
    Not all links will work that way, but the most common do. 
-   This is called the compatibility mode, and is `true` by default. 
+   This is called the compatibility mode, and is `false` by default. 
  * If you know that the remote Trac knows how to dispatch InterTrac links, 
    you can explicitly disable this compatibility mode and then ''any'' 
    TracLinks can become an InterTrac link.
diff --git a/trac/trac/wiki/default-pages/PageTemplates b/trac/trac/wiki/default-pages/PageTemplates
index e08bede..8c57735 100644
--- a/trac/trac/wiki/default-pages/PageTemplates
+++ b/trac/trac/wiki/default-pages/PageTemplates
@@ -1,6 +1,6 @@
 = Wiki Page Templates = 
 
-  ''(since [http://trac.edgewall.org/milestone/0.11 0.11])''
+  ''(since [trac:milestone:0.11 0.11])''
 
 The default content for a new wiki page can be chosen from a list of page templates. 
 
diff --git a/trac/trac/wiki/default-pages/TracAccessibility b/trac/trac/wiki/default-pages/TracAccessibility
index f4e17f2..b626639 100644
--- a/trac/trac/wiki/default-pages/TracAccessibility
+++ b/trac/trac/wiki/default-pages/TracAccessibility
@@ -4,9 +4,9 @@
 
 The keyboard shortcuts must be enabled for a session through the [/prefs/keybindings Keyboard Shortcuts] preferences panel.
 
-Trac supports accessibility keys for the most common operations.
+Trac supports accessibility keys for the most common operations. The access keys differ by browser and the following work for several browsers, but see [http://en.wikipedia.org/wiki/Access_key#Access_in_different_browsers access in different browsers] for more details.
  - on Linux platforms, press any of the keys listed below in combination with the `<Alt>` key 
- - on a Mac, use the `<ctrl>` key instead
+ - on a Mac, use the `<Ctrl>` + `<Opt>` key instead
  - on Windows, you need to hit `<Shift> + <Alt> + <Key>`. This works for most browsers (Firefox, Chrome, Safari and Internet Explorer)
 
 == Global Access Keys ==
diff --git a/trac/trac/wiki/default-pages/TracAdmin b/trac/trac/wiki/default-pages/TracAdmin
index 9ca2659..065ff6c 100644
--- a/trac/trac/wiki/default-pages/TracAdmin
+++ b/trac/trac/wiki/default-pages/TracAdmin
@@ -32,7 +32,7 @@
 
 [[TracAdminHelp(initenv)]]
 
-It supports an extra `--inherit` option, which can be used to specify a global configuration file which can be used share settings between several environments. You can also inherit from a shared configuration afterwards, by setting the `[inherit] file` option in the `conf/trac.ini` file in your newly created environment, but the advantage of specifying the inherited configuration file at environment creation time is that only the options ''not'' already specified in the global configuration file will be written in the created environment's `conf/trac.ini` file.
+It supports an extra `--inherit` option, which can be used to specify a global configuration file which can be used to share settings between several environments. You can also inherit from a shared configuration afterwards, by setting the `[inherit] file` option in the `conf/trac.ini` file in your newly created environment, but the advantage of specifying the inherited configuration file at environment creation time is that only the options ''not'' already specified in the global configuration file will be written in the created environment's `conf/trac.ini` file.
 See TracIni#GlobalConfiguration.
 
 Note that in version 0.11 of Trac, `initenv` lost an extra last argument `<templatepath>`, which was used in previous versions to point to the `templates` folder. If you are using the one-liner '`trac-admin /path/to/trac/ initenv <projectname> <db> <repostype> <repospath>`' in the above and getting an error that reads ''''`Wrong number of arguments to initenv: 4`'''', then this is because you're using a `trac-admin` script from an '''older''' version of Trac.
diff --git a/trac/trac/wiki/default-pages/TracBackup b/trac/trac/wiki/default-pages/TracBackup
index c4083db..52adadc 100644
--- a/trac/trac/wiki/default-pages/TracBackup
+++ b/trac/trac/wiki/default-pages/TracBackup
@@ -8,17 +8,15 @@
 == Creating a Backup ==
 
 To create a backup of a live TracEnvironment, simply run:
-{{{
-
-  $ trac-admin /path/to/projenv hotcopy /path/to/backupdir
-
+ {{{#!sh
+$ trac-admin /path/to/projenv hotcopy /path/to/backupdir
 }}}
 
 [wiki:TracAdmin trac-admin] will lock the database while copying.''
 
 The resulting backup directory is safe to handle using standard file-based backup tools like `tar` or `dump`/`restore`.
 
-Please, note, that hotcopy command does not overwrite target directory and when such exists, hotcopy ends with error: `Command failed: [Errno 17] File exists:` This is discussed in [trac:ticket:3198 #3198].
+Please note, the `hotcopy` command will not overwrite a target directory and when such exists the operation ends with error: `Command failed: [Errno 17] File exists:` This is discussed in [trac:ticket:3198 #3198].
 
 === Restoring a Backup ===
 
diff --git a/trac/trac/wiki/default-pages/TracBatchModify b/trac/trac/wiki/default-pages/TracBatchModify
index d209536..0b8d102 100644
--- a/trac/trac/wiki/default-pages/TracBatchModify
+++ b/trac/trac/wiki/default-pages/TracBatchModify
@@ -1,10 +1,14 @@
 = Trac Ticket Batch Modification =
 [[TracGuideToc]]
 
-From [wiki:TracQuery custom query] results Trac provides support for modifying a batch of tickets in one request.
+From [TracQuery custom query] results Trac provides support for modifying a batch of tickets in one request.
 
 To perform a batch modification select the tickets you wish to modify and set the new field values using the section underneath the query results. 
 
 == List fields
 
 The `Keywords` and `Cc` fields are treated as lists, where list items can be added and/or removed in addition of replacing the entire list value. All list field controls accept multiple items (i.e. multiple keywords or cc addresses).
+
+== Excluded fields
+
+Multi-line text fields are not supported, because no valid use-case has been presented for syncing them across several tickets. That restriction applies to the `Description` field as well as to any [TracTicketsCustomFields#AvailableFieldTypesandOptions custom field] of type 'textarea'. However in conjunction with more suitable actions like 'prepend', 'append' or search & replace ([http://trac-hacks.org/ticket/2415 th:#2415]) this could change in future Trac versions.
\ No newline at end of file
diff --git a/trac/trac/wiki/default-pages/TracBrowser b/trac/trac/wiki/default-pages/TracBrowser
index 0609b57..3b28e83 100644
--- a/trac/trac/wiki/default-pages/TracBrowser
+++ b/trac/trac/wiki/default-pages/TracBrowser
@@ -52,20 +52,15 @@
 If you're using a Javascript enabled browser, you'll be able to expand and 
 collapse directories in-place by clicking on the arrow head at the right side of a 
 directory. Alternatively, the [trac:TracKeys keyboard] can also be used for this: 
- - use `'j'` and `'k'` to select the next or previous entry, starting with the first
- - `'o'` (open) to toggle between expanded and collapsed state of the selected 
+ - use `j` and `k` to select the next or previous entry, starting with the first
+ - `o` ('''o'''pen) to toggle between expanded and collapsed state of the selected 
    directory or for visiting the selected file 
- - `'v'` (view, visit) and `'<Enter>'`, same as above
- - `'r'` can be used to force the reload of an already expanded directory
- - `'A'` can be used to directly visit a file in annotate (blame) mode
- - `'L'` to view the log for the selected entry
-If no row has been selected using `'j'` or `'k'` these keys will operate on the entry under the mouse.
+ - `v` ('''v'''iew, '''v'''isit) and `<Enter>`, same as above
+ - `r` can be used to force the '''r'''eload of an already expanded directory
+ - `a` can be used to directly visit a file in '''a'''nnotate (blame) mode
+ - `l` to view the '''l'''og for the selected entry
+If no row has been selected using `j` or `k` these keys will operate on the entry under the mouse.
 
-{{{#!comment
-MMM: I guess that some keys are upper case and some lower to avoid conflicts with browser defined keys.
-I find for example in Firefox and IE on windows that 'a' works as well as 'A' but 'l' does not work for 'L'.
- cboos: 'l' is reserved for Vim like behavior, see #7867
-}}}
 
 For the Subversion backend, some advanced additional features are available:
  - The `svn:needs-lock` property will be displayed
diff --git a/trac/trac/wiki/default-pages/TracCgi b/trac/trac/wiki/default-pages/TracCgi
index ba862ba..1ddae9a 100644
--- a/trac/trac/wiki/default-pages/TracCgi
+++ b/trac/trac/wiki/default-pages/TracCgi
@@ -1,39 +1,38 @@
-= Installing Trac as CGI =
+= Installing Trac as CGI
 
-{{{
-#!div class=important
+{{{#!div class=important
   ''Please note that using Trac via CGI is the slowest deployment method available. It is slower than [TracModPython mod_python], [TracFastCgi FastCGI] and even [trac:TracOnWindowsIisAjp IIS/AJP] on Windows.''
 }}}
 
 CGI script is the entrypoint that web-server calls when a web-request to an application is made. To generate the `trac.cgi` script run:
-{{{
+{{{#!sh
 trac-admin /path/to/env deploy /path/to/www/trac
 }}}
 `trac.cgi` will be in the `cgi-bin` folder inside the given path. ''Make sure it is executable by your web server''. This command also copies `static resource` files to a `htdocs` directory of a given destination.
 
-== Apache web-server configuration ==
+== Apache web-server configuration
 
 In [http://httpd.apache.org/ Apache] there are two ways to run Trac as CGI:
 
  1. Use a `ScriptAlias` directive that maps an URL to the `trac.cgi` script (recommended)
- 2. Copy the `trac.cgi` file into the directory for CGI executables used by your web server (commonly named `cgi-bin`). You can also create a symbolic link, but in that case make sure that the `FollowSymLinks` option is enabled for the `cgi-bin` directory.
+ 1. Copy the `trac.cgi` file into the directory for CGI executables used by your web server (commonly named `cgi-bin`). You can also create a symbolic link, but in that case make sure that the `FollowSymLinks` option is enabled for the `cgi-bin` directory.
 
 To make Trac available at `http://yourhost.example.org/trac` add `ScriptAlias` directive to Apache configuration file, changing `trac.cgi` path to match your installation:
-{{{
+{{{#!sh
 ScriptAlias /trac /path/to/www/trac/cgi-bin/trac.cgi
 }}}
 
  ''Note that this directive requires enabled `mod_alias` module.''
 
 If you're using Trac with a single project you need to set its location using the `TRAC_ENV` environment variable:
-{{{
+{{{#!apache
 <Location "/trac">
   SetEnv TRAC_ENV "/path/to/projectenv"
 </Location>
 }}}
 
 Or to use multiple projects you can specify their common parent directory using the `TRAC_ENV_PARENT_DIR` variable:
-{{{
+{{{#!apache
 <Location "/trac">
   SetEnv TRAC_ENV_PARENT_DIR "/path/to/project/parent/dir"
 </Location>
@@ -41,31 +40,31 @@
 
  ''Note that the `SetEnv` directive requires enabled `mod_env` module. It is also possible to set TRAC_ENV in trac.cgi. Just add the following code between "try:" and "from trac.web ...":''
 
-{{{
+{{{#!python
     import os
     os.environ['TRAC_ENV'] = "/path/to/projectenv"
 }}}
 
  '' Or for TRAC_ENV_PARENT_DIR: ''
 
-{{{
+{{{#!python
     import os
     os.environ['TRAC_ENV_PARENT_DIR'] = "/path/to/project/parent/dir"
 }}}
 
-If you are using the [http://httpd.apache.org/docs/suexec.html Apache suEXEC] feature please see [http://trac.edgewall.org/wiki/ApacheSuexec].
+If you are using the [http://httpd.apache.org/docs/suexec.html Apache suEXEC] feature please see [trac:ApacheSuexec].
 
 On some systems, you ''may'' need to edit the shebang line in the `trac.cgi` file to point to your real Python installation path. On a Windows system you may need to configure Windows to know how to execute a .cgi file (Explorer -> Tools -> Folder Options -> File Types -> CGI).
 
-=== Using WSGI ===
+=== Using WSGI
 
 You can run a [http://henry.precheur.org/python/how_to_serve_cgi WSGI handler] [http://pythonweb.org/projects/webmodules/doc/0.5.3/html_multipage/lib/example-webserver-web-wsgi-simple-cgi.html under CGI].  You can [wiki:TracModWSGI#Thetrac.wsgiscript write your own application function], or use the deployed trac.wsgi's application.
 
-== Mapping Static Resources ==
+== Mapping Static Resources
 
 See TracInstall#MappingStaticResources.
 
-== Adding Authentication ==
+== Adding Authentication
 
 See TracInstall#ConfiguringAuthentication.
 
diff --git a/trac/trac/wiki/default-pages/TracEnvironment b/trac/trac/wiki/default-pages/TracEnvironment
index 7cbdfba..1267cdb 100644
--- a/trac/trac/wiki/default-pages/TracEnvironment
+++ b/trac/trac/wiki/default-pages/TracEnvironment
@@ -13,6 +13,9 @@
 database connection string (explained below).
 
 === Some Useful Tips
+
+ - Place your environment's directory on a filesystem which supports sub-second timestamps, as Trac monitors the timestamp of its configuration files and changes happening on a filesystem with too coarse-grained timestamp resolution may go undetected in Trac < 1.0.2 (this is also true for the location of authentication files when using TracStandalone).
+
  - The user under which the web server runs will require file system write permission to 
  the environment directory and all the files inside. Please remember to set
  the appropriate permissions. The same applies to the source code repository, 
@@ -35,6 +38,8 @@
 file is then stored in the environment directory, and can easily be 
 [wiki:TracBackup backed up] together with the rest of the environment.
 
+Note that if the username or password of the connection string (if applicable) contains the `:`, `/` or `@` characters, they need to be URL encoded.
+
 === SQLite Connection String ===
 The connection string for an SQLite database is:
 {{{
@@ -50,7 +55,6 @@
 {{{
 postgres://johndoe:letmein@localhost/trac
 }}}
-''Note that due to the way the above string is parsed, the "/" and "@" characters cannot be part of the password.''
 
 If PostgreSQL is running on a non-standard port (for example 9342), use:
 {{{
@@ -91,9 +95,8 @@
 
 === MySQL Connection String ===
 
-If you want to use MySQL instead, you'll have to use a
-different connection string. For example, to connect to a MySQL
-database on the same machine called `trac`, that allows access to the
+The format of the MySQL connection string is similar to the examples presented for PostgreSQL, with the `postgres` schema being replaced by `mysql`. For example, to connect to a MySQL
+database on the same machine called `trac`, allowing access to the
 user `johndoe` with the password `letmein`, the mysql connection string is:
 {{{
 mysql://johndoe:letmein@localhost:3306/trac
@@ -153,4 +156,4 @@
 structure, but those are two disjoint entities and they are not and ''must not'' be located at the same place.
 
 ----
-See also: TracAdmin, TracBackup, TracIni, TracGuide
+See also: TracAdmin, TracBackup, TracIni, TracGuide
\ No newline at end of file
diff --git a/trac/trac/wiki/default-pages/TracFastCgi b/trac/trac/wiki/default-pages/TracFastCgi
index f6cedd3..1013f30 100644
--- a/trac/trac/wiki/default-pages/TracFastCgi
+++ b/trac/trac/wiki/default-pages/TracFastCgi
@@ -370,7 +370,7 @@
 Nginx is able to communicate with FastCGI processes, but can not spawn them. So you need to start FastCGI server for Trac separately.
 
  1. Nginx configuration with basic authentication handled by Nginx - confirmed to work on 0.6.32
-{{{
+ {{{
     server {
         listen       10.9.8.7:443;
         server_name  trac.example;
@@ -414,7 +414,7 @@
             # WSGI application name - trac instance prefix.
 	    # (Or ``fastcgi_param  SCRIPT_NAME  /some/prefix``.)
             fastcgi_param  SCRIPT_NAME        "";
-            fastcgi_param  PATH_INFO          $path_info;
+            fastcgi_param  PATH_INFO           $fastcgi_script_name;
 
             ## WSGI NEEDED VARIABLES - trac warns about them
             fastcgi_param  REQUEST_METHOD     $request_method;
@@ -437,10 +437,8 @@
         }
     }
 }}}
-
- 2. Modified trac.fcgi:
-
-{{{
+ 1. Modified trac.fcgi:
+ {{{
 #!/usr/bin/env python
 import os
 sockaddr = '/home/trac/run/instance.sock'
@@ -471,10 +469,8 @@
     print tb.getvalue()
 
 }}}
-
- 3. reload nginx and launch trac.fcgi like that:
-
-{{{
+ 1. reload nginx and launch trac.fcgi like that: 
+ {{{#!sh
 trac@trac.example ~ $ ./trac-standalone-fcgi.py 
 }}}
 
diff --git a/trac/trac/wiki/default-pages/TracFineGrainedPermissions b/trac/trac/wiki/default-pages/TracFineGrainedPermissions
index 604b66e..9fda317 100644
--- a/trac/trac/wiki/default-pages/TracFineGrainedPermissions
+++ b/trac/trac/wiki/default-pages/TracFineGrainedPermissions
@@ -16,9 +16,9 @@
 e.g.
 {{{
 [trac]
-permission_policies = AuthzSourcePolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy
+permission_policies = ReadonlyWikiPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy
 }}}
-This lists the [#AuthzSourcePolicy] described below as the first policy, followed by the !DefaultPermissionPolicy which checks for the traditional coarse grained style permissions described in TracPermissions, and the !LegacyAttachmentPolicy which knows how to use the coarse grained permissions for checking the permissions available on attachments.
+This lists the [#ReadonlyWikiPolicy] which controls readonly access to wiki pages, followed by the !DefaultPermissionPolicy which checks for the traditional coarse grained style permissions described in TracPermissions, and the !LegacyAttachmentPolicy which knows how to use the coarse grained permissions for checking the permissions available on attachments.
 
 Among the possible optional choices, there is [#AuthzPolicy], a very generic permission policy, based on an Authz-style system. See
 [trac:source:branches/0.12-stable/tracopt/perm/authz_policy.py authz_policy.py] for details. 
@@ -30,15 +30,15 @@
 
 === !AuthzPolicy === 
 ==== Configuration ====
-* Install [http://www.voidspace.org.uk/python/configobj.html ConfigObj] (still needed for 0.12).
-* Copy authz_policy.py into your plugins directory (only for Trac 0.11).
+* Install [http://www.voidspace.org.uk/python/configobj.html ConfigObj] (still needed for 0.12 and later).
+* Copy [browser:/trunk/tracopt/perm/authz_policy.py /tracopt/perm/authz_policy.py] to your environment's plugins directory (only for Trac 0.11).
 * Put a [http://swapoff.org/files/authzpolicy.conf authzpolicy.conf] file somewhere, preferably on a secured location on the server, not readable for others than the webuser. If the  file contains non-ASCII characters, the UTF-8 encoding should be used.
 * Update your `trac.ini`:
   1. modify the [TracIni#trac-section permission_policies] entry in the `[trac]` section
 {{{
 [trac]
 ...
-permission_policies = AuthzPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy
+permission_policies = AuthzPolicy, ReadonlyWikiPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy
 }}}
   1. add a new `[authz_policy]` section
 {{{
@@ -90,14 +90,14 @@
 [wiki:WikiStart]
 }}}
 
-  Example: Match the attachment `wiki:WikiStart@117/attachment/FOO.JPG@*`
+  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]
+[wiki:WikiStart@*/attachment:*]
+[wiki:WikiStart@117/attachment:FOO.JPG]
 }}}
 
 * Sections are checked against the current Trac resource descriptor '''IN ORDER''' of
@@ -160,10 +160,15 @@
 john = BROWSER_VIEW, FILE_VIEW
 # John has BROWSER_VIEW and FILE_VIEW for the entire test_repo
 
+# The default repository (requires Trac 1.0.2 or later):
+[repository:@*]
+john = BROWSER_VIEW, FILE_VIEW
+# John has BROWSER_VIEW and FILE_VIEW for the entire default repository
+
 # All repositories:
 [repository:*@*]
 jack = BROWSER_VIEW, FILE_VIEW
-# John has BROWSER_VIEW and FILE_VIEW for all repositories
+# Jack has BROWSER_VIEW and FILE_VIEW for all repositories
 }}}
 
 Very fine grain repository access:
@@ -271,7 +276,7 @@
 
 {{{ 
 [trac]
-permission_policies = AuthzSourcePolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy
+permission_policies = AuthzSourcePolicy, ReadonlyWikiPolicy, DefaultPermissionPolicy, LegacyAttachmentPolicy
 }}}
 
 ==== Subversion Configuration ====
@@ -289,6 +294,35 @@
 
 For information about how to restrict access to entire projects in a multiple project environment see [trac:wiki:TracMultipleProjectsSVNAccess]
 
+=== ReadonlyWikiPolicy
+
+Since 1.1.2, the read-only attribute of wiki pages is enabled and enforced when `ReadonlyWikiPolicy` is in the list of active permission policies. The default for new Trac installations in 1.1.2 and later is:
+{{{
+[trac]
+permission_policies = ReadonlyWikiPolicy,
+ DefaultPermissionPolicy,
+ LegacyAttachmentPolicy
+}}}
+
+When upgrading from earlier versions of Trac, `ReadonlyWikiPolicy` will be appended to the list of `permission_policies` when upgrading the environment, provided that `permission_policies` has the default value. If any non-default `permission_polices` are active, `ReadonlyWikiPolicy` **will need to be manually added** to the list. A message will be echoed to the console when upgrading the environment, indicating if any action needs to be taken.
+
+**!ReadonlyWikiPolicy must be listed //before// !DefaultPermissionPolicy**. The latter returns `True` to allow modify, delete or rename actions when the user has the respective `WIKI_*` permission, without consideration for the read-only attribute.
+
+The `ReadonlyWikiPolicy` returns `False` to deny modify, delete and rename actions on wiki pages when the page has the read-only attribute set and the user does not have `WIKI_ADMIN`, regardless of `WIKI_MODIFY`, `WIKI_DELETE` and `WIKI_RENAME` permissions. It returns `None` for all other cases.
+
+When active, the [#AuthzPolicy] should therefore come before `ReadonlyWikiPolicy`, allowing it to grant or deny the actions on individual resources, which is the usual ordering for `AuthzPolicy` in the `permission_policies` list.
+{{{
+[trac]
+permission_policies = AuthzPolicy,
+ ReadonlyWikiPolicy,
+ DefaultPermissionPolicy,
+ LegacyAttachmentPolicy
+}}}
+
+The placement of [#AuthzSourcePolicy] relative to `ReadonlyWikiPolicy` does not matter since they don't perform checks on the same realms.
+
+For all other permission policies, the user will need to decide the proper ordering. Generally, if the permission policy should be capable of overriding the check performed by `ReadonlyWikiPolicy`, it should come before `ReadonlyWikiPolicy` in the list. If the `ReadonlyWikiPolicy` should override the check performed by another permission policy, as is the case for `DefaultPermissionPolicy`, then `ReadonlyWikiPolicy` should come first.
+
 == Debugging permissions
 In trac.ini set:
 {{{
@@ -305,7 +339,6 @@
 
 to understand what checks are being performed. See the sourced documentation of the plugin for more info.
 
-
 ----
 See also: TracPermissions,
 [http://trac-hacks.org/wiki/FineGrainedPageAuthzEditorPlugin TracHacks:FineGrainedPageAuthzEditorPlugin] for a simple editor plugin.
\ No newline at end of file
diff --git a/trac/trac/wiki/default-pages/TracGuide b/trac/trac/wiki/default-pages/TracGuide
index 1078343..e65debf 100644
--- a/trac/trac/wiki/default-pages/TracGuide
+++ b/trac/trac/wiki/default-pages/TracGuide
@@ -60,4 +60,4 @@
  * [trac:TracDev] and [trac:TracDev/ApiDocs API docs] — Trac Developer documentation
  * TracSupport — How to get more information
 
-If you are looking for a good place to ask a question about Trac, look no further than the [http://trac.edgewall.org/wiki/MailingList MailingList]. It provides a friendly environment to discuss openly among Trac users and developers.
+If you are looking for a good place to ask a question about Trac, look no further than the [trac:MailingList MailingList]. It provides a friendly environment to discuss openly among Trac users and developers.
diff --git a/trac/trac/wiki/default-pages/TracImport b/trac/trac/wiki/default-pages/TracImport
index bf6ad98..65d8d04 100644
--- a/trac/trac/wiki/default-pages/TracImport
+++ b/trac/trac/wiki/default-pages/TracImport
@@ -18,7 +18,7 @@
 
  [http://trac-hacks.org/wiki/BugzillaIssueTrackingPlugin BugzillaIssueTrackingPlugin] :: integrates Bugzilla into Trac keeping TracLinks
 
-Ticket data can be imported from Bugzilla using the [http://trac.edgewall.org/browser/trunk/contrib/bugzilla2trac.py bugzilla2trac.py] script, available in the contrib/ directory of the Trac distribution.
+Ticket data can be imported from Bugzilla using the [trac:browser:trunk/contrib/bugzilla2trac.py bugzilla2trac.py] script, available in the contrib/ directory of the Trac distribution.
 
 {{{
 $ bugzilla2trac.py
@@ -83,14 +83,14 @@
 
  [http://trac-hacks.org/wiki/SfnToTracScript SfnToTracScript] :: importer of !SourceForge's new backup file (originated from #Trac3521)
 
-Also, ticket data can be imported from Sourceforge using the [http://trac.edgewall.org/browser/trunk/contrib/sourceforge2trac.py sourceforge2trac.py] script, available in the contrib/ directory of the Trac distribution.
+Also, ticket data can be imported from Sourceforge using the [trac:browser:trunk/contrib/sourceforge2trac.py sourceforge2trac.py] script, available in the contrib/ directory of the Trac distribution.
 
 == Other ==
 
 Since trac uses a SQL database to store the data, you can import from other systems by examining the database tables. Just go into [http://www.sqlite.org/sqlite.html sqlite] command line to look at the tables and import into them from your application.
 
 === Comma delimited file - CSV ===
-See [http://trac.edgewall.org/attachment/wiki/TracSynchronize/csv2trac.2.py csv2trac.2.py] for details.  This approach is particularly useful if one needs to enter a large number of tickets by hand. (note that the ticket type type field, (task etc...) is also needed for this script to work with more recent Trac releases)
+See [trac:attachment:csv2trac.2.py:wiki:TracSynchronize csv2trac.2.py] for details.  This approach is particularly useful if one needs to enter a large number of tickets by hand. (note that the ticket type type field, (task etc...) is also needed for this script to work with more recent Trac releases)
 Comments on script: The script has an error on line 168, ('Ticket' needs to be 'ticket').  Also, the listed values for severity and priority are swapped. 
 
 ----
diff --git a/trac/trac/wiki/default-pages/TracIni b/trac/trac/wiki/default-pages/TracIni
index 4e518d2..91cb0a8 100644
--- a/trac/trac/wiki/default-pages/TracIni
+++ b/trac/trac/wiki/default-pages/TracIni
@@ -11,7 +11,7 @@
 
 == Global Configuration ==
 
-In versions prior to 0.11, the global configuration was by default located in `$prefix/share/trac/conf/trac.ini` or /etc/trac/trac.ini, depending on the distribution. If you're upgrading, you may want to specify that file to inherit from.  Literally, when you're upgrading to 0.11, you have to add an `[inherit]` section to your project's `trac.ini` file. Additionally, you have to move your customized templates and common images from `$prefix/share/trac/...` to the new location.
+In versions prior to 0.11, the global configuration was by default located in `$prefix/share/trac/conf/trac.ini` or `/etc/trac/trac.ini`, depending on the distribution. If you're upgrading, you may want to specify that file to inherit from.  Literally, when you're upgrading to 0.11, you have to add an `[inherit]` section to your project's `trac.ini` file. Additionally, you have to move your customized templates and common images from `$prefix/share/trac/...` to the new location.
 
 Global options will be merged with the environment-specific options, where local options override global options. The options file is specified as follows:
 {{{
@@ -30,7 +30,21 @@
 
 This is a brief reference of available configuration options, and their default settings.
 
+ ''Note that the [hg], [spam-filter], [translatedpages], [vote], [wikiextras] and [wikiextras-symbols] sections below are added by plugins enabled on this Trac, and therefore won't be part of a default installation.''
+
+{{{ 
+#!comment 
+Suggest your documentation fixes in the Discussion section at  
+the bottom of the page, or better send us patches against 
+the corresponding docstrings you'll find in the code!
+
+Please don't waste your time by editing the HTML code below, changes won't be picked up. 
+}}}
 [[TracIni]]
 
+== Discussion == 
+''Please discuss documentation changes here. Even better, send us documentation patches against the code, either on Trac-dev or on new tickets.'' 
+
+
 ----
 See also: TracGuide, TracAdmin, TracEnvironment
diff --git a/trac/trac/wiki/default-pages/TracInstall b/trac/trac/wiki/default-pages/TracInstall
index d63a451..ba2f5dd 100644
--- a/trac/trac/wiki/default-pages/TracInstall
+++ b/trac/trac/wiki/default-pages/TracInstall
@@ -1,24 +1,30 @@
-= Trac Installation Guide for 1.0 = 
+{{{#!div style="margin-top: .5em; padding: 0 1em; background-color: #ffd; border:1px outset #ddc; text-align: center"
+
+ '''NOTE: the information in this page applies to Trac 1.0, the current version of Trac. \\
+ For installing previous Trac versions, please refer to [[wiki:0.12/TracInstall]] (for Trac 0.12)''' 
+}}}
+
+= Trac Installation Guide for 1.0
 [[TracGuideToc]]
 
 Trac is written in the Python programming language and needs a database, [http://sqlite.org/ SQLite], [http://www.postgresql.org/ PostgreSQL], or [http://mysql.com/ MySQL]. For HTML rendering, Trac uses the [http://genshi.edgewall.org Genshi] templating system.
 
-Since version 0.12, Trac can also be localized, and there's probably a translation available for your language. If you want to be able to use the Trac interface in other languages, then make sure you have installed the optional package [#OtherPythonPackages Babel]. Pay attention to the extra steps for localization support in the [#InstallingTrac Installing Trac] section below. Lacking Babel, you will only get the default english version, as usual.
+Since version 0.12, Trac can also be localized, and there's probably a translation available for your language. If you want to be able to use the Trac interface in other languages, then make sure you have installed the optional package [#OtherPythonPackages Babel]. Pay attention to the extra steps for localization support in the [#InstallingTrac Installing Trac] section below. Lacking Babel, you will only get the default English version, as usual.
 
-If you're interested in contributing new translations for other languages or enhance the existing translations, then please have a look at [[trac:TracL10N]].
+If you're interested in contributing new translations for other languages or enhance the existing translations, then please have a look at [trac:wiki:TracL10N TracL10N].
 
 What follows are generic instructions for installing and setting up Trac and its requirements. While you may find instructions for installing Trac on specific systems at [trac:TracInstallPlatforms TracInstallPlatforms] on the main Trac site, please be sure to '''first read through these general instructions''' to get a good understanding of the tasks involved.
 
 [[PageOutline(2-3,Installation Steps,inline)]]
 
-== Dependencies ==
+== Dependencies
 === Mandatory Dependencies
 To install Trac, the following software packages must be installed:
 
  * [http://www.python.org/ Python], version >= 2.5 and < 3.0
    (note that we dropped the support for Python 2.4 in this release)
- * [http://peak.telecommunity.com/DevCenter/setuptools setuptools], version >= 0.6, or better yet, [http://pypi.python.org/pypi/distribute distribute]
- * [http://genshi.edgewall.org/wiki/Download Genshi], version >= 0.6 (unreleased version 0.7dev should work as well)
+ * [http://peak.telecommunity.com/DevCenter/setuptools setuptools], version >= 0.6
+ * [http://genshi.edgewall.org/wiki/Download Genshi], version >= 0.6
 
 You also need a database system and the corresponding python bindings.
 The database can be either SQLite, PostgreSQL or MySQL.
@@ -27,10 +33,10 @@
 
 As you must be using Python 2.5, 2.6 or 2.7, you already have the SQLite database bindings bundled with the standard distribution of Python (the `sqlite3` module).
 
-However, if you'd like, you can download the latest and greatest version of [[trac:Pysqlite]] from 
+However, if you'd like, you can download the latest and greatest version of [[trac:PySqlite]] from 
 [http://code.google.com/p/pysqlite/downloads/list google code], where you'll find the Windows
 installers or the `tar.gz` archive for building from source: 
-{{{
+{{{#!sh
 $ tar xvfz <version>.tar.gz 
 $ cd <version> 
 $ python setup.py build_static install 
@@ -40,7 +46,7 @@
 
 SQLite 2.x is no longer supported.
 
-A known bug PySqlite versions 2.5.2-4 prohibits upgrade of trac databases
+A known bug in PySqlite versions 2.5.2-4 prohibits upgrades of Trac databases
 from 0.11.x to 0.12. Please use versions 2.5.5 and newer or 2.5.1 and
 older. See #9434 for more detail.
 
@@ -50,7 +56,7 @@
 
 You need to install the database and its Python bindings:
  * [http://www.postgresql.org/ PostgreSQL], version 8.0 or later
- * [http://pypi.python.org/pypi/psycopg2 psycopg2]
+ * [http://pypi.python.org/pypi/psycopg2 psycopg2], version 2.0 or later
 
 See [trac:DatabaseBackend#Postgresql DatabaseBackend] for details.
 
@@ -66,9 +72,9 @@
 
 === Optional Dependencies
 
-==== Version Control System ====
+==== Version Control System
 
-===== Subversion =====
+===== Subversion
  * [http://subversion.apache.org/ Subversion], 1.5.x or 1.6.x and the '''''corresponding''''' Python bindings. Older versions starting from 1.0, like 1.2.4, 1.3.2 or 1.4.2, etc. should still work. For troubleshooting information, check the [trac:TracSubversion#Troubleshooting TracSubversion] page.
 
 There are [http://subversion.apache.org/packages.html pre-compiled SWIG bindings] available for various platforms. (Good luck finding precompiled SWIG bindings for any Windows package at that listing. TracSubversion points you to [http://alagazam.net Algazam], which works for me under Python 2.6.)
@@ -79,18 +85,18 @@
 '''Please note:''' if using Subversion, Trac must be installed on the '''same machine'''. Remote repositories are currently [trac:ticket:493 not supported].
 
 
-===== Others =====
+===== Others 
 
 Support for other version control systems is provided via third-parties. See [trac:PluginList] and [trac:VersionControlSystem].
 
-==== Web Server ====
-A web server is optional because Trac is shipped with a server included, see the [#RunningtheStandaloneServer Running the Standalone Server ] section below.
+==== Web Server
+A web server is optional because Trac is shipped with a server included, see the [#RunningtheStandaloneServer Running the Standalone Server] section below.
 
-Alternatively you configure Trac to run in any of the following environments.
+Alternatively you can configure Trac to run in any of the following environments.
  * [http://httpd.apache.org/ Apache] with 
    - [http://code.google.com/p/modwsgi/ mod_wsgi], see [wiki:TracModWSGI] and 
      http://code.google.com/p/modwsgi/wiki/IntegrationWithTrac
-   - [http://modpython.org/ mod_python 3.3.1], deprecated: see TracModPython)
+   - [http://modpython.org/ mod_python 3.3.1], (deprecated: see TracModPython)
  * a [http://www.fastcgi.com/ FastCGI]-capable web server (see TracFastCgi)
  * an [http://tomcat.apache.org/connectors-doc/ajp/ajpv13a.html AJP]-capable web
    server (see [trac:TracOnWindowsIisAjp TracOnWindowsIisAjp])
@@ -98,13 +104,13 @@
    is highly discouraged''', better use one of the previous options. 
    
 
-==== Other Python Packages ====
+==== Other Python Packages
 
  * [http://babel.edgewall.org Babel], version >= 0.9.5, 
    needed for localization support (unreleased version 1.0dev should work as well)
  * [http://docutils.sourceforge.net/ docutils], version >= 0.3.9 
    for WikiRestructuredText.
- * [http://pygments.pocoo.org Pygments] for 
+ * [http://pygments.org Pygments] for 
    [wiki:TracSyntaxColoring syntax highlighting].
    [http://silvercity.sourceforge.net/ SilverCity] and/or 
    [http://gnu.org/software/enscript/enscript.html Enscript] may still be used
@@ -113,106 +119,109 @@
    otherwise Trac will fall back on a shorter list from 
    an internal time zone implementation.
 
-'''Attention''': The various available versions of these dependencies are not necessarily interchangable, so please pay attention to the version numbers above. If you are having trouble getting Trac to work please double-check all the dependencies before asking for help on the [trac:MailingList] or [trac:IrcChannel].
+'''Attention''': The various available versions of these dependencies are not necessarily interchangeable, so please pay attention to the version numbers above. If you are having trouble getting Trac to work please double-check all the dependencies before asking for help on the [trac:MailingList] or [trac:IrcChannel].
 
 Please refer to the documentation of these packages to find out how they are best installed. In addition, most of the [trac:TracInstallPlatforms platform-specific instructions] also describe the installation of the dependencies. Keep in mind however that the information there ''probably concern older versions of Trac than the one you're installing'' (there are even some pages that are still talking about Trac 0.8!).
 
 
-== Installing Trac ==
+== Installing Trac
 === Using `easy_install`
 One way to install Trac is using [http://pypi.python.org/pypi/setuptools setuptools].
-With setuptools you can install Trac from the subversion repository; 
+With setuptools you can install Trac from the Subversion repository; 
 
 A few examples:
 
- - install Trac 1.0:
-   {{{
+ - Install Trac 1.0:
+   {{{#!sh
    easy_install Trac==1.0
    }}}
-   (NOT YET ENABLED)
- - install latest development version 1.0dev:
-   {{{
+ - Install latest development version:
+   {{{#!sh
    easy_install Trac==dev
    }}}
    Note that in this case you won't have the possibility to run a localized version of Trac;
    either use a released version or install from source 
 
+{{{#!div style="border: 1pt dotted; margin: 1em"
+**Setuptools Warning:** If the version of your setuptools is in the range 5.4 through 5.6, the environment variable `PKG_RESOURCES_CACHE_ZIP_MANIFESTS` must be set in order to avoid significant performance degradation. More information may be found in the sections on [#RunningtheStandaloneServer Running The Standalone Server] and [#RunningTraconaWebServer Running Trac on a Web Server]. 
+}}}
+
 === Using `pip`
 'pip' is an easy_install replacement that is very useful to quickly install python packages.
-To get a trac installation up and running in less than 5 minutes:
+To get a Trac installation up and running in less than 5 minutes:
 
 Assuming you want to have your entire pip installation in `/opt/user/trac`
 
  - 
-{{{
-pip -E /opt/user/trac install trac psycopg2 
+ {{{#!sh
+pip install trac psycopg2 
 }}}
 or
  - 
-{{{
-pip -E /opt/user/trac install trac mysql-python 
+ {{{#!sh
+pip install trac mysql-python 
 }}}
 
-Make sure your OS specific headers are available for pip to automatically build PostgreSQL (libpq-dev) or MySQL (libmysqlclient-dev) bindings.
+Make sure your OS specific headers are available for pip to automatically build PostgreSQL (`libpq-dev`) or MySQL (`libmysqlclient-dev`) bindings.
 
 pip will automatically resolve all dependencies (like Genshi, pygments, etc.) and download the latest packages on pypi.python.org and create a self contained installation in `/opt/user/trac`.
 
 All commands (`tracd`, `trac-admin`) are available in `/opt/user/trac/bin`. This can also be leveraged for `mod_python` (using `PythonHandler` directive) and `mod_wsgi` (using `WSGIDaemonProcess` directive)
 
-Additionally, you can install several trac plugins (listed [http://pypi.python.org/pypi?:action=search&term=trac&submit=search here]) through pip.
+Additionally, you can install several Trac plugins (listed [http://pypi.python.org/pypi?:action=search&term=trac&submit=search here]) through pip.
 
 
 
 === From source
 Of course, using the python-typical setup at the top of the source directory also works.
 
-You can obtain the source for a .tar.gz or .zip file corresponding to a release (e.g. Trac-1.0.tar.gz), or you can get the source directly from the repository (see Trac:SubversionRepository for details).
+You can obtain the source for a .tar.gz or .zip file corresponding to a release (e.g. `Trac-1.0.tar.gz`), or you can get the source directly from the repository (see [trac:SubversionRepository] for details).
 
-{{{
+{{{#!sh
 $ python ./setup.py install
 }}}
 
 ''You'll need root permissions or equivalent for this step.''
 
-This will byte-compile the python source code and install it as an .egg file or folder in the `site-packages` directory
-of your Python installation. The .egg will also contain all other resources needed by standard Trac, such as htdocs and templates.
+This will byte-compile the Python source code and install it as an .egg file or folder in the `site-packages` directory
+of your Python installation. The .egg will also contain all other resources needed by standard Trac, such as `htdocs` and `templates`.
 
 The script will also install the [wiki:TracAdmin trac-admin] command-line tool, used to create and maintain [wiki:TracEnvironment project environments], as well as the [wiki:TracStandalone tracd] standalone server.
 
-If you install from source and want to make Trac available in other languages, make sure  Babel is installed. Only then, perform the `install` (or simply redo the `install` once again afterwards if you realize Babel was not yet installed):
-{{{
+If you install from source and want to make Trac available in other languages, make sure Babel is installed. Only then, perform the `install` (or simply redo the `install` once again afterwards if you realize Babel was not yet installed):
+{{{#!sh
 $ python ./setup.py install
 }}}
-Alternatively, you can do a `bdist_egg` and copy the .egg from dist/ to the place of your choice, or you can create a Windows installer (`bdist_wininst`).
+Alternatively, you can run `bdist_egg` and copy the .egg from `dist/` to the place of your choice, or you can create a Windows installer (`bdist_wininst`).
 
-=== Advanced Options ===
+=== Advanced Options
 
 To install Trac to a custom location, or find out about other advanced installation options, run:
-{{{
+{{{#!sh
 easy_install --help
 }}}
 
-Also see [http://docs.python.org/inst/inst.html Installing Python Modules] for detailed information.
+Also see [http://docs.python.org/2/install/index.html Installing Python Modules] for detailed information.
 
 Specifically, you might be interested in:
-{{{
+{{{#!sh
 easy_install --prefix=/path/to/installdir
 }}}
-or, if installing Trac to a Mac OS X system:
-{{{
+or, if installing Trac on a Mac OS X system:
+{{{#!sh
 easy_install --prefix=/usr/local --install-dir=/Library/Python/2.5/site-packages
 }}}
-Note: If installing on Mac OS X 10.6 running {{{ easy_install http://svn.edgewall.org/repos/trac/trunk }}} will install into {{{ /usr/local }}} and {{{ /Library/Python/2.6/site-packages }}} by default
+Note: If installing on Mac OS X 10.6 running {{{ easy_install http://svn.edgewall.org/repos/trac/trunk }}} will install into {{{ /usr/local }}} and {{{ /Library/Python/2.6/site-packages }}} by default.
 
 The above will place your `tracd` and `trac-admin` commands into `/usr/local/bin` and will install the Trac libraries and dependencies into `/Library/Python/2.5/site-packages`, which is Apple's preferred location for third-party Python application installations.
 
 
-== Creating a Project Environment ==
+== Creating a Project Environment
 
 A [TracEnvironment Trac environment] is the backend storage where Trac stores information like wiki pages, tickets, reports, settings, etc. An environment is basically a directory that contains a human-readable [TracIni configuration file], and various other files and directories.
 
 A new environment is created using [wiki:TracAdmin trac-admin]:
-{{{
+{{{#!sh
 $ trac-admin /path/to/myproject initenv
 }}}
 
@@ -221,15 +230,19 @@
 Using the default database connection string in particular will always work as long as you have SQLite installed.
 For the other [DatabaseBackend database backends] you should plan ahead and already have a database ready to use at this point.
 
-Since 0.12, Trac doesn't ask for a [TracEnvironment#SourceCodeRepository source code repository] anymore when creating an environment. Repositories can be [TracRepositoryAdmin added] afterward, or the version control support can be disabled completely if you don't need it.
+Since 0.12, Trac doesn't ask for a [TracEnvironment#SourceCodeRepository source code repository] anymore when creating an environment. Repositories can be [TracRepositoryAdmin added] afterwards, and support for specific version control systems is disabled by default.
 
 Also note that the values you specify here can be changed later by directly editing the [TracIni conf/trac.ini] configuration file.
 
+When selecting the location of your environment, make sure that the filesystem on which the environment directory resides supports sub-second timestamps (i.e. **not** `ext2` or `ext3` on Linux), as the modification time of the `conf/trac.ini` file will be monitored to decide whether an environment restart is needed or not. A too coarse-grained timestamp resolution may result in inconsistencies in Trac < 1.0.2 (though the best advice is to opt for a platform with sub-second timestamp resolution when possible regardless of the version of Trac you are running).
+
 Finally, make sure the user account under which the web front-end runs will have '''write permissions''' to the environment directory and all the files inside. This will be the case if you run `trac-admin ... initenv` as this user. If not, you should set the correct user afterwards. For example on Linux, with the web server running as user `apache` and group `apache`, enter:
-{{{
-# chown -R apache.apache /path/to/myproject
+{{{#!sh
+$ chown -R apache.apache /path/to/myproject
 }}}
 
+The actual username and groupname of the apache server may not be exactly `apache`, and are specified in the Apache configuration file by the directives `User` and `Group` (if Apache `httpd` is what you use).
+
 {{{#!div class=important
 '''Warning:''' Please only use ASCII-characters for account name and project path, unicode characters are not supported there.
 }}}
@@ -237,19 +250,33 @@
 
 == Deploying Trac
 
-=== Running the Standalone Server ===
+=== Running the Standalone Server
 
 After having created a Trac environment, you can easily try the web interface by running the standalone server [wiki:TracStandalone tracd]:
-{{{
+{{{#!sh
 $ tracd --port 8000 /path/to/myproject
 }}}
 
 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. If you only plan on managing a single project with Trac you can have the standalone server skip the environment list by starting it like this:
-{{{
+{{{#!sh
 $ tracd -s --port 8000 /path/to/myproject
 }}}
 
-=== Running Trac on a Web Server ===
+{{{#!div style="border: 1pt dotted; margin: 1em"
+**Setuptools Warning:** If the version of your setuptools is in the range 5.4 through 5.6, the environment variable `PKG_RESOURCES_CACHE_ZIP_MANIFESTS` must be set in order to avoid significant performance degradation. The environment variable can be set system-wide, or for just the user that runs the `tracd` process. There are several ways to accomplish this in addition to what is discussed here, and depending on the distribution of your OS.
+
+To be effective system-wide a shell script with the `export` statement may be added to `/etc/profile.d`. To be effective for a user session the `export` statement may be added to `~/.profile`.
+{{{#!sh
+export PKG_RESOURCES_CACHE_ZIP_MANIFESTS=1
+}}}
+
+Alternatively, the variable can be set in the shell before executing `tracd`:
+{{{#!sh
+$ PKG_RESOURCES_CACHE_ZIP_MANIFESTS=1 tracd --port 8000 /path/to/myproject
+}}}
+}}}
+
+=== Running Trac on a Web Server
 
 Trac provides various options for connecting to a "real" web server: 
  - [wiki:TracFastCgi FastCGI]
@@ -259,20 +286,21 @@
 
 Trac also supports [trac:TracOnWindowsIisAjp AJP] which may be your choice if you want to connect to IIS. Other deployment scenarios are possible: [trac:TracNginxRecipe nginx], [http://projects.unbit.it/uwsgi/wiki/Example#Traconapacheinasub-uri uwsgi], [trac:TracOnWindowsIisIsapi Isapi-wsgi] etc.
 
-==== Generating the Trac cgi-bin directory ==== #cgi-bin
+==== Generating the Trac cgi-bin directory #cgi-bin
 
 In order for Trac to function properly with FastCGI you need to have a `trac.fcgi` file and for mod_wsgi a `trac.wsgi` file. These are Python scripts which load the appropriate Python code. They can be generated using the `deploy` option of [wiki:TracAdmin trac-admin].
 
 There is, however, a bit of a chicken-and-egg problem. The [wiki:TracAdmin trac-admin] command requires an existing environment to function, but complains if the deploy directory already exists. This is a problem, because environments are often stored in a subdirectory of the deploy. The solution is to do something like this:
-{{{
+{{{#!sh
 mkdir -p /usr/share/trac/projects/my-project
 trac-admin /usr/share/trac/projects/my-project initenv
 trac-admin /usr/share/trac/projects/my-project deploy /tmp/deploy
 mv /tmp/deploy/* /usr/share/trac
 }}}
+Don't forget to check that the web server has the execution right on scripts in the `/usr/share/trac/cgi-bin` directory.
 
 
-==== Mapping Static Resources ====
+==== Mapping Static Resources
 
 Out of the box, Trac will pass static resources such as style sheets or images through itself. For anything but a tracd only based deployment, this is far from optimal as the web server could be set up to directly serve those static resources (for CGI setup, this is '''highly undesirable''' and will cause abysmal performance).
 
@@ -288,15 +316,15 @@
  - `common/` - the static resources of Trac itself
  - `<plugins>/` - one directory for each resource directory managed by the plugins enabled for this environment
 
-===== Example: Apache and `ScriptAlias` ===== #ScriptAlias-example
+===== Example: Apache and `ScriptAlias` #ScriptAlias-example
 
 Assuming the deployment has been done this way:
-{{{
+{{{#!sh
 $ trac-admin /var/trac/env deploy /path/to/trac/htdocs/common
 }}}
 
 Add the following snippet to Apache configuration ''before'' the `ScriptAlias` or `WSGIScriptAlias` (which map all the other requests to the Trac application), changing paths to match your deployment:
-{{{
+{{{#!apache
 Alias /trac/chrome/common /path/to/trac/htdocs/common
 Alias /trac/chrome/site /path/to/trac/htdocs/site
 
@@ -307,7 +335,7 @@
 }}}
 
 If using mod_python, you might want to add this too (otherwise, the alias will be ignored):
-{{{
+{{{#!apache
 <Location "/trac/chrome/common/">
   SetHandler None
 </Location>
@@ -316,7 +344,7 @@
 Note that we mapped `/trac` part of the URL to the `trac.*cgi` script, and the path `/trac/chrome/common` is the path you have to append to that location to intercept requests to the static resources. 
 
 Similarly, if you have static resources in a project's `htdocs` directory (which is referenced by `/trac/chrome/site` URL in themes), you can configure Apache to serve those resources (again, put this ''before'' the `ScriptAlias` or `WSGIScriptAlias` for the .*cgi scripts, and adjust names and locations to match your installation):
-{{{
+{{{#!apache
 Alias /trac/chrome/site /path/to/projectenv/htdocs
 
 <Directory "/path/to/projectenv/htdocs">
@@ -326,25 +354,25 @@
 }}}
 
 Alternatively to aliasing `/trac/chrome/common`, you can tell Trac to generate direct links for those static resources (and only those), using the [[wiki:TracIni#trac-section| [trac] htdocs_location]] configuration setting:
-{{{
+{{{#!ini
 [trac]
 htdocs_location = http://static.example.org/trac-common/
 }}}
 Note that this makes it easy to have a dedicated domain serve those static resources (preferentially [http://code.google.com/speed/page-speed/docs/request.html#ServeFromCookielessDomain cookie-less]).
 
 Of course, you still need to make the Trac `htdocs/common` directory available through the web server at the specified URL, for example by copying (or linking) the directory into the document root of the web server:
-{{{
+{{{#!sh
 $ ln -s /path/to/trac/htdocs/common /var/www/static.example.org/trac-common
 }}}
 
 
-==== Setting up the Plugin Cache ====
+==== Setting up the Plugin Cache
 
-Some Python plugins need to be extracted to a cache directory. By default the cache resides in the home directory of the current user. When running Trac on a Web Server as a dedicated user (which is highly recommended) who has no home directory, this might prevent the plugins from starting. To override the cache location you can set the PYTHON_EGG_CACHE environment variable. Refer to your server documentation for detailed instructions on how to set environment variables.
+Some Python plugins need to be extracted to a cache directory. By default the cache resides in the home directory of the current user. When running Trac on a Web Server as a dedicated user (which is highly recommended) who has no home directory, this might prevent the plugins from starting. To override the cache location you can set the `PYTHON_EGG_CACHE` environment variable. Refer to your server documentation for detailed instructions on how to set environment variables.
 
-== Configuring Authentication ==
+== Configuring Authentication
 
-Trac uses HTTP authentication. You'll need to configure your webserver to request authentication when the `.../login` URL is hit (the virtual path of the "login" button). Trac will automatically pick the REMOTE_USER variable up after you provide your credentials. Therefore, all user management goes through your web server configuration. Please consult the documentation of your web server for more info.
+Trac uses HTTP authentication. You'll need to configure your webserver to request authentication when the `.../login` URL is hit (the virtual path of the "login" button). Trac will automatically pick the `REMOTE_USER` variable up after you provide your credentials. Therefore, all user management goes through your web server configuration. Please consult the documentation of your web server for more info.
 
 The process of adding, removing, and configuring user accounts for authentication depends on the specific way you run Trac. 
 
@@ -353,28 +381,46 @@
  * [wiki:TracModWSGI#ConfiguringAuthentication TracModWSGI#ConfiguringAuthentication] if you use the Apache web server, with any of its front end: `mod_wsgi` of course, but the same instructions applies also for `mod_python`, `mod_fcgi` or `mod_fastcgi`.
  * TracFastCgi if you're using another web server with FCGI support (Cherokee, Lighttpd, !LiteSpeed, nginx)
 
+The following document also constains some useful information for beginners: [trac:TracAuthenticationIntroduction].
+
 == Granting admin rights to the admin user
 Grant admin rights to user admin:
-{{{
+{{{#!sh
 $ trac-admin /path/to/myproject permission add admin TRAC_ADMIN
 }}}
-This user will have an "Admin" entry menu that will allow you to admin your trac project.
+This user will have an "Admin" entry menu that will allow you to administrate your Trac project.
 
 == Finishing the install
 
-=== Automatic reference to the SVN changesets in Trac tickets ===
+=== Enable version control components
+
+Support for version control systems is provided by optional components in Trac and the components are disabled by default //(since 1.0)//. Subversion and Git must be explicitly enabled if you wish to use them. See TracRepositoryAdmin for more details.
+
+The components can be enabled by adding the following to the `[components]` section of your [TracIni#components-section trac.ini], or enabling the components in the "Plugins" admin panel.
+
+{{{#!ini
+tracopt.versioncontrol.svn.* = enabled
+}}}
+
+{{{#!ini
+tracopt.versioncontrol.git.* = enabled
+}}}
+
+After enabling the components, repositories can be configured through the "Repositories" admin panel or by editing [TracIni#repositories-section trac.ini].
+
+=== Automatic reference to the SVN changesets in Trac tickets
 
 You can configure SVN to automatically add a reference to the changeset into the ticket comments, whenever changes are committed to the repository. The description of the commit needs to contain one of the following formulas:
  * '''`Refs #123`''' - to reference this changeset in `#123` ticket
  * '''`Fixes #123`''' - to reference this changeset and close `#123` ticket with the default status ''fixed''
 
 This functionality requires a post-commit hook to be installed as described in [wiki:TracRepositoryAdmin#ExplicitSync TracRepositoryAdmin], and enabling the optional commit updater components by adding the following line to the `[components]` section of your [wiki:TracIni#components-section trac.ini], or enabling the components in the "Plugins" admin panel.
-{{{
+{{{#!ini
 tracopt.ticket.commit_updater.* = enabled
 }}}
 For more information, see the documentation of the `CommitTicketUpdater` component in the "Plugins" admin panel.
 
-=== Using Trac ===
+=== Using Trac
 
 Once you have your Trac site up and running, you should be able to create tickets, view the timeline, browse your version control repository if configured, etc.
 
diff --git a/trac/trac/wiki/default-pages/TracInterfaceCustomization b/trac/trac/wiki/default-pages/TracInterfaceCustomization
index 90a9643..753a81b 100644
--- a/trac/trac/wiki/default-pages/TracInterfaceCustomization
+++ b/trac/trac/wiki/default-pages/TracInterfaceCustomization
@@ -33,7 +33,7 @@
 icon = site/my_icon.ico
 }}}
 
-Note though that this icon is ignored by Internet Explorer, which only accepts a file named ``favicon.ico`` at the root of the host. To make the project icon work in both IE and other browsers, you can store the icon in the document root of the host, and reference it from ``trac.ini`` as follows:
+Note though that this icon is ignored by Internet Explorer, which only accepts a file named `favicon.ico` at the root of the host. To make the project icon work in both IE and other browsers, you can store the icon in the document root of the host, and reference it from `trac.ini` as follows:
 
 {{{
 [project]
@@ -98,7 +98,7 @@
 </html>
 }}}
 
-Those who are familiar with XSLT may notice that Genshi templates bear some similarities. However, there are some Trac specific features - for example `${href.chrome('site/style.css')}` attribute references a CSS file placed into environment's `htdocs/` directory. In a similar fashion `${chrome.htdocs_location}` is used to specify the common `htdocs/` directory belonging to a Trac installation. That latter location can however be overriden using the [[TracIni#trac-config|[trac] htdocs_location]] configuration setting.
+Those who are familiar with XSLT may notice that Genshi templates bear some similarities. However, there are some Trac specific features - for example `${href.chrome('site/style.css')}` attribute references a CSS file placed into environment's `htdocs/` directory. In a similar fashion `${chrome.htdocs_location}` is used to specify the common `htdocs/` directory belonging to a Trac installation. That latter location can however be overriden using the [[TracIni#trac-section|[trac] htdocs_location]] configuration setting.
 
 `site.html` is one file to contain all your modifications. It usually works using the `py:match` directive (element or attribute), and it allows you to modify the page as it renders - the matches hook onto specific sections depending on what it tries to find
 and modify them.
@@ -135,7 +135,7 @@
 </form>
 }}}
 
-Also note that the `site.html` (despite its name) can be put in a common templates directory - see the [[TracIni#inherit-section|[inherit] templates_dir]] option. This could provide easier maintainence (and a migration path from 0.10 for larger installations) as one new global `site.html` file can be made to include any existing header, footer and newticket snippets.
+Also note that the `site.html` (despite its name) can be put in a shared templates directory - see the [[TracIni#inherit-section|[inherit] templates_dir]] option. This could provide easier maintainence (and a migration path from 0.10 for larger installations) as one new global `site.html` file can be made to include any existing header, footer and newticket snippets.
 
 == Project List == #ProjectList
 
@@ -220,4 +220,4 @@
 Trac caches templates in memory by default to improve performance. To apply a template you need to restart the server.
 
 ----
-See also TracGuide, TracIni
+See also TracGuide, TracIni
\ No newline at end of file
diff --git a/trac/trac/wiki/default-pages/TracLinks b/trac/trac/wiki/default-pages/TracLinks
index bac0c08..fdce80e 100644
--- a/trac/trac/wiki/default-pages/TracLinks
+++ b/trac/trac/wiki/default-pages/TracLinks
@@ -27,7 +27,7 @@
  Reports :: `{1}` or `report:1`
  Milestones :: `milestone:1.0`
  Attachment :: `attachment:example.tgz` (for current page attachment), `attachment:attachment.1073.diff:ticket:944` (absolute path)
- Changesets :: `r1`, `[1]`, `changeset:1` or (restricted) `[1/trunk]`, `changeset:1/trunk`
+ Changesets :: `r1`, `[1]`, `changeset:1` or (restricted) `[1/trunk]`, `changeset:1/trunk`, `[1/repository]`
  Revision log :: `r1:3`, `[1:3]` or `log:@1:3`, `log:trunk@1:3`, `[2:5/trunk]`
  Diffs :: `diff:@1:3`, `diff:plugins/0.12/mercurial-plugin@9128:9953`,
           `diff:tags/trac-0.9.2/wiki-default//tags/trac-0.9.3/wiki-default` 
@@ -42,7 +42,7 @@
  Reports :: {1} or report:1
  Milestones :: milestone:1.0
  Attachment :: attachment:example.tgz (for current page attachment), attachment:attachment.1073.diff:ticket:944 (absolute path)
- Changesets :: r1, [1], changeset:1 or (restricted) [1/trunk], changeset:1/trunk
+ Changesets :: r1, [1], changeset:1 or (restricted) [1/trunk], changeset:1/trunk, [1/repository]
  Revision log :: r1:3, [1:3] or log:@1:3, log:trunk@1:3, [2:5/trunk]
  Diffs :: diff:@1:3, diff:plugins/0.12/mercurial-plugin@9128:9953,
           diff:tags/trac-0.9.2/wiki-default//tags/trac-0.9.3/wiki-default 
@@ -315,13 +315,15 @@
 
 === timeline: links ===
 
-Links to the timeline can be created by specifying a date in the ISO:8601 format. The date can be optionally followed by a time specification. The time is interpreted as being UTC time, but alternatively you can specify your local time, followed by your timezone if you don't want to compute the UTC time.
+Links to the timeline can be created by specifying a date in the ISO:8601 format. The date can be optionally followed by a time specification. The time is interpreted as being UTC time, but if you don't want to compute the UTC time, you can specify a local time followed by your timezone offset relative to UTC.
 
 Examples:
  - `timeline:2008-01-29`
  - `timeline:2008-01-29T15:48`
  - `timeline:2008-01-29T15:48Z`
  - `timeline:2008-01-29T16:48+01`
+ - `timeline:2008-01-29T16:48+0100`
+ - `timeline:2008-01-29T16:48+01:00`
 
 ''(since Trac 0.11)''
 
@@ -349,9 +351,9 @@
  - `source:/some/file@123#L10`
  - `source:/tag/0.10@head#L10`
 
-Finally, one can also highlight an arbitrary set of lines:
- - `source:/some/file@123:10-20,100,103#L99` - highlight lines 10 to 20, and lines 100 and 103.
-   ''(since 0.11)''
+Finally, one can also highlight an arbitrary set of lines ''(since 0.11)'':
+ - `source:/some/file@123:10-20,100,103#L99` - highlight lines 10 to 20, and lines 100 and 103, and target line 99
+ - or without version number (the `@` is still needed): `source:/some/file@:10-20,100,103#L99`. Version can be omitted when the path is pointing to a source file that will no longer change (like `source:/tags/...`), otherwise it's better to specify which lines of //which version// of the file you're talking about
 
 Note that in presence of multiple repositories, the name of the repository is simply integrated in the path you specify for `source:` (e.g. `source:reponame/trunk/README`). ''(since 0.12)''
 
@@ -386,4 +388,4 @@
 
 ----
 See also: WikiFormatting, TracWiki, WikiPageNames, InterTrac, InterWiki
- 
+ 
\ No newline at end of file
diff --git a/trac/trac/wiki/default-pages/TracModWSGI b/trac/trac/wiki/default-pages/TracModWSGI
index 4e4ce7d..266d044 100644
--- a/trac/trac/wiki/default-pages/TracModWSGI
+++ b/trac/trac/wiki/default-pages/TracModWSGI
@@ -1,7 +1,7 @@
 = Trac and mod_wsgi =
 
 
-[http://code.google.com/p/modwsgi/ mod_wsgi] is an Apache module for running WSGI-compatible Python applications directly on top of the Apache webserver. The mod_wsgi adapter is written completely in C and provides very good performances.
+[http://code.google.com/p/modwsgi/ mod_wsgi] is an Apache module for running WSGI-compatible Python applications directly on top of the Apache webserver. The mod_wsgi adapter is written completely in C and provides very good performance.
 
 [[PageOutline(2-3,Overview,inline)]]
 
@@ -24,6 +24,16 @@
 
 The `TRAC_ENV` variable should naturally be the directory for your Trac environment (if you have several Trac environments in a directory, you can also use `TRAC_ENV_PARENT_DIR` instead), while the `PYTHON_EGG_CACHE` should be a directory where Python can temporarily extract Python eggs.
 
+On Windows:
+ - If run under the user's session, the Python Egg cache can be found in `%AppData%\Roaming`, for example:
+{{{#!python
+os.environ['PYTHON_EGG_CACHE'] = r'C:\Users\Administrator\AppData\Roaming\Python-Eggs'
+}}}
+ - If run under a Window service, you should create a directory for Python Egg cache.
+{{{#!python
+os.environ['PYTHON_EGG_CACHE'] = r'C:\Trac-Python-Eggs'
+}}}
+
 === A more elaborate script
 
 If you're using multiple `.wsgi` files (for example one per Trac environment) you must ''not'' use `os.environ['TRAC_ENV']` to set the path to the Trac environment. Using this method may lead to Trac delivering the content of another Trac environment, as the variable may be filled with the path of a previously viewed Trac environment. 
@@ -72,7 +82,7 @@
 
 Here, the script is in a subdirectory of the Trac environment.
 
-If you followed the directions [http://trac.edgewall.org/wiki/TracInstall#cgi-bin Generating the Trac cgi-bin directory], your Apache configuration file should look like following:
+If you followed the directions [TracInstall#cgi-bin Generating the Trac cgi-bin directory], your Apache configuration file should look like following:
 
 {{{
 WSGIScriptAlias /trac /usr/share/trac/cgi-bin/trac.wsgi
@@ -171,6 +181,8 @@
 
 For multiple environments, you can use the same `LocationMatch` as described with the previous method.
 
+'''Note: `Location` cannot be used inside .htaccess files, but must instead live within the main httpd.conf file. If you are on a shared server, you therefore will not be able to provide this level of granularity. '''
+
 Don't forget to activate the mod_auth_digest. For example, on a Debian 4.0r1 (etch) system:
 {{{
     LoadModule auth_digest_module /usr/lib/apache2/modules/mod_auth_digest.so
@@ -307,7 +319,7 @@
    Require valid-user
 </Location>
 }}}
-Note that '''authFile''' need not exist. See the !HttpAuthStore link above for examples where multiple Trac projects are hosted on a server.
+Note that '''authFile''' need not exist (unless you are using Account Manager older than 0.4). See the !HttpAuthStore link above for examples where multiple Trac projects are hosted on a server.
 
 === Example: Apache/mod_wsgi with Basic Authentication, Trac being at the root of a virtual host
 
@@ -387,14 +399,14 @@
 
 But it's not necessary to edit the source of Trac, the following lines in `trac.wsgi` will also work:
 
-{{{
+{{{#!python
 import trac.db.postgres_backend
 trac.db.postgres_backend.PostgreSQLConnection.poolable = False
 }}}
 
 or
 
-{{{
+{{{#!python
 import trac.db.mysql_backend
 trac.db.mysql_backend.MySQLConnection.poolable = False
 }}}
diff --git a/trac/trac/wiki/default-pages/TracNotification b/trac/trac/wiki/default-pages/TracNotification
index 6e8e557..ded15b3 100644
--- a/trac/trac/wiki/default-pages/TracNotification
+++ b/trac/trac/wiki/default-pages/TracNotification
@@ -27,41 +27,7 @@
 === Configuration Options ===
 These are the available options for the `[notification]` section in trac.ini.
 
- * '''`smtp_enabled`''': Enable email notification.
- * '''`smtp_from`''': Email address to use for ''Sender''-headers in notification emails.
- * '''`smtp_from_name`''': Sender name to use for ''Sender''-headers in notification emails.
- * '''`smtp_from_author`''': (''since 1.0'') Use the author of a change (the reporter of a new ticket, or the author of a comment) as the `From:` header value in notification e-mails (default: false). If the author hasn't set an e-mail address, `smtp_from` and `smtp_from_name` are used instead.
- * '''`smtp_replyto`''': Email address to use for ''Reply-To''-headers in notification emails.
- * '''`smtp_default_domain`''': (''since 0.10'') Append the specified domain to addresses that do not contain one. Fully qualified addresses are not modified. The default domain is appended to all username/login for which an email address cannot be found from the user settings.
- * '''`smtp_always_cc`''': List of email addresses to always send notifications to. ''Typically used to post ticket changes to a dedicated mailing list.''
- * '''`smtp_always_bcc`''': (''since 0.10'') List of email addresses to always send notifications to, but keeps addresses not visible from other recipients of the notification email 
- * '''`smtp_subject_prefix`''': (''since 0.10.1'') Text that is inserted before the subject of the email. Set to "!__default!__" by default.
- * '''`always_notify_reporter`''':  Always send notifications to any address in the reporter field (default: false).
- * '''`always_notify_owner`''': (''since 0.9'') Always send notifications to the address in the owner field (default: false).
- * '''`always_notify_updater`''': (''since 0.10'') Always send a notification to the updater of a ticket (default: true).
- * '''`use_public_cc`''': (''since 0.10'') Addresses in To: (owner, reporter) and Cc: lists are visible by all recipients (default is ''Bcc:'' - hidden copy).
- * '''`use_short_addr`''': (''since 0.10'') Enable delivery of notifications to addresses that do not contain a domain (i.e. do not end with ''@<domain.com>'').This option is useful for intranets, where the SMTP server can handle local addresses and map the username/login to a local mailbox. See also `smtp_default_domain`. Do not use this option with a public SMTP server. 
- * '''`ignore_domains`''': Comma-separated list of domains that should not be considered part of email addresses (for usernames with Kerberos domains).
- * '''`mime_encoding`''': (''since 0.10'') This option allows selecting the MIME encoding scheme. Supported values:
-   * `none`: default value, uses 7bit encoding if the text is plain ASCII, or 8bit otherwise. 
-   * `base64`: works with any kind of content. May cause some issues with touchy anti-spam/anti-virus engines.
-   * `qp` or `quoted-printable`: best for european languages (more compact than base64) if 8bit encoding cannot be used.
- * '''`ticket_subject_template`''': (''since 0.11'') A [http://genshi.edgewall.org/wiki/Documentation/text-templates.html Genshi text template] snippet used to get the notification subject.
- * '''`email_sender`''': (''since 0.12'') Name of the component implementing `IEmailSender`. This component is used by the notification system to send emails. Trac currently provides the following components:
-   * `SmtpEmailSender`: connects to an SMTP server (default).
-   * `SendmailEmailSender`: runs a `sendmail`-compatible executable.
-
-Either '''`smtp_from`''' or '''`smtp_replyto`''' (or both) ''must'' be set, otherwise Trac refuses to send notification mails.
-
-The following options are specific to email delivery through SMTP.
- * '''`smtp_server`''': SMTP server used for notification messages.
- * '''`smtp_port`''': (''since 0.9'') Port used to contact the SMTP server.
- * '''`smtp_user`''': (''since 0.9'') User name for authentication SMTP account.
- * '''`smtp_password`''': (''since 0.9'') Password for authentication SMTP account.
- * '''`use_tls`''': (''since 0.10'') Toggle to send notifications via a SMTP server using [http://en.wikipedia.org/wiki/Transport_Layer_Security TLS], such as GMail.
-
-The following option is specific to email delivery through a `sendmail`-compatible executable.
- * '''`sendmail_path`''': (''since 0.12'') Path to the sendmail executable. The sendmail program must accept the `-i` and `-f` options.
+[[TracIni(notification)]]
 
 === Example Configuration (SMTP) ===
 {{{
@@ -98,7 +64,7 @@
 
 === Customizing the e-mail content ===
 
-The notification e-mail content is generated based on `ticket_notify_email.txt` in `trac/templates`.  You can add your own version of this template by adding a `ticket_notify_email.txt` to the templates directory of your environment. The default looks like this:
+The notification e-mail content is generated based on `ticket_notify_email.txt` in `trac/ticket/templates`.  You can add your own version of this template by adding a `ticket_notify_email.txt` to the templates directory of your environment. The default looks like this:
 
 {{{
 $ticket_body_hdr
@@ -245,17 +211,12 @@
 where ''user'' and ''password'' match an existing GMail account, ''i.e.'' the ones you use to log in on [http://gmail.com]
 
 Alternatively, you can use `smtp_port = 25`.[[br]]
-You should not use `smtp_port = 465`. It will not work and your ticket submission may deadlock. Port 465 is reserved for the SMTPS protocol, which is not supported by Trac. See [comment:ticket:7107:2 #7107] for details.
+You should not use `smtp_port = 465`. It will not work and your ticket submission may deadlock. Port 465 is reserved for the SMTPS protocol, which is not supported by Trac. See [trac:comment:2:ticket:7107 #7107] for details.
  
-== Filtering notifications for one's own changes ==
+== Filtering notifications for one's own changes and comments ==
 In Gmail, use the filter:
 
 {{{
-from:(<smtp_from>) (("Reporter: <username>" -Changes) OR "Changes (by <username>)")
-}}}
-
-For Trac .10, use the filter:
-{{{
 from:(<smtp_from>) (("Reporter: <username>" -Changes -Comment) OR "Changes (by <username>)" OR "Comment (by <username>)")
 }}}
 
@@ -264,11 +225,8 @@
 In Thunderbird, there is no such solution if you use IMAP
 (see http://kb.mozillazine.org/Filters_(Thunderbird)#Filtering_the_message_body).
 
-The best you can do is to set "always_notify_updater" in conf/trac.ini to false.
-You will however still get an email if you comment a ticket that you own or have reported.
-
 You can also add this plugin:
-http://trac-hacks.org/wiki/NeverNotifyUpdaterPlugin
+http://trac-hacks.org/wiki/NeverNotifyUpdaterPlugin, or vote for [trac:#2247] to be fixed.
 
 == Troubleshooting ==
 
diff --git a/trac/trac/wiki/default-pages/TracPermissions b/trac/trac/wiki/default-pages/TracPermissions
index cdf1ae5..a8d18d1 100644
--- a/trac/trac/wiki/default-pages/TracPermissions
+++ b/trac/trac/wiki/default-pages/TracPermissions
@@ -13,7 +13,7 @@
 == Graphical Admin Tab ==
 ''This feature is new in version 0.11.''
 
-To access this tab, a user must have one of the following permissions: `TRAC_ADMIN`, `PERMISSION_ADMIN`, `PERMISSION_ADD`, `PERMISSION_REMOVE`. The permissions can granted using the `trac-admin` command (more on `trac-admin` below):
+To access this tab, a user must have one of the following permissions: `TRAC_ADMIN`, `PERMISSION_ADMIN`, `PERMISSION_GRANT`, `PERMISSION_REVOKE`. The permissions can be granted using the `trac-admin` command (more on `trac-admin` below):
 {{{
   $ trac-admin /path/to/projenv permission add bob TRAC_ADMIN
 }}}
@@ -26,6 +26,8 @@
 
 An easy way to quickly secure a new Trac install is to run the above command on the anonymous user, install the [http://trac-hacks.org/wiki/AccountManagerPlugin AccountManagerPlugin], create a new admin account graphically and then remove the TRAC_ADMIN permission from the anonymous user.
 
+From the graphical admin tab, users with `PERMISSION_GRANT` will only be allowed to grant permissions that they possess, and users with `PERMISSION_REVOKE` will only be allowed to revoke permissions that they possess. For example, a user cannot grant `MILESTONE_ADMIN` unless they have `PERMISSION_GRANT` and `MILESTONE_ADMIN`, and they cannot revoke `MILESTONE_ADMIN` unless they have `PERMISSION_REVOKE` and `MILESTONE_ADMIN`. `PERMISSION_ADMIN` just grants the user both `PERMISSION_GRANT` and `PERMISSION_REVOKE`, and users with `TRAC_ADMIN` can grant or revoke any permission.
+
 == Available Privileges ==
 
 To enable all privileges for a user, use the `TRAC_ADMIN` permission. Having `TRAC_ADMIN` is like being `root` on a *NIX system: it will allow you to perform any operation.
@@ -97,7 +99,7 @@
 
 == Creating New Privileges ==
 
-To create custom permissions, for example to be used in a custom workflow, enable the optional [trac:ExtraPermissionsProvider tracopt.perm.config_perm_provider.ExtraPermissionsProvider] component in the "Plugins" admin panel, and add the desired permissions to the `[extra-permissions]` section in your [wiki:TracIni#extra-permissions-section trac.ini]. For more information, please refer to the documentation of the component in the admin panel.
+To create custom permissions, for example to be used in a custom workflow, enable the optional [trac:ExtraPermissionsProvider tracopt.perm.config_perm_provider.ExtraPermissionsProvider] component in the "Plugins" admin panel, and add the desired permissions to the `[extra-permissions]` section in your [wiki:TracIni#extra-permissions-section trac.ini]. For more information, please refer to the documentation  on the [wiki:TracIni#extra-permissions-section TracIni] page after enabling the component.
 
 == Granting Privileges ==
 
diff --git a/trac/trac/wiki/default-pages/TracPlugins b/trac/trac/wiki/default-pages/TracPlugins
index 07f2fbb..75ab393 100644
--- a/trac/trac/wiki/default-pages/TracPlugins
+++ b/trac/trac/wiki/default-pages/TracPlugins
@@ -168,6 +168,7 @@
  * ...you actually added the necessary line(s) to the `[components]` section.
  * ...the package/module names are correct.
  * ...the value is "enabled", not "enable" or "Enable".
+ * ...the section name is "components", not "component".
 
 === Check the permissions on the .egg file ===
 
diff --git a/trac/trac/wiki/default-pages/TracQuery b/trac/trac/wiki/default-pages/TracQuery
index 5899c26..05cf048 100644
--- a/trac/trac/wiki/default-pages/TracQuery
+++ b/trac/trac/wiki/default-pages/TracQuery
@@ -83,7 +83,7 @@
 This is displayed as:
   [[TicketQuery(version=0.6|0.7&resolution=duplicate, compact)]]
 
-Finally, if you wish to receive only the number of defects that match the query, use the ``count`` parameter.
+Finally, if you wish to receive only the number of defects that match the query, use the `count` parameter.
 
 {{{
 [[TicketQuery(version=0.6|0.7&resolution=duplicate, count)]]
diff --git a/trac/trac/wiki/default-pages/TracReports b/trac/trac/wiki/default-pages/TracReports
index ab4d3ca..180ee18 100644
--- a/trac/trac/wiki/default-pages/TracReports
+++ b/trac/trac/wiki/default-pages/TracReports
@@ -135,7 +135,6 @@
  http://trac.edgewall.org/reports/14?PRIORITY=high&SEVERITY=critical
 }}}
 
-Dynamic variables can also be used in the report title and description (since 1.1.1).
 
 === !Special/Constant Variables ===
 There is one dynamic variable whose value is set automatically (the URL does not have to be changed) to allow practical reports. 
@@ -163,6 +162,7 @@
  * '''ticket''' — Ticket ID number. Becomes a hyperlink to that ticket. 
  * '''id''' — same as '''ticket''' above when '''realm''' is not set
  * '''realm''' — together with '''id''', can be used to create links to other resources than tickets (e.g. a realm of ''wiki'' and an ''id'' to a page name will create a link to that wiki page)
+   - for some kind of resources, it may be necessary to specify their ''parent'' resources (e.g. for ''changeset'', which ''repos'') and this can be achieved using the '''parent_realm''' and '''parent_id''' columns
  * '''created, modified, date, time''' — Format cell as a date and/or time.
  * '''description''' — Ticket description field, parsed through the wiki engine.
 
diff --git a/trac/trac/wiki/default-pages/TracRepositoryAdmin b/trac/trac/wiki/default-pages/TracRepositoryAdmin
index 1723705..8a02e3c 100644
--- a/trac/trac/wiki/default-pages/TracRepositoryAdmin
+++ b/trac/trac/wiki/default-pages/TracRepositoryAdmin
@@ -89,7 +89,7 @@
 Please note that at the time of writing, no initial resynchronization or any hooks are necessary for Mercurial repositories - see [trac:#9485] for more information. 
 
 === Explicit synchronization === #ExplicitSync
-This is the preferred method of repository synchronization. It requires setting the `[trac]  repository_sync_per_request` option in [wiki:TracIni#trac-section trac.ini] to an empty value, and adding a call to `trac-admin` in the post-commit hook of each repository. Additionally, if a repository allows changing revision metadata, a call to `trac-admin` must be added to the post-revprop-change hook as well.
+This is the preferred method of repository synchronization. It requires setting the `[trac]  repository_sync_per_request` option in [wiki:TracIni#trac-section trac.ini] to an empty value, and adding a call to `trac-admin` in the `post-commit` hook of each repository. Additionally, if a repository allows changing revision metadata, a call to `trac-admin` must be added to the `post-revprop-change` hook as well.
 
  `changeset added <repos> <rev> [...]`::
    Notify Trac that one or more changesets have been added to a repository.
@@ -99,7 +99,9 @@
 
 The `<repos>` argument can be either a repository name (use "`(default)`" for the default repository) or the path to the repository.
 
-Note that you may have to set the environment variable PYTHON_EGG_CACHE to the same value as was used for the web server configuration before calling trac-admin, if you changed it from its default location. See [wiki:TracPlugins Trac Plugins] for more information.
+Note that you may have to set the environment variable `PYTHON_EGG_CACHE` to the same value as was used for the web server configuration before calling `trac-admin`, if you changed it from its default location. See [wiki:TracPlugins Trac Plugins] for more information.
+
+==== Subversion ====
 
 The following examples are complete post-commit and post-revprop-change scripts for Subversion. They should be edited for the specific environment, marked executable (where applicable) and placed in the `hooks` directory of each repository. On Unix (`post-commit`):
 {{{#!sh
@@ -107,14 +109,9 @@
 export PYTHON_EGG_CACHE="/path/to/dir"
 /usr/bin/trac-admin /path/to/env changeset added "$1" "$2"
 }}}
-Note: Ubuntu doesn't seem to like /usr/bin/trac-admin, so just use:
-{{{#!sh
-#!/bin/sh
-export PYTHON_EGG_CACHE="/path/to/dir"
-trac-admin /path/to/env/ changeset added "$1" "$2"
-}}}
+Note: Check with `whereis trac-admin`, whether `trac-admin` is really installed under `/usr/bin/` or maybe under `/usr/local/bin/` and adapt the path.
 On Windows (`post-commit.cmd`):
-{{{#!application/x-dos-batch
+{{{#!bat
 @C:\Python26\Scripts\trac-admin.exe C:\path\to\env changeset added "%1" "%2"
 }}}
 
@@ -125,7 +122,7 @@
 /usr/bin/trac-admin /path/to/env changeset modified "$1" "$2"
 }}}
 On Windows (`post-revprop-change.cmd`):
-{{{#!application/x-dos-batch
+{{{#!bat
 @C:\Python26\Scripts\trac-admin.exe C:\path\to\env changeset modified "%1" "%2"
 }}}
 
@@ -135,12 +132,30 @@
 
 See the [http://svnbook.red-bean.com/en/1.5/svn.reposadmin.create.html#svn.reposadmin.create.hooks section about hooks] in the Subversion book for more information. Other repository types will require different hook setups.
 
-Git hooks can be used in the same way for explicit syncing of git repositories. Add the following to `.git/hooks/post-commit`:
+==== Git ====
+
+Git hooks can be used in the same way for explicit syncing of Git repositories.  If your git repository is one that gets committed to directly on the machine that hosts trac, add the following to the `hooks/post-receive` file in your git repo (note: this will do nothing if you only update the repo by pushing to it):
 {{{#!sh
-REV=$(git rev-parse HEAD)
-trac-admin /path/to/env changeset added <my-repository> $REV
+#!/bin/sh 
+REV=$(git rev-parse HEAD) 
+trac-admin /path/to/env changeset added <repos> $REV 
 }}}
 
+Alternately, if your repository is one that only gets pushed to, add the following to the `hooks/post-receive` file in the repo:
+{{{#!sh
+#!/bin/sh
+while read oldrev newrev refname; do
+        git rev-list --reverse $newrev ^$oldrev  | \
+        while read rev; do
+                trac-admin /path/to/env changeset added <repos> $rev
+        done
+done
+}}}
+
+The `<repos>` argument can be either a repository name (use "`(default)`" for the default repository) or the path to the repository.
+
+==== Mercurial ====
+
 For Mercurial, add the following entries to the `.hgrc` file of each repository accessed by Trac (if [trac:TracMercurial] is installed in a Trac `plugins` directory, download [trac:source:mercurial-plugin/tracext/hg/hooks.py hooks.py] and place it somewhere accessible):
 {{{#!ini
 [hooks]
diff --git a/trac/trac/wiki/default-pages/TracSearch b/trac/trac/wiki/default-pages/TracSearch
index 0366205..5013d2e 100644
--- a/trac/trac/wiki/default-pages/TracSearch
+++ b/trac/trac/wiki/default-pages/TracSearch
@@ -16,7 +16,8 @@
  * ![42] -- Opens change set 42
  * !#42 -- Opens ticket number 42
  * !{1} -- Opens report 1
- * /trunk -- Opens the browser for the `trunk` directory
+ * /trunk -- Opens the browser for the `trunk` directory in the default repository
+ * /repos1/trunk -- Opens the browser for the `trunk` directory in the `repos1` repository
 
 == Advanced ==
 
diff --git a/trac/trac/wiki/default-pages/TracStandalone b/trac/trac/wiki/default-pages/TracStandalone
index 520ae44..73d2d95 100644
--- a/trac/trac/wiki/default-pages/TracStandalone
+++ b/trac/trac/wiki/default-pages/TracStandalone
@@ -13,7 +13,7 @@
 
  * Fewer features: Tracd implements a very simple web-server and is not as configurable or as scalable as Apache httpd.
  * No native HTTPS support: [http://www.rickk.com/sslwrap/ sslwrap] can be used instead,
-   or [http://trac.edgewall.org/wiki/STunnelTracd stunnel -- a tutorial on how to use stunnel with tracd] or Apache with mod_proxy.
+   or [trac:wiki:STunnelTracd stunnel -- a tutorial on how to use stunnel with tracd] or Apache with mod_proxy.
 
 == Usage examples ==
 
@@ -21,7 +21,7 @@
 {{{
  $ tracd -p 8080 /path/to/project
 }}}
-Stricly speaking this will make your Trac accessible to everybody from your network rather than ''localhost only''. To truly limit it use ''--hostname'' option.
+Strictly speaking this will make your Trac accessible to everybody from your network rather than ''localhost only''. To truly limit it use ''--hostname'' option.
 {{{
  $ tracd --hostname=localhost -p 8080 /path/to/project
 }}}
@@ -95,6 +95,8 @@
 
 Tracd allows you to run Trac without the need for Apache, but you can take advantage of Apache's password tools (htpasswd and htdigest) to easily create a password file in the proper format for tracd to use in authentication. (It is also possible to create the password file without htpasswd or htdigest; see below for alternatives)
 
+Make sure you place the generated password files on a filesystem which supports sub-second timestamps, as Trac will monitor their modified time and changes happening on a filesystem with too coarse-grained timestamp resolution (like `ext2` or `ext3` on Linux) may go undetected.
+
 Tracd provides support for both Basic and Digest authentication. Digest is considered more secure. The examples below use Digest; to use Basic authentication, replace `--auth` with `--basic-auth` in the command line.
 
 The general format for using authentication is:
diff --git a/trac/trac/wiki/default-pages/TracSyntaxColoring b/trac/trac/wiki/default-pages/TracSyntaxColoring
index ff7305a..7a876a5 100644
--- a/trac/trac/wiki/default-pages/TracSyntaxColoring
+++ b/trac/trac/wiki/default-pages/TracSyntaxColoring
@@ -6,8 +6,8 @@
 Currently Trac supports syntax coloring using one or more of the following packages:
 
  * [http://pygments.pocoo.org/ Pygments], by far the preferred system, as it covers a wide range of programming languages and other structured texts and is actively supported
- * [http://www.codento.com/people/mtr/genscript/ GNU Enscript], commonly available on Unix but somewhat unsupported on Windows
- * [http://silvercity.sourceforge.net/ SilverCity], legacy system, some versions can be [http://trac.edgewall.org/wiki/TracFaq#why-is-my-css-code-not-being-highlighted-even-though-i-have-silvercity-installed problematic]
+ * [http://www.codento.com/people/mtr/genscript/ GNU Enscript], commonly available on Unix but somewhat unsupported on Windows //(use is deprecated)//
+ * [http://silvercity.sourceforge.net/ SilverCity], legacy system, some versions can be problematic //(use is deprecated)//
 
 
 To activate syntax coloring, simply install either one (or more) of these packages (see [#ExtraSoftware] section below).
@@ -16,7 +16,7 @@
 
 === About Pygments ===
 
-Starting with trac 0.11 [http://pygments.org/ pygments] will be the new default highlighter. It's a highlighting library implemented in pure python, very fast, easy to extend and [http://pygments.org/docs/ well documented].
+Since Trac 0.11 [http://pygments.org/ pygments] is the new default highlighter. It's a highlighting library implemented in pure python, very fast, easy to extend and [http://pygments.org/docs/ well documented].
 
 The Pygments default style can specified in the [TracIni#mimeviewer-section mime-viewer] section of trac.ini. The default style can be overridden by setting a Style preference on the [/prefs/pygments preferences page]. 
 
@@ -35,7 +35,7 @@
 
 HTML documents are directly rendered only if the `render_unsafe_html` settings are enabled in the TracIni (those settings are present in multiple sections, as there are different security concerns depending where the document comes from). If you want to ensure that an HTML document gets syntax highlighted and not rendered, use the `text/xml` mimetype.
 
-If mimetype such as 'svn:mime-type' is set to 'text/plain', there is no coloring even if file is known type like 'java'.
+If a mimetype property such as 'svn:mime-type' is set to 'text/plain', there is no coloring even if file is known type like 'java'.
 
 === List of Languages Supported, by Highlighter #language-supported
 
diff --git a/trac/trac/wiki/default-pages/TracTickets b/trac/trac/wiki/default-pages/TracTickets
index 6dd7e0e..227269a 100644
--- a/trac/trac/wiki/default-pages/TracTickets
+++ b/trac/trac/wiki/default-pages/TracTickets
@@ -27,7 +27,7 @@
  * '''Assigned to/Owner''' — Principal person responsible for handling the issue.
  * '''Cc''' — A comma-separated list of other users or E-Mail addresses to notify. ''Note that this does not imply responsiblity or any other policy.''
  * '''Resolution''' — Reason for why a ticket was closed. One of {{{fixed}}}, {{{invalid}}}, {{{wontfix}}}, {{{duplicate}}}, {{{worksforme}}}.
- * '''Status''' — What is the current status? One of {{{new}}}, {{{assigned}}}, {{{closed}}}, {{{reopened}}}.
+ * '''Status''' — What is the current status? One of {{{new}}}, {{{assigned}}}, {{{accepted}}}, {{{closed}}}, {{{reopened}}}.
  * '''Summary''' — A brief description summarizing the problem or issue. Simple text without WikiFormatting.
  * '''Description''' — The body of the ticket. A good description should be specific, descriptive and to the point. Accepts WikiFormatting.
 
diff --git a/trac/trac/wiki/default-pages/TracTicketsCustomFields b/trac/trac/wiki/default-pages/TracTicketsCustomFields
index 3a5007b..c58e37e 100644
--- a/trac/trac/wiki/default-pages/TracTicketsCustomFields
+++ b/trac/trac/wiki/default-pages/TracTicketsCustomFields
@@ -39,11 +39,13 @@
  * '''textarea''': Multi-line text area.
    * label: Descriptive label.
    * value: Default text.
-   * cols: Width in columns.
+   * cols: Width in columns
    * rows: Height in lines.
    * order: Sort order placement.
    * format: Either `plain` for plain text or `wiki` to interpret the content as WikiFormatting. (''since 0.11.3'')
 
+Macros will be expanded when rendering `textarea` fields with format `wiki`, but not when rendering `text` fields with format `wiki`.
+
 === Sample Config ===
 {{{
 [ticket-custom]
@@ -113,6 +115,14 @@
 
 Note in particular the `LEFT OUTER JOIN` statement here.
 
+Note that if your config file uses an uppercase name, e.g.,
+{{{
+[ticket-custom]
+
+Progress_Type = text
+}}}
+you would use lowercase in the SQL:  `AND c.name = 'progress_type'`
+
 === Updating the database ===
 
 As noted above, any tickets created before a custom field has been defined will not have a value for that field. Here's a bit of SQL (tested with SQLite) that you can run directly on the Trac database to set an initial value for custom ticket fields. Inserts the default value of 'None' into a custom field called 'request_source' for all tickets that have no existing value:
diff --git a/trac/trac/wiki/default-pages/TracUpgrade b/trac/trac/wiki/default-pages/TracUpgrade
index 9b3e6c0..dbdaad8 100644
--- a/trac/trac/wiki/default-pages/TracUpgrade
+++ b/trac/trac/wiki/default-pages/TracUpgrade
@@ -14,10 +14,10 @@
 
 Get the new version as described in TracInstall, or your operating system specific procedure.
 
-If you already have a 0.11 version of Trac installed via `easy_install`, it might be easiest to also use `easy_install` to upgrade your Trac installation:
+If you already have a 0.12 version of Trac installed via `easy_install`, it might be easiest to also use `easy_install` to upgrade your Trac installation:
 
 {{{
-# easy_install --upgrade Trac==0.12
+# easy_install --upgrade Trac==1.0
 }}}
 
 If you do a manual (not operating system-specific) upgrade, you should also stop any running Trac servers before the installation. Doing "hot" upgrades is not advised, especially on Windows ([trac:#7265]).
@@ -73,8 +73,13 @@
 }}}
 
 === 6. Steps specific to a given Trac version  ===
+
 ==== Upgrading from Trac 0.12 to Trac 1.0 ==== #to1.0
 
+===== Python 2.4 no longer supported =====
+The minimum supported version of python is now 2.5
+
+===== Subversion components not enabled by default for new installations
 The Trac components for Subversion support are no longer enabled by default. To enable the svn support, you need to make sure the `tracopt.versioncontrol.svn` components are enabled, for example by setting the following in the TracIni:
 {{{
 [components]
@@ -82,8 +87,13 @@
 }}}
 The upgrade procedure should take care of this and change the TracIni appropriately, unless you already had the svn components explicitly disabled.
 
+
+===== Attachments migrated to new location
 Another step in the automatic upgrade will change the way the attachments are stored. If you're a bit paranoid, you might want to take a backup of the `attachments` directory before upgrading (but if you are, you already did a full copy of the environment, no?). In case the `attachments` directory contains some files which are //not// attachments, the last step of the migration to the new layout will fail: the deletion of the now unused `attachments` directory can't be done if there are still files and folders in it. You may ignore this error, but better go have a look to these files, move them elsewhere and remove the `attachments` directory manually to cleanup the environment. The attachments themselves are now all located in your environment below the `files/attachments` directory.
 
+===== Behavior of `[ticket] default_owner` changed
+Prior to 1.0, the owner field of new tickets always defaulted to `[ticket] default_owner` when the value was not empty. If the value was empty, the owner field defaulted to to the Component's owner. In 1.0 and later, the `default_owner` must be set to `< default >` to make new tickets default to the Component's owner. This change allows the `default_owner` to be set to an empty value if no default owner is desired.
+
 
 ==== Upgrading from Trac 0.11 to Trac 0.12 ====
 
@@ -103,7 +113,7 @@
 
 ===== Resynchronize the Trac Environment Against the Source Code Repository =====
 
-Each [TracEnvironment Trac environment] must be resynchronized against the source code repository in order to avoid errors such as "[http://trac.edgewall.org/ticket/6120 No changeset ??? in the repository]" while browsing the source through the Trac interface:
+Each [TracEnvironment Trac environment] must be resynchronized against the source code repository in order to avoid errors such as "[trac:#6120 No changeset ??? in the repository]" while browsing the source through the Trac interface:
 
 {{{
 trac-admin /path/to/projenv repository resync '*'
@@ -182,9 +192,8 @@
 If you've been using !CollabNet's Subversion package, you may need to uninstall that in favor of [http://alagazam.net/ Alagazam], which has the Python bindings readily available (see TracSubversion).  The good news is, that works with no tweaking.
 
 === Changing Database Backend ===
-==== SQLite to PostgreSQL ====
 
-The [http://trac-hacks.org/wiki/SqliteToPgScript sqlite2pg] script on [http://trac-hacks.org trac-hacks.org] has been written to assist in migrating a SQLite database to a PostgreSQL database
+The [http://trac-hacks.org/wiki/TracMigratePlugin TracMigratePlugin] on [http://trac-hacks.org trac-hacks.org] has been written to assist in migrating between SQLite, MySQL and PostgreSQL databases.
 
 === Upgrading from older versions of Trac === #OlderVersions
 
diff --git a/trac/trac/wiki/default-pages/TracWiki b/trac/trac/wiki/default-pages/TracWiki
index 4749e2b..de33e08 100644
--- a/trac/trac/wiki/default-pages/TracWiki
+++ b/trac/trac/wiki/default-pages/TracWiki
@@ -14,7 +14,7 @@
 general advice regarding HTML authoring apply here as well.
 For example, the ''[http://www.w3.org/Provider/Style Style Guide for online hypertext]'' explains how to think about the
 [http://www.w3.org/Provider/Style/Structure.html overall structure of a work] 
-and how to organize information [http://www.w3.org/Provider/Style/WithinDocument.html within each document]. One of the most important tip is “make your HTML page such that you can read it even if you don't follow any links.”
+and how to organize information [http://www.w3.org/Provider/Style/WithinDocument.html within each document]. One of the most important tips is “make your HTML page such that you can read it even if you don't follow any links.”
 
 Learn more about:
  * WikiFormatting rules, including advanced topics like WikiMacros and WikiProcessors
diff --git a/trac/trac/wiki/default-pages/TracWorkflow b/trac/trac/wiki/default-pages/TracWorkflow
index 1775efd..caaba28 100644
--- a/trac/trac/wiki/default-pages/TracWorkflow
+++ b/trac/trac/wiki/default-pages/TracWorkflow
@@ -1,7 +1,7 @@
 = The Trac Ticket Workflow System =
 [[TracGuideToc]]
 
-The Trac issue database provides a configurable workflow.
+The Trac ticket system provides a configurable workflow.
 
 == The Default Ticket Workflow ==
 === Environments upgraded from 0.10 ===
@@ -58,7 +58,7 @@
 
 There are several example workflows provided in the Trac source tree; look in [trac:source:trunk/contrib/workflow contrib/workflow] for `.ini` config sections.  One of those may be a good match for what you want. They can be pasted into the `[ticket-workflow]` section of your `trac.ini` file. However if you have existing tickets then there may be issues if those tickets have states that are not in the new workflow. 
 
-Here are some [http://trac.edgewall.org/wiki/WorkFlow/Examples diagrams] of the above examples.
+Here are some [trac:WorkFlow/Examples diagrams] of the above examples.
 
 == Basic Ticket Workflow Customization ==
 
@@ -67,7 +67,7 @@
 Create a `[ticket-workflow]` section in `trac.ini`.
 Within this section, each entry is an action that may be taken on a ticket. 
 For example, consider the `accept` action from `simple-workflow.ini`:
-{{{
+{{{#!ini
 accept = new,accepted -> accepted
 accept.permissions = TICKET_MODIFY
 accept.operations = set_owner_to_self
@@ -77,34 +77,34 @@
 The `accept.operations` line specifies changes that will be made to the ticket in addition to the status change when this action is taken.  In this case, when a user clicks on `accept`, the ticket owner field is updated to the logged in user.  Multiple operations may be specified in a comma separated list.
 
 The available operations are:
- - del_owner -- Clear the owner field.
- - set_owner -- Sets the owner to the selected or entered owner.
-   - ''actionname''`.set_owner` may optionally be set to a comma delimited list or a single value.
- - set_owner_to_self -- Sets the owner to the logged in user.
- - del_resolution -- Clears the resolution field
- - set_resolution -- Sets the resolution to the selected value.
-   - ''actionname''`.set_resolution` may optionally be set to a comma delimited list or a single value. Example:
-     {{{
+- **del_owner** -- Clear the owner field.
+- **set_owner** -- Sets the owner to the selected or entered owner. Defaults to the current user. When `[ticket] restrict_owner = true`, the select will be populated with users that have `TICKET_MODIFY` permission and an authenticated session.
+ - ''actionname''`.set_owner` may optionally be set to a comma delimited list of users that will be used to populate the select, or a single user.
+- **set_owner_to_self** -- Sets the owner to the logged in user.
+- **del_resolution** -- Clears the resolution field
+- **set_resolution** -- Sets the resolution to the selected value.
+ - ''actionname''`.set_resolution` may optionally be set to a comma delimited list or a single value. Example:
+ {{{#!ini
 resolve_new = new -> closed
 resolve_new.name = resolve
 resolve_new.operations = set_resolution
 resolve_new.permissions = TICKET_MODIFY
 resolve_new.set_resolution = invalid,wontfix
-     }}}
- - leave_status -- Displays "leave as <current status>" and makes no change to the ticket.
+}}}
+- **leave_status** -- Displays "leave as <current status>" and makes no change to the ticket.
 '''Note:''' Specifying conflicting operations (such as `set_owner` and `del_owner`) has unspecified results.
 
-{{{
+In this example, we see the `.name` attribute used.  The action here is `resolve_accepted`, but it will be presented to the user as `resolve`.
+
+{{{#!ini
 resolve_accepted = accepted -> closed
 resolve_accepted.name = resolve
 resolve_accepted.permissions = TICKET_MODIFY
 resolve_accepted.operations = set_resolution
 }}}
 
-In this example, we see the `.name` attribute used.  The action here is `resolve_accepted`, but it will be presented to the user as `resolve`.
-
 For actions that should be available in all states, `*` may be used in place of the state.  The obvious example is the `leave` action:
-{{{
+{{{#!ini
 leave = * -> *
 leave.operations = leave_status
 leave.default = 1
@@ -114,24 +114,22 @@
 
 There are a couple of hard-coded constraints to the workflow.  In particular, tickets are created with status `new`, and tickets are expected to have a `closed` state.  Further, the default reports/queries treat any state other than `closed` as an open state.
 
-While creating or modifying a ticket workflow, `contrib/workflow/workflow_parser.py` may be useful.  It can create `.dot` files that [http://www.graphviz.org GraphViz] understands to provide a visual description of the workflow.
+Workflows can be visualized by rendering them on the wiki using the [WikiMacros#Workflow-macro Workflow macro].
 
-This can be done as follows (your install path may be different).
-{{{
+Workflows can also be visualized using the `contrib/workflow/workflow_parser.py` script.  The script outputs `.dot` files that [http://www.graphviz.org GraphViz] understands. The script can be used as follows (your install path may be different):
+{{{#!sh
 cd /var/local/trac_devel/contrib/workflow/
 sudo ./showworkflow /srv/trac/PlannerSuite/conf/trac.ini
 }}}
 And then open up the resulting `trac.pdf` file created by the script (it will be in the same directory as the `trac.ini` file).
 
-An online copy of the workflow parser is available at http://foss.wush.net/cgi-bin/visual-workflow.pl
-
 After you have changed a workflow, you need to restart apache for the changes to take effect. This is important, because the changes will still show up when you run your script, but all the old workflow steps will still be there until the server is restarted.
 
 == Example: Adding optional Testing with Workflow ==
 
 By adding the following to your [ticket-workflow] section of trac.ini you get optional testing.  When the ticket is in new, accepted or needs_work status you can choose to submit it for testing.  When it's in the testing status the user gets the option to reject it and send it back to needs_work, or pass the testing and send it along to closed.  If they accept it then it gets automatically marked as closed and the resolution is set to fixed.  Since all the old work flow remains, a ticket can skip this entire section.
 
-{{{
+{{{#!ini
 testing = new,accepted,needs_work,assigned,reopened -> testing
 testing.name = Submit to reporter for testing
 testing.permissions = TICKET_MODIFY
@@ -161,7 +159,7 @@
 
 The new `reviewing` state along with its associated `review` action looks like this:
 
-{{{
+{{{#!ini
 review = new,assigned,reopened -> reviewing
 review.operations = set_owner
 review.permissions = TICKET_MODIFY
@@ -169,7 +167,7 @@
 
 Then, to integrate this with the default Trac 0.11 workflow, you also need to add the `reviewing` state to the `accept` and `resolve` actions, like so:
 
-{{{
+{{{#!ini
 accept = new,reviewing -> assigned
 […]
 resolve = new,assigned,reopened,reviewing -> closed
@@ -177,7 +175,7 @@
 
 Optionally, you can also add a new action that allows you to change the ticket's owner without moving the ticket out of the `reviewing` state. This enables you to reassign review work without pushing the ticket back to the `new` status.
 
-{{{
+{{{#!ini
 reassign_reviewing = reviewing -> *
 reassign_reviewing.name = reassign review
 reassign_reviewing.operations = set_owner
@@ -186,7 +184,7 @@
 
 The full `[ticket-workflow]` configuration will thus look like this:
 
-{{{
+{{{#!ini
 [ticket-workflow]
 accept = new,reviewing -> assigned
 accept.operations = set_owner_to_self
@@ -214,9 +212,9 @@
 
 == Example: Limit the resolution options for a new ticket ==
 
-The above resolve_new operation allows you to set the possible resolutions for a new ticket.  By modifying the existing resolve action and removing the new status from before the `->` we then get two resolve actions.  One with limited resolutions for new tickets, and then the regular one once a ticket is accepted.
+The above `resolve_new` operation allows you to set the possible resolutions for a new ticket.  By modifying the existing resolve action and removing the new status from before the `->` we then get two resolve actions.  One with limited resolutions for new tickets, and then the regular one once a ticket is accepted.
 
-{{{
+{{{#!ini
 resolve_new = new -> closed
 resolve_new.name = resolve
 resolve_new.operations = set_resolution
@@ -238,32 +236,6 @@
 
 If you add additional states to your workflow, you may want to customize your milestone progress bars as well.  See [TracIni#milestone-groups-section TracIni].
 
-== some ideas for next steps ==
+== Ideas for next steps ==
 
-New enhancement ideas for the workflow system should be filed as enhancement tickets against the `ticket system` component.  If desired, add a single-line link to that ticket here.  Also look at the [http://trac-hacks.org/wiki/AdvancedTicketWorkflowPlugin AdvancedTicketWorkflowPlugin] as it provides experimental operations.
-
-If you have a response to the comments below, create an enhancement ticket, and replace the description below with a link to the ticket.
-
- * the "operation" could be on the nodes, possible operations are:
-   * '''preops''': automatic, before entering the state/activity
-   * '''postops''': automatic, when leaving the state/activity
-   * '''actions''': can be chosen by the owner in the list at the bottom, and/or drop-down/pop-up together with the default actions of leaving the node on one of the arrows.
-''This appears to add complexity without adding functionality; please provide a detailed example where these additions allow something currently impossible to implement.''
-
- * operations could be anything: sum up the time used for the activity, or just write some statistical fields like 
-''A workflow plugin can add an arbitrary workflow operation, so this is already possible.''
-
- * set_actor should be an operation allowing to set the owner, e.g. as a "preop":
-   * either to a role, a person
-   * entered fix at define time, or at run time, e.g. out of a field, or select.
-''This is either duplicating the existing `set_owner` operation, or needs to be clarified.''
-
- * Actions should be selectable based on the ticket type (different Workflows for different tickets)
-''Look into the [http://trac-hacks.org/wiki/AdvancedTicketWorkflowPlugin AdvancedTicketWorkflowPlugin]'s `triage` operation.''
-
- * I'd wish to have an option to perform automatic status changes. In my case, I do not want to start with "new", but with "assigned". So tickets in state "new" should automatically go into state "assigned". Or is there already a way to do this and I just missed it?
-''Have a look at [http://trac-hacks.org/wiki/TicketCreationStatusPlugin TicketCreationStatusPlugin] and [http://trac-hacks.org/wiki/TicketConditionalCreationStatusPlugin TicketConditionalCreationStatusPlugin]''
-
- * I added a 'testing' state. A tester can close the ticket or reject it. I'd like the transition from testing to rejected to set the owner to the person that put the ticket in 'testing'. The [http://trac-hacks.org/wiki/AdvancedTicketWorkflowPlugin AdvancedTicketWorkflowPlugin] is close with set_owner_to_field, but we need something like set_field_to_owner.
-
- * I'd like to track the time a ticket is in each state, adding up 'disjoints' intervals in the same state.
+New enhancement ideas for the workflow system should be filed as enhancement tickets against the `ticket system` component.  You can also document ideas on the [trac:TracIdeas/TracWorkflow TracIdeas/TracWorkflow] page.  Also look at the [http://trac-hacks.org/wiki/AdvancedTicketWorkflowPlugin AdvancedTicketWorkflowPlugin] as it provides experimental operations.
diff --git a/trac/trac/wiki/default-pages/WikiFormatting b/trac/trac/wiki/default-pages/WikiFormatting
index 904a677..18e63a3 100644
--- a/trac/trac/wiki/default-pages/WikiFormatting
+++ b/trac/trac/wiki/default-pages/WikiFormatting
@@ -263,6 +263,7 @@
    * ,,subscript,,
    * **also bold**, //italic as well//, 
      and **'' bold italic **'' //(since 0.12)//
+   * [[span(style=color: #FF0000, a red text )]]
   }}}
 }}}
 {{{#!td
@@ -280,6 +281,7 @@
  * ,,subscript,,
  * **also bold**, //italic as well//, 
    and **'' bold italic **'' //(since 0.12)//
+ * [[span(style=color: #FF0000, a red text )]]
 }}}
 
 Notes:
@@ -736,9 +738,9 @@
   }}}
   {{{
 Various forms of escaping for list markup:
- `-` escaped minus sign \\
- ``1. escaped number  \\
- {{{*}}} escaped asterisk sign
+ ^^- escaped minus sign \\
+ ^^1. escaped number  \\
+ ^^* escaped asterisk sign
   }}}
 }}}
 {{{#!td
@@ -746,9 +748,9 @@
  !#42 is not a link
 
 Various forms of escaping for list markup:
- `-` escaped minus sign \\
- ``1. escaped number  \\
- {{{*}}} escaped asterisk sign
+ ^^- escaped minus sign \\
+ ^^1. escaped number  \\
+ ^^* escaped asterisk sign
 }}}
 
 == Images ==
@@ -1004,4 +1006,3 @@
 {{{#!td
 !WikiCreole style \\ line\\break
 }}}
-
diff --git a/trac/trac/wiki/default-pages/WikiMacros b/trac/trac/wiki/default-pages/WikiMacros
index 16c7afc..4c5838f 100644
--- a/trac/trac/wiki/default-pages/WikiMacros
+++ b/trac/trac/wiki/default-pages/WikiMacros
@@ -101,7 +101,7 @@
 
     def expand_macro(self, formatter, name, text):
         t = datetime.now(utc)
-        return tag.b(format_datetime(t, '%c'))
+        return tag.strong(format_datetime(t, '%c'))
 }}}
 
 === Macro with arguments ===
diff --git a/trac/trac/wiki/default-pages/WikiNewPage b/trac/trac/wiki/default-pages/WikiNewPage
index 4c458bb..880e882 100644
--- a/trac/trac/wiki/default-pages/WikiNewPage
+++ b/trac/trac/wiki/default-pages/WikiNewPage
@@ -3,14 +3,14 @@
 
 Note: make sure you actually have the rights to edit wiki pages. If you don't see the **Edit this page** button, read the information relative to the editing policy for your Trac installation (usually on the front page WikiStart), or contact your local Trac administrator.
 
+You can create a new wiki page by typing the CamelCase name of the page in the quick-search field at the top of the page, or by trying to view a wiki page of that name (That is by visiting http://trac.edgewall.org/wiki/MyNewWikiPage for example). But note that the page will effectively be "orphaned" unless you link to it from somewhere else. Alternatively:
+
  1. Choose a name for your new page. See WikiPageNames for naming conventions.
  2. Edit an existing page (or any other resources that support WikiFormatting and add a [TracLinks link] to your new page. Save your changes.
  3. Follow the link you created to take you to the new page. Trac will display a "describe !PageName here" message.
  4. Click the "Edit this page" button to edit and add content to your new page. Save your changes.
  5. All done. Your new page is published.
 
-You can skip the second step by entering the CamelCase name of the page in the quick-search field at the top of the page. But note that the page will effectively be "orphaned" unless you link to it from somewhere else.
-
 == Rename a page #renaming
 
 While picking up good WikiPageNames is important, you can always change your mind
diff --git a/trac/trac/wiki/default-pages/WikiProcessors b/trac/trac/wiki/default-pages/WikiProcessors
index c64fe01..cff6478 100644
--- a/trac/trac/wiki/default-pages/WikiProcessors
+++ b/trac/trac/wiki/default-pages/WikiProcessors
@@ -155,8 +155,9 @@
 
 || '''`#!default`''' || Present the text verbatim in a preformatted text block. This is the same as specifying ''no'' processor name (and no `#!`) ||
 || '''`#!comment`''' || Do not process the text in this section (i.e. contents exist only in the plain text - not in the rendered page). ||
+|| '''`#!rtl`''' || Introduce a Right-To-Left block with appropriate CSS direction and styling ''(since 0.12.2)'' ||
 |||| ||
-||||= '''HTML related''' =||
+||||= '''[=#HTMLrelated HTML related]''' =||
 || '''`#!html`''' || Insert custom HTML in a wiki page. ||
 || '''`#!htmlcomment`''' || Insert an HTML comment in a wiki page (''since 0.12''). ||
 || || Note that `#!html` blocks have to be ''self-contained'', i.e. you can't start an HTML element in one block and close it later in a second block. Use the following processors for achieving a similar effect.  ||
@@ -164,14 +165,15 @@
 || '''`#!span`''' || Wrap an arbitrary Wiki content inside a <span> element (''since 0.11''). ||
 || '''`#!td`''' || Wrap an arbitrary Wiki content inside a <td> element (''since 0.12'') ||
 || '''`#!th`''' || Wrap an arbitrary Wiki content inside a <th> element (''since 0.12'') ||
-|| '''`#!tr`''' || Can optionally be used for wrapping `#!td` and `#!th` blocks, either for specifying row attributes of better visual grouping (''since 0.12'') ||
+|| '''`#!tr`''' || Can optionally be used for wrapping `#!td` and `#!th` blocks, either for specifying row attributes or better visual grouping (''since 0.12'') ||
+|| '''`#!table`''' || Can optionally be used for wrapping `#!tr`, `#!td` and `#!th` blocks, for specifying table attributes. One current limitation however is that tables cannot be nested. (''since 0.12'') || 
 || || See WikiHtml for example usage and more details about these processors. ||
 |||| ||
 ||||= '''Other Markups''' =||
 || '''`#!rst`''' || Trac support for Restructured Text. See WikiRestructuredText. ||
 || '''`#!textile`''' || Supported if [http://cheeseshop.python.org/pypi/textile Textile] is installed. See [http://www.textism.com/tools/textile/ a Textile reference]. ||
 |||| ||
-||||= '''Code Highlighting Support''' =||
+||||= '''[=#CodeHighlightingSupport Code Highlighting Support]''' =||
 || '''`#!c`''' [[BR]] '''`#!cpp`''' (C++) [[BR]] '''`#!python`''' [[BR]] '''`#!perl`''' [[BR]] '''`#!ruby`''' [[BR]] '''`#!php`''' [[BR]] '''`#!asp`''' [[BR]] '''`#!java`''' [[BR]] '''`#!js`''' (Javascript) [[BR]] '''`#!sql`''' [[BR]] '''`#!xml`''' (XML or HTML) [[BR]] '''`#!sh`''' (!Bourne/Bash shell) [[BR]] '''etc.''' [[BR]] || Trac includes processors to provide inline syntax highlighting for source code in various languages. [[BR]] [[BR]] Trac relies on external software packages for syntax coloring, like [http://pygments.org Pygments]. [[BR]] [[BR]] See TracSyntaxColoring for information about which languages are supported and how to enable support for more languages. ||
 |||| ||
 
diff --git a/trac/trac/wiki/default-pages/WikiStart b/trac/trac/wiki/default-pages/WikiStart
index 341e28e..7f7123f 100644
--- a/trac/trac/wiki/default-pages/WikiStart
+++ b/trac/trac/wiki/default-pages/WikiStart
@@ -1,4 +1,4 @@
-= Welcome to Trac 1.0.1 =
+= Welcome to Trac 1.0.2 =
 
 Trac is a '''minimalistic''' approach to '''web-based''' management of
 '''software projects'''. Its goal is to simplify effective tracking and handling of software issues, enhancements and overall progress.
diff --git a/trac/trac/wiki/formatter.py b/trac/trac/wiki/formatter.py
index 32101de..6baa0c9 100644
--- a/trac/trac/wiki/formatter.py
+++ b/trac/trac/wiki/formatter.py
@@ -35,7 +35,7 @@
 from trac.util.text import exception_to_unicode, shorten_line, to_unicode, \
                            unicode_quote, unicode_quote_plus, unquote_label
 from trac.util.html import TracHTMLSanitizer
-from trac.util.translation import _
+from trac.util.translation import _, tag_
 from trac.wiki.api import WikiSystem, parse_args
 from trac.wiki.parser import WikiParser, parse_processor_args
 
@@ -69,7 +69,8 @@
     idx = target.find('?')
     if idx >= 0:
         target, query = target[:idx], target[idx:]
-    return (target, query, fragment)
+    return target, query, fragment
+
 
 def concat_path_query_fragment(path, query, fragment=None):
     """Assemble `path`, `query` and `fragment` into a proper URL.
@@ -97,6 +98,7 @@
         f = fragment
     return p + q + ('' if f == '#' else f)
 
+
 def _markup_to_unicode(markup):
     stream = None
     if isinstance(markup, Element):
@@ -150,9 +152,9 @@
                               }
 
         self.inline_check = {'html': self._html_is_inline,
-                                'htmlcomment': True, 'comment': True,
-                                'span': True, 'Span': True,
-                                }.get(name)
+                             'htmlcomment': True, 'comment': True,
+                             'span': True, 'Span': True,
+                             }.get(name)
 
         self._sanitizer = TracHTMLSanitizer(formatter.wiki.safe_schemes)
 
@@ -172,7 +174,6 @@
                         break
         if not self.processor:
             # Find a matching mimeview renderer
-            from trac.mimeview.api import Mimeview
             mimeview = Mimeview(formatter.env)
             for renderer in mimeview.renderers:
                 if renderer.get_quality_ratio(self.name) > 1:
@@ -185,7 +186,8 @@
                     self.processor = self._mimeview_processor
         if not self.processor:
             self.processor = self._default_processor
-            self.error = "No macro or processor named '%s' found" % name
+            self.error = _("No macro or processor named '%(name)s' found",
+                           name=name)
 
     # inline checks
 
@@ -349,8 +351,8 @@
 
     def process(self, text, in_paragraph=False):
         if self.error:
-            text = system_message(tag('Error: Failed to load processor ',
-                                      tag.code(self.name)),
+            text = system_message(tag_("Error: Failed to load processor "
+                                       "%(name)s", name=tag.code(self.name)),
                                   self.error)
         else:
             text = self.processor(text)
@@ -438,7 +440,7 @@
         'MM_STRIKE': ('<del>', '</del>'),
         'MM_SUBSCRIPT': ('<sub>', '</sub>'),
         'MM_SUPERSCRIPT': ('<sup>', '</sup>'),
-        }
+    }
 
     def _get_open_tag(self, tag):
         """Retrieve opening tag for direct or indirect `tag`."""
@@ -477,12 +479,12 @@
 
         If `close_tag` is not specified, it's an indirect tag (0.12)
         """
-        tmp =  ''
+        tmp = ''
         for i in xrange(len(self._open_tags) - 1, -1, -1):
             tag = self._open_tags[i]
             tmp += self._get_close_tag(tag)
             if (open_tag == tag,
-                (open_tag, close_tag) == tag)[bool(close_tag)]:
+                    (open_tag, close_tag) == tag)[bool(close_tag)]:
                 del self._open_tags[i]
                 for j in xrange(i, len(self._open_tags)):
                     tmp += self._get_open_tag(self._open_tags[j])
@@ -491,21 +493,42 @@
 
     def _indirect_tag_handler(self, match, tag):
         """Handle binary inline style tags (indirect way, 0.12)"""
+        if self._list_stack and not self.in_list_item:
+            self.close_list()
+
         if self.tag_open_p(tag):
             return self.close_tag(tag)
         else:
             return self.open_tag(tag)
 
     def _bolditalic_formatter(self, match, fullmatch):
+        if self._list_stack and not self.in_list_item:
+            self.close_list()
+
+        bold_open = self.tag_open_p('MM_BOLD')
         italic_open = self.tag_open_p('MM_ITALIC')
-        tmp = ''
-        if italic_open:
-            tmp += self._get_close_tag('MM_ITALIC')
-            self.close_tag('MM_ITALIC')
-        tmp += self._bold_formatter(match, fullmatch)
-        if not italic_open:
-            tmp += self.open_tag('MM_ITALIC')
-        return tmp
+        if bold_open and italic_open:
+            bold_idx = self._open_tags.index('MM_BOLD')
+            italic_idx = self._open_tags.index('MM_ITALIC')
+            if italic_idx < bold_idx:
+                close_tags = ('MM_BOLD', 'MM_ITALIC')
+            else:
+                close_tags = ('MM_ITALIC', 'MM_BOLD')
+            open_tags = ()
+        elif bold_open:
+            close_tags = ('MM_BOLD',)
+            open_tags = ('MM_ITALIC',)
+        elif italic_open:
+            close_tags = ('MM_ITALIC',)
+            open_tags = ('MM_BOLD',)
+        else:
+            close_tags = ()
+            open_tags = ('MM_BOLD', 'MM_ITALIC')
+
+        tmp = []
+        tmp.extend(self.close_tag(tag) for tag in close_tags)
+        tmp.extend(self.open_tag(tag) for tag in open_tags)
+        return ''.join(tmp)
 
     def _bold_formatter(self, match, fullmatch):
         return self._indirect_tag_handler(match, 'MM_BOLD')
@@ -549,7 +572,7 @@
 
     # -- Post- IWikiSyntaxProvider rules
 
-    # WikiCreole line brekas
+    # WikiCreole line breaks
 
     def _linebreak_wc_formatter(self, match, fullmatch):
         return '<br />'
@@ -575,7 +598,7 @@
         ns = fullmatch.group('snsbr')
         target = unquote_label(fullmatch.group('stgtbr'))
         match = match[1:-1]
-        return '&lt;%s&gt;' % \
+        return u'&lt;%s&gt;' % \
                 self._make_link(ns, target, match, match, fullmatch)
 
     def _shref_formatter(self, match, fullmatch):
@@ -764,10 +787,10 @@
         try:
             return macro.ensure_inline(macro.process(args))
         except Exception, e:
-            self.env.log.error('Macro %s(%s) failed: %s' %
-                    (name, args, exception_to_unicode(e, traceback=True)))
-            return system_message('Error: Macro %s(%s) failed' % (name, args),
-                                  e)
+            self.env.log.error('Macro %s(%s) failed:%s', name, args,
+                               exception_to_unicode(e, traceback=True))
+            return system_message(_("Error: Macro %(name)s(%(args)s) failed",
+                                    name=name, args=args), to_unicode(e))
 
     # Headings
 
@@ -1005,7 +1028,7 @@
         if separator[-1] == '=':
             numpipes -= 1
             cell = 'th'
-        colspan = numpipes/2
+        colspan = numpipes / 2
         if is_last is not None:
             if is_last and is_last[-1] == '\\':
                 self.continue_table_row = 1
@@ -1131,7 +1154,8 @@
                                          for l in self.code_buf]
                     self.code_buf.append('')
                 code_text = os.linesep.join(self.code_buf)
-                processed = self.code_processor.process(code_text)
+                processed = self._exec_processor(self.code_processor,
+                                                 code_text)
                 self.out.write(_markup_to_unicode(processed))
             else:
                 self.code_buf.append(line)
@@ -1152,6 +1176,15 @@
         while self.in_code_block > 0:
             self.handle_code_block(WikiParser.ENDBLOCK)
 
+    def _exec_processor(self, processor, text):
+        try:
+            return processor.process(text)
+        except Exception, e:
+            self.env.log.error('Processor %s failed:%s', processor.name,
+                               exception_to_unicode(e, traceback=True))
+            return system_message(_("Error: Processor %(name)s failed",
+                                    name=processor.name), to_unicode(e))
+
     # > quotes
 
     def handle_quote_block(self, line):
@@ -1505,7 +1538,7 @@
 class InlineHtmlFormatter(object):
     """Format parsed wiki text to inline elements HTML.
 
-    Block level content will be disguarded or compacted.
+    Block level content will be discarded or compacted.
     """
 
     flavor = 'oneliner'
diff --git a/trac/trac/wiki/intertrac.py b/trac/trac/wiki/intertrac.py
index 58e00c2..d0d7708 100644
--- a/trac/trac/wiki/intertrac.py
+++ b/trac/trac/wiki/intertrac.py
@@ -20,9 +20,8 @@
 
 from trac.config import ConfigSection
 from trac.core import *
-from trac.perm import PermissionError
 from trac.util.html import find_element
-from trac.util.translation import _, N_
+from trac.util.translation import N_, _, tag_
 from trac.web.api import IRequestHandler
 from trac.wiki.api import IWikiMacroProvider
 from trac.wiki.formatter import extract_link
@@ -53,7 +52,7 @@
            it doesn't know how to dispatch an InterTrac link, and it's up to
            the local Trac to prepare the correct link. Not all links will work
            that way, but the most common do. This is called the compatibility
-           mode, and is `true` by default.
+           mode, and is `false` by default.
          * If you know that the remote Trac knows how to dispatch InterTrac
            links, you can explicitly disable this compatibility mode and then
            ''any'' TracLinks can become InterTrac links.
@@ -90,8 +89,10 @@
         link_frag = extract_link(self.env, web_context(req), link)
         if isinstance(link_frag, (Element, Fragment)):
             elt = find_element(link_frag, 'href')
-            if elt is None: # most probably no permissions to view
-                raise PermissionError(_("Can't view %(link)s:", link=link))
+            if elt is None:
+                raise TracError(
+                    _("Can't view %(link)s. Resource doesn't exist or "
+                      "you don't have the required permission.", link=link))
             href = elt.attrib.get('href')
         else:
             href = req.href(link.rstrip(':'))
@@ -122,16 +123,18 @@
         def generate_prefix(prefix):
             intertrac = intertracs[prefix]
             if isinstance(intertrac, basestring):
-                yield tag.tr(tag.td(tag.b(prefix)),
-                             tag.td('Alias for ', tag.b(intertrac)))
+                yield tag.tr(tag.td(tag.strong(prefix)),
+                             tag.td(tag_("Alias for %(name)s",
+                                         name=tag.strong(intertrac))))
             else:
                 url = intertrac.get('url', '')
                 if url:
                     title = intertrac.get('title', url)
-                    yield tag.tr(tag.td(tag.a(tag.b(prefix),
+                    yield tag.tr(tag.td(tag.a(tag.strong(prefix),
                                               href=url + '/timeline')),
                                  tag.td(tag.a(title, href=url)))
 
         return tag.table(class_="wiki intertrac")(
-            tag.tr(tag.th(tag.em('Prefix')), tag.th(tag.em('Trac Site'))),
+            tag.tr(tag.th(tag.em(_("Prefix"))),
+                   tag.th(tag.em(_("Trac Site")))),
             [generate_prefix(p) for p in sorted(intertracs.keys())])
diff --git a/trac/trac/wiki/interwiki.py b/trac/trac/wiki/interwiki.py
index aabdf2f..7779d2f 100644
--- a/trac/trac/wiki/interwiki.py
+++ b/trac/trac/wiki/interwiki.py
@@ -172,8 +172,8 @@
                 'rc_url': self._expand_or_append(url, ['RecentChanges']),
                 'description': url if title == prefix else title})
 
-        return tag.table(tag.tr(tag.th(tag.em("Prefix")),
-                                tag.th(tag.em("Site"))),
+        return tag.table(tag.tr(tag.th(tag.em(_("Prefix"))),
+                                tag.th(tag.em(_("Site")))),
                          [tag.tr(tag.td(tag.a(w['prefix'], href=w['rc_url'])),
                                  tag.td(tag.a(w['description'],
                                               href=w['url'])))
diff --git a/trac/trac/wiki/macros.py b/trac/trac/wiki/macros.py
index 123bc35..eb8cefc 100644
--- a/trac/trac/wiki/macros.py
+++ b/trac/trac/wiki/macros.py
@@ -14,6 +14,8 @@
 #
 # Author: Christopher Lenz <cmlenz@gmx.de>
 
+from __future__ import with_statement
+
 from fnmatch import fnmatchcase
 from itertools import groupby
 import inspect
@@ -71,7 +73,7 @@
     def parse_macro(self, parser, name, content):
         raise NotImplementedError
 
-    def expand_macro(self, formatter, name, content):
+    def expand_macro(self, formatter, name, content, args=None):
         raise NotImplementedError(
             "pre-0.11 Wiki macro %s by provider %s no longer supported" %
             (name, self.__class__))
@@ -327,8 +329,9 @@
                         max(time) AS max_time FROM wiki"""
         args = []
         if prefix:
-            sql += " WHERE name LIKE %s"
-            args.append(prefix + '%')
+            with self.env.db_query as db:
+                sql += " WHERE name %s" % db.prefix_match()
+                args.append(db.prefix_match_value(prefix))
         sql += " GROUP BY name ORDER BY max_time DESC"
         if limit:
             sql += " LIMIT %s"
@@ -355,8 +358,9 @@
 
         items_per_date = (
             (date, (tag.li(tag.a(page, href=formatter.href.wiki(name)),
-                           tag.small(' (', tag.a('diff', href=diff_href), ')')
-                           if diff_href else None, '\n')
+                           tag.small(' (', tag.a(_("diff"), href=diff_href),
+                                     ')') if diff_href else None,
+                           '\n')
                     for page, name, version, diff_href in entries))
             for date, entries in entries_per_date)
 
@@ -477,21 +481,22 @@
 
     Examples:
     {{{
-        [[Image(photo.jpg)]]                           # simplest
-        [[Image(photo.jpg, 120px)]]                    # with image width size
-        [[Image(photo.jpg, right)]]                    # aligned by keyword
-        [[Image(photo.jpg, nolink)]]                   # without link to source
-        [[Image(photo.jpg, align=right)]]              # aligned by attribute
+    [[Image(photo.jpg)]]               # simplest
+    [[Image(photo.jpg, 120px)]]        # with image width size
+    [[Image(photo.jpg, right)]]        # aligned by keyword
+    [[Image(photo.jpg, nolink)]]       # without link to source
+    [[Image(photo.jpg, align=right)]]  # aligned by attribute
     }}}
 
-    You can use image from other page, other ticket or other module.
+    You can use an image from a wiki page, ticket or other module.
     {{{
-        [[Image(OtherPage:foo.bmp)]]    # if current module is wiki
-        [[Image(base/sub:bar.bmp)]]     # from hierarchical wiki page
-        [[Image(#3:baz.bmp)]]           # if in a ticket, point to #3
-        [[Image(ticket:36:boo.jpg)]]
-        [[Image(source:/images/bee.jpg)]] # straight from the repository!
-        [[Image(htdocs:foo/bar.png)]]   # image file in project htdocs dir.
+    [[Image(OtherPage:foo.bmp)]]    # from a wiki page
+    [[Image(base/sub:bar.bmp)]]     # from hierarchical wiki page
+    [[Image(#3:baz.bmp)]]           # from another ticket
+    [[Image(ticket:36:boo.jpg)]]    # from another ticket (long form)
+    [[Image(source:/img/bee.jpg)]]  # from the repository
+    [[Image(htdocs:foo/bar.png)]]   # from project htdocs dir
+    [[Image(shared:foo/bar.png)]]   # from shared htdocs dir (since 1.0.2)
     }}}
 
     ''Adapted from the Image.py macro created by Shun-ichi Goto
@@ -501,6 +506,8 @@
     def is_inline(self, content):
         return True
 
+    _split_filespec_re = re.compile(r''':(?!(?:[^"':]|[^"']:[^'"])+["'])''')
+
     def expand_macro(self, formatter, name, content):
         # args will be null if the macro is called without parenthesis.
         if not content:
@@ -534,7 +541,7 @@
         except Exception:
             browser_links = []
         while args:
-            arg = args.pop(0).strip()
+            arg = stripws(args.pop(0))
             if size_re.match(arg):
                 # 'width' keyword
                 attr['width'] = arg
@@ -578,7 +585,8 @@
                         attr[str(key)] = val # will be used as a __call__ kwd
 
         # parse filespec argument to get realm and id if contained.
-        parts = filespec.split(':')
+        parts = [i.strip('''['"]''')
+                 for i in self._split_filespec_re.split(filespec)]
         url = raw_url = desc = None
         attachment = None
         if (parts and parts[0] in ('http', 'https', 'ftp')): # absolute
@@ -626,6 +634,9 @@
                 elif id == 'htdocs':
                     raw_url = url = formatter.href.chrome('site', filename)
                     desc = os.path.basename(filename)
+                elif id == 'shared':
+                    raw_url = url = formatter.href.chrome('shared', filename)
+                    desc = os.path.basename(filename)
                 else:
                     realm = 'wiki'
                 if realm:
@@ -732,10 +743,10 @@
     options whose section and name start with the filters are output.
     """)
 
-    def expand_macro(self, formatter, name, args):
+    def expand_macro(self, formatter, name, content):
         from trac.config import ConfigSection, Option
         section_filter = key_filter = ''
-        args, kw = parse_args(args)
+        args, kw = parse_args(content)
         if args:
             section_filter = args.pop(0).strip()
         if args:
@@ -761,19 +772,11 @@
 
         def default_cell(option):
             default = option.default
-            if default is True:
-                default = 'true'
-            elif default is False:
-                default = 'false'
-            elif default == 0:
-                default = '0.0' if isinstance(default, float) else '0'
-            elif default:
-                default = ', '.join(to_unicode(val) for val in default) \
-                          if isinstance(default, (list, tuple)) \
-                          else to_unicode(default)
+            if default is not None and default != '':
+                return tag.td(tag.code(option.dumps(default)),
+                              class_='default')
             else:
                 return tag.td(_("(no default)"), class_='nodefault')
-            return tag.td(tag.code(default), class_='default')
 
         return tag.div(class_='tracini')(
             (tag.h3(tag.code('[%s]' % section), id='%s-section' % section),
@@ -782,9 +785,11 @@
                  tag.tr(tag.td(tag.tt(option.name)),
                         tag.td(format_to_oneliner(
                             self.env, formatter.context, getdoc(option))),
-                        default_cell(option))
-                 for option in sorted(options.get(section, {}).itervalues(),
-                                      key=lambda o: o.name)
+                        default_cell(option),
+                        class_='odd' if idx % 2 else 'even')
+                 for idx, option in \
+                    enumerate(sorted(options.get(section, {}).itervalues(),
+                                     key=lambda o: o.name))
                  if option.name.startswith(key_filter))))
             for section, section_doc in sorted(sections.iteritems()))
 
@@ -798,11 +803,11 @@
     Can be given an optional argument which is interpreted as mime-type filter.
     """)
 
-    def expand_macro(self, formatter, name, args):
+    def expand_macro(self, formatter, name, content):
         from trac.mimeview.api import Mimeview
         mime_map = Mimeview(self.env).mime_map
         mime_type_filter = ''
-        args, kw = parse_args(args)
+        args, kw = parse_args(content)
         if args:
             mime_type_filter = args.pop(0).strip().rstrip('*')
 
@@ -865,7 +870,7 @@
            ('TracNotification',             'Notification'),
           ]
 
-    def expand_macro(self, formatter, name, args):
+    def expand_macro(self, formatter, name, content):
         curpage = formatter.resource.id
 
         # scoped TOC (e.g. TranslateRu/TracGuide or 0.11/TracGuide ...)
diff --git a/trac/trac/wiki/model.py b/trac/trac/wiki/model.py
index 3e359d4..fec6382 100644
--- a/trac/trac/wiki/model.py
+++ b/trac/trac/wiki/model.py
@@ -39,7 +39,7 @@
             name = self.resource.id
         else:
             if version:
-                version = int(version) # must be a number or None
+                version = int(version)  # must be a number or None
             self.resource = Resource('wiki', name, version)
         self.name = name
         if name:
@@ -84,7 +84,8 @@
         :since 1.0: the `db` parameter is no longer needed and will be removed
         in version 1.1.1
         """
-        assert self.exists, "Cannot delete non-existent page"
+        if not self.exists:
+            raise TracError(_("Cannot delete non-existent page"))
 
         with self.env.db_transaction as db:
             if version is None:
@@ -186,9 +187,10 @@
     def rename(self, new_name):
         """Rename wiki page in-place, keeping the history intact.
         Renaming a page this way will eventually leave dangling references
-        to the old page - which litterally doesn't exist anymore.
+        to the old page - which literally doesn't exist anymore.
         """
-        assert self.exists, "Cannot rename non-existent page"
+        if not self.exists:
+            raise TracError(_("Cannot rename non-existent page"))
 
         if not validate_page_name(new_name):
             raise TracError(_("Invalid Wiki page name '%(name)s'",
@@ -209,8 +211,8 @@
             Attachment.reparent_all(self.env, 'wiki', old_name, 'wiki',
                                     new_name)
 
-        self.name = new_name
-        self.env.log.info('Renamed page %s to %s', old_name, new_name)
+        self.name = self.resource.id = new_name
+        self.env.log.info("Renamed page %s to %s", old_name, new_name)
 
         for listener in WikiSystem(self.env).change_listeners:
             if hasattr(listener, 'wiki_page_renamed'):
diff --git a/trac/trac/wiki/parser.py b/trac/trac/wiki/parser.py
index f514b89..13efd15 100644
--- a/trac/trac/wiki/parser.py
+++ b/trac/trac/wiki/parser.py
@@ -23,6 +23,7 @@
 from trac.core import *
 from trac.notification import EMAIL_LOOKALIKE_PATTERN
 
+
 class WikiParser(Component):
     """Wiki text parser."""
 
diff --git a/trac/trac/wiki/templates/wiki_delete.html b/trac/trac/wiki/templates/wiki_delete.html
index 5019e56..d52290d 100644
--- a/trac/trac/wiki/templates/wiki_delete.html
+++ b/trac/trac/wiki/templates/wiki_delete.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -95,10 +105,11 @@
           <strong>This is an irreversible operation.</strong>
         </p>
         <div class="buttons">
+          <input type="submit" class="trac-disable-on-submit"
+                 value="${what == 'multiple' and _('Delete those versions')
+                                   or what == 'single' and _('Delete this version')
+                                   or _('Delete page')}" />
           <input type="submit" name="cancel" value="${_('Cancel')}" />
-          <input type="submit" value="${what == 'multiple' and _('Delete those versions')
-                                        or what == 'single' and _('Delete this version')
-                                        or _('Delete page')}" />
         </div>
       </form>
     </div>
diff --git a/trac/trac/wiki/templates/wiki_diff.html b/trac/trac/wiki/templates/wiki_diff.html
index 601e223..00b26e6 100644
--- a/trac/trac/wiki/templates/wiki_diff.html
+++ b/trac/trac/wiki/templates/wiki_diff.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
diff --git a/trac/trac/wiki/templates/wiki_edit.html b/trac/trac/wiki/templates/wiki_edit.html
index a8353a0..0009311 100644
--- a/trac/trac/wiki/templates/wiki_edit.html
+++ b/trac/trac/wiki/templates/wiki_edit.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -39,9 +49,6 @@
         $("#sidebyside").change(function() {
           $("#edit input[type=submit][name=preview]").click();
         });
-        <py:if test="not sidebyside and (action == 'preview' or diff)">
-          $("#info").scrollToTop();
-        </py:if>
         <py:if test="sidebyside">/*<![CDATA[*/
           function editorHeight() {
             return $("#text").closest("fieldset").innerHeight();
@@ -96,16 +103,6 @@
         &darr;
       </div>
       <h1 i18n:msg="name">Editing ${name_of(page.resource)}</h1>
-      <div py:if="merge" class="system-message">
-        <p>Someone else has modified that page since you started your edits.</p><br />
-        <p i18n:msg=""><b>If you save right away, you risk to revert those changes</b>
-        (highlighted below as deletions).</p><br />
-        <p i18n:msg="">Please review all those changes and manually merge them with your
-        own changes. <br />
-        If you're unsure about what you're doing, please press <tt>Cancel</tt>
-        (losing your changes) and start editing the latest version of the page
-        again.</p>
-      </div>
 
       <!--!
 
@@ -131,10 +128,26 @@
       (the  #preview will float at the right of the edit form's textarea)
 
       -->
-      <py:if test="not sidebyside"><xi:include href="wiki_edit_form.html" /></py:if>
       <py:choose test="action">
+        <py:when test="'collision'">
+          <div class="system-message">
+            Sorry, this page has been modified by somebody else since you started
+            editing. Your changes cannot be saved.
+          </div>
+        </py:when>
+        <py:if test="not sidebyside"><xi:include href="wiki_edit_form.html" /></py:if>
+        <div py:if="merge" class="system-message">
+          <p>Someone else has modified that page since you started your edits.</p><br />
+          <p i18n:msg=""><strong>If you save right away, you risk to revert those changes</strong>
+            (highlighted below as deletions).</p><br />
+          <p i18n:msg="">Please review all those changes and manually merge them with your
+            own changes. <br />
+            If you're unsure about what you're doing, please press <tt>Cancel</tt>
+            (losing your changes) and start editing the latest version of the page
+            again.</p>
+        </div>
         <py:when test="'preview'">
-          <table id="info" summary="Revision info">
+          <table id="info" summary="Revision info" class="${'trac-scroll' if not sidebyside else None}">
             <tbody>
               <tr><th scope="col" i18n:msg="version, author">
                 Change information for future version ${page.version+1} (modified by $author):
@@ -158,7 +171,7 @@
                 <xi:include href="diff_div.html" py:with="no_id=True" />
               </div>
               <div py:otherwise="" class="wikipage" xml:space="preserve">
-                ${wiki_to_html(context.child(page.resource), page.text)}
+                ${wiki_to_html(context, page.text)}
               </div>
             </div>
             <div py:if="not sidebyside and page.text" class="trac-nav">
@@ -167,12 +180,6 @@
             </div>
           </div>
         </py:when>
-        <py:when test="'collision'">
-          <div class="system-message">
-            Sorry, this page has been modified by somebody else since you started
-            editing. Your changes cannot be saved.
-          </div>
-        </py:when>
       </py:choose>
       <py:if test="sidebyside"><xi:include href="wiki_edit_form.html" /></py:if>
 
diff --git a/trac/trac/wiki/templates/wiki_edit_form.html b/trac/trac/wiki/templates/wiki_edit_form.html
index 18f8958..35b05e7 100644
--- a/trac/trac/wiki/templates/wiki_edit_form.html
+++ b/trac/trac/wiki/templates/wiki_edit_form.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2009-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -26,12 +36,13 @@
         </label>
         <input type="checkbox" name="sidebyside" id="sidebyside" checked="$sidebyside" />
       </div>
-      <p><textarea id="text" class="wikitext${' trac-resizable' if not sidebyside else None}"
+      <p><textarea id="text" class="${classes('wikitext', 'trac-autofocus', 'trac-fullwidth',
+                                              'trac-resizable' if not sidebyside else None)}"
                    name="text" cols="80" rows="$edit_rows">
 $page.text</textarea>
       </p>
       <div id="help" i18n:msg="">
-        <b>Note:</b> See <a href="${href.wiki('WikiFormatting')}">WikiFormatting</a> and
+        <strong>Note:</strong> See <a href="${href.wiki('WikiFormatting')}">WikiFormatting</a> and
         <a href="${href.wiki('TracWiki')}">TracWiki</a> for help on editing wiki content.
       </div>
     </fieldset>
@@ -43,7 +54,7 @@
             <input id="author" type="text" name="author" size="30" value="$author" />
           </label>
           <p py:if="author == 'anonymous'" class="hint">
-            <i18n:msg>E-mail address and user name can be saved in the <a href="${href.prefs()}">Preferences</a>.</i18n:msg>
+            <i18n:msg>E-mail address and user name can be saved in the <a href="${href.prefs()}" class="trac-target-new">Preferences</a>.</i18n:msg>
           </p>
         </div>
         <div class="field">
@@ -68,7 +79,7 @@
       <py:otherwise>
         <input type="submit" name="preview" value="${_('Preview Page')}" accesskey="p" />&nbsp;
         <input type="submit" name="diff" value="${_('Review Changes')}" accesskey="r" />&nbsp;
-        <input type="submit" id="save" name="save" value="${_('Submit changes')}" />&nbsp;
+        <input type="submit" id="save" name="save" class="trac-disable-on-submit" value="${_('Submit changes')}" />&nbsp;
       </py:otherwise>
       <input type="submit" name="cancel" value="${_('Cancel')}" />
     </div>
diff --git a/trac/trac/wiki/templates/wiki_page_path.html b/trac/trac/wiki/templates/wiki_page_path.html
index e81bd43..86d9082 100644
--- a/trac/trac/wiki/templates/wiki_page_path.html
+++ b/trac/trac/wiki/templates/wiki_page_path.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+-->
 <div xmlns="http://www.w3.org/1999/xhtml"
      xmlns:py="http://genshi.edgewall.org/"
      xmlns:i18n="http://genshi.edgewall.org/i18n"
diff --git a/trac/trac/wiki/templates/wiki_rename.html b/trac/trac/wiki/templates/wiki_rename.html
index 4fce35a..fdfb8a5 100644
--- a/trac/trac/wiki/templates/wiki_rename.html
+++ b/trac/trac/wiki/templates/wiki_rename.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -15,22 +25,22 @@
       <h1 i18n:msg="name">Rename <a href="$current_href">$page.name</a></h1>
       <form id="rename-form" action="$current_href" method="post">
         <p>
-          <input type="hidden" name="action" value="rename"/>
-          <strong>Renaming the page will rename all existing versions of the page in place.</strong><br/>
+          <input type="hidden" name="action" value="rename" />
+          <strong>Renaming the page will rename all existing versions of the page in place.</strong><br />
           The complete history of the page will be moved to the new location.
         </p>
         <div class="field">
-          <label>New name: <input type="text" name="new_name" size="40" value="$new_name"/></label>
+          <label>New name: <input type="text" name="new_name" class="trac-autofocus" size="40" value="$new_name" /></label>
         </div>
         <div class="field">
           <label>
-            <input type="checkbox" id="redirect" name="redirect"/>
+            <input type="checkbox" id="redirect" name="redirect" />
             Leave a redirection page at the old location
           </label>
         </div>
         <div class="buttons">
-          <input type="submit" name="cancel" value="${_('Cancel')}"/>
-          <input type="submit" name="submit" value="${_('Rename page')}"/>
+          <input type="submit" name="submit" class="trac-disable-on-submit" value="${_('Rename page')}" />
+          <input type="submit" name="cancel" value="${_('Cancel')}" />
         </div>
       </form>
     </div>
diff --git a/trac/trac/wiki/templates/wiki_view.html b/trac/trac/wiki/templates/wiki_view.html
index 644bf46..81c51ce 100644
--- a/trac/trac/wiki/templates/wiki_view.html
+++ b/trac/trac/wiki/templates/wiki_view.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2006-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -6,6 +16,7 @@
       xmlns:i18n="http://genshi.edgewall.org/i18n"
       xmlns:xi="http://www.w3.org/2001/XInclude"
       py:with="modify_perm = 'WIKI_MODIFY' in perm(page.resource);
+               create_perm = 'WIKI_CREATE' in perm(page.resource);
                admin_perm = 'WIKI_ADMIN' in perm(page.resource);
                is_not_latest = page.exists and page.version != latest_version">
   <xi:include href="layout.html" />
@@ -78,21 +89,21 @@
 
       <py:with vars="delete_perm = 'WIKI_DELETE' in perm(page.resource);
                      rename_perm = 'WIKI_RENAME' in perm(page.resource)">
-        <py:if test="admin_perm or (not page.readonly and (modify_perm or delete_perm))">
+        <py:if test="admin_perm or (not page.readonly and (modify_perm or create_perm or delete_perm))">
           <div class="buttons">
-            <py:if test="modify_perm">
+            <py:if test="modify_perm or create_perm">
               <form method="get" action="${href.wiki(page.name)}" id="modifypage">
                 <div>
                   <input type="hidden" name="action" value="edit" />
                   <py:choose>
-                    <py:when test="is_not_latest">
+                    <py:when test="is_not_latest and modify_perm">
                       <input type="hidden" name="version" value="${page.version}"/>
                       <input type="submit" value="${_('Revert to this version')}"/>
                     </py:when>
-                    <py:when test="page.exists">
+                    <py:when test="page.exists and modify_perm">
                       <input type="submit" value="${_('Edit this page')}" accesskey="e" />
                     </py:when>
-                    <py:otherwise>
+                    <py:when test="not page.exists and create_perm">
                       <input type="submit" value="${_('Create this page')}" accesskey="e" />
                       <div py:if="templates" id="template">
                         <label for="template">Using the template:</label>
@@ -103,7 +114,7 @@
                                   selected="${t == default_template or None}">$t</option>
                         </select>
                       </div>
-                    </py:otherwise>
+                    </py:when>
                   </py:choose>
                 </div>
               </form>
diff --git a/trac/trac/wiki/tests/__init__.py b/trac/trac/wiki/tests/__init__.py
index 6d6417d..aded545 100644
--- a/trac/trac/wiki/tests/__init__.py
+++ b/trac/trac/wiki/tests/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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 doctest
 import unittest
 
diff --git a/trac/trac/wiki/tests/formatter.py b/trac/trac/wiki/tests/formatter.py
index ef200bb..a6c69c7 100644
--- a/trac/trac/wiki/tests/formatter.py
+++ b/trac/trac/wiki/tests/formatter.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-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 difflib
 import os
 import re
@@ -14,7 +27,7 @@
 
 from datetime import datetime
 
-from trac.core import *
+from trac.core import Component, TracError, implements
 from trac.test import Mock, MockPerm, EnvironmentStub, locale_en
 from trac.util.datefmt import utc
 from trac.util.html import html
@@ -88,6 +101,14 @@
                 ''.join('<dt>%s</dt><dd>%s</dd>' % kv for kv in args.items()) \
                 + content
 
+class ValueErrorWithUtf8Macro(WikiMacroBase):
+    def expand_macro(self, formatter, name, content, args):
+        raise ValueError(content.encode('utf-8'))
+
+class TracErrorWithUnicodeMacro(WikiMacroBase):
+    def expand_macro(self, formatter, name, content, args):
+        raise TracError(unicode(content))
+
 class SampleResolver(Component):
     """A dummy macro returning a div block, used by the unit test."""
 
@@ -126,6 +147,7 @@
         self._teardown = teardown
 
         req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'),
+                   chrome={}, session={},
                    authname='anonymous', perm=MockPerm(), tz=utc, args={},
                    locale=locale_en, lc_time=locale_en)
         if context:
@@ -182,7 +204,7 @@
         v = v.replace('\r', '').replace(u'\u200b', '') # FIXME: keep ZWSP
         v = strip_line_ws(v, leading=False)
         try:
-            self.assertEquals(self.correct, v)
+            self.assertEqual(self.correct, v)
         except AssertionError, e:
             msg = to_unicode(e)
             match = re.match(r"u?'(.*)' != u?'(.*)'", msg)
diff --git a/trac/trac/wiki/tests/functional.py b/trac/trac/wiki/tests/functional.py
index 51608df..00acce5 100755
--- a/trac/trac/wiki/tests/functional.py
+++ b/trac/trac/wiki/tests/functional.py
@@ -1,22 +1,124 @@
-#!/usr/bin/python
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-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/.
+
 from trac.tests.functional import *
 from trac.mimeview.rst import has_docutils
-from trac.util import get_pkginfo
+from trac.util import create_file, get_pkginfo
+
+try:
+    from configobj import ConfigObj
+except ImportError:
+    ConfigObj = None
+
 
 class TestWiki(FunctionalTwillTestCaseSetup):
     def runTest(self):
-        """Create a wiki page and attach a file"""
-        # TODO: this should be split into multiple tests
-        pagename = random_unique_camel()
-        self._tester.create_wiki_page(pagename)
-        self._tester.attach_file_to_wiki(pagename)
+        """Create a wiki page."""
+        self._tester.create_wiki_page()
+
+
+class TestWikiAddAttachment(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Add attachment to a wiki page. Test that the attachment
+        button reads 'Attach file' when no files have been attached, and
+        'Attach another file' when there are existing attachments.
+        Feature added in http://trac.edgewall.org/ticket/10281"""
+        name = self._tester.create_wiki_page()
+        self._tester.go_to_wiki(name)
+        tc.find("Attach file")
+        filename = self._tester.attach_file_to_wiki(name)
+
+        self._tester.go_to_wiki(name)
+        tc.find("Attach another file")
+        tc.find('Attachments <span class="trac-count">\(1\)</span>')
+        tc.find(filename)
+        tc.find('Download all attachments as:\s+<a rel="nofollow" '
+                'href="/zip-attachment/wiki/%s/">.zip</a>' % name)
+
+
+class TestWikiPageManipulator(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        plugin_name = self.__class__.__name__
+        env = self._testenv.get_trac_environment()
+        env.config.set('components', plugin_name + '.*', 'enabled')
+        env.config.save()
+        create_file(os.path.join(env.path, 'plugins', plugin_name + '.py'),
+"""\
+from genshi.builder import tag
+from trac.core import Component, implements
+from trac.util.translation import tag_
+from trac.wiki.api import IWikiPageManipulator
+
+
+class WikiPageManipulator(Component):
+    implements(IWikiPageManipulator)
+
+    def prepare_wiki_page(self, req, page, fields):
+        pass
+
+    def validate_wiki_page(self, req, page):
+        field = 'comment'
+        yield None, tag_("The page contains invalid markup at"
+                         " line %(number)s.", number=tag.strong('10'))
+        yield field, tag_("The field %(field)s cannot be empty.",
+                          field=tag.strong(field))
+""")
+        self._testenv.restart()
+
+        try:
+            self._tester.go_to_front()
+            tc.follow("Wiki")
+            tc.formvalue('modifypage', 'action', 'edit')
+            tc.submit()
+            tc.submit('save', 'edit')
+            tc.url(self._tester.url + '/wiki/WikiStart$')
+            tc.find("Invalid Wiki page: The page contains invalid markup at"
+                    " line <strong>10</strong>.")
+            tc.find("The Wiki page field 'comment' is invalid:"
+                    " The field <strong>comment</strong> cannot be empty.")
+        finally:
+            env.config.set('components', plugin_name + '.*', 'disabled')
+            env.config.save()
+
+
+class TestWikiHistory(FunctionalTwillTestCaseSetup):
+    """Create wiki page and navigate to page history."""
+    def runTest(self):
+        pagename = self._tester.create_wiki_page()
+        self._tester.edit_wiki_page(pagename)
+        tc.follow(r"\bHistory\b")
+        tc.url(self._tester.url + r'/wiki/%s\?action=history' % pagename)
+        version_link = '<td class="version">[ \t\n]*' \
+                       '<a href="/wiki/%(pagename)s\?version=%%(version)s" ' \
+                       'title="View this version">%%(version)s[ \t\n]*</a>' \
+                        % {'pagename': pagename}
+        tc.find(version_link % {'version': 1})
+        tc.find(version_link % {'version': 2})
+        tc.formvalue('history', 'old_version', '1')
+        tc.formvalue('history', 'version', '2')
+        tc.submit()
+        tc.url(r'%s/wiki/%s\?action=diff&version=2&old_version=1'
+               % (self._tester.url, pagename))
+        tc.find(r'<a href="/wiki/%s\?version=1">Version 1</a>' % pagename)
+        tc.find(r'<a href="/wiki/%s\?version=2">Version 2</a>' % pagename)
+        tc.find(r'<a href="/wiki/%(name)s">%(name)s</a>' % {'name': pagename})
 
 
 class TestWikiRename(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for simple wiki rename"""
-        pagename = random_unique_camel()
-        self._tester.create_wiki_page(pagename)
+        pagename = self._tester.create_wiki_page()
         attachment = self._tester.attach_file_to_wiki(pagename)
         base_url = self._tester.url
         page_url = base_url + "/wiki/" + pagename
@@ -60,6 +162,10 @@
         # check redirection page
         tc.url(page_url)
         tc.find("See.*/wiki/" + newpagename)
+        tc.find("The page %s has been renamed to %s."
+                % (pagename, newpagename))
+        tc.find("The page %s has been recreated with a redirect to %s."
+                % (pagename, newpagename))
         # check whether attachment exists on the new page but not on old page
         tc.go(base_url + '/attachment/wiki/' + newpagename + '/' + attachment)
         tc.notfind("Error: Invalid Attachment")
@@ -73,6 +179,8 @@
         tc.formvalue('rename-form', 'redirect', False)
         tc.submit('submit')
         tc.url(base_url + "/wiki/" + newpagename)
+        tc.find("The page %s has been renamed to %s."
+                % (pagename, newpagename))
         # this time, the original page is gone
         tc.go(page_url)
         tc.url(page_url)
@@ -91,8 +199,7 @@
 class ReStructuredTextWikiTest(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Render reStructured text using a wikiprocessor"""
-        pagename = random_unique_camel()
-        self._tester.create_wiki_page(pagename, content="""
+        pagename = self._tester.create_wiki_page(content="""
 {{{
 #!rst
 Hello
@@ -112,8 +219,7 @@
 class ReStructuredTextCodeBlockTest(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Render reStructured code block"""
-        pagename = random_unique_camel()
-        self._tester.create_wiki_page(pagename, content="""
+        pagename = self._tester.create_wiki_page(content="""
 {{{
 #!rst
 .. code-block:: python
@@ -127,6 +233,54 @@
         tc.find('"123"')
 
 
+class RegressionTestTicket8976(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/8976
+        Test fine grained permissions policy on wiki for specific page
+        versions."""
+        name = self._tester.create_wiki_page()
+        self._tester.edit_wiki_page(name)
+        self._tester.edit_wiki_page(name)
+        self._tester.logout()
+        self._tester.login('user')
+        try:
+            self._tester.go_to_wiki(name, 1)
+            tc.notfind(r"\bError: Forbidden\b")
+            self._tester.go_to_wiki(name, 2)
+            tc.notfind(r"\bError: Forbidden\b")
+            self._tester.go_to_wiki(name, 3)
+            tc.notfind(r"\bError: Forbidden\b")
+            self._tester.go_to_wiki(name, 4)
+            tc.find(r"\bTrac Error\b")
+            self._tester.go_to_wiki(name)
+            tc.notfind(r"\bError: Forbidden\b")
+            self._testenv.enable_authz_permpolicy("""
+                [wiki:%(name)s@1]
+                * = !WIKI_VIEW
+                [wiki:%(name)s@2]
+                * = WIKI_VIEW
+                [wiki:%(name)s@3]
+                * = !WIKI_VIEW
+                [wiki:%(name)s]
+                * = WIKI_VIEW
+            """ % {'name': name})
+            self._tester.go_to_wiki(name, 1)
+            tc.find(r"\bError: Forbidden\b")
+            self._tester.go_to_wiki(name, 2)
+            tc.notfind(r"\bError: Forbidden\b")
+            self._tester.go_to_wiki(name, 3)
+            tc.find(r"\bError: Forbidden\b")
+            self._tester.go_to_wiki(name, 4)
+            tc.find(r"\bTrac Error\b")
+            self._tester.go_to_wiki(name)
+            tc.notfind(r"\bError: Forbidden\b")
+            self._tester.edit_wiki_page(name)
+        finally:
+            self._tester.logout()
+            self._tester.login('admin')
+            self._testenv.disable_authz_permpolicy()
+
+
 class RegressionTestTicket10274(FunctionalTwillTestCaseSetup):
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/10274"""
@@ -139,47 +293,142 @@
 
 
 class RegressionTestTicket10850(FunctionalTwillTestCaseSetup):
-
     def runTest(self):
         """Test for regression of http://trac.edgewall.org/ticket/10850"""
-        pagename = random_unique_camel()
-        self._tester.create_wiki_page(pagename)
+        pagename = self._tester.create_wiki_page()
         # colon characters
         attachment = self._tester.attach_file_to_wiki(
-            pagename, tempfilename='2012-09-11_15:36:40-test.tbz2')
+            pagename, filename='2012-09-11_15:36:40-test.tbz2')
         base_url = self._tester.url
         tc.go(base_url + '/attachment/wiki/' + pagename +
               '/2012-09-11_15:36:40-test.tbz2')
         tc.notfind('Error: Invalid Attachment')
         # backslash characters
         attachment = self._tester.attach_file_to_wiki(
-            pagename, tempfilename=r'/tmp/back\slash.txt')
+            pagename, filename=r'/tmp/back\slash.txt')
         base_url = self._tester.url
         tc.go(base_url + '/attachment/wiki/' + pagename + r'/back\slash.txt')
         tc.notfind('Error: Invalid Attachment')
         # Windows full path
         attachment = self._tester.attach_file_to_wiki(
-            pagename, tempfilename=r'z:\tmp\windows:path.txt')
+            pagename, filename=r'z:\tmp\windows:path.txt')
         base_url = self._tester.url
         tc.go(base_url + '/attachment/wiki/' + pagename + r'/windows:path.txt')
         tc.notfind('Error: Invalid Attachment')
         # Windows share folder path
         attachment = self._tester.attach_file_to_wiki(
-            pagename, tempfilename=r'\\server\share\file:name.txt')
+            pagename, filename=r'\\server\share\file:name.txt')
         base_url = self._tester.url
         tc.go(base_url + '/attachment/wiki/' + pagename + r'/file:name.txt')
         tc.notfind('Error: Invalid Attachment')
 
 
+class RegressionTestTicket10957(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/10957"""
+
+        self._tester.go_to_front()
+        try:
+            self._tester.logout()
+
+            # Check that page can't be created without WIKI_CREATE
+            page_name = random_unique_camel()
+            self._tester.go_to_wiki(page_name)
+            tc.find("Trac Error")
+            tc.find("Page %s not found" % page_name)
+            tc.notfind("Create this page")
+            tc.go(self._tester.url + '/wiki/%s?action=edit' % page_name)
+            tc.find("Error: Forbidden")
+            tc.find("WIKI_CREATE privileges are required to perform this "
+                    "operation on %s. You don't have the required permissions."
+                    % page_name)
+
+            # Check that page can be created when user has WIKI_CREATE
+            self._testenv.grant_perm('anonymous', 'WIKI_CREATE')
+            content_v1 = random_sentence()
+            self._tester.create_wiki_page(page_name, content_v1)
+            tc.find(content_v1)
+
+            # Check that page can't be edited without WIKI_MODIFY
+            tc.notfind("Edit this page")
+            tc.notfind("Attach file")
+            tc.go(self._tester.url + '/wiki/%s?action=edit' % page_name)
+            tc.find("Error: Forbidden")
+            tc.find("WIKI_MODIFY privileges are required to perform this "
+                    "operation on %s. You don't have the required permissions."
+                    % page_name)
+
+            # Check that page can be edited when user has WIKI_MODIFY
+            self._testenv.grant_perm('anonymous', 'WIKI_MODIFY')
+            self._tester.go_to_wiki(page_name)
+            tc.find("Edit this page")
+            tc.find("Attach file")
+            content_v2 = random_sentence()
+            self._tester.edit_wiki_page(page_name, content_v2)
+            tc.find(content_v2)
+
+            # Check that page can be reverted to a previous revision
+            tc.go(self._tester.url + '/wiki/%s?version=1' % page_name)
+            tc.find("Revert to this version")
+            tc.formvalue('modifypage', 'action', 'edit')
+            tc.submit()
+            tc.find(content_v1)
+
+            # Check that page can't be reverted without WIKI_MODIFY
+            self._tester.edit_wiki_page(page_name)
+            self._testenv.revoke_perm('anonymous', 'WIKI_MODIFY')
+            tc.go(self._tester.url + '/wiki/%s?version=1' % page_name)
+            tc.notfind("Revert to this version")
+            tc.go(self._tester.url + '/wiki/%s?action=edit&version=1' % page_name)
+            tc.find("WIKI_MODIFY privileges are required to perform this "
+                    "operation on %s. You don't have the required permissions."
+                    % page_name)
+
+        finally:
+            # Restore pre-test state.
+            self._tester.login('admin')
+            self._testenv.revoke_perm('anonymous', 'WIKI_CREATE')
+
+
+class RegressionTestTicket11302(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11302"""
+        pagename = self._tester.create_wiki_page()
+        attachment = self._tester.attach_file_to_wiki(
+            pagename, description="illustrates [./@1#point1]")
+        self._tester.go_to_wiki(pagename + '?action=edit')
+        tc.find(r'illustrates <a class="wiki"'
+                r' href="/wiki/%s\?version=1#point1">@1</a>' % pagename)
+
+
+class RegressionTestTicket11518(FunctionalTwillTestCaseSetup):
+    def runTest(self):
+        """Test for regression of http://trac.edgewall.org/ticket/11518
+        ResourceNotFound should be raised when version is invalid.
+        """
+        tc.go(self._tester.url + '/wiki/WikiStart?version=1abc')
+        tc.find(r"<h1>Trac Error</h1>")
+        tc.find('No version "1abc" for Wiki page "WikiStart')
+        tc.go(self._tester.url + '/wiki/WikiStart?version=')
+        tc.find(r"<h1>Trac Error</h1>")
+        tc.find('No version "" for Wiki page "WikiStart')
+
+
 def functionalSuite(suite=None):
     if not suite:
-        import trac.tests.functional.testcases
-        suite = trac.tests.functional.testcases.functionalSuite()
+        import trac.tests.functional
+        suite = trac.tests.functional.functionalSuite()
     suite.addTest(TestWiki())
+    suite.addTest(TestWikiAddAttachment())
+    suite.addTest(TestWikiPageManipulator())
+    suite.addTest(TestWikiHistory())
     suite.addTest(TestWikiRename())
     suite.addTest(RegressionTestTicket4812())
     suite.addTest(RegressionTestTicket10274())
     suite.addTest(RegressionTestTicket10850())
+    suite.addTest(RegressionTestTicket10957())
+    suite.addTest(RegressionTestTicket11302())
+    suite.addTest(RegressionTestTicket11518())
     if has_docutils:
         import docutils
         if get_pkginfo(docutils):
@@ -189,6 +438,10 @@
             print "SKIP: reST wiki tests (docutils has no setuptools metadata)"
     else:
         print "SKIP: reST wiki tests (no docutils)"
+    if ConfigObj:
+        suite.addTest(RegressionTestTicket8976())
+    else:
+        print "SKIP: RegressionTestTicket8976 (ConfigObj not installed)"
     return suite
 
 
diff --git a/trac/trac/wiki/tests/macros.py b/trac/trac/wiki/tests/macros.py
index 17d0ce2..1c204dc 100644
--- a/trac/trac/wiki/tests/macros.py
+++ b/trac/trac/wiki/tests/macros.py
@@ -1,15 +1,54 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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/.
+
+from StringIO import StringIO
 from datetime import datetime
+import os
+import shutil
+import tempfile
 import unittest
 
-from trac.config import Option
+from trac.config import Option, ListOption, IntOption, BoolOption
 from trac.test import locale_en
 from trac.util.datefmt import format_date, utc
 from trac.wiki.model import WikiPage
 from trac.wiki.tests import formatter
 
+
+def add_pages(tc, names):
+    now = datetime.now(utc)
+    for name in names:
+        w = WikiPage(tc.env)
+        w.name = name
+        w.text = '--'
+        w.save('joe', 'the page ' + name, '::1', now)
+
+
 # == [[Image]]
 
+def image_setup(tc):
+    add_pages(tc, ['page:fr'])
+    from trac.attachment import Attachment
+    tc.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
+    attachment = Attachment(tc.env, 'wiki', 'page:fr')
+    attachment.description = "image in page:fr"
+    attachment.insert('img.png', StringIO(''), 0, 2)
+
+def image_teardown(tc):
+    shutil.rmtree(os.path.join(tc.env.path, 'files'))
+    os.rmdir(tc.env.path)
+    tc.env.reset_db()
+
 # Note: using `« test »` string in the following tests for checking
 #       unicode robustness and whitespace support (first space is
 #       normal ASCII SPACE, second is Unicode NO-BREAK SPACE).
@@ -91,13 +130,39 @@
 ------------------------------
 <a style="padding:0; border:none" href="/wiki/WikiStart"><img src="/browser/%C2%AB%20test%C2%A0%C2%BB?format=raw" alt="/browser/« test »" title="/browser/« test »" /></a>
 ============================== Strip unicode white-spaces and ZWSPs (#10668)
-[[Image(  ​source:« test ».png  ​, nolink)]]
+[[Image(  ​source:« test ».png  ​, nolink, 100%  ​)]]
 ------------------------------
 <p>
-<img src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" alt="source:« test ».png" title="source:« test ».png" />
+<img width="100%" alt="source:« test ».png" title="source:« test ».png" src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" />
 </p>
 ------------------------------
-<img src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" alt="source:« test ».png" title="source:« test ».png" />
+<img width="100%" alt="source:« test ».png" title="source:« test ».png" src="/browser/%C2%AB%20test%C2%A0%C2%BB.png?format=raw" />
+------------------------------
+============================== Attachments on page with ':' characters (#10562)
+[[Image("page:fr":img.png​,nolink)]]
+------------------------------
+<p>
+<img src="/raw-attachment/wiki/page%3Afr/img.png" alt="image in page:fr" title="image in page:fr" />
+</p>
+------------------------------
+<img src="/raw-attachment/wiki/page%3Afr/img.png" alt="image in page:fr" title="image in page:fr" />
+------------------------------
+============================== htdocs: Image, nolink
+[[Image(htdocs:trac_logo.png, nolink)]]
+------------------------------
+<p>
+<img src="/chrome/site/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" />
+</p>
+------------------------------
+<img src="/chrome/site/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" />
+============================== shared: Image, nolink
+[[Image(shared:trac_logo.png, nolink)]]
+------------------------------
+<p>
+<img src="/chrome/shared/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" />
+</p>
+------------------------------
+<img src="/chrome/shared/trac_logo.png" alt="trac_logo.png" title="trac_logo.png" />
 ------------------------------
 """
 
@@ -110,14 +175,6 @@
 
 # == [[TitleIndex]]
 
-def add_pages(tc, names):
-    now = datetime.now(utc)
-    for name in names:
-        w = WikiPage(tc.env)
-        w.name = name
-        w.text = '--'
-        w.save('joe', 'the page ' + name, '::1', now)
-
 def titleindex_teardown(tc):
     tc.env.reset_db()
 
@@ -391,8 +448,36 @@
 </p><div class="tracini">\
 <h3 id="section-42-section"><code>[section-42]</code></h3>\
 <table class="wiki"><tbody>\
-<tr><td><tt>option1</tt></td><td></td><td class="default"><code>value</code></td></tr>\
-<tr><td><tt>option2</tt></td><td>blah</td><td class="default"><code>value</code></td></tr>\
+<tr class="even"><td><tt>option1</tt></td><td></td><td class="default"><code>value</code></td></tr>\
+<tr class="odd"><td><tt>option2</tt></td><td>blah</td><td class="default"><code>value</code></td></tr>\
+</tbody></table>\
+</div><p>
+</p>
+------------------------------
+============================== TracIni, list option with sep=| (#11074)
+[[TracIni(section-list)]]
+------------------------------
+<p>
+</p><div class="tracini">\
+<h3 id="section-list-section"><code>[section-list]</code></h3>\
+<table class="wiki"><tbody>\
+<tr class="even"><td><tt>option1</tt></td><td></td><td class="default"><code>4.2|42|42||0|enabled</code></td></tr>\
+</tbody></table>\
+</div><p>
+</p>
+------------------------------
+============================== TracIni, option with "false" value as default
+[[TracIni(section-def)]]
+------------------------------
+<p>
+</p><div class="tracini">\
+<h3 id="section-def-section"><code>[section-def]</code></h3>\
+<table class="wiki"><tbody>\
+<tr class="even"><td><tt>option1</tt></td><td></td><td class="nodefault">(no default)</td></tr>\
+<tr class="odd"><td><tt>option2</tt></td><td></td><td class="nodefault">(no default)</td></tr>\
+<tr class="even"><td><tt>option3</tt></td><td></td><td class="default"><code>0</code></td></tr>\
+<tr class="odd"><td><tt>option4</tt></td><td></td><td class="default"><code>disabled</code></td></tr>\
+<tr class="even"><td><tt>option5</tt></td><td></td><td class="default"><code></code></td></tr>\
 </tbody></table>\
 </div><p>
 </p>
@@ -404,6 +489,13 @@
     class Foo(object):
         option_a1 = (Option)('section-42', 'option1', 'value', doc='')
         option_a2 = (Option)('section-42', 'option2', 'value', doc='blah')
+        option_l1 = (ListOption)('section-list', 'option1',
+                                 [4.2, '42', 42, None, 0, True], sep='|')
+        option_d1 = (Option)('section-def', 'option1', None)
+        option_d2 = (Option)('section-def', 'option2', '')
+        option_d3 = (IntOption)('section-def', 'option3', 0)
+        option_d4 = (BoolOption)('section-def', 'option4', False)
+        option_d5 = (ListOption)('section-def', 'option5', [])
 
 def tracini_teardown(tc):
     Option.registry = tc._orig_registry
@@ -411,7 +503,9 @@
 
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(formatter.suite(IMAGE_MACRO_TEST_CASES, file=__file__))
+    suite.addTest(formatter.suite(IMAGE_MACRO_TEST_CASES, file=__file__,
+                                  setup=image_setup,
+                                  teardown=image_teardown))
     suite.addTest(formatter.suite(TITLEINDEX1_MACRO_TEST_CASES, file=__file__))
     suite.addTest(formatter.suite(TITLEINDEX2_MACRO_TEST_CASES, file=__file__,
                                   setup=titleindex2_setup,
diff --git a/trac/trac/wiki/tests/model.py b/trac/trac/wiki/tests/model.py
index ab2b19b..2f0462b 100644
--- a/trac/trac/wiki/tests/model.py
+++ b/trac/trac/wiki/tests/model.py
@@ -1,16 +1,28 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-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/.
 
 from __future__ import with_statement
 
 from datetime import datetime
-import os.path
 import shutil
 from StringIO import StringIO
 import tempfile
 import unittest
 
+import trac.tests.compat
 from trac.attachment import Attachment
 from trac.core import *
+from trac.resource import Resource
 from trac.test import EnvironmentStub
 from trac.tests.resource import TestResourceChangeListener
 from trac.util.datefmt import utc, to_utimestamp
@@ -48,8 +60,7 @@
 
     def setUp(self):
         self.env = EnvironmentStub()
-        self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
-        os.mkdir(self.env.path)
+        self.env.path = tempfile.mkdtemp(prefix='trac-tempenv-')
 
     def tearDown(self):
         shutil.rmtree(self.env.path)
@@ -57,14 +68,14 @@
 
     def test_new_page(self):
         page = WikiPage(self.env)
-        self.assertEqual(False, page.exists)
-        self.assertEqual(None, page.name)
+        self.assertFalse(page.exists)
+        self.assertIsNone(page.name)
         self.assertEqual(0, page.version)
         self.assertEqual('', page.text)
         self.assertEqual(0, page.readonly)
         self.assertEqual('', page.author)
         self.assertEqual('', page.comment)
-        self.assertEqual(None, page.time)
+        self.assertIsNone(page.time)
 
     def test_existing_page(self):
         t = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
@@ -74,10 +85,10 @@
              'Testing', 0))
 
         page = WikiPage(self.env, 'TestPage')
-        self.assertEqual(True, page.exists)
+        self.assertTrue(page.exists)
         self.assertEqual('TestPage', page.name)
         self.assertEqual(1, page.version)
-        self.assertEqual(None, page.resource.version)   # FIXME: Intentional?
+        self.assertIsNone(page.resource.version)   # FIXME: Intentional?
         self.assertEqual('Bla bla', page.text)
         self.assertEqual(0, page.readonly)
         self.assertEqual('joe', page.author)
@@ -90,6 +101,11 @@
 
         page = WikiPage(self.env, 'TestPage', 1)
         self.assertEqual(1, page.resource.version)
+        self.assertEqual(1, page.version)
+
+        resource = Resource('wiki', 'TestPage')
+        page = WikiPage(self.env, resource, 1)
+        self.assertEqual(1, page.version)
 
     def test_create_page(self):
         page = WikiPage(self.env)
@@ -98,7 +114,7 @@
         t = datetime(2001, 1, 1, 1, 1, 1, 0, utc)
         page.save('joe', 'Testing', '::1', t)
 
-        self.assertEqual(True, page.exists)
+        self.assertTrue(page.exists)
         self.assertEqual(1, page.version)
         self.assertEqual(1, page.resource.version)
         self.assertEqual(0, page.readonly)
@@ -165,7 +181,7 @@
         page = WikiPage(self.env, 'TestPage')
         page.delete()
 
-        self.assertEqual(False, page.exists)
+        self.assertFalse(page.exists)
 
         self.assertEqual([], self.env.db_query("""
             SELECT version, time, author, ipnr, text, comment, readonly
@@ -184,7 +200,7 @@
         page = WikiPage(self.env, 'TestPage')
         page.delete(version=2)
 
-        self.assertEqual(True, page.exists)
+        self.assertTrue(page.exists)
         self.assertEqual(
             [(1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0)],
             self.env.db_query("""
@@ -203,7 +219,7 @@
         page = WikiPage(self.env, 'TestPage')
         page.delete(version=1)
 
-        self.assertEqual(False, page.exists)
+        self.assertFalse(page.exists)
 
         self.assertEqual([], self.env.db_query("""
             SELECT version, time, author, ipnr, text, comment, readonly
@@ -224,6 +240,7 @@
         page = WikiPage(self.env, 'TestPage')
         page.rename('PageRenamed')
         self.assertEqual('PageRenamed', page.name)
+        self.assertEqual('PageRenamed', page.resource.id)
 
         self.assertEqual([data], self.env.db_query("""
             SELECT version, time, author, ipnr, text, comment, readonly
@@ -236,8 +253,7 @@
         Attachment.delete_all(self.env, 'wiki', 'PageRenamed')
 
         old_page = WikiPage(self.env, 'TestPage')
-        self.assertEqual(False, old_page.exists)
-
+        self.assertFalse(old_page.exists)
 
         self.assertEqual([], self.env.db_query("""
             SELECT version, time, author, ipnr, text, comment, readonly
@@ -267,6 +283,24 @@
             page = WikiPage(self.env, 'TestPage')
             self.assertRaises(TracError, page.rename, name)
 
+    def test_invalid_version(self):
+        data = (1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0)
+        self.env.db_transaction(
+            "INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)",
+            ('TestPage',) + data)
+
+        self.assertRaises(ValueError, WikiPage, self.env,
+                          'TestPage', '1abc')
+
+        resource = Resource('wiki', 'TestPage')
+        self.assertRaises(ValueError, WikiPage, self.env,
+                          resource, '1abc')
+
+        resource = Resource('wiki', 'TestPage', '1abc')
+        page = WikiPage(self.env, resource)
+        self.assertEqual(1, page.version)
+
+
 class WikiResourceChangeListenerTestCase(unittest.TestCase):
     INITIAL_NAME = "Wiki page 1"
     INITIAL_TEXT = "some text"
@@ -332,12 +366,13 @@
         self.wiki_name = resource.name
         self.wiki_text = resource.text
 
+
 def suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(WikiPageTestCase, 'test'))
-    suite.addTest(unittest.makeSuite(
-        WikiResourceChangeListenerTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(WikiPageTestCase))
+    suite.addTest(unittest.makeSuite(WikiResourceChangeListenerTestCase))
     return suite
 
+
 if __name__ == '__main__':
     unittest.main(defaultTest='suite')
diff --git a/trac/trac/wiki/tests/wiki-tests.txt b/trac/trac/wiki/tests/wiki-tests.txt
index 3339193..faa73e1 100644
--- a/trac/trac/wiki/tests/wiki-tests.txt
+++ b/trac/trac/wiki/tests/wiki-tests.txt
@@ -316,6 +316,13 @@
 nolink:&#34;&lt;blink&gt;&#34;
 </p>
 ------------------------------
+============================== Bracketed links
+See <http://en.wikipedia.org/wiki/Mornington_Crescent_(game)>
+------------------------------
+<p>
+See &lt;<a class="ext-link" href="http://en.wikipedia.org/wiki/Mornington_Crescent_(game)"><span class="icon"></span>http://en.wikipedia.org/wiki/Mornington_Crescent_(game)</a>&gt;
+</p>
+------------------------------
 ============================================================
 
         Other Links
@@ -1177,6 +1184,35 @@
 </p>
 ------------------------------
 Inline  comment
+============================== Exception with ascii bytes
+[[ValueErrorWithUtf8(error)]]
+------------------------------
+<p>
+<div class="system-message"><strong>Error: Macro ValueErrorWithUtf8(error) failed</strong><pre>error</pre></div>
+</p>
+------------------------------
+============================== Exception with utf-8 bytes
+{{{#!ValueErrorWithUtf8
+Érrör
+}}}
+[[ValueErrorWithUtf8(érrör)]]
+------------------------------
+<div class="system-message"><strong>Error: Processor ValueErrorWithUtf8 failed</strong><pre>Érrör
+</pre></div><p>
+<div class="system-message"><strong>Error: Macro ValueErrorWithUtf8(érrör) failed</strong><pre>érrör</pre></div>
+</p>
+------------------------------
+============================== TracError with unicode
+{{{#!TracErrorWithUnicode
+Érrör
+}}}
+[[TracErrorWithUnicode(érrör)]]
+------------------------------
+<div class="system-message"><strong>Error: Processor TracErrorWithUnicode failed</strong><pre>Érrör
+</pre></div><p>
+<div class="system-message"><strong>Error: Macro TracErrorWithUnicode(érrör) failed</strong><pre>érrör</pre></div>
+</p>
+------------------------------
 ============================================================
 
            Headings
@@ -2573,3 +2609,96 @@
 ------------------------------
  […]
 &gt; c
+============================== List immediately followed by binary inline markup, #11009
+***
+* list
+***
+------------------------------
+<p>
+<strong>*
+</strong></p>
+<ul><li>list
+</li></ul><p>
+<strong>*
+</strong></p>
+------------------------------
+============================== List immediately followed by binary inline markup 1, #11373
+ 1. normal in list
+''italic
+in paragraph
+
+ 1. normal in list
+//italic
+in paragraph
+------------------------------
+<ol><li>normal in list
+</li></ol><p>
+<em>italic
+in paragraph
+</em></p>
+<ol><li>normal in list
+</li></ol><p>
+<em>italic
+in paragraph
+</em></p>
+------------------------------
+============================== List immediately followed by binary inline markup 2, #11373
+ 1. //italic in list
+''italic
+in paragraph
+
+ 1. ''italic in list
+//italic
+in paragraph
+
+1. ''italic in list
+''italic
+in paragraph
+
+1. //italic in list
+//italic
+in paragraph
+------------------------------
+<ol><li><em>italic in list
+</em></li></ol><p>
+<em>italic
+in paragraph
+</em></p>
+<ol><li><em>italic in list
+</em></li></ol><p>
+<em>italic
+in paragraph
+</em></p>
+<ol><li><em>italic in list
+</em></li></ol><p>
+<em>italic
+in paragraph
+</em></p>
+<ol><li><em>italic in list
+</em></li></ol><p>
+<em>italic
+in paragraph
+</em></p>
+------------------------------
+============================== List immediately followed by binary inline markup 3, #11373
+ 1. ''italic in list
+'''''bolditalic
+in paragraph'''''
+------------------------------
+<ol><li><em>italic in list
+</em></li></ol><p>
+<strong><em>bolditalic
+in paragraph</em></strong>
+</p>
+------------------------------
+============================== List immediately followed by binary inline markup 4, #11373
+ 1. '''bold in list
+'''''bolditalic
+in paragraph'''''
+------------------------------
+<ol><li><strong>bold in list
+</strong></li></ol><p>
+<strong><em>bolditalic
+in paragraph</em></strong>
+</p>
+------------------------------
diff --git a/trac/trac/wiki/tests/wikisyntax.py b/trac/trac/wiki/tests/wikisyntax.py
index 6d594fc..b8e5a56 100644
--- a/trac/trac/wiki/tests/wikisyntax.py
+++ b/trac/trac/wiki/tests/wikisyntax.py
@@ -1,4 +1,15 @@
 # -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-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/.
 
 from datetime import datetime
 import unittest
@@ -7,6 +18,7 @@
 from trac.wiki.model import WikiPage
 from trac.wiki.tests import formatter
 
+
 TEST_CASES = u"""
 ============================== wiki: link resolver
 wiki:TestPage
@@ -711,6 +723,8 @@
     w.text = '--'
     w.save('joe', 'other third level of hierarchy', '::1', now)
 
+    tc.env.db_transaction("INSERT INTO ticket (id) VALUES ('123')")
+
 
 def wiki_teardown(tc):
     tc.env.reset_db()
diff --git a/trac/trac/wiki/web_ui.py b/trac/trac/wiki/web_ui.py
index f12ad7a..94e8bfc 100644
--- a/trac/trac/wiki/web_ui.py
+++ b/trac/trac/wiki/web_ui.py
@@ -37,10 +37,10 @@
 from trac.util.translation import _, tag_
 from trac.versioncontrol.diff import get_diff_options, diff_blocks
 from trac.web.api import IRequestHandler
-from trac.web.chrome import (Chrome, INavigationContributor, ITemplateProvider,
-                             add_ctxtnav, add_link, add_notice, add_script,
-                             add_stylesheet, add_warning, prevnext_nav,
-                             web_context)
+from trac.web.chrome import (Chrome, INavigationContributor,
+                             ITemplateProvider, add_ctxtnav, add_link,
+                             add_notice, add_script, add_stylesheet,
+                             add_warning, prevnext_nav, web_context)
 from trac.wiki.api import IWikiPageManipulator, WikiSystem, validate_page_name
 from trac.wiki.formatter import format_to, OneLinerFormatter
 from trac.wiki.model import WikiPage
@@ -55,14 +55,14 @@
 
 class WikiModule(Component):
 
-    implements(IContentConverter, INavigationContributor, IPermissionRequestor,
-               IRequestHandler, ITimelineEventProvider, ISearchSource,
-               ITemplateProvider)
+    implements(IContentConverter, INavigationContributor,
+               IPermissionRequestor, IRequestHandler, ITimelineEventProvider,
+               ISearchSource, ITemplateProvider)
 
     page_manipulators = ExtensionPoint(IWikiPageManipulator)
 
     max_size = IntOption('wiki', 'max_size', 262144,
-        """Maximum allowed wiki page size in bytes. (''since 0.11.2'')""")
+        """Maximum allowed wiki page size in characters. (''since 0.11.2'')""")
 
     PAGE_TEMPLATES_PREFIX = 'PageTemplates/'
     DEFAULT_PAGE_TEMPLATE = 'DefaultPage'
@@ -70,11 +70,11 @@
     # IContentConverter methods
 
     def get_supported_conversions(self):
-        yield ('txt', _('Plain Text'), 'txt', 'text/x-trac-wiki', 'text/plain',
-               9)
+        yield ('txt', _("Plain Text"), 'txt', 'text/x-trac-wiki',
+               'text/plain', 9)
 
     def convert_content(self, req, mimetype, content, key):
-        return (content, 'text/plain;charset=utf-8')
+        return content, 'text/plain;charset=utf-8'
 
     # INavigationContributor methods
 
@@ -82,11 +82,12 @@
         return 'wiki'
 
     def get_navigation_items(self, req):
-        if 'WIKI_VIEW' in req.perm('wiki'):
+        if 'WIKI_VIEW' in req.perm('wiki', 'WikiStart'):
             yield ('mainnav', 'wiki',
-                   tag.a(_('Wiki'), href=req.href.wiki(), accesskey=1))
+                   tag.a(_("Wiki"), href=req.href.wiki(), accesskey=1))
+        if 'WIKI_VIEW' in req.perm('wiki', 'TracGuide'):
             yield ('metanav', 'help',
-                   tag.a(_('Help/Guide'), href=req.href.wiki('TracGuide'),
+                   tag.a(_("Help/Guide"), href=req.href.wiki('TracGuide'),
                          accesskey=6))
 
     # IPermissionRequestor methods
@@ -119,10 +120,17 @@
             raise TracError(_("Invalid Wiki page name '%(name)s'",
                               name=pagename))
 
+        if version is not None:
+            try:
+                version = int(version)
+            except (ValueError, TypeError):
+                raise ResourceNotFound(
+                    _('No version "%(num)s" for Wiki page "%(name)s"',
+                      num=version, name=pagename))
+
         page = WikiPage(self.env, pagename)
         versioned_page = WikiPage(self.env, pagename, version=version)
 
-        req.perm(page.resource).require('WIKI_VIEW')
         req.perm(versioned_page.resource).require('WIKI_VIEW')
 
         if version and versioned_page.version != int(version):
@@ -147,7 +155,8 @@
                 if action == 'edit' and not has_collision and valid:
                     return self._do_save(req, versioned_page)
                 else:
-                    return self._render_editor(req, page, action, has_collision)
+                    return self._render_editor(req, page, action,
+                                               has_collision)
             elif action == 'delete':
                 self._do_delete(req, versioned_page)
             elif action == 'rename':
@@ -192,8 +201,8 @@
 
         # Validate page size
         if len(req.args.get('text', '')) > self.max_size:
-            add_warning(req, _('The wiki page is too long (must be less '
-                               'than %(num)s characters)',
+            add_warning(req, _("The wiki page is too long (must be less "
+                               "than %(num)s characters)",
                                num=self.max_size))
             valid = False
 
@@ -202,12 +211,12 @@
             for field, message in manipulator.validate_wiki_page(req, page):
                 valid = False
                 if field:
-                    add_warning(req, _("The Wiki page field '%(field)s' is "
-                                       "invalid: %(message)s",
-                                       field=field, message=message))
+                    add_warning(req, tag_("The Wiki page field '%(field)s'"
+                                          " is invalid: %(message)s",
+                                          field=field, message=message))
                 else:
-                    add_warning(req, _("Invalid Wiki page: %(message)s",
-                                       message=message))
+                    add_warning(req, tag_("Invalid Wiki page: %(message)s",
+                                          message=message))
         return valid
 
     def _page_data(self, req, page, action=''):
@@ -233,9 +242,10 @@
         def version_info(v, last=0):
             return {'path': get_resource_name(self.env, page.resource),
                     # TRANSLATOR: wiki page
-                    'rev': v or _('currently edited'),
+                    'rev': v or _("currently edited"),
                     'shortrev': v or last + 1,
-                    'href': req.href.wiki(page.name, version=v) if v else None}
+                    'href': req.href.wiki(page.name, version=v)
+                            if v else None}
         changes = [{'diffs': diffs, 'props': [],
                     'new': version_info(new_version, old_version),
                     'old': version_info(old_version)}]
@@ -271,12 +281,12 @@
             req.redirect(req.href.wiki())
         else:
             if version and old_version and version > old_version + 1:
-                add_notice(req, _('The versions %(from_)d to %(to)d of the '
-                                  'page %(name)s have been deleted.',
-                            from_=old_version + 1, to=version, name=page.name))
+                add_notice(req, _("The versions %(from_)d to %(to)d of the "
+                                  "page %(name)s have been deleted.",
+                           from_=old_version + 1, to=version, name=page.name))
             else:
-                add_notice(req, _('The version %(version)d of the page '
-                                  '%(name)s has been deleted.',
+                add_notice(req, _("The version %(version)d of the page "
+                                  "%(name)s has been deleted.",
                                   version=version, name=page.name))
             req.redirect(req.href.wiki(page.name))
 
@@ -319,6 +329,14 @@
                           new_name, old_version, old_name, new_name)
                 redirection.save(author, comment, req.remote_addr)
 
+        add_notice(req, _("The page %(old_name)s has been renamed to "
+                          "%(new_name)s.", old_name=old_name,
+                          new_name=new_name))
+        if redirect:
+            add_notice(req, _("The page %(old_name)s has been recreated "
+                              "with a redirect to %(new_name)s.",
+                              old_name=old_name, new_name=new_name))
+
         req.redirect(req.href.wiki(old_name if redirect else new_name))
 
     def _do_save(self, req, page):
@@ -394,8 +412,8 @@
 
     def _render_diff(self, req, page):
         if not page.exists:
-            raise TracError(_('Version %(num)s of page "%(name)s" does not '
-                              'exist',
+            raise TracError(_("Version %(num)s of page \"%(name)s\" does not "
+                              "exist",
                               num=req.args.get('version'), name=page.name))
 
         old_version = req.args.get('old_version')
@@ -446,13 +464,13 @@
         if prev_version:
             add_link(req, 'prev', req.href.wiki(page.name, action='diff',
                                                 version=prev_version),
-                     _('Version %(num)s', num=prev_version))
+                     _("Version %(num)s", num=prev_version))
         add_link(req, 'up', req.href.wiki(page.name, action='history'),
                  _('Page history'))
         if next_version:
             add_link(req, 'next', req.href.wiki(page.name, action='diff',
                                                 version=next_version),
-                     _('Version %(num)s', num=next_version))
+                     _("Version %(num)s", num=next_version))
 
         data = self._page_data(req, page, 'diff')
         data.update({
@@ -465,8 +483,8 @@
             'changes': changes,
             'diff': diff_data,
         })
-        prevnext_nav(req, _('Previous Change'), _('Next Change'),
-                     _('Wiki History'))
+        prevnext_nav(req, _("Previous Change"), _("Next Change"),
+                     _("Wiki History"))
         return 'wiki_diff.html', data, None
 
     def _render_editor(self, req, page, action='edit', has_collision=False):
@@ -479,6 +497,8 @@
 
         if page.readonly:
             req.perm(page.resource).require('WIKI_ADMIN')
+        elif not page.exists:
+            req.perm(page.resource).require('WIKI_CREATE')
         else:
             req.perm(page.resource).require('WIKI_MODIFY')
         original_text = page.text
@@ -489,7 +509,7 @@
             template = self.PAGE_TEMPLATES_PREFIX + req.args.get('template')
             template_page = WikiPage(self.env, template)
             if template_page and template_page.exists and \
-                   'WIKI_VIEW' in req.perm(template_page.resource):
+                    'WIKI_VIEW' in req.perm(template_page.resource):
                 page.text = template_page.text
         elif 'version' in req.args:
             old_page = WikiPage(self.env, page.name,
@@ -528,9 +548,11 @@
         data = self._page_data(req, page, action)
         context = web_context(req, page.resource)
         data.update({
+            'context': context,
             'author': author,
             'comment': comment,
-            'edit_rows': editrows, 'sidebyside': sidebyside,
+            'edit_rows': editrows,
+            'sidebyside': sidebyside,
             'scroll_bar_pos': req.args.get('scroll_bar_pos', ''),
             'diff': None,
             'attachments': AttachmentModule(self.env).attachment_data(context),
@@ -574,7 +596,7 @@
             })
         data.update({'history': history, 'resource': page.resource})
         add_ctxtnav(req, _("Back to %(wikipage)s", wikipage=page.name),
-                           req.href.wiki(page.name))
+                    req.href.wiki(page.name))
         return 'history_view.html', data, None
 
     def _render_view(self, req, page):
@@ -582,13 +604,10 @@
 
         # Add registered converters
         if page.exists:
-            for conversion in Mimeview(self.env).get_supported_conversions(
-                                                 'text/x-trac-wiki'):
+            for conversion in Mimeview(self.env) \
+                              .get_supported_conversions('text/x-trac-wiki'):
                 conversion_href = req.href.wiki(page.name, version=version,
                                                 format=conversion[0])
-                # or...
-                conversion_href = get_resource_url(self.env, page.resource,
-                                                req.href, format=conversion[0])
                 add_link(req, 'alternate', conversion_href, conversion[1],
                          conversion[3])
 
@@ -601,7 +620,7 @@
         higher, related = [], []
         if not page.exists:
             if 'WIKI_CREATE' not in req.perm(page.resource):
-                raise ResourceNotFound(_('Page %(name)s not found',
+                raise ResourceNotFound(_("Page %(name)s not found",
                                          name=page.name))
             formatter = OneLinerFormatter(self.env, context)
             if '/' in page.name:
@@ -610,7 +629,7 @@
                     name = '/'.join(parts[:i] + [parts[-1]])
                     if not ws.has_page(name):
                         higher.append(ws._format_link(formatter, 'wiki',
-                                                    '/' + name, name, False))
+                                                      '/' + name, name, False))
             else:
                 name = page.name
             name = name.lower()
@@ -623,7 +642,6 @@
                        for each in related]
 
         latest_page = WikiPage(self.env, page.name, version=None)
-        req.perm(latest_page.resource).require('WIKI_VIEW')
 
         prev_version = next_version = None
         if version:
@@ -650,12 +668,12 @@
         if prev_version:
             add_link(req, 'prev',
                      req.href.wiki(page.name, version=prev_version),
-                     _('Version %(num)s', num=prev_version))
+                     _("Version %(num)s", num=prev_version))
 
         parent = None
         if version:
             add_link(req, 'up', req.href.wiki(page.name, version=None),
-                     _('View latest version'))
+                     _("View latest version"))
         elif '/' in page.name:
             parent = page.name[:page.name.rindex('/')]
             add_link(req, 'up', req.href.wiki(parent, version=None),
@@ -668,8 +686,8 @@
 
         # Add ctxtnav entries
         if version:
-            prevnext_nav(req, _('Previous Version'), _('Next Version'),
-                         _('View Latest Version'))
+            prevnext_nav(req, _("Previous Version"), _("Next Version"),
+                         _("View Latest Version"))
         else:
             if parent:
                 add_ctxtnav(req, _('Up'), req.href.wiki(parent))
@@ -697,10 +715,10 @@
 
     def _wiki_ctxtnav(self, req, page):
         """Add the normal wiki ctxtnav entries."""
-        add_ctxtnav(req, _('Start Page'), req.href.wiki('WikiStart'))
-        add_ctxtnav(req, _('Index'), req.href.wiki('TitleIndex'))
+        add_ctxtnav(req, _("Start Page"), req.href.wiki('WikiStart'))
+        add_ctxtnav(req, _("Index"), req.href.wiki('TitleIndex'))
         if page.exists:
-            add_ctxtnav(req, _('History'), req.href.wiki(page.name,
+            add_ctxtnav(req, _("History"), req.href.wiki(page.name,
                                                          action='history'))
 
     # ITimelineEventProvider methods
@@ -724,7 +742,7 @@
 
             # Attachments
             for event in AttachmentModule(self.env).get_timeline_events(
-                req, wiki_realm, start, stop):
+                    req, wiki_realm, start, stop):
                 yield event
 
     def render_timeline_event(self, context, field, event):
@@ -734,9 +752,9 @@
         elif field == 'title':
             name = tag.em(get_resource_name(self.env, wiki_page))
             if wiki_page.version > 1:
-                return tag_('%(page)s edited', page=name)
+                return tag_("%(page)s edited", page=name)
             else:
-                return tag_('%(page)s created', page=name)
+                return tag_("%(page)s created", page=name)
         elif field == 'description':
             markup = format_to(self.env, None,
                                context.child(resource=wiki_page), comment)
@@ -744,7 +762,7 @@
                 diff_href = context.href.wiki(
                     wiki_page.id, version=wiki_page.version, action='diff')
                 markup = tag(markup,
-                             ' (', tag.a(_('diff'), href=diff_href), ')')
+                             " (", tag.a(_("diff"), href=diff_href), ")")
             return markup
 
     # ISearchSource methods
@@ -775,5 +793,5 @@
 
         # Attachments
         for result in AttachmentModule(self.env).get_search_results(
-            req, wiki_realm, terms):
+                req, wiki_realm, terms):
             yield result
diff --git a/trac/tracopt/mimeview/enscript.py b/trac/tracopt/mimeview/enscript.py
index 205e73f..e258cf6 100644
--- a/trac/tracopt/mimeview/enscript.py
+++ b/trac/tracopt/mimeview/enscript.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2004-2009 Edgewall Software
+# Copyright (C) 2004-2013 Edgewall Software
 # Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
diff --git a/trac/tracopt/mimeview/php.py b/trac/tracopt/mimeview/php.py
index 3e5e5fd..91995a7 100644
--- a/trac/tracopt/mimeview/php.py
+++ b/trac/tracopt/mimeview/php.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005 Christian Boos <cboos@bct-technology.com>
 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
diff --git a/trac/tracopt/mimeview/silvercity.py b/trac/tracopt/mimeview/silvercity.py
index 74b62d7..a8e3794 100644
--- a/trac/tracopt/mimeview/silvercity.py
+++ b/trac/tracopt/mimeview/silvercity.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2004-2009 Edgewall Software
+# Copyright (C) 2004-2013 Edgewall Software
 # Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
 # All rights reserved.
 #
diff --git a/trac/tracopt/mimeview/tests/__init__.py b/trac/tracopt/mimeview/tests/__init__.py
index 63070ff..cc7c89f 100644
--- a/trac/tracopt/mimeview/tests/__init__.py
+++ b/trac/tracopt/mimeview/tests/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2009 Edgewall Software
+# Copyright (C) 2009-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
diff --git a/trac/tracopt/mimeview/tests/php.py b/trac/tracopt/mimeview/tests/php.py
index ff6cb38..70eb6b0 100644
--- a/trac/tracopt/mimeview/tests/php.py
+++ b/trac/tracopt/mimeview/tests/php.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2006-2009 Edgewall Software
+# Copyright (C) 2006-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -140,8 +140,8 @@
     suite = unittest.TestSuite()
     php = locate("php")
     if php:
-        suite.addTest(unittest.makeSuite(PhpDeuglifierTestCase, 'test'))
-        suite.addTest(unittest.makeSuite(PhpRendererTestCase, 'test'))
+        suite.addTest(unittest.makeSuite(PhpDeuglifierTestCase))
+        suite.addTest(unittest.makeSuite(PhpRendererTestCase))
     else:
         print("SKIP: tracopt/mimeview/tests/php.py (php cli binary, 'php', "
               "not found)")
diff --git a/trac/tracopt/perm/authz_policy.py b/trac/tracopt/perm/authz_policy.py
index bd33096..45135f4 100644
--- a/trac/tracopt/perm/authz_policy.py
+++ b/trac/tracopt/perm/authz_policy.py
@@ -14,17 +14,19 @@
 #
 # Author: Alec Thomas <alec@swapoff.org>
 
-from fnmatch import fnmatch
+from fnmatch import fnmatchcase
 from itertools import groupby
 import os
 
 from trac.core import *
-from trac.config import Option
+from trac.config import ConfigurationError, Option
 from trac.perm import PermissionSystem, IPermissionPolicy
+from trac.util import lazy
+from trac.util.text import to_unicode
 
 ConfigObj = None
 try:
-    from configobj import ConfigObj
+    from configobj import ConfigObj, ConfigObjError
 except ImportError:
     pass
 
@@ -138,12 +140,8 @@
     # 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:
+        if 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)
@@ -166,19 +164,42 @@
 
     # Internal methods
 
+    @lazy
     def get_authz_file(self):
-        f = self.authz_file
-        return f if os.path.isabs(f) else os.path.join(self.env.path, f)
+        if not self.authz_file:
+            self.log.error('The `[authz_policy] authz_file` configuration '
+                           'option in trac.ini is empty or not defined.')
+            raise ConfigurationError()
+
+        authz_file = self.authz_file if os.path.isabs(self.authz_file) \
+                                     else os.path.join(self.env.path,
+                                                       self.authz_file)
+        try:
+            os.stat(authz_file)
+        except OSError, e:
+            self.log.error("Error parsing authz permission policy file: %s",
+                           to_unicode(e))
+            raise ConfigurationError()
+        return authz_file
 
     def parse_authz(self):
+        if ConfigObj is None:
+            self.log.error('ConfigObj package not found.')
+            raise ConfigurationError()
         self.log.debug('Parsing authz security policy %s',
-                       self.get_authz_file())
-        self.authz = ConfigObj(self.get_authz_file(), encoding='utf8')
+                       self.get_authz_file)
+        try:
+            self.authz = ConfigObj(self.get_authz_file, encoding='utf8',
+                                   raise_errors=True)
+        except ConfigObjError, e:
+            self.log.error("Error parsing authz permission policy file: %s",
+                           to_unicode(e))
+            raise ConfigurationError()
         groups = {}
         for group, users in self.authz.get('groups', {}).iteritems():
             if isinstance(users, basestring):
                 users = [users]
-            groups[group] = users
+            groups[group] = map(to_unicode, users)
 
         self.groups_by_user = {}
 
@@ -192,31 +213,32 @@
         for group, users in groups.iteritems():
             add_items('@' + group, users)
 
-        self.authz_mtime = os.path.getmtime(self.get_authz_file())
+        self.authz_mtime = os.path.getmtime(self.get_authz_file)
 
     def normalise_resource(self, resource):
+        def to_descriptor(resource):
+            id = resource.id
+            return '%s:%s@%s' % (resource.realm or '*',
+                                 id if id is not None else '*',
+                                 resource.version or '*')
+
         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 '*')]
+            descriptor = to_descriptor(resource)
+            if not resource.realm and resource.id is None:
+                return [descriptor]
             # 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)):
+            while parent and resource.realm == parent.realm:
                 parent = parent.parent
             if parent:
-                parent = flatten(parent)
+                return flatten(parent) + [descriptor]
             else:
-                parent = []
-            return parent + ['%s:%s@%s' % (resource.realm or '*',
-                                           resource.id or '*',
-                                           resource.version or '*')]
+                return [descriptor]
+
         return '/'.join(flatten(resource))
 
     def authz_permissions(self, resource_key, username):
@@ -227,14 +249,15 @@
         else:
             valid_users = ['*', 'anonymous']
         for resource_section in [a for a in self.authz.sections
-                                 if a != 'groups']:
-            resource_glob = resource_section
+                                   if a != 'groups']:
+            resource_glob = to_unicode(resource_section)
             if '@' not in resource_glob:
                 resource_glob += '@*'
 
-            if fnmatch(resource_key, resource_glob):
+            if fnmatchcase(resource_key, resource_glob):
                 section = self.authz[resource_section]
                 for who, permissions in section.iteritems():
+                    who = to_unicode(who)
                     if who in valid_users or \
                             who in self.groups_by_user.get(username, []):
                         self.log.debug('%s matched section %s for user %s',
diff --git a/trac/tracopt/perm/config_perm_provider.py b/trac/tracopt/perm/config_perm_provider.py
index 2dfec53..5983dc2 100644
--- a/trac/tracopt/perm/config_perm_provider.py
+++ b/trac/tracopt/perm/config_perm_provider.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2009 Edgewall Software
+# Copyright (C) 2009-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -17,7 +17,10 @@
 
 
 class ExtraPermissionsProvider(Component):
-    """Extra permission provider."""
+    """Define arbitrary permissions.
+
+    Documentation can be found on the [wiki:TracIni#extra-permissions-section]
+    page after enabling the component."""
 
     implements(IPermissionRequestor)
 
@@ -31,17 +34,21 @@
         and a comma-separated list of permissions. For example:
         {{{
         [extra-permissions]
-        extra_admin = extra_view, extra_modify, extra_delete
+        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.
 
+        The permissions are created in upper-case characters regardless of
+        the casing of the definitions in `trac.ini`. For example, the
+        definition `extra_view` would create the permission `EXTRA_VIEW`.
+
         If you don't want a meta-permission, start the meta-name with an
         underscore (`_`):
         {{{
         [extra-permissions]
-        _perms = extra_view, extra_modify
+        _perms = EXTRA_VIEW, EXTRA_MODIFY
         }}}
         """)
 
diff --git a/trac/tracopt/perm/tests/__init__.py b/trac/tracopt/perm/tests/__init__.py
index 7dba11b..6aaeac4 100644
--- a/trac/tracopt/perm/tests/__init__.py
+++ b/trac/tracopt/perm/tests/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2012 Edgewall Software
+# Copyright (C) 2012-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
diff --git a/trac/tracopt/perm/tests/authz_policy.py b/trac/tracopt/perm/tests/authz_policy.py
index 1672b70..12e3b6b 100644
--- a/trac/tracopt/perm/tests/authz_policy.py
+++ b/trac/tracopt/perm/tests/authz_policy.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2012 Edgewall Software
+# Copyright (C) 2012-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -19,9 +19,13 @@
 except ImportError:
     ConfigObj = None
 
+import trac.tests.compat
+from trac.config import ConfigurationError
+from trac.perm import PermissionCache
 from trac.resource import Resource
-from trac.test import EnvironmentStub
+from trac.test import EnvironmentStub, Mock
 from trac.util import create_file
+from trac.versioncontrol.api import Repository
 from tracopt.perm.authz_policy import AuthzPolicy
 
 
@@ -45,8 +49,33 @@
 änon =
 @administrators = WIKI_VIEW
 * =
+
+# Tickets
+[ticket:43]
+änon = TICKET_VIEW
+@administrators =
+* =
+
+[ticket:*]
+änon =
+@administrators = TICKET_VIEW
+* =
+
+# Default repository
+[repository:@*]
+änon =
+@administrators = BROWSER_VIEW, FILE_VIEW
+* =
+
+# Non-default repository
+[repository:bláh@*]
+änon = BROWSER_VIEW, FILE_VIEW
+@administrators = BROWSER_VIEW, FILE_VIEW
+* =
 """)
-        self.env = EnvironmentStub(enable=[AuthzPolicy])
+        self.env = EnvironmentStub(enable=['trac.*', AuthzPolicy], path=tmpdir)
+        self.env.config.set('trac', 'permission_policies',
+                            'AuthzPolicy, DefaultPermissionPolicy')
         self.env.config.set('authz_policy', 'authz_file', self.authz_file)
         self.authz_policy = AuthzPolicy(self.env)
 
@@ -57,32 +86,168 @@
     def check_permission(self, action, user, resource, perm):
         return self.authz_policy.check_permission(action, user, resource, perm)
 
+    def get_repository(self, reponame):
+        params = {'id': 1, 'name': reponame}
+        return Mock(Repository, 'mock', params, self.env.log)
+
+    def get_perm(self, username, *args):
+        perm = PermissionCache(self.env, username)
+        if args:
+            return perm(*args)
+        return perm
+
     def test_unicode_username(self):
         resource = Resource('wiki', 'WikiStart')
+
+        perm = self.get_perm('anonymous')
         self.assertEqual(
             False,
-            self.check_permission('WIKI_VIEW', 'anonymous', resource, None))
+            self.check_permission('WIKI_VIEW', 'anonymous', resource, perm))
+        self.assertNotIn('WIKI_VIEW', perm)
+        self.assertNotIn('WIKI_VIEW', perm(resource))
+
+        perm = self.get_perm(u'änon')
         self.assertEqual(
             True,
-            self.check_permission('WIKI_VIEW', u'änon', resource, None))
+            self.check_permission('WIKI_VIEW', u'änon', resource, perm))
+        self.assertNotIn('WIKI_VIEW', perm)
+        self.assertIn('WIKI_VIEW', perm(resource))
 
     def test_unicode_resource_name(self):
         resource = Resource('wiki', u'résumé')
+
+        perm = self.get_perm('anonymous')
         self.assertEqual(
             False,
-            self.check_permission('WIKI_VIEW', 'anonymous', resource, None))
+            self.check_permission('WIKI_VIEW', 'anonymous', resource, perm))
+        self.assertNotIn('WIKI_VIEW', perm)
+        self.assertNotIn('WIKI_VIEW', perm(resource))
+
+        perm = self.get_perm(u'änon')
         self.assertEqual(
             False,
-            self.check_permission('WIKI_VIEW', u'änon', resource, None))
+            self.check_permission('WIKI_VIEW', u'änon', resource, perm))
+        self.assertNotIn('WIKI_VIEW', perm)
+        self.assertNotIn('WIKI_VIEW', perm(resource))
+
+        perm = self.get_perm(u'éat')
         self.assertEqual(
             True,
-            self.check_permission('WIKI_VIEW', u'éat', resource, None))
+            self.check_permission('WIKI_VIEW', u'éat', resource, perm))
+        self.assertNotIn('WIKI_VIEW', perm)
+        self.assertIn('WIKI_VIEW', perm(resource))
+
+    def test_resource_without_id(self):
+        perm = self.get_perm('anonymous')
+        self.assertNotIn('TICKET_VIEW', perm)
+        self.assertNotIn('TICKET_VIEW', perm('ticket'))
+        self.assertNotIn('TICKET_VIEW', perm('ticket', 42))
+        self.assertNotIn('TICKET_VIEW', perm('ticket', 43))
+
+        perm = self.get_perm(u'änon')
+        self.assertNotIn('TICKET_VIEW', perm)
+        self.assertNotIn('TICKET_VIEW', perm('ticket'))
+        self.assertNotIn('TICKET_VIEW', perm('ticket', 42))
+        self.assertIn('TICKET_VIEW', perm('ticket', 43))
+
+        perm = self.get_perm(u'éat')
+        self.assertNotIn('TICKET_VIEW', perm)
+        self.assertIn('TICKET_VIEW', perm('ticket'))
+        self.assertIn('TICKET_VIEW', perm('ticket', 42))
+        self.assertNotIn('TICKET_VIEW', perm('ticket', 43))
+
+    def test_default_repository(self):
+        repos = self.get_repository('')
+        self.assertEqual(False, repos.is_viewable(self.get_perm('anonymous')))
+        self.assertEqual(False, repos.is_viewable(self.get_perm(u'änon')))
+        self.assertEqual(True, repos.is_viewable(self.get_perm(u'éat')))
+
+    def test_non_default_repository(self):
+        repos = self.get_repository(u'bláh')
+        self.assertEqual(False, repos.is_viewable(self.get_perm('anonymous')))
+        self.assertEqual(True, repos.is_viewable(self.get_perm(u'änon')))
+        self.assertEqual(True, repos.is_viewable(self.get_perm(u'éat')))
+
+    def test_case_sensitive_resource(self):
+        resource = Resource('WIKI', 'wikistart')
+        self.assertEqual(
+            None,
+            self.check_permission('WIKI_VIEW', 'anonymous', resource, None))
+        self.assertEqual(
+            None,
+            self.check_permission('WIKI_VIEW', u'änon', resource, None))
+
+    def test_get_authz_file(self):
+        """get_authz_file should resolve a relative path and lazily compute.
+        """
+        authz_file = self.authz_policy.get_authz_file
+        self.assertEqual(os.path.join(self.env.path, 'trac-authz-policy'),
+                         authz_file)
+        self.assertIs(authz_file, self.authz_policy.get_authz_file)
+
+    def test_get_authz_file_notfound_raises(self):
+        """ConfigurationError exception should be raised if file not found."""
+        authz_file = os.path.join(self.env.path, 'some-nonexistent-file')
+        self.env.config.set('authz_policy', 'authz_file', authz_file)
+        self.assertRaises(ConfigurationError, getattr, self.authz_policy,
+                          'get_authz_file')
+
+    def test_get_authz_file_notdefined_raises(self):
+        """ConfigurationError exception should be raised if the option
+        `[authz_policy] authz_file` is not specified in trac.ini."""
+        self.env.config.remove('authz_policy', 'authz_file')
+        self.assertRaises(ConfigurationError, getattr, self.authz_policy,
+                          'get_authz_file')
+
+    def test_get_authz_file_empty_raises(self):
+        """ConfigurationError exception should be raised if the option
+        `[authz_policy] authz_file` is empty."""
+        self.env.config.set('authz_policy', 'authz_file', '')
+        self.assertRaises(ConfigurationError, getattr, self.authz_policy,
+                          'get_authz_file')
+
+    def test_parse_authz_empty(self):
+        """Allow the file to be empty."""
+        create_file(self.authz_file, '')
+        self.authz_policy.parse_authz()
+        self.assertFalse(self.authz_policy.authz)
+
+    def test_parse_authz_no_settings(self):
+        """Allow the file to have no settings."""
+        create_file(self.authz_file, """\
+# [wiki:WikiStart]
+# änon = WIKI_VIEW
+# * =
+""")
+        self.authz_policy.parse_authz()
+        self.assertFalse(self.authz_policy.authz)
+
+    def test_parse_authz_malformed_raises(self):
+        """ConfigurationError should be raised if the file is malformed."""
+        create_file(self.authz_file, """\
+wiki:WikiStart]
+änon = WIKI_VIEW
+* =
+""")
+        self.assertRaises(ConfigurationError, self.authz_policy.parse_authz)
+
+    def test_parse_authz_duplicated_sections_raises(self):
+        """ConfigurationError should be raised if the file has duplicate
+        sections."""
+        create_file(self.authz_file, """\
+[wiki:WikiStart]
+änon = WIKI_VIEW
+
+[wiki:WikiStart]
+änon = WIKI_VIEW
+""")
+        self.assertRaises(ConfigurationError, self.authz_policy.parse_authz)
 
 
 def suite():
     suite = unittest.TestSuite()
     if ConfigObj:
-        suite.addTest(unittest.makeSuite(AuthzPolicyTestCase, 'test'))
+        suite.addTest(unittest.makeSuite(AuthzPolicyTestCase))
     else:
         print "SKIP: tracopt/perm/tests/authz_policy.py (no configobj " + \
               "installed)"
diff --git a/trac/tracopt/ticket/commit_updater.py b/trac/tracopt/ticket/commit_updater.py
index aad5cfc..aa4a9dc 100644
--- a/trac/tracopt/ticket/commit_updater.py
+++ b/trac/tracopt/ticket/commit_updater.py
@@ -50,7 +50,7 @@
 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.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
@@ -182,10 +182,11 @@
 
     def _parse_message(self, message):
         """Parse the commit message and return the ticket references."""
-        cmd_groups = self.command_re.findall(message)
+        cmd_groups = self.command_re.finditer(message)
         functions = self._get_functions()
         tickets = {}
-        for cmd, tkts in cmd_groups:
+        for m in cmd_groups:
+            cmd, tkts = m.group('action', 'ticket')
             func = functions.get(cmd.lower())
             if not func and self.commands_refs.strip() == '<ALL>':
                 func = self.cmd_refs
@@ -208,7 +209,8 @@
 
     def _update_tickets(self, tickets, changeset, comment, date):
         """Update the tickets with the given comment."""
-        perm = PermissionCache(self.env, changeset.author)
+        authname = self._authname(changeset)
+        perm = PermissionCache(self.env, authname)
         for tkt_id, cmds in tickets.iteritems():
             try:
                 self.log.debug("Updating ticket #%d", tkt_id)
@@ -220,7 +222,7 @@
                         if cmd(ticket, changeset, ticket_perm) is not False:
                             save = True
                     if save:
-                        ticket.save_changes(changeset.author, comment, date)
+                        ticket.save_changes(authname, comment, date)
                 if save:
                     self._notify(ticket, date)
             except Exception, e:
@@ -250,23 +252,29 @@
                 functions[cmd] = func
         return functions
 
+    def _authname(self, changeset):
+        return changeset.author.lower() \
+               if self.env.config.getbool('trac', 'ignore_auth_case') \
+               else changeset.author
+
     # Command-specific behavior
     # The ticket isn't updated if all extracted commands return False.
 
     def cmd_close(self, ticket, changeset, perm):
+        authname = self._authname(changeset)
         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)
+                          authname, ticket.id)
             return False
         ticket['status'] = 'closed'
         ticket['resolution'] = 'fixed'
         if not ticket['owner']:
-            ticket['owner'] = changeset.author
+            ticket['owner'] = authname
 
     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)
+                          self._authname(changeset), ticket.id)
             return False
 
 
@@ -302,8 +310,8 @@
             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')
+                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),
diff --git a/trac/tracopt/ticket/templates/ticket_delete.html b/trac/tracopt/ticket/templates/ticket_delete.html
index f8fbdca..1daef33 100644
--- a/trac/tracopt/ticket/templates/ticket_delete.html
+++ b/trac/tracopt/ticket/templates/ticket_delete.html
@@ -1,3 +1,13 @@
+<!--!  Copyright (C) 2010-2014 Edgewall Software
+
+  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/.
+-->
 <!DOCTYPE html
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
@@ -38,8 +48,8 @@
             </p>
           </div>
           <div class="buttons">
+            <input type="submit" class="trac-disable-on-submit" value="${_('Delete ticket')}"/>
             <input type="submit" name="cancel" value="${_('Cancel')}"/>
-            <input type="submit" value="${_('Delete ticket')}"/>
           </div>
         </form>
       </py:when>
@@ -62,8 +72,8 @@
                This is an irreversible operation.</p>
           </div>
           <div class="buttons">
+            <input type="submit" class="trac-disable-on-submit" value="${_('Delete comment')}"/>
             <input type="submit" name="cancel" value="${_('Cancel')}"/>
-            <input type="submit" value="${_('Delete comment')}"/>
           </div>
         </form>
       </py:otherwise>
diff --git a/trac/tracopt/ticket/tests/__init__.py b/trac/tracopt/ticket/tests/__init__.py
new file mode 100644
index 0000000..1434075
--- /dev/null
+++ b/trac/tracopt/ticket/tests/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 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 unittest
+
+from tracopt.ticket.tests import commit_updater
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(commit_updater.suite())
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/tracopt/ticket/tests/commit_updater.py b/trac/tracopt/ticket/tests/commit_updater.py
new file mode 100644
index 0000000..3bfe479
--- /dev/null
+++ b/trac/tracopt/ticket/tests/commit_updater.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 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 unittest
+from datetime import datetime
+
+from trac.test import EnvironmentStub, Mock
+from trac.tests.contentgen import random_sentence
+from trac.ticket.model import Ticket
+from trac.util.datefmt import utc
+from trac.versioncontrol.api import Repository
+from tracopt.ticket.commit_updater import CommitTicketUpdater
+
+
+class CommitTicketUpdaterTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(enable=['trac.*',
+                                           'tracopt.ticket.commit_updater.*'])
+        self.env.config.set('ticket', 'commit_ticket_update_check_perms', False)
+        self.repos = Mock(Repository, 'repos1', {'name': 'repos1', 'id': 1},
+                          self.env.log)
+        self.updater = CommitTicketUpdater(self.env)
+
+    def tearDown(self):
+        self.env.reset_db()
+
+    def _make_tickets(self, num):
+        self.tickets = []
+        for i in xrange(0, num):
+            ticket = Ticket(self.env)
+            ticket['reporter'] = 'someone'
+            ticket['summary'] = random_sentence()
+            ticket.insert()
+            self.tickets.append(ticket)
+
+    def test_changeset_added(self):
+        self._make_tickets(1)
+        message = 'This is the first comment. Refs #1.'
+        chgset = Mock(repos=self.repos, rev=1, message=message, author='joe',
+                      date=datetime(2001, 1, 1, 1, 1, 1, 0, utc))
+        self.updater.changeset_added(self.repos, chgset)
+        self.assertEqual("""\
+In [changeset:"1/repos1"]:
+{{{
+#!CommitTicketReference repository="repos1" revision="1"
+This is the first comment. Refs #1.
+}}}""", self.tickets[0].get_change(cnum=1)['fields']['comment']['new'])
+
+    def test_changeset_modified(self):
+        self._make_tickets(2)
+        message = 'This is the first comment. Refs #1.'
+        old_chgset = Mock(repos=self.repos, rev=1,
+                          message=message, author='joe',
+                          date=datetime(2001, 1, 1, 1, 1, 1, 0, utc))
+        message = 'This is the first comment after an edit. Refs #1, #2.'
+        new_chgset = Mock(repos=self.repos, rev=1,
+                          message=message, author='joe',
+                          date=datetime(2001, 1, 2, 1, 1, 1, 0, utc))
+        self.updater.changeset_added(self.repos, old_chgset)
+        self.updater.changeset_modified(self.repos, new_chgset, old_chgset)
+        self.assertEqual("""\
+In [changeset:"1/repos1"]:
+{{{
+#!CommitTicketReference repository="repos1" revision="1"
+This is the first comment. Refs #1.
+}}}""", self.tickets[0].get_change(cnum=1)['fields']['comment']['new'])
+        self.assertEqual("""\
+In [changeset:"1/repos1"]:
+{{{
+#!CommitTicketReference repository="repos1" revision="1"
+This is the first comment after an edit. Refs #1, #2.
+}}}""", self.tickets[1].get_change(cnum=1)['fields']['comment']['new'])
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(CommitTicketUpdaterTestCase))
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/tracopt/versioncontrol/git/PyGIT.py b/trac/tracopt/versioncontrol/git/PyGIT.py
index 2ce272c..144c975 100644
--- a/trac/tracopt/versioncontrol/git/PyGIT.py
+++ b/trac/tracopt/versioncontrol/git/PyGIT.py
@@ -28,36 +28,12 @@
 import time
 import weakref
 
+from trac.util import terminate
+from trac.util.text import to_unicode
 
 __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
 
@@ -95,12 +71,29 @@
     return '\n'.join(lines), props
 
 
+_unquote_re = re.compile(r'\\(?:[abtnvfr"\\]|[0-7]{3})')
+_unquote_chars = {'a': '\a', 'b': '\b', 't': '\t', 'n': '\n', 'v': '\v',
+                  'f': '\f', 'r': '\r', '"': '"', '\\': '\\'}
+
+
+def _unquote(path):
+    if path.startswith('"') and path.endswith('"'):
+        def replace(match):
+            s = match.group(0)[1:]
+            if len(s) == 3:
+                return chr(int(s, 8))  # \ooo
+            return _unquote_chars[s]
+        path = _unquote_re.sub(replace, path[1:-1])
+    return path
+
+
 class GitCore(object):
     """Low-level wrapper around git executable"""
 
-    def __init__(self, git_dir=None, git_bin='git'):
+    def __init__(self, git_dir=None, git_bin='git', log=None):
         self.__git_bin = git_bin
         self.__git_dir = git_dir
+        self.__log = log
 
     def __repr__(self):
         return '<GitCore bin="%s" dir="%s">' % (self.__git_bin,
@@ -132,7 +125,10 @@
         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
+        if self.__log and (p.returncode != 0 or stderr_data):
+            self.__log.debug('%s exits with %d, dir: %r, args: %s %r, '
+                             'stderr: %r', self.__git_bin, p.returncode,
+                             self.__git_dir, git_cmd, cmd_args, stderr_data)
 
         return stdout_data
 
@@ -201,21 +197,23 @@
         self.logger = log
 
         with StorageFactory.__dict_lock:
+            if weak:
+                # remove additional reference which is created
+                # with non-weak argument
+                try:
+                    del StorageFactory.__dict_nonweak[repo]
+                except KeyError:
+                    pass
+
             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
+            # create additional reference depending on 'weak' argument
+            if not weak:
+                StorageFactory.__dict_nonweak[repo] = i
 
         self.__inst = i
         self.__repo = repo
@@ -227,13 +225,19 @@
                              self.__repo))
         return self.__inst
 
+    @classmethod
+    def _clean(cls):
+        """For testing purpose only"""
+        with StorageFactory.__dict_lock:
+            cls.__dict.clear()
+            cls.__dict_nonweak.clear()
+
 
 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)
@@ -383,18 +387,29 @@
 
         # 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..."
+        control_files = ['HEAD', 'objects', 'refs']
+        control_files_exist = \
+            lambda p: all(map(os.path.exists, map(p, control_files)))
+        if not control_files_exist(__git_file_path):
+            __git_file_path = partial(os.path.join, git_dir, '.git')
+            if os.path.exists(__git_file_path()) and \
+                    control_files_exist(__git_file_path):
+                git_dir = __git_file_path()
+            else:
+                self.logger.error("GIT control files missing in '%s'"
                                   % git_dir)
-            raise GitError("GIT control files not found, maybe wrong "
-                           "directory?")
+                raise GitError("GIT control files not found, maybe wrong "
+                               "directory?")
+        # at least, check that the HEAD file is readable
+        head_file = os.path.join(git_dir, 'HEAD')
+        try:
+            with open(head_file, 'rb') as f:
+                pass
+        except IOError, e:
+            raise GitError("Make sure the Git repository '%s' is readable: %s"
+                           % (git_dir, to_unicode(e)))
 
-        self.repo = GitCore(git_dir, git_bin=git_bin)
+        self.repo = GitCore(git_dir, git_bin=git_bin, log=log)
 
         self.logger.debug("PyGIT.Storage instance %d constructed" % id(self))
 
@@ -410,25 +425,29 @@
     #
 
     # called by Storage.sync()
-    def __rev_cache_sync(self, youngest_rev=None):
+    def __rev_cache_sync(self):
         """invalidates revision db cache if necessary"""
 
+        branches = self._get_branches()
+
         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:
+            if not self.__rev_cache:
                 need_update = True # almost NOOP
+            elif branches != self.__rev_cache.branch_dict:
+                self.logger.debug('invalidated caches for %d cause repository '
+                                  'has been changed', id(self))
+                need_update = True
 
             if need_update:
                 self.__rev_cache = None
-
             return need_update
 
+    def invalidate_rev_cache(self):
+        with self.__rev_cache_lock:
+            self.__rev_cache = None
+            self.logger.debug('invalidated caches for %d', id(self))
+
     def get_rev_cache(self):
         """Retrieve revision cache
 
@@ -463,7 +482,7 @@
                                 for k, v in self._get_branches()]
                 head_revs = set(v for _, v in new_branches)
 
-                rev = ord_rev = 0
+                rev = ord_rev = None
                 for ord_rev, revs in enumerate(
                                         self.repo.rev_list('--parents',
                                                            '--topo-order',
@@ -565,8 +584,12 @@
         """
 
         result = []
-        for e in self.repo.branch('-v', '--no-abbrev').splitlines():
-            bname, bsha = e[1:].strip().split()[:2]
+        for e in self.repo.branch('-v', '--no-abbrev').rstrip('\n') \
+                                                      .split('\n'):
+            tokens = e[1:].strip().split()[:2]
+            if len(tokens) != 2:
+                continue
+            bname, bsha = tokens
             if e.startswith('*'):
                 result.insert(0, (bname, bsha))
             else:
@@ -640,8 +663,8 @@
     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'
+                self.repo.config('--get', 'i18n.commitEncoding').strip() or \
+                'utf-8'
 
         return self.commit_encoding
 
@@ -812,7 +835,7 @@
             raise GitErrorSha
 
         with self.__commit_msg_lock:
-            if self.__commit_msg_cache.has_key(commit_id):
+            if commit_id in self.__commit_msg_cache:
                 # cache hit
                 result = self.__commit_msg_cache[commit_id]
                 return result[0], dict(result[1])
@@ -882,15 +905,14 @@
         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)
+        return self.__rev_cache_sync()
 
     @contextmanager
     def get_historian(self, sha, base_path):
         p = []
         change = {}
         next_path = []
+        base_path = self._fs_from_unicode(base_path)
 
         def name_status_gen():
             p[:] = [self.repo.log_pipe('--pretty=format:%n%H',
@@ -904,6 +926,8 @@
                     if l == '\n':
                         break
                     _, path = l.rstrip('\n').split('\t', 1)
+                    # git-log without -z option quotes each pathname
+                    path = _unquote(path)
                     while path not in change:
                         change[path] = old_sha
                         if next_path == [path]:
@@ -921,6 +945,7 @@
         gen = name_status_gen()
 
         def historian(path):
+            path = self._fs_from_unicode(path)
             try:
                 return change[path]
             except KeyError:
@@ -936,34 +961,32 @@
     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
+        tmp = self.history(sha, path, limit=1)
+        return tmp[0] if tmp else 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() ]
+        args = ['--max-count=%d' % limit, str(sha)]
+        if path:
+            args.extend(('--', self._fs_from_unicode(path)))
+        tmp = self.repo.rev_list(*args)
+        return [rev.strip() for rev in tmp.splitlines()]
 
     def history_timerange(self, start, stop):
+        # retrieve start <= committer-time < stop,
+        # see CachedRepository.get_changesets()
         return [ rev.strip() for rev in \
-                     self.repo.rev_list('--reverse',
+                     self.repo.rev_list('--date-order',
                                         '--max-age=%d' % start,
-                                        '--min-age=%d' % stop,
+                                        '--min-age=%d' % (stop - 1),
                                         '--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))
 
diff --git a/trac/tracopt/versioncontrol/git/git_fs.py b/trac/tracopt/versioncontrol/git/git_fs.py
index 3802d7f..4c49c15 100644
--- a/trac/tracopt/versioncontrol/git/git_fs.py
+++ b/trac/tracopt/versioncontrol/git/git_fs.py
@@ -15,20 +15,25 @@
 from __future__ import with_statement
 
 from datetime import datetime
+import itertools
 import os
 import sys
 
 from genshi.builder import tag
+from genshi.core import Markup
 
+from trac.cache import cached
 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.util.text import to_unicode, exception_to_unicode
+from trac.util.translation import _
 from trac.versioncontrol.api import Changeset, Node, Repository, \
                                     IRepositoryConnector, NoSuchChangeset, \
                                     NoSuchNode, IRepositoryProvider
-from trac.versioncontrol.cache import CachedRepository, CachedChangeset
+from trac.versioncontrol.cache import CACHE_YOUNGEST_REV, CachedRepository, \
+                                      CachedChangeset
 from trac.versioncontrol.web_ui import IPropertyRenderer
 from trac.web.chrome import Chrome
 from trac.wiki import IWikiSyntaxProvider
@@ -37,10 +42,7 @@
 
 
 class GitCachedRepository(CachedRepository):
-    """Git-specific cached repository.
-
-    Passes through {display,short,normalize}_rev
-    """
+    """Git-specific cached repository."""
 
     def display_rev(self, rev):
         return self.short_rev(rev)
@@ -50,15 +52,113 @@
 
     def normalize_rev(self, rev):
         if not rev:
-            return self.repos.get_youngest_rev()
+            return self.get_youngest_rev()
         normrev = self.repos.git.verifyrev(rev)
         if normrev is None:
             raise NoSuchChangeset(rev)
         return normrev
 
+    def get_youngest_rev(self):
+        # return None if repository is empty
+        return CachedRepository.get_youngest_rev(self) or None
+
+    def child_revs(self, rev):
+        return self.repos.child_revs(rev)
+
+    def get_changesets(self, start, stop):
+        for key, csets in itertools.groupby(
+                CachedRepository.get_changesets(self, start, stop),
+                key=lambda cset: cset.date):
+            csets = list(csets)
+            if len(csets) == 1:
+                yield csets[0]
+                continue
+            rev_csets = dict((cset.rev, cset) for cset in csets)
+            while rev_csets:
+                revs = [rev for rev in rev_csets
+                            if not any(r in rev_csets
+                                       for r in self.repos.child_revs(rev))]
+                for rev in sorted(revs):
+                    yield rev_csets.pop(rev)
+
     def get_changeset(self, rev):
         return GitCachedChangeset(self, self.normalize_rev(rev), self.env)
 
+    def sync(self, feedback=None, clean=False):
+        if clean:
+            self.remove_cache()
+
+        metadata = self.metadata
+        self.save_metadata(metadata)
+        meta_youngest = metadata.get(CACHE_YOUNGEST_REV)
+        repos = self.repos
+
+        def is_synced(rev):
+            for count, in self.env.db_query("""
+                    SELECT COUNT(*) FROM revision WHERE repos=%s AND rev=%s
+                    """, (self.id, rev)):
+                return count > 0
+            return False
+
+        def traverse(rev, seen, revs=None):
+            if revs is None:
+                revs = []
+            while True:
+                if rev in seen:
+                    return revs
+                seen.add(rev)
+                if is_synced(rev):
+                    return revs
+                revs.append(rev)
+                parent_revs = repos.parent_revs(rev)
+                if not parent_revs:
+                    return revs
+                if len(parent_revs) == 1:
+                    rev = parent_revs[0]
+                    continue
+                idx = len(revs)
+                traverse(parent_revs.pop(), seen, revs)
+                for parent in parent_revs:
+                    revs[idx:idx] = traverse(parent, seen)
+
+        while True:
+            repos.sync()
+            repos_youngest = repos.youngest_rev
+            updated = False
+            seen = set()
+
+            for rev in repos.git.all_revs():
+                if repos.child_revs(rev):
+                    continue
+                revs = traverse(rev, seen)  # topology ordered
+                while revs:
+                    # sync revision from older revision to newer revision
+                    rev = revs.pop()
+                    self.log.info("Trying to sync revision [%s]", rev)
+                    cset = repos.get_changeset(rev)
+                    with self.env.db_transaction as db:
+                        try:
+                            self._insert_changeset(db, rev, cset)
+                            updated = True
+                        except self.env.db_exc.IntegrityError, e:
+                            self.log.info('Revision %s already cached: %r',
+                                          rev, e)
+                            db.rollback()
+                            continue
+                    if feedback:
+                        feedback(rev)
+
+            if updated:
+                continue  # sync again
+
+            if meta_youngest != repos_youngest:
+                with self.env.db_transaction as db:
+                    db("""
+                        UPDATE repository SET value=%s WHERE id=%s AND name=%s
+                        """, (repos_youngest, self.id, CACHE_YOUNGEST_REV))
+                    del self.metadata
+            return
+
 
 class GitCachedChangeset(CachedChangeset):
     """Git-specific cached changeset.
@@ -250,7 +350,7 @@
             def rlookup_uid(_):
                 return None
 
-        repos = GitRepository(dir, params, self.log,
+        repos = GitRepository(self.env, dir, params, self.log,
                               persistent_cache=self.persistent_cache,
                               git_bin=self.git_bin,
                               git_fs_encoding=self.git_fs_encoding,
@@ -320,9 +420,9 @@
                 parent_links = intersperse(', ', \
                     ((sha_link(rev),
                       ' (',
-                      tag.a('diff',
-                            title="Diff against this parent (show the " \
-                                  "changes merged from the other parents)",
+                      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)),
                       ')')
@@ -330,15 +430,16 @@
 
                 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."),
+                           tag.span(Markup(_("Note: this is a <strong>merge"
+                                             "</strong> 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."),
+                           tag.span(Markup(_("Use the <tt>(diff)</tt> links "
+                                             "above to see all the changes "
+                                             "relative to each parent.")),
                                     class_='hint'))
 
             # simple non-merge commit
@@ -357,7 +458,7 @@
 class GitRepository(Repository):
     """Git repository"""
 
-    def __init__(self, path, params, log,
+    def __init__(self, env, path, params, log,
                  persistent_cache=False,
                  git_bin='git',
                  git_fs_encoding='utf-8',
@@ -367,27 +468,43 @@
                  use_committer_time=False,
                  ):
 
+        self.env = env
         self.logger = log
         self.gitrepo = path
         self.params = params
+        self.persistent_cache = persistent_cache
         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()
+            factory = PyGIT.StorageFactory(path, log, not persistent_cache,
+                                           git_bin=git_bin,
+                                           git_fs_encoding=git_fs_encoding)
+            self._git = factory.getInstance()
         except PyGIT.GitError, e:
+            log.error(exception_to_unicode(e))
             raise TracError("%s does not appear to be a Git "
                             "repository." % path)
 
-        Repository.__init__(self, 'git:'+path, self.params, log)
+        Repository.__init__(self, 'git:' + path, self.params, log)
+        self._cached_git_id = str(self.id)
 
     def close(self):
-        self.git = None
+        self._git = None
+
+    @property
+    def git(self):
+        if self.persistent_cache:
+            return self._cached_git
+        else:
+            return self._git
+
+    @cached('_cached_git_id')
+    def _cached_git(self):
+        self._git.invalidate_rev_cache()
+        return self._git
 
     def get_youngest_rev(self):
         return self.git.youngest_rev()
@@ -434,6 +551,9 @@
         """GitChangeset factory method"""
         return GitChangeset(self, rev)
 
+    def get_changeset_uid(self, rev):
+        return self.normalize_rev(rev)
+
     def get_changes(self, old_path, old_rev, new_path, new_rev,
                     ignore_ancestry=0):
         # TODO: handle renames/copies, ignore_ancestry
@@ -477,8 +597,8 @@
         return self.git.children(rev)
 
     def rev_older_than(self, rev1, rev2):
-        rc = self.git.rev_is_anchestor_of(rev1, rev2)
-        return rc
+        return self.git.rev_is_anchestor_of(self.normalize_rev(rev1),
+                                            self.normalize_rev(rev2))
 
     # def clear(self, youngest_rev=None):
     #     self.youngest = None
@@ -493,6 +613,8 @@
         if rev_callback:
             revs = set(self.git.all_revs())
 
+        if self.persistent_cache:
+            del self._cached_git  # invalidate persistent cache
         if not self.git.sync():
             return None # nothing expected to change
 
@@ -511,11 +633,16 @@
         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'
+        if rev:
+            rev = repos.normalize_rev(to_unicode(rev))
+        else:
+            rev = repos.youngest_rev
 
         kind = Node.DIRECTORY
         p = path.strip('/')
-        if p: # ie. not the root-tree
+        if p:  # ie. not the root-tree
+            if not rev:
+                raise NoSuchNode(path, rev)
             if not ls_tree_info:
                 ls_tree_info = repos.git.ls_tree(rev, p) or None
                 if ls_tree_info:
@@ -574,6 +701,8 @@
                 self.repos.git.blame(self.rev,self.__git_path())]
 
     def get_entries(self):
+        if not self.rev:  # if empty repository
+            return
         if not self.isdir:
             return
 
@@ -599,6 +728,8 @@
         return self.fs_size
 
     def get_history(self, limit=None):
+        if not self.rev:  # if empty repository
+            return
         # 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)):
diff --git a/trac/tracopt/versioncontrol/git/tests/PyGIT.py b/trac/tracopt/versioncontrol/git/tests/PyGIT.py
index fe78712..40506e1 100644
--- a/trac/tracopt/versioncontrol/git/tests/PyGIT.py
+++ b/trac/tracopt/versioncontrol/git/tests/PyGIT.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2012 Edgewall Software
+# Copyright (C) 2012-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -11,26 +11,38 @@
 # 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 shutil
 import tempfile
 import unittest
+from datetime import datetime
 from subprocess import Popen, PIPE
 
+import trac.tests.compat
 from trac.test import locate, EnvironmentStub
+from trac.tests.compat import rmtree
 from trac.util import create_file
 from trac.util.compat import close_fds
-from tracopt.versioncontrol.git.PyGIT import GitCore, Storage, parse_commit
+from trac.versioncontrol.api import Changeset, DbRepositoryProvider, \
+                                    RepositoryManager
+from tracopt.versioncontrol.git.git_fs import GitConnector
+from tracopt.versioncontrol.git.PyGIT import GitCore, GitError, Storage, \
+                                             StorageFactory, parse_commit
+from tracopt.versioncontrol.git.tests.git_fs import GitCommandMixin
+
+
+git_bin = None
 
 
 class GitTestCase(unittest.TestCase):
 
     def test_is_sha(self):
-        self.assertTrue(not GitCore.is_sha('123'))
+        self.assertFalse(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))
+        self.assertFalse(GitCore.is_sha('x' + 'f' * 39))
+        self.assertFalse(GitCore.is_sha('f' * 41))
 
     def test_git_version(self):
         v = Storage.git_version()
@@ -91,17 +103,17 @@
         msg, props = parse_commit(self.commit2240a7b)
         self.assertTrue(msg)
         self.assertTrue(props)
-        self.assertEquals(
+        self.assertEqual(
             ['30aaca4582eac20a52ac7b2ec35bdb908133e5b1',
              '5a0dc7365c240795bf190766eba7a27600be3b3e'],
             props['parent'])
-        self.assertEquals(
+        self.assertEqual(
             ['Linus Torvalds <torvalds@linux-foundation.org> 1323915958 -0800'],
             props['author'])
-        self.assertEquals(props['author'], props['committer'])
+        self.assertEqual(props['author'], props['committer'])
 
         # Merge tag
-        self.assertEquals(['''\
+        self.assertEqual(['''\
 object 5a0dc7365c240795bf190766eba7a27600be3b3e
 type commit
 tag tytso-for-linus-20111214A
@@ -127,7 +139,7 @@
 -----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
+        self.assertEqual("""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()
@@ -144,75 +156,260 @@
 prettier.  I'll tell Ted to use nicer tag names for future cases.""", msg)
 
 
-class UnicodeNameTestCase(unittest.TestCase):
+class NormalTestCase(unittest.TestCase, GitCommandMixin):
 
     def setUp(self):
         self.env = EnvironmentStub()
-        self.repos_path = tempfile.mkdtemp(prefix='trac-gitrepos')
-        self.git_bin = locate('git')
+        self.repos_path = tempfile.mkdtemp(prefix='trac-gitrepos-')
         # create git repository and master branch
-        self._git('init', self.repos_path)
+        self._git('init')
+        self._git('config', 'core.quotepath', 'true')  # ticket:11198
+        self._git('config', 'user.name', "Joe")
+        self._git('config', 'user.email', "joe@example.com")
         create_file(os.path.join(self.repos_path, '.gitignore'))
         self._git('add', '.gitignore')
-        self._git('commit', '-a', '-m', 'test')
+        self._git_commit('-a', '-m', 'test',
+                         date=datetime(2013, 1, 1, 9, 4, 56))
 
     def tearDown(self):
+        RepositoryManager(self.env).reload_repositories()
+        StorageFactory._clean()
+        self.env.reset_db()
         if os.path.isdir(self.repos_path):
-            shutil.rmtree(self.repos_path)
+            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 _factory(self, weak, path=None):
+        if path is None:
+            path = os.path.join(self.repos_path, '.git')
+        return StorageFactory(path, self.env.log, weak)
+
+    def _storage(self, path=None):
+        if path is None:
+            path = os.path.join(self.repos_path, '.git')
+        return Storage(path, self.env.log, git_bin, 'utf-8')
+
+    def test_control_files_detection(self):
+        # Exception not raised when path points to ctrl file dir
+        self.assertIsInstance(self._storage().repo, GitCore)
+        # Exception not raised when path points to parent of ctrl files dir
+        self.assertIsInstance(self._storage(self.repos_path).repo, GitCore)
+        # Exception raised when path points to dir with no ctrl files
+        path = tempfile.mkdtemp(dir=self.repos_path)
+        self.assertRaises(GitError, self._storage, path)
+        # Exception raised if a ctrl file is missing
+        os.remove(os.path.join(self.repos_path, '.git', 'HEAD'))
+        self.assertRaises(GitError, self._storage, self.repos_path)
+
+    def test_get_branches_with_cr_in_commitlog(self):
+        # regression test for #11598
+        message = 'message with carriage return'.replace(' ', '\r')
+
+        create_file(os.path.join(self.repos_path, 'ticket11598.txt'))
+        self._git('add', 'ticket11598.txt')
+        self._git_commit('-m', message,
+                         date=datetime(2013, 5, 9, 11, 5, 21))
+
+        storage = self._storage()
+        branches = sorted(storage.get_branches())
+        self.assertEqual('master', branches[0][0])
+        self.assertEqual(1, len(branches))
+
+    if os.name == 'nt':
+        del test_get_branches_with_cr_in_commitlog
+
+    def test_rev_is_anchestor_of(self):
+        # regression test for #11215
+        path = os.path.join(self.repos_path, '.git')
+        DbRepositoryProvider(self.env).add_repository('gitrepos', path, 'git')
+        repos = self.env.get_repository('gitrepos')
+        parent_rev = repos.youngest_rev
+
+        create_file(os.path.join(self.repos_path, 'ticket11215.txt'))
+        self._git('add', 'ticket11215.txt')
+        self._git_commit('-m', 'ticket11215',
+                         date=datetime(2013, 6, 27, 18, 26, 2))
+        repos.sync()
+        rev = repos.youngest_rev
+
+        self.assertNotEqual(rev, parent_rev)
+        self.assertFalse(repos.rev_older_than(None, None))
+        self.assertFalse(repos.rev_older_than(None, rev[:7]))
+        self.assertFalse(repos.rev_older_than(rev[:7], None))
+        self.assertTrue(repos.rev_older_than(parent_rev, rev))
+        self.assertTrue(repos.rev_older_than(parent_rev[:7], rev[:7]))
+        self.assertFalse(repos.rev_older_than(rev, parent_rev))
+        self.assertFalse(repos.rev_older_than(rev[:7], parent_rev[:7]))
+
+    def test_node_get_history_with_empty_commit(self):
+        # regression test for #11328
+        path = os.path.join(self.repos_path, '.git')
+        DbRepositoryProvider(self.env).add_repository('gitrepos', path, 'git')
+        repos = self.env.get_repository('gitrepos')
+        parent_rev = repos.youngest_rev
+
+        self._git_commit('-m', 'ticket:11328', '--allow-empty',
+                         date=datetime(2013, 10, 15, 9, 46, 27))
+        repos.sync()
+        rev = repos.youngest_rev
+
+        node = repos.get_node('', rev)
+        self.assertEqual(rev, repos.git.last_change(rev, ''))
+        history = list(node.get_history())
+        self.assertEqual(u'', history[0][0])
+        self.assertEqual(rev, history[0][1])
+        self.assertEqual(Changeset.EDIT, history[0][2])
+        self.assertEqual(u'', history[1][0])
+        self.assertEqual(parent_rev, history[1][1])
+        self.assertEqual(Changeset.ADD, history[1][2])
+        self.assertEqual(2, len(history))
+
+    def test_sync_after_removing_branch(self):
+        self._git('checkout', '-b', 'b1', 'master')
+        self._git('checkout', 'master')
+        create_file(os.path.join(self.repos_path, 'newfile.txt'))
+        self._git('add', 'newfile.txt')
+        self._git_commit('-m', 'added newfile.txt to master',
+                         date=datetime(2013, 12, 23, 6, 52, 23))
+
+        storage = self._storage()
+        storage.sync()
+        self.assertEqual(['b1', 'master'],
+                         sorted(b[0] for b in storage.get_branches()))
+        self._git('branch', '-D', 'b1')
+        self.assertEqual(True, storage.sync())
+        self.assertEqual(['master'],
+                         sorted(b[0] for b in storage.get_branches()))
+        self.assertEqual(False, storage.sync())
+
+    def test_turn_off_persistent_cache(self):
+        # persistent_cache is enabled
+        parent_rev = self._factory(False).getInstance().youngest_rev()
+
+        create_file(os.path.join(self.repos_path, 'newfile.txt'))
+        self._git('add', 'newfile.txt')
+        self._git_commit('-m', 'test_turn_off_persistent_cache',
+                         date=datetime(2014, 1, 29, 13, 13, 25))
+
+        # persistent_cache is disabled
+        rev = self._factory(True).getInstance().youngest_rev()
+        self.assertNotEqual(rev, parent_rev)
+
+
+class UnicodeNameTestCase(unittest.TestCase, GitCommandMixin):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.repos_path = tempfile.mkdtemp(prefix='trac-gitrepos-')
+        # create git repository and master branch
+        self._git('init')
+        self._git('config', 'core.quotepath', 'true')  # ticket:11198
+        self._git('config', 'user.name', "Joé")  # passing utf-8 bytes
+        self._git('config', 'user.email', "joe@example.com")
+        create_file(os.path.join(self.repos_path, '.gitignore'))
+        self._git('add', '.gitignore')
+        self._git_commit('-a', '-m', 'test',
+                         date=datetime(2013, 1, 1, 9, 4, 57))
+
+    def tearDown(self):
+        self.env.reset_db()
+        if os.path.isdir(self.repos_path):
+            rmtree(self.repos_path)
 
     def _storage(self):
         path = os.path.join(self.repos_path, '.git')
-        return Storage(path, self.env.log, self.git_bin, 'utf-8')
+        return Storage(path, self.env.log, 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é'))
+        self.assertIsNone(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')
+        self._git_commit('-m', 'unicode-filename', date='1359912600 +0100')
         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])
+        self.assertEqual(unicode, type(filenames[0]))
+        self.assertEqual(unicode, type(filenames[1]))
+        self.assertEqual(u'.gitignore', filenames[0])
+        self.assertEqual(u'tickét.txt', filenames[1])
+        # check commit author, for good measure
+        self.assertEqual(u'Joé <joe@example.com> 1359912600 +0100',
+                         storage.read_commit(storage.head())[1]['author'][0])
 
     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])
+        self.assertEqual(unicode, type(branches[0][0]))
+        self.assertEqual(unicode, type(branches[1][0]))
+        self.assertEqual(u'master', branches[0][0])
+        self.assertEqual(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])
+        self.assertEqual(unicode, type(contains[0][0]))
+        self.assertEqual(unicode, type(contains[1][0]))
+        self.assertEqual(u'master', contains[0][0])
+        self.assertEqual(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.assertEqual(unicode, type(tags[0]))
+        self.assertEqual(u'täg-t10980', tags[0])
         self.assertNotEqual(None, storage.verifyrev(u'täg-t10980'))
 
+    def test_ls_tree(self):
+        paths = [u'normal-path.txt',
+                 u'tickét.tx\\t',
+                 u'\a\b\t\n\v\f\r\x1b"\\.tx\\t']
+        for path in paths:
+            path_utf8 = path.encode('utf-8')
+            create_file(os.path.join(self.repos_path, path_utf8))
+            self._git('add', path_utf8)
+        self._git_commit('-m', 'ticket:11180 and ticket:11198',
+                         date=datetime(2013, 4, 30, 13, 48, 57))
+
+        storage = self._storage()
+        rev = storage.head()
+        entries = storage.ls_tree(rev, '/')
+        self.assertEqual(4, len(entries))
+        self.assertEqual(u'\a\b\t\n\v\f\r\x1b"\\.tx\\t', entries[0][4])
+        self.assertEqual(u'.gitignore', entries[1][4])
+        self.assertEqual(u'normal-path.txt', entries[2][4])
+        self.assertEqual(u'tickét.tx\\t', entries[3][4])
+
+    def test_get_historian(self):
+        paths = [u'normal-path.txt',
+                 u'tickét.tx\\t',
+                 u'\a\b\t\n\v\f\r\x1b"\\.tx\\t']
+
+        for path in paths:
+            path_utf8 = path.encode('utf-8')
+            create_file(os.path.join(self.repos_path, path_utf8))
+            self._git('add', path_utf8)
+        self._git_commit('-m', 'ticket:11180 and ticket:11198',
+                         date=datetime(2013, 4, 30, 17, 48, 57))
+
+        def validate(path, quotepath):
+            self._git('config', 'core.quotepath', quotepath)
+            storage = self._storage()
+            rev = storage.head()
+            with storage.get_historian('HEAD', path) as historian:
+                hrev = storage.last_change('HEAD', path, historian)
+                self.assertEquals(rev, hrev)
+
+        validate(paths[0], 'true')
+        validate(paths[0], 'false')
+        validate(paths[1], 'true')
+        validate(paths[1], 'false')
+        validate(paths[2], 'true')
+        validate(paths[2], 'false')
+
 
 #class GitPerformanceTestCase(unittest.TestCase):
 #    """Performance test. Not really a unit test.
@@ -232,7 +429,7 @@
 #                i = str(i)
 #                s = g.shortrev(i, min_len=4)
 #                self.assertTrue(i.startswith(s))
-#                self.assertEquals(g.fullrev(s), i)
+#                self.assertEqual(g.fullrev(s), i)
 #
 #        iters = 1
 #        t = timeit.Timer("shortrev_test()",
@@ -260,7 +457,7 @@
 #                    t = open(__proc_statm)
 #                    result = t.read().split()
 #                    t.close()
-#                    assert len(result) == 7
+#                    self.assertEqual(7, len(result))
 #                    return tuple([ __pagesize*int(p) for p in result ])
 #                except:
 #                    raise RuntimeError("failed to get memory stats")
@@ -363,14 +560,16 @@
 
 
 def suite():
+    global git_bin
     suite = unittest.TestSuite()
-    git = locate("git")
-    if git:
-        suite.addTest(unittest.makeSuite(GitTestCase, 'test'))
-        suite.addTest(unittest.makeSuite(TestParseCommit, 'test'))
+    git_bin = locate('git')
+    if git_bin:
+        suite.addTest(unittest.makeSuite(GitTestCase))
+        suite.addTest(unittest.makeSuite(TestParseCommit))
+        suite.addTest(unittest.makeSuite(NormalTestCase))
         if os.name != 'nt':
             # Popen doesn't accept unicode path and arguments on Windows
-            suite.addTest(unittest.makeSuite(UnicodeNameTestCase, 'test'))
+            suite.addTest(unittest.makeSuite(UnicodeNameTestCase))
     else:
         print("SKIP: tracopt/versioncontrol/git/tests/PyGIT.py (git cli "
               "binary, 'git', not found)")
diff --git a/trac/tracopt/versioncontrol/git/tests/__init__.py b/trac/tracopt/versioncontrol/git/tests/__init__.py
index c186271..0e6f8d0 100644
--- a/trac/tracopt/versioncontrol/git/tests/__init__.py
+++ b/trac/tracopt/versioncontrol/git/tests/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C) 2012 Edgewall Software
+# Copyright (C) 2012-2013 Edgewall Software
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -13,12 +13,13 @@
 
 import unittest
 
-from tracopt.versioncontrol.git.tests import PyGIT
+from tracopt.versioncontrol.git.tests import PyGIT, git_fs
 
 
 def suite():
     suite = unittest.TestSuite()
     suite.addTest(PyGIT.suite())
+    suite.addTest(git_fs.suite())
     return suite
 
 
diff --git a/trac/tracopt/versioncontrol/git/tests/git_fs.py b/trac/tracopt/versioncontrol/git/tests/git_fs.py
new file mode 100644
index 0000000..bf594ce
--- /dev/null
+++ b/trac/tracopt/versioncontrol/git/tests/git_fs.py
@@ -0,0 +1,521 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2014 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
+from datetime import datetime, timedelta
+from subprocess import Popen, PIPE
+
+from trac.core import TracError
+from trac.test import EnvironmentStub, Mock, MockPerm, locate
+from trac.tests.compat import rmtree
+from trac.util import create_file
+from trac.util.compat import close_fds
+from trac.util.datefmt import to_timestamp, utc
+from trac.versioncontrol.api import Changeset, DbRepositoryProvider, \
+                                    NoSuchChangeset, NoSuchNode, \
+                                    RepositoryManager
+from trac.versioncontrol.web_ui.browser import BrowserModule
+from trac.versioncontrol.web_ui.log import LogModule
+from trac.web.href import Href
+from tracopt.versioncontrol.git.PyGIT import StorageFactory
+from tracopt.versioncontrol.git.git_fs import GitCachedRepository, \
+                                              GitConnector, GitRepository
+
+
+git_bin = None
+
+
+class GitCommandMixin(object):
+
+    def _git_commit(self, *args, **kwargs):
+        env = kwargs.get('env') or os.environ.copy()
+        if 'date' in kwargs:
+            self._set_committer_date(env, kwargs.pop('date'))
+        args = ('commit',) + args
+        kwargs['env'] = env
+        return self._git(*args, **kwargs)
+
+    def _git(self, *args, **kwargs):
+        args = (git_bin,) + args
+        proc = Popen(args, stdout=PIPE, stderr=PIPE, close_fds=close_fds,
+                     cwd=self.repos_path, **kwargs)
+        stdout, stderr = proc.communicate()
+        self.assertEqual(0, proc.returncode,
+                         'git exits with %r, args %r, stdout %r, stderr %r' %
+                         (proc.returncode, args, stdout, stderr))
+        return proc
+
+    def _git_date_format(self, dt):
+        if dt.tzinfo is None:
+            dt = dt.replace(tzinfo=utc)
+        offset = dt.utcoffset()
+        secs = offset.days * 3600 * 24 + offset.seconds
+        hours, rem = divmod(abs(secs), 3600)
+        return '%d %c%02d:%02d' % (to_timestamp(dt), '-' if secs < 0 else '+',
+                                   hours, rem / 60)
+
+    def _set_committer_date(self, env, dt):
+        if not isinstance(dt, basestring):
+            if dt.tzinfo is None:
+                dt = dt.replace(tzinfo=utc)
+            dt = self._git_date_format(dt)
+        env['GIT_COMMITTER_DATE'] = dt
+        env['GIT_AUTHOR_DATE'] = dt
+
+
+class BaseTestCase(unittest.TestCase, GitCommandMixin):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.repos_path = tempfile.mkdtemp(prefix='trac-gitrepos-')
+        if git_bin:
+            self.env.config.set('git', 'git_bin', git_bin)
+
+    def tearDown(self):
+        self._repomgr.reload_repositories()
+        StorageFactory._clean()
+        self.env.reset_db()
+        if os.path.isdir(self.repos_path):
+            rmtree(self.repos_path)
+
+    @property
+    def _repomgr(self):
+        return RepositoryManager(self.env)
+
+    @property
+    def _dbrepoprov(self):
+        return DbRepositoryProvider(self.env)
+
+    def _add_repository(self, reponame='gitrepos', bare=False):
+        path = self.repos_path \
+               if bare else os.path.join(self.repos_path, '.git')
+        self._dbrepoprov.add_repository(reponame, path, 'git')
+
+    def _git_init(self, data=True, bare=False):
+        if bare:
+            self._git('init', '--bare')
+        else:
+            self._git('init')
+        if not bare and data:
+            self._git('config', 'user.name', 'Joe')
+            self._git('config', 'user.email', 'joe@example.com')
+            create_file(os.path.join(self.repos_path, '.gitignore'))
+            self._git('add', '.gitignore')
+            self._git_commit('-a', '-m', 'test',
+                             date=datetime(2001, 1, 29, 16, 39, 56))
+
+
+class SanityCheckingTestCase(BaseTestCase):
+
+    def test_bare(self):
+        self._git_init(bare=True)
+        self._dbrepoprov.add_repository('gitrepos', self.repos_path, 'git')
+        self._repomgr.get_repository('gitrepos')
+
+    def test_non_bare(self):
+        self._git_init(bare=False)
+        self._dbrepoprov.add_repository('gitrepos.1',
+                                        os.path.join(self.repos_path, '.git'),
+                                        'git')
+        self._repomgr.get_repository('gitrepos.1')
+        self._dbrepoprov.add_repository('gitrepos.2', self.repos_path, 'git')
+        self._repomgr.get_repository('gitrepos.2')
+
+    def test_no_head_file(self):
+        self._git_init(bare=True)
+        os.unlink(os.path.join(self.repos_path, 'HEAD'))
+        self._dbrepoprov.add_repository('gitrepos', self.repos_path, 'git')
+        self.assertRaises(TracError, self._repomgr.get_repository, 'gitrepos')
+
+    def test_no_objects_dir(self):
+        self._git_init(bare=True)
+        rmtree(os.path.join(self.repos_path, 'objects'))
+        self._dbrepoprov.add_repository('gitrepos', self.repos_path, 'git')
+        self.assertRaises(TracError, self._repomgr.get_repository, 'gitrepos')
+
+    def test_no_refs_dir(self):
+        self._git_init(bare=True)
+        rmtree(os.path.join(self.repos_path, 'refs'))
+        self._dbrepoprov.add_repository('gitrepos', self.repos_path, 'git')
+        self.assertRaises(TracError, self._repomgr.get_repository, 'gitrepos')
+
+
+class PersistentCacheTestCase(BaseTestCase):
+
+    def test_persistent(self):
+        self.env.config.set('git', 'persistent_cache', 'enabled')
+        self._git_init()
+        self._add_repository()
+        youngest = self._repository.youngest_rev
+        self._repomgr.reload_repositories()  # clear repository cache
+
+        self._commit(datetime(2014, 1, 29, 16, 44, 54, 0, utc))
+        self.assertEqual(youngest, self._repository.youngest_rev)
+        self._repository.sync()
+        self.assertNotEqual(youngest, self._repository.youngest_rev)
+
+    def test_non_persistent(self):
+        self.env.config.set('git', 'persistent_cache', 'disabled')
+        self._git_init()
+        self._add_repository()
+        youngest = self._repository.youngest_rev
+        self._repomgr.reload_repositories()  # clear repository cache
+
+        self._commit(datetime(2014, 1, 29, 16, 44, 54, 0, utc))
+        youngest_2 = self._repository.youngest_rev
+        self.assertNotEqual(youngest, youngest_2)
+        self._repository.sync()
+        self.assertNotEqual(youngest, self._repository.youngest_rev)
+        self.assertEqual(youngest_2, self._repository.youngest_rev)
+
+    def _commit(self, date):
+        gitignore = os.path.join(self.repos_path, '.gitignore')
+        create_file(gitignore, date.isoformat())
+        self._git_commit('-a', '-m', date.isoformat(), date=date)
+
+    @property
+    def _repository(self):
+        return self._repomgr.get_repository('gitrepos')
+
+
+class HistoryTimeRangeTestCase(BaseTestCase):
+
+    def test_without_cache(self):
+        self._test_timerange('disabled')
+
+    def test_with_cache(self):
+        self._test_timerange('enabled')
+
+    def _test_timerange(self, cached_repository):
+        self.env.config.set('git', 'cached_repository', cached_repository)
+
+        self._git_init()
+        filename = os.path.join(self.repos_path, '.gitignore')
+        start = datetime(2000, 1, 1, 0, 0, 0, 0, utc)
+        ts = datetime(2014, 2, 5, 15, 24, 6, 0, utc)
+        for idx in xrange(3):
+            create_file(filename, 'commit-%d.txt' % idx)
+            self._git_commit('-a', '-m', 'commit %d' % idx, date=ts)
+        self._add_repository()
+        repos = self._repomgr.get_repository('gitrepos')
+        repos.sync()
+
+        revs = [repos.youngest_rev]
+        while True:
+            parents = repos.parent_revs(revs[-1])
+            if not parents:
+                break
+            revs.extend(parents)
+        self.assertEqual(4, len(revs))
+
+        csets = list(repos.get_changesets(start, ts))
+        self.assertEqual(1, len(csets))
+        self.assertEqual(revs[-1], csets[0].rev)  # is oldest rev
+
+        csets = list(repos.get_changesets(start, ts + timedelta(seconds=1)))
+        self.assertEqual(revs, [cset.rev for cset in csets])
+
+
+class GitNormalTestCase(BaseTestCase):
+
+    def _create_req(self, **kwargs):
+        data = dict(args={}, perm=MockPerm(), href=Href('/'), chrome={},
+                    authname='trac', tz=utc, get_header=lambda name: None)
+        data.update(kwargs)
+        return Mock(**data)
+
+    def test_get_node(self):
+        self.env.config.set('git', 'persistent_cache', 'false')
+        self.env.config.set('git', 'cached_repository', 'false')
+
+        self._git_init()
+        self._add_repository()
+        repos = self._repomgr.get_repository('gitrepos')
+        rev = repos.youngest_rev
+        self.assertNotEqual(None, rev)
+        self.assertEqual(40, len(rev))
+
+        self.assertEqual(rev, repos.get_node('/').rev)
+        self.assertEqual(rev, repos.get_node('/', rev[:7]).rev)
+        self.assertEqual(rev, repos.get_node('/.gitignore').rev)
+        self.assertEqual(rev, repos.get_node('/.gitignore', rev[:7]).rev)
+
+        self.assertRaises(NoSuchNode, repos.get_node, '/non-existent')
+        self.assertRaises(NoSuchNode, repos.get_node, '/non-existent', rev[:7])
+        self.assertRaises(NoSuchNode, repos.get_node, '/non-existent', rev)
+        self.assertRaises(NoSuchChangeset,
+                          repos.get_node, '/', 'invalid-revision')
+        self.assertRaises(NoSuchChangeset,
+                          repos.get_node, '/.gitignore', 'invalid-revision')
+        self.assertRaises(NoSuchChangeset,
+                          repos.get_node, '/non-existent', 'invalid-revision')
+
+        # git_fs doesn't support non-ANSI strings on Windows
+        if os.name != 'nt':
+            self._git('branch', u'tïckét10605', 'master')
+            repos.sync()
+            self.assertEqual(rev, repos.get_node('/', u'tïckét10605').rev)
+            self.assertEqual(rev, repos.get_node('/.gitignore',
+                                                 u'tïckét10605').rev)
+
+    def _test_on_empty_repos(self, cached_repository):
+        self.env.config.set('git', 'persistent_cache', 'false')
+        self.env.config.set('git', 'cached_repository', cached_repository)
+
+        self._git_init(data=False, bare=True)
+        self._add_repository(bare=True)
+        repos = self._repomgr.get_repository('gitrepos')
+        repos.sync()
+        youngest_rev = repos.youngest_rev
+        self.assertEqual(None, youngest_rev)
+        self.assertEqual(None, repos.oldest_rev)
+        self.assertEqual(None, repos.normalize_rev(''))
+        self.assertEqual(None, repos.normalize_rev(None))
+
+        node = repos.get_node('/', youngest_rev)
+        self.assertEqual([], list(node.get_entries()))
+        self.assertEqual([], list(node.get_history()))
+        self.assertRaises(NoSuchNode, repos.get_node, '/path', youngest_rev)
+
+        req = self._create_req(path_info='/browser/gitrepos')
+        browser_mod = BrowserModule(self.env)
+        self.assertTrue(browser_mod.match_request(req))
+        rv = browser_mod.process_request(req)
+        self.assertEqual('browser.html', rv[0])
+        self.assertEqual(None, rv[1]['rev'])
+
+        req = self._create_req(path_info='/log/gitrepos')
+        log_mod = LogModule(self.env)
+        self.assertTrue(log_mod.match_request(req))
+        rv = log_mod.process_request(req)
+        self.assertEqual('revisionlog.html', rv[0])
+        self.assertEqual([], rv[1]['items'])
+
+    def test_on_empty_and_cached_repos(self):
+        self._test_on_empty_repos('true')
+
+    def test_on_empty_and_non_cached_repos(self):
+        self._test_on_empty_repos('false')
+
+
+class GitRepositoryTestCase(BaseTestCase):
+
+    cached_repository = 'disabled'
+
+    def setUp(self):
+        BaseTestCase.setUp(self)
+        self.env.config.set('git', 'cached_repository', self.cached_repository)
+
+    def _create_merge_commit(self):
+        for idx, branch in enumerate(('alpha', 'beta')):
+            self._git('checkout', '-b', branch, 'master')
+            for n in xrange(2):
+                filename = 'file-%s-%d.txt' % (branch, n)
+                create_file(os.path.join(self.repos_path, filename))
+                self._git('add', filename)
+                self._git_commit('-a', '-m', filename,
+                                 date=datetime(2014, 2, 2, 17, 12,
+                                               n * 2 + idx))
+        self._git('checkout', 'alpha')
+        self._git('merge', '-m', 'Merge branch "beta" to "alpha"', 'beta')
+
+    def test_repository_instance(self):
+        self._git_init()
+        self._add_repository('gitrepos')
+        self.assertEqual(GitRepository,
+                         type(self._repomgr.get_repository('gitrepos')))
+
+    def test_reset_head(self):
+        self._git_init()
+        create_file(os.path.join(self.repos_path, 'file.txt'), 'text')
+        self._git('add', 'file.txt')
+        self._git_commit('-a', '-m', 'test',
+                         date=datetime(2014, 2, 2, 17, 12, 18))
+        self._add_repository('gitrepos')
+        repos = self._repomgr.get_repository('gitrepos')
+        repos.sync()
+        youngest_rev = repos.youngest_rev
+        entries = list(repos.get_node('').get_history())
+        self.assertEqual(2, len(entries))
+        self.assertEqual('', entries[0][0])
+        self.assertEqual(Changeset.EDIT, entries[0][2])
+        self.assertEqual('', entries[1][0])
+        self.assertEqual(Changeset.ADD, entries[1][2])
+
+        self._git('reset', '--hard', 'HEAD~')
+        repos.sync()
+        new_entries = list(repos.get_node('').get_history())
+        self.assertEqual(1, len(new_entries))
+        self.assertEqual(new_entries[0], entries[1])
+        self.assertNotEqual(youngest_rev, repos.youngest_rev)
+
+    def test_tags(self):
+        self._git_init()
+        self._add_repository('gitrepos')
+        repos = self._repomgr.get_repository('gitrepos')
+        repos.sync()
+        self.assertEqual(['master'], self._get_quickjump_names(repos))
+        self._git('tag', 'v1.0', 'master')  # add tag
+        repos.sync()
+        self.assertEqual(['master', 'v1.0'], self._get_quickjump_names(repos))
+        self._git('tag', '-d', 'v1.0')  # delete tag
+        repos.sync()
+        self.assertEqual(['master'], self._get_quickjump_names(repos))
+
+    def test_branchs(self):
+        self._git_init()
+        self._add_repository('gitrepos')
+        repos = self._repomgr.get_repository('gitrepos')
+        repos.sync()
+        self.assertEqual(['master'], self._get_quickjump_names(repos))
+        self._git('branch', 'alpha', 'master')  # add branch
+        repos.sync()
+        self.assertEqual(['alpha', 'master'], self._get_quickjump_names(repos))
+        self._git('branch', '-m', 'alpha', 'beta')  # rename branch
+        repos.sync()
+        self.assertEqual(['beta', 'master'], self._get_quickjump_names(repos))
+        self._git('branch', '-D', 'beta')  # delete branch
+        repos.sync()
+        self.assertEqual(['master'], self._get_quickjump_names(repos))
+
+    def test_parent_child_revs(self):
+        self._git_init()
+        self._git('branch', 'initial')
+        self._create_merge_commit()
+        self._git('branch', 'latest')
+
+        self._add_repository('gitrepos')
+        repos = self._repomgr.get_repository('gitrepos')
+        repos.sync()
+
+        rev = repos.normalize_rev('initial')
+        children = repos.child_revs(rev)
+        self.assertEqual(2, len(children), 'child_revs: %r' % children)
+        parents = repos.parent_revs(rev)
+        self.assertEqual(0, len(parents), 'parent_revs: %r' % parents)
+        self.assertEqual(1, len(repos.child_revs(children[0])))
+        self.assertEqual(1, len(repos.child_revs(children[1])))
+
+        rev = repos.normalize_rev('latest')
+        children = repos.child_revs(rev)
+        self.assertEqual(0, len(children), 'child_revs: %r' % children)
+        parents = repos.parent_revs(rev)
+        self.assertEqual(2, len(parents), 'parent_revs: %r' % parents)
+        self.assertEqual(1, len(repos.parent_revs(parents[0])))
+        self.assertEqual(1, len(repos.parent_revs(parents[1])))
+
+    def _get_quickjump_names(self, repos):
+        return sorted(name for type, name, path, rev
+                           in repos.get_quickjump_entries('HEAD'))
+
+
+class GitCachedRepositoryTestCase(GitRepositoryTestCase):
+
+    cached_repository = 'enabled'
+
+    def test_repository_instance(self):
+        self._git_init()
+        self._add_repository('gitrepos')
+        self.assertEqual(GitCachedRepository,
+                         type(self._repomgr.get_repository('gitrepos')))
+
+    def test_sync(self):
+        self._git_init()
+        for idx in xrange(3):
+            filename = 'file%d.txt' % idx
+            create_file(os.path.join(self.repos_path, filename))
+            self._git('add', filename)
+            self._git_commit('-a', '-m', filename,
+                             date=datetime(2014, 2, 2, 17, 12, idx))
+        self._add_repository('gitrepos')
+        repos = self._repomgr.get_repository('gitrepos')
+        revs = [entry[1] for entry in repos.repos.get_node('').get_history()]
+        revs.reverse()
+        revs2 = []
+        def feedback(rev):
+            revs2.append(rev)
+        repos.sync(feedback=feedback)
+        self.assertEqual(revs, revs2)
+        self.assertEqual(4, len(revs2))
+
+        revs2 = []
+        def feedback_1(rev):
+            revs2.append(rev)
+            if len(revs2) == 2:
+                raise StopSync
+        def feedback_2(rev):
+            revs2.append(rev)
+        try:
+            repos.sync(feedback=feedback_1, clean=True)
+        except StopSync:
+            self.assertEqual(revs[:2], revs2)
+            repos.sync(feedback=feedback_2)  # restart sync
+        self.assertEqual(revs, revs2)
+
+    def test_sync_merge(self):
+        self._git_init()
+        self._create_merge_commit()
+
+        self._add_repository('gitrepos')
+        repos = self._repomgr.get_repository('gitrepos')
+        youngest_rev = repos.repos.youngest_rev
+        oldest_rev = repos.repos.oldest_rev
+
+        revs = []
+        def feedback(rev):
+            revs.append(rev)
+        repos.sync(feedback=feedback)
+        self.assertEqual(6, len(revs))
+        self.assertEqual(youngest_rev, revs[-1])
+        self.assertEqual(oldest_rev, revs[0])
+
+        revs2 = []
+        def feedback_1(rev):
+            revs2.append(rev)
+            if len(revs2) == 3:
+                raise StopSync
+        def feedback_2(rev):
+            revs2.append(rev)
+        try:
+            repos.sync(feedback=feedback_1, clean=True)
+        except StopSync:
+            self.assertEqual(revs[:3], revs2)
+            repos.sync(feedback=feedback_2)  # restart sync
+        self.assertEqual(revs, revs2)
+
+
+class StopSync(Exception):
+    pass
+
+
+def suite():
+    global git_bin
+    suite = unittest.TestSuite()
+    git_bin = locate('git')
+    if git_bin:
+        suite.addTest(unittest.makeSuite(SanityCheckingTestCase))
+        suite.addTest(unittest.makeSuite(PersistentCacheTestCase))
+        suite.addTest(unittest.makeSuite(HistoryTimeRangeTestCase))
+        suite.addTest(unittest.makeSuite(GitNormalTestCase))
+        suite.addTest(unittest.makeSuite(GitRepositoryTestCase))
+        suite.addTest(unittest.makeSuite(GitCachedRepositoryTestCase))
+    else:
+        print("SKIP: tracopt/versioncontrol/git/tests/git_fs.py (git cli "
+              "binary, 'git', not found)")
+    return suite
+
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
diff --git a/trac/tracopt/versioncontrol/svn/svn_fs.py b/trac/tracopt/versioncontrol/svn/svn_fs.py
index 08a0eaa..7e1eb56 100644
--- a/trac/tracopt/versioncontrol/svn/svn_fs.py
+++ b/trac/tracopt/versioncontrol/svn/svn_fs.py
@@ -47,11 +47,15 @@
   those properties...
 """
 
+from __future__ import with_statement
+
 import os.path
+import re
 import weakref
 import posixpath
+from urllib import quote
 
-from trac.config import ListOption
+from trac.config import ListOption, ChoiceOption
 from trac.core import *
 from trac.env import ISystemInfoProvider
 from trac.versioncontrol import Changeset, Node, Repository, \
@@ -59,19 +63,25 @@
                                 NoSuchChangeset, NoSuchNode
 from trac.versioncontrol.cache import CachedRepository
 from trac.util import embedded_numbers
+from trac.util.concurrency import threading
 from trac.util.text import exception_to_unicode, to_unicode
 from trac.util.translation import _
-from trac.util.datefmt import from_utimestamp
+from trac.util.datefmt import from_utimestamp, to_datetime, utc
 
 
 application_pool = None
+application_pool_lock = threading.Lock()
 
 
 def _import_svn():
-    global fs, repos, core, delta, _kindmap
+    global fs, repos, core, delta, _kindmap, _svn_uri_canonicalize
     from svn import fs, repos, core, delta
     _kindmap = {core.svn_node_dir: Node.DIRECTORY,
                 core.svn_node_file: Node.FILE}
+    try:
+        _svn_uri_canonicalize = core.svn_uri_canonicalize  # Subversion 1.7+
+    except AttributeError:
+        _svn_uri_canonicalize = lambda v: v
     # Protect svn.core methods from GC
     Pool.apr_pool_clear = staticmethod(core.apr_pool_clear)
     Pool.apr_pool_destroy = staticmethod(core.apr_pool_destroy)
@@ -150,19 +160,21 @@
         """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
+        with application_pool_lock:
+            self._parent_pool = parent_pool or application_pool
 
-            self._pool = core.svn_pool_create(None)
+            # 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()
+                self._pool = core.svn_pool_create(None)
+                application_pool = self
+
         self._mark_valid()
 
     def __call__(self):
@@ -265,6 +277,17 @@
         Example: `/tags/*, /projectAlpha/tags/A-1.0, /projectAlpha/tags/A-v1.1`
         """)
 
+    eol_style = ChoiceOption(
+        'svn', 'eol_style', ['native', 'LF', 'CRLF', 'CR'], doc=
+        """End-of-Line character sequences when `svn:eol-style` property is
+        `native`.
+
+        If `native` (the default), substitute with the native EOL marker on
+        the server. Otherwise, if `LF`, `CRLF` or `CR`, substitute with the
+        specified EOL marker.
+
+        (''since 1.0.2'')""")
+
     error = None
 
     def __init__(self):
@@ -307,6 +330,7 @@
         'direct-svnfs'.
         """
         params.update(tags=self.tags, branches=self.branches)
+        params.setdefault('eol_style', self.eol_style)
         repos = SubversionRepository(dir, params, self.log)
         if type != 'direct-svnfs':
             repos = SvnCachedRepository(self.env, repos, self.log)
@@ -328,7 +352,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('\\', '/')
+        path_utf8 = core.svn_path_canonicalize(
+                                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())
@@ -361,7 +386,8 @@
         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.ra_url_utf8 = _svn_uri_canonicalize(ra_prefix +
+                                                 quote(root_path_utf8))
         self.clear()
 
     def clear(self, youngest_rev=None):
@@ -475,7 +501,7 @@
         specifications. No revision given means use the latest.
         """
         path = path or ''
-        if path and path[-1] == '/':
+        if path and path != '/' and path[-1] == '/':
             path = path[:-1]
         rev = self.normalize_rev(rev) or self.youngest_rev
         return SubversionNode(path, rev, self, self.pool)
@@ -493,6 +519,18 @@
             revs.append(r)
         return revs
 
+    def _get_changed_revs(self, node_infos):
+        path_revs = {}
+        for node, first in node_infos:
+            path = node.path
+            revs = []
+            for p, r, chg in node.get_history():
+                if p != path or r < first:
+                    break
+                revs.append(r)
+            path_revs[path] = revs
+        return path_revs
+
     def _history(self, path, start, end, pool):
         """`path` is a unicode path in the scope.
 
@@ -640,14 +678,6 @@
 
         (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)
@@ -688,8 +718,12 @@
                                       entry_props,
                                       ignore_ancestry,
                                       subpool())
-            for path, kind, change in editor.deltas:
-                path = _from_svn(path)
+            # sort deltas by path before creating `SubversionNode`s to reduce
+            # memory usage (#10978)
+            deltas = sorted(((_from_svn(path), kind, change)
+                             for path, kind, change in editor.deltas),
+                            key=lambda entry: entry[0])
+            for path, kind, change in deltas:
                 old_node = new_node = None
                 if change != Changeset.ADD:
                     old_node = self.get_node(posixpath.join(old_path, path),
@@ -753,13 +787,15 @@
         """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
+        return FileContentStream(self)
+
+    def get_processed_content(self, keyword_substitution=True, eol_hint=None):
+        """Retrieve processed content as a "read()"able object."""
+        if self.isdir:
+            return None
+        eol_style = self.repos.params.get('eol_style') if eol_hint is None \
+            else eol_hint
+        return FileContentStream(self, keyword_substitution, eol_style)
 
     def get_entries(self):
         """Yield `SubversionNode` corresponding to entries in this directory.
@@ -811,7 +847,10 @@
                 rev = _svn_rev(self.rev)
                 start = _svn_rev(0)
                 file_url_utf8 = posixpath.join(self.repos.ra_url_utf8,
-                                               self._scoped_path_utf8)
+                                               quote(self._scoped_path_utf8))
+                # svn_client_blame2() requires a canonical uri since
+                # Subversion 1.7 (#11167)
+                file_url_utf8 = _svn_uri_canonicalize(file_url_utf8)
                 self.repos.log.info('opening ra_local session to %r',
                                     file_url_utf8)
                 from svn import client
@@ -1006,7 +1045,7 @@
                 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):
+                if base_rev in revroots:
                     b_root = revroots[base_rev]
                 else:
                     b_root = fs.revision_root(self.fs_ptr, base_rev, pool())
@@ -1094,3 +1133,186 @@
 
     return DiffChangeEditor()
 
+
+class FileContentStream(object):
+
+    KEYWORD_GROUPS = {
+        'rev': ['LastChangedRevision', 'Rev', 'Revision'],
+        'date': ['LastChangedDate', 'Date'],
+        'author': ['LastChangedBy', 'Author'],
+        'url': ['HeadURL', 'URL'],
+        'id': ['Id'],
+        'header': ['Header'],
+        }
+    KEYWORDS = reduce(set.union, map(set, KEYWORD_GROUPS.values()))
+    NATIVE_EOL = '\r\n' if os.name == 'nt' else '\n'
+    NEWLINES = {'LF': '\n', 'CRLF': '\r\n', 'CR': '\r', 'native': NATIVE_EOL}
+    KEYWORD_MAX_SIZE = 256
+    CHUNK_SIZE = 4096
+
+    keywords_re = None
+    native_eol = None
+    newline = '\n'
+
+    def __init__(self, node, keyword_substitution=None, eol=None):
+        self.translated = ''
+        self.buffer = ''
+        self.repos = node.repos
+        self.node = node
+        self.fs_ptr = node.fs_ptr
+        self.pool = Pool()
+        # Note: we _must_ use a detached pool here, as the lifetime of
+        # this object can exceed those of the node or even the repository
+        if keyword_substitution:
+            keywords = (node._get_prop(core.SVN_PROP_KEYWORDS) or '').split()
+            self.keywords = self._get_keyword_values(set(keywords) &
+                                                 set(self.KEYWORDS))
+            self.keywords_re = self._build_keywords_re(self.keywords)
+        if self.NEWLINES.get(eol, '\n') != '\n' and \
+           node._get_prop(core.SVN_PROP_EOL_STYLE) == 'native':
+            self.native_eol = True
+            self.newline = self.NEWLINES[eol]
+        self.stream = core.Stream(fs.file_contents(node.root,
+                                                   node._scoped_path_utf8,
+                                                   self.pool()))
+
+    def __del__(self):
+        self.close()
+
+    def close(self):
+        self.stream = None
+        self.fs_ptr = None
+        if self.pool:
+            self.pool.destroy()
+            self.pool = None
+
+    def read(self, n=None):
+        if self.stream is None:
+            raise ValueError('I/O operation on closed file')
+        if self.keywords_re is None and not self.native_eol:
+            return self._read_dumb(self.stream, n)
+        else:
+            return self._read_substitute(self.stream, n)
+
+    def _get_revprop(self, name):
+        return fs.revision_prop(self.fs_ptr, self.node.rev, name, self.pool())
+
+    def _get_keyword_values(self, keywords):
+        if not keywords:
+            return None
+
+        node = self.node
+        mtime = to_datetime(node.last_modified, utc)
+        shortdate = mtime.strftime('%Y-%m-%d %H:%M:%SZ')
+        created_rev = unicode(node.created_rev)
+        # Note that the `to_unicode` has a small probability to mess-up binary
+        # properties, see #4321.
+        author = to_unicode(self._get_revprop(core.SVN_PROP_REVISION_AUTHOR))
+        url = node.repos.get_path_url(node.path, node.rev) or node.path
+        data = {
+            'rev': created_rev, 'author': author, 'url': url,
+            'date': mtime.strftime('%Y-%m-%d %H:%M:%S +0000 (%a, %d %b %Y)'),
+            'id': ' '.join((posixpath.basename(node.path), created_rev,
+                            shortdate, author)),
+            'header': ' '.join((url, created_rev, shortdate, author)),
+            }
+        values = {}
+        for name, aliases in self.KEYWORD_GROUPS.iteritems():
+            if any(kw for kw in aliases if kw in keywords):
+                for kw in aliases:
+                    values[kw] = data[name]
+        if values:
+            return dict((key, value.encode('utf-8'))
+                        for key, value in values.iteritems())
+        else:
+            return None
+
+    def _build_keywords_re(self, keywords):
+        if keywords:
+            return re.compile("""
+                [$]
+                (?P<keyword>%s)
+                (?P<rest>
+                    (?: :[ ][^$\r\n]+?[ ]
+                    |   ::[ ][^$\r\n]+?[ #]
+                    )
+                )?
+                [$]""" % '|'.join(keywords),
+                re.VERBOSE)
+        else:
+            return None
+
+    def _read_dumb(self, stream, n):
+        return stream.read(n)
+
+    def _read_substitute(self, stream, n):
+        if n is None:
+            n = -1
+
+        buffer = self.buffer
+        translated = self.translated
+        while True:
+            if 0 <= n <= len(translated):
+                self.buffer = buffer
+                self.translated = translated[n:]
+                return translated[:n]
+
+            if len(buffer) < self.KEYWORD_MAX_SIZE:
+                buffer += stream.read(self.CHUNK_SIZE) or ''
+                if not buffer:
+                    self.buffer = buffer
+                    self.translated = ''
+                    return translated
+
+            # search first "$" character
+            pos = buffer.find('$') if self.keywords_re else -1
+            if pos == -1:
+                translated += self._translate_newline(buffer)
+                buffer = ''
+                continue
+            if pos > 0:
+                # move to the first "$" character
+                translated += self._translate_newline(buffer[:pos])
+                buffer = buffer[pos:]
+
+            match = None
+            while True:
+                # search second "$" character
+                pos = buffer.find('$', 1)
+                if pos == -1:
+                    translated += self._translate_newline(buffer)
+                    buffer = ''
+                    break
+                if pos < self.KEYWORD_MAX_SIZE:
+                    match = self.keywords_re.match(buffer)
+                    if match:
+                        break  # found "$Keyword$" in the first 255 bytes
+                # move to the second "$" character
+                translated += self._translate_newline(buffer[:pos])
+                buffer = buffer[pos:]
+            if pos == -1 or not match:
+                continue
+
+            # move to the next character of the second "$" character
+            pos += 1
+            translated += self._translate_keyword(buffer[:pos], match)
+            buffer = buffer[pos:]
+            continue
+
+    def _translate_newline(self, data):
+        if self.native_eol:
+            data = data.replace('\n', self.newline)
+        return data
+
+    def _translate_keyword(self, buffer, match):
+        keyword = match.group('keyword')
+        value = self.keywords.get(keyword)
+        if value is None:
+            return buffer
+        rest = match.group('rest')
+        if rest is None or not rest.startswith('::'):
+            return '$%s: %s $' % (keyword, value)
+        elif len(rest) - 4 >= len(value):
+            return '$%s:: %-*s $' % (keyword, len(rest) - 4, value)
+        else:
+            return '$%s:: %s#$' % (keyword, value[:len(rest) - 4])
diff --git a/trac/tracopt/versioncontrol/svn/svn_prop.py b/trac/tracopt/versioncontrol/svn/svn_prop.py
index cd8e210..2b44305 100644
--- a/trac/tracopt/versioncontrol/svn/svn_prop.py
+++ b/trac/tracopt/versioncontrol/svn/svn_prop.py
@@ -154,7 +154,7 @@
 
     def _render_needslock(self, context):
         return tag.img(src=context.href.chrome('common/lock-locked.png'),
-                       alt="needs lock", title="needs lock")
+                       alt=_("needs lock"), title=_("needs lock"))
 
     def _render_mergeinfo(self, name, mode, context, props):
         rows = []
@@ -197,6 +197,7 @@
                 if path not in branch_starts:
                     branch_starts[path] = rev + 1
         rows = []
+        eligible_infos = []
         if name.startswith('svnmerge-'):
             sources = props[name].split()
         else:
@@ -232,9 +233,9 @@
                         if blocked:
                             eligible -= set(Ranges(blocked))
                         if eligible:
-                            nrevs = repos._get_node_revs(spath, max(eligible),
-                                                         min(eligible))
-                            eligible &= set(nrevs)
+                            node = repos.get_node(spath, max(eligible))
+                            eligible_infos.append((spath, node, eligible, row))
+                            continue
                         eligible = to_ranges(eligible)
                         row.append(_get_revs_link(_('eligible'), context,
                                                   spath, eligible))
@@ -246,6 +247,22 @@
             rows.append((deleted, spath,
                          [tag.td('/' + spath),
                           tag.td(revs, colspan=revs_cols)]))
+
+        # fetch eligible revisions for each path at a time
+        changed_revs = {}
+        changed_nodes = [(node, min(eligible))
+                         for spath, node, eligible, row in eligible_infos]
+        if changed_nodes:
+            changed_revs = repos._get_changed_revs(changed_nodes)
+        for spath, node, eligible, row in eligible_infos:
+            if spath in changed_revs:
+                eligible &= set(changed_revs[spath])
+            else:
+                eligible.clear()
+            row.append(_get_revs_link(_("eligible"), context, spath,
+                                      to_ranges(eligible)))
+            rows.append((False, spath, [tag.td(each) for each in row]))
+
         if not rows:
             return None
         rows.sort()
@@ -346,33 +363,52 @@
         removed_label = [_("reverse-merged: "), _("un-blocked: ")][blocked]
         added_ni_label = _("marked as non-inheritable: ")
         removed_ni_label = _("unmarked as non-inheritable: ")
+
+        sources = []
+        changed_revs = {}
+        changed_nodes = []
+        for spath, (new_revs, new_revs_ni) in new_sources.iteritems():
+            new_spath = spath not in old_sources
+            if new_spath:
+                old_revs = old_revs_ni = set()
+            else:
+                old_revs, old_revs_ni = old_sources.pop(spath)
+            added = new_revs - old_revs
+            removed = old_revs - new_revs
+            # unless new revisions differ from old revisions
+            if not added and not removed:
+                continue
+            added_ni = new_revs_ni - old_revs_ni
+            removed_ni = old_revs_ni - new_revs_ni
+            revs = sorted(added | removed | added_ni | removed_ni)
+            try:
+                node = repos.get_node(spath, revs[-1])
+                changed_nodes.append((node, revs[0]))
+            except NoSuchNode:
+                pass
+            sources.append((spath, new_spath, added, removed, added_ni,
+                            removed_ni))
+        if changed_nodes:
+            changed_revs = repos._get_changed_revs(changed_nodes)
+
         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
+        for spath, new_spath, added, removed, added_ni, removed_ni in sources:
+            if spath in changed_revs:
+                revs = set(changed_revs[spath])
+                added &= revs
+                removed &= revs
             if added or removed:
+                added_ni &= revs
+                removed_ni &= revs
+                if new_spath:
+                    status = _(" (added)")
+                else:
+                    status = None
                 modified_sources.append((
                     spath, [_get_source_link(spath, new_context), status],
                     added and tag(added_label, revs_link(added, new_context)),
diff --git a/trac/tracopt/versioncontrol/svn/tests/__init__.py b/trac/tracopt/versioncontrol/svn/tests/__init__.py
index 32cf6bd..4f228c8 100644
--- a/trac/tracopt/versioncontrol/svn/tests/__init__.py
+++ b/trac/tracopt/versioncontrol/svn/tests/__init__.py
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012-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 unittest
 
 from tracopt.versioncontrol.svn.tests import svn_fs
diff --git a/trac/tracopt/versioncontrol/svn/tests/svn_fs.py b/trac/tracopt/versioncontrol/svn/tests/svn_fs.py
index 531ffde..74f80a8 100644
--- a/trac/tracopt/versioncontrol/svn/tests/svn_fs.py
+++ b/trac/tracopt/versioncontrol/svn/tests/svn_fs.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (C)2005-2009 Edgewall Software
+# Copyright (C) 2005-2013 Edgewall Software
 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
 # All rights reserved.
 #
@@ -17,8 +17,6 @@
 from datetime import datetime
 import new
 import os.path
-import stat
-import shutil
 import tempfile
 import unittest
 
@@ -30,20 +28,35 @@
 except ImportError:
     has_svn = False
 
-from trac.test import EnvironmentStub, TestSetup
+from genshi.core import Stream
+
+import trac.tests.compat
+from trac.test import EnvironmentStub, Mock, MockPerm, TestSetup
 from trac.core import TracError
+from trac.mimeview.api import Context
 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
+from trac.versioncontrol.api import DbRepositoryProvider, Changeset, Node, \
+                                    NoSuchChangeset, RepositoryManager
+from trac.versioncontrol import svn_fs, svn_prop
+from trac.web.href import Href
 
-REPOS_PATH = os.path.join(tempfile.gettempdir(), 'trac-svnrepos')
+REPOS_PATH = None
 REPOS_NAME = 'repo'
+URL = 'svn://test'
 
-HEAD = 22
-TETE = 21
+HEAD = 29
+TETE = 26
+
+NATIVE_EOL = '\r\n' if os.name == 'nt' else '\n'
+
+
+def _create_context():
+    req = Mock(base_path='', chrome={}, args={}, session={},
+               abs_href=Href('/'), href=Href('/'), locale=None,
+               perm=MockPerm(), authname='anonymous', tz=utc)
+    return Context.from_request(req)
 
 
 class SubversionRepositoryTestSetup(TestSetup):
@@ -57,8 +70,6 @@
         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(),
@@ -85,14 +96,14 @@
 
     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')))
+        self.assertTrue(resource_exists(self.env, repos))
+        self.assertFalse(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')))
+        self.assertTrue(resource_exists(self.env, node))
+        self.assertFalse(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)))
+        self.assertTrue(resource_exists(self.env, cset))
+        self.assertFalse(resource_exists(self.env, cset(id=123456)))
 
     def test_repos_normalize_path(self):
         self.assertEqual('/', self.repos.normalize_path('/'))
@@ -115,42 +126,49 @@
 
     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.assertIsNone(self.repos.previous_rev(0))
+        self.assertIsNone(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.assertIsNone(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.assertIsNone(self.repos.previous_rev(0, u'tête'))
+        self.assertIsNone(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'))
+        self.assertIsNone(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.assertIsNone(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'))
+        self.assertFalse(self.repos.has_node(u'/tête/dir1', 3))
+        self.assertTrue(self.repos.has_node(u'/tête/dir1', 4))
+        self.assertTrue(self.repos.has_node(u'/tête/dir1'))
 
     def test_get_node(self):
+        node = self.repos.get_node(u'/')
+        self.assertEqual(u'', node.name)
+        self.assertEqual(u'/', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(HEAD, node.rev)
+        self.assertEqual(HEAD, node.created_rev)
+        self.assertEqual(datetime(2014, 4, 14, 16, 49, 44, 990695, utc),
+                         node.last_modified)
         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),
+        self.assertEqual(datetime(2013, 4, 28, 5, 36, 6, 29637, utc),
                          node.last_modified)
         node = self.repos.get_node(u'/tête/README.txt')
         self.assertEqual('README.txt', node.name)
@@ -195,9 +213,9 @@
 
     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())
+        self.assertIsNone(node.content_length)
+        self.assertIsNone(node.content_type)
+        self.assertIsNone(node.get_content())
 
     def test_get_file_content(self):
         node = self.repos.get_node(u'/tête/README.txt')
@@ -216,6 +234,141 @@
         self.assertEqual('native', props['svn:eol-style'])
         self.assertEqual('text/plain', props['svn:mime-type'])
 
+    def test_get_file_content_without_native_eol_style(self):
+        f = self.repos.get_node(u'/tête/README.txt', 2)
+        props = f.get_properties()
+        self.assertIsNone(props.get('svn:eol-style'))
+        self.assertEqual('A text.\n', f.get_content().read())
+        self.assertEqual('A text.\n', f.get_processed_content().read())
+
+    def test_get_file_content_with_native_eol_style(self):
+        f = self.repos.get_node(u'/tête/README.txt', 3)
+        props = f.get_properties()
+        self.assertEqual('native', props.get('svn:eol-style'))
+
+        self.repos.params['eol_style'] = 'native'
+        self.assertEqual('A test.\n', f.get_content().read())
+        self.assertEqual('A test.' + NATIVE_EOL,
+                         f.get_processed_content().read())
+
+        self.repos.params['eol_style'] = 'LF'
+        self.assertEqual('A test.\n', f.get_content().read())
+        self.assertEqual('A test.\n', f.get_processed_content().read())
+
+        self.repos.params['eol_style'] = 'CRLF'
+        self.assertEqual('A test.\n', f.get_content().read())
+        self.assertEqual('A test.\r\n', f.get_processed_content().read())
+
+        self.repos.params['eol_style'] = 'CR'
+        self.assertEqual('A test.\n', f.get_content().read())
+        self.assertEqual('A test.\r', f.get_processed_content().read())
+        # check that the hint is stronger than the repos default
+        self.assertEqual('A test.\r\n',
+                         f.get_processed_content(eol_hint='CRLF').read())
+
+    def test_get_file_content_with_native_eol_style_and_no_keywords_28(self):
+        f = self.repos.get_node(u'/branches/v4/README.txt', 28)
+        props = f.get_properties()
+        self.assertEqual('native', props.get('svn:eol-style'))
+        self.assertIsNone(props.get('svn:keywords'))
+
+        self.assertEqual(
+            'A test.\n' +
+            '# $Rev$ is not substituted with no svn:keywords.\n',
+            f.get_content().read())
+        self.assertEqual(
+            'A test.\r\n' +
+            '# $Rev$ is not substituted with no svn:keywords.\r\n',
+            f.get_processed_content(eol_hint='CRLF').read())
+
+    def test_get_file_content_with_keyword_substitution_23(self):
+        f = self.repos.get_node(u'/tête/Résumé.txt', 23)
+        props = f.get_properties()
+        self.assertEqual('Revision Author URL', props['svn:keywords'])
+        self.assertEqual('''\
+# Simple test for svn:keywords property substitution (#717)
+# $Rev: 23 $:     Revision of last commit
+# $Author: cboos $:  Author of last commit
+# $Date$:    Date of last commit (not substituted)
+
+Now with fixed width fields:
+# $URL:: svn://test/tête/Résumé.txt                $ the configured URL
+# $HeadURL:: svn://test/tête/Résumé.txt            $ same
+# $URL:: svn://test/tê#$ same, but truncated
+
+En r\xe9sum\xe9 ... \xe7a marche.
+''', f.get_processed_content().read())
+    # Note: "En résumé ... ça marche." in the content is really encoded in
+    #       latin1 in the file, and our substitutions are UTF-8 encoded...
+    #       This is expected.
+
+    def test_get_file_content_with_keyword_substitution_24(self):
+        f = self.repos.get_node(u'/tête/Résumé.txt', 24)
+        props = f.get_properties()
+        self.assertEqual('Revision Author URL Id', props['svn:keywords'])
+        self.assertEqual('''\
+# Simple test for svn:keywords property substitution (#717)
+# $Rev: 24 $:     Revision of last commit
+# $Author: cboos $:  Author of last commit
+# $Date$:    Date of last commit (now substituted)
+# $Id: Résumé.txt 24 2013-04-27 14:38:50Z cboos $:      Combination
+
+Now with fixed width fields:
+# $URL:: svn://test/t\xc3\xaate/R\xc3\xa9sum\xc3\xa9.txt                $ the configured URL
+# $HeadURL:: svn://test/t\xc3\xaate/R\xc3\xa9sum\xc3\xa9.txt            $ same
+# $URL:: svn://test/t\xc3\xaa#$ same, but truncated
+# $Header::                                           $ combination with URL
+
+En r\xe9sum\xe9 ... \xe7a marche.
+''', f.get_processed_content().read())
+
+    def test_get_file_content_with_keyword_substitution_25(self):
+        f = self.repos.get_node(u'/tête/Résumé.txt', 25)
+        props = f.get_properties()
+        self.assertEqual('Revision Author URL Date Id Header',
+                         props['svn:keywords'])
+        self.assertEqual('''\
+# Simple test for svn:keywords property substitution (#717)
+# $Rev: 25 $:     Revision of last commit
+# $Author: cboos $:  Author of last commit
+# $Date: 2013-04-27 14:43:15 +0000 (Sat, 27 Apr 2013) $:    Date of last commit (now really substituted)
+# $Id: Résumé.txt 25 2013-04-27 14:43:15Z cboos $:      Combination
+
+Now with fixed width fields:
+# $URL:: svn://test/tête/Résumé.txt                $ the configured URL
+# $HeadURL:: svn://test/tête/Résumé.txt            $ same
+# $URL:: svn://test/tê#$ same, but truncated
+# $Header:: svn://test/t\xc3\xaate/R\xc3\xa9sum\xc3\xa9.txt 25 2013-04-#$ combination with URL
+
+En r\xe9sum\xe9 ... \xe7a marche.
+''', f.get_processed_content().read())
+
+    def test_get_file_content_with_keyword_substitution_27(self):
+        f = self.repos.get_node(u'/tête/Résumé.txt', 27)
+        props = f.get_properties()
+        self.assertEqual('Revision Author URL Date Id Header',
+                         props['svn:keywords'])
+        self.assertEqual('''\
+# Simple test for svn:keywords property substitution (#717)
+# $Rev: 26 $:     Revision of last commit
+# $Author: jomae $:  Author of last commit
+# $Date: 2013-04-28 05:36:06 +0000 (Sun, 28 Apr 2013) $:    Date of last commit (now really substituted)
+# $Id: Résumé.txt 26 2013-04-28 05:36:06Z jomae $:      Combination
+
+Now with fixed width fields:
+# $URL:: svn://test/tête/Résumé.txt                $ the configured URL
+# $HeadURL:: svn://test/tête/Résumé.txt            $ same
+# $URL:: svn://test/tê#$ same, but truncated
+# $Header:: svn://test/t\xc3\xaate/R\xc3\xa9sum\xc3\xa9.txt 26 2013-04-#$ combination with URL
+
+Overlapped keywords:
+# $Xxx$Rev: 26 $Xxx$
+# $Rev: 26 $Xxx$Rev: 26 $
+# $Rev: 26 $Rev$Rev: 26 $
+
+En r\xe9sum\xe9 ... \xe7a marche.
+''', f.get_processed_content().read())
+
     def test_created_path_rev(self):
         node = self.repos.get_node(u'/tête/README3.txt', 15)
         self.assertEqual(15, node.rev)
@@ -230,6 +383,32 @@
         self.assertEqual(3, node.created_rev)
         self.assertEqual(u'tête/README.txt', node.created_path)
 
+    def test_get_annotations(self):
+        # svn_client_blame2() requires a canonical uri since Subversion 1.7.
+        # If the uri is not canonical, assertion raises (#11167).
+        node = self.repos.get_node(u'/tête/R\xe9sum\xe9.txt', 25)
+        self.assertEqual([23, 23, 23, 25, 24, 23, 23, 23, 23, 23, 24, 23, 20],
+                         node.get_annotations())
+
+    def test_get_annotations_lower_drive_letter(self):
+        # If the drive letter in the uri is lower case on Windows, a
+        # SubversionException raises (#10514).
+        drive, tail = os.path.splitdrive(REPOS_PATH)
+        repos_path = drive.lower() + tail
+        DbRepositoryProvider(self.env).add_repository('lowercase', repos_path,
+                                                      'direct-svnfs')
+        repos = self.env.get_repository('lowercase')
+        node = repos.get_node(u'/tête/R\xe9sum\xe9.txt', 25)
+        self.assertEqual([23, 23, 23, 25, 24, 23, 23, 23, 23, 23, 24, 23, 20],
+                         node.get_annotations())
+
+    if os.name != 'nt':
+        del test_get_annotations_lower_drive_letter
+
+    def test_get_annotations_with_urlencoded_percent_sign(self):
+        node = self.repos.get_node(u'/branches/t10386/READ%25ME.txt')
+        self.assertEqual([14], node.get_annotations())
+
     # Revision Log / node history
 
     def test_get_node_history(self):
@@ -538,6 +717,122 @@
         self.assertEqual(u'Chez moi ça marche\n', chgset.message)
         self.assertEqual(u'Jonas Borgström', chgset.author)
 
+    def test_canonical_repos_path(self):
+        # Assertion `svn_dirent_is_canonical` with leading double slashes
+        # in repository path if os.name == 'posix' (#10390)
+        DbRepositoryProvider(self.env).add_repository(
+            'canonical-path', '//' + REPOS_PATH.lstrip('/'), 'direct-svnfs')
+        repos = self.env.get_repository('canonical-path')
+        self.assertEqual(REPOS_PATH, repos.path)
+
+    if os.name != 'posix':
+        del test_canonical_repos_path
+
+    def test_merge_prop_renderer_without_deleted_branches(self):
+        context = _create_context()
+        context = context(self.repos.get_node('branches/v1x', HEAD).resource)
+        renderer = svn_prop.SubversionMergePropertyRenderer(self.env)
+        props = {'svn:mergeinfo': u"""\
+/tête:1-20,23-26
+/branches/v3:22
+/branches/v2:16
+"""}
+        result = Stream(renderer.render_property('svn:mergeinfo', 'browser',
+                                                 context, props))
+
+        node = unicode(result.select('//tr[1]//td[1]'))
+        self.assertIn(' href="/browser/repo/branches/v2?rev=%d"' % HEAD, node)
+        self.assertIn('>/branches/v2</a>', node)
+        node = unicode(result.select('//tr[1]//td[2]'))
+        self.assertIn(' title="16"', node)
+        self.assertIn('>merged</a>', node)
+        node = unicode(result.select('//tr[1]//td[3]'))
+        self.assertIn(' title="No revisions"', node)
+        self.assertIn('>eligible</span>', node)
+
+        node = unicode(result.select('//tr[3]//td[1]'))
+        self.assertIn(' href="/browser/repo/%s?rev=%d"' % ('t%C3%AAte', HEAD),
+                      node)
+        self.assertIn(u'>/tête</a>', node)
+        node = unicode(result.select('//tr[3]//td[2]'))
+        self.assertIn(' title="1-20, 23-26"', node)
+        self.assertIn(' href="/log/repo/t%C3%AAte?revs=1-20%2C23-26"', node)
+        self.assertIn('>merged</a>', node)
+        node = unicode(result.select('//tr[3]//td[3]'))
+        self.assertIn(' title="21"', node)
+        self.assertIn(' href="/changeset/21/repo/t%C3%AAte"', node)
+        self.assertIn('>eligible</a>', node)
+
+        self.assertNotIn('(toggle deleted branches)', unicode(result))
+
+    def test_merge_prop_renderer_with_deleted_branches(self):
+        context = _create_context()
+        context = context(self.repos.get_node('branches/v1x', HEAD).resource)
+        renderer = svn_prop.SubversionMergePropertyRenderer(self.env)
+        props = {'svn:mergeinfo': u"""\
+/tête:19
+/branches/v3:22
+/branches/deleted:1,3-5,22
+"""}
+        result = Stream(renderer.render_property('svn:mergeinfo', 'browser',
+                                                 context, props))
+
+        node = unicode(result.select('//tr[1]//td[1]'))
+        self.assertIn(' href="/browser/repo/branches/v3?rev=%d"' % HEAD, node)
+        self.assertIn('>/branches/v3</a>', node)
+        node = unicode(result.select('//tr[1]//td[2]'))
+        self.assertIn(' title="22"', node)
+        self.assertIn('>merged</a>', node)
+        node = unicode(result.select('//tr[1]//td[3]'))
+        self.assertIn(' title="No revisions"', node)
+        self.assertIn('>eligible</span>', node)
+
+        node = unicode(result.select('//tr[2]//td[1]'))
+        self.assertIn(' href="/browser/repo/%s?rev=%d"' % ('t%C3%AAte', HEAD),
+                      node)
+        self.assertIn(u'>/tête</a>', node)
+        node = unicode(result.select('//tr[2]//td[2]'))
+        self.assertIn(' title="19"', node)
+        self.assertIn(' href="/changeset/19/repo/t%C3%AAte"', node)
+        self.assertIn('>merged</a>', node)
+        node = unicode(result.select('//tr[2]//td[3]'))
+        self.assertIn(' title="13-14, 17-18, 20-21, 23-26"', node)
+        self.assertIn(' href="/log/repo/t%C3%AAte?revs='
+                      '13-14%2C17-18%2C20-21%2C23-26"', node)
+        self.assertIn('>eligible</a>', node)
+
+        self.assertIn('(toggle deleted branches)', unicode(result))
+        self.assertIn('<td>/branches/deleted</td>',
+                      unicode(result.select('//tr[3]//td[1]')))
+        self.assertIn(u'<td colspan="2">1,\u200b3-5,\u200b22</td>',
+                      unicode(result.select('//tr[3]//td[2]')))
+
+    def test_merge_prop_diff_renderer_added(self):
+        context = _create_context()
+        old_context = context(self.repos.get_node(u'tête', 20).resource)
+        old_props = {'svn:mergeinfo': u"""\
+/branches/v2:1,8-9,12-15
+/branches/v1x:12
+/branches/deleted:1,3-5,22
+"""}
+        new_context = context(self.repos.get_node(u'tête', 21).resource)
+        new_props = {'svn:mergeinfo': u"""\
+/branches/v2:1,8-9,12-16
+/branches/v1x:12
+/branches/deleted:1,3-5,22
+"""}
+        options = {}
+        renderer = svn_prop.SubversionMergePropertyDiffRenderer(self.env)
+        result = Stream(renderer.render_property_diff(
+                'svn:mergeinfo', old_context, old_props, new_context,
+                new_props, options))
+
+        node = unicode(result.select('//tr[1]//td[1]'))
+        self.assertIn(' href="/browser/repo/branches/v2?rev=21"', node)
+        self.assertIn('>/branches/v2</a>', node)
+        node = unicode(result.select('//tr[1]//td[2]'))
+        self.assertIn(' title="16"', node)
+        self.assertIn(' href="/changeset/16/repo/branches/v2"', node)
 
 
 class ScopedTests(object):
@@ -561,17 +856,17 @@
 
     def test_rev_navigation(self):
         self.assertEqual(1, self.repos.oldest_rev)
-        self.assertEqual(None, self.repos.previous_rev(0))
+        self.assertIsNone(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))
+        self.assertIsNone(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))
+        self.assertFalse(self.repos.has_node('/dir1', 3))
+        self.assertTrue(self.repos.has_node('/dir1', 4))
 
     def test_get_node(self):
         node = self.repos.get_node('/dir1')
@@ -625,9 +920,9 @@
 
     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())
+        self.assertIsNone(node.content_length)
+        self.assertIsNone(node.content_type)
+        self.assertIsNone(node.get_content())
 
     def test_get_file_content(self):
         node = self.repos.get_node('/README.txt')
@@ -808,14 +1103,14 @@
 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))
+        self.assertFalse(self.repos.has_node('/', 1))
+        self.assertFalse(self.repos.has_node('/', 2))
+        self.assertFalse(self.repos.has_node('/', 3))
+        self.assertTrue(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))
+        self.assertIsNone(self.repos.previous_rev(4))
 
 
 class NonSelfContainedScopedTests(object):
@@ -847,11 +1142,17 @@
     def setUp(self):
         self.env = EnvironmentStub()
         repositories = self.env.config['repositories']
-        DbRepositoryProvider(self.env).add_repository(REPOS_NAME, self.path,
-                                                      'direct-svnfs')
+        dbprovider = DbRepositoryProvider(self.env)
+        dbprovider.add_repository(REPOS_NAME, self.path, 'direct-svnfs')
+        dbprovider.modify_repository(REPOS_NAME, {'url': URL})
         self.repos = self.env.get_repository(REPOS_NAME)
 
+
     def tearDown(self):
+        self.repos.close()
+        self.repos = None
+        # clear cached repositories to avoid TypeError on termination (#11505)
+        RepositoryManager(self.env).reload_repositories()
         self.env.reset_db()
         # needed to avoid issue with 'WindowsError: The process cannot access
         # the file ... being used by another process: ...\rep-cache.db'
@@ -864,8 +1165,9 @@
 
     def setUp(self):
         self.env = EnvironmentStub()
-        DbRepositoryProvider(self.env).add_repository(REPOS_NAME, self.path,
-                                                      'svn')
+        dbprovider = DbRepositoryProvider(self.env)
+        dbprovider.add_repository(REPOS_NAME, self.path, 'svn')
+        dbprovider.modify_repository(REPOS_NAME, {'url': URL})
         self.repos = self.env.get_repository(REPOS_NAME)
         self.repos.sync()
 
@@ -873,11 +1175,16 @@
         self.env.reset_db()
         self.repos.close()
         self.repos = None
+        # clear cached repositories to avoid TypeError on termination (#11505)
+        RepositoryManager(self.env).reload_repositories()
 
 
 def suite():
+    global REPOS_PATH
     suite = unittest.TestSuite()
     if has_svn:
+        REPOS_PATH = tempfile.mkdtemp(prefix='trac-svnrepos-')
+        os.rmdir(REPOS_PATH)
         tests = [(NormalTests, ''),
                  (ScopedTests, u'/tête'),
                  (RecentPathScopedTests, u'/tête/dir1'),
@@ -898,19 +1205,18 @@
                               (SubversionRepositoryTestCase, test),
                               {'path': REPOS_PATH + scope})
             suite.addTest(unittest.makeSuite(
-                tc, 'test', suiteClass=SubversionRepositoryTestSetup))
+                tc, 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))
+                tc, 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())
+    unittest.main(defaultTest='suite')
diff --git a/trac/tracopt/versioncontrol/svn/tests/svnrepos.dump b/trac/tracopt/versioncontrol/svn/tests/svnrepos.dump
index 019233e..8d16b04 100644
--- a/trac/tracopt/versioncontrol/svn/tests/svnrepos.dump
+++ b/trac/tracopt/versioncontrol/svn/tests/svnrepos.dump
@@ -16,10 +16,6 @@
 Prop-content-length: 124
 Content-length: 124
 
-K 7
-svn:log
-V 25
-Initial directory layout.
 K 10
 svn:author
 V 4
@@ -28,6 +24,10 @@
 svn:date
 V 27
 2005-04-01T10:00:52.353248Z
+K 7
+svn:log
+V 25
+Initial directory layout.
 PROPS-END
 
 Node-path: branches
@@ -61,10 +61,6 @@
 Prop-content-length: 112
 Content-length: 112
 
-K 7
-svn:log
-V 13
-Added README.
 K 10
 svn:author
 V 4
@@ -73,6 +69,10 @@
 svn:date
 V 27
 2005-04-01T13:12:18.216267Z
+K 7
+svn:log
+V 13
+Added README.
 PROPS-END
 
 Node-path: tête/README.txt
@@ -92,11 +92,6 @@
 Prop-content-length: 113
 Content-length: 113
 
-K 7
-svn:log
-V 14
-Fixed README.
-
 K 10
 svn:author
 V 4
@@ -105,6 +100,11 @@
 svn:date
 V 27
 2005-04-01T13:24:58.234643Z
+K 7
+svn:log
+V 14
+Fixed README.
+
 PROPS-END
 
 Node-path: tête/README.txt
@@ -117,13 +117,13 @@
 Content-length: 83
 
 K 13
-svn:mime-type
-V 10
-text/plain
-K 13
 svn:eol-style
 V 6
 native
+K 13
+svn:mime-type
+V 10
+text/plain
 PROPS-END
 A test.
 
@@ -132,10 +132,6 @@
 Prop-content-length: 116
 Content-length: 116
 
-K 7
-svn:log
-V 17
-More directories.
 K 10
 svn:author
 V 4
@@ -144,6 +140,10 @@
 svn:date
 V 27
 2005-04-01T15:42:35.450595Z
+K 7
+svn:log
+V 17
+More directories.
 PROPS-END
 
 Node-path: tête/dir1
@@ -177,10 +177,6 @@
 Prop-content-length: 117
 Content-length: 117
 
-K 7
-svn:log
-V 18
-Moved directories.
 K 10
 svn:author
 V 4
@@ -189,6 +185,10 @@
 svn:date
 V 27
 2005-04-01T16:25:39.658099Z
+K 7
+svn:log
+V 18
+Moved directories.
 PROPS-END
 
 Node-path: tête/dir1/dir2
@@ -217,10 +217,6 @@
 Prop-content-length: 118
 Content-length: 118
 
-K 7
-svn:log
-V 19
-More things to read
 K 10
 svn:author
 V 4
@@ -229,6 +225,10 @@
 svn:date
 V 27
 2005-04-01T18:56:46.985846Z
+K 7
+svn:log
+V 19
+More things to read
 PROPS-END
 
 Node-path: tête/README2.txt
@@ -244,10 +244,6 @@
 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
@@ -256,6 +252,10 @@
 svn:date
 V 27
 2005-04-14T15:06:20.717616Z
+K 7
+svn:log
+V 42
+test the tag operation (copy of directory)
 PROPS-END
 
 Node-path: tags/v1
@@ -269,10 +269,6 @@
 Prop-content-length: 124
 Content-length: 124
 
-K 7
-svn:log
-V 15
-Fix stuff in v1
 K 10
 svn:author
 V 13
@@ -281,6 +277,10 @@
 svn:date
 V 27
 2005-04-22T08:57:33.499643Z
+K 7
+svn:log
+V 15
+Fix stuff in v1
 PROPS-END
 
 Node-path: branches/v1x
@@ -294,10 +294,6 @@
 Prop-content-length: 127
 Content-length: 127
 
-K 7
-svn:log
-V 18
-Now that's the fix
 K 10
 svn:author
 V 13
@@ -306,6 +302,10 @@
 svn:date
 V 27
 2005-04-22T08:59:24.308979Z
+K 7
+svn:log
+V 18
+Now that's the fix
 PROPS-END
 
 Node-path: branches/v1x/README.txt
@@ -323,10 +323,6 @@
 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
@@ -335,6 +331,10 @@
 svn:date
 V 27
 2005-04-22T09:00:34.549980Z
+K 7
+svn:log
+V 32
+Tagging v1.1 from the fix branch
 PROPS-END
 
 Node-path: tags/v1.1
@@ -348,10 +348,6 @@
 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
@@ -360,6 +356,10 @@
 svn:date
 V 27
 2005-04-22T09:01:38.361737Z
+K 7
+svn:log
+V 82
+''(a few months later)'' We don't need the fix branch anymore, 1.1 is super-stable
 PROPS-END
 
 Node-path: branches/v1x
@@ -370,10 +370,6 @@
 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
@@ -382,6 +378,10 @@
 svn:date
 V 27
 2005-04-22T09:06:37.011174Z
+K 7
+svn:log
+V 57
+''(a few years later)'' Argh... v1.1 was buggy, after all
 PROPS-END
 
 Node-path: branches/v1x
@@ -395,10 +395,6 @@
 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
@@ -407,6 +403,10 @@
 svn:date
 V 27
 2005-11-17T15:13:16.197772Z
+K 7
+svn:log
+V 43
+Setting property on the repository_dir root
 PROPS-END
 
 Node-path: 
@@ -441,10 +441,6 @@
 Prop-content-length: 119
 Content-length: 119
 
-K 7
-svn:log
-V 19
-Testing rename+edit
 K 10
 svn:author
 V 5
@@ -453,6 +449,10 @@
 svn:date
 V 27
 2005-11-30T08:47:03.467814Z
+K 7
+svn:log
+V 19
+Testing rename+edit
 PROPS-END
 
 Node-path: tête/README3.txt
@@ -478,10 +478,6 @@
 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
@@ -490,6 +486,10 @@
 svn:date
 V 27
 2005-12-06T12:47:36.271020Z
+K 7
+svn:log
+V 62
+Removing original file, just before committing the wc->wc copy
 PROPS-END
 
 Node-path: tags/v1.1/README2.txt
@@ -500,10 +500,6 @@
 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
@@ -512,6 +508,10 @@
 svn:date
 V 27
 2005-12-06T12:47:51.122376Z
+K 7
+svn:log
+V 38
+Committing wc->wc copy + local changes
 PROPS-END
 
 Node-path: branches/v2
@@ -536,10 +536,6 @@
 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
@@ -548,6 +544,10 @@
 svn:date
 V 27
 2006-03-31T12:30:25.421875Z
+K 7
+svn:log
+V 39
+Test des caractères accentués (cp437)
 PROPS-END
 
 Node-path: tête/Résumé.txt
@@ -566,10 +566,6 @@
 Prop-content-length: 124
 Content-length: 124
 
-K 7
-svn:log
-V 24
-Prepare for fancy rename
 K 10
 svn:author
 V 5
@@ -578,6 +574,10 @@
 svn:date
 V 27
 2006-09-27T10:40:38.671875Z
+K 7
+svn:log
+V 24
+Prepare for fancy rename
 PROPS-END
 
 Node-path: tête/Xprimary_proc
@@ -606,10 +606,6 @@
 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
@@ -618,6 +614,10 @@
 svn:date
 V 27
 2006-09-27T10:41:27.484375Z
+K 7
+svn:log
+V 45
+Fancy copy+rename resulting in double delete.
 PROPS-END
 
 Node-path: tête/mpp_proc
@@ -650,11 +650,6 @@
 Prop-content-length: 132
 Content-length: 132
 
-K 7
-svn:log
-V 20
-Chez moi ça marche
-
 K 10
 svn:author
 V 16
@@ -663,6 +658,11 @@
 svn:date
 V 27
 2006-12-04T10:47:24.015625Z
+K 7
+svn:log
+V 20
+Chez moi ça marche
+
 PROPS-END
 
 Node-path: tête/Résumé.txt
@@ -680,10 +680,6 @@
 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
@@ -692,6 +688,10 @@
 svn:date
 V 27
 2007-04-30T17:45:26.234375Z
+K 7
+svn:log
+V 39
+copy from outside of the scope + delete
 PROPS-END
 
 Node-path: tête/v2
@@ -713,10 +713,6 @@
 Prop-content-length: 130
 Content-length: 130
 
-K 7
-svn:log
-V 30
-test delete after copy (#4900)
 K 10
 svn:author
 V 5
@@ -725,6 +721,10 @@
 svn:date
 V 27
 2010-02-26T14:48:19.377000Z
+K 7
+svn:log
+V 30
+test delete after copy (#4900)
 PROPS-END
 
 Node-path: branches/v3
@@ -746,3 +746,295 @@
 Node-action: delete
 
 
+Revision-number: 23
+Prop-content-length: 137
+Content-length: 137
+
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2013-04-27T13:00:34.579240Z
+K 7
+svn:log
+V 37
+Verifying keyword substitution (#717)
+PROPS-END
+
+Node-path: tête/Résumé.txt
+Node-kind: file
+Node-action: change
+Prop-content-length: 53
+Text-content-length: 421
+Text-content-md5: c9a55f49668aff4b30606a4821f13c3a
+Text-content-sha1: 1b7c636fcaed8360bbf39589655b4e5b37b4ea9a
+Content-length: 474
+
+K 12
+svn:keywords
+V 19
+Revision Author URL
+PROPS-END
+# Simple test for svn:keywords property substitution (#717)
+# $Rev$:     Revision of last commit
+# $Author$:  Author of last commit
+# $Date$:    Date of last commit (not substituted)
+
+Now with fixed width fields:
+# $URL::                                              $ the configured URL
+# $HeadURL::                                          $ same
+# $URL::                $ same, but truncated
+
+En résumé ... ça marche.
+
+
+Revision-number: 24
+Prop-content-length: 144
+Content-length: 144
+
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2013-04-27T14:38:50.346459Z
+K 7
+svn:log
+V 44
+Adding Id keyword and testing Id and Header.
+PROPS-END
+
+Node-path: tête/Résumé.txt
+Node-kind: file
+Node-action: change
+Prop-content-length: 56
+Text-content-length: 523
+Text-content-md5: 2387ca7586289babae8f3714750677b6
+Text-content-sha1: 983c9d507f95133315e9a8942a1a9161e69a0644
+Content-length: 579
+
+K 12
+svn:keywords
+V 22
+Revision Author URL Id
+PROPS-END
+# Simple test for svn:keywords property substitution (#717)
+# $Rev$:     Revision of last commit
+# $Author$:  Author of last commit
+# $Date$:    Date of last commit (now substituted)
+# $Id$:      Combination
+
+Now with fixed width fields:
+# $URL::                                              $ the configured URL
+# $HeadURL::                                          $ same
+# $URL::                $ same, but truncated
+# $Header::                                           $ combination with URL
+
+En résumé ... ça marche.
+
+
+Revision-number: 25
+Prop-content-length: 132
+Content-length: 132
+
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2013-04-27T14:43:15.010597Z
+K 7
+svn:log
+V 32
+Adding Header and Date keywords.
+PROPS-END
+
+Node-path: tête/Résumé.txt
+Node-kind: file
+Node-action: change
+Prop-content-length: 68
+Text-content-length: 530
+Text-content-md5: 6f322fe2e36a5340ab89a45c5e1a99ea
+Text-content-sha1: 796511deb4b792770ffa96c72665d04013e7e351
+Content-length: 598
+
+K 12
+svn:keywords
+V 34
+Revision Author URL Date Id Header
+PROPS-END
+# Simple test for svn:keywords property substitution (#717)
+# $Rev$:     Revision of last commit
+# $Author$:  Author of last commit
+# $Date$:    Date of last commit (now really substituted)
+# $Id$:      Combination
+
+Now with fixed width fields:
+# $URL::                                              $ the configured URL
+# $HeadURL::                                          $ same
+# $URL::                $ same, but truncated
+# $Header::                                           $ combination with URL
+
+En résumé ... ça marche.
+
+
+Revision-number: 26
+Prop-content-length: 99
+Content-length: 99
+
+K 10
+svn:author
+V 5
+jomae
+K 8
+svn:date
+V 27
+2013-04-28T05:36:06.029637Z
+K 7
+svn:log
+V 0
+
+PROPS-END
+
+Node-path: tête/Résumé.txt
+Node-kind: file
+Node-action: change
+Text-content-length: 600
+Text-content-md5: 72f0bd05783567014a5c9b5b25624bd5
+Text-content-sha1: c7c17825559c15a3ac12cfdbc25c3bfeecc33444
+Content-length: 600
+
+# Simple test for svn:keywords property substitution (#717)
+# $Rev$:     Revision of last commit
+# $Author$:  Author of last commit
+# $Date$:    Date of last commit (now really substituted)
+# $Id$:      Combination
+
+Now with fixed width fields:
+# $URL::                                              $ the configured URL
+# $HeadURL::                                          $ same
+# $URL::                $ same, but truncated
+# $Header::                                           $ combination with URL
+
+Overlapped keywords:
+# $Xxx$Rev$Xxx$
+# $Rev$Xxx$Rev$
+# $Rev$Rev$Rev$
+
+En résumé ... ça marche.
+
+
+Revision-number: 27
+Prop-content-length: 99
+Content-length: 99
+
+K 10
+svn:author
+V 5
+jomae
+K 8
+svn:date
+V 27
+2013-04-28T06:04:22.547569Z
+K 7
+svn:log
+V 0
+
+PROPS-END
+
+Node-path: 
+Node-kind: dir
+Node-action: change
+Prop-content-length: 40
+Content-length: 40
+
+K 10
+svn:ignore
+V 9
+*.py[co]
+
+PROPS-END
+
+
+Revision-number: 28
+Prop-content-length: 99
+Content-length: 99
+
+K 10
+svn:author
+V 5
+jomae
+K 8
+svn:date
+V 27
+2013-05-02T14:15:35.530857Z
+K 7
+svn:log
+V 0
+
+PROPS-END
+
+Node-path: branches/v4
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 27
+Node-copyfrom-path: tête
+
+
+Node-path: branches/v4/README.txt
+Node-kind: file
+Node-action: change
+Text-content-length: 57
+Text-content-md5: 59d8741096e01b80360963223f5c7394
+Text-content-sha1: f0c8bf5fedb3b2c06381e8dba6957a7d36667027
+Content-length: 57
+
+A test.
+# $Rev$ is not substituted with no svn:keywords.
+
+
+Revision-number: 29
+Prop-content-length: 134
+Content-length: 134
+
+K 10
+svn:author
+V 5
+jomae
+K 8
+svn:date
+V 27
+2014-04-14T16:49:44.990695Z
+K 7
+svn:log
+V 34
+blame with urlencoded percent sign
+PROPS-END
+
+Node-path: branches/t10386
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 28
+Node-copyfrom-path: tête
+
+
+Node-path: branches/t10386/READ%25ME.txt
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 28
+Node-copyfrom-path: tête/README3.txt
+Text-copy-source-md5: 211b820b566541dd49a1283d6476d89f
+Text-copy-source-sha1: 7e9f46800519d6ae305f44cc07bd7568be3c9d5c
+
+
+Node-path: branches/t10386/README3.txt
+Node-action: delete
+
+