# HG changeset patch # User Pierre-Yves David # Date 1458631154 25200 # Node ID 692a1aa1350c819354dc89606583dbe2e5541f0e # Parent 442a7cb8404e4aab09f167381dfecc6e99383b07# Parent 27ea12c05e9963b657c93fcadf85c05180ea04d9 merge with other duplicate head diff -r 27ea12c05e99 -r 692a1aa1350c README.md --- 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 diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/__init__.py --- /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__) diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/topic/__init__.py --- /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')) diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/topic/constants.py --- /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' diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/topic/destination.py --- /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) diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/topic/discovery.py --- /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 diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/topic/revset.py --- /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}) diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/topic/stack.py --- /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 diff -r 27ea12c05e99 -r 692a1aa1350c hgext3rd/topic/topicmap.py --- /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 diff -r 27ea12c05e99 -r 692a1aa1350c setup.py --- 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, ) diff -r 27ea12c05e99 -r 692a1aa1350c src/topic/__init__.py --- 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')) diff -r 27ea12c05e99 -r 692a1aa1350c src/topic/constants.py --- 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' diff -r 27ea12c05e99 -r 692a1aa1350c src/topic/destination.py --- 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) diff -r 27ea12c05e99 -r 692a1aa1350c src/topic/discovery.py --- 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) - - - diff -r 27ea12c05e99 -r 692a1aa1350c src/topic/revset.py --- 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}) diff -r 27ea12c05e99 -r 692a1aa1350c src/topic/stack.py --- 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 diff -r 27ea12c05e99 -r 692a1aa1350c src/topic/topicmap.py --- 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 diff -r 27ea12c05e99 -r 692a1aa1350c tests/test-topic-dest.t --- 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 <> .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 diff -r 27ea12c05e99 -r 692a1aa1350c tests/test-topic-push.t --- 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 diff -r 27ea12c05e99 -r 692a1aa1350c tests/test-topic-stack.t --- 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) diff -r 27ea12c05e99 -r 692a1aa1350c tests/testlib --- 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