| 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 [] |
|---|