| 1 | # -*- coding: iso-8859-1 -*-
|
|---|
| 2 | #
|
|---|
| 3 | # Copyright (C) 2005-2012 Edgewall Software
|
|---|
| 4 | # Copyright (C) 2005-2012 Christian Boos <cboos@edgewall.org>
|
|---|
| 5 | # All rights reserved.
|
|---|
| 6 | #
|
|---|
| 7 | # This software may be used and distributed according to the terms
|
|---|
| 8 | # of the GNU General Public License, incorporated herein by reference.
|
|---|
| 9 | #
|
|---|
| 10 | # This software consists of voluntary contributions made by many
|
|---|
| 11 | # individuals. For the exact contribution history, see the revision
|
|---|
| 12 | # history and logs, available at http://trac.edgewall.org/log/.
|
|---|
| 13 | #
|
|---|
| 14 | # Author: Christian Boos <cboos@edgewall.org>
|
|---|
| 15 |
|
|---|
| 16 | from bisect import bisect
|
|---|
| 17 | from datetime import datetime
|
|---|
| 18 | from heapq import heappop, heappush
|
|---|
| 19 | import os
|
|---|
| 20 | import time
|
|---|
| 21 | import posixpath
|
|---|
| 22 | import re
|
|---|
| 23 | import sys
|
|---|
| 24 |
|
|---|
| 25 | import pkg_resources
|
|---|
| 26 |
|
|---|
| 27 | from genshi.builder import tag
|
|---|
| 28 |
|
|---|
| 29 | from trac.core import *
|
|---|
| 30 | from trac.config import BoolOption, ChoiceOption, ListOption, PathOption
|
|---|
| 31 | from trac.env import ISystemInfoProvider
|
|---|
| 32 | from trac.util import arity
|
|---|
| 33 | from trac.util.datefmt import FixedOffset, utc
|
|---|
| 34 | from trac.util.text import exception_to_unicode, shorten_line, to_unicode
|
|---|
| 35 | from trac.util.translation import domain_functions
|
|---|
| 36 | from trac.versioncontrol.api import Changeset, Node, Repository, \
|
|---|
| 37 | IRepositoryConnector, RepositoryManager, \
|
|---|
| 38 | NoSuchChangeset, NoSuchNode
|
|---|
| 39 | from trac.versioncontrol.web_ui import IPropertyRenderer, RenderedProperty
|
|---|
| 40 | from trac.wiki import IWikiSyntaxProvider
|
|---|
| 41 |
|
|---|
| 42 | # -- plugin i18n
|
|---|
| 43 |
|
|---|
| 44 | gettext, _, tag_, N_, add_domain = \
|
|---|
| 45 | domain_functions('tracmercurial',
|
|---|
| 46 | ('gettext', '_', 'tag_', 'N_', 'add_domain'))
|
|---|
| 47 |
|
|---|
| 48 | # -- Using internal Mercurial API, see:
|
|---|
| 49 | # * http://mercurial.selenic.com/wiki/MercurialApi
|
|---|
| 50 | # * http://mercurial.selenic.com/wiki/ApiChanges
|
|---|
| 51 |
|
|---|
| 52 | hg_import_error = []
|
|---|
| 53 | try:
|
|---|
| 54 | # The new `demandimport` mechanism doesn't play well with code relying
|
|---|
| 55 | # on the `ImportError` exception being caught.
|
|---|
| 56 | # OTOH, we can't disable `demandimport` because mercurial relies on it
|
|---|
| 57 | # (circular reference issue). So for now, we activate `demandimport`
|
|---|
| 58 | # before loading mercurial modules, and desactivate it afterwards.
|
|---|
| 59 | #
|
|---|
| 60 | # See http://www.selenic.com/mercurial/bts/issue605
|
|---|
| 61 |
|
|---|
| 62 | try:
|
|---|
| 63 | from mercurial import demandimport
|
|---|
| 64 | demandimport.enable();
|
|---|
| 65 | except ImportError, hg_import_error:
|
|---|
| 66 | demandimport = None
|
|---|
| 67 |
|
|---|
| 68 | from mercurial import hg
|
|---|
| 69 | from mercurial.context import filectx
|
|---|
| 70 | from mercurial.ui import ui
|
|---|
| 71 | from mercurial.node import hex, short, nullid, nullrev
|
|---|
| 72 | from mercurial.util import pathto, cachefunc
|
|---|
| 73 | from mercurial import cmdutil
|
|---|
| 74 | from mercurial import encoding
|
|---|
| 75 | from mercurial import extensions
|
|---|
| 76 | from mercurial.extensions import loadall
|
|---|
| 77 | from mercurial.error import RepoLookupError
|
|---|
| 78 |
|
|---|
| 79 | # Note: due to the nature of demandimport, there will be no actual
|
|---|
| 80 | # import error until those symbols get accessed, so here we go:
|
|---|
| 81 | for sym in ("filectx ui hex short nullid pathto "
|
|---|
| 82 | "cachefunc loadall".split()):
|
|---|
| 83 | if repr(globals()[sym]) == "<unloaded module '%s'>" % sym:
|
|---|
| 84 | hg_import_error.append(sym)
|
|---|
| 85 | if hg_import_error:
|
|---|
| 86 | hg_import_error = "Couldn't import symbols: "+','.join(hg_import_error)
|
|---|
| 87 |
|
|---|
| 88 | # Mercurial versions >= 1.2 won't have mercurial.repo.RepoError anymore
|
|---|
| 89 | from mercurial.repo import RepoError
|
|---|
| 90 | from mercurial.revlog import LookupError as HgLookupError
|
|---|
| 91 | if repr(RepoError) == "<unloaded module 'RepoError'>":
|
|---|
| 92 | from mercurial.error import RepoError, LookupError as HgLookupError
|
|---|
| 93 |
|
|---|
| 94 | # Force local encoding to be non-lossy (#7217)
|
|---|
| 95 | os.environ['HGENCODING'] = 'utf-8'
|
|---|
| 96 | encoding.tolocal = str
|
|---|
| 97 |
|
|---|
| 98 | if demandimport:
|
|---|
| 99 | demandimport.disable()
|
|---|
| 100 |
|
|---|
| 101 | # API compatibility (watch http://mercurial.selenic.com/wiki/ApiChanges)
|
|---|
| 102 |
|
|---|
| 103 | if hasattr(cmdutil, 'match'):
|
|---|
| 104 | def match(ctx, *args, **kwargs):
|
|---|
| 105 | return cmdutil.match(ctx._repo, *args, **kwargs)
|
|---|
| 106 | else:
|
|---|
| 107 | from mercurial.scmutil import match
|
|---|
| 108 |
|
|---|
| 109 |
|
|---|
| 110 | except ImportError, e:
|
|---|
| 111 | hg_import_error = e
|
|---|
| 112 | ui = object
|
|---|
| 113 |
|
|---|
| 114 |
|
|---|
| 115 | ### Helpers
|
|---|
| 116 |
|
|---|
| 117 | def checked_encode(u, encodings, check):
|
|---|
| 118 | """Convert `unicode` to `str` trying several encodings until a
|
|---|
| 119 | condition is met.
|
|---|
| 120 |
|
|---|
| 121 | :param u: the `unicode` input
|
|---|
| 122 | :param encodings: the list of possible encodings
|
|---|
| 123 | :param check: the predicate to satisfy
|
|---|
| 124 |
|
|---|
| 125 | :return: the first converted `str` if `check(s)` is `True`,
|
|---|
| 126 | otherwise `None`. Note that if no encoding is able to
|
|---|
| 127 | successfully convert the input, the empty string will be
|
|---|
| 128 | given to `check`, which can accept it as valid or not.
|
|---|
| 129 | """
|
|---|
| 130 | s = u
|
|---|
| 131 | if isinstance(u, unicode):
|
|---|
| 132 | for enc in encodings:
|
|---|
| 133 | try:
|
|---|
| 134 | s = u.encode(enc)
|
|---|
| 135 | if check(s):
|
|---|
| 136 | return s
|
|---|
| 137 | except UnicodeEncodeError:
|
|---|
| 138 | pass
|
|---|
| 139 | else:
|
|---|
| 140 | s = ''
|
|---|
| 141 | if check(s):
|
|---|
| 142 | return s
|
|---|
| 143 |
|
|---|
| 144 |
|
|---|
| 145 | class trac_ui(ui):
|
|---|
| 146 | # Note: will be dropped in 0.13, see MercurialConnector._setup_ui
|
|---|
| 147 | def __init__(self, *args, **kwargs):
|
|---|
| 148 | ui.__init__(self, *args)
|
|---|
| 149 | self.setconfig('ui', 'interactive', 'off')
|
|---|
| 150 | self.log = kwargs.get('log', args and args[0].log or None)
|
|---|
| 151 |
|
|---|
| 152 | def write(self, *args, **opts):
|
|---|
| 153 | for a in args:
|
|---|
| 154 | self.log.info('(mercurial status) %s', a)
|
|---|
| 155 |
|
|---|
| 156 | def write_err(self, *args, **opts):
|
|---|
| 157 | for a in args:
|
|---|
| 158 | self.log.warn('(mercurial warning) %s', a)
|
|---|
| 159 |
|
|---|
| 160 | def plain(self, *args, **kw):
|
|---|
| 161 | return False # so that '[hg] hgrc' file can specify [ui] options
|
|---|
| 162 |
|
|---|
| 163 | def interactive(self):
|
|---|
| 164 | return False
|
|---|
| 165 |
|
|---|
| 166 | def readline(self):
|
|---|
| 167 | raise TracError('*** Mercurial ui.readline called ***')
|
|---|
| 168 |
|
|---|
| 169 |
|
|---|
| 170 | ### Components
|
|---|
| 171 |
|
|---|
| 172 | class CsetPropertyRenderer(Component):
|
|---|
| 173 |
|
|---|
| 174 | implements(IPropertyRenderer)
|
|---|
| 175 |
|
|---|
| 176 | def match_property(self, name, mode):
|
|---|
| 177 | return (name.startswith('hg-') and
|
|---|
| 178 | name[3:] in ('Parents', 'Children', 'Tags', 'Branch') and
|
|---|
| 179 | mode == 'revprop') and 4 or 0
|
|---|
| 180 |
|
|---|
| 181 | def render_property(self, name, mode, context, props):
|
|---|
| 182 | return RenderedProperty(name=gettext(name[3:] + ':'),
|
|---|
| 183 | name_attributes=[("class", "property")],
|
|---|
| 184 | content=self._render_property(name, mode, context, props))
|
|---|
| 185 |
|
|---|
| 186 | def _render_property(self, name, mode, context, props):
|
|---|
| 187 | repos, revs = props[name]
|
|---|
| 188 |
|
|---|
| 189 | if name in ('hg-Parents', 'hg-Children'):
|
|---|
| 190 | label = repos.display_rev
|
|---|
| 191 | else:
|
|---|
| 192 | label = lambda rev: rev
|
|---|
| 193 |
|
|---|
| 194 | def link(rev):
|
|---|
| 195 | chgset = repos.get_changeset(rev)
|
|---|
| 196 | return tag.a(label(rev), class_="changeset",
|
|---|
| 197 | title=shorten_line(chgset.message),
|
|---|
| 198 | href=context.href.changeset(rev, repos.reponame))
|
|---|
| 199 |
|
|---|
| 200 | if name == 'hg-Parents' and len(revs) == 2: # merge
|
|---|
| 201 | new = context.resource.id
|
|---|
| 202 | parent_links = [
|
|---|
| 203 | (link(rev), ' (',
|
|---|
| 204 | tag.a('diff', title=_("Diff against this parent "
|
|---|
| 205 | "(show the changes merged from the other parents)"),
|
|---|
| 206 | href=context.href.changeset(new, repos.reponame,
|
|---|
| 207 | old=rev)), ')')
|
|---|
| 208 | for rev in revs]
|
|---|
| 209 | return tag([(parent, ', ') for parent in parent_links[:-1]],
|
|---|
| 210 | parent_links[-1], tag.br(),
|
|---|
| 211 | tag.span(tag_("Note: this is a %(merge)s changeset, "
|
|---|
| 212 | "the changes displayed below correspond "
|
|---|
| 213 | "to the merge itself.",
|
|---|
| 214 | merge=tag.strong('merge')),
|
|---|
| 215 | class_='hint'), tag.br(),
|
|---|
| 216 | # TODO: only keep chunks present in both parents
|
|---|
| 217 | # (conflicts) or in none (extra changes)
|
|---|
| 218 | # tag.span('No changes means the merge was clean.',
|
|---|
| 219 | # class_='hint'), tag.br(),
|
|---|
| 220 | tag.span(tag_("Use the %(diff)s links above to see all "
|
|---|
| 221 | "the changes relative to each parent.",
|
|---|
| 222 | diff=tag.tt('(diff)')),
|
|---|
| 223 | class_='hint'))
|
|---|
| 224 | return tag([tag(link(rev), ', ') for rev in revs[:-1]],
|
|---|
| 225 | link(revs[-1]))
|
|---|
| 226 |
|
|---|
| 227 |
|
|---|
| 228 | class HgExtPropertyRenderer(Component):
|
|---|
| 229 |
|
|---|
| 230 | implements(IPropertyRenderer)
|
|---|
| 231 |
|
|---|
| 232 | def match_property(self, name, mode):
|
|---|
| 233 | return name in ('hg-transplant_source', 'hg-convert_revision') and \
|
|---|
| 234 | mode == 'revprop' and 4 or 0
|
|---|
| 235 |
|
|---|
| 236 | def render_property(self, name, mode, context, props):
|
|---|
| 237 | repos, value = props[name]
|
|---|
| 238 | if name == 'hg-transplant_source':
|
|---|
| 239 | try:
|
|---|
| 240 | ctx = self.changectx(value)
|
|---|
| 241 | chgset = MercurialChangeset(repos, ctx)
|
|---|
| 242 | href = context.href.changeset(ctx.hex(), repos.reponame)
|
|---|
| 243 | link = tag.a(repos._display(ctx), class_="changeset",
|
|---|
| 244 | title=shorten_line(chgset.message), href=href)
|
|---|
| 245 | except NoSuchChangeset:
|
|---|
| 246 | link = tag.a(hex(value), class_="missing changeset",
|
|---|
| 247 | title=_("no such changeset"), rel="nofollow")
|
|---|
| 248 | return RenderedProperty(name=_("Transplant:"), content=link,
|
|---|
| 249 | name_attributes=[("class", "property")])
|
|---|
| 250 |
|
|---|
| 251 | elif name == 'hg-convert_revision':
|
|---|
| 252 | text = repos.to_u(value)
|
|---|
| 253 | if value.startswith('svn:'):
|
|---|
| 254 | # e.g. 'svn:af82e41b-90c4-0310-8c96-b1721e28e2e2/trunk@9517'
|
|---|
| 255 | uuid = value[:40]
|
|---|
| 256 | rev = value.rsplit('@', 1)[-1]
|
|---|
| 257 | for r in RepositoryManager(self.env).get_real_repositories():
|
|---|
| 258 | if r.name.startswith(uuid + ':'):
|
|---|
| 259 | path = r.reponame
|
|---|
| 260 | href = context.href.changeset(rev, path or None)
|
|---|
| 261 | text = tag.a('[%s%s]' % (rev, path and '/' + path),
|
|---|
| 262 | class_='changeset', href=href,
|
|---|
| 263 | title=_('Changeset in source repository'))
|
|---|
| 264 | break
|
|---|
| 265 | return RenderedProperty(name=_('Convert:'), content=text,
|
|---|
| 266 | name_attributes=[("class", "property")])
|
|---|
| 267 |
|
|---|
| 268 |
|
|---|
| 269 | class HgDefaultPropertyRenderer(Component):
|
|---|
| 270 |
|
|---|
| 271 | implements(IPropertyRenderer)
|
|---|
| 272 |
|
|---|
| 273 | def match_property(self, name, mode):
|
|---|
| 274 | return name.startswith('hg-') and mode == 'revprop' and 1 or 0
|
|---|
| 275 |
|
|---|
| 276 | def render_property(self, name, mode, context, props):
|
|---|
| 277 | return RenderedProperty(name=name[3:] + ':',
|
|---|
| 278 | name_attributes=[("class", "property")],
|
|---|
| 279 | content=self._render_property(name, mode,
|
|---|
| 280 | context, props))
|
|---|
| 281 |
|
|---|
| 282 | def _render_property(self, name, mode, context, props):
|
|---|
| 283 | repos, value = props[name]
|
|---|
| 284 | try:
|
|---|
| 285 | return unicode(value)
|
|---|
| 286 | except UnicodeDecodeError:
|
|---|
| 287 | if len(value) <= 100:
|
|---|
| 288 | return tag.tt(''.join(("%02x" % ord(c)) for c in value))
|
|---|
| 289 | else:
|
|---|
| 290 | return tag.em(_("(binary, size greater than 100 bytes)"))
|
|---|
| 291 |
|
|---|
| 292 |
|
|---|
| 293 | class MercurialConnector(Component):
|
|---|
| 294 |
|
|---|
| 295 | implements(ISystemInfoProvider, IRepositoryConnector, IWikiSyntaxProvider)
|
|---|
| 296 |
|
|---|
| 297 | encoding = ListOption('hg', 'encoding', 'utf-8', doc="""
|
|---|
| 298 | Encoding that should be used to decode filenames, file
|
|---|
| 299 | content, and changeset metadata. If multiple encodings are
|
|---|
| 300 | used for these different situations (or even multiple
|
|---|
| 301 | encodings were used for filenames), simply specify a list of
|
|---|
| 302 | encodings which will be tried in turn (''since 0.12.0.24'').
|
|---|
| 303 | """)
|
|---|
| 304 |
|
|---|
| 305 | show_rev = BoolOption('hg', 'show_rev', True, doc="""
|
|---|
| 306 | Show decimal revision in front of the commit SHA1 hash. While
|
|---|
| 307 | this number is specific to the particular clone used to browse
|
|---|
| 308 | the repository, this can sometimes give an useful hint about
|
|---|
| 309 | the relative "age" of a revision.
|
|---|
| 310 | """)
|
|---|
| 311 |
|
|---|
| 312 | node_format = ChoiceOption('hg', 'node_format', ['short', 'hex'], doc="""
|
|---|
| 313 | Specify how the commit SHA1 hashes should be
|
|---|
| 314 | displayed. Possible choices are: 'short', the SHA1 hash is
|
|---|
| 315 | abbreviated to its first 12 digits, or 'hex', the hash is
|
|---|
| 316 | shown in full.
|
|---|
| 317 | """)
|
|---|
| 318 |
|
|---|
| 319 | hgrc = PathOption('hg', 'hgrc', '', doc="""
|
|---|
| 320 | Optional path to an hgrc file which will be used to specify
|
|---|
| 321 | extra Mercurial configuration options (see
|
|---|
| 322 | http://www.selenic.com/mercurial/hgrc.5.html).
|
|---|
| 323 | """)
|
|---|
| 324 |
|
|---|
| 325 | def __init__(self):
|
|---|
| 326 | self.ui = None
|
|---|
| 327 | locale_dir = pkg_resources.resource_filename(__name__, 'locale')
|
|---|
| 328 | add_domain(self.env.path, locale_dir)
|
|---|
| 329 | self._version = self._version_info = None
|
|---|
| 330 | if not hg_import_error:
|
|---|
| 331 | try:
|
|---|
| 332 | from mercurial.version import get_version
|
|---|
| 333 | self._version = get_version()
|
|---|
| 334 | except ImportError: # gone in Mercurial 1.2 (hg:9626819b2e3d)
|
|---|
| 335 | from mercurial.util import version
|
|---|
| 336 | self._version = version()
|
|---|
| 337 | # development version assumed to be always the ''newest'' one,
|
|---|
| 338 | # i.e. old development version won't be supported
|
|---|
| 339 | self._version_info = (999, 0, 0)
|
|---|
| 340 | m = re.match(r'(\d+)\.(\d+)(?:\.(\d+))?', self._version or '')
|
|---|
| 341 | if m:
|
|---|
| 342 | self._version_info = tuple([int(n or 0) for n in m.groups()])
|
|---|
| 343 |
|
|---|
| 344 | def _setup_ui(self, hgrc_path):
|
|---|
| 345 | # Starting with Mercurial 1.3 we can probably do simply:
|
|---|
| 346 | #
|
|---|
| 347 | # ui = baseui.copy() # there's no longer a parent/child concept
|
|---|
| 348 | # ui.setconfig('ui', 'interactive', 'off')
|
|---|
| 349 | #
|
|---|
| 350 | self.ui = trac_ui(log=self.log)
|
|---|
| 351 |
|
|---|
| 352 | # (code below adapted from mercurial.dispatch._dispatch)
|
|---|
| 353 |
|
|---|
| 354 | # read the local repository .hgrc into a local ui object
|
|---|
| 355 | if hgrc_path:
|
|---|
| 356 | if not os.path.exists(hgrc_path):
|
|---|
| 357 | self.log.warn("'[hg] hgrc' file (%s) not found ", hgrc_path)
|
|---|
| 358 | try:
|
|---|
| 359 | self.ui = trac_ui(self.ui, log=self.log)
|
|---|
| 360 | self.ui.check_trusted = False
|
|---|
| 361 | self.ui.readconfig(hgrc_path)
|
|---|
| 362 | except IOError, e:
|
|---|
| 363 | self.log.warn("'[hg] hgrc' file (%s) can't be read: %s",
|
|---|
| 364 | hgrc_path, e)
|
|---|
| 365 |
|
|---|
| 366 | extensions.loadall(self.ui)
|
|---|
| 367 | if hasattr(extensions, 'extensions'):
|
|---|
| 368 | for name, module in extensions.extensions():
|
|---|
| 369 | # setup extensions
|
|---|
| 370 | extsetup = getattr(module, 'extsetup', None)
|
|---|
| 371 | if extsetup:
|
|---|
| 372 | if arity(extsetup) == 1:
|
|---|
| 373 | extsetup(self.ui)
|
|---|
| 374 | else:
|
|---|
| 375 | extsetup()
|
|---|
| 376 |
|
|---|
| 377 | # ISystemInfoProvider methods
|
|---|
| 378 |
|
|---|
| 379 | def get_system_info(self):
|
|---|
| 380 | if self._version is not None:
|
|---|
| 381 | yield 'Mercurial', self._version
|
|---|
| 382 |
|
|---|
| 383 | # IRepositoryConnector methods
|
|---|
| 384 |
|
|---|
| 385 | def get_supported_types(self):
|
|---|
| 386 | """Support for `repository_type = hg`"""
|
|---|
| 387 | if hg_import_error:
|
|---|
| 388 | self.error = hg_import_error
|
|---|
| 389 | yield ("hg", -1)
|
|---|
| 390 | else:
|
|---|
| 391 | yield ("hg", 8)
|
|---|
| 392 |
|
|---|
| 393 | def get_repository(self, type, dir, params):
|
|---|
| 394 | """Return a `MercurialRepository`"""
|
|---|
| 395 | if not self.ui:
|
|---|
| 396 | self._setup_ui(self.hgrc)
|
|---|
| 397 | repos = MercurialRepository(dir, params, self.log, self)
|
|---|
| 398 | repos.version_info = self._version_info
|
|---|
| 399 | return repos
|
|---|
| 400 |
|
|---|
| 401 | # IWikiSyntaxProvider methods
|
|---|
| 402 |
|
|---|
| 403 | def get_wiki_syntax(self):
|
|---|
| 404 | yield (r'!?(?P<hgrev>[0-9a-f]{12,40})(?P<hgpath>/\S+\b)?',
|
|---|
| 405 | lambda formatter, label, match:
|
|---|
| 406 | self._format_link(formatter, 'cset', match.group(0),
|
|---|
| 407 | match.group(0), match))
|
|---|
| 408 |
|
|---|
| 409 | def get_link_resolvers(self):
|
|---|
| 410 | yield ('cset', self._format_link)
|
|---|
| 411 | yield ('chgset', self._format_link)
|
|---|
| 412 | yield ('branch', self._format_link) # go to the corresponding head
|
|---|
| 413 | yield ('tag', self._format_link)
|
|---|
| 414 |
|
|---|
| 415 | def _format_link(self, formatter, ns, rev, label, fullmatch=None):
|
|---|
| 416 | reponame = path = ''
|
|---|
| 417 | repos = None
|
|---|
| 418 | rm = RepositoryManager(self.env)
|
|---|
| 419 | try:
|
|---|
| 420 | if fullmatch:
|
|---|
| 421 | rev = fullmatch.group('hgrev')
|
|---|
| 422 | path = fullmatch.group('hgpath')
|
|---|
| 423 | if path:
|
|---|
| 424 | reponame, repos, path = \
|
|---|
| 425 | rm.get_repository_by_path(path.strip('/'))
|
|---|
| 426 | if not repos:
|
|---|
| 427 | context = formatter.context
|
|---|
| 428 | while context:
|
|---|
| 429 | if context.resource.realm in ('source', 'changeset'):
|
|---|
| 430 | reponame = context.resource.parent.id
|
|---|
| 431 | break
|
|---|
| 432 | context = context.parent
|
|---|
| 433 | repos = rm.get_repository(reponame)
|
|---|
| 434 | if repos:
|
|---|
| 435 | if ns == 'branch':
|
|---|
| 436 | for b, n in repos.repo.branchtags().items():
|
|---|
| 437 | if repos.to_u(b) == rev:
|
|---|
| 438 | rev = repos.repo.changelog.rev(n)
|
|---|
| 439 | break
|
|---|
| 440 | else:
|
|---|
| 441 | raise NoSuchChangeset(rev)
|
|---|
| 442 | chgset = repos.get_changeset(rev)
|
|---|
| 443 | return tag.a(label, class_="changeset",
|
|---|
| 444 | title=shorten_line(chgset.message),
|
|---|
| 445 | href=formatter.href.changeset(rev, reponame,
|
|---|
| 446 | path))
|
|---|
| 447 | raise TracError("Repository not found")
|
|---|
| 448 | except NoSuchChangeset, e:
|
|---|
| 449 | errmsg = to_unicode(e)
|
|---|
| 450 | except TracError:
|
|---|
| 451 | errmsg = _("Repository '%(repo)s' not found", repo=reponame)
|
|---|
| 452 | return tag.a(label, class_="missing changeset",
|
|---|
| 453 | title=errmsg, rel="nofollow")
|
|---|
| 454 |
|
|---|
| 455 |
|
|---|
| 456 |
|
|---|
| 457 | ### Version Control API
|
|---|
| 458 |
|
|---|
| 459 | class MercurialRepository(Repository):
|
|---|
| 460 | """Repository implementation based on the mercurial API.
|
|---|
| 461 |
|
|---|
| 462 | This wraps an hg.repository object. The revision navigation
|
|---|
| 463 | follows the branches, and defaults to the first parent/child in
|
|---|
| 464 | case there are many. The eventual other parents/children are
|
|---|
| 465 | listed as additional changeset properties.
|
|---|
| 466 | """
|
|---|
| 467 |
|
|---|
| 468 | def __init__(self, path, params, log, connector):
|
|---|
| 469 | self.ui = connector.ui
|
|---|
| 470 | self._show_rev = connector.show_rev
|
|---|
| 471 | self._node_fmt = connector.node_format
|
|---|
| 472 | # TODO 0.13: per repository ui and options
|
|---|
| 473 |
|
|---|
| 474 | # -- encoding
|
|---|
| 475 | encoding = connector.encoding
|
|---|
| 476 | if not encoding:
|
|---|
| 477 | encoding = ['utf-8']
|
|---|
| 478 | # verify given encodings
|
|---|
| 479 | for enc in encoding:
|
|---|
| 480 | try:
|
|---|
| 481 | u''.encode(enc)
|
|---|
| 482 | except LookupError, e:
|
|---|
| 483 | log.warning("'[hg] encoding' (%r) not valid", e)
|
|---|
| 484 | if 'latin1' not in encoding:
|
|---|
| 485 | encoding.append('latin1')
|
|---|
| 486 | self.encoding = encoding
|
|---|
| 487 |
|
|---|
| 488 | def to_u(s):
|
|---|
| 489 | if isinstance(s, unicode):
|
|---|
| 490 | return s
|
|---|
| 491 | for enc in encoding:
|
|---|
| 492 | try:
|
|---|
| 493 | return unicode(s, enc)
|
|---|
| 494 | except UnicodeDecodeError:
|
|---|
| 495 | pass
|
|---|
| 496 | def to_s(u):
|
|---|
| 497 | if isinstance(u, str):
|
|---|
| 498 | return u
|
|---|
| 499 | for enc in encoding:
|
|---|
| 500 | try:
|
|---|
| 501 | return u.encode(enc)
|
|---|
| 502 | except UnicodeEncodeError:
|
|---|
| 503 | pass
|
|---|
| 504 | self.to_u = to_u
|
|---|
| 505 | self.to_s = to_s
|
|---|
| 506 |
|
|---|
| 507 | # -- repository path
|
|---|
| 508 | self.path = str_path = path
|
|---|
| 509 | # Note: `path` is a filesystem path obtained either from the
|
|---|
| 510 | # trac.ini file or from the `repository` table, so it's
|
|---|
| 511 | # normally an `unicode` instance. '[hg] encoding'
|
|---|
| 512 | # shouldn't play a role here, but we can nevertheless
|
|---|
| 513 | # use that as secondary choices.
|
|---|
| 514 | fsencoding = [sys.getfilesystemencoding() or 'utf-8'] + encoding
|
|---|
| 515 | str_path = checked_encode(path, fsencoding, os.path.exists)
|
|---|
| 516 | if str_path is None:
|
|---|
| 517 | raise TracError(_("Repository path '%(path)s' does not exist.",
|
|---|
| 518 | path=path))
|
|---|
| 519 | try:
|
|---|
| 520 | self.repo = hg.repository(ui=self.ui, path=str_path)
|
|---|
| 521 | except RepoError, e:
|
|---|
| 522 | version = connector._version
|
|---|
| 523 | error = exception_to_unicode(e)
|
|---|
| 524 | log.error("Mercurial %s can't open repository (%s)", version, error)
|
|---|
| 525 | raise TracError(_("'%(path)s' does not appear to contain a"
|
|---|
| 526 | " repository (Mercurial %(version)s says "
|
|---|
| 527 | "%(error)s)",
|
|---|
| 528 | path=path, version=version, error=error))
|
|---|
| 529 | Repository.__init__(self, 'hg:%s' % path, params, log)
|
|---|
| 530 |
|
|---|
| 531 | def from_hg_time(self, timeinfo):
|
|---|
| 532 | time, tz = timeinfo
|
|---|
| 533 | tzinfo = FixedOffset(tz / 60, 'GMT %d:00' % (tz / 3600))
|
|---|
| 534 | return datetime.fromtimestamp(time, tzinfo)
|
|---|
| 535 |
|
|---|
| 536 | def changectx(self, rev=None):
|
|---|
| 537 | """Produce a Mercurial `context.changectx` from given Trac revision."""
|
|---|
| 538 | return self.repo[self.short_rev(rev)]
|
|---|
| 539 |
|
|---|
| 540 | def close(self):
|
|---|
| 541 | self.repo = None
|
|---|
| 542 |
|
|---|
| 543 | def normalize_path(self, path):
|
|---|
| 544 | """Remove leading "/" (even at root)"""
|
|---|
| 545 | return path and path.strip('/') or ''
|
|---|
| 546 |
|
|---|
| 547 | def normalize_rev(self, rev):
|
|---|
| 548 | """Return the full hash for the specified rev."""
|
|---|
| 549 | return self.changectx(rev).hex()
|
|---|
| 550 |
|
|---|
| 551 | def short_rev(self, rev):
|
|---|
| 552 | """Find Mercurial revision number corresponding to given Trac revision.
|
|---|
| 553 |
|
|---|
| 554 | :param rev: any kind of revision specification, either an
|
|---|
| 555 | `unicode` string, or a revision number. If `None`
|
|---|
| 556 | or '', latest revision will be returned.
|
|---|
| 557 |
|
|---|
| 558 | :return: an integer revision
|
|---|
| 559 | """
|
|---|
| 560 | repo = self.repo
|
|---|
| 561 | if rev == 0:
|
|---|
| 562 | return rev
|
|---|
| 563 | if not rev:
|
|---|
| 564 | return len(repo) - 1
|
|---|
| 565 | if isinstance(rev, (long, int)):
|
|---|
| 566 | return rev
|
|---|
| 567 | if rev[0] != "'": # "'11:11'" can be a tag name?
|
|---|
| 568 | rev = rev.split(':', 1)[0]
|
|---|
| 569 | if rev == '-1':
|
|---|
| 570 | return nullrev
|
|---|
| 571 | if rev.isdigit():
|
|---|
| 572 | r = int(rev)
|
|---|
| 573 | if 0 <= r < len(repo):
|
|---|
| 574 | return r
|
|---|
| 575 | try:
|
|---|
| 576 | return repo[repo.lookup(self.to_s(rev))].rev()
|
|---|
| 577 | except (HgLookupError, RepoError):
|
|---|
| 578 | raise NoSuchChangeset(rev)
|
|---|
| 579 |
|
|---|
| 580 | def display_rev(self, rev):
|
|---|
| 581 | return self._display(self.changectx(rev))
|
|---|
| 582 |
|
|---|
| 583 | def _display(self, ctx):
|
|---|
| 584 | """Return user-readable revision information for node `n`.
|
|---|
| 585 |
|
|---|
| 586 | The specific format depends on the `node_format` and
|
|---|
| 587 | `show_rev` options.
|
|---|
| 588 | """
|
|---|
| 589 | nodestr = self._node_fmt == "hex" and ctx.hex() or str(ctx)
|
|---|
| 590 | if self._show_rev:
|
|---|
| 591 | return '%s:%s' % (ctx.rev(), nodestr)
|
|---|
| 592 | else:
|
|---|
| 593 | return nodestr
|
|---|
| 594 |
|
|---|
| 595 | def get_quickjump_entries(self, rev):
|
|---|
| 596 | # map ctx to (unicode) branch
|
|---|
| 597 | branches = {}
|
|---|
| 598 | closed_branches = {}
|
|---|
| 599 | for b, n in self.repo.branchtags().items():
|
|---|
| 600 | b = self.to_u(b)
|
|---|
| 601 | ctx = self.repo[n]
|
|---|
| 602 | if 'close' in ctx.extra():
|
|---|
| 603 | closed_branches[ctx] = b
|
|---|
| 604 | else:
|
|---|
| 605 | branches[ctx] = b
|
|---|
| 606 | # map node to tag names
|
|---|
| 607 | tags = {}
|
|---|
| 608 | tagslist = self.repo.tagslist()
|
|---|
| 609 | for tag, n in tagslist:
|
|---|
| 610 | tags.setdefault(n, []).append(self.to_u(tag))
|
|---|
| 611 | def taginfo(ctx):
|
|---|
| 612 | t = tags.get(ctx.node())
|
|---|
| 613 | if t:
|
|---|
| 614 | return ' (%s)' % ', '.join(t)
|
|---|
| 615 | else:
|
|---|
| 616 | return ''
|
|---|
| 617 | # branches
|
|---|
| 618 | for ctx, b in sorted(branches.items(), reverse=True,
|
|---|
| 619 | key=lambda (ctx, b): ctx.rev()):
|
|---|
| 620 | yield ('branches', b + taginfo(ctx), '/', self._display(ctx))
|
|---|
| 621 | # heads
|
|---|
| 622 | for n in self.repo.heads():
|
|---|
| 623 | ctx = self.repo[n]
|
|---|
| 624 | if ctx not in branches and ctx not in closed_branches:
|
|---|
| 625 | h = self._display(ctx)
|
|---|
| 626 | yield ('extra heads', h + taginfo(ctx), '/', h)
|
|---|
| 627 | # tags
|
|---|
| 628 | for t, n in reversed(tagslist):
|
|---|
| 629 | try:
|
|---|
| 630 | yield ('tags', ', '.join(tags.pop(n)), # FIXME: pop?
|
|---|
| 631 | '/', self._display(self.repo[n]))
|
|---|
| 632 | except (KeyError, RepoLookupError):
|
|---|
| 633 | pass
|
|---|
| 634 | # closed branches
|
|---|
| 635 | for ctx, b in sorted(closed_branches.items(), reverse=True,
|
|---|
| 636 | key=lambda (ctx, b): ctx.rev()):
|
|---|
| 637 | yield ('closed branches', b + taginfo(ctx), '/', self._display(ctx))
|
|---|
| 638 |
|
|---|
| 639 | def get_path_url(self, path, rev):
|
|---|
| 640 | url = self.params.get('url')
|
|---|
| 641 | if url and (not path or path == '/'):
|
|---|
| 642 | if not rev:
|
|---|
| 643 | return url
|
|---|
| 644 | branch = self.changectx(rev).branch()
|
|---|
| 645 | if branch == 'default':
|
|---|
| 646 | return url
|
|---|
| 647 | return url + '#' + self.to_u(branch) # URL for cloning that branch
|
|---|
| 648 |
|
|---|
| 649 | # Note: link to matching location in Mercurial's file browser
|
|---|
| 650 | #rev = rev is not None and short(n) or 'tip'
|
|---|
| 651 | #return '/'.join([url, 'file', rev, path])
|
|---|
| 652 |
|
|---|
| 653 | def get_changeset(self, rev):
|
|---|
| 654 | return MercurialChangeset(self, self.changectx(rev))
|
|---|
| 655 |
|
|---|
| 656 | def get_changeset_uid(self, rev):
|
|---|
| 657 | return self.changectx(rev).hex()
|
|---|
| 658 |
|
|---|
| 659 | def get_changesets(self, start, stop):
|
|---|
| 660 | """Follow each head and parents in order to get all changesets
|
|---|
| 661 |
|
|---|
| 662 | FIXME: this can only be handled correctly and efficiently by
|
|---|
| 663 | using the db repository cache.
|
|---|
| 664 |
|
|---|
| 665 | The code below is only an heuristic, and doesn't work in the
|
|---|
| 666 | general case. E.g. look at the mercurial repository timeline
|
|---|
| 667 | for 2006-10-18, you need to give ''38'' daysback in order to
|
|---|
| 668 | see the changesets from 2006-10-17...
|
|---|
| 669 |
|
|---|
| 670 | This is because of the following '''linear''' sequence of csets:
|
|---|
| 671 | - 3445:233c733e4af5 10/18/2006 9:08:36 AM mpm
|
|---|
| 672 | - 3446:0b450267cf47 9/10/2006 3:25:06 AM hopper
|
|---|
| 673 | - 3447:ef1032c223e7 9/10/2006 3:25:06 AM hopper
|
|---|
| 674 | - 3448:6ca49c5fe268 9/10/2006 3:25:07 AM hopper
|
|---|
| 675 | - 3449:c8686e3f0291 10/18/2006 9:14:26 AM hopper
|
|---|
| 676 |
|
|---|
| 677 | This is most probably because [3446:3448] correspond to old
|
|---|
| 678 | changesets that have been ''hg import''ed, with their
|
|---|
| 679 | original dates.
|
|---|
| 680 | """
|
|---|
| 681 | seen = {nullrev: 1}
|
|---|
| 682 | seeds = [self.repo[n] for n in self.repo.heads()]
|
|---|
| 683 | while seeds:
|
|---|
| 684 | ctx = seeds.pop(0)
|
|---|
| 685 | time = self.from_hg_time(ctx.date())
|
|---|
| 686 | if time < start:
|
|---|
| 687 | continue # assume no ancestor is younger and use next seed
|
|---|
| 688 | # (and that assumption is wrong for 3448 in the example above)
|
|---|
| 689 | elif time < stop:
|
|---|
| 690 | yield MercurialChangeset(self, ctx)
|
|---|
| 691 | for p in ctx.parents():
|
|---|
| 692 | if p.rev() not in seen:
|
|---|
| 693 | seen[p.rev()] = 1
|
|---|
| 694 | seeds.append(p)
|
|---|
| 695 |
|
|---|
| 696 | def get_node(self, path, rev=None):
|
|---|
| 697 | return MercurialNode(self, self.normalize_path(path),
|
|---|
| 698 | self.changectx(rev))
|
|---|
| 699 |
|
|---|
| 700 | def get_oldest_rev(self):
|
|---|
| 701 | return 0
|
|---|
| 702 |
|
|---|
| 703 | def get_youngest_rev(self):
|
|---|
| 704 | return self.changectx().hex()
|
|---|
| 705 |
|
|---|
| 706 | def previous_rev(self, rev, path=''): # FIXME: path ignored for now
|
|---|
| 707 | for p in self.changectx(rev).parents():
|
|---|
| 708 | if p:
|
|---|
| 709 | return p.hex() # always follow first parent
|
|---|
| 710 |
|
|---|
| 711 | def next_rev(self, rev, path=''):
|
|---|
| 712 | ctx = self.changectx(rev)
|
|---|
| 713 | if path: # might be a file
|
|---|
| 714 | fc = filectx(self.repo, self.to_s(path), ctx.node())
|
|---|
| 715 | # Note: the simpler form below raises an HgLookupError for a dir
|
|---|
| 716 | # fc = ctx.filectx(self.to_s(path))
|
|---|
| 717 | if fc: # it is a file
|
|---|
| 718 | for c in fc.children():
|
|---|
| 719 | return c.hex()
|
|---|
| 720 | else:
|
|---|
| 721 | return None
|
|---|
| 722 | # it might be a directory (not supported for now) FIXME
|
|---|
| 723 | for c in ctx.children():
|
|---|
| 724 | return c.hex() # always follow first child
|
|---|
| 725 |
|
|---|
| 726 | def parent_revs(self, rev):
|
|---|
| 727 | return [p.hex() for p in self.changectx(rev).parents()]
|
|---|
| 728 |
|
|---|
| 729 | def rev_older_than(self, rev1, rev2):
|
|---|
| 730 | # FIXME use == and ancestors?
|
|---|
| 731 | return self.short_rev(rev1) < self.short_rev(rev2)
|
|---|
| 732 |
|
|---|
| 733 | # def get_path_history(self, path, rev=None, limit=None):
|
|---|
| 734 | # (not really relevant for Mercurial)
|
|---|
| 735 |
|
|---|
| 736 | def get_changes(self, old_path, old_rev, new_path, new_rev,
|
|---|
| 737 | ignore_ancestry=1):
|
|---|
| 738 | """Generates changes corresponding to generalized diffs.
|
|---|
| 739 |
|
|---|
| 740 | Generator that yields change tuples (old_node, new_node, kind,
|
|---|
| 741 | change) for each node change between the two arbitrary
|
|---|
| 742 | (path,rev) pairs.
|
|---|
| 743 |
|
|---|
| 744 | The old_node is assumed to be None when the change is an ADD,
|
|---|
| 745 | the new_node is assumed to be None when the change is a
|
|---|
| 746 | DELETE.
|
|---|
| 747 | """
|
|---|
| 748 | old_node = new_node = None
|
|---|
| 749 | old_node = self.get_node(old_path, old_rev)
|
|---|
| 750 | new_node = self.get_node(new_path, new_rev)
|
|---|
| 751 | # check kind, both should be same.
|
|---|
| 752 | if new_node.kind != old_node.kind:
|
|---|
| 753 | raise TracError(
|
|---|
| 754 | _("Diff mismatch: "
|
|---|
| 755 | "Base is a %(okind)s (%(opath)s in revision %(orev)s) "
|
|---|
| 756 | "and Target is a %(nkind)s (%(npath)s in revision %(nrev)s).",
|
|---|
| 757 | okind=old_node.kind, opath=old_path, orev=old_rev,
|
|---|
| 758 | nkind=new_node.kind, npath=new_path, nrev=new_rev))
|
|---|
| 759 | # Correct change info from changelog(revlog)
|
|---|
| 760 | # Finding changes between two revs requires tracking back
|
|---|
| 761 | # several routes.
|
|---|
| 762 |
|
|---|
| 763 | if new_node.isdir:
|
|---|
| 764 | # TODO: Should we follow rename and copy?
|
|---|
| 765 | # As temporary workaround, simply compare entry names.
|
|---|
| 766 | changes = []
|
|---|
| 767 | str_new_path = self.to_s(new_path)
|
|---|
| 768 | str_old_path = self.to_s(old_path)
|
|---|
| 769 | # additions and edits
|
|---|
| 770 | for str_path in new_node.manifest:
|
|---|
| 771 | # changes out of scope
|
|---|
| 772 | if str_new_path and not str_path.startswith(str_new_path + '/'):
|
|---|
| 773 | continue
|
|---|
| 774 | # 'added' if not present in old manifest
|
|---|
| 775 | str_op = str_old_path + str_path[len(str_new_path):]
|
|---|
| 776 | if str_op not in old_node.manifest:
|
|---|
| 777 | changes.append((str_path, None, new_node.subnode(str_path),
|
|---|
| 778 | Node.FILE, Changeset.ADD))
|
|---|
| 779 | elif old_node.manifest[str_op] != new_node.manifest[str_path]:
|
|---|
| 780 | changes.append((str_path, old_node.subnode(str_op),
|
|---|
| 781 | new_node.subnode(str_path),
|
|---|
| 782 | Node.FILE, Changeset.EDIT))
|
|---|
| 783 | # deletions
|
|---|
| 784 | for str_path in old_node.manifest:
|
|---|
| 785 | # changes out of scope
|
|---|
| 786 | if str_old_path and not str_path.startswith(str_old_path + '/'):
|
|---|
| 787 | continue
|
|---|
| 788 | # 'deleted' if not present in new manifest
|
|---|
| 789 | str_np = str_new_path + str_path[len(str_old_path):]
|
|---|
| 790 | if str_np not in new_node.manifest:
|
|---|
| 791 | changes.append((str_path, old_node.subnode(str_np), None,
|
|---|
| 792 | Node.FILE, Changeset.DELETE))
|
|---|
| 793 | # Note: `str_path` only used as a key, no need to convert to_u
|
|---|
| 794 | for change in sorted(changes, key=lambda c: c[0]):
|
|---|
| 795 | yield(change[1], change[2], change[3], change[4])
|
|---|
| 796 | else:
|
|---|
| 797 | if old_node.manifest[old_node.str_path] != \
|
|---|
| 798 | new_node.manifest[new_node.str_path]:
|
|---|
| 799 | yield(old_node, new_node, Node.FILE, Changeset.EDIT)
|
|---|
| 800 |
|
|---|
| 801 |
|
|---|
| 802 | class MercurialNode(Node):
|
|---|
| 803 | """A path in the repository, at a given revision.
|
|---|
| 804 |
|
|---|
| 805 | It encapsulates the repository manifest for the given revision.
|
|---|
| 806 |
|
|---|
| 807 | As directories are not first-class citizens in Mercurial,
|
|---|
| 808 | retrieving revision information for directory can be much slower
|
|---|
| 809 | than for files, except when created as a `subnode()` of an
|
|---|
| 810 | existing MercurialNode.
|
|---|
| 811 | """
|
|---|
| 812 |
|
|---|
| 813 | filectx = dirnode = None
|
|---|
| 814 |
|
|---|
| 815 | def __init__(self, repos, path, changectx,
|
|---|
| 816 | manifest=None, dirctx=None, str_entry=None):
|
|---|
| 817 | """
|
|---|
| 818 | :param repos: the `MercurialRepository`
|
|---|
| 819 | :param path: the `unicode` path corresponding to this node
|
|---|
| 820 | :param rev: requested revision (i.e. "browsing at")
|
|---|
| 821 | :param changectx: the `changectx` for the "requested" revision
|
|---|
| 822 |
|
|---|
| 823 | The following parameters are passed when creating a subnode
|
|---|
| 824 | instance:
|
|---|
| 825 |
|
|---|
| 826 | :param manifest: `manifest` object from parent `MercurialNode`
|
|---|
| 827 | :param dirctx: `changectx` for a directory determined by
|
|---|
| 828 | parent `MercurialNode`
|
|---|
| 829 | :param str_entry: entry name if node created from parent node
|
|---|
| 830 | """
|
|---|
| 831 | repo = repos.repo
|
|---|
| 832 | self.repos = repos
|
|---|
| 833 | self.changectx = changectx
|
|---|
| 834 | self.manifest = manifest or changectx.manifest()
|
|---|
| 835 | str_entries = []
|
|---|
| 836 |
|
|---|
| 837 | if path == '' or path == '/':
|
|---|
| 838 | str_path = ''
|
|---|
| 839 | elif dirctx:
|
|---|
| 840 | str_path = str_entry
|
|---|
| 841 | else:
|
|---|
| 842 | # Fast path: check for existing file
|
|---|
| 843 | str_path = checked_encode(path, repos.encoding,
|
|---|
| 844 | lambda s: s in self.manifest)
|
|---|
| 845 | if str_path is None:
|
|---|
| 846 | # Slow path: this might be a directory node
|
|---|
| 847 | str_files = sorted(self.manifest)
|
|---|
| 848 | idx = [-1]
|
|---|
| 849 | def has_dir_node(str_dir):
|
|---|
| 850 | if not str_dir: # no encoding matched, i.e. not existing
|
|---|
| 851 | return False
|
|---|
| 852 | idx[0] = lo = bisect(str_files, str_dir)
|
|---|
| 853 | return lo < len(str_files) \
|
|---|
| 854 | and str_files[lo].startswith(str_dir)
|
|---|
| 855 | str_path = checked_encode(path + '/', repos.encoding,
|
|---|
| 856 | has_dir_node)
|
|---|
| 857 | if str_path is None:
|
|---|
| 858 | raise NoSuchNode(path, changectx.hex())
|
|---|
| 859 | lo = idx[0]
|
|---|
| 860 | for hi in xrange(lo, len(str_files)):
|
|---|
| 861 | if not str_files[hi].startswith(str_path):
|
|---|
| 862 | break
|
|---|
| 863 | str_path = str_path[:-1]
|
|---|
| 864 | str_entries = str_files[lo:hi]
|
|---|
| 865 | self.str_path = str_path
|
|---|
| 866 |
|
|---|
| 867 | # Determine `kind`, `rev` (requested rev) and `created_rev`
|
|---|
| 868 | # (last changed revision before requested rev)
|
|---|
| 869 |
|
|---|
| 870 | kind = None
|
|---|
| 871 | rev = changectx.rev()
|
|---|
| 872 | if str_path == '':
|
|---|
| 873 | kind = Node.DIRECTORY
|
|---|
| 874 | dirctx = changectx
|
|---|
| 875 | elif str_path in self.manifest: # then it's a file
|
|---|
| 876 | kind = Node.FILE
|
|---|
| 877 | self.filectx = changectx.filectx(str_path)
|
|---|
| 878 | created_rev = self.filectx.linkrev()
|
|---|
| 879 | # FIXME (0.13) this is a hack, we should fix that at the
|
|---|
| 880 | # Trac level, which should really show the
|
|---|
| 881 | # created_rev value for files in the browser.
|
|---|
| 882 | rev = created_rev
|
|---|
| 883 | else: # we already know it's a dir
|
|---|
| 884 | kind = Node.DIRECTORY
|
|---|
| 885 | if not dirctx:
|
|---|
| 886 | # we need to find the most recent change for a file below dir
|
|---|
| 887 | str_dir = str_path + '/'
|
|---|
| 888 | dirctxs = self._find_dirctx(changectx.rev(), [str_dir,],
|
|---|
| 889 | {str_dir: str_entries})
|
|---|
| 890 | dirctx = dirctxs.values()[0]
|
|---|
| 891 |
|
|---|
| 892 | if not kind:
|
|---|
| 893 | if repo.changelog.tip() == nullid or \
|
|---|
| 894 | not (self.manifest or str_path):
|
|---|
| 895 | # empty or emptied repository
|
|---|
| 896 | kind = Node.DIRECTORY
|
|---|
| 897 | dirctx = changectx
|
|---|
| 898 | else:
|
|---|
| 899 | raise NoSuchNode(path, changectx.hex())
|
|---|
| 900 |
|
|---|
| 901 | self.time = self.repos.from_hg_time(changectx.date())
|
|---|
| 902 | if dirctx is not None:
|
|---|
| 903 | # FIXME (0.13) same remark as above
|
|---|
| 904 | rev = created_rev = dirctx.rev()
|
|---|
| 905 | Node.__init__(self, self.repos, path, rev or '0', kind)
|
|---|
| 906 | self.created_path = path
|
|---|
| 907 | self.created_rev = created_rev
|
|---|
| 908 | self.data = None
|
|---|
| 909 |
|
|---|
| 910 | def _find_dirctx(self, max_rev, str_dirnames, str_entries):
|
|---|
| 911 | """Find most recent modification for each given directory path.
|
|---|
| 912 |
|
|---|
| 913 | :param max_rev: find no revision more recent than this one
|
|---|
| 914 | :param str_dirnames: directory paths to consider
|
|---|
| 915 | (list of `str` ending with '/')
|
|---|
| 916 | :param str_entries: maps each directory to the files it contains
|
|---|
| 917 |
|
|---|
| 918 | :return: a `dict` with `str_dirnames` as keys, `changectx` as values
|
|---|
| 919 |
|
|---|
| 920 | As directories are not first-class citizens in Mercurial, this
|
|---|
| 921 | operation is not trivial. There are basically two strategies:
|
|---|
| 922 |
|
|---|
| 923 | - for each file below the given directories, retrieve the
|
|---|
| 924 | linkrev (most recent modification for this file), and take
|
|---|
| 925 | the max; this approach is very inefficient for repositories
|
|---|
| 926 | containing many files (#7746)
|
|---|
| 927 |
|
|---|
| 928 | - retrieve the files modified when going backward through the
|
|---|
| 929 | changelog and detect the first occurrence of a change in
|
|---|
| 930 | each directory; this is much faster but can still be slow
|
|---|
| 931 | if some folders are only modified in the distant past
|
|---|
| 932 |
|
|---|
| 933 | It is possible to combine both approaches, and this can
|
|---|
| 934 | produce excellent results in some cases, for example browsing
|
|---|
| 935 | the root of the Hg mirror of the Linux repository (at revision
|
|---|
| 936 | 118733) takes several minutes with the first approach, 11s
|
|---|
| 937 | with the second, but only 1.2s with the hybrid approach.
|
|---|
| 938 |
|
|---|
| 939 | Note that the specialized scan of the changelog we do below is
|
|---|
| 940 | more efficient than the general cmdutil.walkchangerevs.
|
|---|
| 941 | """
|
|---|
| 942 | str_dirctxs = {}
|
|---|
| 943 | repo = self.repos.repo
|
|---|
| 944 | max_ctx = repo[max_rev]
|
|---|
| 945 | orevs = [-max_rev]
|
|---|
| 946 | revs = set(orevs)
|
|---|
| 947 | while orevs:
|
|---|
| 948 | r = -heappop(orevs)
|
|---|
| 949 | ctx = repo[r]
|
|---|
| 950 | for p in ctx.parents():
|
|---|
| 951 | if p and p.rev() not in revs:
|
|---|
| 952 | revs.add(p.rev())
|
|---|
| 953 | heappush(orevs, -p.rev())
|
|---|
| 954 | # lookup changes to str_dirnames in current cset
|
|---|
| 955 | for str_file in ctx.files():
|
|---|
| 956 | for str_dir in str_dirnames[:]:
|
|---|
| 957 | if str_file.startswith(str_dir):
|
|---|
| 958 | # rev for str_dir was found using first strategy
|
|---|
| 959 | str_dirctxs[str_dir] = ctx
|
|---|
| 960 | str_dirnames.remove(str_dir)
|
|---|
| 961 | if not str_dirnames: # nothing left to find
|
|---|
| 962 | return str_dirctxs
|
|---|
| 963 |
|
|---|
| 964 | # In parallel, try the filelog strategy (the 463, 2, 40
|
|---|
| 965 | # values below look a bit like magic numbers; actually
|
|---|
| 966 | # they were selected by testing the plugin on the Linux
|
|---|
| 967 | # and NetBeans repositories)
|
|---|
| 968 |
|
|---|
| 969 | # only use the filelog strategy every `n` revs
|
|---|
| 970 | n = 463
|
|---|
| 971 |
|
|---|
| 972 | # k, the number of files to examine per directory,
|
|---|
| 973 | # will be comprised between `min_files` and `max_files`
|
|---|
| 974 | min_files = 2
|
|---|
| 975 | max_files = 40 # (will be the max if there's only one dir left)
|
|---|
| 976 |
|
|---|
| 977 | if r % n == 0:
|
|---|
| 978 | k = max(min_files, max_files / len(str_dirnames))
|
|---|
| 979 | for str_dir in str_dirnames[:]:
|
|---|
| 980 | str_files = str_entries[str_dir]
|
|---|
| 981 | dr = str_dirctxs.get(str_dir, 0)
|
|---|
| 982 | for f in str_files[:k]:
|
|---|
| 983 | dr = max(dr, max_ctx.filectx(f).linkrev())
|
|---|
| 984 | str_files = str_files[k:]
|
|---|
| 985 | if str_files:
|
|---|
| 986 | # not all files for str_dir seen yet,
|
|---|
| 987 | # store max rev found so far
|
|---|
| 988 | str_entries[str_dir] = str_files
|
|---|
| 989 | str_dirctxs[str_dir] = dr
|
|---|
| 990 | else:
|
|---|
| 991 | # all files for str_dir were examined,
|
|---|
| 992 | # rev found using filelog strategy
|
|---|
| 993 | str_dirctxs[str_dir] = repo[dr]
|
|---|
| 994 | str_dirnames.remove(str_dir)
|
|---|
| 995 | if not str_dirnames:
|
|---|
| 996 | return str_dirctxs
|
|---|
| 997 |
|
|---|
| 998 |
|
|---|
| 999 | def subnode(self, str_path, subctx=None):
|
|---|
| 1000 | """Return a node with the same revision information but for
|
|---|
| 1001 | another path
|
|---|
| 1002 |
|
|---|
| 1003 | :param str_path: should be the an existing entry in the manifest
|
|---|
| 1004 | """
|
|---|
| 1005 | return MercurialNode(self.repos, self.repos.to_u(str_path),
|
|---|
| 1006 | self.changectx, self.manifest, subctx, str_path)
|
|---|
| 1007 |
|
|---|
| 1008 | def get_content(self):
|
|---|
| 1009 | if self.isdir:
|
|---|
| 1010 | return None
|
|---|
| 1011 | self.pos = 0 # reset the read()
|
|---|
| 1012 | return self # something that can be `read()` ...
|
|---|
| 1013 |
|
|---|
| 1014 | def read(self, size=None):
|
|---|
| 1015 | if self.isdir:
|
|---|
| 1016 | return TracError(_("Can't read from directory %(path)s",
|
|---|
| 1017 | path=self.path))
|
|---|
| 1018 | if self.data is None:
|
|---|
| 1019 | self.data = self.filectx.data()
|
|---|
| 1020 | self.pos = 0
|
|---|
| 1021 | if size:
|
|---|
| 1022 | prev_pos = self.pos
|
|---|
| 1023 | self.pos += size
|
|---|
| 1024 | return self.data[prev_pos:self.pos]
|
|---|
| 1025 | return self.data
|
|---|
| 1026 |
|
|---|
| 1027 | def get_entries(self):
|
|---|
| 1028 | if self.isfile:
|
|---|
| 1029 | return
|
|---|
| 1030 |
|
|---|
| 1031 | # dirnames are entries which are sub-directories
|
|---|
| 1032 | str_entries = {}
|
|---|
| 1033 | str_dirnames = []
|
|---|
| 1034 | def add_entry(str_file, idx):
|
|---|
| 1035 | str_entry = str_file
|
|---|
| 1036 | if idx > -1: # directory
|
|---|
| 1037 | str_entry = str_file[:idx + 1]
|
|---|
| 1038 | str_files = str_entries.setdefault(str_entry, [])
|
|---|
| 1039 | if not str_files:
|
|---|
| 1040 | str_dirnames.append(str_entry)
|
|---|
| 1041 | str_files.append(str_file)
|
|---|
| 1042 | else:
|
|---|
| 1043 | str_entries[str_entry] = 1
|
|---|
| 1044 |
|
|---|
| 1045 | if self.str_path:
|
|---|
| 1046 | str_dir = self.str_path + '/'
|
|---|
| 1047 | for str_file in self.manifest:
|
|---|
| 1048 | if str_file.startswith(str_dir):
|
|---|
| 1049 | add_entry(str_file, str_file.find('/', len(str_dir)))
|
|---|
| 1050 | else:
|
|---|
| 1051 | for str_file in self.manifest:
|
|---|
| 1052 | add_entry(str_file, str_file.find('/'))
|
|---|
| 1053 |
|
|---|
| 1054 | # pre-computing the changectx for the last change in each sub-directory
|
|---|
| 1055 | if str_dirnames:
|
|---|
| 1056 | dirctxs = self._find_dirctx(self.created_rev, str_dirnames,
|
|---|
| 1057 | str_entries)
|
|---|
| 1058 | else:
|
|---|
| 1059 | dirctxs = {}
|
|---|
| 1060 |
|
|---|
| 1061 | for str_entry in str_entries:
|
|---|
| 1062 | yield self.subnode(str_entry.rstrip('/'), dirctxs.get(str_entry))
|
|---|
| 1063 |
|
|---|
| 1064 | def get_history(self, limit=None):
|
|---|
| 1065 | repo = self.repos.repo
|
|---|
| 1066 | pats = []
|
|---|
| 1067 | if self.str_path:
|
|---|
| 1068 | pats.append('path:' + self.str_path)
|
|---|
| 1069 | opts = {'rev': ['%s:0' % self.changectx.hex()]}
|
|---|
| 1070 | if self.isfile:
|
|---|
| 1071 | opts['follow'] = True
|
|---|
| 1072 | if arity(cmdutil.walkchangerevs) == 4:
|
|---|
| 1073 | return self._get_history_1_4(repo, pats, opts, limit)
|
|---|
| 1074 | else:
|
|---|
| 1075 | return self._get_history_1_3(repo, pats, opts, limit)
|
|---|
| 1076 |
|
|---|
| 1077 | def _get_history_1_4(self, repo, pats, opts, limit):
|
|---|
| 1078 | matcher = match(repo[None], pats, opts)
|
|---|
| 1079 | if self.isfile:
|
|---|
| 1080 | fncache = {}
|
|---|
| 1081 | def prep(ctx, fns):
|
|---|
| 1082 | if self.isfile:
|
|---|
| 1083 | fncache[ctx.rev()] = self.repos.to_u(fns[0])
|
|---|
| 1084 | else:
|
|---|
| 1085 | def prep(ctx, fns):
|
|---|
| 1086 | pass
|
|---|
| 1087 |
|
|---|
| 1088 | # keep one lookahead entry so that we can detect renames
|
|---|
| 1089 | path = self.path
|
|---|
| 1090 | entry = None
|
|---|
| 1091 | count = 0
|
|---|
| 1092 | for ctx in cmdutil.walkchangerevs(repo, matcher, opts, prep):
|
|---|
| 1093 | if self.isfile and entry:
|
|---|
| 1094 | path = fncache[ctx.rev()]
|
|---|
| 1095 | if path != entry[0]:
|
|---|
| 1096 | entry = entry[0:2] + (Changeset.COPY,)
|
|---|
| 1097 | if entry:
|
|---|
| 1098 | yield entry
|
|---|
| 1099 | count += 1
|
|---|
| 1100 | if limit is not None and count >= limit:
|
|---|
| 1101 | return
|
|---|
| 1102 | entry = (path, ctx.hex(), Changeset.EDIT)
|
|---|
| 1103 | if entry:
|
|---|
| 1104 | if limit is None or count < limit:
|
|---|
| 1105 | entry = entry[0:2] + (Changeset.ADD,)
|
|---|
| 1106 | yield entry
|
|---|
| 1107 |
|
|---|
| 1108 | def _get_history_1_3(self, repo, pats, opts, limit):
|
|---|
| 1109 | if self.repos.version_info > (1, 3, 999):
|
|---|
| 1110 | changefn = lambda r: repo[r]
|
|---|
| 1111 | else:
|
|---|
| 1112 | changefn = lambda r: repo[r].changeset()
|
|---|
| 1113 | get = cachefunc(changefn)
|
|---|
| 1114 | if self.isfile:
|
|---|
| 1115 | fncache = {}
|
|---|
| 1116 | chgiter, matchfn = cmdutil.walkchangerevs(self.repos.ui, repo, pats,
|
|---|
| 1117 | get, opts)
|
|---|
| 1118 | # keep one lookahead entry so that we can detect renames
|
|---|
| 1119 | path = self.path
|
|---|
| 1120 | entry = None
|
|---|
| 1121 | count = 0
|
|---|
| 1122 | for st, rev, fns in chgiter:
|
|---|
| 1123 | if st == 'add' and self.isfile:
|
|---|
| 1124 | fncache[rev] = self.repos.to_u(fns[0])
|
|---|
| 1125 | elif st == 'iter':
|
|---|
| 1126 | if self.isfile and entry:
|
|---|
| 1127 | path = fncache[rev]
|
|---|
| 1128 | if path != entry[0]:
|
|---|
| 1129 | entry = entry[0:2] + (Changeset.COPY,)
|
|---|
| 1130 | if entry:
|
|---|
| 1131 | yield entry
|
|---|
| 1132 | count += 1
|
|---|
| 1133 | if limit is not None and count >= limit:
|
|---|
| 1134 | return
|
|---|
| 1135 | n = repo.changelog.node(rev)
|
|---|
| 1136 | entry = (path, hex(n), Changeset.EDIT)
|
|---|
| 1137 | if entry:
|
|---|
| 1138 | if limit is None or count < limit:
|
|---|
| 1139 | entry = entry[0:2] + (Changeset.ADD,)
|
|---|
| 1140 | yield entry
|
|---|
| 1141 |
|
|---|
| 1142 | def get_annotations(self):
|
|---|
| 1143 | annotations = []
|
|---|
| 1144 | if self.filectx:
|
|---|
| 1145 | for fc, line in self.filectx.annotate(follow=True):
|
|---|
| 1146 | annotations.append(fc.rev() or '0')
|
|---|
| 1147 | return annotations
|
|---|
| 1148 |
|
|---|
| 1149 | def get_properties(self):
|
|---|
| 1150 | if self.isfile and 'x' in self.manifest.flags(self.str_path):
|
|---|
| 1151 | return {'exe': '*'}
|
|---|
| 1152 | else:
|
|---|
| 1153 | return {}
|
|---|
| 1154 |
|
|---|
| 1155 | def get_content_length(self):
|
|---|
| 1156 | if self.isdir:
|
|---|
| 1157 | return None
|
|---|
| 1158 | return self.filectx.size()
|
|---|
| 1159 |
|
|---|
| 1160 | def get_content_type(self):
|
|---|
| 1161 | if self.isdir:
|
|---|
| 1162 | return None
|
|---|
| 1163 | if 'mq' in self.repos.params: # FIXME
|
|---|
| 1164 | if self.str_path not in ('.hgignore', 'series'):
|
|---|
| 1165 | return 'text/x-diff'
|
|---|
| 1166 | return ''
|
|---|
| 1167 |
|
|---|
| 1168 | def get_last_modified(self):
|
|---|
| 1169 | return self.time
|
|---|
| 1170 |
|
|---|
| 1171 |
|
|---|
| 1172 | class MercurialChangeset(Changeset):
|
|---|
| 1173 | """A changeset in the repository.
|
|---|
| 1174 |
|
|---|
| 1175 | This wraps the corresponding information from the changelog. The
|
|---|
| 1176 | files changes are obtained by comparing the current manifest to
|
|---|
| 1177 | the parent manifest(s).
|
|---|
| 1178 | """
|
|---|
| 1179 |
|
|---|
| 1180 | def __init__(self, repos, ctx):
|
|---|
| 1181 | self.repos = repos
|
|---|
| 1182 | self.ctx = ctx
|
|---|
| 1183 | self.branch = self.repos.to_u(ctx.branch())
|
|---|
| 1184 | # Note: desc and time are already processed by hg's
|
|---|
| 1185 | # `encoding.tolocal`; by setting $HGENCODING to latin1, we are
|
|---|
| 1186 | # however guaranteed to get back the bytes as they were
|
|---|
| 1187 | # stored.
|
|---|
| 1188 | desc = repos.to_u(ctx.description())
|
|---|
| 1189 | user = repos.to_u(ctx.user())
|
|---|
| 1190 | time = repos.from_hg_time(ctx.date())
|
|---|
| 1191 | Changeset.__init__(self, repos, ctx.hex(), desc, user, time)
|
|---|
| 1192 |
|
|---|
| 1193 | hg_properties = [
|
|---|
| 1194 | N_("Parents:"), N_("Children:"), N_("Branch:"), N_("Tags:")
|
|---|
| 1195 | ]
|
|---|
| 1196 |
|
|---|
| 1197 | def get_properties(self):
|
|---|
| 1198 | properties = {}
|
|---|
| 1199 | parents = self.ctx.parents()
|
|---|
| 1200 | if len(parents) > 1:
|
|---|
| 1201 | properties['hg-Parents'] = (self.repos,
|
|---|
| 1202 | [p.hex() for p in parents if p])
|
|---|
| 1203 | children = self.ctx.children()
|
|---|
| 1204 | if len(children) > 1:
|
|---|
| 1205 | properties['hg-Children'] = (self.repos,
|
|---|
| 1206 | [c.hex() for c in children])
|
|---|
| 1207 | if self.branch:
|
|---|
| 1208 | properties['hg-Branch'] = (self.repos, [self.branch])
|
|---|
| 1209 | tags = self.get_tags()
|
|---|
| 1210 | if len(tags):
|
|---|
| 1211 | properties['hg-Tags'] = (self.repos, tags)
|
|---|
| 1212 | for k, v in self.ctx.extra().iteritems():
|
|---|
| 1213 | if k != 'branch':
|
|---|
| 1214 | properties['hg-' + k] = (self.repos, v)
|
|---|
| 1215 | return properties
|
|---|
| 1216 |
|
|---|
| 1217 | def get_changes(self):
|
|---|
| 1218 | u = self.repos.to_u
|
|---|
| 1219 | repo = self.repos.repo
|
|---|
| 1220 | manifest = self.ctx.manifest()
|
|---|
| 1221 | parents = self.ctx.parents()
|
|---|
| 1222 |
|
|---|
| 1223 | renames = []
|
|---|
| 1224 | str_deletions = {}
|
|---|
| 1225 | changes = []
|
|---|
| 1226 | for str_file in self.ctx.files(): # added, edited and deleted files
|
|---|
| 1227 | f = u(str_file)
|
|---|
| 1228 | # TODO: find a way to detect conflicts and show how they were
|
|---|
| 1229 | # solved (kind of 3-way diff - theirs/mine/merged)
|
|---|
| 1230 | edits = [p for p in parents if str_file in p.manifest()]
|
|---|
| 1231 |
|
|---|
| 1232 | if str_file not in manifest:
|
|---|
| 1233 | str_deletions[str_file] = edits[0]
|
|---|
| 1234 | elif edits:
|
|---|
| 1235 | for p in edits:
|
|---|
| 1236 | changes.append((f, Node.FILE, Changeset.EDIT, f, p.rev()))
|
|---|
| 1237 | else:
|
|---|
| 1238 | renamed = repo.file(str_file).renamed(manifest[str_file])
|
|---|
| 1239 | if renamed:
|
|---|
| 1240 | renames.append((f, renamed))
|
|---|
| 1241 | else:
|
|---|
| 1242 | changes.append((f, Node.FILE, Changeset.ADD, '', None))
|
|---|
| 1243 | # distinguish between move and copy
|
|---|
| 1244 | for f, (str_base_path, base_filenode) in renames:
|
|---|
| 1245 | base_ctx = repo.filectx(str_base_path, fileid=base_filenode)
|
|---|
| 1246 | if str_base_path in str_deletions:
|
|---|
| 1247 | del str_deletions[str_base_path]
|
|---|
| 1248 | action = Changeset.MOVE
|
|---|
| 1249 | else:
|
|---|
| 1250 | action = Changeset.COPY
|
|---|
| 1251 | changes.append((f, Node.FILE, action, u(str_base_path),
|
|---|
| 1252 | base_ctx.rev()))
|
|---|
| 1253 | # remaining str_deletions are real deletions
|
|---|
| 1254 | for str_file, p in str_deletions.items():
|
|---|
| 1255 | f = u(str_file)
|
|---|
| 1256 | changes.append((f, Node.FILE, Changeset.DELETE, f, p.rev()))
|
|---|
| 1257 | changes.sort()
|
|---|
| 1258 | for change in changes:
|
|---|
| 1259 | yield change
|
|---|
| 1260 |
|
|---|
| 1261 | def get_branches(self):
|
|---|
| 1262 | """Yield branch names to which this changeset belong."""
|
|---|
| 1263 | if self.branch:
|
|---|
| 1264 | yield (self.branch, len(self.ctx.children()) == 0)
|
|---|
| 1265 |
|
|---|
| 1266 | def get_tags(self):
|
|---|
| 1267 | """Yield tag names to which this changeset belong."""
|
|---|
| 1268 | return [self.repos.to_u(t) for t in self.ctx.tags()]
|
|---|