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