Edgewall Software

Ticket #8417: backend.3.py

File backend.3.py, 55.1 KB (added by me@…, 12 years ago)

Revised again: error handling & branch display in the timeline

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