Switch to the 0.11 branch as of 5798 mercurial-plugin
authorjohn.wright@hp.com
Thu Dec 13 23:20:51 2007 -0700 (2007-12-13)
branchmercurial-plugin
changeset 5344061ad86c7d
parent 39 c68003e593af
child 54 4426b8d42273
Switch to the 0.11 branch as of 5798
setup.py
tracext/__init__.py
tracext/hg/__init__.py
tracext/hg/backend.py
tracvc/__init__.py
tracvc/hg/__init__.py
tracvc/hg/backend.py
     1.1 --- a/setup.py	Tue Jul 03 09:22:15 2007 -0600
     1.2 +++ b/setup.py	Thu Dec 13 23:20:51 2007 -0700
     1.3 @@ -5,18 +5,18 @@
     1.4  TracMercurial = 'http://trac.edgewall.org/wiki/TracMercurial',
     1.5  
     1.6  setup(name='TracMercurial',
     1.7 -      description='Mercurial plugin for Trac 0.10',
     1.8 +      description='Mercurial plugin for Trac 0.11',
     1.9        keywords='trac scm plugin mercurial hg',
    1.10 -      version='0.10.0.2',
    1.11 +      version='0.11.0.1',
    1.12        url=TracMercurial,
    1.13        license='GPL',
    1.14        author='Christian Boos',
    1.15        author_email='cboos@neuf.fr',
    1.16        long_description="""
    1.17 -      This plugin for Trac 0.10 provides support for the Mercurial SCM.
    1.18 +      This plug for Trac 0.11 provides support for the Mercurial SCM.
    1.19        
    1.20        See %s for more details.
    1.21        """ % TracMercurial,
    1.22 -      packages=['tracvc', 'tracvc.hg'],
    1.23 +      packages=['tracext', 'tracext.hg'],
    1.24        data_files=['COPYING', 'README'],
    1.25 -      entry_points={'trac.plugins': 'hg = tracvc.hg.backend'})
    1.26 +      entry_points={'trac.plugins': 'hg = tracext.hg.backend'})
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/tracext/__init__.py	Thu Dec 13 23:20:51 2007 -0700
     2.3 @@ -0,0 +1,1 @@
     2.4 +__import__('pkg_resources').declare_namespace(__name__)
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/tracext/hg/__init__.py	Thu Dec 13 23:20:51 2007 -0700
     3.3 @@ -0,0 +1,2 @@
     3.4 +
     3.5 +
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/tracext/hg/backend.py	Thu Dec 13 23:20:51 2007 -0700
     4.3 @@ -0,0 +1,655 @@
     4.4 +# -*- coding: iso-8859-1 -*-
     4.5 +#
     4.6 +# Copyright (C) 2005 Edgewall Software
     4.7 +# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
     4.8 +# All rights reserved.
     4.9 +#
    4.10 +# This software may be used and distributed according to the terms
    4.11 +# of the GNU General Public License, incorporated herein by reference.
    4.12 +#
    4.13 +# Author: Christian Boos <cboos@neuf.fr>
    4.14 +
    4.15 +from datetime import datetime
    4.16 +import os
    4.17 +import time
    4.18 +import posixpath
    4.19 +import re
    4.20 +
    4.21 +from genshi.builder import tag
    4.22 +
    4.23 +from trac.core import *
    4.24 +from trac.config import _TRUE_VALUES as TRUE
    4.25 +from trac.util.compat import sorted, reversed
    4.26 +from trac.util.datefmt import utc
    4.27 +from trac.util.text import shorten_line, to_unicode
    4.28 +from trac.versioncontrol.api import Changeset, Node, Repository, \
    4.29 +                                    IRepositoryConnector, \
    4.30 +                                    NoSuchChangeset, NoSuchNode
    4.31 +from trac.wiki import IWikiSyntaxProvider
    4.32 +
    4.33 +try:
    4.34 +    # The new `demandimport` mechanism doesn't play well with code relying
    4.35 +    # on the `ImportError` exception being caught.
    4.36 +    # OTOH, we can't disable `demandimport` because mercurial relies on it
    4.37 +    # (circular reference issue). So for now, we activate `demandimport`
    4.38 +    # before loading mercurial modules, and desactivate it afterwards.
    4.39 +    #
    4.40 +    # See http://www.selenic.com/mercurial/bts/issue605
    4.41 +    
    4.42 +    try:
    4.43 +        from mercurial import demandimport
    4.44 +        demandimport.enable();
    4.45 +    except ImportError:
    4.46 +        demandimport = None
    4.47 +
    4.48 +    from mercurial import hg
    4.49 +    from mercurial.ui import ui
    4.50 +    from mercurial.repo import RepoError
    4.51 +    from mercurial.node import hex, short, nullid
    4.52 +    from mercurial.util import pathto, cachefunc
    4.53 +    from mercurial.cmdutil import walkchangerevs
    4.54 +
    4.55 +    if demandimport:
    4.56 +        demandimport.disable();
    4.57 +    has_mercurial = True
    4.58 +except ImportError:
    4.59 +    has_mercurial = False
    4.60 +    ui = object
    4.61 +
    4.62 +try:
    4.63 +    from trac.versioncontrol.web_ui import IPropertyRenderer
    4.64 +    has_property_renderer = True
    4.65 +except ImportError:
    4.66 +    has_property_renderer = False
    4.67 +
    4.68 +
    4.69 +### Components
    4.70 +
    4.71 +if has_property_renderer:
    4.72 +    class CsetPropertyRenderer(Component):
    4.73 +        implements(IPropertyRenderer)
    4.74 +
    4.75 +        def match_property(self, name, mode):
    4.76 +            return (name in ('Parents', 'Children', 'Tags') and
    4.77 +                    mode == 'revprop') and 4 or 0
    4.78 +        
    4.79 +        def render_property(self, name, mode, context, props):
    4.80 +            revs = props[name]
    4.81 +            def link(rev):
    4.82 +                chgset = context.env.get_repository().get_changeset(rev)
    4.83 +                return tag.a(rev, class_="changeset",
    4.84 +                             title=shorten_line(chgset.message),
    4.85 +                             href=context.href.changeset(rev))
    4.86 +            return tag([tag(link(rev), ', ') for rev in revs[:-1]],
    4.87 +                       link(revs[-1]))
    4.88 +
    4.89 +
    4.90 +class MercurialConnector(Component):
    4.91 +
    4.92 +    implements(IRepositoryConnector, IWikiSyntaxProvider)
    4.93 +
    4.94 +    def __init__(self):
    4.95 +        self._version = None
    4.96 +
    4.97 +    def get_supported_types(self):
    4.98 +        """Support for `repository_type = hg`"""
    4.99 +        global has_mercurial
   4.100 +        if has_mercurial:
   4.101 +            yield ("hg", 8)
   4.102 +
   4.103 +    def get_repository(self, type, dir, authname):
   4.104 +        """Return a `MercurialRepository`"""
   4.105 +        if not self._version:
   4.106 +            from mercurial.version import get_version
   4.107 +            self._version = get_version()
   4.108 +            self.env.systeminfo.append(('Mercurial', self._version))
   4.109 +        options = {}
   4.110 +        for key, val in self.config.options(type):
   4.111 +            options[key] = val
   4.112 +        return MercurialRepository(dir, self.log, options)
   4.113 +
   4.114 +
   4.115 +    # IWikiSyntaxProvider methods
   4.116 +    
   4.117 +    def get_wiki_syntax(self):
   4.118 +        yield (r'[0-9a-f]{12,40}', lambda formatter, label, match:
   4.119 +               self._format_link(formatter, 'cset', label, label))
   4.120 +
   4.121 +    def get_link_resolvers(self):
   4.122 +        yield ('cset', self._format_link)
   4.123 +        yield ('chgset', self._format_link)
   4.124 +        yield ('branch', self._format_link)    # go to the corresponding head
   4.125 +        yield ('tag', self._format_link)
   4.126 +
   4.127 +    def _format_link(self, formatter, ns, rev, label):
   4.128 +        repos = self.env.get_repository()
   4.129 +        if ns == 'branch':
   4.130 +            for b, head in repos.get_branches(): ## FIXME
   4.131 +                if b == rev:
   4.132 +                    rev = head
   4.133 +                    break
   4.134 +        try:
   4.135 +            chgset = repos.get_changeset(rev)
   4.136 +            return tag.a(label, class_="changeset",
   4.137 +                         title=shorten_line(chgset.message),
   4.138 +                         href=formatter.href.changeset(rev))
   4.139 +        except NoSuchChangeset, e:
   4.140 +            return tag.a(label, class_="missing changeset",
   4.141 +                         title=to_unicode(e), rel="nofollow",
   4.142 +                         href=formatter.href.changeset(rev))
   4.143 +
   4.144 +### Helpers 
   4.145 +        
   4.146 +class trac_ui(ui):
   4.147 +    def __init__(self):
   4.148 +        ui.__init__(self, interactive=False)
   4.149 +        
   4.150 +    def write(self, *args): pass
   4.151 +    def write_err(self, str): pass
   4.152 +
   4.153 +    def readline(self):
   4.154 +        raise TracError('*** Mercurial ui.readline called ***')
   4.155 +
   4.156 +
   4.157 +
   4.158 +### Version Control API
   4.159 +    
   4.160 +class MercurialRepository(Repository):
   4.161 +    """Repository implementation based on the mercurial API.
   4.162 +
   4.163 +    This wraps a hg.repository object.
   4.164 +    The revision navigation follows the branches, and defaults
   4.165 +    to the first parent/child in case there are many.
   4.166 +    The eventual other parents/children are listed as
   4.167 +    additional changeset properties.
   4.168 +    """
   4.169 +
   4.170 +    def __init__(self, path, log, options):
   4.171 +        self.ui = trac_ui()
   4.172 +        if isinstance(path, unicode):
   4.173 +            str_path = path.encode('utf-8')
   4.174 +            if not os.path.exists(str_path):
   4.175 +                str_path = path.encode('latin-1')
   4.176 +            path = str_path
   4.177 +        self.repo = hg.repository(ui=self.ui, path=path)
   4.178 +        self.path = self.repo.root
   4.179 +        self._show_rev = True
   4.180 +        if 'show_rev' in options and not options['show_rev'] in TRUE:
   4.181 +            self._show_rev = False
   4.182 +        self._node_fmt = 'node_format' in options \
   4.183 +                         and options['node_format']    # will default to 'short'
   4.184 +        if self.path is None:
   4.185 +            raise TracError(path + ' does not appear to ' \
   4.186 +                            'contain a Mercurial repository.')
   4.187 +        Repository.__init__(self, 'hg:%s' % path, None, log)
   4.188 +
   4.189 +    def hg_time(self, timeinfo):
   4.190 +        # [hg b47f96a178a3] introduced an API change:
   4.191 +        if isinstance(timeinfo, tuple): # Mercurial 0.8 
   4.192 +            time = timeinfo[0]
   4.193 +        else:                           # Mercurial 0.7
   4.194 +            time = timeinfo.split()[0]
   4.195 +        return datetime.fromtimestamp(time, utc)
   4.196 +
   4.197 +    def hg_node(self, rev):
   4.198 +        """Return a changelog node for the given revision.
   4.199 +
   4.200 +        `rev` can be any kind of revision specification string.
   4.201 +        If `None`, ''tip'' will be returned.
   4.202 +        """
   4.203 +        try:
   4.204 +            if rev:
   4.205 +                rev = rev.split(':', 1)[0]
   4.206 +                if rev.isdigit():
   4.207 +                    try:
   4.208 +                        return self.repo.changelog.node(rev)
   4.209 +                    except:
   4.210 +                        pass
   4.211 +                return self.repo.lookup(rev)
   4.212 +            return self.repo.changelog.tip()
   4.213 +        except RepoError, e:
   4.214 +            raise NoSuchChangeset(rev)
   4.215 +
   4.216 +    def hg_display(self, n):
   4.217 +        """Return user-readable revision information for node `n`.
   4.218 +
   4.219 +        The specific format depends on the `node_format` and `show_rev` options
   4.220 +        """
   4.221 +        nodestr = self._node_fmt == "hex" and hex(n) or short(n)
   4.222 +        if self._show_rev:
   4.223 +            return '%s:%s' % (self.repo.changelog.rev(n), nodestr)
   4.224 +        else:
   4.225 +            return nodestr
   4.226 +
   4.227 +    def close(self):
   4.228 +        self.repo = None
   4.229 +
   4.230 +    def normalize_path(self, path):
   4.231 +        """Remove leading "/", except for the root"""
   4.232 +        return path and path.strip('/') or ''
   4.233 +
   4.234 +    def normalize_rev(self, rev):
   4.235 +        """Return the changelog node for the specified rev"""
   4.236 +        return self.hg_display(self.hg_node(rev))
   4.237 +
   4.238 +    def short_rev(self, rev):
   4.239 +        """Return the revision number for the specified rev, in compact form.
   4.240 +        """
   4.241 +        if rev:
   4.242 +            if isinstance(rev, basestring) and rev.isdigit():
   4.243 +                rev = int(rev)
   4.244 +            if 0 <= rev < self.repo.changelog.count():
   4.245 +                return rev # it was already a short rev
   4.246 +        return self.repo.changelog.rev(self.hg_node(rev))
   4.247 +
   4.248 +    def get_quickjump_entries(self, rev):
   4.249 +        # branches
   4.250 +        if hasattr(self.repo, 'branchtags'):
   4.251 +            # New 0.9.2 style branches, since [hg 028fff46a4ac]
   4.252 +            for t, n in sorted(self.repo.branchtags().items(), reverse=True,
   4.253 +                               key=lambda (t, n): self.repo.changelog.rev(n)):
   4.254 +                yield ('branches', t, '/', self.hg_display(n))
   4.255 +        else:
   4.256 +            # Old style branches
   4.257 +            heads = self.repo.changelog.heads()
   4.258 +            brinfo = self.repo.branchlookup(heads)
   4.259 +            for head in heads:
   4.260 +                rev = self.hg_display(head)
   4.261 +                if head in brinfo:
   4.262 +                    branch = ' '.join(brinfo[head])
   4.263 +                else:
   4.264 +                    branch = rev
   4.265 +                yield ('(old-style) branches', branch, '/', rev)
   4.266 +        # tags
   4.267 +        for t, n in reversed(self.repo.tagslist()):
   4.268 +            try:
   4.269 +                yield ('tags', t, '/', self.hg_display(n))
   4.270 +            except KeyError:
   4.271 +                pass
   4.272 +
   4.273 +    def get_changeset(self, rev):
   4.274 +        return MercurialChangeset(self, self.hg_node(unicode(rev)))
   4.275 +
   4.276 +    def get_changesets(self, start, stop):
   4.277 +        """Follow each head and parents in order to get all changesets
   4.278 +
   4.279 +        FIXME: this should be handled by the repository cache as well.
   4.280 +        
   4.281 +        The code below is only an heuristic, and doesn't work in the
   4.282 +        general case. E.g. look at the mercurial repository timeline
   4.283 +        for 2006-10-18, you need to give ''38'' daysback in order to see
   4.284 +        the changesets from 2006-10-17...
   4.285 +        This is because of the following '''linear''' sequence of csets:
   4.286 +          - 3445:233c733e4af5         10/18/2006 9:08:36 AM mpm
   4.287 +          - 3446:0b450267cf47         9/10/2006 3:25:06 AM  hopper
   4.288 +          - 3447:ef1032c223e7         9/10/2006 3:25:06 AM  hopper
   4.289 +          - 3448:6ca49c5fe268         9/10/2006 3:25:07 AM  hopper
   4.290 +          - 3449:c8686e3f0291         10/18/2006 9:14:26 AM hopper
   4.291 +          This is most probably because [3446:3448] correspond to
   4.292 +          old changesets that have been ''hg import''ed, with their
   4.293 +          original dates.
   4.294 +        """
   4.295 +        log = self.repo.changelog
   4.296 +        seen = {nullid: 1}
   4.297 +        seeds = log.heads()
   4.298 +        while seeds:
   4.299 +            cn = seeds[0]
   4.300 +            del seeds[0]
   4.301 +            time = self.hg_time(log.read(cn)[2])
   4.302 +            rev = log.rev(cn)
   4.303 +            if time < start:
   4.304 +                continue # assume no ancestor is younger and use next seed
   4.305 +                # (and that assumption is wrong for 3448 in the example above)
   4.306 +            elif time < stop:
   4.307 +                yield MercurialChangeset(self, cn)
   4.308 +            for p in log.parents(cn):
   4.309 +                if p not in seen:
   4.310 +                    seen[p] = 1
   4.311 +                    seeds.append(p)
   4.312 +
   4.313 +    def get_node(self, path, rev=None):
   4.314 +        return MercurialNode(self, self.normalize_path(path), self.hg_node(rev))
   4.315 +
   4.316 +    def get_oldest_rev(self):
   4.317 +        return self.hg_display(nullid)
   4.318 +
   4.319 +    def get_youngest_rev(self):
   4.320 +        return self.hg_display(self.repo.changelog.tip())
   4.321 +
   4.322 +    def previous_rev(self, rev):
   4.323 +        n = self.hg_node(rev)
   4.324 +        log = self.repo.changelog
   4.325 +        parents = [self.hg_display(p) for p in log.parents(n) if p != nullid]
   4.326 +        parents.sort()
   4.327 +        return parents and parents[0] or None
   4.328 +    
   4.329 +    def next_rev(self, rev, path=''): # NOTE: path ignored for now
   4.330 +        n = self.hg_node(rev)
   4.331 +        log = self.repo.changelog
   4.332 +        children = [self.hg_display(c) for c in log.children(n)]
   4.333 +        children.sort()
   4.334 +        return children and children[0] or None
   4.335 +    
   4.336 +    def rev_older_than(self, rev1, rev2):
   4.337 +        log = self.repo.changelog
   4.338 +        return log.rev(self.hg_node(rev1)) < log.rev(self.hg_node(rev2))
   4.339 +
   4.340 +#    def get_path_history(self, path, rev=None, limit=None):
   4.341 +#         path = self.normalize_path(path)
   4.342 +#         rev = self.normalize_rev(rev)
   4.343 +#         expect_deletion = False
   4.344 +#         while rev:
   4.345 +#             if self.has_node(path, rev):
   4.346 +#                 if expect_deletion:
   4.347 +#                     # it was missing, now it's there again:
   4.348 +#                     #  rev+1 must be a delete
   4.349 +#                     yield path, rev+1, Changeset.DELETE
   4.350 +#                 newer = None # 'newer' is the previously seen history tuple
   4.351 +#                 older = None # 'older' is the currently examined history tuple
   4.352 +#                 for p, r in _get_history(path, 0, rev, limit):
   4.353 +#                     older = (p, r, Changeset.ADD)
   4.354 +#                     rev = self.previous_rev(r)
   4.355 +#                     if newer:
   4.356 +#                         if older[0] == path:
   4.357 +#                             # still on the path: 'newer' was an edit
   4.358 +#                             yield newer[0], newer[1], Changeset.EDIT
   4.359 +#                         else:
   4.360 +#                             # the path changed: 'newer' was a copy
   4.361 +#                             rev = self.previous_rev(newer[1])
   4.362 +#                             # restart before the copy op
   4.363 +#                             yield newer[0], newer[1], Changeset.COPY
   4.364 +#                             older = (older[0], older[1], 'unknown')
   4.365 +#                             break
   4.366 +#                     newer = older
   4.367 +#                 if older:
   4.368 +#                     # either a real ADD or the source of a COPY
   4.369 +#                     yield older
   4.370 +#             else:
   4.371 +#                 expect_deletion = True
   4.372 +#                 rev = self.previous_rev(rev)
   4.373 +
   4.374 +    def sync(self):
   4.375 +        pass
   4.376 +
   4.377 +
   4.378 +class MercurialNode(Node):
   4.379 +    """A path in the repository, at a given revision.
   4.380 +
   4.381 +    It encapsulates the repository manifest for the given revision.
   4.382 +
   4.383 +    As directories are not first-class citizens in Mercurial,
   4.384 +    retrieving revision information for directory is much slower than
   4.385 +    for files.
   4.386 +    """
   4.387 +
   4.388 +    def __init__(self, repos, path, n, manifest=None, mflags=None):
   4.389 +        self.repos = repos
   4.390 +        self.n = n
   4.391 +        log = repos.repo.changelog
   4.392 +        
   4.393 +        if not manifest:
   4.394 +            manifest_n = log.read(n)[0] # 0: manifest node
   4.395 +            manifest = repos.repo.manifest.read(manifest_n)
   4.396 +            if hasattr(repos.repo.manifest, 'readflags'):
   4.397 +                mflags = repos.repo.manifest.readflags(manifest_n)
   4.398 +        self.manifest = manifest
   4.399 +        self.mflags = mflags
   4.400 +        if isinstance(path, unicode):
   4.401 +            try:
   4.402 +                self._init_path(log, path.encode('utf-8'))
   4.403 +            except NoSuchNode:
   4.404 +                self._init_path(log, path.encode('latin-1'))
   4.405 +                # TODO: configurable charset for the repository, i.e. #3809
   4.406 +        else:
   4.407 +            self._init_path(log, path)
   4.408 +
   4.409 +    def _init_path(self, log, path):
   4.410 +        kind = None
   4.411 +        if path in self.manifest: # then it's a file
   4.412 +            kind = Node.FILE
   4.413 +            self.file_n = self.manifest[path]
   4.414 +            self.file = self.repos.repo.file(path)
   4.415 +            log_rev = self.file.linkrev(self.file_n)
   4.416 +            node = log.node(log_rev)
   4.417 +        else: # it will be a directory if there are matching entries
   4.418 +            dir = path and path+'/' or ''
   4.419 +            entries = {}
   4.420 +            newest = -1
   4.421 +            for file in self.manifest.keys():
   4.422 +                if file.startswith(dir):
   4.423 +                    entry = file[len(dir):].split('/', 1)[0]
   4.424 +                    entries[entry] = 1
   4.425 +                    if path: # small optimization: we skip this for root node
   4.426 +                        file_n = self.manifest[file]
   4.427 +                        log_rev = self.repos.repo.file(file).linkrev(file_n)
   4.428 +                        newest = max(log_rev, newest)
   4.429 +            if entries:
   4.430 +                kind = Node.DIRECTORY
   4.431 +                self.entries = entries.keys()
   4.432 +                if newest >= 0:
   4.433 +                    node = log.node(newest)
   4.434 +                else: # ... as it's the youngest anyway
   4.435 +                    node = log.tip()
   4.436 +        if not kind:
   4.437 +            if log.tip() == nullid: # empty repository
   4.438 +                kind = Node.DIRECTORY
   4.439 +                self.entries = []
   4.440 +                node = nullid
   4.441 +            else:
   4.442 +                raise NoSuchNode(path, self.repos.hg_display(self.n))
   4.443 +        self.time = self.repos.hg_time(log.read(node)[2])
   4.444 +        rev = self.repos.hg_display(node)
   4.445 +        Node.__init__(self, path, rev, kind)
   4.446 +        self.created_path = path
   4.447 +        self.created_rev = rev
   4.448 +        self.data = None
   4.449 +
   4.450 +    def get_content(self):
   4.451 +        if self.isdir:
   4.452 +            return None
   4.453 +        self.pos = 0 # reset the read()
   4.454 +        return self # something that can be `read()` ...
   4.455 +
   4.456 +    def read(self, size=None):
   4.457 +        if self.isdir:
   4.458 +            return TracError("Can't read from directory %s" % self.path)
   4.459 +        if self.data is None:
   4.460 +            self.data = self.file.read(self.file_n)
   4.461 +            self.pos = 0
   4.462 +        if size:
   4.463 +            prev_pos = self.pos
   4.464 +            self.pos += size
   4.465 +            return self.data[prev_pos:self.pos]
   4.466 +        return self.data
   4.467 +
   4.468 +    def get_entries(self):
   4.469 +        if self.isfile:
   4.470 +            return
   4.471 +        for entry in self.entries:
   4.472 +            if self.path:
   4.473 +                entry = posixpath.join(self.path, entry)
   4.474 +            yield MercurialNode(self.repos, entry, self.n,
   4.475 +                                self.manifest, self.mflags)
   4.476 +
   4.477 +    def get_history(self, limit=None):
   4.478 +        newer = None # 'newer' is the previously seen history tuple
   4.479 +        older = None # 'older' is the currently examined history tuple
   4.480 +        repo = self.repos.repo
   4.481 +        log = repo.changelog
   4.482 +        
   4.483 +        # directory history
   4.484 +        if self.isdir:
   4.485 +            if not self.path: # special case for the root
   4.486 +                for r in xrange(log.rev(self.n), -1, -1):
   4.487 +                    yield ('', self.repos.hg_display(log.node(r)),
   4.488 +                           r and Changeset.EDIT or Changeset.ADD)
   4.489 +                return
   4.490 +            getchange = cachefunc(lambda r:repo.changectx(r).changeset())
   4.491 +            pats = ['path:' + self.path]
   4.492 +            opts = {'rev': ['%s:0' % hex(self.n)]}
   4.493 +            wcr = walkchangerevs(self.repos.ui, repo, pats, getchange, opts)
   4.494 +            for st, rev, fns in wcr[0]:
   4.495 +                if st == 'iter':
   4.496 +                    yield (self.path, self.repos.hg_display(log.node(rev)),
   4.497 +                           Changeset.EDIT)
   4.498 +            return
   4.499 +        # file history
   4.500 +        # FIXME: COPY currently unsupported        
   4.501 +        for file_rev in xrange(self.file.rev(self.file_n), -1, -1):
   4.502 +            rev = log.node(self.file.linkrev(self.file.node(file_rev)))
   4.503 +            older = (self.path, self.repos.hg_display(rev), Changeset.ADD)
   4.504 +            if newer:
   4.505 +                change = newer[0] == older[0] and Changeset.EDIT or \
   4.506 +                         Changeset.COPY
   4.507 +                newer = (newer[0], newer[1], change)
   4.508 +                yield newer
   4.509 +            newer = older
   4.510 +        if newer:
   4.511 +            yield newer
   4.512 +
   4.513 +    def get_annotations(self):
   4.514 +        from mercurial.context import filectx
   4.515 +        fctx = filectx(self.repos.repo, self.path, self.n)
   4.516 +        annotations = []
   4.517 +        for fc, line in fctx.annotate(follow=True):
   4.518 +            annotations.append(fc.rev())
   4.519 +        return annotations
   4.520 +        
   4.521 +    def get_properties(self):
   4.522 +        if self.isfile:
   4.523 +            if self.mflags: # Mercurial upto 9.1
   4.524 +                exe = self.mflags[self.path]
   4.525 +            else: # assume Mercurial version >= [abd9a05fca0b]
   4.526 +                exe = self.manifest.execf(self.path)
   4.527 +            return exe and {'exe': '*'} or {}
   4.528 +        return {}
   4.529 +    # FIXME++: implement pset/pget/plist etc. in hg
   4.530 +    # (hm, extended changelog is about the *changelog*, not the manifest...)
   4.531 +
   4.532 +    def get_content_length(self):
   4.533 +        if self.isdir:
   4.534 +            return None
   4.535 +        # since 441ea218414e, i.e. shortly after 0.8.1
   4.536 +        return self.file.size(self.file.rev(self.file_n))
   4.537 +
   4.538 +    def get_content_type(self):
   4.539 +        if self.isdir:
   4.540 +            return None
   4.541 +        return ''
   4.542 +
   4.543 +    def get_last_modified(self):
   4.544 +        return self.time
   4.545 +
   4.546 +
   4.547 +class MercurialChangeset(Changeset):
   4.548 +    """A changeset in the repository.
   4.549 +
   4.550 +    This wraps the corresponding information from the changelog.
   4.551 +    The files changes are obtained by comparing the current manifest
   4.552 +    to the parent manifest(s).
   4.553 +    """
   4.554 +    
   4.555 +    def __init__(self, repos, n):
   4.556 +        log = repos.repo.changelog
   4.557 +        log_data = log.read(n)
   4.558 +        manifest, user, timeinfo, files, desc = log_data[:5]
   4.559 +        extra = {}
   4.560 +        if len(log_data) > 5: # extended changelog, since [hg 2f35961854fb]
   4.561 +            extra = log_data[5]
   4.562 +        time = repos.hg_time(timeinfo)
   4.563 +        Changeset.__init__(self, repos.hg_display(n), to_unicode(desc),
   4.564 +                           to_unicode(user), time)
   4.565 +        self.repos = repos
   4.566 +        self.n = n
   4.567 +        self.manifest_n = manifest
   4.568 +        self.files = files
   4.569 +        self.parents = [repos.hg_display(p) for p in log.parents(n) \
   4.570 +                        if p != nullid]
   4.571 +        self.children = [repos.hg_display(c) for c in log.children(n)]
   4.572 +        self.tags = [t for t in repos.repo.nodetags(n)]
   4.573 +        self.extra = extra
   4.574 +
   4.575 +    if has_property_renderer:
   4.576 +        def get_properties(self):
   4.577 +            properties = {}
   4.578 +            if len(self.parents) > 1:
   4.579 +                properties['Parents'] = self.parents
   4.580 +            if len(self.children) > 1:
   4.581 +                properties['Children'] = self.children
   4.582 +            if len(self.tags):
   4.583 +                properties['Tags'] = self.tags
   4.584 +            return properties
   4.585 +    else: # remove once 0.11 is released
   4.586 +        def get_properties(self):
   4.587 +            def changeset_links(csets):
   4.588 +                return ' '.join(['[cset:%s]' % cset for cset in csets])
   4.589 +            if len(self.parents) > 1:
   4.590 +                yield ('Parents', changeset_links(self.parents), True, 'changeset')
   4.591 +            if len(self.children) > 1:
   4.592 +                yield ('Children', changeset_links(self.children), True, 'changeset')
   4.593 +            if len(self.tags):
   4.594 +                yield ('Tags', changeset_links(self.tags), True, 'changeset')
   4.595 +            for k, v in self.extra.iteritems():
   4.596 +                yield (k, v, False, 'message')
   4.597 +
   4.598 +    def get_changes(self):
   4.599 +        repo = self.repos.repo
   4.600 +        log = repo.changelog
   4.601 +        parents = log.parents(self.n)
   4.602 +        manifest = repo.manifest.read(self.manifest_n)
   4.603 +        manifest1 = manifest2 = None
   4.604 +        if parents:
   4.605 +            man_node1 = log.read(parents[0])[0]
   4.606 +            manifest1 = repo.manifest.read(man_node1)
   4.607 +            if len(parents) > 1:
   4.608 +                man_node2 = log.read(parents[1])[0]
   4.609 +                manifest2 = repo.manifest.read(man_node2)
   4.610 +
   4.611 +        deletions = {}
   4.612 +        def detect_delete(pmanifest, p):
   4.613 +            for f in pmanifest.keys():
   4.614 +                if f not in manifest:
   4.615 +                    deletions[f] = p
   4.616 +        if manifest1:
   4.617 +            detect_delete(manifest1, self.parents[0])
   4.618 +        if manifest2:
   4.619 +            detect_delete(manifest2, self.parents[1])
   4.620 +
   4.621 +        renames = {}
   4.622 +        changes = []
   4.623 +        for f in self.files: # 'added' and 'edited' files
   4.624 +            if f in deletions: # and since Mercurial > 0.7 [hg c6ffedc4f11b]
   4.625 +                continue          # also 'deleted' files
   4.626 +            action = None
   4.627 +            # TODO: find a way to detect conflicts and show how they were solved
   4.628 +            if manifest1 and f in manifest1:
   4.629 +                action = Changeset.EDIT                
   4.630 +                changes.append((f, Node.FILE, action, f, self.parents[0]))
   4.631 +            if manifest2 and f in manifest2:
   4.632 +                action = Changeset.EDIT                
   4.633 +                changes.append((f, Node.FILE, action, f, self.parents[1]))
   4.634 +
   4.635 +            if not action:
   4.636 +                rename_info = repo.file(f).renamed(manifest[f])
   4.637 +                if rename_info:
   4.638 +                    base_path = rename_info[0]
   4.639 +                    linkedrev = repo.file(base_path).linkrev(rename_info[1])
   4.640 +                    base_rev = self.repos.hg_display(log.node(linkedrev))
   4.641 +                    if base_path in deletions:
   4.642 +                        action = Changeset.MOVE
   4.643 +                        renames[base_path] = f
   4.644 +                    else:
   4.645 +                        action = Changeset.COPY
   4.646 +                else:
   4.647 +                    action = Changeset.ADD
   4.648 +                    base_path = ''
   4.649 +                    base_rev = None
   4.650 +                changes.append((f, Node.FILE, action, base_path, base_rev))
   4.651 +
   4.652 +        for f, p in deletions.items():
   4.653 +            if f not in renames:
   4.654 +                changes.append((f, Node.FILE, Changeset.DELETE, f, p))
   4.655 +        changes.sort()
   4.656 +        for change in changes:
   4.657 +            yield change
   4.658 +
     5.1 --- a/tracvc/__init__.py	Tue Jul 03 09:22:15 2007 -0600
     5.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.3 @@ -1,1 +0,0 @@
     5.4 -__import__('pkg_resources').declare_namespace(__name__)
     6.1 --- a/tracvc/hg/__init__.py	Tue Jul 03 09:22:15 2007 -0600
     6.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.3 @@ -1,2 +0,0 @@
     6.4 -
     6.5 -
     7.1 --- a/tracvc/hg/backend.py	Tue Jul 03 09:22:15 2007 -0600
     7.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.3 @@ -1,593 +0,0 @@
     7.4 -# -*- coding: iso-8859-1 -*-
     7.5 -#
     7.6 -# Copyright (C) 2005 Edgewall Software
     7.7 -# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
     7.8 -# All rights reserved.
     7.9 -#
    7.10 -# This software may be used and distributed according to the terms
    7.11 -# of the GNU General Public License, incorporated herein by reference.
    7.12 -#
    7.13 -# Author: Christian Boos <cboos@neuf.fr>
    7.14 -
    7.15 -from __future__ import generators
    7.16 -
    7.17 -import os
    7.18 -import time
    7.19 -import posixpath
    7.20 -import re
    7.21 -
    7.22 -from trac.core import *
    7.23 -from trac.config import _TRUE_VALUES as TRUE
    7.24 -from trac.util.text import shorten_line, to_unicode
    7.25 -from trac.util.html import escape, html
    7.26 -from trac.versioncontrol import Changeset, Node, Repository, \
    7.27 -                                IRepositoryConnector, \
    7.28 -                                NoSuchChangeset, NoSuchNode
    7.29 -from trac.versioncontrol.web_ui import ChangesetModule, BrowserModule
    7.30 -from trac.wiki import IWikiSyntaxProvider
    7.31 -
    7.32 -try:
    7.33 -    # The new `demandimport` mechanism doesn't play well with code relying
    7.34 -    # on the `ImportError` exception being caught.
    7.35 -    # OTOH, we can't disable `demandimport` because mercurial relies on it
    7.36 -    # (circular reference issue). So for now, we activate `demandimport`
    7.37 -    # before loading mercurial modules, and desactivate it afterwards.
    7.38 -    #
    7.39 -    # See http://www.selenic.com/mercurial/bts/issue605
    7.40 -    
    7.41 -    try:
    7.42 -        from mercurial import demandimport
    7.43 -        demandimport.enable();
    7.44 -    except ImportError:
    7.45 -        demandimport = None
    7.46 -
    7.47 -    from mercurial import hg
    7.48 -    from mercurial.ui import ui
    7.49 -    from mercurial.repo import RepoError
    7.50 -    from mercurial.node import hex, short, nullid
    7.51 -    from mercurial.util import pathto
    7.52 -    try:
    7.53 -        # for most recent version (hg:chgset:731e739b8659 at 2006-11-15)
    7.54 -        from mercurial.cmdutil import walkchangerevs
    7.55 -    except ImportError:
    7.56 -        # for older version
    7.57 -        from mercurial.commands import walkchangerevs
    7.58 -
    7.59 -    if demandimport:
    7.60 -        demandimport.disable();
    7.61 -    has_mercurial = True
    7.62 -except ImportError:
    7.63 -    has_mercurial = False
    7.64 -    ui = object
    7.65 -
    7.66 -
    7.67 -### Components
    7.68 -
    7.69 -class MercurialConnector(Component):
    7.70 -
    7.71 -    implements(IRepositoryConnector, IWikiSyntaxProvider)
    7.72 -
    7.73 -    def get_supported_types(self):
    7.74 -        """Support for `repository_type = hg`"""
    7.75 -        global has_mercurial
    7.76 -        if has_mercurial:
    7.77 -            yield ("hg", 8)
    7.78 -
    7.79 -    def get_repository(self, type, dir, authname):
    7.80 -        """Return a `MercurialRepository`"""
    7.81 -        options = {}
    7.82 -        for key, val in self.config.options(type):
    7.83 -            options[key] = val
    7.84 -        return MercurialRepository(dir, self.log, options)
    7.85 -
    7.86 -
    7.87 -    # IWikiSyntaxProvider methods
    7.88 -    
    7.89 -    def get_wiki_syntax(self):
    7.90 -        yield (r'[0-9a-f]{12,40}', lambda formatter, label, match:
    7.91 -               self._format_link(formatter, 'cset', label, label))
    7.92 -
    7.93 -    def get_link_resolvers(self):
    7.94 -        yield ('cset', self._format_link)
    7.95 -        yield ('chgset', self._format_link)
    7.96 -        yield ('branch', self._format_link)    # go to the corresponding head
    7.97 -        yield ('tag', self._format_link)
    7.98 -
    7.99 -    def _format_link(self, formatter, ns, rev, label):
   7.100 -        repos = self.env.get_repository()
   7.101 -        if ns == 'branch':
   7.102 -            for b, head in repos.get_branches():
   7.103 -                if b == rev:
   7.104 -                    rev = head
   7.105 -                    break
   7.106 -        try:
   7.107 -            chgset = repos.get_changeset(rev)
   7.108 -            return html.a(label, class_="changeset",
   7.109 -                          title=shorten_line(chgset.message),
   7.110 -                          href=formatter.href.changeset(rev))
   7.111 -        except NoSuchChangeset, e:
   7.112 -            return html.a(label, class_="missing changeset",
   7.113 -                          title=to_unicode(e), rel="nofollow",
   7.114 -                          href=formatter.href.changeset(rev))
   7.115 -
   7.116 -
   7.117 -### Helpers 
   7.118 -        
   7.119 -class trac_ui(ui):
   7.120 -    def __init__(self):
   7.121 -        ui.__init__(self, interactive=False)
   7.122 -        
   7.123 -    def write(self, *args): pass
   7.124 -    def write_err(self, str): pass
   7.125 -
   7.126 -    def readline(self):
   7.127 -        raise TracError('*** Mercurial ui.readline called ***')
   7.128 -
   7.129 -
   7.130 -
   7.131 -### Version Control API
   7.132 -    
   7.133 -class MercurialRepository(Repository):
   7.134 -    """Repository implementation based on the mercurial API.
   7.135 -
   7.136 -    This wraps a hg.repository object.
   7.137 -    The revision navigation follows the branches, and defaults
   7.138 -    to the first parent/child in case there are many.
   7.139 -    The eventual other parents/children are listed as
   7.140 -    additional changeset properties.
   7.141 -    """
   7.142 -
   7.143 -    def __init__(self, path, log, options):
   7.144 -        self.ui = trac_ui()
   7.145 -        if isinstance(path, unicode):
   7.146 -            str_path = path.encode('utf-8')
   7.147 -            if not os.path.exists(str_path):
   7.148 -                str_path = path.encode('latin-1')
   7.149 -            path = str_path
   7.150 -        self.repo = hg.repository(ui=self.ui, path=path)
   7.151 -        self.path = self.repo.root
   7.152 -        self._show_rev = True
   7.153 -        if 'show_rev' in options and not options['show_rev'] in TRUE:
   7.154 -            self._show_rev = False
   7.155 -        self._node_fmt = 'node_format' in options \
   7.156 -                         and options['node_format']    # will default to 'short'
   7.157 -        if self.path is None:
   7.158 -            raise TracError(path + ' does not appear to ' \
   7.159 -                            'contain a Mercurial repository.')
   7.160 -        Repository.__init__(self, 'hg:%s' % path, None, log)
   7.161 -
   7.162 -    def hg_time(self, timeinfo):
   7.163 -        # [hg b47f96a178a3] introduced an API change:
   7.164 -        if isinstance(timeinfo, tuple): # Mercurial 0.8 
   7.165 -            time = timeinfo[0]
   7.166 -        else:                           # Mercurial 0.7
   7.167 -            time = timeinfo.split()[0]
   7.168 -        return int(time)
   7.169 -
   7.170 -    def hg_node(self, rev):
   7.171 -        try:
   7.172 -            if rev:
   7.173 -                m = re.match(r"(\d+):", rev)
   7.174 -                if m:
   7.175 -                    rev = m.group(1)
   7.176 -                return self.repo.lookup(rev)
   7.177 -            return self.repo.changelog.tip()
   7.178 -        except RepoError, e:
   7.179 -            raise NoSuchChangeset(rev)
   7.180 -
   7.181 -    def hg_display(self, n):
   7.182 -        nodestr = self._node_fmt == "hex" and hex(n) or short(n)
   7.183 -        if self._show_rev:
   7.184 -            return '%s:%s' % (self.repo.changelog.rev(n), nodestr)
   7.185 -        else:
   7.186 -            return nodestr
   7.187 -
   7.188 -    def close(self):
   7.189 -        self.repo = None
   7.190 -
   7.191 -    def normalize_path(self, path):
   7.192 -        """Remove leading "/", except for the root"""
   7.193 -        return path and path.strip('/') or ''
   7.194 -
   7.195 -    def normalize_rev(self, rev):
   7.196 -        """Return the changelog node for the specified rev"""
   7.197 -        return self.hg_display(self.hg_node(rev))
   7.198 -
   7.199 -    def short_rev(self, rev):
   7.200 -        """Return the revision number for the specified rev"""
   7.201 -        return self.repo.changelog.rev(self.hg_node(rev))
   7.202 -
   7.203 -    def get_changeset(self, rev):
   7.204 -        return MercurialChangeset(self, self.hg_node(rev))
   7.205 -
   7.206 -    def get_changesets(self, start, stop):
   7.207 -        """Follow each head and parents in order to get all changesets
   7.208 -
   7.209 -        FIXME: this should be handled by the repository cache as well.
   7.210 -        
   7.211 -        The code below is only an heuristic, and doesn't work in the
   7.212 -        general case. E.g. look at the mercurial repository timeline
   7.213 -        for 2006-10-18, you need to give ''38'' daysback in order to see
   7.214 -        the changesets from 2006-10-17...
   7.215 -        This is because of the following '''linear''' sequence of csets:
   7.216 -          - 3445:233c733e4af5         10/18/2006 9:08:36 AM mpm
   7.217 -          - 3446:0b450267cf47         9/10/2006 3:25:06 AM  hopper
   7.218 -          - 3447:ef1032c223e7         9/10/2006 3:25:06 AM  hopper
   7.219 -          - 3448:6ca49c5fe268         9/10/2006 3:25:07 AM  hopper
   7.220 -          - 3449:c8686e3f0291         10/18/2006 9:14:26 AM hopper
   7.221 -          This is most probably because [3446:3448] correspond to
   7.222 -          old changesets that have been ''hg import''ed, with their
   7.223 -          original dates.
   7.224 -        """
   7.225 -        log = self.repo.changelog
   7.226 -        seen = {nullid: 1}
   7.227 -        seeds = log.heads()
   7.228 -        while seeds:
   7.229 -            cn = seeds[0]
   7.230 -            del seeds[0]
   7.231 -            time = self.hg_time(log.read(cn)[2])
   7.232 -            rev = log.rev(cn)
   7.233 -            if time < start:
   7.234 -                continue # assume no ancestor is younger and use next seed
   7.235 -                # (and that assumption is wrong for 3448 in the example above)
   7.236 -            elif time < stop:
   7.237 -                yield MercurialChangeset(self, cn)
   7.238 -            for p in log.parents(cn):
   7.239 -                if p not in seen:
   7.240 -                    seen[p] = 1
   7.241 -                    seeds.append(p)
   7.242 -
   7.243 -    def get_node(self, path, rev=None):
   7.244 -        return MercurialNode(self, self.normalize_path(path), self.hg_node(rev))
   7.245 -
   7.246 -    def get_tags(self, rev):
   7.247 -        for tag, n in self.repo.tagslist():
   7.248 -            yield (tag, self.hg_display(n))
   7.249 -
   7.250 -    def get_branches(self, rev):
   7.251 -        heads = self.repo.changelog.heads()
   7.252 -        brinfo = self.repo.branchlookup(heads)
   7.253 -        for head in heads:
   7.254 -            rev = self.hg_display(head)
   7.255 -            if head in brinfo:
   7.256 -                branch = ' '.join(brinfo[head])
   7.257 -            else:
   7.258 -                branch = rev
   7.259 -            yield (branch, rev)
   7.260 -            
   7.261 -    def get_oldest_rev(self):
   7.262 -        return self.hg_display(nullid)
   7.263 -
   7.264 -    def get_youngest_rev(self):
   7.265 -        return self.hg_display(self.repo.changelog.tip())
   7.266 -
   7.267 -    def previous_rev(self, rev):
   7.268 -        n = self.hg_node(rev)
   7.269 -        log = self.repo.changelog
   7.270 -        parents = [self.hg_display(p) for p in log.parents(n) if p != nullid]
   7.271 -        parents.sort()
   7.272 -        return parents and parents[0] or None
   7.273 -    
   7.274 -    def next_rev(self, rev, path=''): # NOTE: path ignored for now
   7.275 -        n = self.hg_node(rev)
   7.276 -        log = self.repo.changelog
   7.277 -        children = [self.hg_display(c) for c in log.children(n)]
   7.278 -        children.sort()
   7.279 -        return children and children[0] or None
   7.280 -    
   7.281 -    def rev_older_than(self, rev1, rev2):
   7.282 -        log = self.repo.changelog
   7.283 -        return log.rev(self.hg_node(rev1)) < log.rev(self.hg_node(rev2))
   7.284 -
   7.285 -#    def get_path_history(self, path, rev=None, limit=None):
   7.286 -#         path = self.normalize_path(path)
   7.287 -#         rev = self.normalize_rev(rev)
   7.288 -#         expect_deletion = False
   7.289 -#         while rev:
   7.290 -#             if self.has_node(path, rev):
   7.291 -#                 if expect_deletion:
   7.292 -#                     # it was missing, now it's there again:
   7.293 -#                     #  rev+1 must be a delete
   7.294 -#                     yield path, rev+1, Changeset.DELETE
   7.295 -#                 newer = None # 'newer' is the previously seen history tuple
   7.296 -#                 older = None # 'older' is the currently examined history tuple
   7.297 -#                 for p, r in _get_history(path, 0, rev, limit):
   7.298 -#                     older = (p, r, Changeset.ADD)
   7.299 -#                     rev = self.previous_rev(r)
   7.300 -#                     if newer:
   7.301 -#                         if older[0] == path:
   7.302 -#                             # still on the path: 'newer' was an edit
   7.303 -#                             yield newer[0], newer[1], Changeset.EDIT
   7.304 -#                         else:
   7.305 -#                             # the path changed: 'newer' was a copy
   7.306 -#                             rev = self.previous_rev(newer[1])
   7.307 -#                             # restart before the copy op
   7.308 -#                             yield newer[0], newer[1], Changeset.COPY
   7.309 -#                             older = (older[0], older[1], 'unknown')
   7.310 -#                             break
   7.311 -#                     newer = older
   7.312 -#                 if older:
   7.313 -#                     # either a real ADD or the source of a COPY
   7.314 -#                     yield older
   7.315 -#             else:
   7.316 -#                 expect_deletion = True
   7.317 -#                 rev = self.previous_rev(rev)
   7.318 -
   7.319 -    def sync(self):
   7.320 -        pass
   7.321 -
   7.322 -
   7.323 -class MercurialNode(Node):
   7.324 -    """A path in the repository, at a given revision.
   7.325 -
   7.326 -    It encapsulates the repository manifest for the given revision.
   7.327 -
   7.328 -    As directories are not first-class citizens in Mercurial,
   7.329 -    retrieving revision information for directory is much slower than
   7.330 -    for files.
   7.331 -    """
   7.332 -
   7.333 -    def __init__(self, repos, path, n, manifest=None, mflags=None):
   7.334 -        self.repos = repos
   7.335 -        self.n = n
   7.336 -        log = repos.repo.changelog
   7.337 -        
   7.338 -        if not manifest:
   7.339 -            manifest_n = log.read(n)[0] # 0: manifest node
   7.340 -            manifest = repos.repo.manifest.read(manifest_n)
   7.341 -            if hasattr(repos.repo.manifest, 'readflags'):
   7.342 -                mflags = repos.repo.manifest.readflags(manifest_n)
   7.343 -        self.manifest = manifest
   7.344 -        self.mflags = mflags
   7.345 -        if isinstance(path, unicode):
   7.346 -            try:
   7.347 -                self._init_path(log, path.encode('utf-8'))
   7.348 -            except NoSuchNode:
   7.349 -                self._init_path(log, path.encode('latin-1'))
   7.350 -                # TODO: configurable charset for the repository, i.e. #3809
   7.351 -        else:
   7.352 -            self._init_path(log, path)
   7.353 -
   7.354 -    def _init_path(self, log, path):
   7.355 -        kind = None
   7.356 -        if path in self.manifest: # then it's a file
   7.357 -            kind = Node.FILE
   7.358 -            file_n = self.manifest[path]
   7.359 -            log_rev = self.repos.repo.file(path).linkrev(file_n)
   7.360 -            node = log.node(log_rev)
   7.361 -        else: # it will be a directory if there are matching entries
   7.362 -            dir = path and path+'/' or ''
   7.363 -            entries = {}
   7.364 -            newest = -1
   7.365 -            for file in self.manifest.keys():
   7.366 -                if file.startswith(dir):
   7.367 -                    entry = file[len(dir):].split('/', 1)[0]
   7.368 -                    entries[entry] = 1
   7.369 -                    if path: # small optimization: we skip this for root node
   7.370 -                        file_n = self.manifest[file]
   7.371 -                        log_rev = self.repos.repo.file(file).linkrev(file_n)
   7.372 -                        newest = max(log_rev, newest)
   7.373 -            if entries:
   7.374 -                kind = Node.DIRECTORY
   7.375 -                self.entries = entries.keys()
   7.376 -                if newest >= 0:
   7.377 -                    node = log.node(newest)
   7.378 -                else: # ... as it's the youngest anyway
   7.379 -                    node = log.tip()
   7.380 -        if not kind:
   7.381 -            if log.tip() == nullid: # empty repository
   7.382 -                kind = Node.DIRECTORY
   7.383 -                self.entries = []
   7.384 -                node = nullid
   7.385 -            else:
   7.386 -                raise NoSuchNode(path, self.repos.hg_display(self.n))
   7.387 -        self.time = self.repos.hg_time(log.read(node)[2])
   7.388 -        rev = self.repos.hg_display(node)
   7.389 -        Node.__init__(self, path, rev, kind)
   7.390 -        self.created_path = path
   7.391 -        self.created_rev = rev
   7.392 -        self.data = None
   7.393 -
   7.394 -    def get_content(self):
   7.395 -        if self.isdir:
   7.396 -            return None
   7.397 -        self.pos = 0 # reset the read()
   7.398 -        return self # something that can be `read()` ...
   7.399 -
   7.400 -    def read(self, size=None):
   7.401 -        if self.isdir:
   7.402 -            return TracError("Can't read from directory %s" % self.path)
   7.403 -        file_n = self.manifest[self.path]
   7.404 -        file = self.repos.repo.file(self.path)
   7.405 -        if self.data is None:
   7.406 -            self.data = file.read(file_n)
   7.407 -            self.pos = 0
   7.408 -        if size:
   7.409 -            prev_pos = self.pos
   7.410 -            self.pos += size
   7.411 -            return self.data[prev_pos:self.pos]
   7.412 -        return self.data
   7.413 -
   7.414 -    def get_entries(self):
   7.415 -        if self.isfile:
   7.416 -            return
   7.417 -        for entry in self.entries:
   7.418 -            if self.path:
   7.419 -                entry = posixpath.join(self.path, entry)
   7.420 -            yield MercurialNode(self.repos, entry, self.n,
   7.421 -                                self.manifest, self.mflags)
   7.422 -
   7.423 -    def get_history(self, limit=None):
   7.424 -        newer = None # 'newer' is the previously seen history tuple
   7.425 -        older = None # 'older' is the currently examined history tuple
   7.426 -        log = self.repos.repo.changelog
   7.427 -        # directory history
   7.428 -        if self.isdir:
   7.429 -            if not self.path: # special case for the root
   7.430 -                for r in xrange(log.rev(self.n), -1, -1):
   7.431 -                    yield ('', self.repos.hg_display(log.node(r)),
   7.432 -                           r and Changeset.EDIT or Changeset.ADD)
   7.433 -                return
   7.434 -            # Code compatibility for ''walkchangerevs'':
   7.435 -            # In Mercurial 0.7, it had 5 arguments, but
   7.436 -            # [hg 1d7d0c07e8f3] removed the 3rd argument ('cwd').
   7.437 -            args = (self.repos.ui, self.repos.repo)
   7.438 -            if walkchangerevs.func_code.co_argcount == 5:
   7.439 -                args = args + (None,)
   7.440 -            args = args + (['path:%s' % self.path],
   7.441 -                           {'rev': ['%s:0' % hex(self.n)]})
   7.442 -            wcr = walkchangerevs(*args)
   7.443 -                         
   7.444 -            matches = {}
   7.445 -            for st, rev, fns in wcr[0]:
   7.446 -                if st == 'window':
   7.447 -                    matches.clear()
   7.448 -                elif st == 'add':
   7.449 -                    matches[rev] = 1
   7.450 -                elif st == 'iter':
   7.451 -                    if matches[rev]:
   7.452 -                        yield (self.path, self.repos.hg_display(log.node(rev)),
   7.453 -                               Changeset.EDIT)
   7.454 -            return
   7.455 -        # file history
   7.456 -        file_n = self.manifest[self.path]
   7.457 -        file = self.repos.repo.file(self.path)
   7.458 -        # FIXME: COPY currently unsupported        
   7.459 -        for file_rev in xrange(file.rev(file_n), -1, -1):
   7.460 -            rev = log.node(file.linkrev(file.node(file_rev)))
   7.461 -            older = (self.path, self.repos.hg_display(rev), Changeset.ADD)
   7.462 -            if newer:
   7.463 -                change = newer[0] == older[0] and Changeset.EDIT or \
   7.464 -                         Changeset.COPY
   7.465 -                newer = (newer[0], newer[1], change)
   7.466 -                yield newer
   7.467 -            newer = older
   7.468 -        if newer:
   7.469 -            yield newer
   7.470 -
   7.471 -    def get_properties(self):
   7.472 -        if self.isfile:
   7.473 -            if self.mflags: # Mercurial upto 9.1
   7.474 -                exe = self.mflags[self.path]
   7.475 -            else: # assume Mercurial version >= [abd9a05fca0b]
   7.476 -                exe = self.manifest.execf(self.path)
   7.477 -            return exe and {'exe': '*'} or {}
   7.478 -        return {}
   7.479 -    # FIXME++: implement pset/pget/plist etc. in hg
   7.480 -    # (hm, extended changelog is about the *changelog*, not the manifest...)
   7.481 -
   7.482 -    def get_content_length(self):
   7.483 -        if self.isdir:
   7.484 -            return None
   7.485 -        return len(self.read())
   7.486 -
   7.487 -    def get_content_type(self):
   7.488 -        if self.isdir:
   7.489 -            return None
   7.490 -        return ''
   7.491 -
   7.492 -    def get_last_modified(self):
   7.493 -        return self.time
   7.494 -
   7.495 -
   7.496 -class MercurialChangeset(Changeset):
   7.497 -    """A changeset in the repository.
   7.498 -
   7.499 -    This wraps the corresponding information from the changelog.
   7.500 -    The files changes are obtained by comparing the current manifest
   7.501 -    to the parent manifest(s).
   7.502 -    """
   7.503 -    
   7.504 -    def __init__(self, repos, n):
   7.505 -        log = repos.repo.changelog
   7.506 -        log_data = log.read(n)
   7.507 -        manifest, user, timeinfo, files, desc = log_data[:5]
   7.508 -        extra = {}
   7.509 -        if len(log_data) > 5: # extended changelog, since [hg 2f35961854fb]
   7.510 -            extra = log_data[5]
   7.511 -        time = repos.hg_time(timeinfo)
   7.512 -        Changeset.__init__(self, repos.hg_display(n), to_unicode(desc),
   7.513 -                           user, time)
   7.514 -        self.repos = repos
   7.515 -        self.n = n
   7.516 -        self.manifest_n = manifest
   7.517 -        self.files = files
   7.518 -        self.parents = [repos.hg_display(p) for p in log.parents(n) \
   7.519 -                        if p != nullid]
   7.520 -        self.children = [repos.hg_display(c) for c in log.children(n)]
   7.521 -        self.tags = [t for t in repos.repo.nodetags(n)]
   7.522 -        self.extra = extra
   7.523 -
   7.524 -    def get_properties(self):
   7.525 -        def changeset_links(csets):
   7.526 -            return ' '.join(['[cset:%s]' % cset for cset in csets])
   7.527 -        if len(self.parents) > 1:
   7.528 -            yield ('Parents', changeset_links(self.parents), True, 'changeset')
   7.529 -        if len(self.children) > 1:
   7.530 -            yield ('Children', changeset_links(self.children), True, 'changeset')
   7.531 -        if len(self.tags):
   7.532 -            yield ('Tags', changeset_links(self.tags), True, 'changeset')
   7.533 -        for k, v in self.extra.iteritems():
   7.534 -            yield (k, v, False, 'message') # TODO: Improve this API...
   7.535 -
   7.536 -    def get_changes(self):
   7.537 -        repo = self.repos.repo
   7.538 -        log = repo.changelog
   7.539 -        parents = log.parents(self.n)
   7.540 -        manifest = repo.manifest.read(self.manifest_n)
   7.541 -        manifest1 = manifest2 = None
   7.542 -        if parents:
   7.543 -            man_node1 = log.read(parents[0])[0]
   7.544 -            manifest1 = repo.manifest.read(man_node1)
   7.545 -            if len(parents) > 1:
   7.546 -                man_node2 = log.read(parents[1])[0]
   7.547 -                manifest2 = repo.manifest.read(man_node2)
   7.548 -
   7.549 -        deletions = {}
   7.550 -        def detect_delete(pmanifest, p):
   7.551 -            for f in pmanifest.keys():
   7.552 -                if f not in manifest:
   7.553 -                    deletions[f] = p
   7.554 -        if manifest1:
   7.555 -            detect_delete(manifest1, self.parents[0])
   7.556 -        if manifest2:
   7.557 -            detect_delete(manifest2, self.parents[1])
   7.558 -
   7.559 -        renames = {}
   7.560 -        changes = []
   7.561 -        for f in self.files: # 'added' and 'edited' files
   7.562 -            if f in deletions: # and since Mercurial > 0.7 [hg c6ffedc4f11b]
   7.563 -                continue          # also 'deleted' files
   7.564 -            action = None
   7.565 -            # TODO: find a way to detect conflicts and show how they were solved
   7.566 -            if manifest1 and f in manifest1:
   7.567 -                action = Changeset.EDIT                
   7.568 -                changes.append((f, Node.FILE, action, f, self.parents[0]))
   7.569 -            if manifest2 and f in manifest2:
   7.570 -                action = Changeset.EDIT                
   7.571 -                changes.append((f, Node.FILE, action, f, self.parents[1]))
   7.572 -
   7.573 -            if not action:
   7.574 -                rename_info = repo.file(f).renamed(manifest[f])
   7.575 -                if rename_info:
   7.576 -                    base_path = rename_info[0]
   7.577 -                    linkedrev = repo.file(base_path).linkrev(rename_info[1])
   7.578 -                    base_rev = self.repos.hg_display(log.node(linkedrev))
   7.579 -                    if base_path in deletions:
   7.580 -                        action = Changeset.MOVE
   7.581 -                        renames[base_path] = f
   7.582 -                    else:
   7.583 -                        action = Changeset.COPY
   7.584 -                else:
   7.585 -                    action = Changeset.ADD
   7.586 -                    base_path = ''
   7.587 -                    base_rev = None
   7.588 -                changes.append((f, Node.FILE, action, base_path, base_rev))
   7.589 -
   7.590 -        for f, p in deletions.items():
   7.591 -            if f not in renames:
   7.592 -                changes.append((f, Node.FILE, Changeset.DELETE, f, p))
   7.593 -        changes.sort()
   7.594 -        for change in changes:
   7.595 -            yield change
   7.596 -