Mercurial > hg > trac > jungle > src > mercurial-plugin
changeset 0:a97b17f1bac7 0.12
TracMercurial: started new Mercurial repository
branch:0.12 was initialized from:
{{{
svn export https://svn.edgewall.org/repos/trac/plugins/0.12/mercurial-plugin
}}}
at revision r10936.
i.e. http://trac.edgewall.org/changeset/10936/plugins/0.12/mercurial-plugin
author | Christian Boos <christian.boos@free.fr> |
---|---|
date | Sat, 09 Jun 2012 19:27:02 +0200 |
parents | |
children | b30690538379 dcdc4d05f5fa |
files | COPYING README setup.cfg setup.py tracext/__init__.py tracext/hg/__init__.py tracext/hg/backend.py tracext/hg/hooks.py tracext/hg/locale/fr/LC_MESSAGES/tracmercurial.po tracext/hg/locale/messages.pot |
diffstat | 10 files changed, 2099 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/COPYING Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,94 @@ += Mercurial Plugin for Trac (#1847) = + +Please see the online Wiki page for this extension: + + http://trac.edgewall.org/wiki/TracMercurial + +== Minimal Installation Instructions == + +=== Trac === + +This plugin for Trac 0.12 doesn't yet work with Trac ''trunk'', +but requires the ''multirepos'' branch (to be integrated in trunk +shortly): +{{{ +svn co http://svn.edgewall.com/repos/trac/sandbox/multirepos +}}} + +and install from there: +{{{ +$ cd multirepos +$ python setup.py egg_info +$ python setup.py install +}}} + +=== TracMercurial === + +Go into the working copy root (i.e. the folder containing this README file), +then: + - either do `python setup.py bdist_egg`, which creates + an .egg file in the `dist` subfolder. Copy that .egg file in the + `plugins` subfolder of your Trac environment + - or do `python setup.py install`, which creates the .egg and install it + below the site-packages folder of your Python installation. + You'll then need to enable the plugin in the `[components]` section of + your trac.ini file, see below. + + +== Configuration == + +The configuration has to be done on the Trac side, +there's nothing to do on the Mercurial repository side, +except for the fact that the repository should be made +accessible as a local repository. +Thanks to the distributed nature of Mercurial, that's +always possible (if the repository is not already local, +simply `hg clone` it). + + +=== Setting up the mercurial plugin === + +For general instructions about plugins, see also TracPlugins. + +If you installed the egg globally and you're modifying an +existing Trac environment to use the Mercurial backend, +then you have to explicitely ''enable'' the plugin in TracIni: +{{{ +[components] +tracext.hg.* = enabled +}}} + + +=== Setting up a Trac environment === + +It is now recommended to use the new set of trac-admin commands +`repository ...` to create, delete and rename references to repositories +(note that Trac will never do anything to the actual Mercurial repositories +beyond accessing them for read operations). + +The old default settings for defining the default repository are still +supported but deprecated (use an empty repository name or create an empty +alias, see `trac-admin help repository alias`). + +(Old-style) the trac.ini file contains `[trac]` section similar to the +following: +{{{ +[trac] +repository_type = hg +repository_dir = /path/to/my/hg/repository +}}} + +There's also a few Mercurial specific settings in TracIni: +{{{ +[hg] +# -- Show revision number in addition to the changeset hash +show_rev = yes + +# -- Changeset hash format +node_format = short +# hex: Show the full SHA1 hash +# short: Show a shortened hash for the changesets +}}} + + +'' -- ChristianBoos ''
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.cfg Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,24 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true + +[extract_messages] +add_comments = TRANSLATOR: +msgid_bugs_address = cboos@edgewall.org +output_file = tracext/hg/locale/messages.pot +keywords = _ ngettext:1,2 N_ tag_ +width = 72 + +[init_catalog] +input_file = tracext/hg/locale/messages.pot +output_dir = tracext/hg/locale +domain = tracmercurial + +[compile_catalog] +directory = tracext/hg/locale +domain = tracmercurial + +[update_catalog] +input_file = tracext/hg/locale/messages.pot +output_dir = tracext/hg/locale +domain = tracmercurial
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- coding: iso-8859-1 -*- +# +# Copyright (C) 2005-2012 Edgewall Software +# Copyright (C) 2005-2012 Christian Boos <cboos@edgewall.org> +# All rights reserved. +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. +# +# Author: Christian Boos <cboos@edgewall.org> + +from setuptools import setup, find_packages + +extra = {} + +try: + import babel + + extra['message_extractors'] = { + 'tracext': [ + ('**.py', 'python', None), + ], + } + + from trac.util.dist import get_l10n_cmdclass + extra['cmdclass'] = get_l10n_cmdclass() + +except ImportError: + pass + +TracMercurial = 'http://trac.edgewall.org/wiki/TracMercurial' + +setup(name='TracMercurial', + install_requires='Trac >=0.12dev-r9125', + description='Mercurial plugin for Trac multirepos branch', + keywords='trac scm plugin mercurial hg', + version='0.12.0.29', + url=TracMercurial, + license='GPL', + author='Christian Boos', + author_email='cboos@edgewall.org', + long_description=""" + This plugin for Trac 0.12 provides support for the Mercurial SCM. + + See %s for more details. + """ % TracMercurial, + namespace_packages=['tracext'], + packages=['tracext', 'tracext.hg'], + package_data={ + '': ['COPYING', 'README'], + 'tracext.hg': ['locale/*.*', 'locale/*/LC_MESSAGES/*.*'], + }, + entry_points={'trac.plugins': 'hg = tracext.hg.backend'}, + **extra)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tracext/__init__.py Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,1 @@ +__import__('pkg_resources').declare_namespace(__name__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tracext/hg/__init__.py Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,2 @@ + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tracext/hg/backend.py Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,1242 @@ +# -*- coding: iso-8859-1 -*- +# +# Copyright (C) 2005-2012 Edgewall Software +# Copyright (C) 2005-2012 Christian Boos <cboos@edgewall.org> +# All rights reserved. +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://trac.edgewall.org/log/. +# +# Author: Christian Boos <cboos@edgewall.org> + +from bisect import bisect +from datetime import datetime +import os +import time +import posixpath +import re +import sys + +import pkg_resources + +from genshi.builder import tag + +from trac.core import * +from trac.config import BoolOption, ChoiceOption, ListOption, PathOption +from trac.env import ISystemInfoProvider +from trac.util import arity +from trac.util.datefmt import FixedOffset, utc +from trac.util.text import exception_to_unicode, shorten_line, to_unicode +from trac.util.translation import domain_functions +from trac.versioncontrol.api import Changeset, Node, Repository, \ + IRepositoryConnector, RepositoryManager, \ + NoSuchChangeset, NoSuchNode +from trac.versioncontrol.web_ui import IPropertyRenderer, RenderedProperty +from trac.wiki import IWikiSyntaxProvider + +# -- plugin i18n + +gettext, _, tag_, N_, add_domain = \ + domain_functions('tracmercurial', + ('gettext', '_', 'tag_', 'N_', 'add_domain')) + +# -- Using internal Mercurial API, see: +# * http://mercurial.selenic.com/wiki/MercurialApi +# * http://mercurial.selenic.com/wiki/ApiChanges + +hg_import_error = [] +try: + # The new `demandimport` mechanism doesn't play well with code relying + # on the `ImportError` exception being caught. + # OTOH, we can't disable `demandimport` because mercurial relies on it + # (circular reference issue). So for now, we activate `demandimport` + # before loading mercurial modules, and desactivate it afterwards. + # + # See http://www.selenic.com/mercurial/bts/issue605 + + try: + from mercurial import demandimport + demandimport.enable(); + except ImportError, hg_import_error: + demandimport = None + + from mercurial import hg + from mercurial.context import filectx + from mercurial.ui import ui + from mercurial.node import hex, short, nullid, nullrev + from mercurial.util import pathto, cachefunc + from mercurial import cmdutil + from mercurial import encoding + from mercurial import extensions + from mercurial.extensions import loadall + from mercurial.error import RepoLookupError + + # Note: due to the nature of demandimport, there will be no actual + # import error until those symbols get accessed, so here we go: + for sym in ("filectx ui hex short nullid pathto " + "cachefunc loadall".split()): + if repr(globals()[sym]) == "<unloaded module '%s'>" % sym: + hg_import_error.append(sym) + if hg_import_error: + hg_import_error = "Couldn't import symbols: "+','.join(hg_import_error) + + # Mercurial versions >= 1.2 won't have mercurial.repo.RepoError anymore + from mercurial.repo import RepoError + from mercurial.revlog import LookupError as HgLookupError + if repr(RepoError) == "<unloaded module 'RepoError'>": + from mercurial.error import RepoError, LookupError as HgLookupError + + # Force local encoding to be non-lossy (#7217) + os.environ['HGENCODING'] = 'utf-8' + encoding.tolocal = str + + if demandimport: + demandimport.disable() + + # API compatibility (watch http://mercurial.selenic.com/wiki/ApiChanges) + + if hasattr(cmdutil, 'match'): + def match(ctx, *args, **kwargs): + return cmdutil.match(ctx._repo, *args, **kwargs) + else: + from mercurial.scmutil import match + + +except ImportError, e: + hg_import_error = e + ui = object + + +### Helpers + +def checked_encode(u, encodings, check): + """Convert `unicode` to `str` trying several encodings until a + condition is met. + + :param u: the `unicode` input + :param encodings: the list of possible encodings + :param check: the predicate to satisfy + + :return: the first converted `str` if `check(s)` is `True`, + otherwise `None`. Note that if no encoding is able to + successfully convert the input, the empty string will be + given to `check`, which can accept it as valid or not. + """ + s = u + if isinstance(u, unicode): + for enc in encodings: + try: + s = u.encode(enc) + if check(s): + return s + except UnicodeEncodeError: + pass + else: + s = '' + if check(s): + return s + + +class trac_ui(ui): + # Note: will be dropped in 0.13, see MercurialConnector._setup_ui + def __init__(self, *args, **kwargs): + ui.__init__(self, *args) + self.setconfig('ui', 'interactive', 'off') + self.log = kwargs.get('log', args and args[0].log or None) + + def write(self, *args, **opts): + for a in args: + self.log.info('(mercurial status) %s', a) + + def write_err(self, *args, **opts): + for a in args: + self.log.warn('(mercurial warning) %s', a) + + def plain(self, *args, **kw): + return False # so that '[hg] hgrc' file can specify [ui] options + + def interactive(self): + return False + + def readline(self): + raise TracError('*** Mercurial ui.readline called ***') + + +### Components + +class CsetPropertyRenderer(Component): + + implements(IPropertyRenderer) + + def match_property(self, name, mode): + return (name.startswith('hg-') and + name[3:] in ('Parents', 'Children', 'Tags', 'Branch') and + mode == 'revprop') and 4 or 0 + + def render_property(self, name, mode, context, props): + return RenderedProperty(name=gettext(name[3:] + ':'), + name_attributes=[("class", "property")], + content=self._render_property(name, mode, context, props)) + + def _render_property(self, name, mode, context, props): + repos, revs = props[name] + + if name in ('hg-Parents', 'hg-Children'): + label = repos.display_rev + else: + label = lambda rev: rev + + def link(rev): + chgset = repos.get_changeset(rev) + return tag.a(label(rev), class_="changeset", + title=shorten_line(chgset.message), + href=context.href.changeset(rev, repos.reponame)) + + if name == 'hg-Parents' and len(revs) == 2: # merge + new = context.resource.id + parent_links = [ + (link(rev), ' (', + tag.a('diff', title=_("Diff against this parent " + "(show the changes merged from the other parents)"), + href=context.href.changeset(new, repos.reponame, + old=rev)), ')') + for rev in revs] + return tag([(parent, ', ') for parent in parent_links[:-1]], + parent_links[-1], tag.br(), + tag.span(tag_("Note: this is a %(merge)s changeset, " + "the changes displayed below correspond " + "to the merge itself.", + merge=tag.strong('merge')), + class_='hint'), tag.br(), + # TODO: only keep chunks present in both parents + # (conflicts) or in none (extra changes) + # tag.span('No changes means the merge was clean.', + # class_='hint'), tag.br(), + tag.span(tag_("Use the %(diff)s links above to see all " + "the changes relative to each parent.", + diff=tag.tt('(diff)')), + class_='hint')) + return tag([tag(link(rev), ', ') for rev in revs[:-1]], + link(revs[-1])) + + +class HgExtPropertyRenderer(Component): + + implements(IPropertyRenderer) + + def match_property(self, name, mode): + return name in ('hg-transplant_source', 'hg-convert_revision') and \ + mode == 'revprop' and 4 or 0 + + def render_property(self, name, mode, context, props): + repos, value = props[name] + if name == 'hg-transplant_source': + try: + ctx = self.changectx(value) + chgset = MercurialChangeset(repos, ctx) + href = context.href.changeset(ctx.hex(), repos.reponame) + link = tag.a(repos._display(ctx), class_="changeset", + title=shorten_line(chgset.message), href=href) + except NoSuchChangeset: + link = tag.a(hex(value), class_="missing changeset", + title=_("no such changeset"), rel="nofollow") + return RenderedProperty(name=_("Transplant:"), content=link, + name_attributes=[("class", "property")]) + + elif name == 'hg-convert_revision': + text = repos.to_u(value) + if value.startswith('svn:'): + # e.g. 'svn:af82e41b-90c4-0310-8c96-b1721e28e2e2/trunk@9517' + uuid = value[:40] + rev = value.rsplit('@', 1)[-1] + for r in RepositoryManager(self.env).get_real_repositories(): + if r.name.startswith(uuid + ':'): + path = r.reponame + href = context.href.changeset(rev, path or None) + text = tag.a('[%s%s]' % (rev, path and '/' + path), + class_='changeset', href=href, + title=_('Changeset in source repository')) + break + return RenderedProperty(name=_('Convert:'), content=text, + name_attributes=[("class", "property")]) + + +class HgDefaultPropertyRenderer(Component): + + implements(IPropertyRenderer) + + def match_property(self, name, mode): + return name.startswith('hg-') and mode == 'revprop' and 1 or 0 + + def render_property(self, name, mode, context, props): + return RenderedProperty(name=name[3:] + ':', + name_attributes=[("class", "property")], + content=self._render_property(name, mode, + context, props)) + + def _render_property(self, name, mode, context, props): + repos, value = props[name] + try: + return unicode(value) + except UnicodeDecodeError: + if len(value) <= 100: + return tag.tt(''.join(("%02x" % ord(c)) for c in value)) + else: + return tag.em(_("(binary, size greater than 100 bytes)")) + + +class MercurialConnector(Component): + + implements(ISystemInfoProvider, IRepositoryConnector, IWikiSyntaxProvider) + + encoding = ListOption('hg', 'encoding', 'utf-8', doc=""" + Encoding that should be used to decode filenames, file + content, and changeset metadata. If multiple encodings are + used for these different situations (or even multiple + encodings were used for filenames), simply specify a list of + encodings which will be tried in turn (''since 0.12.0.24''). + """) + + show_rev = BoolOption('hg', 'show_rev', True, doc=""" + Show decimal revision in front of the commit SHA1 hash. While + this number is specific to the particular clone used to browse + the repository, this can sometimes give an useful hint about + the relative "age" of a revision. + """) + + node_format = ChoiceOption('hg', 'node_format', ['short', 'hex'], doc=""" + Specify how the commit SHA1 hashes should be + displayed. Possible choices are: 'short', the SHA1 hash is + abbreviated to its first 12 digits, or 'hex', the hash is + shown in full. + """) + + hgrc = PathOption('hg', 'hgrc', '', doc=""" + Optional path to an hgrc file which will be used to specify + extra Mercurial configuration options (see + http://www.selenic.com/mercurial/hgrc.5.html). + """) + + def __init__(self): + self.ui = None + locale_dir = pkg_resources.resource_filename(__name__, 'locale') + add_domain(self.env.path, locale_dir) + self._version = self._version_info = None + if not hg_import_error: + try: + from mercurial.version import get_version + self._version = get_version() + except ImportError: # gone in Mercurial 1.2 (hg:9626819b2e3d) + from mercurial.util import version + self._version = version() + # development version assumed to be always the ''newest'' one, + # i.e. old development version won't be supported + self._version_info = (999, 0, 0) + m = re.match(r'(\d+)\.(\d+)(?:\.(\d+))?', self._version or '') + if m: + self._version_info = tuple([int(n or 0) for n in m.groups()]) + + def _setup_ui(self, hgrc_path): + # Starting with Mercurial 1.3 we can probably do simply: + # + # ui = baseui.copy() # there's no longer a parent/child concept + # ui.setconfig('ui', 'interactive', 'off') + # + self.ui = trac_ui(log=self.log) + + # (code below adapted from mercurial.dispatch._dispatch) + + # read the local repository .hgrc into a local ui object + if hgrc_path: + if not os.path.exists(hgrc_path): + self.log.warn("'[hg] hgrc' file (%s) not found ", hgrc_path) + try: + self.ui = trac_ui(self.ui, log=self.log) + self.ui.check_trusted = False + self.ui.readconfig(hgrc_path) + except IOError, e: + self.log.warn("'[hg] hgrc' file (%s) can't be read: %s", + hgrc_path, e) + + extensions.loadall(self.ui) + if hasattr(extensions, 'extensions'): + for name, module in extensions.extensions(): + # setup extensions + extsetup = getattr(module, 'extsetup', None) + if extsetup: + if arity(extsetup) == 1: + extsetup(self.ui) + else: + extsetup() + + # ISystemInfoProvider methods + + def get_system_info(self): + if self._version is not None: + yield 'Mercurial', self._version + + # IRepositoryConnector methods + + def get_supported_types(self): + """Support for `repository_type = hg`""" + if hg_import_error: + self.error = hg_import_error + yield ("hg", -1) + else: + yield ("hg", 8) + + def get_repository(self, type, dir, params): + """Return a `MercurialRepository`""" + if not self.ui: + self._setup_ui(self.hgrc) + repos = MercurialRepository(dir, params, self.log, self) + repos.version_info = self._version_info + return repos + + # IWikiSyntaxProvider methods + + def get_wiki_syntax(self): + yield (r'!?(?P<hgrev>[0-9a-f]{12,40})(?P<hgpath>/\S+\b)?', + lambda formatter, label, match: + self._format_link(formatter, 'cset', match.group(0), + match.group(0), match)) + + def get_link_resolvers(self): + yield ('cset', self._format_link) + yield ('chgset', self._format_link) + yield ('branch', self._format_link) # go to the corresponding head + yield ('tag', self._format_link) + + def _format_link(self, formatter, ns, rev, label, fullmatch=None): + reponame = path = '' + repos = None + rm = RepositoryManager(self.env) + try: + if fullmatch: + rev = fullmatch.group('hgrev') + path = fullmatch.group('hgpath') + if path: + reponame, repos, path = \ + rm.get_repository_by_path(path.strip('/')) + if not repos: + context = formatter.context + while context: + if context.resource.realm in ('source', 'changeset'): + reponame = context.resource.parent.id + break + context = context.parent + repos = rm.get_repository(reponame) + if repos: + if ns == 'branch': + for b, n in repos.repo.branchtags().items(): + if repos.to_u(b) == rev: + rev = repos.repo.changelog.rev(n) + break + else: + raise NoSuchChangeset(rev) + chgset = repos.get_changeset(rev) + return tag.a(label, class_="changeset", + title=shorten_line(chgset.message), + href=formatter.href.changeset(rev, reponame, + path)) + raise TracError("Repository not found") + except NoSuchChangeset, e: + errmsg = to_unicode(e) + except TracError: + errmsg = _("Repository '%(repo)s' not found", repo=reponame) + return tag.a(label, class_="missing changeset", + title=errmsg, rel="nofollow") + + + +### Version Control API + +class MercurialRepository(Repository): + """Repository implementation based on the mercurial API. + + This wraps an hg.repository object. The revision navigation + follows the branches, and defaults to the first parent/child in + case there are many. The eventual other parents/children are + listed as additional changeset properties. + """ + + def __init__(self, path, params, log, connector): + self.ui = connector.ui + self._show_rev = connector.show_rev + self._node_fmt = connector.node_format + # TODO 0.13: per repository ui and options + + # -- encoding + encoding = connector.encoding + if not encoding: + encoding = ['utf-8'] + # verify given encodings + for enc in encoding: + try: + u''.encode(enc) + except LookupError, e: + log.warning("'[hg] encoding' (%r) not valid", e) + if 'latin1' not in encoding: + encoding.append('latin1') + self.encoding = encoding + + def to_u(s): + if isinstance(s, unicode): + return s + for enc in encoding: + try: + return unicode(s, enc) + except UnicodeDecodeError: + pass + def to_s(u): + if isinstance(u, str): + return u + for enc in encoding: + try: + return u.encode(enc) + except UnicodeEncodeError: + pass + self.to_u = to_u + self.to_s = to_s + + # -- repository path + self.path = str_path = path + # Note: `path` is a filesystem path obtained either from the + # trac.ini file or from the `repository` table, so it's + # normally an `unicode` instance. '[hg] encoding' + # shouldn't play a role here, but we can nevertheless + # use that as secondary choices. + fsencoding = [sys.getfilesystemencoding() or 'utf-8'] + encoding + str_path = checked_encode(path, fsencoding, os.path.exists) + if str_path is None: + raise TracError(_("Repository path '%(path)s' does not exist.", + path=path)) + try: + self.repo = hg.repository(ui=self.ui, path=str_path) + except RepoError, e: + version = connector._version + error = exception_to_unicode(e) + log.error("Mercurial %s can't open repository (%s)", version, error) + raise TracError(_("'%(path)s' does not appear to contain a" + " repository (Mercurial %(version)s says " + "%(error)s)", + path=path, version=version, error=error)) + Repository.__init__(self, 'hg:%s' % path, params, log) + + def from_hg_time(self, timeinfo): + time, tz = timeinfo + tzinfo = FixedOffset(tz / 60, 'GMT %d:00' % (tz / 3600)) + return datetime.fromtimestamp(time, tzinfo) + + def changectx(self, rev=None): + """Produce a Mercurial `context.changectx` from given Trac revision.""" + return self.repo[self.short_rev(rev)] + + def close(self): + self.repo = None + + def normalize_path(self, path): + """Remove leading "/" (even at root)""" + return path and path.strip('/') or '' + + def normalize_rev(self, rev): + """Return the full hash for the specified rev.""" + return self.changectx(rev).hex() + + def short_rev(self, rev): + """Find Mercurial revision number corresponding to given Trac revision. + + :param rev: any kind of revision specification, either an + `unicode` string, or a revision number. If `None` + or '', latest revision will be returned. + + :return: an integer revision + """ + repo = self.repo + if rev == 0: + return rev + if not rev: + return len(repo) - 1 + if isinstance(rev, (long, int)): + return rev + if rev[0] != "'": # "'11:11'" can be a tag name? + rev = rev.split(':', 1)[0] + if rev == '-1': + return nullrev + if rev.isdigit(): + r = int(rev) + if 0 <= r < len(repo): + return r + try: + return repo[repo.lookup(self.to_s(rev))].rev() + except (HgLookupError, RepoError): + raise NoSuchChangeset(rev) + + def display_rev(self, rev): + return self._display(self.changectx(rev)) + + def _display(self, ctx): + """Return user-readable revision information for node `n`. + + The specific format depends on the `node_format` and + `show_rev` options. + """ + nodestr = self._node_fmt == "hex" and ctx.hex() or str(ctx) + if self._show_rev: + return '%s:%s' % (ctx.rev(), nodestr) + else: + return nodestr + + def get_quickjump_entries(self, rev): + # map ctx to (unicode) branch + branches = {} + closed_branches = {} + for b, n in self.repo.branchtags().items(): + b = self.to_u(b) + ctx = self.repo[n] + if 'close' in ctx.extra(): + closed_branches[ctx] = b + else: + branches[ctx] = b + # map node to tag names + tags = {} + tagslist = self.repo.tagslist() + for tag, n in tagslist: + tags.setdefault(n, []).append(self.to_u(tag)) + def taginfo(ctx): + t = tags.get(ctx.node()) + if t: + return ' (%s)' % ', '.join(t) + else: + return '' + # branches + for ctx, b in sorted(branches.items(), reverse=True, + key=lambda (ctx, b): ctx.rev()): + yield ('branches', b + taginfo(ctx), '/', self._display(ctx)) + # heads + for n in self.repo.heads(): + ctx = self.repo[n] + if ctx not in branches and ctx not in closed_branches: + h = self._display(ctx) + yield ('extra heads', h + taginfo(ctx), '/', h) + # tags + for t, n in reversed(tagslist): + try: + yield ('tags', ', '.join(tags.pop(n)), # FIXME: pop? + '/', self._display(self.repo[n])) + except (KeyError, RepoLookupError): + pass + # closed branches + for ctx, b in sorted(closed_branches.items(), reverse=True, + key=lambda (ctx, b): ctx.rev()): + yield ('closed branches', b + taginfo(ctx), '/', self._display(ctx)) + + def get_path_url(self, path, rev): + url = self.params.get('url') + if url and (not path or path == '/'): + if not rev: + return url + branch = self.changectx(rev).branch() + if branch == 'default': + return url + return url + '#' + self.to_u(branch) # URL for cloning that branch + + # Note: link to matching location in Mercurial's file browser + #rev = rev is not None and short(n) or 'tip' + #return '/'.join([url, 'file', rev, path]) + + def get_changeset(self, rev): + return MercurialChangeset(self, self.changectx(rev)) + + def get_changeset_uid(self, rev): + return self.changectx(rev).hex() + + def get_changesets(self, start, stop): + """Follow each head and parents in order to get all changesets + + FIXME: this can only be handled correctly and efficiently by + using the db repository cache. + + The code below is only an heuristic, and doesn't work in the + general case. E.g. look at the mercurial repository timeline + for 2006-10-18, you need to give ''38'' daysback in order to + see the changesets from 2006-10-17... + + This is because of the following '''linear''' sequence of csets: + - 3445:233c733e4af5 10/18/2006 9:08:36 AM mpm + - 3446:0b450267cf47 9/10/2006 3:25:06 AM hopper + - 3447:ef1032c223e7 9/10/2006 3:25:06 AM hopper + - 3448:6ca49c5fe268 9/10/2006 3:25:07 AM hopper + - 3449:c8686e3f0291 10/18/2006 9:14:26 AM hopper + + This is most probably because [3446:3448] correspond to old + changesets that have been ''hg import''ed, with their + original dates. + """ + seen = {nullrev: 1} + seeds = [self.repo[n] for n in self.repo.heads()] + while seeds: + ctx = seeds.pop(0) + time = self.from_hg_time(ctx.date()) + if time < start: + continue # assume no ancestor is younger and use next seed + # (and that assumption is wrong for 3448 in the example above) + elif time < stop: + yield MercurialChangeset(self, ctx) + for p in ctx.parents(): + if p.rev() not in seen: + seen[p.rev()] = 1 + seeds.append(p) + + def get_node(self, path, rev=None): + return MercurialNode(self, self.normalize_path(path), + self.changectx(rev)) + + def get_oldest_rev(self): + return 0 + + def get_youngest_rev(self): + return self.changectx().hex() + + def previous_rev(self, rev, path=''): # FIXME: path ignored for now + for p in self.changectx(rev).parents(): + if p: + return p.hex() # always follow first parent + + def next_rev(self, rev, path=''): + ctx = self.changectx(rev) + if path: # might be a file + fc = filectx(self.repo, self.to_s(path), ctx.node()) + # Note: the simpler form below raises an HgLookupError for a dir + # fc = ctx.filectx(self.to_s(path)) + if fc: # it is a file + for c in fc.children(): + return c.hex() + else: + return None + # it might be a directory (not supported for now) FIXME + for c in ctx.children(): + return c.hex() # always follow first child + + def rev_older_than(self, rev1, rev2): + # FIXME use == and ancestors? + return self.short_rev(rev1) < self.short_rev(rev2) + +# def get_path_history(self, path, rev=None, limit=None): +# (not really relevant for Mercurial) + + def get_changes(self, old_path, old_rev, new_path, new_rev, + ignore_ancestry=1): + """Generates changes corresponding to generalized diffs. + + Generator that yields change tuples (old_node, new_node, kind, + change) for each node change between the two arbitrary + (path,rev) pairs. + + The old_node is assumed to be None when the change is an ADD, + the new_node is assumed to be None when the change is a + DELETE. + """ + old_node = new_node = None + old_node = self.get_node(old_path, old_rev) + new_node = self.get_node(new_path, new_rev) + # check kind, both should be same. + if new_node.kind != old_node.kind: + raise TracError( + _("Diff mismatch: " + "Base is a %(okind)s (%(opath)s in revision %(orev)s) " + "and Target is a %(nkind)s (%(npath)s in revision %(nrev)s).", + okind=old_node.kind, opath=old_path, orev=old_rev, + nkind=new_node.kind, npath=new_path, nrev=new_rev)) + # Correct change info from changelog(revlog) + # Finding changes between two revs requires tracking back + # several routes. + + if new_node.isdir: + # TODO: Should we follow rename and copy? + # As temporary workaround, simply compare entry names. + changes = [] + str_new_path = self.to_s(new_path) + str_old_path = self.to_s(old_path) + # additions and edits + for str_path in new_node.manifest: + # changes out of scope + if str_new_path and not str_path.startswith(str_new_path + '/'): + continue + # 'added' if not present in old manifest + str_op = str_old_path + str_path[len(str_new_path):] + if str_op not in old_node.manifest: + changes.append((str_path, None, new_node.subnode(str_path), + Node.FILE, Changeset.ADD)) + elif old_node.manifest[str_op] != new_node.manifest[str_path]: + changes.append((str_path, old_node.subnode(str_op), + new_node.subnode(str_path), + Node.FILE, Changeset.EDIT)) + # deletions + for str_path in old_node.manifest: + # changes out of scope + if str_old_path and not str_path.startswith(str_old_path + '/'): + continue + # 'deleted' if not present in new manifest + str_np = str_new_path + str_path[len(str_old_path):] + if str_np not in new_node.manifest: + changes.append((str_path, old_node.subnode(str_np), None, + Node.FILE, Changeset.DELETE)) + # Note: `str_path` only used as a key, no need to convert to_u + for change in sorted(changes, key=lambda c: c[0]): + yield(change[1], change[2], change[3], change[4]) + else: + if old_node.manifest[old_node.str_path] != \ + new_node.manifest[new_node.str_path]: + yield(old_node, new_node, Node.FILE, Changeset.EDIT) + + +class MercurialNode(Node): + """A path in the repository, at a given revision. + + It encapsulates the repository manifest for the given revision. + + As directories are not first-class citizens in Mercurial, + retrieving revision information for directory can be much slower + than for files, except when created as a `subnode()` of an + existing MercurialNode. + """ + + filectx = dirnode = None + + def __init__(self, repos, path, changectx, + manifest=None, dirctx=None, str_entry=None): + """ + :param repos: the `MercurialRepository` + :param path: the `unicode` path corresponding to this node + :param rev: requested revision (i.e. "browsing at") + :param changectx: the `changectx` for the "requested" revision + + The following parameters are passed when creating a subnode + instance: + + :param manifest: `manifest` object from parent `MercurialNode` + :param dirctx: `changectx` for a directory determined by + parent `MercurialNode` + :param str_entry: entry name if node created from parent node + """ + repo = repos.repo + self.repos = repos + self.changectx = changectx + self.manifest = manifest or changectx.manifest() + str_entries = [] + + if path == '' or path == '/': + str_path = '' + elif dirctx: + str_path = str_entry + else: + # Fast path: check for existing file + str_path = checked_encode(path, repos.encoding, + lambda s: s in self.manifest) + if str_path is None: + # Slow path: this might be a directory node + str_files = sorted(self.manifest) + idx = [-1] + def has_dir_node(str_dir): + if not str_dir: # no encoding matched, i.e. not existing + return False + idx[0] = lo = bisect(str_files, str_dir) + return lo < len(str_files) \ + and str_files[lo].startswith(str_dir) + str_path = checked_encode(path + '/', repos.encoding, + has_dir_node) + if str_path is None: + raise NoSuchNode(path, changectx.hex()) + lo = idx[0] + for hi in xrange(lo, len(str_files)): + if not str_files[hi].startswith(str_path): + break + str_path = str_path[:-1] + str_entries = str_files[lo:hi] + self.str_path = str_path + + # Determine `kind`, `rev` (requested rev) and `created_rev` + # (last changed revision before requested rev) + + kind = None + rev = changectx.rev() + if str_path == '': + kind = Node.DIRECTORY + dirctx = changectx + elif str_path in self.manifest: # then it's a file + kind = Node.FILE + self.filectx = changectx.filectx(str_path) + created_rev = self.filectx.linkrev() + # FIXME (0.13) this is a hack, we should fix that at the + # Trac level, which should really show the + # created_rev value for files in the browser. + rev = created_rev + else: # we already know it's a dir + kind = Node.DIRECTORY + if not dirctx: + # we need to find the most recent change for a file below dir + str_dir = str_path + '/' + dirctxs = self.find_dirctx(changectx.rev(), [str_dir,], + {str_dir: str_entries}) + dirctx = dirctxs.values()[0] + + if not kind: + if repo.changelog.tip() == nullid or \ + not (self.manifest or str_path): + # empty or emptied repository + kind = Node.DIRECTORY + dirctx = changectx + else: + raise NoSuchNode(path, changectx.hex()) + + self.time = self.repos.from_hg_time(changectx.date()) + if dirctx is not None: + # FIXME (0.13) same remark as above + rev = created_rev = dirctx.rev() + Node.__init__(self, self.repos, path, rev or '0', kind) + self.created_path = path + self.created_rev = created_rev + self.data = None + + def find_dirctx(self, max_rev, str_dirnames, str_entries): + """Find most recent modification for each given directory path. + + :param max_rev: find no revision more recent than this one + :param str_dirnames: directory paths to consider + (as `str` ending with '/') + :param str_entries: optionally maps directories to their file content + + :return: a `dict` with `str_dirnames` as keys, `changectx` as values + + As directories are not first-class citizens in Mercurial, this + operation is not trivial. There are basically two strategies: + + - for each file below the given directories, retrieve the + linkrev (most recent modification for this file), and take + the max; this approach is very inefficient for repositories + containing many files (#7746) + + - retrieve the files modified when going backward through the + changelog and detect the first occurrence of a change in + each directory; this is much faster but can still be slow + if some folders are only modified in the distant past + + It is possible to combine both approach, and this can yield + excellent results in some cases (e.g. browsing the Linux repos + @ 118733 takes several minutes with the first approach, 11s + with the second, but only 1.2s with the hybrid approach) + + Note that the specialized scan of the changelog we do below is + more efficient than the general cmdutil.walkchangerevs here. + """ + str_dirctxs = {} + repo = self.repos.repo + max_ctx = repo[max_rev] + for r in xrange(max_rev, -1, -1): + ctx = repo[r] + # lookup changes to str_dirnames in current cset + for str_file in ctx.files(): + for str_dir in str_dirnames[:]: + if str_file.startswith(str_dir): + str_dirctxs[str_dir] = ctx + str_dirnames.remove(str_dir) + if not str_dirnames: # if nothing left to find + return str_dirctxs + # in parallel, try the filelog strategy (the 463, 2, 40 + # values below look a bit like magic numbers; actually + # they were selected by testing the plugin on the Linux + # and NetBeans repositories) + if r % 463 == 0: + k = max(2, 40 / len(str_dirnames)) + for str_dir in str_dirnames[:]: + str_files = str_entries[str_dir] + dr = str_dirctxs.get(str_dir, 0) + for f in str_files[:k]: + try: + dr = max(dr, max_ctx.filectx(f).linkrev()) + except LookupError: + pass # that file was not on this revision `r` + str_files = str_files[k:] + if str_files: + str_entries[str_dir] = str_files + str_dirctxs[str_dir] = dr + else: + str_dirctxs[str_dir] = repo[dr] + str_dirnames.remove(str_dir) + if not str_dirnames: + return str_dirctxs + + + def subnode(self, str_path, subctx=None): + """Return a node with the same revision information but for + another path + + :param str_path: should be the an existing entry in the manifest + """ + return MercurialNode(self.repos, self.repos.to_u(str_path), + self.changectx, self.manifest, subctx, str_path) + + def get_content(self): + if self.isdir: + return None + self.pos = 0 # reset the read() + return self # something that can be `read()` ... + + def read(self, size=None): + if self.isdir: + return TracError(_("Can't read from directory %(path)s", + path=self.path)) + if self.data is None: + self.data = self.filectx.data() + self.pos = 0 + if size: + prev_pos = self.pos + self.pos += size + return self.data[prev_pos:self.pos] + return self.data + + def get_entries(self): + if self.isfile: + return + + # dirnames are entries which are sub-directories + str_entries = {} + str_dirnames = [] + def add_entry(str_file, idx): + str_entry = str_file + if idx > -1: # directory + str_entry = str_file[:idx + 1] + str_files = str_entries.setdefault(str_entry, []) + if not str_files: + str_dirnames.append(str_entry) + str_files.append(str_file) + else: + str_entries[str_entry] = 1 + + if self.str_path: + str_dir = self.str_path + '/' + for str_file in self.manifest: + if str_file.startswith(str_dir): + add_entry(str_file, str_file.find('/', len(str_dir))) + else: + for str_file in self.manifest: + add_entry(str_file, str_file.find('/')) + + # pre-computing the changectx for the last change in each sub-directory + if str_dirnames: + dirctxs = self.find_dirctx(self.created_rev, str_dirnames, + str_entries) + else: + dirctxs = {} + + for str_entry in str_entries: + yield self.subnode(str_entry.rstrip('/'), + dirctxs.get(str_entry, None)) + + def get_history(self, limit=None): + repo = self.repos.repo + pats = [] + if self.str_path: + pats.append('path:' + self.str_path) + opts = {'rev': ['%s:0' % self.changectx.hex()]} + if self.isfile: + opts['follow'] = True + if arity(cmdutil.walkchangerevs) == 4: + return self._get_history_1_4(repo, pats, opts, limit) + else: + return self._get_history_1_3(repo, pats, opts, limit) + + def _get_history_1_4(self, repo, pats, opts, limit): + matcher = match(repo[None], pats, opts) + if self.isfile: + fncache = {} + def prep(ctx, fns): + if self.isfile: + fncache[ctx.rev()] = self.repos.to_u(fns[0]) + else: + def prep(ctx, fns): + pass + + # keep one lookahead entry so that we can detect renames + path = self.path + entry = None + count = 0 + for ctx in cmdutil.walkchangerevs(repo, matcher, opts, prep): + if self.isfile and entry: + path = fncache[ctx.rev()] + if path != entry[0]: + entry = entry[0:2] + (Changeset.COPY,) + if entry: + yield entry + count += 1 + if limit is not None and count >= limit: + return + entry = (path, ctx.hex(), Changeset.EDIT) + if entry: + if limit is None or count < limit: + entry = entry[0:2] + (Changeset.ADD,) + yield entry + + def _get_history_1_3(self, repo, pats, opts, limit): + if self.repos.version_info > (1, 3, 999): + changefn = lambda r: repo[r] + else: + changefn = lambda r: repo[r].changeset() + get = cachefunc(changefn) + if self.isfile: + fncache = {} + chgiter, matchfn = cmdutil.walkchangerevs(self.repos.ui, repo, pats, + get, opts) + # keep one lookahead entry so that we can detect renames + path = self.path + entry = None + count = 0 + for st, rev, fns in chgiter: + if st == 'add' and self.isfile: + fncache[rev] = self.repos.to_u(fns[0]) + elif st == 'iter': + if self.isfile and entry: + path = fncache[rev] + if path != entry[0]: + entry = entry[0:2] + (Changeset.COPY,) + if entry: + yield entry + count += 1 + if limit is not None and count >= limit: + return + n = repo.changelog.node(rev) + entry = (path, hex(n), Changeset.EDIT) + if entry: + if limit is None or count < limit: + entry = entry[0:2] + (Changeset.ADD,) + yield entry + + def get_annotations(self): + annotations = [] + if self.filectx: + for fc, line in self.filectx.annotate(follow=True): + annotations.append(fc.rev() or '0') + return annotations + + def get_properties(self): + if self.isfile and 'x' in self.manifest.flags(self.str_path): + return {'exe': '*'} + else: + return {} + + def get_content_length(self): + if self.isdir: + return None + return self.filectx.size() + + def get_content_type(self): + if self.isdir: + return None + if 'mq' in self.repos.params: # FIXME + if self.str_path not in ('.hgignore', 'series'): + return 'text/x-diff' + return '' + + def get_last_modified(self): + return self.time + + +class MercurialChangeset(Changeset): + """A changeset in the repository. + + This wraps the corresponding information from the changelog. The + files changes are obtained by comparing the current manifest to + the parent manifest(s). + """ + + def __init__(self, repos, ctx): + self.repos = repos + self.ctx = ctx + self.branch = self.repos.to_u(ctx.branch()) + # Note: desc and time are already processed by hg's + # `encoding.tolocal`; by setting $HGENCODING to latin1, we are + # however guaranteed to get back the bytes as they were + # stored. + desc = repos.to_u(ctx.description()) + user = repos.to_u(ctx.user()) + time = repos.from_hg_time(ctx.date()) + Changeset.__init__(self, repos, ctx.hex(), desc, user, time) + + hg_properties = [ + N_("Parents:"), N_("Children:"), N_("Branch:"), N_("Tags:") + ] + + def get_properties(self): + properties = {} + parents = self.ctx.parents() + if len(parents) > 1: + properties['hg-Parents'] = (self.repos, + [p.hex() for p in parents if p]) + children = self.ctx.children() + if len(children) > 1: + properties['hg-Children'] = (self.repos, + [c.hex() for c in children]) + if self.branch: + properties['hg-Branch'] = (self.repos, [self.branch]) + tags = self.ctx.tags() + if len(tags): + properties['hg-Tags'] = (self.repos, + [self.repos.to_u(t) for t in tags]) + for k, v in self.ctx.extra().iteritems(): + if k != 'branch': + properties['hg-' + k] = (self.repos, v) + return properties + + def get_changes(self): + u = self.repos.to_u + repo = self.repos.repo + manifest = self.ctx.manifest() + parents = self.ctx.parents() + + renames = [] + str_deletions = {} + changes = [] + for str_file in self.ctx.files(): # added, edited and deleted files + f = u(str_file) + # TODO: find a way to detect conflicts and show how they were + # solved (kind of 3-way diff - theirs/mine/merged) + edits = [p for p in parents if str_file in p.manifest()] + + if str_file not in manifest: + str_deletions[str_file] = edits[0] + elif edits: + for p in edits: + changes.append((f, Node.FILE, Changeset.EDIT, f, p.rev())) + else: + renamed = repo.file(str_file).renamed(manifest[str_file]) + if renamed: + renames.append((f, renamed)) + else: + changes.append((f, Node.FILE, Changeset.ADD, '', None)) + # distinguish between move and copy + for f, (str_base_path, base_filenode) in renames: + base_ctx = repo.filectx(str_base_path, fileid=base_filenode) + if str_base_path in str_deletions: + del str_deletions[str_base_path] + action = Changeset.MOVE + else: + action = Changeset.COPY + changes.append((f, Node.FILE, action, u(str_base_path), + base_ctx.rev())) + # remaining str_deletions are real deletions + for str_file, p in str_deletions.items(): + f = u(str_file) + changes.append((f, Node.FILE, Changeset.DELETE, f, p.rev())) + changes.sort() + for change in changes: + yield change + + def get_branches(self): + """Yield branch names to which this changeset belong.""" + return self.branch and [(self.branch, + len(self.ctx.children()) == 0)] or []
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tracext/hg/hooks.py Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,126 @@ +# -*- coding: iso-8859-1 -*- +# +# Copyright (C) 2011-2012 Edgewall Software +# All rights reserved. +# +# This software may be used and distributed according to the terms +# of the GNU General Public License, incorporated herein by reference. +# +# 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/. + + +""" +Mercurial hook calling `trac-admin $ENV changeset added` for newly added +changesets + +The Trac environments to be notified are configured in the `[trac]` section +of `hgrc`. The path to an environment is specified in the `env` key. More +environments can be specified by adding a suffix separated by a dot. + +The path to the `trac-admin` executable can be specified in the `trac-admin` +key. A specific path can be set for each environment, by adding the same +suffix to the `trac-admin` key. + +The maximum number of changesets to pass per call to `trac-admin` can be +configured with the `revs_per_call` key. The default is relatively low (80 +on Windows, 500 on other systems) for maximum compatibility, and can be +increased if you often push thousands of changesets and the system supports +it. + +If the `trac-admin` option is left empty, the hook opens the Trac environment +and calls the relevant Trac hook directly in the same process. This will only +work if Trac can be imported by the Python interpreter running Mercurial (e.g. +if both are installed globally). Note that this may result in increased memory +usage if Mercurial is executed as a long-running process (e.g. hgweb in WSGI +mode). In this last case, the option `cache_env` can be set to `true` to +cache the Trac environments across invocations, therefore avoiding the +environment setup time on each invocation. + +If the Mercurial plugin is installed globally (i.e. not in one of Trac's +plugins directories), the hooks can be configured as follows: +{{{ +[hooks] +commit = python:tracext.hg.hooks.add_changesets +changegroup = python:tracext.hg.hooks.add_changesets +}}} + +Otherwise, place this file somewhere accessible, and configure the hooks as +follows: +{{{ +[hooks] +commit = python:/path/to/hooks.py:add_changesets +changegroup = python:/path/to/hooks.py:add_changesets +}}} + +A typical configuration for three environments looks like this: +{{{ +[trac] +; For a single Trac environment +env = /path/to/env +trac-admin = /path/to/trac-admin + +; Two more environments, with a specific trac-admin for the second +env.other = /path/to/other/env +env.third = /path/to/third/env +trac-admin.third = /path/to/third/trac-admin +}}} +""" + +import os.path +import subprocess + +close_fds = os.name == 'posix' + + +def expand_path(path): + """Expand user references and environment variables in a path.""" + return os.path.expanduser(os.path.expandvars(path)) + + +def add_changesets(ui, repo, node, **kwargs): + """Commit hook calling `trac-admin $ENV changeset added $REPO $NODE ...` + on all configured Trac environments, for all newly added changesets. + """ + revs = range(repo[node].rev(), len(repo)) + error = False + for name, env in ui.configitems('trac'): + p = name.split('.', 1) + if p[0] != 'env' or not env: + continue + env = expand_path(env) + + trac_admin = ui.config('trac', 'trac-admin', '') + if len(p) > 1: + trac_admin = ui.config('trac', 'trac-admin.' + p[1], trac_admin) + + if not trac_admin: + cache_env = ui.configbool('trac', 'cache_env') + from trac.env import open_environment + from trac.versioncontrol.api import RepositoryManager + env = open_environment(env, use_cache=cache_env) + RepositoryManager(env).notify('changeset_added', repo.root, revs) + continue + + try: + revs_per_call = int(ui.config('trac', 'revs_per_call')) + except (TypeError, ValueError): + revs_per_call = os.name == 'nt' and 160 or 1000 + + trac_admin = expand_path(trac_admin) + for i in xrange(0, len(revs), revs_per_call): + command = [trac_admin, env, 'changeset', 'added', repo.root] + command.extend(str(r) for r in revs[i : i + revs_per_call]) + ui.debug("Calling %r\n" % (command,)) + proc = subprocess.Popen(command, close_fds=close_fds, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + for line in proc.stdout: + ui.write(line) + proc.wait() + if proc.returncode != 0: + error = True + ui.warn("trac-admin failed with return code %d for " + "environment:\n %s\n" % (proc.returncode, env)) + return error
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tracext/hg/locale/fr/LC_MESSAGES/tracmercurial.po Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,109 @@ +# French (France) translations for TracMercurial. +# Copyright (C) 2008 ORGANIZATION +# This file is distributed under the same license as the TracMercurial +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: TracMercurial 0.12.0.4\n" +"Report-Msgid-Bugs-To: cboos@edgewall.org\n" +"POT-Creation-Date: 2008-11-13 12:33+0100\n" +"PO-Revision-Date: 2009-11-19 16:22+0100\n" +"Last-Translator: Christian Boos <cboos@edgewall.org>\n" +"Language-Team: fr_FR <LL@li.org>\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.4\n" + +#: tracext/hg/backend.py:99 +msgid ":" +msgstr "" + +#: tracext/hg/backend.py:114 +msgid "Diff against this parent (show the changes merged from the other parents)" +msgstr "" +"Différences vis-à-vis de ce parent (montre les changements apportés par " +"l'autre parent)" + +#: tracext/hg/backend.py:124 +#, python-format +msgid "" +"Note: this is a %(merge)s changeset, the changes displayed below " +"correspond to the merge itself." +msgstr "" +"Note: ceci est un changeset de type %(merge)s, les modifications ci-" +"dessous correspondent au merge lui-même." + +#: tracext/hg/backend.py:132 +#, python-format +msgid "" +"Use the %(diff)s links above to see all the changes relative to each " +"parent." +msgstr "" +"Utilisez les liens %(diff)s ci-dessus pour voir les modifications " +"relatives à chaque parent." + +#: tracext/hg/backend.py:155 +msgid "no such changeset" +msgstr "pas de révisio" + +#: tracext/hg/backend.py:156 +msgid "Transplant:" +msgstr "" + +#: tracext/hg/backend.py:173 +msgid "(binary, size greater than 100 bytes)" +msgstr "(binaire, taille supérieure à 100 octets)" + +#: tracext/hg/backend.py:285 +#, python-format +msgid "Repository '%(repo)s' not found" +msgstr "Dépôt '%(repo)s' non trouvé" + +#: tracext/hg/backend.py:344 +#, python-format +msgid "%(path)s does not appear to contain a Mercurial repository." +msgstr "%(path)s ne semble pas contenir un dépôt Mercurial" + +#: tracext/hg/backend.py:566 +msgid "The Base for Diff is invalid" +msgstr "" + +#: tracext/hg/backend.py:571 +msgid "The Target for Diff is invalid" +msgstr "" + +#: tracext/hg/backend.py:575 +#, python-format +msgid "" +"Diff mismatch: Base is a %(okind)s (%(opath)s in revision %(orev)s) and " +"Target is a %(nkind)s (%(npath)s in revision %(nrev)s)." +msgstr "" +"Différences incompatibles : Source est un %(okind)s (%(opath)s dans la " +"révision %(orev)s) et Destination est un %(nkind)s (%(npath)s dans la " +"révision %(nrev)s)." + +#: tracext/hg/backend.py:719 +#, python-format +msgid "Can't read from directory %(path)s" +msgstr "Lecture du répertoire %(path)s impossible" + +#: tracext/hg/backend.py:892 +msgid "Parents:" +msgstr "Ascendants :" + +#: tracext/hg/backend.py:892 +msgid "Children:" +msgstr "Descendants :" + +#: tracext/hg/backend.py:892 +msgid "Branch:" +msgstr "Branche :" + +#: tracext/hg/backend.py:892 +msgid "Tags:" +msgstr "Tags :" +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tracext/hg/locale/messages.pot Sat Jun 09 19:27:02 2012 +0200 @@ -0,0 +1,102 @@ +# Translations template for TracMercurial. +# Copyright (C) 2009 ORGANIZATION +# This file is distributed under the same license as the TracMercurial +# project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2009. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: TracMercurial 0.12.0.9\n" +"Report-Msgid-Bugs-To: cboos@edgewall.org\n" +"POT-Creation-Date: 2009-11-19 16:21+0100\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.4\n" + +#: tracext/hg/backend.py:99 +msgid ":" +msgstr "" + +#: tracext/hg/backend.py:114 +msgid "" +"Diff against this parent (show the changes merged from the other " +"parents)" +msgstr "" + +#: tracext/hg/backend.py:124 +#, python-format +msgid "" +"Note: this is a %(merge)s changeset, the changes displayed below " +"correspond to the merge itself." +msgstr "" + +#: tracext/hg/backend.py:132 +#, python-format +msgid "" +"Use the %(diff)s links above to see all the changes relative to each " +"parent." +msgstr "" + +#: tracext/hg/backend.py:155 +msgid "no such changeset" +msgstr "" + +#: tracext/hg/backend.py:156 +msgid "Transplant:" +msgstr "" + +#: tracext/hg/backend.py:173 +msgid "(binary, size greater than 100 bytes)" +msgstr "" + +#: tracext/hg/backend.py:285 +#, python-format +msgid "Repository '%(repo)s' not found" +msgstr "" + +#: tracext/hg/backend.py:344 +#, python-format +msgid "%(path)s does not appear to contain a Mercurial repository." +msgstr "" + +#: tracext/hg/backend.py:566 +msgid "The Base for Diff is invalid" +msgstr "" + +#: tracext/hg/backend.py:571 +msgid "The Target for Diff is invalid" +msgstr "" + +#: tracext/hg/backend.py:575 +#, python-format +msgid "" +"Diff mismatch: Base is a %(okind)s (%(opath)s in revision %(orev)s) " +"and Target is a %(nkind)s (%(npath)s in revision %(nrev)s)." +msgstr "" + +#: tracext/hg/backend.py:719 +#, python-format +msgid "Can't read from directory %(path)s" +msgstr "" + +#: tracext/hg/backend.py:892 +msgid "Parents:" +msgstr "" + +#: tracext/hg/backend.py:892 +msgid "Children:" +msgstr "" + +#: tracext/hg/backend.py:892 +msgid "Branch:" +msgstr "" + +#: tracext/hg/backend.py:892 +msgid "Tags:" +msgstr "" +