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 -