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