hgext3rd/topic/__init__.py
branchmercurial-4.1
changeset 3002 00e4d31b8cdb
parent 2999 d94a4e150633
child 3009 548506eb68f4
equal deleted inserted replaced
2809:f25c79365f5d 3002:00e4d31b8cdb
    52 
    52 
    53 from __future__ import absolute_import
    53 from __future__ import absolute_import
    54 
    54 
    55 import re
    55 import re
    56 import time
    56 import time
       
    57 import weakref
    57 
    58 
    58 from mercurial.i18n import _
    59 from mercurial.i18n import _
    59 from mercurial import (
    60 from mercurial import (
       
    61     bookmarks,
       
    62     changelog,
    60     cmdutil,
    63     cmdutil,
    61     commands,
    64     commands,
    62     context,
    65     context,
    63     error,
    66     error,
    64     extensions,
    67     extensions,
    65     hg,
    68     hg,
    66     localrepo,
    69     localrepo,
    67     lock,
    70     lock as lockmod,
    68     merge,
    71     merge,
    69     namespaces,
    72     namespaces,
    70     node,
    73     node,
    71     obsolete,
    74     obsolete,
    72     obsutil,
       
    73     patch,
    75     patch,
    74     phases,
    76     phases,
    75     registrar,
    77     registrar,
       
    78     scmutil,
    76     templatefilters,
    79     templatefilters,
    77     util,
    80     util,
    78 )
    81 )
    79 
    82 
    80 from . import (
    83 from . import (
       
    84     compat,
    81     constants,
    85     constants,
    82     revset as topicrevset,
    86     revset as topicrevset,
    83     destination,
    87     destination,
    84     stack,
    88     stack,
    85     topicmap,
    89     topicmap,
   116               # (first pick I could think off, update as needed
   120               # (first pick I could think off, update as needed
   117               'log.topic': 'green_background',
   121               'log.topic': 'green_background',
   118               'topic.active': 'green',
   122               'topic.active': 'green',
   119              }
   123              }
   120 
   124 
   121 version = '0.2.0'
   125 __version__ = '0.3.0.dev'
   122 testedwith = '4.0.2 4.1.3 4.2.1'
   126 testedwith = '4.0.2 4.1.3 4.2.1'
   123 minimumhgversion = '4.0'
   127 minimumhgversion = '4.0'
   124 buglink = 'https://bz.mercurial-scm.org/'
   128 buglink = 'https://bz.mercurial-scm.org/'
   125 
   129 
   126 def _contexttopic(self, force=False):
   130 def _contexttopic(self, force=False):
   132     topic = self.topic()
   136     topic = self.topic()
   133     if not topic:
   137     if not topic:
   134         # XXX we might want to include t0 here,
   138         # XXX we might want to include t0 here,
   135         # however t0 is related to  'currenttopic' which has no place here.
   139         # however t0 is related to  'currenttopic' which has no place here.
   136         return None
   140         return None
   137     revlist = stack.getstack(self._repo, topic=topic)
   141     revlist = stack.stack(self._repo, topic=topic)
   138     try:
   142     try:
   139         return revlist.index(self.rev())
   143         return revlist.index(self.rev())
   140     except IndexError:
   144     except IndexError:
   141         # Lets move to the last ctx of the current topic
   145         # Lets move to the last ctx of the current topic
   142         return None
   146         return None
   151         idx = int(name[1:])
   155         idx = int(name[1:])
   152         ttype = 'topic'
   156         ttype = 'topic'
   153         tname = topic = repo.currenttopic
   157         tname = topic = repo.currenttopic
   154         if not tname:
   158         if not tname:
   155             raise error.Abort(_('cannot resolve "%s": no active topic') % name)
   159             raise error.Abort(_('cannot resolve "%s": no active topic') % name)
   156         revs = list(stack.getstack(repo, topic=topic))
   160         revs = list(stack.stack(repo, topic=topic))
   157     elif branchrev.match(name):
   161     elif branchrev.match(name):
   158         ttype = 'branch'
   162         ttype = 'branch'
   159         idx = int(name[1:])
   163         idx = int(name[1:])
   160         tname = branch = repo[None].branch()
   164         tname = branch = repo[None].branch()
   161         revs = list(stack.getstack(repo, branch=branch))
   165         revs = list(stack.stack(repo, branch=branch))
   162 
   166 
   163     if revs is not None:
   167     if revs is not None:
   164         try:
   168         try:
   165             r = revs[idx]
   169             r = revs[idx]
   166         except IndexError:
   170         except IndexError:
   183         return [t]
   187         return [t]
   184     return []
   188     return []
   185 
   189 
   186 def uisetup(ui):
   190 def uisetup(ui):
   187     destination.modsetup(ui)
   191     destination.modsetup(ui)
   188     topicrevset.modsetup(ui)
       
   189     discovery.modsetup(ui)
   192     discovery.modsetup(ui)
   190     topicmap.modsetup(ui)
   193     topicmap.modsetup(ui)
   191     setupimportexport(ui)
   194     setupimportexport(ui)
   192 
   195 
   193     extensions.afterloaded('rebase', _fixrebase)
   196     extensions.afterloaded('rebase', _fixrebase)
   194 
   197 
   195     entry = extensions.wrapcommand(commands.table, 'commit', commitwrap)
   198     entry = extensions.wrapcommand(commands.table, 'commit', commitwrap)
   196     entry[1].append(('t', 'topic', '',
   199     entry[1].append(('t', 'topic', '',
   197                      _("use specified topic"), _('TOPIC')))
   200                      _("use specified topic"), _('TOPIC')))
       
   201 
       
   202     entry = extensions.wrapcommand(commands.table, 'push', pushoutgoingwrap)
       
   203     entry[1].append(('t', 'topic', '',
       
   204                      _("topic to push"), _('TOPIC')))
       
   205 
       
   206     entry = extensions.wrapcommand(commands.table, 'outgoing',
       
   207                                    pushoutgoingwrap)
       
   208     entry[1].append(('t', 'topic', '',
       
   209                      _("topic to push"), _('TOPIC')))
   198 
   210 
   199     extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap)
   211     extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap)
   200     extensions.wrapfunction(merge, 'update', mergeupdatewrap)
   212     extensions.wrapfunction(merge, 'update', mergeupdatewrap)
   201     # We need to check whether t0 or b0 is passed to override the default update
   213     # We need to check whether t0 or b0 is passed to override the default update
   202     # behaviour of changing topic and I can't find a better way
   214     # behaviour of changing topic and I can't find a better way
   298                         usetopic = not self._repo.publishing()
   310                         usetopic = not self._repo.publishing()
   299                         return self._repo.branchmap(topic=usetopic)
   311                         return self._repo.branchmap(topic=usetopic)
   300                 peer.__class__ = topicpeer
   312                 peer.__class__ = topicpeer
   301             return peer
   313             return peer
   302 
   314 
       
   315         def transaction(self, desc, *a, **k):
       
   316             ctr = self.currenttransaction()
       
   317             tr = super(topicrepo, self).transaction(desc, *a, **k)
       
   318             if desc in ('strip', 'repair') or ctr is not None:
       
   319                 return tr
       
   320 
       
   321             # real transaction start
       
   322             ct = self.currenttopic
       
   323             if not ct:
       
   324                 return tr
       
   325             ctwasempty = stack.stackdata(self, topic=ct)['changesetcount'] == 0
       
   326 
       
   327             reporef = weakref.ref(self)
       
   328 
       
   329             def currenttopicempty(tr):
       
   330                 # check active topic emptyness
       
   331                 repo = reporef()
       
   332                 csetcount = stack.stackdata(repo, topic=ct)['changesetcount']
       
   333                 empty = csetcount == 0
       
   334                 if empty and not ctwasempty:
       
   335                     ui.status('active topic %r is now empty\n' % ct)
       
   336                 if ctwasempty and not empty:
       
   337                     if csetcount == 1:
       
   338                         msg = _('active topic %r grew its first changeset\n')
       
   339                         ui.status(msg % ct)
       
   340                     else:
       
   341                         msg = _('active topic %r grew its %s first changesets\n')
       
   342                         ui.status(msg % (ct, csetcount))
       
   343 
       
   344             tr.addpostclose('signalcurrenttopicempty', currenttopicempty)
       
   345             return tr
       
   346 
   303     repo.__class__ = topicrepo
   347     repo.__class__ = topicrepo
   304     repo._topics = None
   348     repo._topics = None
   305     if util.safehasattr(repo, 'names'):
   349     if util.safehasattr(repo, 'names'):
   306         repo.names.addnamespace(namespaces.namespace(
   350         repo.names.addnamespace(namespaces.namespace(
   307             'topics', 'topic', namemap=_namemap, nodemap=_nodemap,
   351             'topics', 'topic', namemap=_namemap, nodemap=_nodemap,
   308             listnames=lambda repo: repo.topics))
   352             listnames=lambda repo: repo.topics))
       
   353     # Wrap workingctx extra to return the topic name
       
   354     extensions.wrapfunction(context.workingctx, '__init__', wrapinit)
       
   355     # Wrap changelog.add to drop empty topic
       
   356     extensions.wrapfunction(changelog.changelog, 'add', wrapadd)
       
   357 
       
   358 def wrapinit(orig, self, repo, *args, **kwargs):
       
   359     orig(self, repo, *args, **kwargs)
       
   360     if repo.currenttopic:
       
   361         self._extra[constants.extrakey] = repo.currenttopic
       
   362     else:
       
   363         # Empty key will be dropped from extra by another hack at the changegroup level
       
   364         self._extra[constants.extrakey] = ''
       
   365 
       
   366 def wrapadd(orig, cl, manifest, files, desc, transaction, p1, p2, user,
       
   367             date=None, extra=None):
       
   368     if constants.extrakey in extra and not extra[constants.extrakey]:
       
   369         extra = extra.copy()
       
   370         del extra[constants.extrakey]
       
   371     return orig(cl, manifest, files, desc, transaction, p1, p2, user,
       
   372                 date=date, extra=extra)
       
   373 
       
   374 # revset predicates are automatically registered at loading via this symbol
       
   375 revsetpredicate = topicrevset.revsetpredicate
   309 
   376 
   310 @command('topics', [
   377 @command('topics', [
   311         ('', 'clear', False, 'clear active topic if any'),
   378         ('', 'clear', False, 'clear active topic if any'),
   312         ('r', 'rev', '', 'revset of existing revisions', _('REV')),
   379         ('r', 'rev', [], 'revset of existing revisions', _('REV')),
   313         ('l', 'list', False, 'show the stack of changeset in the topic'),
   380         ('l', 'list', False, 'show the stack of changeset in the topic'),
   314         ('', 'age', False, 'show when you last touched the topics')
   381         ('', 'age', False, 'show when you last touched the topics'),
       
   382         ('', 'current', None, 'display the current topic only'),
   315     ] + commands.formatteropts,
   383     ] + commands.formatteropts,
   316     _('hg topics [TOPIC]'))
   384     _('hg topics [TOPIC]'))
   317 def topics(ui, repo, topic='', clear=False, rev=None, list=False, **opts):
   385 def topics(ui, repo, topic=None, **opts):
   318     """View current topic, set current topic, change topic for a set of revisions, or see all topics.
   386     """View current topic, set current topic, change topic for a set of revisions, or see all topics.
   319 
   387 
   320     Clear topic on existing topiced revisions:
   388     Clear topic on existing topiced revisions::
   321         `hg topic --rev <related revset> --clear`
   389 
   322 
   390       hg topics --rev <related revset> --clear
   323     Change topic on some revisions:
   391 
   324         `hg topic <newtopicname> --rev <related revset>`
   392     Change topic on some revisions::
   325 
   393 
   326     Clear current topic:
   394       hg topics <newtopicname> --rev <related revset>
   327         `hg topic --clear`
   395 
   328 
   396     Clear current topic::
   329     Set current topic:
   397 
   330         `hg topic <topicname>`
   398       hg topics --clear
   331 
   399 
   332     List of topics:
   400     Set current topic::
   333         `hg topics`
   401 
   334 
   402       hg topics <topicname>
   335     List of topics with their last touched time sorted according to it:
   403 
   336         `hg topic --age`
   404     List of topics::
       
   405 
       
   406       hg topics
       
   407 
       
   408     List of topics sorted according to their last touched time displaying last
       
   409     touched time and the user who last touched the topic::
       
   410 
       
   411       hg topics --age
   337 
   412 
   338     The active topic (if any) will be prepended with a "*".
   413     The active topic (if any) will be prepended with a "*".
   339 
   414 
       
   415     The `--current` flag helps to take active topic into account. For
       
   416     example, if you want to set the topic on all the draft changesets to the
       
   417     active topic, you can do:
       
   418         `hg topics -r "draft()" --current`
       
   419 
   340     The --verbose version of this command display various information on the state of each topic."""
   420     The --verbose version of this command display various information on the state of each topic."""
       
   421 
       
   422     clear = opts.get('clear')
       
   423     list = opts.get('list')
       
   424     rev = opts.get('rev')
       
   425     current = opts.get('current')
       
   426     age = opts.get('age')
       
   427 
       
   428     if current and topic:
       
   429         raise error.Abort(_("cannot use --current when setting a topic"))
       
   430     if current and clear:
       
   431         raise error.Abort(_("cannot use --current and --clear"))
       
   432     if clear and topic:
       
   433         raise error.Abort(_("cannot use --clear when setting a topic"))
       
   434     if age and topic:
       
   435         raise error.Abort(_("cannot use --age while setting a topic"))
       
   436 
       
   437     touchedrevs = set()
       
   438     if rev:
       
   439         touchedrevs = scmutil.revrange(repo, rev)
       
   440 
       
   441     if topic:
       
   442         topic = topic.strip()
       
   443         if not topic:
       
   444             raise error.Abort(_("topic name cannot consist entirely of whitespaces"))
       
   445         # Have some restrictions on the topic name just like bookmark name
       
   446         scmutil.checknewlabel(repo, topic, 'topic')
       
   447 
   341     if list:
   448     if list:
   342         if clear or rev:
   449         if clear or rev:
   343             raise error.Abort(_("cannot use --clear or --rev with --list"))
   450             raise error.Abort(_("cannot use --clear or --rev with --list"))
   344         if not topic:
   451         if not topic:
   345             topic = repo.currenttopic
   452             topic = repo.currenttopic
   346         if not topic:
   453         if not topic:
   347             raise error.Abort(_('no active topic to list'))
   454             raise error.Abort(_('no active topic to list'))
   348         return stack.showstack(ui, repo, topic=topic, opts=opts)
   455         return stack.showstack(ui, repo, topic=topic, opts=opts)
   349 
   456 
   350     if rev:
   457     if touchedrevs:
   351         if not obsolete.isenabled(repo, obsolete.createmarkersopt):
   458         if not obsolete.isenabled(repo, obsolete.createmarkersopt):
   352             raise error.Abort(_('must have obsolete enabled to change topics'))
   459             raise error.Abort(_('must have obsolete enabled to change topics'))
   353         if clear:
   460         if clear:
   354             topic = None
   461             topic = None
       
   462         elif opts.get('current'):
       
   463             topic = repo.currenttopic
   355         elif not topic:
   464         elif not topic:
   356             raise error.Abort('changing topic requires a topic name or --clear')
   465             raise error.Abort('changing topic requires a topic name or --clear')
   357         if any(not c.mutable() for c in repo.set('%r and public()', rev)):
   466         if repo.revs('%ld and public()', touchedrevs):
   358             raise error.Abort("can't change topic of a public change")
   467             raise error.Abort("can't change topic of a public change")
   359         return _changetopics(ui, repo, rev, topic)
   468         wl = l = txn = None
   360 
   469         try:
       
   470             wl = repo.wlock()
       
   471             l = repo.lock()
       
   472             txn = repo.transaction('rewrite-topics')
       
   473             rewrote = _changetopics(ui, repo, touchedrevs, topic)
       
   474             txn.close()
       
   475             ui.status('changed topic on %d changes\n' % rewrote)
       
   476         finally:
       
   477             lockmod.release(txn, l, wl)
       
   478             repo.invalidate()
       
   479         return
       
   480 
       
   481     ct = repo.currenttopic
   361     if clear:
   482     if clear:
       
   483         empty = stack.stackdata(repo, topic=ct)['changesetcount'] == 0
       
   484         if empty:
       
   485             if ct:
       
   486                 ui.status(_('clearing empty topic "%s"\n') % ct)
   362         return _changecurrenttopic(repo, None)
   487         return _changecurrenttopic(repo, None)
   363 
   488 
   364     if topic:
   489     if topic:
       
   490         if not ct:
       
   491             ui.status(_('marked working directory as topic: %s\n') % topic)
   365         return _changecurrenttopic(repo, topic)
   492         return _changecurrenttopic(repo, topic)
   366 
   493 
   367     _listtopics(ui, repo, opts)
   494     # `hg topic --current`
       
   495     ret = 0
       
   496     if current and not ct:
       
   497         ui.write_err(_('no active topic\n'))
       
   498         ret = 1
       
   499     elif current:
       
   500         fm = ui.formatter('topic', opts)
       
   501         namemask = '%s\n'
       
   502         label = 'topic.active'
       
   503         fm.startitem()
       
   504         fm.write('topic', namemask, ct, label=label)
       
   505         fm.end()
       
   506     else:
       
   507         _listtopics(ui, repo, opts)
       
   508     return ret
   368 
   509 
   369 @command('stack', [
   510 @command('stack', [
   370     ] + commands.formatteropts,
   511     ] + commands.formatteropts,
   371     _('hg stack [TOPIC]'))
   512     _('hg stack [TOPIC]'))
   372 def cmdstack(ui, repo, topic='', **opts):
   513 def cmdstack(ui, repo, topic='', **opts):
   383         topic = repo.currenttopic
   524         topic = repo.currenttopic
   384     if topic is None:
   525     if topic is None:
   385         branch = repo[None].branch()
   526         branch = repo[None].branch()
   386     return stack.showstack(ui, repo, branch=branch, topic=topic, opts=opts)
   527     return stack.showstack(ui, repo, branch=branch, topic=topic, opts=opts)
   387 
   528 
       
   529 @command('debugcb|debugconvertbookmark', [
       
   530         ('b', 'bookmark', '', _('bookmark to convert to topic')),
       
   531         ('', 'all', None, _('convert all bookmarks to topics')),
       
   532     ],
       
   533     _('[-b BOOKMARK] [--all]'))
       
   534 def debugconvertbookmark(ui, repo, **opts):
       
   535     """Converts a bookmark to a topic with the same name.
       
   536     """
       
   537 
       
   538     bookmark = opts.get('bookmark')
       
   539     convertall = opts.get('all')
       
   540 
       
   541     if convertall and bookmark:
       
   542         raise error.Abort(_("cannot use '--all' and '-b' together"))
       
   543     if not (convertall or bookmark):
       
   544         raise error.Abort(_("you must specify either '--all' or '-b'"))
       
   545 
       
   546     bmstore = repo._bookmarks
       
   547 
       
   548     nodetobook = {}
       
   549     for book, revnode in bmstore.iteritems():
       
   550         if nodetobook.get(revnode):
       
   551             nodetobook[revnode].append(book)
       
   552         else:
       
   553             nodetobook[revnode] = [book]
       
   554 
       
   555     # a list of nodes which we have skipped so that we don't print the skip
       
   556     # warning repeatedly
       
   557     skipped = []
       
   558 
       
   559     actions = {}
       
   560 
       
   561     lock = wlock = tr = None
       
   562     try:
       
   563         wlock = repo.wlock()
       
   564         lock = repo.lock()
       
   565         if bookmark:
       
   566             try:
       
   567                 node = bmstore[bookmark]
       
   568             except KeyError:
       
   569                 raise error.Abort(_("no such bookmark exists: '%s'") % bookmark)
       
   570 
       
   571             revnum = repo[node].rev()
       
   572             if len(nodetobook[node]) > 1:
       
   573                 ui.status(_("skipping revision '%d' as it has multiple bookmarks "
       
   574                           "on it\n") % revnum)
       
   575                 return
       
   576             targetrevs = _findconvertbmarktopic(repo, bookmark)
       
   577             if targetrevs:
       
   578                 actions[(bookmark, revnum)] = targetrevs
       
   579 
       
   580         elif convertall:
       
   581             for bmark, revnode in sorted(bmstore.iteritems()):
       
   582                 revnum = repo[revnode].rev()
       
   583                 if revnum in skipped:
       
   584                     continue
       
   585                 if len(nodetobook[revnode]) > 1:
       
   586                     ui.status(_("skipping '%d' as it has multiple bookmarks on"
       
   587                               " it\n") % revnum)
       
   588                     skipped.append(revnum)
       
   589                     continue
       
   590                 if bmark == '@':
       
   591                     continue
       
   592                 targetrevs = _findconvertbmarktopic(repo, bmark)
       
   593                 if targetrevs:
       
   594                     actions[(bmark, revnum)] = targetrevs
       
   595 
       
   596         if actions:
       
   597             try:
       
   598                 tr = repo.transaction('debugconvertbookmark')
       
   599                 for ((bmark, revnum), targetrevs) in sorted(actions.iteritems()):
       
   600                     _applyconvertbmarktopic(ui, repo, targetrevs, revnum, bmark, tr)
       
   601                 tr.close()
       
   602             finally:
       
   603                 tr.release()
       
   604     finally:
       
   605         lockmod.release(lock, wlock)
       
   606 
       
   607 # inspired from mercurial.repair.stripbmrevset
       
   608 CONVERTBOOKREVSET = """
       
   609 not public() and (
       
   610     ancestors(bookmark(%s))
       
   611     and not ancestors(
       
   612         (
       
   613             (head() and not bookmark(%s))
       
   614             or (bookmark() - bookmark(%s))
       
   615         ) - (
       
   616             descendants(bookmark(%s))
       
   617             - bookmark(%s)
       
   618         )
       
   619     )
       
   620 )
       
   621 """
       
   622 
       
   623 def _findconvertbmarktopic(repo, bmark):
       
   624     """find revisions unambigiously defined by a bookmark
       
   625 
       
   626     find all changesets under the bookmark and under that bookmark only.
       
   627     """
       
   628     return repo.revs(CONVERTBOOKREVSET, bmark, bmark, bmark, bmark, bmark)
       
   629 
       
   630 def _applyconvertbmarktopic(ui, repo, revs, old, bmark, tr):
       
   631     """apply bookmark convertion to topic
       
   632 
       
   633     Sets a topic as same as bname to all the changesets under the bookmark
       
   634     and delete the bookmark, if topic is set to any changeset
       
   635 
       
   636     old is the revision on which bookmark bmark is and tr is transaction object.
       
   637     """
       
   638 
       
   639     rewrote = _changetopics(ui, repo, revs, bmark)
       
   640     # We didn't changed topic to any changesets because the revset
       
   641     # returned an empty set of revisions, so let's skip deleting the
       
   642     # bookmark corresponding to which we didn't put a topic on any
       
   643     # changeset
       
   644     if rewrote == 0:
       
   645         return
       
   646     ui.status(_('changed topic to "%s" on %d revisions\n') % (bmark,
       
   647               rewrote))
       
   648     ui.debug('removing bookmark "%s" from "%d"' % (bmark, old))
       
   649     bookmarks.delete(repo, tr, [bmark])
       
   650 
   388 def _changecurrenttopic(repo, newtopic):
   651 def _changecurrenttopic(repo, newtopic):
   389     """changes the current topic."""
   652     """changes the current topic."""
   390 
   653 
   391     if newtopic:
   654     if newtopic:
   392         with repo.wlock():
   655         with repo.wlock():
   394                 f.write(newtopic)
   657                 f.write(newtopic)
   395     else:
   658     else:
   396         if repo.vfs.exists('topic'):
   659         if repo.vfs.exists('topic'):
   397             repo.vfs.unlink('topic')
   660             repo.vfs.unlink('topic')
   398 
   661 
   399 def _changetopics(ui, repo, revset, newtopic):
   662 def _changetopics(ui, repo, revs, newtopic):
       
   663     """ Changes topic to newtopic of all the revisions in the revset and return
       
   664     the count of revisions whose topic has been changed.
       
   665     """
   400     rewrote = 0
   666     rewrote = 0
   401     wl = l = txn = None
   667     p1 = None
   402     try:
   668     p2 = None
   403         wl = repo.wlock()
   669     successors = {}
   404         l = repo.lock()
   670     for r in revs:
   405         txn = repo.transaction('rewrite-topics')
   671         c = repo[r]
   406         p1 = None
   672 
   407         p2 = None
   673         def filectxfn(repo, ctx, path):
   408         successors = {}
   674             try:
   409         for c in repo.set('%r', revset):
   675                 return c[path]
   410             def filectxfn(repo, ctx, path):
   676             except error.ManifestLookupError:
   411                 try:
   677                 return None
   412                     return c[path]
   678         fixedextra = dict(c.extra())
   413                 except error.ManifestLookupError:
   679         ui.debug('old node id is %s\n' % node.hex(c.node()))
   414                     return None
   680         ui.debug('origextra: %r\n' % fixedextra)
   415             fixedextra = dict(c.extra())
   681         oldtopic = fixedextra.get(constants.extrakey, None)
   416             ui.debug('old node id is %s\n' % node.hex(c.node()))
   682         if oldtopic == newtopic:
   417             ui.debug('origextra: %r\n' % fixedextra)
   683             continue
   418             oldtopic = fixedextra.get(constants.extrakey, None)
   684         if newtopic is None:
   419             if oldtopic == newtopic:
   685             del fixedextra[constants.extrakey]
   420                 continue
   686         else:
   421             if newtopic is None:
   687             fixedextra[constants.extrakey] = newtopic
   422                 del fixedextra[constants.extrakey]
   688         fixedextra[constants.changekey] = c.hex()
   423             else:
   689         if 'amend_source' in fixedextra:
   424                 fixedextra[constants.extrakey] = newtopic
   690             # TODO: right now the commitctx wrapper in
   425             fixedextra[constants.changekey] = c.hex()
   691             # topicrepo overwrites the topic in extra if
   426             if 'amend_source' in fixedextra:
   692             # amend_source is set to support 'hg commit
   427                 # TODO: right now the commitctx wrapper in
   693             # --amend'. Support for amend should be adjusted
   428                 # topicrepo overwrites the topic in extra if
   694             # to not be so invasive.
   429                 # amend_source is set to support 'hg commit
   695             del fixedextra['amend_source']
   430                 # --amend'. Support for amend should be adjusted
   696         ui.debug('changing topic of %s from %s to %s\n' % (
   431                 # to not be so invasive.
   697             c, oldtopic, newtopic))
   432                 del fixedextra['amend_source']
   698         ui.debug('fixedextra: %r\n' % fixedextra)
   433             ui.debug('changing topic of %s from %s to %s\n' % (
   699         # While changing topic of set of linear commits, make sure that
   434                 c, oldtopic, newtopic))
   700         # we base our commits on new parent rather than old parent which
   435             ui.debug('fixedextra: %r\n' % fixedextra)
   701         # was obsoleted while changing the topic
   436             # While changing topic of set of linear commits, make sure that
   702         p1 = c.p1().node()
   437             # we base our commits on new parent rather than old parent which
   703         p2 = c.p2().node()
   438             # was obsoleted while changing the topic
   704         if p1 in successors:
   439             p1 = c.p1().node()
   705             p1 = successors[p1]
   440             p2 = c.p2().node()
   706         if p2 in successors:
   441             if p1 in successors:
   707             p2 = successors[p2]
   442                 p1 = successors[p1]
   708         mc = context.memctx(
   443             if p2 in successors:
   709             repo, (p1, p2), c.description(),
   444                 p2 = successors[p2]
   710             c.files(), filectxfn,
   445             mc = context.memctx(
   711             user=c.user(), date=c.date(), extra=fixedextra)
   446                 repo, (p1, p2), c.description(),
   712         newnode = repo.commitctx(mc)
   447                 c.files(), filectxfn,
   713         successors[c.node()] = newnode
   448                 user=c.user(), date=c.date(), extra=fixedextra)
   714         ui.debug('new node id is %s\n' % node.hex(newnode))
   449             newnode = repo.commitctx(mc)
   715         obsolete.createmarkers(repo, [(c, (repo[newnode],))])
   450             successors[c.node()] = newnode
   716         rewrote += 1
   451             ui.debug('new node id is %s\n' % node.hex(newnode))
   717     # move the working copy too
   452             obsolete.createmarkers(repo, [(c, (repo[newnode],))])
   718     wctx = repo[None]
   453             rewrote += 1
   719     # in-progress merge is a bit too complex for now.
   454         # move the working copy too
   720     if len(wctx.parents()) == 1:
   455         wctx = repo[None]
   721         newid = successors.get(wctx.p1().node())
   456         # in-progress merge is a bit too complex for now.
   722         if newid is not None:
   457         if len(wctx.parents()) == 1:
   723             hg.update(repo, newid, quietempty=True)
   458             newid = successors.get(wctx.p1().node())
   724     return rewrote
   459             if newid is not None:
       
   460                 hg.update(repo, newid, quietempty=True)
       
   461         txn.close()
       
   462     finally:
       
   463         lock.release(txn, l, wl)
       
   464         repo.invalidate()
       
   465     ui.status('changed topic on %d changes\n' % rewrote)
       
   466 
   725 
   467 def _listtopics(ui, repo, opts):
   726 def _listtopics(ui, repo, opts):
   468     fm = ui.formatter('topics', opts)
   727     fm = ui.formatter('topics', opts)
   469     showlast = opts.get('age')
   728     showlast = opts.get('age')
   470     if showlast:
   729     if showlast:
   515                          data['behindcount'],
   774                          data['behindcount'],
   516                          label='topic.list.behindcount')
   775                          label='topic.list.behindcount')
   517             elif -1 == data['behindcount']:
   776             elif -1 == data['behindcount']:
   518                 fm.plain(', ')
   777                 fm.plain(', ')
   519                 fm.write('behinderror', '%s',
   778                 fm.write('behinderror', '%s',
   520                          _('ambiguous destination'),
   779                          _('ambiguous destination: %s') % data['behinderror'],
   521                          label='topic.list.behinderror')
   780                          label='topic.list.behinderror')
   522             fm.plain(')')
   781             fm.plain(')')
   523         fm.plain('\n')
   782         fm.plain('\n')
   524     fm.end()
   783     fm.end()
   525 
   784 
   531     if topics:
   790     if topics:
   532         maxwidth = max(len(t) for t in topics)
   791         maxwidth = max(len(t) for t in topics)
   533         namemask = '%%-%is' % maxwidth
   792         namemask = '%%-%is' % maxwidth
   534     activetopic = repo.currenttopic
   793     activetopic = repo.currenttopic
   535     for timevalue in times:
   794     for timevalue in times:
   536         curtopics = timedict[timevalue][1]
   795         curtopics = sorted(timedict[timevalue][1])
   537         for topic in curtopics:
   796         for topic, user in curtopics:
   538             fm.startitem()
   797             fm.startitem()
   539             marker = ' '
   798             marker = ' '
   540             label = 'topic'
   799             label = 'topic'
   541             active = (topic == activetopic)
   800             active = (topic == activetopic)
   542             if active:
   801             if active:
   545             fm.plain(' %s ' % marker, label=label)
   804             fm.plain(' %s ' % marker, label=label)
   546             fm.write('topic', namemask, topic, label=label)
   805             fm.write('topic', namemask, topic, label=label)
   547             fm.data(active=active)
   806             fm.data(active=active)
   548             fm.plain(' (')
   807             fm.plain(' (')
   549             if timevalue == -1:
   808             if timevalue == -1:
   550                 timestr = 'not yet touched'
   809                 timestr = 'empty and active'
   551             else:
   810             else:
   552                 timestr = templatefilters.age(timedict[timevalue][0])
   811                 timestr = templatefilters.age(timedict[timevalue][0])
   553             fm.write('lasttouched', '%s', timestr, label='topic.list.time')
   812             fm.write('lasttouched', '%s', timestr, label='topic.list.time')
       
   813             if user:
       
   814                 fm.write('usertouched', ' by %s', user, label='topic.list.user')
   554             fm.plain(')')
   815             fm.plain(')')
   555             fm.plain('\n')
   816             fm.plain('\n')
   556     fm.end()
   817     fm.end()
   557 
   818 
   558 def _getlasttouched(repo, topics):
   819 def _getlasttouched(repo, topics):
   561     passed from current time for a topic as keys and topic name as values.
   822     passed from current time for a topic as keys and topic name as values.
   562     """
   823     """
   563     topicstime = {}
   824     topicstime = {}
   564     curtime = time.time()
   825     curtime = time.time()
   565     for t in topics:
   826     for t in topics:
       
   827         secspassed = -1
       
   828         user = None
   566         maxtime = (0, 0)
   829         maxtime = (0, 0)
   567         trevs = repo.revs("topic(%s)", t)
   830         trevs = repo.revs("topic(%s)", t)
   568         # Need to check for the time of all changesets in the topic, whether
   831         # Need to check for the time of all changesets in the topic, whether
   569         # they are obsolete of non-heads
   832         # they are obsolete of non-heads
   570         # XXX: can we just rely on the max rev number for this
   833         # XXX: can we just rely on the max rev number for this
   571         for revs in trevs:
   834         for revs in trevs:
   572             rt = repo[revs].date()
   835             rt = repo[revs].date()
   573             if rt[0] > maxtime[0]:
   836             if rt[0] >= maxtime[0]:
   574                 # Can store the rev to gather more info
   837                 # Can store the rev to gather more info
   575                 # latesthead = revs
   838                 # latesthead = revs
   576                 maxtime = rt
   839                 maxtime = rt
       
   840                 user = repo[revs].user()
   577             # looking on the markers also to get more information and accurate
   841             # looking on the markers also to get more information and accurate
   578             # last touch time.
   842             # last touch time.
   579             obsmarkers = obsutil.getmarkers(repo, [repo[revs].node()])
   843             obsmarkers = compat.getmarkers(repo, [repo[revs].node()])
   580             for marker in obsmarkers:
   844             for marker in obsmarkers:
   581                 rt = marker.date()
   845                 rt = marker.date()
   582                 if rt[0] > maxtime[0]:
   846                 if rt[0] > maxtime[0]:
       
   847                     user = marker.metadata().get('user', user)
   583                     maxtime = rt
   848                     maxtime = rt
   584         # is the topic still yet untouched
   849 
   585         if not trevs:
   850         # Making the username more better
   586             secspassed = -1
   851         username = None
   587         else:
   852         if user:
       
   853             # user is of form "abc <abc@xyz.com>"
       
   854             username = user.split('<')[0]
       
   855             if not username:
       
   856                 # user is of form "<abc@xyz.com>"
       
   857                 username = user[1:-1]
       
   858             username = username.strip()
       
   859 
       
   860         topicuser = (t, username)
       
   861 
       
   862         if trevs:
   588             secspassed = (curtime - maxtime[0])
   863             secspassed = (curtime - maxtime[0])
   589         try:
   864         try:
   590             topicstime[secspassed][1].append(t)
   865             topicstime[secspassed][1].append(topicuser)
   591         except KeyError:
   866         except KeyError:
   592             topicstime[secspassed] = (maxtime, [t])
   867             topicstime[secspassed] = (maxtime, [topicuser])
   593 
   868 
   594     return topicstime
   869     return topicstime
   595 
   870 
   596 def summaryhook(ui, repo):
   871 def summaryhook(ui, repo):
   597     t = repo.currenttopic
   872     t = repo.currenttopic
   620     if t:
   895     if t:
   621         ret = ret.replace("\nHG: branch",
   896         ret = ret.replace("\nHG: branch",
   622                           "\nHG: topic '%s'\nHG: branch" % t)
   897                           "\nHG: topic '%s'\nHG: branch" % t)
   623     return ret
   898     return ret
   624 
   899 
       
   900 def pushoutgoingwrap(orig, ui, repo, *args, **opts):
       
   901     if opts.get('topic'):
       
   902         topicrevs = repo.revs('topic(%s) - obsolete()', opts['topic'])
       
   903         opts.setdefault('rev', []).extend(topicrevs)
       
   904     return orig(ui, repo, *args, **opts)
       
   905 
   625 def mergeupdatewrap(orig, repo, node, branchmerge, force, *args, **kwargs):
   906 def mergeupdatewrap(orig, repo, node, branchmerge, force, *args, **kwargs):
   626     matcher = kwargs.get('matcher')
   907     matcher = kwargs.get('matcher')
   627     partial = not (matcher is None or matcher.always())
   908     partial = not (matcher is None or matcher.always())
   628     wlock = repo.wlock()
   909     wlock = repo.wlock()
   629     isrebase = False
   910     isrebase = False
   633         # The mergeupdatewrap function makes the destination's topic as the
   914         # The mergeupdatewrap function makes the destination's topic as the
   634         # current topic. This is right for merge but wrong for rebase. We check
   915         # current topic. This is right for merge but wrong for rebase. We check
   635         # if rebase is running and update the currenttopic to topic of new
   916         # if rebase is running and update the currenttopic to topic of new
   636         # rebased commit. We have explicitly stored in config if rebase is
   917         # rebased commit. We have explicitly stored in config if rebase is
   637         # running.
   918         # running.
       
   919         ot = repo.currenttopic
       
   920         empty = stack.stackdata(repo, topic=ot)['changesetcount'] == 0
   638         if repo.ui.hasconfig('experimental', 'topicrebase'):
   921         if repo.ui.hasconfig('experimental', 'topicrebase'):
   639             isrebase = True
   922             isrebase = True
   640         if repo.ui.configbool('_internal', 'keep-topic'):
   923         if repo.ui.configbool('_internal', 'keep-topic'):
   641             ist0 = True
   924             ist0 = True
   642         if ((not partial and not branchmerge) or isrebase) and not ist0:
   925         if ((not partial and not branchmerge) or isrebase) and not ist0:
   643             ot = repo.currenttopic
       
   644             t = ''
   926             t = ''
   645             pctx = repo[node]
   927             pctx = repo[node]
   646             if pctx.phase() > phases.public:
   928             if pctx.phase() > phases.public:
   647                 t = pctx.topic()
   929                 t = pctx.topic()
   648             with repo.vfs.open('topic', 'w') as f:
   930             with repo.vfs.open('topic', 'w') as f:
   649                 f.write(t)
   931                 f.write(t)
   650             if t and t != ot:
   932             if t and t != ot:
   651                 repo.ui.status(_("switching to topic %s\n") % t)
   933                 repo.ui.status(_("switching to topic %s\n") % t)
       
   934             if ot and not t and empty:
       
   935                 repo.ui.status(_('clearing empty topic "%s"\n') % ot)
   652         elif ist0:
   936         elif ist0:
   653             repo.ui.status(_("preserving the current topic '%s'\n") %
   937             repo.ui.status(_("preserving the current topic '%s'\n") % ot)
   654                            repo.currenttopic)
       
   655         return ret
   938         return ret
   656     finally:
   939     finally:
   657         wlock.release()
   940         wlock.release()
   658 
   941 
   659 def checkt0(orig, ui, repo, node=None, rev=None, *args, **kwargs):
   942 def checkt0(orig, ui, repo, node=None, rev=None, *args, **kwargs):