Edgewall Software

source: mercurial-plugin/tracext/hg/backend.py@ 70:afea9958cf7c

1.0
Last change on this file since 70:afea9958cf7c was 70:afea9958cf7c, checked in by Jun Omae <jun66j5@…>, 5 years ago

1.0.0.9dev: compatibility fix with Mercurial 4.8 (closes #13100)

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