--- a/README.md Wed Mar 16 12:14:20 2016 -0700
+++ b/README.md Tue Mar 22 00:19:14 2016 -0700
@@ -13,3 +13,8 @@
[extensions]
topics=path/to/hg-topics/src
+
+If you are using Mercurial 3.7 use:
+
+ [extensions]
+ hgext3rd.topics=path/to/hg-topics/src
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/__init__.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,3 @@
+from __future__ import absolute_import
+import pkgutil
+__path__ = pkgutil.extend_path(__path__, __name__)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/__init__.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,373 @@
+# __init__.py - topic extension
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+"""support for topic branches
+
+Topic branches are lightweight branches which
+disappear when changes are finalized.
+
+This is sort of similar to a bookmark, but it applies to a whole
+series instead of a single revision.
+"""
+import functools
+import contextlib
+import re
+
+from mercurial.i18n import _
+from mercurial import branchmap
+from mercurial import bundle2
+from mercurial import changegroup
+from mercurial import cmdutil
+from mercurial import commands
+from mercurial import context
+from mercurial import discovery as discoverymod
+from mercurial import error
+from mercurial import exchange
+from mercurial import extensions
+from mercurial import localrepo
+from mercurial import lock
+from mercurial import merge
+from mercurial import namespaces
+from mercurial import node
+from mercurial import obsolete
+from mercurial import patch
+from mercurial import phases
+from mercurial import util
+from mercurial import wireproto
+
+from . import constants
+from . import revset as topicrevset
+from . import destination
+from . import stack
+from . import topicmap
+from . import discovery
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+colortable = {'topic.stack.index': 'yellow',
+ 'topic.stack.state.base': 'dim',
+ 'topic.stack.state.clean': 'green',
+ 'topic.stack.index.current': 'cyan', # random pick
+ 'topic.stack.state.current': 'cyan bold', # random pick
+ 'topic.stack.desc.current': 'cyan', # random pick
+ 'topic.stack.state.unstable': 'red',
+ }
+
+testedwith = '3.7'
+
+def _contexttopic(self):
+ return self.extra().get(constants.extrakey, '')
+context.basectx.topic = _contexttopic
+
+topicrev = re.compile(r'^t\d+$')
+
+def _namemap(repo, name):
+ if topicrev.match(name):
+ idx = int(name[1:])
+ topic = repo.currenttopic
+ if not topic:
+ raise error.Abort(_('cannot resolve "%s": no active topic') % name)
+ revs = list(stack.getstack(repo, topic))
+ try:
+ r = revs[idx]
+ except IndexError:
+ msg = _('cannot resolve "%s": topic "%s" has only %d changesets')
+ raise error.Abort(msg % (name, topic, len(revs)))
+ return [repo[r].node()]
+ return [ctx.node() for ctx in
+ repo.set('not public() and extra(topic, %s)', name)]
+
+def _nodemap(repo, node):
+ ctx = repo[node]
+ t = ctx.topic()
+ if t and ctx.phase() > phases.public:
+ return [t]
+ return []
+
+def uisetup(ui):
+ destination.setupdest()
+
+@contextlib.contextmanager
+def usetopicmap(repo):
+ """use awful monkey patching to update the topic cache"""
+ oldbranchcache = branchmap.branchcache
+ oldfilename = branchmap._filename
+ oldread = branchmap.read
+ oldcaches = getattr(repo, '_branchcaches', {})
+ try:
+ branchmap.branchcache = topicmap.topiccache
+ branchmap._filename = topicmap._filename
+ branchmap.read = topicmap.readtopicmap
+ repo._branchcaches = getattr(repo, '_topiccaches', {})
+ yield
+ repo._topiccaches = repo._branchcaches
+ finally:
+ repo._branchcaches = oldcaches
+ branchmap.branchcache = oldbranchcache
+ branchmap._filename = oldfilename
+ branchmap.read = oldread
+
+def cgapply(orig, repo, *args, **kwargs):
+ with usetopicmap(repo):
+ return orig(repo, *args, **kwargs)
+
+def reposetup(ui, repo):
+ orig = repo.__class__
+ if not isinstance(repo, localrepo.localrepository):
+ return # this can be a peer in the ssh case (puzzling)
+ class topicrepo(repo.__class__):
+
+ def _restrictcapabilities(self, caps):
+ caps = super(topicrepo, self)._restrictcapabilities(caps)
+ caps.add('topics')
+ return caps
+
+ def commit(self, *args, **kwargs):
+ backup = self.ui.backupconfig('ui', 'allowemptycommit')
+ try:
+ if repo.currenttopic != repo['.'].topic():
+ # bypass the core "nothing changed" logic
+ self.ui.setconfig('ui', 'allowemptycommit', True)
+ return orig.commit(self, *args, **kwargs)
+ finally:
+ self.ui.restoreconfig(backup)
+
+ def commitctx(self, ctx, error=None):
+ if isinstance(ctx, context.workingcommitctx):
+ current = self.currenttopic
+ if current:
+ ctx.extra()[constants.extrakey] = current
+ if (isinstance(ctx, context.memctx) and
+ ctx.extra().get('amend_source') and
+ ctx.topic() and
+ not self.currenttopic):
+ # we are amending and need to remove a topic
+ del ctx.extra()[constants.extrakey]
+ with usetopicmap(self):
+ return orig.commitctx(self, ctx, error=error)
+
+ @property
+ def topics(self):
+ topics = set(['', self.currenttopic])
+ for c in self.set('not public()'):
+ topics.add(c.topic())
+ topics.remove('')
+ return topics
+
+ @property
+ def currenttopic(self):
+ return self.vfs.tryread('topic')
+
+ def branchmap(self, topic=True):
+ if not topic:
+ super(topicrepo, self).branchmap()
+ with usetopicmap(self):
+ branchmap.updatecache(self)
+ return self._topiccaches[self.filtername]
+
+ def destroyed(self, *args, **kwargs):
+ with usetopicmap(self):
+ return super(topicrepo, self).destroyed(*args, **kwargs)
+
+ def invalidatecaches(self):
+ super(topicrepo, self).invalidatecaches()
+ if '_topiccaches' in vars(self.unfiltered()):
+ self.unfiltered()._topiccaches.clear()
+
+ def peer(self):
+ peer = super(topicrepo, self).peer()
+ if getattr(peer, '_repo', None) is not None: # localpeer
+ class topicpeer(peer.__class__):
+ def branchmap(self):
+ usetopic = not self._repo.publishing()
+ return self._repo.branchmap(topic=usetopic)
+ peer.__class__ = topicpeer
+ return peer
+
+
+ repo.__class__ = topicrepo
+ if util.safehasattr(repo, 'names'):
+ repo.names.addnamespace(namespaces.namespace(
+ 'topics', 'topic', namemap=_namemap, nodemap=_nodemap,
+ listnames=lambda repo: repo.topics))
+
+@command('topics [TOPIC]', [
+ ('', 'clear', False, 'clear active topic if any'),
+ ('', 'change', '', 'revset of existing revisions to change topic'),
+ ('l', 'list', False, 'show the stack of changeset in the topic'),
+ ] + commands.formatteropts)
+def topics(ui, repo, topic='', clear=False, change=None, list=False, **opts):
+ """View current topic, set current topic, or see all topics."""
+ if list:
+ if clear or change:
+ raise error.Abort(_("cannot use --clear or --change with --list"))
+ return stack.showstack(ui, repo, topic, opts)
+
+ if change:
+ if not obsolete.isenabled(repo, obsolete.createmarkersopt):
+ raise error.Abort(_('must have obsolete enabled to use --change'))
+ if not topic and not clear:
+ raise error.Abort('changing topic requires a topic name or --clear')
+ if any(not c.mutable() for c in repo.set('%r and public()', change)):
+ raise error.Abort("can't change topic of a public change")
+ rewrote = 0
+ needevolve = False
+ l = repo.lock()
+ txn = repo.transaction('rewrite-topics')
+ try:
+ for c in repo.set('%r', change):
+ def filectxfn(repo, ctx, path):
+ try:
+ return c[path]
+ except error.ManifestLookupError:
+ return None
+ fixedextra = dict(c.extra())
+ ui.debug('old node id is %s\n' % node.hex(c.node()))
+ ui.debug('origextra: %r\n' % fixedextra)
+ newtopic = None if clear else topic
+ oldtopic = fixedextra.get(constants.extrakey, None)
+ if oldtopic == newtopic:
+ continue
+ if clear:
+ del fixedextra[constants.extrakey]
+ else:
+ fixedextra[constants.extrakey] = topic
+ if 'amend_source' in fixedextra:
+ # TODO: right now the commitctx wrapper in
+ # topicrepo overwrites the topic in extra if
+ # amend_source is set to support 'hg commit
+ # --amend'. Support for amend should be adjusted
+ # to not be so invasive.
+ del fixedextra['amend_source']
+ ui.debug('changing topic of %s from %s to %s\n' % (
+ c, oldtopic, newtopic))
+ ui.debug('fixedextra: %r\n' % fixedextra)
+ mc = context.memctx(
+ repo, (c.p1().node(), c.p2().node()), c.description(),
+ c.files(), filectxfn,
+ user=c.user(), date=c.date(), extra=fixedextra)
+ newnode = repo.commitctx(mc)
+ ui.debug('new node id is %s\n' % node.hex(newnode))
+ needevolve = needevolve or (len(c.children()) > 0)
+ obsolete.createmarkers(repo, [(c, (repo[newnode],))])
+ rewrote += 1
+ txn.close()
+ except:
+ try:
+ txn.abort()
+ finally:
+ repo.invalidate()
+ raise
+ finally:
+ lock.release(txn, l)
+ ui.status('changed topic on %d changes\n' % rewrote)
+ if needevolve:
+ evolvetarget = 'topic(%s)' % topic if topic else 'not topic()'
+ ui.status('please run hg evolve --rev "%s" now\n' % evolvetarget)
+ if clear:
+ if repo.vfs.exists('topic'):
+ repo.vfs.unlink('topic')
+ return
+ if topic:
+ with repo.vfs.open('topic', 'w') as f:
+ f.write(topic)
+ return
+ current = repo.currenttopic
+ for t in sorted(repo.topics):
+ marker = '*' if t == current else ' '
+ ui.write(' %s %s\n' % (marker, t))
+
+def summaryhook(ui, repo):
+ t = repo.currenttopic
+ if not t:
+ return
+ # i18n: column positioning for "hg summary"
+ ui.write(_("topic: %s\n") % t)
+
+def commitwrap(orig, ui, repo, *args, **opts):
+ if opts.get('topic'):
+ t = opts['topic']
+ with repo.vfs.open('topic', 'w') as f:
+ f.write(t)
+ return orig(ui, repo, *args, **opts)
+
+def committextwrap(orig, repo, ctx, subs, extramsg):
+ ret = orig(repo, ctx, subs, extramsg)
+ t = repo.currenttopic
+ if t:
+ ret = ret.replace("\nHG: branch",
+ "\nHG: topic '%s'\nHG: branch" % t)
+ return ret
+
+def mergeupdatewrap(orig, repo, node, branchmerge, force, *args, **kwargs):
+ partial = bool(len(args)) or 'matcher' in kwargs
+ wlock = repo.wlock()
+ try:
+ ret = orig(repo, node, branchmerge, force, *args, **kwargs)
+ if not partial and not branchmerge:
+ ot = repo.currenttopic
+ t = ''
+ pctx = repo[node]
+ if pctx.phase() > phases.public:
+ t = pctx.topic()
+ with repo.vfs.open('topic', 'w') as f:
+ f.write(t)
+ if t and t != ot:
+ repo.ui.status(_("switching to topic %s\n") % t)
+ return ret
+ finally:
+ wlock.release()
+
+def _fixrebase(loaded):
+ if not loaded:
+ return
+
+ def savetopic(ctx, extra):
+ if ctx.topic():
+ extra[constants.extrakey] = ctx.topic()
+
+ def newmakeextrafn(orig, copiers):
+ return orig(copiers + [savetopic])
+
+ rebase = extensions.find("rebase")
+ extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn)
+
+def _exporttopic(seq, ctx):
+ topic = ctx.topic()
+ if topic:
+ return 'EXP-Topic %s' % topic
+ return None
+
+def _importtopic(repo, patchdata, extra, opts):
+ if 'topic' in patchdata:
+ extra['topic'] = patchdata['topic']
+
+extensions.afterloaded('rebase', _fixrebase)
+
+entry = extensions.wrapcommand(commands.table, 'commit', commitwrap)
+entry[1].append(('t', 'topic', '',
+ _("use specified topic"), _('TOPIC')))
+
+extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap)
+extensions.wrapfunction(merge, 'update', mergeupdatewrap)
+extensions.wrapfunction(discoverymod, '_headssummary', discovery._headssummary)
+extensions.wrapfunction(wireproto, 'branchmap', discovery.wireprotobranchmap)
+extensions.wrapfunction(wireproto, '_capabilities', discovery.wireprotocaps)
+extensions.wrapfunction(bundle2, 'handlecheckheads', discovery.handlecheckheads)
+bundle2.handlecheckheads.params = frozenset() # we need a proper wrape b2 part stuff
+bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads
+extensions.wrapfunction(exchange, '_pushb2phases', discovery._pushb2phases)
+extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply)
+exchange.b2partsgenmapping['phase'] = exchange._pushb2phases
+topicrevset.modsetup()
+cmdutil.summaryhooks.add('topic', summaryhook)
+
+if util.safehasattr(cmdutil, 'extraexport'):
+ cmdutil.extraexport.append('topic')
+ cmdutil.extraexportmap['topic'] = _exporttopic
+if util.safehasattr(cmdutil, 'extrapreimport'):
+ cmdutil.extrapreimport.append('topic')
+ cmdutil.extrapreimportmap['topic'] = _importtopic
+if util.safehasattr(patch, 'patchheadermap'):
+ patch.patchheadermap.append(('EXP-Topic', 'topic'))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/constants.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,1 @@
+extrakey = 'topic'
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/destination.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,98 @@
+from mercurial import error
+from mercurial import util
+from mercurial import destutil
+from mercurial import extensions
+from mercurial import bookmarks
+from mercurial.i18n import _
+
+def _destmergebranch(orig, repo, action='merge', sourceset=None, onheadcheck=True):
+ p1 = repo['.']
+ top = p1.topic()
+ if top:
+ heads = repo.revs('heads(topic(.)::topic(.))')
+ if p1.rev() not in heads:
+ raise error.Abort(_("not at topic head, update or explicit"))
+ elif 1 == len(heads):
+ # should look at all branch involved but... later
+ bhead = ngtip(repo, p1.branch(), all=True)
+ if not bhead:
+ raise error.Abort(_("nothing to merge"))
+ elif 1 == len(bhead):
+ return bhead.first()
+ else:
+ raise error.Abort(_("branch '%s' has %d heads - "
+ "please merge with an explicit rev")
+ % (p1.branch(), len(bhead)),
+ hint=_("run 'hg heads .' to see heads"))
+ elif 2 == len(heads):
+ heads = [r for r in heads if r != p1.rev()]
+ # XXX: bla bla bla bla bla
+ if 1 < len(heads):
+ raise error.Abort(_('working directory not at a head revision'),
+ hint=_("use 'hg update' or merge with an "
+ "explicit revision"))
+ return heads[0]
+ elif 2 < len(heads):
+ raise error.Abort(_("topic '%s' has %d heads - "
+ "please merge with an explicit rev")
+ % (top, len(heads)))
+ else:
+ assert False # that's impossible
+ if getattr(orig, 'func_default', ()): # version above hg-3.7
+ return orig(repo, action, sourceset, onheadcheck)
+ else:
+ return orig(repo)
+
+def _destupdatetopic(repo, clean, check):
+ """decide on an update destination from current topic"""
+ movemark = node = None
+ topic = repo.currenttopic
+ revs = repo.revs('.::topic("%s")' % topic)
+ if not revs:
+ return None, None, None
+ node = revs.last()
+ if bookmarks.isactivewdirparent(repo):
+ movemark = repo['.'].node()
+ return node, movemark, None
+
+def desthistedit(orig, ui, repo):
+ if not (ui.config('histedit', 'defaultrev', None) is None
+ and repo.currenttopic):
+ return orig(ui, repo)
+ revs = repo.revs('::. and stack()')
+ if revs:
+ return revs.min()
+ return None
+
+def setupdest():
+ if util.safehasattr(destutil, '_destmergebranch'):
+ extensions.wrapfunction(destutil, '_destmergebranch', _destmergebranch)
+ rebase = extensions.find('rebase')
+ if (util.safehasattr(rebase, '_destrebase')
+ # logic not shared with merge yet < hg-3.8
+ and not util.safehasattr(rebase, '_definesets')):
+ extensions.wrapfunction(rebase, '_destrebase', _destmergebranch)
+ if util.safehasattr(destutil, 'destupdatesteps'):
+ bridx = destutil.destupdatesteps.index('branch')
+ destutil.destupdatesteps.insert(bridx, 'topic')
+ destutil.destupdatestepmap['topic'] = _destupdatetopic
+ if util.safehasattr(destutil, 'desthistedit'):
+ extensions.wrapfunction(destutil, 'desthistedit', desthistedit)
+
+def ngtip(repo, branch, all=False):
+ """tip new generation"""
+ ## search for untopiced heads of branch
+ # could be heads((::branch(x) - topic()))
+ # but that is expensive
+ #
+ # we should write plain code instead
+ subquery = '''heads(
+ parents(
+ ancestor(
+ (head() and branch(%s)
+ or (topic() and branch(%s)))))
+ ::(head() and branch(%s))
+ - topic())'''
+ if not all:
+ subquery = 'max(%s)' % subquery
+ return repo.revs(subquery, branch, branch, branch)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/discovery.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,109 @@
+import weakref
+from mercurial import branchmap
+from mercurial import error
+from mercurial import exchange
+from mercurial.i18n import _
+from . import topicmap
+
+def _headssummary(orig, repo, remote, outgoing):
+ publishing = ('phases' not in remote.listkeys('namespaces')
+ or bool(remote.listkeys('phases').get('publishing', False)))
+ if publishing or not remote.capable('topics'):
+ return orig(repo, remote, outgoing)
+ oldgetitem = repo.__getitem__
+ oldrepo = repo.__class__
+ oldbranchcache = branchmap.branchcache
+ oldfilename = branchmap._filename
+ try:
+ class repocls(repo.__class__):
+ def __getitem__(self, key):
+ ctx = super(repocls, self).__getitem__(key)
+ oldbranch = ctx.branch
+ def branch():
+ branch = oldbranch()
+ topic = ctx.topic()
+ if topic:
+ branch = "%s:%s" % (branch, topic)
+ return branch
+ ctx.branch = branch
+ return ctx
+ repo.__class__ = repocls
+ branchmap.branchcache = topicmap.topiccache
+ branchmap._filename = topicmap._filename
+ summary = orig(repo, remote, outgoing)
+ for key, value in summary.iteritems():
+ if ':' in key: # This is a topic
+ if value[0] is None and value[1]:
+ summary[key] = ([value[1].pop(0)], ) + value[1:]
+ return summary
+ finally:
+ repo.__class__ = oldrepo
+ branchmap.branchcache = oldbranchcache
+ branchmap._filename = oldfilename
+
+def wireprotobranchmap(orig, repo, proto):
+ oldrepo = repo.__class__
+ try:
+ class repocls(repo.__class__):
+ def branchmap(self):
+ usetopic = not self.publishing()
+ return super(repocls, self).branchmap(topic=usetopic)
+ repo.__class__ = repocls
+ return orig(repo, proto)
+ finally:
+ repo.__class__ = oldrepo
+
+
+# Discovery have deficiency around phases, branch can get new heads with pure
+# phases change. This happened with a changeset was allowed to be pushed
+# because it had a topic, but it later become public and create a new branch
+# head.
+#
+# Handle this by doing an extra check for new head creation server side
+def _nbheads(repo):
+ data = {}
+ for b in repo.branchmap().iterbranches():
+ if ':' in b[0]:
+ continue
+ data[b[0]] = len(b[1])
+ return data
+
+def handlecheckheads(orig, op, inpart):
+ orig(op, inpart)
+ if op.repo.publishing():
+ return
+ tr = op.gettransaction()
+ if tr.hookargs['source'] not in ('push', 'serve'): # not a push
+ return
+ tr._prepushheads = _nbheads(op.repo)
+ reporef = weakref.ref(op.repo)
+ oldvalidator = tr.validator
+ def validator(tr):
+ repo = reporef()
+ if repo is not None:
+ repo.invalidatecaches()
+ finalheads = _nbheads(repo)
+ for branch, oldnb in tr._prepushheads.iteritems():
+ newnb = finalheads.pop(branch, 0)
+ if oldnb < newnb:
+ msg = _('push create a new head on branch "%s"' % branch)
+ raise error.Abort(msg)
+ for branch, newnb in finalheads.iteritems():
+ if 1 < newnb:
+ msg = _('push create more than 1 head on new branch "%s"' % branch)
+ raise error.Abort(msg)
+ return oldvalidator(tr)
+ tr.validator = validator
+handlecheckheads.params = frozenset()
+
+def _pushb2phases(orig, pushop, bundler):
+ hascheck = any(p.type == 'check:heads' for p in bundler._parts)
+ if pushop.outdatedphases and not hascheck:
+ exchange._pushb2ctxcheckheads(pushop, bundler)
+ return orig(pushop, bundler)
+
+def wireprotocaps(orig, repo, proto):
+ caps = orig(repo, proto)
+ if repo.peer().capable('topics'):
+ caps.append('topics')
+ return caps
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/revset.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,66 @@
+from mercurial import revset
+from mercurial import util
+
+from . import constants, destination, stack
+
+try:
+ mkmatcher = revset._stringmatcher
+except AttributeError:
+ mkmatcher = util.stringmatcher
+
+
+def topicset(repo, subset, x):
+ """`topic([topic])`
+ Specified topic or all changes with any topic specified.
+
+ If `topic` starts with `re:` the remainder of the name is treated
+ as a regular expression.
+
+ TODO: make `topic(revset)` work the same as `branch(revset)`.
+ """
+ args = revset.getargs(x, 0, 1, 'topic takes one or no arguments')
+ if args:
+ # match a specific topic
+ topic = revset.getstring(args[0], 'topic() argument must be a string')
+ if topic == '.':
+ topic = repo['.'].extra().get('topic', '')
+ _kind, _pattern, matcher = mkmatcher(topic)
+ else:
+ matcher = lambda t: bool(t)
+ drafts = subset.filter(lambda r: repo[r].mutable())
+ return drafts.filter(
+ lambda r: matcher(repo[r].extra().get(constants.extrakey, '')))
+
+def ngtipset(repo, subset, x):
+ """`ngtip([branch])`
+
+ The untopiced tip.
+
+ Name is horrible so that people change it.
+ """
+ args = revset.getargs(x, 1, 1, 'topic takes one')
+ # match a specific topic
+ branch = revset.getstring(args[0], 'ngtip() argument must be a string')
+ if branch == '.':
+ branch = repo['.'].branch()
+ return subset & destination.ngtip(repo, branch)
+
+def stackset(repo, subset, x):
+ """`stack()`
+ All relevant changes in the current topic,
+
+ This is roughly equivalent to 'topic(.) - obsolete' with a sorting moving
+ unstable changeset after there future parent (as if evolve where already
+ run)."""
+ topic = repo.currenttopic
+ if not topic:
+ raise error.Abort(_('no active topic to list'))
+ # ordering hack, boo
+ return revset.baseset(stack.getstack(repo, topic)) & subset
+
+
+
+def modsetup():
+ revset.symbols.update({'topic': topicset})
+ revset.symbols.update({'ngtip': ngtipset})
+ revset.symbols.update({'stack': stackset})
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/stack.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,157 @@
+# stack.py - code related to stack workflow
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+import collections
+from mercurial.i18n import _
+from mercurial import error
+from mercurial import extensions
+from mercurial import obsolete
+
+def getstack(repo, topic):
+ # XXX need sorting
+ trevs = repo.revs("topic(%s) - obsolete()", topic)
+ return _orderrevs(repo, trevs)
+
+def showstack(ui, repo, topic, opts):
+ if not topic:
+ topic = repo.currenttopic
+ if not topic:
+ raise error.Abort(_('no active topic to list'))
+ fm = ui.formatter('topicstack', opts)
+ prev = None
+ for idx, r in enumerate(getstack(repo, topic)):
+ ctx = repo[r]
+ p1 = ctx.p1()
+ if p1.obsolete():
+ p1 = repo[_singlesuccessor(repo, p1)]
+ if p1.rev() != prev:
+ # display the parent of the chanset
+ state = 'base'
+ symbol= '_'
+ fm.startitem()
+ fm.plain(' ') # XXX 2 is the right padding by chance
+ fm.write('topic.stack.state', '%s', symbol,
+ label='topic.stack.state topic.stack.state.%s' % state)
+ fm.plain(' ')
+ fm.write('topic.stack.desc', '%s',
+ p1.description().splitlines()[0],
+ label='topic.stack.desc topic.stack.desc.%s' % state)
+ fm.plain('\n')
+ fm.end()
+ # super crude initial version
+ symbol = ':'
+ state = 'clean'
+ if repo.revs('%d and parents()', r):
+ symbol = '@'
+ state = 'current'
+ if repo.revs('%d and unstable()', r):
+ symbol = '$'
+ state = 'unstable'
+ fm.startitem()
+ fm.write('topic.stack.index', 't%d', idx,
+ label='topic.stack.index topic.stack.index.%s' % state)
+ fm.write('topic.stack.state.symbol', '%s', symbol,
+ label='topic.stack.state topic.stack.state.%s' % state)
+ fm.plain(' ')
+ fm.write('topic.stack.desc', '%s', ctx.description().splitlines()[0],
+ label='topic.stack.desc topic.stack.desc.%s' % state)
+ fm.condwrite(state != 'clean', 'topic.stack.state', ' (%s)', state,
+ label='topic.stack.state topic.stack.state.%s' % state)
+ fm.plain('\n')
+ fm.end()
+ prev = r
+
+# Copied from evolve 081605c2e9b6
+
+def _orderrevs(repo, revs):
+ """Compute an ordering to solve instability for the given revs
+
+ revs is a list of unstable revisions.
+
+ Returns the same revisions ordered to solve their instability from the
+ bottom to the top of the stack that the stabilization process will produce
+ eventually.
+
+ This ensures the minimal number of stabilizations, as we can stabilize each
+ revision on its final stabilized destination.
+ """
+ # Step 1: Build the dependency graph
+ dependencies, rdependencies = builddependencies(repo, revs)
+ # Step 2: Build the ordering
+ # Remove the revisions with no dependency(A) and add them to the ordering.
+ # Removing these revisions leads to new revisions with no dependency (the
+ # one depending on A) that we can remove from the dependency graph and add
+ # to the ordering. We progress in a similar fashion until the ordering is
+ # built
+ solvablerevs = [r for r in sorted(dependencies.keys())
+ if not dependencies[r]]
+ ordering = []
+ while solvablerevs:
+ rev = solvablerevs.pop()
+ for dependent in rdependencies[rev]:
+ dependencies[dependent].remove(rev)
+ if not dependencies[dependent]:
+ solvablerevs.append(dependent)
+ del dependencies[rev]
+ ordering.append(rev)
+
+ ordering.extend(sorted(dependencies))
+ return ordering
+
+def builddependencies(repo, revs):
+ """returns dependency graphs giving an order to solve instability of revs
+ (see _orderrevs for more information on usage)"""
+
+ # For each troubled revision we keep track of what instability if any should
+ # be resolved in order to resolve it. Example:
+ # dependencies = {3: [6], 6:[]}
+ # Means that: 6 has no dependency, 3 depends on 6 to be solved
+ dependencies = {}
+ # rdependencies is the inverted dict of dependencies
+ rdependencies = collections.defaultdict(set)
+
+ for r in revs:
+ dependencies[r] = set()
+ for p in repo[r].parents():
+ try:
+ succ = _singlesuccessor(repo, p)
+ except MultipleSuccessorsError as exc:
+ dependencies[r] = exc.successorssets
+ continue
+ if succ in revs:
+ dependencies[r].add(succ)
+ rdependencies[succ].add(r)
+ return dependencies, rdependencies
+
+def _singlesuccessor(repo, p):
+ """returns p (as rev) if not obsolete or its unique latest successors
+
+ fail if there are no such successor"""
+
+ if not p.obsolete():
+ return p.rev()
+ obs = repo[p]
+ ui = repo.ui
+ newer = obsolete.successorssets(repo, obs.node())
+ # search of a parent which is not killed
+ while not newer:
+ ui.debug("stabilize target %s is plain dead,"
+ " trying to stabilize on its parent\n" %
+ obs)
+ obs = obs.parents()[0]
+ newer = obsolete.successorssets(repo, obs.node())
+ if len(newer) > 1 or len(newer[0]) > 1:
+ raise MultipleSuccessorsError(newer)
+
+ return repo[newer[0][0]].rev()
+
+class MultipleSuccessorsError(RuntimeError):
+ """Exception raised by _singlesuccessor when multiple successor sets exists
+
+ The object contains the list of successorssets in its 'successorssets'
+ attribute to call to easily recover.
+ """
+
+ def __init__(self, successorssets):
+ self.successorssets = successorssets
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext3rd/topic/topicmap.py Tue Mar 22 00:19:14 2016 -0700
@@ -0,0 +1,200 @@
+from mercurial import branchmap
+from mercurial import encoding
+from mercurial import error
+from mercurial import scmutil
+from mercurial import util
+from mercurial.node import hex, bin, nullid
+
+def _filename(repo):
+ """name of a branchcache file for a given repo or repoview"""
+ filename = "cache/topicmap"
+ if repo.filtername:
+ filename = '%s-%s' % (filename, repo.filtername)
+ return filename
+
+oldbranchcache = branchmap.branchcache
+
+def _phaseshash(repo, maxrev):
+ revs = set()
+ cl = repo.changelog
+ fr = cl.filteredrevs
+ nm = cl.nodemap
+ for roots in repo._phasecache.phaseroots[1:]:
+ for n in roots:
+ r = nm.get(n)
+ if r not in fr and r < maxrev:
+ revs.add(r)
+ key = nullid
+ revs = sorted(revs)
+ if revs:
+ s = util.sha1()
+ for rev in revs:
+ s.update('%s;' % rev)
+ key = s.digest()
+ return key
+
+class topiccache(oldbranchcache):
+
+ def __init__(self, *args, **kwargs):
+ otherbranchcache = branchmap.branchcache
+ try:
+ # super() call may fail otherwise
+ branchmap.branchcache = oldbranchcache
+ super(topiccache, self).__init__(*args, **kwargs)
+ if self.filteredhash is None:
+ self.filteredhash = nullid
+ self.phaseshash = nullid
+ finally:
+ branchmap.branchcache = otherbranchcache
+
+ def copy(self):
+ """return an deep copy of the branchcache object"""
+ new = topiccache(self, self.tipnode, self.tiprev, self.filteredhash,
+ self._closednodes)
+ if self.filteredhash is None:
+ self.filteredhash = nullid
+ new.phaseshash = self.phaseshash
+ return new
+
+ def branchtip(self, branch, topic=''):
+ '''Return the tipmost open head on branch head, otherwise return the
+ tipmost closed head on branch.
+ Raise KeyError for unknown branch.'''
+ if topic:
+ branch = '%s:%s' % (branch, topic)
+ return super(topiccache, self).branchtip(branch)
+
+ def branchheads(self, branch, closed=False, topic=''):
+ if topic:
+ branch = '%s:%s' % (branch, topic)
+ return super(topiccache, self).branchheads(branch, closed=closed)
+
+ def validfor(self, repo):
+ """Is the cache content valid regarding a repo
+
+ - False when cached tipnode is unknown or if we detect a strip.
+ - True when cache is up to date or a subset of current repo."""
+ # This is copy paste of mercurial.branchmap.branchcache.validfor in
+ # 69077c65919d With a small changes to the cache key handling to
+ # include phase information that impact the topic cache.
+ #
+ # All code changes should be flagged on site.
+ try:
+ if (self.tipnode == repo.changelog.node(self.tiprev)):
+ fh = scmutil.filteredhash(repo, self.tiprev)
+ if fh is None:
+ fh = nullid
+ if ((self.filteredhash == fh)
+ and (self.phaseshash == _phaseshash(repo, self.tiprev))):
+ return True
+ return False
+ except IndexError:
+ return False
+
+ def write(self, repo):
+ # This is copy paste of mercurial.branchmap.branchcache.write in
+ # 69077c65919d With a small changes to the cache key handling to
+ # include phase information that impact the topic cache.
+ #
+ # All code changes should be flagged on site.
+ try:
+ f = repo.vfs(_filename(repo), "w", atomictemp=True)
+ cachekey = [hex(self.tipnode), str(self.tiprev)]
+ # [CHANGE] we need a hash in all cases
+ assert self.filteredhash is not None
+ cachekey.append(hex(self.filteredhash))
+ cachekey.append(hex(self.phaseshash))
+ f.write(" ".join(cachekey) + '\n')
+ nodecount = 0
+ for label, nodes in sorted(self.iteritems()):
+ for node in nodes:
+ nodecount += 1
+ if node in self._closednodes:
+ state = 'c'
+ else:
+ state = 'o'
+ f.write("%s %s %s\n" % (hex(node), state,
+ encoding.fromlocal(label)))
+ f.close()
+ repo.ui.log('branchcache',
+ 'wrote %s branch cache with %d labels and %d nodes\n',
+ repo.filtername, len(self), nodecount)
+ except (IOError, OSError, error.Abort) as inst:
+ repo.ui.debug("couldn't write branch cache: %s\n" % inst)
+ # Abort may be raise by read only opener
+ pass
+
+ def update(self, repo, revgen):
+ """Given a branchhead cache, self, that may have extra nodes or be
+ missing heads, and a generator of nodes that are strictly a superset of
+ heads missing, this function updates self to be correct.
+ """
+ oldgetbranchinfo = repo.revbranchcache().branchinfo
+ try:
+ def branchinfo(r):
+ info = oldgetbranchinfo(r)
+ topic = ''
+ ctx = repo[r]
+ if ctx.mutable():
+ topic = ctx.topic()
+ branch = info[0]
+ if topic:
+ branch = '%s:%s' % (branch, topic)
+ return (branch, info[1])
+ repo.revbranchcache().branchinfo = branchinfo
+ super(topiccache, self).update(repo, revgen)
+ if self.filteredhash is None:
+ self.filteredhash = nullid
+ self.phaseshash = _phaseshash(repo, self.tiprev)
+ finally:
+ repo.revbranchcache().branchinfo = oldgetbranchinfo
+
+def readtopicmap(repo):
+ # This is copy paste of mercurial.branchmap.read in 69077c65919d
+ # With a small changes to the cache key handling to include phase
+ # information that impact the topic cache.
+ #
+ # All code changes should be flagged on site.
+ try:
+ f = repo.vfs(_filename(repo))
+ lines = f.read().split('\n')
+ f.close()
+ except (IOError, OSError):
+ return None
+
+ try:
+ cachekey = lines.pop(0).split(" ", 2)
+ last, lrev = cachekey[:2]
+ last, lrev = bin(last), int(lrev)
+ filteredhash = bin(cachekey[2]) # [CHANGE] unconditional filteredhash
+ partial = branchcache(tipnode=last, tiprev=lrev,
+ filteredhash=filteredhash)
+ partial.phaseshash = bin(cachekey[3]) # [CHANGE] read phaseshash
+ if not partial.validfor(repo):
+ # invalidate the cache
+ raise ValueError('tip differs')
+ cl = repo.changelog
+ for l in lines:
+ if not l:
+ continue
+ node, state, label = l.split(" ", 2)
+ if state not in 'oc':
+ raise ValueError('invalid branch state')
+ label = encoding.tolocal(label.strip())
+ node = bin(node)
+ if not cl.hasnode(node):
+ raise ValueError('node %s does not exist' % hex(node))
+ partial.setdefault(label, []).append(node)
+ if state == 'c':
+ partial._closednodes.add(node)
+ except KeyboardInterrupt:
+ raise
+ except Exception as inst:
+ if repo.ui.debugflag:
+ msg = 'invalid branchheads cache'
+ if repo.filtername is not None:
+ msg += ' (%s)' % repo.filtername
+ msg += ': %s\n'
+ repo.ui.debug(msg % inst)
+ partial = None
+ return partial
--- a/setup.py Wed Mar 16 12:14:20 2016 -0700
+++ b/setup.py Tue Mar 22 00:19:14 2016 -0700
@@ -14,9 +14,9 @@
maintainer_email='augie@google.com',
url='http://bitbucket.org/durin42/hg-topics/',
description='Experimental tinkering with workflow ideas for topic branches.',
- long_description=open('README').read(),
+ long_description=open('README.md').read(),
keywords='hg mercurial',
license='GPLv2+',
- py_modules=['src'],
+ packages=['hgext3rd.topic'],
install_requires=requires,
)
--- a/src/topic/__init__.py Wed Mar 16 12:14:20 2016 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,343 +0,0 @@
-# __init__.py - topic extension
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-"""support for topic branches
-
-Topic branches are lightweight branches which
-disappear when changes are finalized.
-
-This is sort of similar to a bookmark, but it applies to a whole
-series instead of a single revision.
-"""
-import functools
-import contextlib
-
-from mercurial.i18n import _
-from mercurial import branchmap
-from mercurial import bundle2
-from mercurial import changegroup
-from mercurial import cmdutil
-from mercurial import commands
-from mercurial import context
-from mercurial import discovery as discoverymod
-from mercurial import error
-from mercurial import exchange
-from mercurial import extensions
-from mercurial import localrepo
-from mercurial import lock
-from mercurial import merge
-from mercurial import namespaces
-from mercurial import node
-from mercurial import obsolete
-from mercurial import patch
-from mercurial import phases
-from mercurial import util
-from mercurial import wireproto
-
-from . import constants
-from . import revset as topicrevset
-from . import destination
-from . import stack
-from . import topicmap
-from . import discovery
-
-cmdtable = {}
-command = cmdutil.command(cmdtable)
-
-testedwith = '3.7'
-
-def _contexttopic(self):
- return self.extra().get(constants.extrakey, '')
-context.basectx.topic = _contexttopic
-
-def _namemap(repo, name):
- return [ctx.node() for ctx in
- repo.set('not public() and extra(topic, %s)', name)]
-
-def _nodemap(repo, node):
- ctx = repo[node]
- t = ctx.topic()
- if t and ctx.phase() > phases.public:
- return [t]
- return []
-
-def uisetup(ui):
- destination.setupdest()
-
-@contextlib.contextmanager
-def usetopicmap(repo):
- """use awful monkey patching to update the topic cache"""
- oldbranchcache = branchmap.branchcache
- oldfilename = branchmap._filename
- oldread = branchmap.read
- oldcaches = getattr(repo, '_branchcaches', {})
- try:
- branchmap.branchcache = topicmap.topiccache
- branchmap._filename = topicmap._filename
- branchmap.read = topicmap.readtopicmap
- repo._branchcaches = getattr(repo, '_topiccaches', {})
- yield
- repo._topiccaches = repo._branchcaches
- finally:
- repo._branchcaches = oldcaches
- branchmap.branchcache = oldbranchcache
- branchmap._filename = oldfilename
- branchmap.read = oldread
-
-def cgapply(orig, repo, *args, **kwargs):
- with usetopicmap(repo):
- return orig(repo, *args, **kwargs)
-
-def reposetup(ui, repo):
- orig = repo.__class__
- if not isinstance(repo, localrepo.localrepository):
- return # this can be a peer in the ssh case (puzzling)
- class topicrepo(repo.__class__):
- def commit(self, *args, **kwargs):
- backup = self.ui.backupconfig('ui', 'allowemptycommit')
- try:
- if repo.currenttopic != repo['.'].topic():
- # bypass the core "nothing changed" logic
- self.ui.setconfig('ui', 'allowemptycommit', True)
- return orig.commit(self, *args, **kwargs)
- finally:
- self.ui.restoreconfig(backup)
-
- def commitctx(self, ctx, error=None):
- if isinstance(ctx, context.workingcommitctx):
- current = self.currenttopic
- if current:
- ctx.extra()[constants.extrakey] = current
- if (isinstance(ctx, context.memctx) and
- ctx.extra().get('amend_source') and
- ctx.topic() and
- not self.currenttopic):
- # we are amending and need to remove a topic
- del ctx.extra()[constants.extrakey]
- with usetopicmap(self):
- return orig.commitctx(self, ctx, error=error)
-
- @property
- def topics(self):
- topics = set(['', self.currenttopic])
- for c in self.set('not public()'):
- topics.add(c.topic())
- topics.remove('')
- return topics
-
- @property
- def currenttopic(self):
- return self.vfs.tryread('topic')
-
- def branchmap(self, topic=True):
- if not topic:
- super(topicrepo, self).branchmap()
- with usetopicmap(self):
- branchmap.updatecache(self)
- return self._topiccaches[self.filtername]
-
- def destroyed(self, *args, **kwargs):
- with usetopicmap(self):
- return super(topicrepo, self).destroyed(*args, **kwargs)
-
- def invalidatecaches(self):
- super(topicrepo, self).invalidatecaches()
- if '_topiccaches' in vars(self.unfiltered()):
- self.unfiltered()._topiccaches.clear()
-
- def peer(self):
- peer = super(topicrepo, self).peer()
- if getattr(peer, '_repo', None) is not None: # localpeer
- class topicpeer(peer.__class__):
- def branchmap(self):
- usetopic = not self._repo.publishing()
- return self._repo.branchmap(topic=usetopic)
- peer.__class__ = topicpeer
- return peer
-
-
- repo.__class__ = topicrepo
- if util.safehasattr(repo, 'names'):
- repo.names.addnamespace(namespaces.namespace(
- 'topics', 'topic', namemap=_namemap, nodemap=_nodemap,
- listnames=lambda repo: repo.topics))
-
-@command('topics [TOPIC]', [
- ('', 'clear', False, 'clear active topic if any'),
- ('', 'change', '', 'revset of existing revisions to change topic'),
- ('l', 'list', False, 'show the stack of changeset in the topic'),
-])
-def topics(ui, repo, topic='', clear=False, change=None, list=False):
- """View current topic, set current topic, or see all topics."""
- if list:
- if clear or change:
- raise error.Abort(_("cannot use --clear or --change with --list"))
- return stack.showstack(ui, repo, topic)
-
- if change:
- if not obsolete.isenabled(repo, obsolete.createmarkersopt):
- raise error.Abort(_('must have obsolete enabled to use --change'))
- if not topic and not clear:
- raise error.Abort('changing topic requires a topic name or --clear')
- if any(not c.mutable() for c in repo.set('%r and public()', change)):
- raise error.Abort("can't change topic of a public change")
- rewrote = 0
- needevolve = False
- l = repo.lock()
- txn = repo.transaction('rewrite-topics')
- try:
- for c in repo.set('%r', change):
- def filectxfn(repo, ctx, path):
- try:
- return c[path]
- except error.ManifestLookupError:
- return None
- fixedextra = dict(c.extra())
- ui.debug('old node id is %s\n' % node.hex(c.node()))
- ui.debug('origextra: %r\n' % fixedextra)
- newtopic = None if clear else topic
- oldtopic = fixedextra.get(constants.extrakey, None)
- if oldtopic == newtopic:
- continue
- if clear:
- del fixedextra[constants.extrakey]
- else:
- fixedextra[constants.extrakey] = topic
- if 'amend_source' in fixedextra:
- # TODO: right now the commitctx wrapper in
- # topicrepo overwrites the topic in extra if
- # amend_source is set to support 'hg commit
- # --amend'. Support for amend should be adjusted
- # to not be so invasive.
- del fixedextra['amend_source']
- ui.debug('changing topic of %s from %s to %s\n' % (
- c, oldtopic, newtopic))
- ui.debug('fixedextra: %r\n' % fixedextra)
- mc = context.memctx(
- repo, (c.p1().node(), c.p2().node()), c.description(),
- c.files(), filectxfn,
- user=c.user(), date=c.date(), extra=fixedextra)
- newnode = repo.commitctx(mc)
- ui.debug('new node id is %s\n' % node.hex(newnode))
- needevolve = needevolve or (len(c.children()) > 0)
- obsolete.createmarkers(repo, [(c, (repo[newnode],))])
- rewrote += 1
- txn.close()
- except:
- try:
- txn.abort()
- finally:
- repo.invalidate()
- raise
- finally:
- lock.release(txn, l)
- ui.status('changed topic on %d changes\n' % rewrote)
- if needevolve:
- evolvetarget = 'topic(%s)' % topic if topic else 'not topic()'
- ui.status('please run hg evolve --rev "%s" now\n' % evolvetarget)
- if clear:
- if repo.vfs.exists('topic'):
- repo.vfs.unlink('topic')
- return
- if topic:
- with repo.vfs.open('topic', 'w') as f:
- f.write(topic)
- return
- current = repo.currenttopic
- for t in sorted(repo.topics):
- marker = '*' if t == current else ' '
- ui.write(' %s %s\n' % (marker, t))
-
-def summaryhook(ui, repo):
- t = repo.currenttopic
- if not t:
- return
- # i18n: column positioning for "hg summary"
- ui.write(_("topic: %s\n") % t)
-
-def commitwrap(orig, ui, repo, *args, **opts):
- if opts.get('topic'):
- t = opts['topic']
- with repo.vfs.open('topic', 'w') as f:
- f.write(t)
- return orig(ui, repo, *args, **opts)
-
-def committextwrap(orig, repo, ctx, subs, extramsg):
- ret = orig(repo, ctx, subs, extramsg)
- t = repo.currenttopic
- if t:
- ret = ret.replace("\nHG: branch",
- "\nHG: topic '%s'\nHG: branch" % t)
- return ret
-
-def mergeupdatewrap(orig, repo, node, branchmerge, force, *args, **kwargs):
- partial = bool(len(args)) or 'matcher' in kwargs
- wlock = repo.wlock()
- try:
- ret = orig(repo, node, branchmerge, force, *args, **kwargs)
- if not partial and not branchmerge:
- ot = repo.currenttopic
- t = ''
- pctx = repo[node]
- if pctx.phase() > phases.public:
- t = pctx.topic()
- with repo.vfs.open('topic', 'w') as f:
- f.write(t)
- if t and t != ot:
- repo.ui.status(_("switching to topic %s\n") % t)
- return ret
- finally:
- wlock.release()
-
-def _fixrebase(loaded):
- if not loaded:
- return
-
- def savetopic(ctx, extra):
- if ctx.topic():
- extra[constants.extrakey] = ctx.topic()
-
- def newmakeextrafn(orig, copiers):
- return orig(copiers + [savetopic])
-
- rebase = extensions.find("rebase")
- extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn)
-
-def _exporttopic(seq, ctx):
- topic = ctx.topic()
- if topic:
- return 'EXP-Topic %s' % topic
- return None
-
-def _importtopic(repo, patchdata, extra, opts):
- if 'topic' in patchdata:
- extra['topic'] = patchdata['topic']
-
-extensions.afterloaded('rebase', _fixrebase)
-
-entry = extensions.wrapcommand(commands.table, 'commit', commitwrap)
-entry[1].append(('t', 'topic', '',
- _("use specified topic"), _('TOPIC')))
-
-extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap)
-extensions.wrapfunction(merge, 'update', mergeupdatewrap)
-extensions.wrapfunction(discoverymod, '_headssummary', discovery._headssummary)
-extensions.wrapfunction(wireproto, 'branchmap', discovery.wireprotobranchmap)
-extensions.wrapfunction(bundle2, 'handlecheckheads', discovery.handlecheckheads)
-bundle2.handlecheckheads.params = frozenset() # we need a proper wrape b2 part stuff
-bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads
-extensions.wrapfunction(exchange, '_pushb2phases', discovery._pushb2phases)
-extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply)
-exchange.b2partsgenmapping['phase'] = exchange._pushb2phases
-topicrevset.modsetup()
-cmdutil.summaryhooks.add('topic', summaryhook)
-
-if util.safehasattr(cmdutil, 'extraexport'):
- cmdutil.extraexport.append('topic')
- cmdutil.extraexportmap['topic'] = _exporttopic
-if util.safehasattr(cmdutil, 'extrapreimport'):
- cmdutil.extrapreimport.append('topic')
- cmdutil.extrapreimportmap['topic'] = _importtopic
-if util.safehasattr(patch, 'patchheadermap'):
- patch.patchheadermap.append(('EXP-Topic', 'topic'))
--- a/src/topic/constants.py Wed Mar 16 12:14:20 2016 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-extrakey = 'topic'
--- a/src/topic/destination.py Wed Mar 16 12:14:20 2016 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,87 +0,0 @@
-from mercurial import error
-from mercurial import util
-from mercurial import destutil
-from mercurial import extensions
-from mercurial import bookmarks
-from mercurial.i18n import _
-
-def _destmergebranch(orig, repo, action='merge', sourceset=None, onheadcheck=True):
- p1 = repo['.']
- top = p1.topic()
- if top:
- heads = repo.revs('heads(topic(.)::topic(.))')
- if p1.rev() not in heads:
- raise error.Abort(_("not at topic head, update or explicit"))
- elif 1 == len(heads):
- # should look at all branch involved but... later
- bhead = ngtip(repo, p1.branch(), all=True)
- if not bhead:
- raise error.Abort(_("nothing to merge"))
- elif 1 == len(bhead):
- return bhead.first()
- else:
- raise error.Abort(_("branch '%s' has %d heads - "
- "please merge with an explicit rev")
- % (p1.branch(), len(bhead)),
- hint=_("run 'hg heads .' to see heads"))
- elif 2 == len(heads):
- heads = [r for r in heads if r != p1.rev()]
- # XXX: bla bla bla bla bla
- if 1 < len(heads):
- raise error.Abort(_('working directory not at a head revision'),
- hint=_("use 'hg update' or merge with an "
- "explicit revision"))
- return heads[0]
- elif 2 < len(heads):
- raise error.Abort(_("topic '%s' has %d heads - "
- "please merge with an explicit rev")
- % (top, len(heads)))
- else:
- assert False # that's impossible
- if getattr(orig, 'func_default', ()): # version above hg-3.7
- return orig(repo, action, sourceset, onheadcheck)
- else:
- return orig(repo)
-
-def _destupdatetopic(repo, clean, check):
- """decide on an update destination from current topic"""
- movemark = node = None
- topic = repo.currenttopic
- revs = repo.revs('.::topic("%s")' % topic)
- if not revs:
- return None, None, None
- node = revs.last()
- if bookmarks.isactivewdirparent(repo):
- movemark = repo['.'].node()
- return node, movemark, None
-
-def setupdest():
- if util.safehasattr(destutil, '_destmergebranch'):
- extensions.wrapfunction(destutil, '_destmergebranch', _destmergebranch)
- rebase = extensions.find('rebase')
- if (util.safehasattr(rebase, '_destrebase')
- # logic not shared with merge yet < hg-3.8
- and not util.safehasattr(rebase, '_definesets')):
- extensions.wrapfunction(rebase, '_destrebase', _destmergebranch)
- if util.safehasattr(destutil, 'destupdatesteps'):
- bridx = destutil.destupdatesteps.index('branch')
- destutil.destupdatesteps.insert(bridx, 'topic')
- destutil.destupdatestepmap['topic'] = _destupdatetopic
-
-def ngtip(repo, branch, all=False):
- """tip new generation"""
- ## search for untopiced heads of branch
- # could be heads((::branch(x) - topic()))
- # but that is expensive
- #
- # we should write plain code instead
- subquery = '''heads(
- parents(
- ancestor(
- (head() and branch(%s)
- or (topic() and branch(%s)))))
- ::(head() and branch(%s))
- - topic())'''
- if not all:
- subquery = 'max(%s)' % subquery
- return repo.revs(subquery, branch, branch, branch)
--- a/src/topic/discovery.py Wed Mar 16 12:14:20 2016 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,106 +0,0 @@
-import weakref
-from mercurial import branchmap
-from mercurial import error
-from mercurial import exchange
-from mercurial.i18n import _
-from . import topicmap
-
-def _headssummary(orig, repo, remote, outgoing):
- publishing = ('phases' not in remote.listkeys('namespaces')
- or bool(remote.listkeys('phases').get('publishing', False)))
- if publishing:
- return orig(repo, remote, outgoing)
- oldgetitem = repo.__getitem__
- oldrepo = repo.__class__
- oldbranchcache = branchmap.branchcache
- oldfilename = branchmap._filename
- try:
- class repocls(repo.__class__):
- def __getitem__(self, key):
- ctx = super(repocls, self).__getitem__(key)
- oldbranch = ctx.branch
- def branch():
- branch = oldbranch()
- topic = ctx.topic()
- if topic:
- branch = "%s:%s" % (branch, topic)
- return branch
- ctx.branch = branch
- return ctx
- repo.__class__ = repocls
- branchmap.branchcache = topicmap.topiccache
- branchmap._filename = topicmap._filename
- summary = orig(repo, remote, outgoing)
- for key, value in summary.iteritems():
- if ':' in key: # This is a topic
- if value[0] is None and value[1]:
- summary[key] = ([value[1].pop(0)], ) + value[1:]
- return summary
- finally:
- repo.__class__ = oldrepo
- branchmap.branchcache = oldbranchcache
- branchmap._filename = oldfilename
-
-def wireprotobranchmap(orig, repo, proto):
- oldrepo = repo.__class__
- try:
- class repocls(repo.__class__):
- def branchmap(self):
- usetopic = not self.publishing()
- return super(repocls, self).branchmap(topic=usetopic)
- repo.__class__ = repocls
- return orig(repo, proto)
- finally:
- repo.__class__ = oldrepo
-
-
-# Discovery have deficiency around phases, branch can get new heads with pure
-# phases change. This happened with a changeset was allowed to be pushed
-# because it had a topic, but it later become public and create a new branch
-# head.
-#
-# Handle this by doing an extra check for new head creation server side
-def _nbheads(repo):
- data = {}
- for b in repo.branchmap().iterbranches():
- if ':' in b[0]:
- continue
- data[b[0]] = len(b[1])
- return data
-
-def handlecheckheads(orig, op, inpart):
- orig(op, inpart)
- if op.repo.publishing():
- return
- tr = op.gettransaction()
- if tr.hookargs['source'] not in ('push', 'serve'): # not a push
- return
- tr._prepushheads = _nbheads(op.repo)
- reporef = weakref.ref(op.repo)
- oldvalidator = tr.validator
- def validator(tr):
- repo = reporef()
- if repo is not None:
- repo.invalidatecaches()
- finalheads = _nbheads(repo)
- for branch, oldnb in tr._prepushheads.iteritems():
- newnb = finalheads.pop(branch, 0)
- if oldnb < newnb:
- msg = _('push create a new head on branch "%s"' % branch)
- raise error.Abort(msg)
- for branch, newnb in finalheads.iteritems():
- if 1 < newnb:
- msg = _('push create more than 1 head on new branch "%s"' % branch)
- raise error.Abort(msg)
- return oldvalidator(tr)
- tr.validator = validator
-handlecheckheads.params = frozenset()
-
-def _pushb2phases(orig, pushop, bundler):
- hascheck = any(p.type == 'check:heads' for p in bundler._parts)
- if pushop.outdatedphases and not hascheck:
- exchange._pushb2ctxcheckheads(pushop, bundler)
- return orig(pushop, bundler)
-
-
-
--- a/src/topic/revset.py Wed Mar 16 12:14:20 2016 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-from mercurial import revset
-from mercurial import util
-
-from . import constants, destination
-
-try:
- mkmatcher = revset._stringmatcher
-except AttributeError:
- mkmatcher = util.stringmatcher
-
-
-def topicset(repo, subset, x):
- """`topic([topic])`
- Specified topic or all changes with any topic specified.
-
- If `topic` starts with `re:` the remainder of the name is treated
- as a regular expression.
-
- TODO: make `topic(revset)` work the same as `branch(revset)`.
- """
- args = revset.getargs(x, 0, 1, 'topic takes one or no arguments')
- if args:
- # match a specific topic
- topic = revset.getstring(args[0], 'topic() argument must be a string')
- if topic == '.':
- topic = repo['.'].extra().get('topic', '')
- _kind, _pattern, matcher = mkmatcher(topic)
- else:
- matcher = lambda t: bool(t)
- drafts = subset.filter(lambda r: repo[r].mutable())
- return drafts.filter(
- lambda r: matcher(repo[r].extra().get(constants.extrakey, '')))
-
-def ngtipset(repo, subset, x):
- """`ngtip([branch])`
-
- The untopiced tip.
-
- Name is horrible so that people change it.
- """
- args = revset.getargs(x, 1, 1, 'topic takes one')
- # match a specific topic
- branch = revset.getstring(args[0], 'ngtip() argument must be a string')
- if branch == '.':
- branch = repo['.'].branch()
- return subset & destination.ngtip(repo, branch)
-
-def modsetup():
- revset.symbols.update({'topic': topicset})
- revset.symbols.update({'ngtip': ngtipset})
--- a/src/topic/stack.py Wed Mar 16 12:14:20 2016 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
-# stack.py - code related to stack workflow
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-import collections
-from mercurial.i18n import _
-from mercurial import error
-from mercurial import extensions
-from mercurial import obsolete
-
-def _getstack(repo, topic):
- # XXX need sorting
- trevs = repo.revs("topic(%s) - obsolete()", topic)
- return _orderrevs(repo, trevs)
-
-def showstack(ui, repo, topic):
- if not topic:
- topic = repo.currenttopic
- if not topic:
- raise error.Abort(_('no active topic to list'))
- for idx, r in enumerate(_getstack(repo, topic)):
- # super crude initial version
- l = "%d: %s\n" % (idx, repo[r].description().splitlines()[0])
- ui.write(l)
-
-# Copied from evolve 081605c2e9b6
-
-def _orderrevs(repo, revs):
- """Compute an ordering to solve instability for the given revs
-
- revs is a list of unstable revisions.
-
- Returns the same revisions ordered to solve their instability from the
- bottom to the top of the stack that the stabilization process will produce
- eventually.
-
- This ensures the minimal number of stabilizations, as we can stabilize each
- revision on its final stabilized destination.
- """
- # Step 1: Build the dependency graph
- dependencies, rdependencies = builddependencies(repo, revs)
- # Step 2: Build the ordering
- # Remove the revisions with no dependency(A) and add them to the ordering.
- # Removing these revisions leads to new revisions with no dependency (the
- # one depending on A) that we can remove from the dependency graph and add
- # to the ordering. We progress in a similar fashion until the ordering is
- # built
- solvablerevs = collections.deque([r for r in sorted(dependencies.keys())
- if not dependencies[r]])
- ordering = []
- while solvablerevs:
- rev = solvablerevs.popleft()
- for dependent in rdependencies[rev]:
- dependencies[dependent].remove(rev)
- if not dependencies[dependent]:
- solvablerevs.append(dependent)
- del dependencies[rev]
- ordering.append(rev)
-
- ordering.extend(sorted(dependencies))
- return ordering
-
-def builddependencies(repo, revs):
- """returns dependency graphs giving an order to solve instability of revs
- (see _orderrevs for more information on usage)"""
-
- # For each troubled revision we keep track of what instability if any should
- # be resolved in order to resolve it. Example:
- # dependencies = {3: [6], 6:[]}
- # Means that: 6 has no dependency, 3 depends on 6 to be solved
- dependencies = {}
- # rdependencies is the inverted dict of dependencies
- rdependencies = collections.defaultdict(set)
-
- for r in revs:
- dependencies[r] = set()
- for p in repo[r].parents():
- try:
- succ = _singlesuccessor(repo, p)
- except MultipleSuccessorsError as exc:
- dependencies[r] = exc.successorssets
- continue
- if succ in revs:
- dependencies[r].add(succ)
- rdependencies[succ].add(r)
- return dependencies, rdependencies
-
-def _singlesuccessor(repo, p):
- """returns p (as rev) if not obsolete or its unique latest successors
-
- fail if there are no such successor"""
-
- if not p.obsolete():
- return p.rev()
- obs = repo[p]
- ui = repo.ui
- newer = obsolete.successorssets(repo, obs.node())
- # search of a parent which is not killed
- while not newer:
- ui.debug("stabilize target %s is plain dead,"
- " trying to stabilize on its parent\n" %
- obs)
- obs = obs.parents()[0]
- newer = obsolete.successorssets(repo, obs.node())
- if len(newer) > 1 or len(newer[0]) > 1:
- raise MultipleSuccessorsError(newer)
-
- return repo[newer[0][0]].rev()
-
-class MultipleSuccessorsError(RuntimeError):
- """Exception raised by _singlesuccessor when multiple successor sets exists
-
- The object contains the list of successorssets in its 'successorssets'
- attribute to call to easily recover.
- """
-
- def __init__(self, successorssets):
- self.successorssets = successorssets
--- a/src/topic/topicmap.py Wed Mar 16 12:14:20 2016 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,200 +0,0 @@
-from mercurial import branchmap
-from mercurial import encoding
-from mercurial import error
-from mercurial import scmutil
-from mercurial import util
-from mercurial.node import hex, bin, nullid
-
-def _filename(repo):
- """name of a branchcache file for a given repo or repoview"""
- filename = "cache/topicmap"
- if repo.filtername:
- filename = '%s-%s' % (filename, repo.filtername)
- return filename
-
-oldbranchcache = branchmap.branchcache
-
-def _phaseshash(repo, maxrev):
- revs = set()
- cl = repo.changelog
- fr = cl.filteredrevs
- nm = cl.nodemap
- for roots in repo._phasecache.phaseroots[1:]:
- for n in roots:
- r = nm.get(n)
- if r not in fr and r < maxrev:
- revs.add(r)
- key = nullid
- revs = sorted(revs)
- if revs:
- s = util.sha1()
- for rev in revs:
- s.update('%s;' % rev)
- key = s.digest()
- return key
-
-class topiccache(oldbranchcache):
-
- def __init__(self, *args, **kwargs):
- otherbranchcache = branchmap.branchcache
- try:
- # super() call may fail otherwise
- branchmap.branchcache = oldbranchcache
- super(topiccache, self).__init__(*args, **kwargs)
- if self.filteredhash is None:
- self.filteredhash = nullid
- self.phaseshash = nullid
- finally:
- branchmap.branchcache = otherbranchcache
-
- def copy(self):
- """return an deep copy of the branchcache object"""
- new = topiccache(self, self.tipnode, self.tiprev, self.filteredhash,
- self._closednodes)
- if self.filteredhash is None:
- self.filteredhash = nullid
- new.phaseshash = self.phaseshash
- return new
-
- def branchtip(self, branch, topic=''):
- '''Return the tipmost open head on branch head, otherwise return the
- tipmost closed head on branch.
- Raise KeyError for unknown branch.'''
- if topic:
- branch = '%s:%s' % (branch, topic)
- return super(topiccache, self).branchtip(branch)
-
- def branchheads(self, branch, closed=False, topic=''):
- if topic:
- branch = '%s:%s' % (branch, topic)
- return super(topiccache, self).branchheads(branch, closed=closed)
-
- def validfor(self, repo):
- """Is the cache content valid regarding a repo
-
- - False when cached tipnode is unknown or if we detect a strip.
- - True when cache is up to date or a subset of current repo."""
- # This is copy paste of mercurial.branchmap.branchcache.validfor in
- # 69077c65919d With a small changes to the cache key handling to
- # include phase information that impact the topic cache.
- #
- # All code changes should be flagged on site.
- try:
- if (self.tipnode == repo.changelog.node(self.tiprev)):
- fh = scmutil.filteredhash(repo, self.tiprev)
- if fh is None:
- fh = nullid
- if ((self.filteredhash == fh)
- and (self.phaseshash == _phaseshash(repo, self.tiprev))):
- return True
- return False
- except IndexError:
- return False
-
- def write(self, repo):
- # This is copy paste of mercurial.branchmap.branchcache.write in
- # 69077c65919d With a small changes to the cache key handling to
- # include phase information that impact the topic cache.
- #
- # All code changes should be flagged on site.
- try:
- f = repo.vfs(_filename(repo), "w", atomictemp=True)
- cachekey = [hex(self.tipnode), str(self.tiprev)]
- # [CHANGE] we need a hash in all cases
- assert self.filteredhash is not None
- cachekey.append(hex(self.filteredhash))
- cachekey.append(hex(self.phaseshash))
- f.write(" ".join(cachekey) + '\n')
- nodecount = 0
- for label, nodes in sorted(self.iteritems()):
- for node in nodes:
- nodecount += 1
- if node in self._closednodes:
- state = 'c'
- else:
- state = 'o'
- f.write("%s %s %s\n" % (hex(node), state,
- encoding.fromlocal(label)))
- f.close()
- repo.ui.log('branchcache',
- 'wrote %s branch cache with %d labels and %d nodes\n',
- repo.filtername, len(self), nodecount)
- except (IOError, OSError, error.Abort) as inst:
- repo.ui.debug("couldn't write branch cache: %s\n" % inst)
- # Abort may be raise by read only opener
- pass
-
- def update(self, repo, revgen):
- """Given a branchhead cache, self, that may have extra nodes or be
- missing heads, and a generator of nodes that are strictly a superset of
- heads missing, this function updates self to be correct.
- """
- oldgetbranchinfo = repo.revbranchcache().branchinfo
- try:
- def branchinfo(r):
- info = oldgetbranchinfo(r)
- topic = ''
- ctx = repo[r]
- if ctx.mutable():
- topic = ctx.topic()
- branch = info[0]
- if topic:
- branch = '%s:%s' % (branch, topic)
- return (branch, info[1])
- repo.revbranchcache().branchinfo = branchinfo
- super(topiccache, self).update(repo, revgen)
- if self.filteredhash is None:
- self.filteredhash = nullid
- self.phaseshash = _phaseshash(repo, self.tiprev)
- finally:
- repo.revbranchcache().branchinfo = oldgetbranchinfo
-
-def readtopicmap(repo):
- # This is copy paste of mercurial.branchmap.read in 69077c65919d
- # With a small changes to the cache key handling to include phase
- # information that impact the topic cache.
- #
- # All code changes should be flagged on site.
- try:
- f = repo.vfs(_filename(repo))
- lines = f.read().split('\n')
- f.close()
- except (IOError, OSError):
- return None
-
- try:
- cachekey = lines.pop(0).split(" ", 2)
- last, lrev = cachekey[:2]
- last, lrev = bin(last), int(lrev)
- filteredhash = bin(cachekey[2]) # [CHANGE] unconditional filteredhash
- partial = branchcache(tipnode=last, tiprev=lrev,
- filteredhash=filteredhash)
- partial.phaseshash = bin(cachekey[3]) # [CHANGE] read phaseshash
- if not partial.validfor(repo):
- # invalidate the cache
- raise ValueError('tip differs')
- cl = repo.changelog
- for l in lines:
- if not l:
- continue
- node, state, label = l.split(" ", 2)
- if state not in 'oc':
- raise ValueError('invalid branch state')
- label = encoding.tolocal(label.strip())
- node = bin(node)
- if not cl.hasnode(node):
- raise ValueError('node %s does not exist' % hex(node))
- partial.setdefault(label, []).append(node)
- if state == 'c':
- partial._closednodes.add(node)
- except KeyboardInterrupt:
- raise
- except Exception as inst:
- if repo.ui.debugflag:
- msg = 'invalid branchheads cache'
- if repo.filtername is not None:
- msg += ' (%s)' % repo.filtername
- msg += ': %s\n'
- repo.ui.debug(msg % inst)
- partial = None
- return partial
--- a/tests/test-topic-dest.t Wed Mar 16 12:14:20 2016 -0700
+++ b/tests/test-topic-dest.t Tue Mar 22 00:19:14 2016 -0700
@@ -5,6 +5,7 @@
$ cat <<EOF >> .hg/hgrc
> [extensions]
> rebase=
+ > histedit=
> [phases]
> publish=false
> EOF
@@ -479,3 +480,17 @@
|
o 0 () c_alpha
+
+Default destination for histedit
+================================
+
+By default hisetdit should edit with the current topic only
+(even when based on other draft
+
+ $ hg phase 'desc(c_zeta)'
+ 11: draft
+ $ HGEDITOR=cat hg histedit | grep pick
+ pick e44744d9ad73 12 babar
+ pick 38eea8439aee 14 arthur
+ pick 411315c48bdc 15 pompadour
+ # p, pick = use commit
--- a/tests/test-topic-push.t Wed Mar 16 12:14:20 2016 -0700
+++ b/tests/test-topic-push.t Tue Mar 22 00:19:14 2016 -0700
@@ -200,8 +200,34 @@
o 0 default public CA
+Pushing a new topic to a non publishing server without topic -> new head
+
+ $ cat << EOF >> ../draft/.hg/hgrc
+ > [extensions]
+ > topic=!
+ > EOF
+ $ hg push ssh://user@dummy/draft
+ pushing to ssh://user@dummy/draft
+ searching for changes
+ abort: push creates new remote head 84eaf32db6c3!
+ (merge or see "hg help push" for details about pushing new heads)
+ [255]
+ $ hg log -G
+ @ 6 default celeste draft CE
+ |
+ | o 5 default babar draft CD
+ |/
+ | o 4 mountain public CC
+ |/
+ | o 1 default public CB
+ |/
+ o 0 default public CA
+
+
Pushing a new topic to a non publishing server should not be seen as a new head
+ $ printf "topic=" >> ../draft/.hg/hgrc
+ $ hg config extensions.topic >> ../draft/.hg/hgrc
$ hg push ssh://user@dummy/draft
pushing to ssh://user@dummy/draft
searching for changes
--- a/tests/test-topic-stack.t Wed Mar 16 12:14:20 2016 -0700
+++ b/tests/test-topic-stack.t Tue Mar 22 00:19:14 2016 -0700
@@ -57,10 +57,11 @@
$ hg topic
* foo
$ hg topic --list
- 0: c_c
- 1: c_d
- 2: c_e
- 3: c_f
+ _ c_b
+ t0: c_c
+ t1: c_d
+ t2: c_e
+ t3@ c_f (current)
error case, nothing to list
@@ -69,12 +70,23 @@
abort: no active topic to list
[255]
+Test "t#" reference
+-------------------
+
+
+ $ hg up t1
+ abort: cannot resolve "t1": no active topic
+ [255]
+ $ hg topic foo
+ $ hg up t42
+ abort: cannot resolve "t42": topic "foo" has only 4 changesets
+ [255]
+ $ hg up t1
+ 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+
Case with some of the topic unstable
------------------------------------
- $ hg up 'desc(c_d)'
- switching to topic foo
- 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
$ echo bbb > ddd
$ hg commit --amend
$ hg log -G
@@ -93,7 +105,123 @@
o 0 default {} draft c_a
$ hg topic --list
- 0: c_c
- 1: c_d
- 2: c_e
- 3: c_f
+ _ c_b
+ t0: c_c
+ t1@ c_d (current)
+ t2$ c_e (unstable)
+ t3$ c_f (unstable)
+
+Also test the revset:
+
+ $ hg log -r 'stack()'
+ 2 default {foo} draft c_c
+ 7 default {foo} draft c_d
+ 4 default {foo} draft c_e
+ 5 default {foo} draft c_f
+
+Case with multiple heads on the topic
+-------------------------------------
+
+Make things linear again
+
+ $ hg rebase -s 'desc(c_e)' -d 'desc(c_d) - obsolete()'
+ rebasing 4:91fa8808d101 "c_e"
+ rebasing 5:4ec5094907b7 "c_f"
+ $ hg log -G
+ o 9 default {foo} draft c_f
+ |
+ o 8 default {foo} draft c_e
+ |
+ @ 7 default {foo} draft c_d
+ |
+ o 2 default {foo} draft c_c
+ |
+ o 1 default {} draft c_b
+ |
+ o 0 default {} draft c_a
+
+
+
+Create the second branch
+
+ $ hg up 'desc(c_d)'
+ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ echo aaa > ggg
+ $ hg add ggg
+ $ hg commit -m c_g
+ created new head
+ $ echo aaa > hhh
+ $ hg add hhh
+ $ hg commit -m c_h
+ created new head
+ $ hg log -G
+ @ 11 default {foo} draft c_h
+ |
+ o 10 default {foo} draft c_g
+ |
+ | o 9 default {foo} draft c_f
+ | |
+ | o 8 default {foo} draft c_e
+ |/
+ o 7 default {foo} draft c_d
+ |
+ o 2 default {foo} draft c_c
+ |
+ o 1 default {} draft c_b
+ |
+ o 0 default {} draft c_a
+
+
+Test output
+
+ $ hg top -l
+ _ c_b
+ t0: c_c
+ t1: c_d
+ t2: c_g
+ t3@ c_h (current)
+ _ c_d
+ t4: c_e
+ t5: c_f
+
+Case with multiple heads on the topic with unstability involved
+---------------------------------------------------------------
+
+We amend the message to make sure the display base pick the right changeset
+
+ $ hg up 'desc(c_d)'
+ 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
+ $ echo ccc > ddd
+ $ hg commit --amend -m 'c_D'
+ $ hg rebase -d . -s 'desc(c_g)'
+ rebasing 10:11286b4fcb3d "c_g"
+ rebasing 11:3ad57527186d "c_h"
+ $ hg log -G
+ o 15 default {foo} draft c_h
+ |
+ o 14 default {foo} draft c_g
+ |
+ @ 13 default {foo} draft c_D
+ |
+ | o 9 default {foo} draft c_f
+ | |
+ | o 8 default {foo} draft c_e
+ | |
+ | x 7 default {foo} draft c_d
+ |/
+ o 2 default {foo} draft c_c
+ |
+ o 1 default {} draft c_b
+ |
+ o 0 default {} draft c_a
+
+
+ $ hg topic --list
+ _ c_b
+ t0: c_c
+ t1@ c_D (current)
+ t2: c_g
+ t3: c_h
+ _ c_D
+ t4$ c_e (unstable)
+ t5$ c_f (unstable)
--- a/tests/testlib Wed Mar 16 12:14:20 2016 -0700
+++ b/tests/testlib Tue Mar 22 00:19:14 2016 -0700
@@ -12,4 +12,4 @@
[extensions]
rebase=
EOF
-echo "topic=$(echo $(dirname $TESTDIR))/src/topic" >> $HGRCPATH
+echo "topic=$(echo $(dirname $TESTDIR))/hgext3rd/topic" >> $HGRCPATH