# HG changeset patch # User Pierre-Yves David # Date 1521646518 -3600 # Node ID b92114f201c9bda3878a23e0ec0d68a40f7a0aef # Parent 7a5c3175015e6d33e86aee846d0db06b32b23a8f# Parent 7ac98f83ae6d9a0cef486e47682d9b7b059cb141 test-compat: merge mercurial-4.2 into mercurial-4.1 diff -r 7ac98f83ae6d -r b92114f201c9 .hgtags --- a/.hgtags Sat Jan 20 12:38:11 2018 +0100 +++ b/.hgtags Wed Mar 21 16:35:18 2018 +0100 @@ -63,3 +63,4 @@ c4940c22d76b9c6b3c2117a3b490f3c4fd796972 7.0.1 06a3cb59495636df8b567e49a0fd7fd8fd823074 7.1.0 bf6b859807bac23752a26e58876fe3a4a9a2fef8 7.2.0 +6c772398eb4e209914e1074cdac4f3ebf714e437 7.2.1 diff -r 7ac98f83ae6d -r b92114f201c9 CHANGELOG --- a/CHANGELOG Sat Jan 20 12:38:11 2018 +0100 +++ b/CHANGELOG Wed Mar 21 16:35:18 2018 +0100 @@ -1,6 +1,21 @@ Changelog ========= +7.3.0 -- 2018-03-21 +--------------------- + + * grab: new command to grab a changeset, put in on wdir parent + and update to it + * resolve: shows how to continue evolve after resolving all conflicts + * evolve: `--continue` flag now continue evolving all the remaining revisions + * prev and next now prompts user to choose a changeset in case of ambiguity + * evolve: a new `--stop` flag which can be used to stop interrupted evolution + + * fold: fix issue related to bookmarks movement (issue5772) + * amend: take lock before parsing the commit description (issue5266) + * legacy: respect 'server.bundle1' config if any is set + * previous: fix behavior on obsolete rev when topic is involved (issue5708) + 7.2.1 --2018-01-20 ------------------- diff -r 7ac98f83ae6d -r b92114f201c9 debian/changelog --- a/debian/changelog Sat Jan 20 12:38:11 2018 +0100 +++ b/debian/changelog Wed Mar 21 16:35:18 2018 +0100 @@ -1,3 +1,9 @@ +mercurial-evolve (7.3.0-1) unstable; urgency=medium + + * New upstream release + + -- Pierre-Yves David Wed, 21 Mar 2018 15:34:15 +0100 + mercurial-evolve (7.2.1-1) unstable; urgency=medium * new upstream release diff -r 7ac98f83ae6d -r b92114f201c9 docs/index.rst --- a/docs/index.rst Sat Jan 20 12:38:11 2018 +0100 +++ b/docs/index.rst Wed Mar 21 16:35:18 2018 +0100 @@ -157,6 +157,8 @@ or care about the new features added by evolution won't be negatively impacted by the new default. +You can find the `evolution roadmap in the wiki`_. + .. # .. _`this query`: https://bz.mercurial-scm.org/buglist.cgi?component=evolution&bug_status=UNCONFIRMED&bug_status=CONFIRMED&bug_status=NEED_EXAMPLE Resources @@ -173,3 +175,4 @@ .. _`sharing mutable history`: sharing.html .. _`concepts`: concepts.html .. _`MQ migration guide`: from-mq.html +.. _`evolution roadmap in the wiki`: https://www.mercurial-scm.org/wiki/CEDRoadMap diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/__init__.py --- a/hgext3rd/evolve/__init__.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/__init__.py Wed Mar 21 16:35:18 2018 +0100 @@ -252,11 +252,7 @@ evolution=all """.strip() - -import os import sys -import re -import collections import struct try: @@ -287,23 +283,18 @@ cmdutil, commands, context, - copies, dirstate, error, extensions, help, hg, lock as lockmod, - merge, node, - obsolete, patch, - phases, revset, scmutil, ) -from mercurial.commands import mergetoolopts from mercurial.i18n import _ from mercurial.node import nullid @@ -312,7 +303,8 @@ compat, debugcmd, cmdrewrite, - evolvestate, + state, + evolvecmd, exthelper, metadata, obscache, @@ -330,8 +322,6 @@ minimumhgversion = metadata.minimumhgversion buglink = metadata.buglink -sha1re = re.compile(r'\b[0-9a-f]{6,40}\b') - # Flags for enabling optional parts of evolve commandopt = 'allnewcommands' @@ -345,6 +335,7 @@ 'evolve.date': 'cyan', 'evolve.current_rev': 'bold', 'evolve.verb': '', + 'evolve.operation': 'bold' } _pack = struct.pack @@ -353,7 +344,6 @@ aliases, entry = cmdutil.findcmd('commit', commands.table) commitopts3 = cmdrewrite.commitopts3 interactiveopt = cmdrewrite.interactiveopt -_bookmarksupdater = rewriteutil.bookmarksupdater rewrite = rewriteutil.rewrite # This extension contains the following code @@ -365,6 +355,7 @@ eh = exthelper.exthelper() eh.merge(debugcmd.eh) +eh.merge(evolvecmd.eh) eh.merge(obsexchange.eh) eh.merge(checkheads.eh) eh.merge(safeguard.eh) @@ -424,7 +415,9 @@ evolveopts = ['all'] repo.ui.setconfig('experimental', 'evolution', evolveopts, 'evolve') if obsolete.isenabled(repo, 'exchange'): - repo.ui.setconfig('server', 'bundle1', False) + # if no config explicitly set, disable bundle1 + if not isinstance(repo.ui.config('server', 'bundle1'), str): + repo.ui.setconfig('server', 'bundle1', False) class trdescrepo(repo.__class__): @@ -520,18 +513,6 @@ ui.setconfig('alias', 'odiff', "diff --hidden --rev 'limit(precursors(.),1)' --rev .", 'evolve') - if ui.config('alias', 'grab') is None: - if os.name == 'nt': - hgexe = ('"%s"' % util.hgexecutable()) - ui.setconfig('alias', 'grab', "! " + hgexe - + " rebase --dest . --rev $@ && " - + hgexe + " up tip", - 'evolve') - else: - ui.setconfig('alias', 'grab', - "! $HG rebase --dest . --rev $@ && $HG up tip", - 'evolve') - ### Troubled revset symbol @@ -710,7 +691,8 @@ ui.warn(msg % shortnode) # Check that evolve is activated for performance reasons - if ui.quiet or not obsolete.isenabled(repo, commandopt): + evolvecommandenabled = any('evolve' in e for e in cmdtable) + if ui.quiet or not evolvecommandenabled: return # Show a warning for helping the user to solve the issue @@ -853,8 +835,8 @@ raise def summaryhook(ui, repo): - state = evolvestate.evolvestate(repo) - if state: + evolvestate = state.cmdstate(repo) + if evolvestate: # i18n: column positioning for "hg summary" ui.status(_('evolve: (evolve --continue)\n')) @@ -888,79 +870,6 @@ ### Old Evolve extension content ### ##################################################################### -# XXX need clean up and proper sorting in other section - -### changeset rewriting logic -############################# - -class MergeFailure(error.Abort): - pass - -def relocate(repo, orig, dest, pctx=None, keepbranch=False): - """rewrite on dest""" - if orig.rev() == dest.rev(): - raise error.Abort(_('tried to relocate a node on top of itself'), - hint=_("This shouldn't happen. If you still " - "need to move changesets, please do so " - "manually with nothing to rebase - working " - "directory parent is also destination")) - - if pctx is None: - if len(orig.parents()) == 2: - raise error.Abort(_("tried to relocate a merge commit without " - "specifying which parent should be moved"), - hint=_("Specify the parent by passing in pctx")) - pctx = orig.p1() - - commitmsg = orig.description() - - cache = {} - sha1s = re.findall(sha1re, commitmsg) - unfi = repo.unfiltered() - for sha1 in sha1s: - ctx = None - try: - ctx = unfi[sha1] - except error.RepoLookupError: - continue - - if not ctx.obsolete(): - continue - - successors = compat.successorssets(repo, ctx.node(), cache) - - # We can't make any assumptions about how to update the hash if the - # cset in question was split or diverged. - if len(successors) == 1 and len(successors[0]) == 1: - newsha1 = node.hex(successors[0][0]) - commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)]) - else: - repo.ui.note(_('The stale commit message reference to %s could ' - 'not be updated\n') % sha1) - - tr = repo.currenttransaction() - assert tr is not None - try: - r = _evolvemerge(repo, orig, dest, pctx, keepbranch) - if r[-1]: # some conflict - raise error.Abort(_('unresolved merge conflicts ' - '(see hg help resolve)')) - nodenew = _relocatecommit(repo, orig, commitmsg) - except error.Abort as exc: - with repo.dirstate.parentchange(): - repo.setparents(repo['.'].node(), nullid) - repo.dirstate.write(tr) - # fix up dirstate for copies and renames - compat.duplicatecopies(repo, repo[None], dest.rev(), orig.p1().rev()) - - class LocalMergeFailure(MergeFailure, exc.__class__): - pass - exc.__class__ = LocalMergeFailure - tr.close() # to keep changes in this transaction (e.g. dirstate) - raise - _finalizerelocate(repo, orig, dest, nodenew, tr) - return nodenew - ### new command ############################# @@ -1038,1009 +947,6 @@ _deprecatealias('gup', 'next') _deprecatealias('gdown', 'previous') -def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category): - """Resolve the troubles affecting one revision""" - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - tr = repo.transaction("evolve") - if 'orphan' == category: - result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb) - elif 'phasedivergent' == category: - result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb) - elif 'contentdivergent' == category: - result = _solvedivergent(ui, repo, ctx, dryrun, confirm, - progresscb) - else: - assert False, "unknown trouble category: %s" % (category) - tr.close() - return result - finally: - lockmod.release(tr, lock, wlock) - -def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat): - """Used by the evolve function to display an error message when - no troubles can be resolved""" - troublecategories = ['phasedivergent', 'contentdivergent', 'orphan'] - unselectedcategories = [c for c in troublecategories if c != targetcat] - msg = None - hint = None - - troubled = { - "orphan": repo.revs("orphan()"), - "contentdivergent": repo.revs("contentdivergent()"), - "phasedivergent": repo.revs("phasedivergent()"), - "all": repo.revs("troubled()"), - } - - hintmap = { - 'phasedivergent': _("do you want to use --phase-divergent"), - 'phasedivergent+contentdivergent': _("do you want to use " - "--phase-divergent or" - " --content-divergent"), - 'phasedivergent+orphan': _("do you want to use --phase-divergent" - " or --orphan"), - 'contentdivergent': _("do you want to use --content-divergent"), - 'contentdivergent+orphan': _("do you want to use --content-divergent" - " or --orphan"), - 'orphan': _("do you want to use --orphan"), - 'any+phasedivergent': _("do you want to use --any (or --rev) and" - " --phase-divergent"), - 'any+phasedivergent+contentdivergent': _("do you want to use --any" - " (or --rev) and" - " --phase-divergent or" - " --content-divergent"), - 'any+phasedivergent+orphan': _("do you want to use --any (or --rev)" - " and --phase-divergent or --orphan"), - 'any+contentdivergent': _("do you want to use --any (or --rev) and" - " --content-divergent"), - 'any+contentdivergent+orphan': _("do you want to use --any (or --rev)" - " and --content-divergent or " - "--orphan"), - 'any+orphan': _("do you want to use --any (or --rev)" - "and --orphan"), - } - - if revopt: - revs = scmutil.revrange(repo, revopt) - if not revs: - msg = _("set of specified revisions is empty") - else: - msg = _("no %s changesets in specified revisions") % targetcat - othertroubles = [] - for cat in unselectedcategories: - if revs & troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['+'.join(othertroubles)] - - elif anyopt: - msg = _("no %s changesets to evolve") % targetcat - othertroubles = [] - for cat in unselectedcategories: - if troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['+'.join(othertroubles)] - - else: - # evolve without any option = relative to the current wdir - if targetcat == 'orphan': - msg = _("nothing to evolve on current working copy parent") - else: - msg = _("current working copy parent is not %s") % targetcat - - p1 = repo['.'].rev() - othertroubles = [] - for cat in unselectedcategories: - if p1 in troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['+'.join(othertroubles)] - else: - length = len(troubled[targetcat]) - if length: - hint = _("%d other %s in the repository, do you want --any " - "or --rev") % (length, targetcat) - else: - othertroubles = [] - for cat in unselectedcategories: - if troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['any+' + ('+'.join(othertroubles))] - else: - msg = _("no troubled changesets") - - assert msg is not None - ui.write_err("%s\n" % msg) - if hint: - ui.write_err("(%s)\n" % hint) - return 2 - else: - return 1 - -def _cleanup(ui, repo, startnode, showprogress): - if showprogress: - ui.progress(_('evolve'), None) - if repo['.'] != startnode: - ui.status(_('working directory is now at %s\n') % repo['.']) - -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 - -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 = compat.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 = compat.successorssets(repo, obs.node()) - if len(newer) > 1 or len(newer[0]) > 1: - raise MultipleSuccessorsError(newer) - - return repo[newer[0][0]].rev() - -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 _dedupedivergents(repo, revs): - """Dedupe the divergents revs in revs to get one from each group with the - lowest revision numbers - """ - repo = repo.unfiltered() - res = set() - # To not reevaluate divergents of the same group once one is encountered - discarded = set() - for rev in revs: - if rev in discarded: - continue - divergent = repo[rev] - base, others = divergentdata(divergent) - othersrevs = [o.rev() for o in others] - res.add(min([divergent.rev()] + othersrevs)) - discarded.update(othersrevs) - return res - -instabilities_map = { - 'contentdivergent': "content-divergent", - 'phasedivergent': "phase-divergent" -} - -def _selectrevs(repo, allopt, revopt, anyopt, targetcat): - """select troubles in repo matching according to given options""" - revs = set() - if allopt or revopt: - revs = repo.revs("%s()" % targetcat) - if revopt: - revs = scmutil.revrange(repo, revopt) & revs - elif not anyopt: - topic = getattr(repo, 'currenttopic', '') - if topic: - revs = repo.revs('topic(%s)', topic) & revs - elif targetcat == 'orphan': - revs = _aspiringdescendant(repo, - repo.revs('(.::) - obsolete()::')) - revs = set(revs) - if targetcat == 'contentdivergent': - # Pick one divergent per group of divergents - revs = _dedupedivergents(repo, revs) - elif anyopt: - revs = repo.revs('first(%s())' % (targetcat)) - elif targetcat == 'orphan': - revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::'))) - if 1 < len(revs): - msg = "multiple evolve candidates" - hint = (_("select one of %s with --rev") - % ', '.join([str(repo[r]) for r in sorted(revs)])) - raise error.Abort(msg, hint=hint) - elif instabilities_map.get(targetcat, targetcat) in repo['.'].instabilities(): - revs = set([repo['.'].rev()]) - return revs - - -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 divergentsets(repo, ctx): - """Compute sets of commits divergent with a given one""" - cache = {} - base = {} - for n in compat.allprecursors(repo.obsstore, [ctx.node()]): - if n == ctx.node(): - # a node can't be a base for divergence with itself - continue - nsuccsets = compat.successorssets(repo, n, cache) - for nsuccset in nsuccsets: - if ctx.node() in nsuccset: - # we are only interested in *other* successor sets - continue - if tuple(nsuccset) in base: - # we already know the latest base for this divergency - continue - base[tuple(nsuccset)] = n - divergence = [] - for divset, b in base.iteritems(): - divergence.append({ - 'divergentnodes': divset, - 'commonprecursor': b - }) - - return divergence - -def _preparelistctxs(items, condition): - return [item.hex() for item in items if condition(item)] - -def _formatctx(fm, ctx): - fm.data(node=ctx.hex()) - fm.data(desc=ctx.description()) - fm.data(date=ctx.date()) - fm.data(user=ctx.user()) - -def listtroubles(ui, repo, troublecategories, **opts): - """Print all the troubles for the repo (or given revset)""" - troublecategories = troublecategories or ['contentdivergent', 'orphan', 'phasedivergent'] - showunstable = 'orphan' in troublecategories - showbumped = 'phasedivergent' in troublecategories - showdivergent = 'contentdivergent' in troublecategories - - revs = repo.revs('+'.join("%s()" % t for t in troublecategories)) - if opts.get('rev'): - revs = scmutil.revrange(repo, opts.get('rev')) - - fm = ui.formatter('evolvelist', opts) - for rev in revs: - ctx = repo[rev] - unpars = _preparelistctxs(ctx.parents(), lambda p: p.orphan()) - obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete()) - imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()), - lambda p: not p.mutable()) - dsets = divergentsets(repo, ctx) - - fm.startitem() - # plain formatter section - hashlen, desclen = 12, 60 - desc = ctx.description() - if desc: - desc = desc.splitlines()[0] - desc = (desc[:desclen] + '...') if len(desc) > desclen else desc - fm.plain('%s: ' % ctx.hex()[:hashlen]) - fm.plain('%s\n' % desc) - fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr()) - - for unpar in unpars if showunstable else []: - fm.plain(' %s: %s (%s parent)\n' % (TROUBLES['ORPHAN'], - unpar[:hashlen], - TROUBLES['ORPHAN'])) - for obspar in obspars if showunstable else []: - fm.plain(' %s: %s (obsolete parent)\n' % (TROUBLES['ORPHAN'], - obspar[:hashlen])) - for imprec in imprecs if showbumped else []: - fm.plain(' %s: %s (immutable precursor)\n' % - (TROUBLES['PHASEDIVERGENT'], imprec[:hashlen])) - - if dsets and showdivergent: - for dset in dsets: - fm.plain(' %s: ' % TROUBLES['CONTENTDIVERGENT']) - first = True - for n in dset['divergentnodes']: - t = "%s (%s)" if first else " %s (%s)" - first = False - fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr())) - comprec = node.hex(dset['commonprecursor'])[:hashlen] - fm.plain(" (precursor %s)\n" % comprec) - fm.plain("\n") - - # templater-friendly section - _formatctx(fm, ctx) - troubles = [] - for unpar in unpars: - troubles.append({'troubletype': TROUBLES['ORPHAN'], - 'sourcenode': unpar, 'sourcetype': 'orphanparent'}) - for obspar in obspars: - troubles.append({'troubletype': TROUBLES['ORPHAN'], - 'sourcenode': obspar, - 'sourcetype': 'obsoleteparent'}) - for imprec in imprecs: - troubles.append({'troubletype': TROUBLES['PHASEDIVERGENT'], - 'sourcenode': imprec, - 'sourcetype': 'immutableprecursor'}) - for dset in dsets: - divnodes = [{'node': node.hex(n), - 'phase': repo[n].phasestr(), - } for n in dset['divergentnodes']] - troubles.append({'troubletype': TROUBLES['CONTENTDIVERGENT'], - 'commonprecursor': node.hex(dset['commonprecursor']), - 'divergentnodes': divnodes}) - fm.data(troubles=troubles) - - fm.end() - -@eh.command( - '^evolve|stabilize|solve', - [('n', 'dry-run', False, - _('do not perform actions, just print what would be done')), - ('', 'confirm', False, - _('ask for confirmation before performing the action')), - ('A', 'any', False, - _('also consider troubled changesets unrelated to current working ' - 'directory')), - ('r', 'rev', [], _('solves troubles of these revisions')), - ('', 'bumped', False, _('solves only bumped changesets')), - ('', 'phase-divergent', False, _('solves only phase-divergent changesets')), - ('', 'divergent', False, _('solves only divergent changesets')), - ('', 'content-divergent', False, _('solves only content-divergent changesets')), - ('', 'unstable', False, _('solves only unstable changesets')), - ('', 'orphan', False, _('solves only orphan changesets (default)')), - ('a', 'all', False, _('evolve all troubled changesets related to the ' - 'current working directory and its descendants')), - ('c', 'continue', False, _('continue an interrupted evolution')), - ('l', 'list', False, 'provide details on troubled changesets in the repo'), - ] + mergetoolopts, - _('[OPTIONS]...') -) -def evolve(ui, repo, **opts): - """solve troubled changesets in your repository - - Modifying history can lead to various types of troubled changesets: - unstable, bumped, or divergent. The evolve command resolves your troubles - by executing one of the following actions: - - - update working copy to a successor - - rebase an unstable changeset - - extract the desired changes from a bumped changeset - - fuse divergent changesets back together - - If you pass no arguments, evolve works in automatic mode: it will execute a - single action to reduce instability related to your working copy. There are - two cases for this action. First, if the parent of your working copy is - obsolete, evolve updates to the parent's successor. Second, if the working - copy parent is not obsolete but has obsolete predecessors, then evolve - determines if there is an unstable changeset that can be rebased onto the - working copy parent in order to reduce instability. - If so, evolve rebases that changeset. If not, evolve refuses to guess your - intention, and gives a hint about what you might want to do next. - - Any time evolve creates a changeset, it updates the working copy to the new - changeset. (Currently, every successful evolve operation involves an update - as well; this may change in future.) - - Automatic mode only handles common use cases. For example, it avoids taking - action in the case of ambiguity, and it ignores unstable changesets that - are not related to your working copy. - It also refuses to solve bumped or divergent changesets unless you - explicitly request such behavior (see below). - - Eliminating all instability around your working copy may require multiple - invocations of :hg:`evolve`. Alternately, use ``--all`` to recursively - select and evolve all unstable changesets that can be rebased onto the - working copy parent. - This is more powerful than successive invocations, since ``--all`` handles - ambiguous cases (e.g. unstable changesets with multiple children) by - evolving all branches. - - When your repository cannot be handled by automatic mode, you might need to - use ``--rev`` to specify a changeset to evolve. For example, if you have - an unstable changeset that is not related to the working copy parent, - you could use ``--rev`` to evolve it. Or, if some changeset has multiple - unstable children, evolve in automatic mode refuses to guess which one to - evolve; you have to use ``--rev`` in that case. - - Alternately, ``--any`` makes evolve search for the next evolvable changeset - regardless of whether it is related to the working copy parent. - - You can supply multiple revisions to evolve multiple troubled changesets - in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev - first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are - ``--rev`` and ``--any``. - - ``hg evolve --any --all`` is useful for cleaning up instability across all - branches, letting evolve figure out the appropriate order and destination. - - When you have troubled changesets that are not unstable, :hg:`evolve` - refuses to consider them unless you specify the category of trouble you - wish to resolve, with ``--bumped`` or ``--divergent``. These options are - currently mutually exclusive with each other and with ``--unstable`` - (the default). You can combine ``--bumped`` or ``--divergent`` with - ``--rev``, ``--all``, or ``--any``. - - You can also use the evolve command to list the troubles affecting your - repository by using the --list flag. You can choose to display only some - categories of troubles with the --unstable, --divergent or --bumped flags. - """ - - opts = _checkevolveopts(repo, opts) - # Options - contopt = opts['continue'] - anyopt = opts['any'] - allopt = opts['all'] - startnode = repo['.'] - dryrunopt = opts['dry_run'] - confirmopt = opts['confirm'] - revopt = opts['rev'] - - troublecategories = ['phase_divergent', 'content_divergent', 'orphan'] - specifiedcategories = [t.replace('_', '') - for t in troublecategories - if opts[t]] - if opts['list']: - compat.startpager(ui, 'evolve') - listtroubles(ui, repo, specifiedcategories, **opts) - return - - targetcat = 'orphan' - if 1 < len(specifiedcategories): - msg = _('cannot specify more than one trouble category to solve (yet)') - raise error.Abort(msg) - elif len(specifiedcategories) == 1: - targetcat = specifiedcategories[0] - elif repo['.'].obsolete(): - displayer = cmdutil.show_changeset(ui, repo, - {'template': shorttemplate}) - # no args and parent is obsolete, update to successors - try: - ctx = repo[_singlesuccessor(repo, repo['.'])] - except MultipleSuccessorsError as exc: - repo.ui.write_err('parent is obsolete with multiple successors:\n') - for ln in exc.successorssets: - for n in ln: - displayer.show(repo[n]) - return 2 - - ui.status(_('update:')) - if not ui.quiet: - displayer.show(ctx) - - if dryrunopt: - return 0 - res = hg.update(repo, ctx.rev()) - if ctx != startnode: - ui.status(_('working directory is now at %s\n') % ctx) - return res - - ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve') - troubled = set(repo.revs('troubled()')) - - # Progress handling - seen = 1 - count = allopt and len(troubled) or 1 - showprogress = allopt - - def progresscb(): - if revopt or allopt: - ui.progress(_('evolve'), seen, unit=_('changesets'), total=count) - - # Continuation handling - if contopt: - state = evolvestate.evolvestate(repo) - if not state: - raise error.Abort('no evolve to continue') - state.load() - orig = repo[state['current']] - with repo.wlock(), repo.lock(): - ctx = orig - source = ctx.extra().get('source') - extra = {} - if source: - extra['source'] = source - extra['intermediate-source'] = ctx.hex() - else: - extra['source'] = ctx.hex() - user = ctx.user() - date = ctx.date() - message = ctx.description() - ui.status(_('evolving %d:%s "%s"\n') % (ctx.rev(), ctx, - message.split('\n', 1)[0])) - targetphase = max(ctx.phase(), phases.draft) - overrides = {('phases', 'new-commit'): targetphase} - - with repo.ui.configoverride(overrides, 'evolve-continue'): - node = repo.commit(text=message, user=user, - date=date, extra=extra) - - obsolete.createmarkers(repo, [(ctx, (repo[node],))]) - state.delete() - return - - cmdutil.bailifchanged(repo) - - revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat) - - if not revs: - return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat) - - # For the progress bar to show - count = len(revs) - # Order the revisions - if targetcat == 'orphan': - revs = _orderrevs(repo, revs) - for rev in revs: - progresscb() - _solveone(ui, repo, repo[rev], dryrunopt, confirmopt, - progresscb, targetcat) - seen += 1 - progresscb() - _cleanup(ui, repo, startnode, showprogress) - -def _checkevolveopts(repo, opts): - """ check the options passed to `hg evolve` and warn for deprecation warning - if any """ - - if opts['continue']: - if opts['any']: - raise error.Abort('cannot specify both "--any" and "--continue"') - if opts['all']: - raise error.Abort('cannot specify both "--all" and "--continue"') - - if opts['rev']: - if opts['any']: - raise error.Abort('cannot specify both "--rev" and "--any"') - if opts['all']: - raise error.Abort('cannot specify both "--rev" and "--all"') - - # Backward compatibility - if opts['unstable']: - msg = ("'evolve --unstable' is deprecated, " - "use 'evolve --orphan'") - repo.ui.deprecwarn(msg, '4.4') - - opts['orphan'] = opts['divergent'] - - if opts['divergent']: - msg = ("'evolve --divergent' is deprecated, " - "use 'evolve --content-divergent'") - repo.ui.deprecwarn(msg, '4.4') - - opts['content_divergent'] = opts['divergent'] - - if opts['bumped']: - msg = ("'evolve --bumped' is deprecated, " - "use 'evolve --phase-divergent'") - repo.ui.deprecwarn(msg, '4.4') - - opts['phase_divergent'] = opts['bumped'] - - return opts - -def _possibledestination(repo, rev): - """return all changesets that may be a new parent for REV""" - tonode = repo.changelog.node - parents = repo.changelog.parentrevs - torev = repo.changelog.rev - dest = set() - tovisit = list(parents(rev)) - while tovisit: - r = tovisit.pop() - succsets = compat.successorssets(repo, tonode(r)) - if not succsets: - tovisit.extend(parents(r)) - else: - # We should probably pick only one destination from split - # (case where '1 < len(ss)'), This could be the currently tipmost - # but logic is less clear when result of the split are now on - # multiple branches. - for ss in succsets: - for n in ss: - dest.add(torev(n)) - return dest - -def _aspiringchildren(repo, revs): - """Return a list of changectx which can be stabilized on top of pctx or - one of its descendants. Empty list if none can be found.""" - target = set(revs) - result = [] - for r in repo.revs('orphan() - %ld', revs): - dest = _possibledestination(repo, r) - if target & dest: - result.append(r) - return result - -def _aspiringdescendant(repo, revs): - """Return a list of changectx which can be stabilized on top of pctx or - one of its descendants recursively. Empty list if none can be found.""" - target = set(revs) - result = set(target) - paths = collections.defaultdict(set) - for r in repo.revs('orphan() - %ld', revs): - for d in _possibledestination(repo, r): - paths[d].add(r) - - result = set(target) - tovisit = list(revs) - while tovisit: - base = tovisit.pop() - for unstable in paths[base]: - if unstable not in result: - tovisit.append(unstable) - result.add(unstable) - return sorted(result - target) - -def _solveunstable(ui, repo, orig, dryrun=False, confirm=False, - progresscb=None): - """Stabilize an unstable changeset""" - pctx = orig.p1() - keepbranch = orig.p1().branch() != orig.branch() - if len(orig.parents()) == 2: - if not pctx.obsolete(): - pctx = orig.p2() # second parent is obsolete ? - keepbranch = orig.p2().branch() != orig.branch() - elif orig.p2().obsolete(): - hint = _("Redo the merge (%s) and use `hg prune " - "--succ ` to obsolete the old one") % orig.hex()[:12] - ui.warn(_("warning: no support for evolving merge changesets " - "with two obsolete parents yet\n") + - _("(%s)\n") % hint) - return False - - if not pctx.obsolete(): - ui.warn(_("cannot solve instability of %s, skipping\n") % orig) - return False - obs = pctx - newer = compat.successorssets(repo, obs.node()) - # search of a parent which is not killed - while not newer or newer == [()]: - ui.debug("stabilize target %s is plain dead," - " trying to stabilize on its parent\n" % - obs) - obs = obs.parents()[0] - newer = compat.successorssets(repo, obs.node()) - if len(newer) > 1: - msg = _("skipping %s: divergent rewriting. can't choose " - "destination\n") % obs - ui.write_err(msg) - return 2 - targets = newer[0] - assert targets - if len(targets) > 1: - # split target, figure out which one to pick, are they all in line? - targetrevs = [repo[r].rev() for r in targets] - roots = repo.revs('roots(%ld)', targetrevs) - heads = repo.revs('heads(%ld)', targetrevs) - if len(roots) > 1 or len(heads) > 1: - msg = "cannot solve split across two branches\n" - ui.write_err(msg) - return 2 - target = repo[heads.first()] - else: - target = targets[0] - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - target = repo[target] - if not ui.quiet or confirm: - repo.ui.write(_('move:')) - displayer.show(orig) - repo.ui.write(_('atop:')) - displayer.show(target) - if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': - raise error.Abort(_('evolve aborted by user')) - if progresscb: - progresscb() - todo = 'hg rebase -r %s -d %s\n' % (orig, target) - if dryrun: - repo.ui.write(todo) - else: - repo.ui.note(todo) - if progresscb: - progresscb() - try: - relocate(repo, orig, target, pctx, keepbranch) - except MergeFailure: - ops = {'current': orig.node()} - state = evolvestate.evolvestate(repo, opts=ops) - state.save() - repo.ui.write_err(_('evolve failed!\n')) - repo.ui.write_err( - _("fix conflict and run 'hg evolve --continue'" - " or use 'hg update -C .' to abort\n")) - raise - -def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False, - progresscb=None): - """Stabilize a bumped changeset""" - repo = repo.unfiltered() - bumped = repo[bumped.rev()] - # For now we deny bumped merge - if len(bumped.parents()) > 1: - msg = _('skipping %s : we do not handle merge yet\n') % bumped - ui.write_err(msg) - return 2 - prec = repo.set('last(allprecursors(%d) and public())', bumped).next() - # For now we deny target merge - if len(prec.parents()) > 1: - msg = _('skipping: %s: public version is a merge, ' - 'this is not handled yet\n') % prec - ui.write_err(msg) - return 2 - - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - if not ui.quiet or confirm: - repo.ui.write(_('recreate:')) - displayer.show(bumped) - repo.ui.write(_('atop:')) - displayer.show(prec) - if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': - raise error.Abort(_('evolve aborted by user')) - if dryrun: - todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1()) - repo.ui.write(todo) - repo.ui.write(('hg update %s;\n' % prec)) - repo.ui.write(('hg revert --all --rev %s;\n' % bumped)) - repo.ui.write(('hg commit --msg "%s update to %s"\n' % - (TROUBLES['PHASEDIVERGENT'], bumped))) - return 0 - if progresscb: - progresscb() - newid = tmpctx = None - tmpctx = bumped - # Basic check for common parent. Far too complicated and fragile - tr = repo.currenttransaction() - assert tr is not None - bmupdate = _bookmarksupdater(repo, bumped.node(), tr) - if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)): - # Need to rebase the changeset at the right place - repo.ui.status( - _('rebasing to destination parent: %s\n') % prec.p1()) - try: - tmpid = relocate(repo, bumped, prec.p1()) - if tmpid is not None: - tmpctx = repo[tmpid] - obsolete.createmarkers(repo, [(bumped, (tmpctx,))]) - except MergeFailure: - repo.vfs.write('graftstate', bumped.hex() + '\n') - repo.ui.write_err(_('evolution failed!\n')) - msg = _("fix conflict and run 'hg evolve --continue'\n") - repo.ui.write_err(msg) - raise - # Create the new commit context - repo.ui.status(_('computing new diff\n')) - files = set() - copied = copies.pathcopies(prec, bumped) - precmanifest = prec.manifest().copy() - # 3.3.2 needs a list. - # future 3.4 don't detect the size change during iteration - # this is fishy - for key, val in list(bumped.manifest().iteritems()): - precvalue = precmanifest.get(key, None) - if precvalue is not None: - del precmanifest[key] - if precvalue != val: - files.add(key) - files.update(precmanifest) # add missing files - # commit it - if files: # something to commit! - def filectxfn(repo, ctx, path): - if path in bumped: - fctx = bumped[path] - flags = fctx.flags() - mctx = compat.memfilectx(repo, ctx, fctx, flags, copied, path) - return mctx - return None - text = '%s update to %s:\n\n' % (TROUBLES['PHASEDIVERGENT'], prec) - text += bumped.description() - - new = context.memctx(repo, - parents=[prec.node(), node.nullid], - text=text, - files=files, - filectxfn=filectxfn, - user=bumped.user(), - date=bumped.date(), - extra=bumped.extra()) - - newid = repo.commitctx(new) - if newid is None: - obsolete.createmarkers(repo, [(tmpctx, ())]) - newid = prec.node() - else: - phases.retractboundary(repo, tr, bumped.phase(), [newid]) - obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))], - flag=obsolete.bumpedfix) - bmupdate(newid) - repo.ui.status(_('committed as %s\n') % node.short(newid)) - # reroute the working copy parent to the new changeset - with repo.dirstate.parentchange(): - repo.dirstate.setparents(newid, node.nullid) - -def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False, - progresscb=None): - repo = repo.unfiltered() - divergent = repo[divergent.rev()] - base, others = divergentdata(divergent) - if len(others) > 1: - othersstr = "[%s]" % (','.join([str(i) for i in others])) - msg = _("skipping %d:%s with a changeset that got split" - " into multiple ones:\n" - "|[%s]\n" - "| This is not handled by automatic evolution yet\n" - "| You have to fallback to manual handling with commands " - "such as:\n" - "| - hg touch -D\n" - "| - hg prune\n" - "| \n" - "| You should contact your local evolution Guru for help.\n" - ) % (divergent, TROUBLES['CONTENTDIVERGENT'], othersstr) - ui.write_err(msg) - return 2 - other = others[0] - if len(other.parents()) > 1: - msg = _("skipping %s: %s changeset can't be " - "a merge (yet)\n") % (divergent, TROUBLES['CONTENTDIVERGENT']) - ui.write_err(msg) - hint = _("You have to fallback to solving this by hand...\n" - "| This probably means redoing the merge and using \n" - "| `hg prune` to kill older version.\n") - ui.write_err(hint) - return 2 - if other.p1() not in divergent.parents(): - msg = _("skipping %s: have a different parent than %s " - "(not handled yet)\n") % (divergent, other) - hint = _("| %(d)s, %(o)s are not based on the same changeset.\n" - "| With the current state of its implementation, \n" - "| evolve does not work in that case.\n" - "| rebase one of them next to the other and run \n" - "| this command again.\n" - "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n" - "| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n" - ) % {'d': divergent, 'o': other} - ui.write_err(msg) - ui.write_err(hint) - return 2 - - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - if not ui.quiet or confirm: - ui.write(_('merge:')) - displayer.show(divergent) - ui.write(_('with: ')) - displayer.show(other) - ui.write(_('base: ')) - displayer.show(base) - if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y': - raise error.Abort(_('evolve aborted by user')) - if dryrun: - ui.write(('hg update -c %s &&\n' % divergent)) - ui.write(('hg merge %s &&\n' % other)) - ui.write(('hg commit -m "auto merge resolving conflict between ' - '%s and %s"&&\n' % (divergent, other))) - ui.write(('hg up -C %s &&\n' % base)) - ui.write(('hg revert --all --rev tip &&\n')) - ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n' - % divergent)) - return - if divergent not in repo[None].parents(): - repo.ui.status(_('updating to "local" conflict\n')) - hg.update(repo, divergent.rev()) - repo.ui.note(_('merging %s changeset\n') % TROUBLES['CONTENTDIVERGENT']) - if progresscb: - progresscb() - stats = merge.update(repo, - other.node(), - branchmerge=True, - force=False, - ancestor=base.node(), - mergeancestor=True) - hg._showstats(repo, stats) - if stats[3]: - repo.ui.status(_("use 'hg resolve' to retry unresolved file merges " - "or 'hg update -C .' to abort\n")) - if stats[3] > 0: - raise error.Abort('merge conflict between several amendments ' - '(this is not automated yet)', - hint="""/!\ You can try: -/!\ * manual merge + resolve => new cset X -/!\ * hg up to the parent of the amended changeset (which are named W and Z) -/!\ * hg revert --all -r X -/!\ * hg ci -m "same message as the amended changeset" => new cset Y -/!\ * hg prune -n Y W Z -""") - if progresscb: - progresscb() - emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit') - tr = repo.currenttransaction() - assert tr is not None - try: - repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve') - with repo.dirstate.parentchange(): - repo.dirstate.setparents(divergent.node(), node.nullid) - oldlen = len(repo) - cmdrewrite.amend(ui, repo, message='', logfile='') - if oldlen == len(repo): - new = divergent - # no changes - else: - new = repo['.'] - obsolete.createmarkers(repo, [(other, (new,))]) - phases.retractboundary(repo, tr, other.phase(), [new.node()]) - finally: - repo.ui.restoreconfig(emtpycommitallowed) - -def divergentdata(ctx): - """return base, other part of a conflict - - This only return the first one. - - XXX this woobly function won't survive XXX - """ - repo = ctx._repo.unfiltered() - for base in repo.set('reverse(allprecursors(%d))', ctx): - newer = compat.successorssets(ctx._repo, base.node()) - # drop filter and solution including the original ctx - newer = [n for n in newer if n and ctx.node() not in n] - if newer: - return base, tuple(ctx._repo[o] for o in newer[0]) - raise error.Abort("base of divergent changeset %s not found" % ctx, - hint='this case is not yet handled') - def _gettopic(ctx): """handle topic fetching with or without the extension""" return getattr(ctx, 'topic', lambda: '')() @@ -2054,10 +960,10 @@ def _prevupdate(repo, displayer, target, bookmark, dryrun): if dryrun: - repo.ui.write(('hg update %s;\n' % target.rev())) + repo.ui.write(_('hg update %s;\n') % target) if bookmark is not None: - repo.ui.write(('hg bookmark %s -r %s;\n' - % (bookmark, target.rev()))) + repo.ui.write(_('hg bookmark %s -r %s;\n') + % (bookmark, target)) else: ret = hg.update(repo, target.rev()) if not ret: @@ -2086,7 +992,7 @@ # we do not filter in the 1 case to allow prev to t0 if currenttopic and topic and _gettopicidx(p1) != 1: - parents = [repo[_singlesuccessor(repo, ctx)] if ctx.mutable() else ctx + parents = [repo[utility._singlesuccessor(repo, ctx)] if ctx.mutable() else ctx for ctx in parents] parents = [ctx for ctx in parents if ctx.topic() == currenttopic] @@ -2102,16 +1008,22 @@ if movebookmark: bookmark = repo._activebookmark else: - for p in parents: - displayer.show(p) - repo.ui.warn(_('multiple parents, explicitly update to one\n')) + header = _("multiple parents, choose one to update:") + prevs = [p.rev() for p in parents] + choosedrev = utility.revselectionprompt(repo.ui, repo, prevs, header) + if choosedrev is None: + for p in parents: + displayer.show(p) + repo.ui.warn(_('multiple parents, explicitly update to one\n')) + else: + target = repo[choosedrev] return target, bookmark @eh.command( '^previous', [('B', 'move-bookmark', False, _('move active bookmark after update')), - ('', 'merge', False, _('bring uncommitted change along')), + ('m', 'merge', False, _('bring uncommitted change along')), ('', 'no-topic', False, _('ignore topic and move topologically')), ('n', 'dry-run', False, _('do not perform actions, just print what would be done'))], @@ -2128,7 +1040,7 @@ wkctx = repo[None] wparents = wkctx.parents() if len(wparents) != 1: - raise error.Abort('merge in progress') + raise error.Abort(_('merge in progress')) if not opts['merge']: try: cmdutil.bailifchanged(repo) @@ -2136,7 +1048,8 @@ exc.hint = _('do you want --merge?') raise - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + displayer = compat.changesetdisplayer(ui, repo, + {'template': shorttemplate}) topic = not opts.get("no_topic", False) target, bookmark = _findprevtarget(repo, displayer, @@ -2160,7 +1073,7 @@ '^next', [('B', 'move-bookmark', False, _('move active bookmark after update')), - ('', 'merge', False, _('bring uncommitted change along')), + ('m', 'merge', False, _('bring uncommitted change along')), ('', 'evolve', False, _('evolve the next changeset if necessary')), ('', 'no-topic', False, _('ignore topic and move topologically')), ('n', 'dry-run', False, @@ -2181,7 +1094,7 @@ wkctx = repo[None] wparents = wkctx.parents() if len(wparents) != 1: - raise error.Abort('merge in progress') + raise error.Abort(_('merge in progress')) if not opts['merge']: try: cmdutil.bailifchanged(repo) @@ -2196,41 +1109,25 @@ filtered = [ctx for ctx in children if ctx.topic() != topic] # XXX N-square membership on children children = [ctx for ctx in children if ctx not in filtered] - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + displayer = compat.changesetdisplayer(ui, repo, + {'template': shorttemplate}) if len(children) == 1: c = children[0] - bm = repo._activebookmark - shouldmove = opts.get('move_bookmark') and bm is not None - if dryrunopt: - ui.write(('hg update %s;\n' % c.rev())) - if shouldmove: - ui.write(('hg bookmark %s -r %s;\n' % (bm, c.rev()))) + return _updatetonext(ui, repo, c, displayer, opts) + elif children: + cheader = _("ambiguous next changeset, choose one to update:") + crevs = [c.rev() for c in children] + choosedrev = utility.revselectionprompt(ui, repo, crevs, cheader) + if choosedrev is None: + ui.warn(_("ambiguous next changeset:\n")) + for c in children: + displayer.show(c) + ui.warn(_("explicitly update to one of them\n")) + return 1 else: - ret = hg.update(repo, c.rev()) - if not ret: - lock = tr = None - try: - lock = repo.lock() - tr = repo.transaction('next') - if shouldmove: - bmchanges = [(bm, c.node())] - compat.bookmarkapplychanges(repo, tr, bmchanges) - else: - bookmarksmod.deactivate(repo) - tr.close() - finally: - lockmod.release(tr, lock) - if not ui.quiet: - displayer.show(c) - result = 0 - elif children: - ui.warn(_("ambiguous next changeset:\n")) - for c in children: - displayer.show(c) - ui.warn(_('explicitly update to one of them\n')) - result = 1 + return _updatetonext(ui, repo, repo[choosedrev], displayer, opts) else: - aspchildren = _aspiringchildren(repo, [repo['.'].rev()]) + aspchildren = evolvecmd._aspiringchildren(repo, [repo['.'].rev()]) if topic: filtered.extend(repo[c] for c in children if repo[c].topic() != topic) @@ -2246,26 +1143,67 @@ msg = _('(%i unstable changesets to be evolved here, ' 'do you want --evolve?)\n') ui.warn(msg % len(aspchildren)) - result = 1 + return 1 elif 1 < len(aspchildren): - ui.warn(_("ambiguous next (unstable) changeset:\n")) - for c in aspchildren: - displayer.show(repo[c]) - ui.warn(_("(run 'hg evolve --rev REV' on one of them)\n")) - return 1 + cheader = _("ambiguous next (unstable) changeset, choose one to" + " evolve and update:") + choosedrev = utility.revselectionprompt(ui, repo, + aspchildren, cheader) + if choosedrev is None: + ui.warn(_("ambiguous next (unstable) changeset:\n")) + for c in aspchildren: + displayer.show(repo[c]) + ui.warn(_("(run 'hg evolve --rev REV' on one of them)\n")) + return 1 + else: + return _nextevolve(ui, repo, repo[choosedrev], opts) else: - cmdutil.bailifchanged(repo) - result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt, - False, lambda: None, category='orphan') - if not result: - ui.status(_('working directory now at %s\n') - % ui.label(str(repo['.']), 'evolve.node')) - return result - return 1 - return result + return _nextevolve(ui, repo, aspchildren[0], opts) finally: lockmod.release(wlock) +def _nextevolve(ui, repo, aspchildren, opts): + """logic for hg next command to evolve and update to an aspiring children""" + + cmdutil.bailifchanged(repo) + evolvestate = state.cmdstate(repo, opts={'command': 'next'}) + result = evolvecmd._solveone(ui, repo, repo[aspchildren], + evolvestate, opts.get('dry_run'), False, + lambda: None, category='orphan') + # making sure a next commit is formed + if result[0] and result[1]: + ui.status(_('working directory now at %s\n') + % ui.label(str(repo['.']), 'evolve.node')) + return 0 + +def _updatetonext(ui, repo, children, displayer, opts): + """ logic for `hg next` command to update to children and move bookmarks if + required """ + bm = repo._activebookmark + shouldmove = opts.get('move_bookmark') and bm is not None + if opts.get('dry_run'): + ui.write(_('hg update %s;\n') % children) + if shouldmove: + ui.write(_('hg bookmark %s -r %s;\n') % (bm, children)) + else: + ret = hg.update(repo, children) + if not ret: + lock = tr = None + try: + lock = repo.lock() + tr = repo.transaction('next') + if shouldmove: + bmchanges = [(bm, children.node())] + compat.bookmarkapplychanges(repo, tr, bmchanges) + else: + bookmarksmod.deactivate(repo) + tr.close() + finally: + lockmod.release(tr, lock) + if not ui.quiet: + displayer.show(children) + return 0 + @eh.wrapcommand('commit') def commitwrapper(orig, ui, repo, *arg, **kwargs): tr = None @@ -2287,7 +1225,7 @@ oldbookmarks.extend(repo.nodebookmarks(old.node())) markers.append((old, (new,))) if markers: - obsolete.createmarkers(repo, markers) + compat.createmarkers(repo, markers, operation="amend") bmchanges = [] for book in oldbookmarks: bmchanges.append((book, new.node())) @@ -2432,47 +1370,6 @@ _helploader)) help.helptable.sort() -def _relocatecommit(repo, orig, commitmsg): - if commitmsg is None: - commitmsg = orig.description() - extra = dict(orig.extra()) - if 'branch' in extra: - del extra['branch'] - extra['rebase_source'] = orig.hex() - - backup = repo.ui.backupconfig('phases', 'new-commit') - try: - targetphase = max(orig.phase(), phases.draft) - repo.ui.setconfig('phases', 'new-commit', targetphase, 'evolve') - # Commit might fail if unresolved files exist - nodenew = repo.commit(text=commitmsg, user=orig.user(), - date=orig.date(), extra=extra) - finally: - repo.ui.restoreconfig(backup) - return nodenew - -def _finalizerelocate(repo, orig, dest, nodenew, tr): - destbookmarks = repo.nodebookmarks(dest.node()) - nodesrc = orig.node() - destphase = repo[nodesrc].phase() - oldbookmarks = repo.nodebookmarks(nodesrc) - bmchanges = [] - - if nodenew is not None: - phases.retractboundary(repo, tr, destphase, [nodenew]) - obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))]) - for book in oldbookmarks: - bmchanges.append((book, nodenew)) - else: - obsolete.createmarkers(repo, [(repo[nodesrc], ())]) - # Behave like rebase, move bookmarks to dest - for book in oldbookmarks: - bmchanges.append((book, dest.node())) - for book in destbookmarks: # restore bookmark that rebase move - bmchanges.append((book, dest.node())) - if bmchanges: - compat.bookmarkapplychanges(repo, tr, bmchanges) - evolvestateversion = 0 @eh.uisetup @@ -2481,32 +1378,13 @@ _("use 'hg evolve --continue' or 'hg update -C .' to abort")) cmdutil.unfinishedstates.append(data) + afterresolved = ('evolvestate', _('hg evolve --continue')) + grabresolved = ('grabstate', _('hg grab --continue')) + cmdutil.afterresolvedstates.append(afterresolved) + cmdutil.afterresolvedstates.append(grabresolved) + @eh.wrapfunction(hg, 'clean') def clean(orig, repo, *args, **kwargs): ret = orig(repo, *args, **kwargs) util.unlinkpath(repo.vfs.join('evolvestate'), ignoremissing=True) return ret - -def _evolvemerge(repo, orig, dest, pctx, keepbranch): - """Used by the evolve function to merge dest on top of pctx. - return the same tuple as merge.graft""" - if repo['.'].rev() != dest.rev(): - merge.update(repo, - dest, - branchmerge=False, - force=True) - if repo._activebookmark: - repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark) - bookmarksmod.deactivate(repo) - if keepbranch: - repo.dirstate.setbranch(orig.branch()) - if util.safehasattr(repo, 'currenttopic'): - # uurrgs - # there no other topic setter yet - if not orig.topic() and repo.vfs.exists('topic'): - repo.vfs.unlink('topic') - else: - with repo.vfs.open('topic', 'w') as f: - f.write(orig.topic()) - - return merge.graft(repo, orig, pctx, ['destination', 'evolving'], True) diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/cmdrewrite.py --- a/hgext3rd/evolve/cmdrewrite.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/cmdrewrite.py Wed Mar 21 16:35:18 2018 +0100 @@ -22,6 +22,7 @@ error, hg, lock as lockmod, + merge, node, obsolete, patch, @@ -34,6 +35,7 @@ from . import ( compat, + state, exthelper, rewriteutil, utility, @@ -72,7 +74,7 @@ """ # N.B. this is extremely similar to setupheaderopts() in mq.py if not opts.get('date') and opts.get('current_date'): - opts['date'] = '%d %d' % util.makedate() + opts['date'] = '%d %d' % compat.makedate() if not opts.get('user') and opts.get('current_user'): opts['user'] = ui.username() @@ -126,13 +128,13 @@ edit = opts.pop('edit', False) log = opts.get('logfile') opts['amend'] = True - if not (edit or opts['message'] or log): - opts['message'] = repo['.'].description() _resolveoptions(ui, opts) _alias, commitcmd = cmdutil.findcmd('commit', commands.table) try: wlock = repo.wlock() lock = repo.lock() + if not (edit or opts['message'] or log): + opts['message'] = repo['.'].description() rewriteutil.precheck(repo, [repo['.'].rev()], action='amend') return commitcmd[0](ui, repo, *pats, **opts) finally: @@ -387,7 +389,8 @@ if opts.get('note'): metadata['note'] = opts['note'] - obsolete.createmarkers(repo, [(old, (repo[newid],))], metadata=metadata) + compat.createmarkers(repo, [(old, (repo[newid],))], metadata=metadata, + operation="uncommit") phases.retractboundary(repo, tr, oldphase, [newid]) if opts.get('revert'): hg.updaterepo(repo, newid, True) @@ -425,7 +428,7 @@ fp.seek(0) newnode = _patchtocommit(ui, repo, old, fp) # creating obs marker temp -> () - obsolete.createmarkers(repo, [(repo[tempnode], ())]) + compat.createmarkers(repo, [(repo[tempnode], ())], operation="uncommit") return newnode def _createtempcommit(ui, repo, old, match): @@ -609,8 +612,14 @@ root.p2().node()], commitopts=commitopts) phases.retractboundary(repo, tr, targetphase, [newid]) - obsolete.createmarkers(repo, [(ctx, (repo[newid],)) - for ctx in allctx], metadata=metadata) + compat.createmarkers(repo, [(ctx, (repo[newid],)) + for ctx in allctx], metadata=metadata, + operation="fold") + # move bookmarks from old nodes to the new one + # XXX: we should make rewriteutil.rewrite() handle such cases + for ctx in allctx: + bmupdater = rewriteutil.bookmarksupdater(repo, ctx.node(), tr) + bmupdater(newid) tr.close() finally: tr.release() @@ -736,9 +745,9 @@ metadata['note'] = opts['note'] phases.retractboundary(repo, tr, targetphase, [newid]) - obsolete.createmarkers(repo, [(ctx, (repo[newid],)) - for ctx in allctx], - metadata=metadata) + compat.createmarkers(repo, [(ctx, (repo[newid],)) + for ctx in allctx], + metadata=metadata, operation="metaedit") else: ui.status(_("nothing changed\n")) tr.close() @@ -764,7 +773,7 @@ date = opts.get('date') user = opts.get('user') if date: - metadata['date'] = '%i %i' % util.parsedate(date) + metadata['date'] = '%i %i' % compat.parsedate(date) if user: metadata['user'] = user return metadata @@ -930,12 +939,14 @@ metadata['note'] = opts['note'] # create markers - obsolete.createmarkers(repo, relations, metadata=metadata) + compat.createmarkers(repo, relations, metadata=metadata, + operation="prune") # informs that changeset have been pruned ui.status(_('%i changesets pruned\n') % len(precs)) - for ctx in repo.unfiltered().set('bookmark() and %ld', precs): + precrevs = (precursor.rev() for precursor in precs) + for ctx in repo.unfiltered().set('bookmark() and %ld', precrevs): # used to be: # # ldest = list(repo.set('max((::%d) - obsolete())', ctx)) @@ -986,11 +997,11 @@ try: wlock = repo.wlock() lock = repo.lock() - rev = scmutil.revsingle(repo, revarg[0]) + ctx = scmutil.revsingle(repo, revarg[0]) + rev = ctx.rev() cmdutil.bailifchanged(repo) rewriteutil.precheck(repo, [rev], action='split') tr = repo.transaction('split') - ctx = repo[rev] if len(ctx.parents()) > 1: raise error.Abort(_("cannot split merge commits")) @@ -1042,8 +1053,8 @@ metadata = {} if opts.get('note'): metadata['note'] = opts['note'] - obsolete.createmarkers(repo, [(repo[rev], newcommits)], - metadata=metadata) + compat.createmarkers(repo, [(repo[rev], newcommits)], + metadata=metadata, operation="split") tr.close() finally: # Restore the old branch @@ -1081,7 +1092,7 @@ if not duplicate: rewriteutil.precheck(repo, revs, touch) tmpl = utility.shorttemplate - displayer = cmdutil.show_changeset(ui, repo, {'template': tmpl}) + displayer = compat.changesetdisplayer(ui, repo, {'template': tmpl}) wlock = lock = tr = None try: wlock = repo.wlock() @@ -1134,8 +1145,8 @@ metadata = {} if opts.get('note'): metadata['note'] = opts['note'] - obsolete.createmarkers(repo, [(ctx, (repo[new],))], - metadata=metadata) + compat.createmarkers(repo, [(ctx, (repo[new],))], + metadata=metadata, operation="touch") phases.retractboundary(repo, tr, ctx.phase(), [new]) if ctx in repo[None].parents(): with repo.dirstate.parentchange(): @@ -1143,3 +1154,94 @@ tr.close() finally: lockmod.release(tr, lock, wlock) + +@eh.command( + 'grab', + [('r', 'rev', '', 'revision to grab'), + ('c', 'continue', False, 'continue interrupted grab'), + ('a', 'abort', False, 'abort interrupted grab'), + ], + _('[-r] rev')) +def grab(ui, repo, *revs, **opts): + """grabs a commit, move it on the top of working directory parent and + updates to it.""" + + cont = opts.get('continue') + abort = opts.get('abort') + + if cont and abort: + raise error.Abort(_("cannot specify both --continue and --abort")) + + revs = list(revs) + if opts.get('rev'): + revs.append(opts['rev']) + + with repo.wlock(), repo.lock(), repo.transaction('grab'): + grabstate = state.cmdstate(repo, path='grabstate') + pctx = repo['.'] + + if not cont and not abort: + cmdutil.bailifchanged(repo) + revs = scmutil.revrange(repo, revs) + if len(revs) > 1: + raise error.Abort(_("specify just one revision")) + elif not revs: + raise error.Abort(_("empty revision set")) + + origctx = repo[revs.first()] + + if origctx in pctx.ancestors() or origctx.node() == pctx.node(): + raise error.Abort(_("cannot grab an ancestor revision")) + + rewriteutil.precheck(repo, [origctx.rev()], 'grab') + + ui.status(_('grabbing %d:%s "%s"\n') % + (origctx.rev(), origctx, + origctx.description().split("\n", 1)[0])) + stats = merge.graft(repo, origctx, origctx.p1(), ['local', + 'destination']) + if stats[3]: + grabstate.addopts({'orignode': origctx.node(), + 'oldpctx': pctx.node()}) + grabstate.save() + raise error.InterventionRequired(_("unresolved merge conflicts" + " (see hg help resolve)")) + + elif abort: + if not grabstate: + raise error.Abort(_("no interrupted grab state exists")) + grabstate.load() + pctxnode = grabstate['oldpctx'] + ui.status(_("aborting grab, updating to %s\n") % + node.hex(pctxnode)[:12]) + hg.updaterepo(repo, pctxnode, True) + return 0 + + else: + if revs: + raise error.Abort(_("cannot specify both --continue and " + "revision")) + if not grabstate: + raise error.Abort(_("no interrupted grab state exists")) + + grabstate.load() + orignode = grabstate['orignode'] + origctx = repo[orignode] + + overrides = {('phases', 'new-commit'): origctx.phase()} + with repo.ui.configoverride(overrides, 'grab'): + newnode = repo.commit(text=origctx.description(), + user=origctx.user(), + date=origctx.date(), extra=origctx.extra()) + + if grabstate: + grabstate.delete() + newctx = repo[newnode] if newnode else pctx + compat.createmarkers(repo, [(origctx, (newctx,))], operation="grab") + + if newnode is None: + ui.warn(_("note: grab of %d:%s created no changes to commit\n") % + (origctx.rev(), origctx)) + return 0 + + return 0 diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/compat.py --- a/hgext3rd/evolve/compat.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/compat.py Wed Mar 21 16:35:18 2018 +0100 @@ -6,14 +6,21 @@ Compatibility module """ +import inspect + +import functools + from mercurial import ( copies, context, hg, + mdiff, obsolete, revset, util, + wireproto, ) +from mercurial.hgweb import hgweb_mod # hg < 4.2 compat try: @@ -28,12 +35,35 @@ except ImportError: obsutil = None +# hg < 4.6 compat (c8e2d6ed1f9e) +try: + from mercurial import logcmdutil + changesetdisplayer = logcmdutil.changesetdisplayer + changesetprinter = logcmdutil.changesetprinter + displaygraph = logcmdutil.displaygraph +except (AttributeError, ImportError): + from mercurial import cmdutil + changesetdisplayer = cmdutil.show_changeset + changesetprinter = cmdutil.changeset_printer + displaygraph = cmdutil.displaygraph + from . import ( exthelper, ) eh = exthelper.exthelper() +# Wrap obsolete.creatmarkers and make it accept but ignore "operation" argument +# for hg < 4.3 +createmarkers = obsolete.createmarkers +originalcreatemarkers = createmarkers +while isinstance(originalcreatemarkers, functools.partial): + originalcreatemarkers = originalcreatemarkers.func +if originalcreatemarkers.__code__.co_argcount < 6: + def createmarkers(repo, relations, flag=0, date=None, metadata=None, + operation=None): + return obsolete.createmarkers(repo, relations, flag, date, metadata) + if not util.safehasattr(hg, '_copycache'): # exact copy of relevantmarkers as in Mercurial-176d1a0ce385 # this fixes relevant markers computation for version < hg-4.3 @@ -226,3 +256,40 @@ cachevfs = vfsmod.vfs(repo.vfs.join('cache')) cachevfs.createmode = repo.store.createmode return cachevfs + +def strdiff(a, b, fn1, fn2): + """ A version of mdiff.unidiff for comparing two strings + """ + args = [a, '', b, '', fn1, fn2] + + # hg < 4.6 compat 8b6dd3922f70 + argspec = inspect.getargspec(mdiff.unidiff) + + if 'binary' in argspec.args: + args.append(False) + + return mdiff.unidiff(*args) + +# date related + +try: + import mercurial.utils.dateutil + makedate = mercurial.utils.dateutil.makedate + parsedate = mercurial.utils.dateutil.parsedate +except ImportError as e: + import mercurial.util + makedate = mercurial.util.makedate + parsedate = mercurial.util.parsedate + +def wireprotocommand(exthelper, name, args='', permission='pull'): + if 3 <= len(wireproto.wireprotocommand.func_defaults): + return wireproto.wireprotocommand(name, args, permission=permission) + + else: + # <= hg-4.5 permission must be registered in dictionnary + def decorator(func): + @eh.extsetup + def install(ui): + hgweb_mod.perms[name] = permission + wireproto.commands[name] = (func, args) + return decorator diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/depthcache.py --- a/hgext3rd/evolve/depthcache.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/depthcache.py Wed Mar 21 16:35:18 2018 +0100 @@ -87,11 +87,11 @@ if util.safehasattr(repo, 'updatecaches'): @localrepo.unfilteredmethod - def updatecaches(self, tr=None): + def updatecaches(self, tr=None, **kwargs): if utility.shouldwarmcache(self, tr): self.depthcache.update(self) self.depthcache.save(self) - super(depthcacherepo, self).updatecaches(tr) + super(depthcacherepo, self).updatecaches(tr, **kwargs) else: def transaction(self, *args, **kwargs): diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/evolvecmd.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/evolvecmd.py Wed Mar 21 16:35:18 2018 +0100 @@ -0,0 +1,1297 @@ +# Copyright 2011 Peter Arrenbrecht +# Logilab SA +# Pierre-Yves David +# Patrick Mezard +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +"""logic related to hg evolve command""" + +import collections +import re + +from mercurial import ( + bookmarks as bookmarksmod, + cmdutil, + commands, + context, + copies, + error, + hg, + lock as lockmod, + merge, + node, + obsolete, + phases, + scmutil, + util, +) + +from mercurial.i18n import _ + +from . import ( + cmdrewrite, + compat, + exthelper, + rewriteutil, + state, + utility, +) + +TROUBLES = compat.TROUBLES +shorttemplate = utility.shorttemplate +_bookmarksupdater = rewriteutil.bookmarksupdater +sha1re = re.compile(r'\b[0-9a-f]{6,40}\b') + +eh = exthelper.exthelper() +_bookmarksupdater = rewriteutil.bookmarksupdater +mergetoolopts = commands.mergetoolopts + +def _solveone(ui, repo, ctx, evolvestate, dryrun, confirm, + progresscb, category): + """Resolve the troubles affecting one revision + + returns a tuple (bool, newnode) where, + bool: a boolean value indicating whether the instability was solved + newnode: if bool is True, then the newnode of the resultant commit + formed. newnode can be node, when resolution led to no new + commit. If bool is False, this is ''. + """ + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction("evolve") + if 'orphan' == category: + result = _solveunstable(ui, repo, ctx, evolvestate, + dryrun, confirm, progresscb) + elif 'phasedivergent' == category: + result = _solvebumped(ui, repo, ctx, evolvestate, + dryrun, confirm, progresscb) + elif 'contentdivergent' == category: + result = _solvedivergent(ui, repo, ctx, evolvestate, + dryrun, confirm, progresscb) + else: + assert False, "unknown trouble category: %s" % (category) + tr.close() + return result + finally: + lockmod.release(tr, lock, wlock) + +def _solveunstable(ui, repo, orig, evolvestate, dryrun=False, confirm=False, + progresscb=None): + """ Tries to stabilize the changeset orig which is orphan. + + returns a tuple (bool, newnode) where, + bool: a boolean value indicating whether the instability was solved + newnode: if bool is True, then the newnode of the resultant commit + formed. newnode can be node, when resolution led to no new + commit. If bool is False, this is ''. + """ + pctx = orig.p1() + keepbranch = orig.p1().branch() != orig.branch() + if len(orig.parents()) == 2: + p1obs = orig.p1().obsolete() + p2obs = orig.p2().obsolete() + if not p1obs and p2obs: + pctx = orig.p2() # second parent is obsolete ? + keepbranch = orig.p2().branch() != orig.branch() + elif not p2obs and p1obs: + pass + else: + # store that we are resolving an orphan merge with both parents + # obsolete and proceed with first parent + evolvestate['orphanmerge'] = True + # we should process the second parent first, so that in case of + # no-conflicts the first parent is processed later and preserved as + # first parent + pctx = orig.p2() + keepbranch = orig.p2().branch() != orig.branch() + + if not pctx.obsolete(): + ui.warn(_("cannot solve instability of %s, skipping\n") % orig) + return (False, '') + obs = pctx + newer = compat.successorssets(repo, obs.node()) + # search of a parent which is not killed + while not newer or newer == [()]: + ui.debug("stabilize target %s is plain dead," + " trying to stabilize on its parent\n" % + obs) + obs = obs.parents()[0] + newer = compat.successorssets(repo, obs.node()) + if len(newer) > 1: + msg = _("skipping %s: divergent rewriting. can't choose " + "destination\n") % obs + ui.write_err(msg) + return (False, '') + targets = newer[0] + assert targets + if len(targets) > 1: + # split target, figure out which one to pick, are they all in line? + targetrevs = [repo[r].rev() for r in targets] + roots = repo.revs('roots(%ld)', targetrevs) + heads = repo.revs('heads(%ld)', targetrevs) + if len(roots) > 1 or len(heads) > 1: + cheader = _("ancestor '%s' split over multiple topological" + " branches.\nchoose an evolve destination:") % orig + selectedrev = utility.revselectionprompt(ui, repo, list(heads), + cheader) + if selectedrev is None: + msg = _("could not solve instability, " + "ambiguous destination: " + "parent split across two branches\n") + ui.write_err(msg) + return (False, '') + target = repo[selectedrev] + else: + target = repo[heads.first()] + else: + target = targets[0] + displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate}) + target = repo[target] + if not ui.quiet or confirm: + repo.ui.write(_('move:'), label='evolve.operation') + displayer.show(orig) + repo.ui.write(_('atop:')) + displayer.show(target) + if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': + raise error.Abort(_('evolve aborted by user')) + if progresscb: + progresscb() + todo = 'hg rebase -r %s -d %s\n' % (orig, target) + if dryrun: + repo.ui.write(todo) + return (False, '') + else: + repo.ui.note(todo) + if progresscb: + progresscb() + try: + newid = relocate(repo, orig, target, pctx, keepbranch) + return (True, newid) + except MergeFailure: + ops = {'current': orig.node()} + evolvestate.addopts(ops) + evolvestate.save() + repo.ui.write_err(_('evolve failed!\n')) + repo.ui.write_err( + _("fix conflict and run 'hg evolve --continue'" + " or use 'hg update -C .' to abort\n")) + raise + +def _solvebumped(ui, repo, bumped, evolvestate, dryrun=False, confirm=False, + progresscb=None): + """Stabilize a bumped changeset + + returns a tuple (bool, newnode) where, + bool: a boolean value indicating whether the instability was solved + newnode: if bool is True, then the newnode of the resultant commit + formed. newnode can be node, when resolution led to no new + commit. If bool is False, this is ''. + """ + repo = repo.unfiltered() + bumped = repo[bumped.rev()] + # For now we deny bumped merge + if len(bumped.parents()) > 1: + msg = _('skipping %s : we do not handle merge yet\n') % bumped + ui.write_err(msg) + return (False, '') + prec = repo.set('last(allprecursors(%d) and public())', bumped.rev()).next() + # For now we deny target merge + if len(prec.parents()) > 1: + msg = _('skipping: %s: public version is a merge, ' + 'this is not handled yet\n') % prec + ui.write_err(msg) + return (False, '') + + displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate}) + if not ui.quiet or confirm: + repo.ui.write(_('recreate:'), label='evolve.operation') + displayer.show(bumped) + repo.ui.write(_('atop:')) + displayer.show(prec) + if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': + raise error.Abort(_('evolve aborted by user')) + if dryrun: + todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1()) + repo.ui.write(todo) + repo.ui.write(('hg update %s;\n' % prec)) + repo.ui.write(('hg revert --all --rev %s;\n' % bumped)) + repo.ui.write(('hg commit --msg "%s update to %s"\n' % + (TROUBLES['PHASEDIVERGENT'], bumped))) + return (False, '') + if progresscb: + progresscb() + newid = tmpctx = None + tmpctx = bumped + # Basic check for common parent. Far too complicated and fragile + tr = repo.currenttransaction() + assert tr is not None + bmupdate = _bookmarksupdater(repo, bumped.node(), tr) + if not list(repo.set('parents(%d) and parents(%d)', bumped.rev(), prec.rev())): + # Need to rebase the changeset at the right place + repo.ui.status( + _('rebasing to destination parent: %s\n') % prec.p1()) + try: + tmpid = relocate(repo, bumped, prec.p1()) + if tmpid is not None: + tmpctx = repo[tmpid] + compat.createmarkers(repo, [(bumped, (tmpctx,))], + operation='evolve') + except MergeFailure: + repo.vfs.write('graftstate', bumped.hex() + '\n') + repo.ui.write_err(_('evolution failed!\n')) + msg = _("fix conflict and run 'hg evolve --continue'\n") + repo.ui.write_err(msg) + raise + # Create the new commit context + repo.ui.status(_('computing new diff\n')) + files = set() + copied = copies.pathcopies(prec, bumped) + precmanifest = prec.manifest().copy() + # 3.3.2 needs a list. + # future 3.4 don't detect the size change during iteration + # this is fishy + for key, val in list(bumped.manifest().iteritems()): + precvalue = precmanifest.get(key, None) + if precvalue is not None: + del precmanifest[key] + if precvalue != val: + files.add(key) + files.update(precmanifest) # add missing files + # commit it + if files: # something to commit! + def filectxfn(repo, ctx, path): + if path in bumped: + fctx = bumped[path] + flags = fctx.flags() + mctx = compat.memfilectx(repo, ctx, fctx, flags, copied, path) + return mctx + return None + text = '%s update to %s:\n\n' % (TROUBLES['PHASEDIVERGENT'], prec) + text += bumped.description() + + new = context.memctx(repo, + parents=[prec.node(), node.nullid], + text=text, + files=files, + filectxfn=filectxfn, + user=bumped.user(), + date=bumped.date(), + extra=bumped.extra()) + + newid = repo.commitctx(new) + if newid is None: + compat.createmarkers(repo, [(tmpctx, ())], operation='evolve') + newid = prec.node() + else: + phases.retractboundary(repo, tr, bumped.phase(), [newid]) + compat.createmarkers(repo, [(tmpctx, (repo[newid],))], + flag=obsolete.bumpedfix, operation='evolve') + bmupdate(newid) + repo.ui.status(_('committed as %s\n') % node.short(newid)) + # reroute the working copy parent to the new changeset + with repo.dirstate.parentchange(): + repo.dirstate.setparents(newid, node.nullid) + return (True, newid) + +def _solvedivergent(ui, repo, divergent, evolvestate, dryrun=False, + confirm=False, progresscb=None): + """tries to solve content-divergence of a changeset + + returns a tuple (bool, newnode) where, + bool: a boolean value indicating whether the instability was solved + newnode: if bool is True, then the newnode of the resultant commit + formed. newnode can be node, when resolution led to no new + commit. If bool is False, this is ''. + """ + repo = repo.unfiltered() + divergent = repo[divergent.rev()] + base, others = divergentdata(divergent) + if len(others) > 1: + othersstr = "[%s]" % (','.join([str(i) for i in others])) + msg = _("skipping %d:%s with a changeset that got split" + " into multiple ones:\n" + "|[%s]\n" + "| This is not handled by automatic evolution yet\n" + "| You have to fallback to manual handling with commands " + "such as:\n" + "| - hg touch -D\n" + "| - hg prune\n" + "| \n" + "| You should contact your local evolution Guru for help.\n" + ) % (divergent, TROUBLES['CONTENTDIVERGENT'], othersstr) + ui.write_err(msg) + return (False, '') + other = others[0] + if len(other.parents()) > 1: + msg = _("skipping %s: %s changeset can't be " + "a merge (yet)\n") % (divergent, TROUBLES['CONTENTDIVERGENT']) + ui.write_err(msg) + hint = _("You have to fallback to solving this by hand...\n" + "| This probably means redoing the merge and using \n" + "| `hg prune` to kill older version.\n") + ui.write_err(hint) + return (False, '') + if other.p1() not in divergent.parents(): + msg = _("skipping %s: have a different parent than %s " + "(not handled yet)\n") % (divergent, other) + hint = _("| %(d)s, %(o)s are not based on the same changeset.\n" + "| With the current state of its implementation, \n" + "| evolve does not work in that case.\n" + "| rebase one of them next to the other and run \n" + "| this command again.\n" + "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n" + "| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n" + ) % {'d': divergent, 'o': other} + ui.write_err(msg) + ui.write_err(hint) + return (False, '') + + displayer = compat.changesetdisplayer(ui, repo, {'template': shorttemplate}) + if not ui.quiet or confirm: + ui.write(_('merge:'), label='evolve.operation') + displayer.show(divergent) + ui.write(_('with: ')) + displayer.show(other) + ui.write(_('base: ')) + displayer.show(base) + if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y': + raise error.Abort(_('evolve aborted by user')) + if dryrun: + ui.write(('hg update -c %s &&\n' % divergent)) + ui.write(('hg merge %s &&\n' % other)) + ui.write(('hg commit -m "auto merge resolving conflict between ' + '%s and %s"&&\n' % (divergent, other))) + ui.write(('hg up -C %s &&\n' % base)) + ui.write(('hg revert --all --rev tip &&\n')) + ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n' + % divergent)) + return (False, '') + if divergent not in repo[None].parents(): + repo.ui.status(_('updating to "local" conflict\n')) + hg.update(repo, divergent.rev()) + repo.ui.note(_('merging %s changeset\n') % TROUBLES['CONTENTDIVERGENT']) + if progresscb: + progresscb() + stats = merge.update(repo, + other.node(), + branchmerge=True, + force=False, + ancestor=base.node(), + mergeancestor=True) + hg._showstats(repo, stats) + if stats[3]: + repo.ui.status(_("use 'hg resolve' to retry unresolved file merges " + "or 'hg update -C .' to abort\n")) + if stats[3] > 0: + raise error.Abort('merge conflict between several amendments ' + '(this is not automated yet)', + hint="""/!\ You can try: +/!\ * manual merge + resolve => new cset X +/!\ * hg up to the parent of the amended changeset (which are named W and Z) +/!\ * hg revert --all -r X +/!\ * hg ci -m "same message as the amended changeset" => new cset Y +/!\ * hg prune -n Y W Z +""") + if progresscb: + progresscb() + emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit') + tr = repo.currenttransaction() + assert tr is not None + try: + repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve') + with repo.dirstate.parentchange(): + repo.dirstate.setparents(divergent.node(), node.nullid) + oldlen = len(repo) + cmdrewrite.amend(ui, repo, message='', logfile='') + if oldlen == len(repo): + new = divergent + # no changes + else: + new = repo['.'] + compat.createmarkers(repo, [(other, (new,))], operation='evolve') + phases.retractboundary(repo, tr, other.phase(), [new.node()]) + return (True, new.node()) + finally: + repo.ui.restoreconfig(emtpycommitallowed) + +class MergeFailure(error.Abort): + pass + +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 = utility.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 relocate(repo, orig, dest, pctx=None, keepbranch=False): + """rewrites the orig rev on dest rev + + returns the node of new commit which is formed + """ + if orig.rev() == dest.rev(): + raise error.Abort(_('tried to relocate a node on top of itself'), + hint=_("This shouldn't happen. If you still " + "need to move changesets, please do so " + "manually with nothing to rebase - working " + "directory parent is also destination")) + + if pctx is None: + if len(orig.parents()) == 2: + raise error.Abort(_("tried to relocate a merge commit without " + "specifying which parent should be moved"), + hint=_("Specify the parent by passing in pctx")) + pctx = orig.p1() + + commitmsg = orig.description() + + cache = {} + sha1s = re.findall(sha1re, commitmsg) + unfi = repo.unfiltered() + for sha1 in sha1s: + ctx = None + try: + ctx = unfi[sha1] + except error.RepoLookupError: + continue + + if not ctx.obsolete(): + continue + + successors = compat.successorssets(repo, ctx.node(), cache) + + # We can't make any assumptions about how to update the hash if the + # cset in question was split or diverged. + if len(successors) == 1 and len(successors[0]) == 1: + newsha1 = node.hex(successors[0][0]) + commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)]) + else: + repo.ui.note(_('The stale commit message reference to %s could ' + 'not be updated\n') % sha1) + + tr = repo.currenttransaction() + assert tr is not None + try: + r = _evolvemerge(repo, orig, dest, pctx, keepbranch) + if r[-1]: # some conflict + raise error.Abort(_('unresolved merge conflicts ' + '(see hg help resolve)')) + nodenew = _relocatecommit(repo, orig, commitmsg) + except error.Abort as exc: + with repo.dirstate.parentchange(): + repo.setparents(repo['.'].node(), node.nullid) + repo.dirstate.write(tr) + # fix up dirstate for copies and renames + compat.duplicatecopies(repo, repo[None], dest.rev(), orig.p1().rev()) + + class LocalMergeFailure(MergeFailure, exc.__class__): + pass + exc.__class__ = LocalMergeFailure + tr.close() # to keep changes in this transaction (e.g. dirstate) + raise + _finalizerelocate(repo, orig, dest, nodenew, tr) + return nodenew + +def _relocatecommit(repo, orig, commitmsg): + if commitmsg is None: + commitmsg = orig.description() + extra = dict(orig.extra()) + if 'branch' in extra: + del extra['branch'] + extra['rebase_source'] = orig.hex() + + backup = repo.ui.backupconfig('phases', 'new-commit') + try: + targetphase = max(orig.phase(), phases.draft) + repo.ui.setconfig('phases', 'new-commit', targetphase, 'evolve') + # Commit might fail if unresolved files exist + nodenew = repo.commit(text=commitmsg, user=orig.user(), + date=orig.date(), extra=extra) + finally: + repo.ui.restoreconfig(backup) + return nodenew + +def _finalizerelocate(repo, orig, dest, nodenew, tr): + destbookmarks = repo.nodebookmarks(dest.node()) + nodesrc = orig.node() + oldbookmarks = repo.nodebookmarks(nodesrc) + bmchanges = [] + + if nodenew is not None: + compat.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))], + operation='evolve') + for book in oldbookmarks: + bmchanges.append((book, nodenew)) + else: + compat.createmarkers(repo, [(repo[nodesrc], ())], operation='evolve') + # Behave like rebase, move bookmarks to dest + for book in oldbookmarks: + bmchanges.append((book, dest.node())) + for book in destbookmarks: # restore bookmark that rebase move + bmchanges.append((book, dest.node())) + if bmchanges: + compat.bookmarkapplychanges(repo, tr, bmchanges) + +def _evolvemerge(repo, orig, dest, pctx, keepbranch): + """Used by the evolve function to merge dest on top of pctx. + return the same tuple as merge.graft""" + if repo['.'].rev() != dest.rev(): + merge.update(repo, + dest, + branchmerge=False, + force=True) + if repo._activebookmark: + repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark) + bookmarksmod.deactivate(repo) + if keepbranch: + repo.dirstate.setbranch(orig.branch()) + if util.safehasattr(repo, 'currenttopic'): + # uurrgs + # there no other topic setter yet + if not orig.topic() and repo.vfs.exists('topic'): + repo.vfs.unlink('topic') + else: + with repo.vfs.open('topic', 'w') as f: + f.write(orig.topic()) + + return merge.graft(repo, orig, pctx, ['destination', 'evolving'], True) + +instabilities_map = { + 'contentdivergent': "content-divergent", + 'phasedivergent': "phase-divergent" +} + +def _selectrevs(repo, allopt, revopt, anyopt, targetcat): + """select troubles in repo matching according to given options""" + revs = set() + if allopt or revopt: + revs = repo.revs("%s()" % targetcat) + if revopt: + revs = scmutil.revrange(repo, revopt) & revs + elif not anyopt: + topic = getattr(repo, 'currenttopic', '') + if topic: + revs = repo.revs('topic(%s)', topic) & revs + elif targetcat == 'orphan': + revs = _aspiringdescendant(repo, + repo.revs('(.::) - obsolete()::')) + revs = set(revs) + if targetcat == 'contentdivergent': + # Pick one divergent per group of divergents + revs = _dedupedivergents(repo, revs) + elif anyopt: + revs = repo.revs('first(%s())' % (targetcat)) + elif targetcat == 'orphan': + revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::'))) + if 1 < len(revs): + msg = "multiple evolve candidates" + hint = (_("select one of %s with --rev") + % ', '.join([str(repo[r]) for r in sorted(revs)])) + raise error.Abort(msg, hint=hint) + elif instabilities_map.get(targetcat, targetcat) in repo['.'].instabilities(): + revs = set([repo['.'].rev()]) + return revs + +def _dedupedivergents(repo, revs): + """Dedupe the divergents revs in revs to get one from each group with the + lowest revision numbers + """ + repo = repo.unfiltered() + res = set() + # To not reevaluate divergents of the same group once one is encountered + discarded = set() + for rev in revs: + if rev in discarded: + continue + divergent = repo[rev] + base, others = divergentdata(divergent) + othersrevs = [o.rev() for o in others] + res.add(min([divergent.rev()] + othersrevs)) + discarded.update(othersrevs) + return res + +def divergentdata(ctx): + """return base, other part of a conflict + + This only return the first one. + + XXX this woobly function won't survive XXX + """ + repo = ctx._repo.unfiltered() + for base in repo.set('reverse(allprecursors(%d))', ctx.rev()): + newer = compat.successorssets(ctx._repo, base.node()) + # drop filter and solution including the original ctx + newer = [n for n in newer if n and ctx.node() not in n] + if newer: + return base, tuple(ctx._repo[o] for o in newer[0]) + raise error.Abort(_("base of divergent changeset %s not found") % ctx, + hint=_('this case is not yet handled')) + +def _aspiringdescendant(repo, revs): + """Return a list of changectx which can be stabilized on top of pctx or + one of its descendants recursively. Empty list if none can be found.""" + target = set(revs) + result = set(target) + paths = collections.defaultdict(set) + for r in repo.revs('orphan() - %ld', revs): + for d in _possibledestination(repo, r): + paths[d].add(r) + + result = set(target) + tovisit = list(revs) + while tovisit: + base = tovisit.pop() + for unstable in paths[base]: + if unstable not in result: + tovisit.append(unstable) + result.add(unstable) + return sorted(result - target) + +def _aspiringchildren(repo, revs): + """Return a list of changectx which can be stabilized on top of pctx or + one of its descendants. Empty list if none can be found.""" + target = set(revs) + result = [] + for r in repo.revs('orphan() - %ld', revs): + dest = _possibledestination(repo, r) + if target & dest: + result.append(r) + return result + +def _possibledestination(repo, rev): + """return all changesets that may be a new parent for REV""" + tonode = repo.changelog.node + parents = repo.changelog.parentrevs + torev = repo.changelog.rev + dest = set() + tovisit = list(parents(rev)) + while tovisit: + r = tovisit.pop() + succsets = compat.successorssets(repo, tonode(r)) + if not succsets: + tovisit.extend(parents(r)) + else: + # We should probably pick only one destination from split + # (case where '1 < len(ss)'), This could be the currently tipmost + # but logic is less clear when result of the split are now on + # multiple branches. + for ss in succsets: + for n in ss: + dest.add(torev(n)) + return dest + +def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat): + """Used by the evolve function to display an error message when + no troubles can be resolved""" + troublecategories = ['phasedivergent', 'contentdivergent', 'orphan'] + unselectedcategories = [c for c in troublecategories if c != targetcat] + msg = None + hint = None + + troubled = { + "orphan": repo.revs("orphan()"), + "contentdivergent": repo.revs("contentdivergent()"), + "phasedivergent": repo.revs("phasedivergent()"), + "all": repo.revs("troubled()"), + } + + hintmap = { + 'phasedivergent': _("do you want to use --phase-divergent"), + 'phasedivergent+contentdivergent': _("do you want to use " + "--phase-divergent or" + " --content-divergent"), + 'phasedivergent+orphan': _("do you want to use --phase-divergent" + " or --orphan"), + 'contentdivergent': _("do you want to use --content-divergent"), + 'contentdivergent+orphan': _("do you want to use --content-divergent" + " or --orphan"), + 'orphan': _("do you want to use --orphan"), + 'any+phasedivergent': _("do you want to use --any (or --rev) and" + " --phase-divergent"), + 'any+phasedivergent+contentdivergent': _("do you want to use --any" + " (or --rev) and" + " --phase-divergent or" + " --content-divergent"), + 'any+phasedivergent+orphan': _("do you want to use --any (or --rev)" + " and --phase-divergent or --orphan"), + 'any+contentdivergent': _("do you want to use --any (or --rev) and" + " --content-divergent"), + 'any+contentdivergent+orphan': _("do you want to use --any (or --rev)" + " and --content-divergent or " + "--orphan"), + 'any+orphan': _("do you want to use --any (or --rev)" + "and --orphan"), + } + + if revopt: + revs = scmutil.revrange(repo, revopt) + if not revs: + msg = _("set of specified revisions is empty") + else: + msg = _("no %s changesets in specified revisions") % targetcat + othertroubles = [] + for cat in unselectedcategories: + if revs & troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['+'.join(othertroubles)] + + elif anyopt: + msg = _("no %s changesets to evolve") % targetcat + othertroubles = [] + for cat in unselectedcategories: + if troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['+'.join(othertroubles)] + + else: + # evolve without any option = relative to the current wdir + if targetcat == 'orphan': + msg = _("nothing to evolve on current working copy parent") + else: + msg = _("current working copy parent is not %s") % targetcat + + p1 = repo['.'].rev() + othertroubles = [] + for cat in unselectedcategories: + if p1 in troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['+'.join(othertroubles)] + else: + length = len(troubled[targetcat]) + if length: + hint = _("%d other %s in the repository, do you want --any " + "or --rev") % (length, targetcat) + else: + othertroubles = [] + for cat in unselectedcategories: + if troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['any+' + ('+'.join(othertroubles))] + else: + msg = _("no troubled changesets") + + assert msg is not None + ui.write_err("%s\n" % msg) + if hint: + ui.write_err("(%s)\n" % hint) + return 2 + else: + return 1 + +def _preparelistctxs(items, condition): + return [item.hex() for item in items if condition(item)] + +def _formatctx(fm, ctx): + fm.data(node=ctx.hex()) + fm.data(desc=ctx.description()) + fm.data(date=ctx.date()) + fm.data(user=ctx.user()) + +def listtroubles(ui, repo, troublecategories, **opts): + """Print all the troubles for the repo (or given revset)""" + troublecategories = troublecategories or ['contentdivergent', 'orphan', 'phasedivergent'] + showunstable = 'orphan' in troublecategories + showbumped = 'phasedivergent' in troublecategories + showdivergent = 'contentdivergent' in troublecategories + + revs = repo.revs('+'.join("%s()" % t for t in troublecategories)) + if opts.get('rev'): + revs = scmutil.revrange(repo, opts.get('rev')) + + fm = ui.formatter('evolvelist', opts) + for rev in revs: + ctx = repo[rev] + unpars = _preparelistctxs(ctx.parents(), lambda p: p.orphan()) + obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete()) + imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()), + lambda p: not p.mutable()) + dsets = divergentsets(repo, ctx) + + fm.startitem() + # plain formatter section + hashlen, desclen = 12, 60 + desc = ctx.description() + if desc: + desc = desc.splitlines()[0] + desc = (desc[:desclen] + '...') if len(desc) > desclen else desc + fm.plain('%s: ' % ctx.hex()[:hashlen]) + fm.plain('%s\n' % desc) + fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr()) + + for unpar in unpars if showunstable else []: + fm.plain(' %s: %s (%s parent)\n' % (TROUBLES['ORPHAN'], + unpar[:hashlen], + TROUBLES['ORPHAN'])) + for obspar in obspars if showunstable else []: + fm.plain(' %s: %s (obsolete parent)\n' % (TROUBLES['ORPHAN'], + obspar[:hashlen])) + for imprec in imprecs if showbumped else []: + fm.plain(' %s: %s (immutable precursor)\n' % + (TROUBLES['PHASEDIVERGENT'], imprec[:hashlen])) + + if dsets and showdivergent: + for dset in dsets: + fm.plain(' %s: ' % TROUBLES['CONTENTDIVERGENT']) + first = True + for n in dset['divergentnodes']: + t = "%s (%s)" if first else " %s (%s)" + first = False + fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr())) + comprec = node.hex(dset['commonprecursor'])[:hashlen] + fm.plain(" (precursor %s)\n" % comprec) + fm.plain("\n") + + # templater-friendly section + _formatctx(fm, ctx) + troubles = [] + for unpar in unpars: + troubles.append({'troubletype': TROUBLES['ORPHAN'], + 'sourcenode': unpar, 'sourcetype': 'orphanparent'}) + for obspar in obspars: + troubles.append({'troubletype': TROUBLES['ORPHAN'], + 'sourcenode': obspar, + 'sourcetype': 'obsoleteparent'}) + for imprec in imprecs: + troubles.append({'troubletype': TROUBLES['PHASEDIVERGENT'], + 'sourcenode': imprec, + 'sourcetype': 'immutableprecursor'}) + for dset in dsets: + divnodes = [{'node': node.hex(n), + 'phase': repo[n].phasestr(), + } for n in dset['divergentnodes']] + troubles.append({'troubletype': TROUBLES['CONTENTDIVERGENT'], + 'commonprecursor': node.hex(dset['commonprecursor']), + 'divergentnodes': divnodes}) + fm.data(troubles=troubles) + + fm.end() + +def _checkevolveopts(repo, opts): + """ check the options passed to `hg evolve` and warn for deprecation warning + if any """ + + if opts['continue']: + if opts['any']: + raise error.Abort(_('cannot specify both "--any" and "--continue"')) + if opts['all']: + raise error.Abort(_('cannot specify both "--all" and "--continue"')) + if opts['rev']: + raise error.Abort(_('cannot specify both "--rev" and "--continue"')) + if opts['stop']: + raise error.Abort(_('cannot specify both "--stop" and' + ' "--continue"')) + + if opts['stop']: + if opts['any']: + raise error.Abort(_('cannot specify both "--any" and "--stop"')) + if opts['all']: + raise error.Abort(_('cannot specify both "--all" and "--stop"')) + if opts['rev']: + raise error.Abort(_('cannot specify both "--rev" and "--stop"')) + + if opts['rev']: + if opts['any']: + raise error.Abort(_('cannot specify both "--rev" and "--any"')) + if opts['all']: + raise error.Abort(_('cannot specify both "--rev" and "--all"')) + + # Backward compatibility + if opts['unstable']: + msg = ("'evolve --unstable' is deprecated, " + "use 'evolve --orphan'") + repo.ui.deprecwarn(msg, '4.4') + + opts['orphan'] = opts['divergent'] + + if opts['divergent']: + msg = ("'evolve --divergent' is deprecated, " + "use 'evolve --content-divergent'") + repo.ui.deprecwarn(msg, '4.4') + + opts['content_divergent'] = opts['divergent'] + + if opts['bumped']: + msg = ("'evolve --bumped' is deprecated, " + "use 'evolve --phase-divergent'") + repo.ui.deprecwarn(msg, '4.4') + + opts['phase_divergent'] = opts['bumped'] + + return opts + +def _cleanup(ui, repo, startnode, showprogress): + if showprogress: + ui.progress(_('evolve'), None) + if repo['.'] != startnode: + ui.status(_('working directory is now at %s\n') % repo['.']) + +def divergentsets(repo, ctx): + """Compute sets of commits divergent with a given one""" + cache = {} + base = {} + for n in compat.allprecursors(repo.obsstore, [ctx.node()]): + if n == ctx.node(): + # a node can't be a base for divergence with itself + continue + nsuccsets = compat.successorssets(repo, n, cache) + for nsuccset in nsuccsets: + if ctx.node() in nsuccset: + # we are only interested in *other* successor sets + continue + if tuple(nsuccset) in base: + # we already know the latest base for this divergency + continue + base[tuple(nsuccset)] = n + divergence = [] + for divset, b in base.iteritems(): + divergence.append({ + 'divergentnodes': divset, + 'commonprecursor': b + }) + + return divergence + +@eh.command( + '^evolve|stabilize|solve', + [('n', 'dry-run', False, + _('do not perform actions, just print what would be done')), + ('', 'confirm', False, + _('ask for confirmation before performing the action')), + ('A', 'any', False, + _('also consider troubled changesets unrelated to current working ' + 'directory')), + ('r', 'rev', [], _('solves troubles of these revisions')), + ('', 'bumped', False, _('solves only bumped changesets')), + ('', 'phase-divergent', False, _('solves only phase-divergent changesets')), + ('', 'divergent', False, _('solves only divergent changesets')), + ('', 'content-divergent', False, _('solves only content-divergent changesets')), + ('', 'unstable', False, _('solves only unstable changesets')), + ('', 'orphan', False, _('solves only orphan changesets (default)')), + ('a', 'all', False, _('evolve all troubled changesets related to the ' + 'current working directory and its descendants')), + ('c', 'continue', False, _('continue an interrupted evolution')), + ('', 'stop', False, _('stop the interrupted evolution')), + ('l', 'list', False, 'provide details on troubled changesets in the repo'), + ] + mergetoolopts, + _('[OPTIONS]...') +) +def evolve(ui, repo, **opts): + """solve troubled changesets in your repository + + Modifying history can lead to various types of troubled changesets: + unstable, bumped, or divergent. The evolve command resolves your troubles + by executing one of the following actions: + + - update working copy to a successor + - rebase an unstable changeset + - extract the desired changes from a bumped changeset + - fuse divergent changesets back together + + If you pass no arguments, evolve works in automatic mode: it will execute a + single action to reduce instability related to your working copy. There are + two cases for this action. First, if the parent of your working copy is + obsolete, evolve updates to the parent's successor. Second, if the working + copy parent is not obsolete but has obsolete predecessors, then evolve + determines if there is an unstable changeset that can be rebased onto the + working copy parent in order to reduce instability. + If so, evolve rebases that changeset. If not, evolve refuses to guess your + intention, and gives a hint about what you might want to do next. + + Any time evolve creates a changeset, it updates the working copy to the new + changeset. (Currently, every successful evolve operation involves an update + as well; this may change in future.) + + Automatic mode only handles common use cases. For example, it avoids taking + action in the case of ambiguity, and it ignores unstable changesets that + are not related to your working copy. + It also refuses to solve bumped or divergent changesets unless you + explicitly request such behavior (see below). + + Eliminating all instability around your working copy may require multiple + invocations of :hg:`evolve`. Alternately, use ``--all`` to recursively + select and evolve all unstable changesets that can be rebased onto the + working copy parent. + This is more powerful than successive invocations, since ``--all`` handles + ambiguous cases (e.g. unstable changesets with multiple children) by + evolving all branches. + + When your repository cannot be handled by automatic mode, you might need to + use ``--rev`` to specify a changeset to evolve. For example, if you have + an unstable changeset that is not related to the working copy parent, + you could use ``--rev`` to evolve it. Or, if some changeset has multiple + unstable children, evolve in automatic mode refuses to guess which one to + evolve; you have to use ``--rev`` in that case. + + Alternately, ``--any`` makes evolve search for the next evolvable changeset + regardless of whether it is related to the working copy parent. + + You can supply multiple revisions to evolve multiple troubled changesets + in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev + first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are + ``--rev`` and ``--any``. + + ``hg evolve --any --all`` is useful for cleaning up instability across all + branches, letting evolve figure out the appropriate order and destination. + + When you have troubled changesets that are not unstable, :hg:`evolve` + refuses to consider them unless you specify the category of trouble you + wish to resolve, with ``--bumped`` or ``--divergent``. These options are + currently mutually exclusive with each other and with ``--unstable`` + (the default). You can combine ``--bumped`` or ``--divergent`` with + ``--rev``, ``--all``, or ``--any``. + + You can also use the evolve command to list the troubles affecting your + repository by using the --list flag. You can choose to display only some + categories of troubles with the --unstable, --divergent or --bumped flags. + """ + + opts = _checkevolveopts(repo, opts) + # Options + contopt = opts['continue'] + anyopt = opts['any'] + allopt = opts['all'] + startnode = repo['.'] + dryrunopt = opts['dry_run'] + confirmopt = opts['confirm'] + revopt = opts['rev'] + stopopt = opts['stop'] + + troublecategories = ['phase_divergent', 'content_divergent', 'orphan'] + specifiedcategories = [t.replace('_', '') + for t in troublecategories + if opts[t]] + if opts['list']: + compat.startpager(ui, 'evolve') + listtroubles(ui, repo, specifiedcategories, **opts) + return + + targetcat = 'orphan' + if 1 < len(specifiedcategories): + msg = _('cannot specify more than one trouble category to solve (yet)') + raise error.Abort(msg) + elif len(specifiedcategories) == 1: + targetcat = specifiedcategories[0] + elif repo['.'].obsolete(): + displayer = compat.changesetdisplayer(ui, repo, + {'template': shorttemplate}) + # no args and parent is obsolete, update to successors + try: + ctx = repo[utility._singlesuccessor(repo, repo['.'])] + except utility.MultipleSuccessorsError as exc: + repo.ui.write_err(_('parent is obsolete with multiple' + ' successors:\n')) + for ln in exc.successorssets: + for n in ln: + displayer.show(repo[n]) + return 2 + + ui.status(_('update:')) + if not ui.quiet: + displayer.show(ctx) + + if dryrunopt: + return 0 + res = hg.update(repo, ctx.rev()) + if ctx != startnode: + ui.status(_('working directory is now at %s\n') % ctx) + return res + + ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve') + troubled = set(repo.revs('troubled()')) + + # Progress handling + seen = 1 + count = allopt and len(troubled) or 1 + showprogress = allopt + + def progresscb(): + if revopt or allopt: + ui.progress(_('evolve'), seen, unit=_('changesets'), total=count) + + evolvestate = state.cmdstate(repo) + # Continuation handling + if contopt: + if not evolvestate: + raise error.Abort(_('no interrupted evolve to continue')) + evolvestate.load() + continueevolve(ui, repo, evolvestate, progresscb) + if evolvestate['command'] != 'evolve': + evolvestate.delete() + return + startnode = repo.unfiltered()[evolvestate['startnode']] + evolvestate.delete() + elif stopopt: + if not evolvestate: + raise error.Abort(_('no interrupted evolve to stop')) + evolvestate.load() + pctx = repo['.'] + hg.updaterepo(repo, pctx.node(), True) + ui.status(_('stopped the interrupted evolve\n')) + ui.status(_('working directory is now at %s\n') % pctx) + evolvestate.delete() + return + else: + cmdutil.bailifchanged(repo) + + revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat) + + if not revs: + return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat) + + # For the progress bar to show + count = len(revs) + # Order the revisions + if targetcat == 'orphan': + revs = _orderrevs(repo, revs) + + # cbor does not know how to serialize sets, using list for skippedrevs + stateopts = {'category': targetcat, 'replacements': {}, 'revs': revs, + 'confirm': confirmopt, 'startnode': startnode.node(), + 'skippedrevs': [], 'command': 'evolve', 'orphanmerge': False} + evolvestate.addopts(stateopts) + for rev in revs: + curctx = repo[rev] + progresscb() + ret = _solveone(ui, repo, curctx, evolvestate, dryrunopt, confirmopt, + progresscb, targetcat) + seen += 1 + if ret[0]: + evolvestate['replacements'][curctx.node()] = [ret[1]] + else: + evolvestate['skippedrevs'].append(curctx.node()) + + if evolvestate['orphanmerge']: + # we were processing an orphan merge with both parents obsolete, + # stabilized for second parent, re-stabilize for the first parent + ret = _solveone(ui, repo, repo[ret[1]], evolvestate, dryrunopt, + confirmopt, progresscb, targetcat) + if ret[0]: + evolvestate['replacements'][curctx.node()] = [ret[1]] + else: + evolvestate['skippedrevs'].append(curctx.node()) + + evolvestate['orphanmerge'] = False + + progresscb() + _cleanup(ui, repo, startnode, showprogress) + +def continueevolve(ui, repo, evolvestate, progresscb): + """logic for handling of `hg evolve --continue`""" + orig = repo[evolvestate['current']] + with repo.wlock(), repo.lock(): + ctx = orig + source = ctx.extra().get('source') + extra = {} + if source: + extra['source'] = source + extra['intermediate-source'] = ctx.hex() + else: + extra['source'] = ctx.hex() + user = ctx.user() + date = ctx.date() + message = ctx.description() + ui.status(_('evolving %d:%s "%s"\n') % (ctx.rev(), ctx, + message.split('\n', 1)[0])) + targetphase = max(ctx.phase(), phases.draft) + overrides = {('phases', 'new-commit'): targetphase} + + ctxparents = orig.parents() + if len(ctxparents) == 2: + currentp1 = repo.dirstate.parents()[0] + p1obs = ctxparents[0].obsolete() + p2obs = ctxparents[1].obsolete() + # asumming that the parent of current wdir is successor of one + # of p1 or p2 of the original changeset + if p1obs and not p2obs: + # p1 is obsolete and p2 is not obsolete, current working + # directory parent should be successor of p1, so we should + # set dirstate parents to (succ of p1, p2) + with repo.dirstate.parentchange(): + repo.dirstate.setparents(currentp1, + ctxparents[1].node()) + elif p2obs and not p1obs: + # p2 is obsolete and p1 is not obsolete, current working + # directory parent should be successor of p2, so we should + # set dirstate parents to (succ of p2, p1) + with repo.dirstate.parentchange(): + repo.dirstate.setparents(ctxparents[0].node(), + currentp1) + + else: + # both the parents were obsoleted, if orphanmerge is set, we + # are processing the second parent first (to keep parent order) + if evolvestate.get('orphanmerge'): + with repo.dirstate.parentchange(): + repo.dirstate.setparents(ctxparents[0].node(), + currentp1) + pass + + with repo.ui.configoverride(overrides, 'evolve-continue'): + node = repo.commit(text=message, user=user, + date=date, extra=extra) + + # resolving conflicts can lead to empty wdir and node can be None in + # those cases + newctx = repo[node] if node is not None else repo['.'] + compat.createmarkers(repo, [(ctx, (newctx,))], operation='evolve') + + # make sure we are continuing evolve and not `hg next --evolve` + if evolvestate['command'] == 'evolve': + evolvestate['replacements'][ctx.node()] = node + category = evolvestate['category'] + confirm = evolvestate['confirm'] + unfi = repo.unfiltered() + if evolvestate['orphanmerge']: + # processing a merge changeset with both parents obsoleted, + # stabilized on second parent, insert in front of list to + # re-process to stabilize on first parent + evolvestate['revs'].insert(0, repo[node].rev()) + evolvestate['orphanmerge'] = False + for rev in evolvestate['revs']: + # XXX: prevent this lookup by storing nodes instead of revnums + curctx = unfi[rev] + if (curctx.node() not in evolvestate['replacements'] and + curctx.node() not in evolvestate['skippedrevs']): + newnode = _solveone(ui, repo, curctx, evolvestate, False, + confirm, progresscb, category) + if newnode[0]: + evolvestate['replacements'][curctx.node()] = newnode[1] + else: + evolvestate['skippedrevs'].append(curctx.node()) + return diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/evolvestate.py --- a/hgext3rd/evolve/evolvestate.py Sat Jan 20 12:38:11 2018 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,74 +0,0 @@ -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -""" -This file contains class to wrap the state for hg evolve command and other -related logic. - -All the data related to the command state is stored as dictionary in the object. -The class has methods using which the data can be stored to disk in -.hg/evolvestate file. - -We store the data on disk in cbor, for which we use cbor library to serialize -and deserialize data. -""" - -from __future__ import absolute_import - -from .thirdparty import cbor - -from mercurial import ( - util, -) - -class evolvestate(): - """a wrapper class to store the state of `hg evolve` command - - All the data for the state is stored in the form of key-value pairs in a - dictionary. - - The class object can write all the data to .hg/evolvestate file and also can - populate the object data reading that file - """ - - def __init__(self, repo, path='evolvestate', opts={}): - self._repo = repo - self.path = path - self.opts = opts - - def __nonzero__(self): - return self.exists() - - def __getitem__(self, key): - return self.opts[key] - - def load(self): - """load the existing evolvestate file into the class object""" - op = self._read() - self.opts.update(op) - - def addopts(self, opts): - """add more key-value pairs to the data stored by the object""" - self.opts.update(opts) - - def save(self): - """write all the evolvestate data stored in .hg/evolvestate file - - we use third-party library cbor to serialize data to write in the file. - """ - with self._repo.vfs(self.path, 'wb', atomictemp=True) as fp: - cbor.dump(self.opts, fp) - - def _read(self): - """reads the evolvestate file and returns a dictionary which contain - data in the same format as it was before storing""" - with self._repo.vfs(self.path, 'rb') as fp: - return cbor.load(fp) - - def delete(self): - """drop the evolvestate file if exists""" - util.unlinkpath(self._repo.vfs.join(self.path), ignoremissing=True) - - def exists(self): - """check whether the evolvestate file exists or not""" - return self._repo.vfs.exists(self.path) diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/exthelper.py --- a/hgext3rd/evolve/exthelper.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/exthelper.py Wed Mar 21 16:35:18 2018 +0100 @@ -128,8 +128,11 @@ revset.loadpredicate(ui, 'evolve', revsetpredicate) templatekeyword = registrar.templatekeyword() - for name, kw in self._templatekws: - templatekeyword(name)(kw) + for name, kw, requires in self._templatekws: + if requires is not None: + templatekeyword(name, requires=requires)(kw) + else: + templatekeyword(name)(kw) templatekw.loadkeyword(ui, 'evolve', templatekeyword) for ext, command, wrapper, opts in self._extcommandwrappers: @@ -215,7 +218,7 @@ return symbol return dec - def templatekw(self, keywordname): + def templatekw(self, keywordname, requires=None): """Decorated function is a template keyword The name of the keyword must be given as the decorator argument. @@ -228,7 +231,7 @@ return 'babar' """ def dec(keyword): - self._templatekws.append((keywordname, keyword)) + self._templatekws.append((keywordname, keyword, requires)) return keyword return dec diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/firstmergecache.py --- a/hgext3rd/evolve/firstmergecache.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/firstmergecache.py Wed Mar 21 16:35:18 2018 +0100 @@ -49,11 +49,11 @@ if util.safehasattr(repo, 'updatecaches'): @localrepo.unfilteredmethod - def updatecaches(self, tr=None): + def updatecaches(self, tr=None, **kwargs): if utility.shouldwarmcache(self, tr): self.firstmergecache.update(self) self.firstmergecache.save(self) - super(firstmergecacherepo, self).updatecaches(tr) + super(firstmergecacherepo, self).updatecaches(tr, **kwargs) else: def transaction(self, *args, **kwargs): diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/legacy.py --- a/hgext3rd/evolve/legacy.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/legacy.py Wed Mar 21 16:35:18 2018 +0100 @@ -30,6 +30,12 @@ from mercurial import registrar from mercurial import util +try: + from mercurial.utils.dateutil import makedate +except ImportError as e: + # compat with hg < 4.6 + from mercurial.util import makedate + if util.safehasattr(registrar, 'command'): commandfunc = registrar.command else: # compat with hg < 4.3 @@ -105,7 +111,7 @@ prec = bin(objhex) sucs = (suc == nullid) and [] or [suc] meta = { - 'date': '%i %i' % util.makedate(), + 'date': '%i %i' % makedate(), 'user': ui.username(), } try: diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/metadata.py --- a/hgext3rd/evolve/metadata.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/metadata.py Wed Mar 21 16:35:18 2018 +0100 @@ -5,7 +5,7 @@ # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. -__version__ = '7.2.1' -testedwith = '4.1.3 4.2.3 4.3.2 4.4.2' +__version__ = '7.3.0' +testedwith = '4.1.3 4.2.3 4.3.2 4.4.2 4.5.2' minimumhgversion = '4.1' buglink = 'https://bz.mercurial-scm.org/' diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/obscache.py --- a/hgext3rd/evolve/obscache.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/obscache.py Wed Mar 21 16:35:18 2018 +0100 @@ -225,6 +225,8 @@ self.clear(reset=True) starttime = timer() + revs = list(revs) + obsmarkers = list(obsmarkers) self._updatefrom(repo, revs, obsmarkers) duration = timer() - starttime repo.ui.log('evoext-cache', 'updated %s in %.4f seconds (%sr, %so)\n', @@ -523,8 +525,8 @@ if util.safehasattr(repo, 'updatecaches'): @localrepo.unfilteredmethod - def updatecaches(self, tr=None): - super(obscacherepo, self).updatecaches(tr) + def updatecaches(self, tr=None, **kwargs): + super(obscacherepo, self).updatecaches(tr, **kwargs) self.obsstore.obscache.update(repo) self.obsstore.obscache.save(repo) diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/obsdiscovery.py --- a/hgext3rd/evolve/obsdiscovery.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/obsdiscovery.py Wed Mar 21 16:35:18 2018 +0100 @@ -44,10 +44,10 @@ util, wireproto, ) -from mercurial.hgweb import hgweb_mod from mercurial.i18n import _ from . import ( + compat, exthelper, obscache, utility, @@ -95,7 +95,8 @@ common = set() undecided = set(probeset) totalnb = len(undecided) - ui.progress(_("comparing with other"), 0, total=totalnb) + ui.progress(_("comparing with other"), 0, total=totalnb, + unit=_("changesets")) _takefullsample = setdiscovery._takefullsample if remote.capable('_evoext_obshash_1'): getremotehash = remote.evoext_obshash1 @@ -114,7 +115,7 @@ roundtrips += 1 ui.progress(_("comparing with other"), totalnb - len(undecided), - total=totalnb) + total=totalnb, unit=_("changesets")) ui.debug("query %i; still undecided: %i, sample size is: %i\n" % (roundtrips, len(undecided), len(sample))) # indices between sample and externalized version must match @@ -175,7 +176,8 @@ local.obsstore.rangeobshashcache.update(local) querycount = 0 - ui.progress(_("comparing obsmarker with other"), querycount) + ui.progress(_("comparing obsmarker with other"), querycount, + unit=_("queries")) overflow = [] while sample or overflow: if overflow: @@ -230,7 +232,8 @@ addentry(new) assert nbsample == nbreplies querycount += 1 - ui.progress(_("comparing obsmarker with other"), querycount) + ui.progress(_("comparing obsmarker with other"), querycount, + unit=_("queries")) ui.progress(_("comparing obsmarker with other"), None) local.obsstore.rangeobshashcache.save(local) duration = timer() - starttime @@ -597,11 +600,11 @@ if util.safehasattr(repo, 'updatecaches'): @localrepo.unfilteredmethod - def updatecaches(self, tr=None): + def updatecaches(self, tr=None, **kwargs): if utility.shouldwarmcache(self, tr): self.obsstore.rangeobshashcache.update(self) self.obsstore.rangeobshashcache.save(self) - super(obshashrepo, self).updatecaches(tr) + super(obshashrepo, self).updatecaches(tr, **kwargs) else: def transaction(self, *args, **kwargs): @@ -675,6 +678,7 @@ except ValueError: self._abort(error.ResponseError(_("unexpected response:"), d)) +@compat.wireprotocommand(eh, 'evoext_obshashrange_v1', 'ranges') def srv_obshashrange_v1(repo, proto, ranges): ranges = wireproto.decodelist(ranges) ranges = [_decrange(r) for r in ranges] @@ -698,17 +702,26 @@ caps = orig(repo, proto) enabled = _useobshashrange(repo) if obsolete.isenabled(repo, obsolete.exchangeopt) and enabled: + + # Compat hg 4.6+ (2f7290555c96) + bytesresponse = False + if util.safehasattr(caps, 'data'): + bytesresponse = True + caps = caps.data + caps = caps.split() - caps.append('_evoext_obshashrange_v1') + caps.append(b'_evoext_obshashrange_v1') caps.sort() - caps = ' '.join(caps) + caps = b' '.join(caps) + + # Compat hg 4.6+ (2f7290555c96) + if bytesresponse: + from mercurial import wireprototypes + caps = wireprototypes.bytesresponse(caps) return caps @eh.extsetup def obshashrange_extsetup(ui): - hgweb_mod.perms['evoext_obshashrange_v1'] = 'pull' - - wireproto.commands['evoext_obshashrange_v1'] = (srv_obshashrange_v1, 'ranges') ### extensions.wrapfunction(wireproto, 'capabilities', _obshashrange_capabilities) # wrap command content @@ -759,7 +772,8 @@ cache = [] unfi = repo.unfiltered() markercache = {} - repo.ui.progress(_("preparing locally"), 0, total=len(unfi)) + repo.ui.progress(_("preparing locally"), 0, total=len(unfi), + unit=_("changesets")) for i in unfi: ctx = unfi[i] entry = 0 @@ -789,7 +803,8 @@ cache.append((ctx.node(), sha.digest())) else: cache.append((ctx.node(), node.nullid)) - repo.ui.progress(_("preparing locally"), i, total=len(unfi)) + repo.ui.progress(_("preparing locally"), i, total=len(unfi), + unit=_("changesets")) repo.ui.progress(_("preparing locally"), None) return cache @@ -828,9 +843,11 @@ except ValueError: self._abort(error.ResponseError(_("unexpected response:"), d)) +@compat.wireprotocommand(eh, 'evoext_obshash', 'nodes') def srv_obshash(repo, proto, nodes): return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes))) +@compat.wireprotocommand(eh, 'evoext_obshash1', 'nodes') def srv_obshash1(repo, proto, nodes): return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), version=1)) @@ -840,20 +857,27 @@ caps = orig(repo, proto) if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.ui.configbool('experimental', 'evolution.obsdiscovery', True)): + + # Compat hg 4.6+ (2f7290555c96) + bytesresponse = False + if util.safehasattr(caps, 'data'): + bytesresponse = True + caps = caps.data + caps = caps.split() - caps.append('_evoext_obshash_0') - caps.append('_evoext_obshash_1') + caps.append(b'_evoext_obshash_0') + caps.append(b'_evoext_obshash_1') caps.sort() - caps = ' '.join(caps) + caps = b' '.join(caps) + + # Compat hg 4.6+ (2f7290555c96) + if bytesresponse: + from mercurial import wireprototypes + caps = wireprototypes.bytesresponse(caps) return caps @eh.extsetup def obshash_extsetup(ui): - hgweb_mod.perms['evoext_obshash'] = 'pull' - hgweb_mod.perms['evoext_obshash1'] = 'pull' - - wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes') - wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes') extensions.wrapfunction(wireproto, 'capabilities', _obshash_capabilities) # wrap command content oldcap, args = wireproto.commands['capabilities'] diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/obsexchange.py --- a/hgext3rd/evolve/obsexchange.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/obsexchange.py Wed Mar 21 16:35:18 2018 +0100 @@ -27,6 +27,12 @@ wireproto, ) +try: + from mercurial import wireprotoserver + wireprotoserver.handlewsgirequest +except (ImportError, AttributeError): + wireprotoserver = None + from mercurial.hgweb import common as hgwebcommon from . import ( @@ -106,10 +112,22 @@ """wrapper to advertise new capability""" caps = orig(repo, proto) if obsolete.isenabled(repo, obsolete.exchangeopt): + + # Compat hg 4.6+ (2f7290555c96) + bytesresponse = False + if util.safehasattr(caps, 'data'): + bytesresponse = True + caps = caps.data + caps = caps.split() - caps.append('_evoext_getbundle_obscommon') + caps.append(b'_evoext_getbundle_obscommon') caps.sort() - caps = ' '.join(caps) + caps = b' '.join(caps) + + # Compat hg 4.6+ (2f7290555c96) + if bytesresponse: + from mercurial import wireprototypes + caps = wireprototypes.bytesresponse(caps) return caps @eh.extsetup diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/obshistory.py --- a/hgext3rd/evolve/obshistory.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/obshistory.py Wed Mar 21 16:35:18 2018 +0100 @@ -10,11 +10,9 @@ import re from mercurial import ( - cmdutil, commands, error, graphmod, - mdiff, patch, obsolete, node as nodemod, @@ -58,7 +56,7 @@ ] + commands.formatteropts, _('hg olog [OPTION]... [REV]')) def cmdobshistory(ui, repo, *revs, **opts): - """show the obsolescence history of the specified revisions. + """show the obsolescence history of the specified revisions If no revision range is specified, we display the log for the current working copy parent. @@ -97,7 +95,7 @@ revs.reverse() _debugobshistoryrevs(ui, repo, revs, opts) -class obsmarker_printer(cmdutil.changeset_printer): +class obsmarker_printer(compat.changesetprinter): """show (available) information about a node We display the node, description (if available) and various information @@ -173,7 +171,7 @@ basename = "changeset-description" succname = "changeset-description" - d = mdiff.unidiff(basedesc, '', succdesc, '', basename, succname) + d = compat.strdiff(basedesc, succdesc, basename, succname) # mercurial 4.1 and before return the patch directly if not isinstance(d, tuple): patch = d @@ -356,7 +354,7 @@ displayer = obsmarker_printer(ui, repo.unfiltered(), matchfn, opts, buffered=True) edges = graphmod.asciiedges walker = _obshistorywalker(repo.unfiltered(), revs, opts.get('all', False)) - cmdutil.displaygraph(ui, repo, walker, displayer, edges) + compat.displaygraph(ui, repo, walker, displayer, edges) def _debugobshistoryrevs(ui, repo, revs, opts): """ Display the obsolescence history for revset @@ -404,7 +402,7 @@ label="evolve.node") fm.plain(' ') - fm.write('rev', '(%d)', int(ctx), + fm.write('rev', '(%d)', ctx.rev(), label="evolve.rev") fm.plain(' ') @@ -473,6 +471,11 @@ fm.write('succnodes', '%s', nodes, label="evolve.node") + operation = metadata.get('operation') + if operation: + fm.plain(' using ') + fm.write('operation', '%s', operation, label="evolve.operation") + fm.plain(' by ') fm.write('user', '%s', metadata['user'], diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/rewriteutil.py --- a/hgext3rd/evolve/rewriteutil.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/rewriteutil.py Wed Mar 21 16:35:18 2018 +0100 @@ -24,6 +24,7 @@ phases, repair, revset, + util, ) from mercurial.i18n import _ @@ -60,6 +61,10 @@ msg = _("cannot %s the null revision") % (action) hint = _("no changeset checked out") raise error.Abort(msg, hint=hint) + if any(util.safehasattr(r, 'rev') for r in revs): + msg = "rewriteutil.precheck called with ctx not revs" + repo.ui.develwarn(msg) + revs = (r.rev() for r in revs) publicrevs = repo.revs('%ld and public()', revs) if publicrevs: summary = _formatrevs(repo, publicrevs) diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/serveronly.py --- a/hgext3rd/evolve/serveronly.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/serveronly.py Wed Mar 21 16:35:18 2018 +0100 @@ -58,4 +58,6 @@ evolveopts = 'all' repo.ui.setconfig('experimental', 'evolution', evolveopts) if obsolete.isenabled(repo, 'exchange'): - repo.ui.setconfig('server', 'bundle1', False) + # if no config explicitly set, disable bundle1 + if not isinstance(repo.ui.config('server', 'bundle1'), str): + repo.ui.setconfig('server', 'bundle1', False) diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/stablerangecache.py --- a/hgext3rd/evolve/stablerangecache.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/stablerangecache.py Wed Mar 21 16:35:18 2018 +0100 @@ -366,11 +366,11 @@ if util.safehasattr(repo, 'updatecaches'): @localrepo.unfilteredmethod - def updatecaches(self, tr=None): + def updatecaches(self, tr=None, **kwargs): if utility.shouldwarmcache(self, tr): self.stablerange.update(self) self.stablerange.save(self) - super(stablerangerepo, self).updatecaches(tr) + super(stablerangerepo, self).updatecaches(tr, **kwargs) else: def transaction(self, *args, **kwargs): diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/stablesort.py --- a/hgext3rd/evolve/stablesort.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/stablesort.py Wed Mar 21 16:35:18 2018 +0100 @@ -14,7 +14,6 @@ from mercurial import ( commands, - cmdutil, localrepo, error, node as nodemod, @@ -76,7 +75,7 @@ raise error.Abort('unknown sorting method: "%s"' % method, hint='pick one of: %s' % valid_method) - displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True) + displayer = compat.changesetdisplayer(ui, repo, opts, buffered=True) kwargs = {} if opts['limit']: kwargs['limit'] = int(opts['limit']) @@ -673,11 +672,11 @@ if util.safehasattr(repo, 'updatecaches'): @localrepo.unfilteredmethod - def updatecaches(self, tr=None): + def updatecaches(self, tr=None, **kwargs): if utility.shouldwarmcache(self, tr): self.stablesort.update(self) self.stablesort.save(self) - super(stablesortrepo, self).updatecaches(tr) + super(stablesortrepo, self).updatecaches(tr, **kwargs) else: def transaction(self, *args, **kwargs): diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/state.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/state.py Wed Mar 21 16:35:18 2018 +0100 @@ -0,0 +1,135 @@ +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +""" +This file contains class to wrap the state for commands and other +related logic. + +All the data related to the command state is stored as dictionary in the object. +The class has methods using which the data can be stored to disk in a file under +.hg/ directory. + +We store the data on disk in cbor, for which we use cbor library to serialize +and deserialize data. +""" + +from __future__ import absolute_import + +import errno +import struct + +from .thirdparty import cbor + +from mercurial import ( + error, + util, +) + +from mercurial.i18n import _ + +class cmdstate(): + """a wrapper class to store the state of commands like `evolve`, `grab` + + All the data for the state is stored in the form of key-value pairs in a + dictionary. + + The class object can write all the data to a file in .hg/ directory and also + can populate the object data reading that file + """ + + def __init__(self, repo, path='evolvestate', opts={}): + self._repo = repo + self.path = path + self.opts = opts + + def __nonzero__(self): + return self.exists() + + def __getitem__(self, key): + return self.opts[key] + + def __setitem__(self, key, value): + updates = {key: value} + self.opts.update(updates) + + def load(self): + """load the existing evolvestate file into the class object""" + op = self._read() + if isinstance(op, dict): + self.opts.update(op) + elif self.path == 'evolvestate': + # it is the old evolvestate file + oldop = _oldevolvestateread(self._repo) + self.opts.update(oldop) + + def addopts(self, opts): + """add more key-value pairs to the data stored by the object""" + self.opts.update(opts) + + def save(self): + """write all the evolvestate data stored in .hg/evolvestate file + + we use third-party library cbor to serialize data to write in the file. + """ + with self._repo.vfs(self.path, 'wb', atomictemp=True) as fp: + cbor.dump(self.opts, fp) + + def _read(self): + """reads the evolvestate file and returns a dictionary which contain + data in the same format as it was before storing""" + with self._repo.vfs(self.path, 'rb') as fp: + return cbor.load(fp) + + def delete(self): + """drop the evolvestate file if exists""" + util.unlinkpath(self._repo.vfs.join(self.path), ignoremissing=True) + + def exists(self): + """check whether the evolvestate file exists or not""" + return self._repo.vfs.exists(self.path) + +def _oldevolvestateread(repo): + """function to read the old evolvestate file + + This exists for BC reasons.""" + try: + f = repo.vfs('evolvestate') + except IOError as err: + if err.errno != errno.ENOENT: + raise + try: + versionblob = f.read(4) + if len(versionblob) < 4: + repo.ui.debug('ignoring corrupted evolvestate (file contains %i bits)' + % len(versionblob)) + return None + version = struct._unpack('>I', versionblob)[0] + if version != 0: + msg = _('unknown evolvestate version %i') % version + raise error.Abort(msg, hint=_('upgrade your evolve')) + records = [] + data = f.read() + off = 0 + end = len(data) + while off < end: + rtype = data[off] + off += 1 + length = struct._unpack('>I', data[off:(off + 4)])[0] + off += 4 + record = data[off:(off + length)] + off += length + if rtype == 't': + rtype, record = record[0], record[1:] + records.append((rtype, record)) + state = {} + for rtype, rdata in records: + if rtype == 'C': + state['current'] = rdata + elif rtype.lower(): + repo.ui.debug('ignore evolve state record type %s' % rtype) + else: + raise error.Abort(_('unknown evolvestate field type %r') + % rtype, hint=_('upgrade your evolve')) + return state + finally: + f.close() diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/templatekw.py --- a/hgext3rd/evolve/templatekw.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/templatekw.py Wed Mar 21 16:35:18 2018 +0100 @@ -9,13 +9,13 @@ """ from . import ( + compat, error, exthelper, obshistory ) from mercurial import ( - cmdutil, templatekw, node, util @@ -34,21 +34,29 @@ return 'obsolete' return '' -@eh.templatekw('troubles') -def showtroubles(**args): - """List of strings. Evolution troubles affecting the changeset - (zero or more of "unstable", "divergent" or "bumped").""" - ctx = args['ctx'] - try: - # specify plural= explicitly to trigger TypeError on hg < 4.2 - return templatekw.showlist('trouble', ctx.instabilities(), args, - plural='troubles') - except TypeError: - return templatekw.showlist('trouble', ctx.instabilities(), plural='troubles', - **args) +if util.safehasattr(templatekw, 'compatlist'): + @eh.templatekw('troubles', requires=set(['ctx', 'templ'])) + def showtroubles(context, mapping): + ctx = context.resource(mapping, 'ctx') + return templatekw.compatlist(context, mapping, 'trouble', + ctx.instabilities(), plural='troubles') +else: + # older template API in hg < 4.6 + @eh.templatekw('troubles') + def showtroubles(**args): + """List of strings. Evolution troubles affecting the changeset + (zero or more of "unstable", "divergent" or "bumped").""" + ctx = args['ctx'] + try: + # specify plural= explicitly to trigger TypeError on hg < 4.2 + return templatekw.showlist('trouble', ctx.instabilities(), args, + plural='troubles') + except TypeError: + return templatekw.showlist('trouble', ctx.instabilities(), plural='troubles', + **args) if util.safehasattr(templatekw, 'showpredecessors'): - eh.templatekw("precursors")(templatekw.showpredecessors) + templatekw.keywords["precursors"] = templatekw.showpredecessors else: # for version <= hg4.3 def closestprecursors(repo, nodeid): @@ -97,7 +105,7 @@ return directsuccessorssets(repo, nodeid) if util.safehasattr(templatekw, 'showsuccessorssets'): - eh.templatekw("successors")(templatekw.showsuccessorssets) + templatekw.keywords["successors"] = templatekw.showsuccessorssets else: # for version <= hg4.3 @@ -252,13 +260,33 @@ return "\n".join(lines) -@eh.templatekw("obsfatedata") -def showobsfatedata(repo, ctx, **args): - # Get the needed obsfate data - values = obsfatedata(repo, ctx) + +if util.safehasattr(templatekw, 'compatlist'): + @eh.templatekw('obsfatedata', requires=set(['ctx', 'templ'])) + def showobsfatedata(context, mapping): + ctx = context.resource(mapping, 'ctx') + repo = ctx.repo() + values = obsfatedata(repo, ctx) - if values is None: - return templatekw.showlist("obsfatedata", [], args) + if values is None: + return templatekw.compatlist(context, mapping, "obsfatedata", []) + args = mapping.copy() + args.pop('ctx') + args['templ'] = context.resource(mapping, 'templ') + return _showobsfatedata(repo, ctx, values, **args) +else: + # pre hg-4.6 + @eh.templatekw("obsfatedata") + def showobsfatedata(repo, ctx, **args): + # Get the needed obsfate data + values = obsfatedata(repo, ctx) + + if values is None: + return templatekw.showlist("obsfatedata", [], args) + + return _showobsfatedata(repo, ctx, values, **args) + +def _showobsfatedata(repo, ctx, values, **args): # Format each successorset successors list for raw in values: @@ -316,10 +344,10 @@ def showobsfate(*args, **kwargs): return showobsfatedata(*args, **kwargs) -if util.safehasattr(cmdutil.changeset_printer, '_showobsfate'): +if util.safehasattr(compat.changesetprinter, '_showobsfate'): pass # already included by default -elif util.safehasattr(cmdutil.changeset_printer, '_exthook'): - @eh.wrapfunction(cmdutil.changeset_printer, '_exthook') +elif util.safehasattr(compat.changesetprinter, '_exthook'): + @eh.wrapfunction(compat.changesetprinter, '_exthook') def exthook(original, self, ctx): # Call potential other extensions original(self, ctx) diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/evolve/utility.py --- a/hgext3rd/evolve/utility.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/evolve/utility.py Wed Mar 21 16:35:18 2018 +0100 @@ -5,8 +5,16 @@ # 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.node import nullrev +from . import ( + compat, +) + shorttemplate = "[{label('evolve.rev', rev)}] {desc|firstline}\n" def obsexcmsg(ui, message, important=False): @@ -66,3 +74,107 @@ if maxrevs is not None and maxrevs < len(repo.unfiltered()): return False return True + +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 + +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 = compat.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 = compat.successorssets(repo, obs.node()) + if len(newer) > 1 or len(newer[0]) > 1: + raise MultipleSuccessorsError(newer) + + return repo[newer[0][0]].rev() + +def revselectionprompt(ui, repo, revs, customheader=""): + """function to prompt user to choose a revision from all the revs and return + that revision for further tasks + + revs is a list of rev number of revision from which one revision should be + choosed by the user + customheader is a text which the caller wants as the header of the prompt + which will list revisions to select + + returns value is: + rev number of revision choosed: if user choose a revision + None: if user entered a wrong input, user quit the prompt, + ui.interactive is not set + """ + + # ui.interactive is not set, fallback to default behavior and avoid showing + # the prompt + if not ui.configbool('ui', 'interactive'): + return None + + promptmsg = customheader + "\n" + for idx, rev in enumerate(revs): + curctx = repo[rev] + revmsg = _("%d: [%s] %s\n" % (idx, curctx, + curctx.description().split("\n")[0])) + promptmsg += revmsg + + promptmsg += _("q: quit the prompt\n") + promptmsg += _("enter the index of the revision you want to select:") + idxselected = ui.prompt(promptmsg) + + intidx = None + try: + intidx = int(idxselected) + except ValueError: + if idxselected == 'q': + return None + ui.write_err(_("invalid value '%s' entered for index\n") % idxselected) + return None + + if intidx >= len(revs) or intidx < 0: + # we can make this error message better + ui.write_err(_("invalid value '%d' entered for index\n") % intidx) + return None + + return revs[intidx] diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/serverminitopic.py --- a/hgext3rd/serverminitopic.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/serverminitopic.py Wed Mar 21 16:35:18 2018 +0100 @@ -68,7 +68,7 @@ def branchinfo(self, rev): """return branch name and close flag for rev, using and updating persistent cache.""" - phase = self._repo._phasecache.phase(self, rev) + phase = self._repo._phasecache.phase(self._repo, rev) if phase: ctx = self._repo[rev] return ctx.branch(), ctx.closesbranch() @@ -163,7 +163,7 @@ valid = super(_topiccache, self).validfor(repo) if not valid: return False - elif not mighttopic(repo) and self.phaseshash is None: + elif self.phaseshash is None: # phasehash at None means this is a branchmap # coming from a public only set return True diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/topic/__init__.py --- a/hgext3rd/topic/__init__.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/topic/__init__.py Wed Mar 21 16:35:18 2018 +0100 @@ -134,13 +134,14 @@ from . import ( compat, constants, + destination, + discovery, + evolvebits, flow, + randomname, revset as topicrevset, - destination, stack, topicmap, - discovery, - randomname ) if util.safehasattr(registrar, 'command'): @@ -175,9 +176,9 @@ 'topic.active': 'green', } -__version__ = '0.7.0' +__version__ = '0.8.0' -testedwith = '4.1.3 4.2.3 4.3.3 4.4.2' +testedwith = '4.1.3 4.2.3 4.3.3 4.4.2 4.5.2' minimumhgversion = '4.1' buglink = 'https://bz.mercurial-scm.org/' @@ -240,6 +241,12 @@ revlist = stack.stack(self._repo, topic=topic) try: return revlist.index(self.rev()) + except ValueError: + if self.obsolete(): + succ = evolvebits._singlesuccessor(self._repo, self) + if succ not in revlist: + return None + return revlist.index(succ) except IndexError: # Lets move to the last ctx of the current topic return None diff -r 7ac98f83ae6d -r b92114f201c9 hgext3rd/topic/compat.py --- a/hgext3rd/topic/compat.py Sat Jan 20 12:38:11 2018 +0100 +++ b/hgext3rd/topic/compat.py Wed Mar 21 16:35:18 2018 +0100 @@ -7,6 +7,8 @@ """ from __future__ import absolute_import +import functools + from mercurial import ( obsolete, scmutil, @@ -27,6 +29,17 @@ if successorssets is None: successorssets = obsolete.successorssets +# Wrap obsolete.creatmarkers and make it accept but ignore "operation" argument +# for hg < 4.3 +createmarkers = obsolete.createmarkers +originalcreatemarkers = createmarkers +while isinstance(originalcreatemarkers, functools.partial): + originalcreatemarkers = originalcreatemarkers.func +if originalcreatemarkers.__code__.co_argcount < 6: + def createmarkers(repo, relations, flag=0, date=None, metadata=None, + operation=None): + return obsolete.createmarkers(repo, relations, flag, date, metadata) + def startpager(ui, cmd): """function to start a pager in case ui.pager() exists""" try: @@ -44,4 +57,4 @@ else: relations = [(repo[o], tuple(repo[n] for n in new)) for (o, new) in replacements.iteritems()] - obsolete.createmarkers(repo, relations) + createmarkers(repo, relations, operation=operation) diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-amend.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-discovery-obshashrange.t --- a/tests/test-discovery-obshashrange.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-discovery-obshashrange.t Wed Mar 21 16:35:18 2018 +0100 @@ -189,7 +189,7 @@ running python "*/dummyssh" user@dummy 'hg -R server serve --stdio' (glob) sending hello command sending between command - remote: 466 + remote: * (glob) remote: capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_obshashrange_v1 batch * (glob) remote: 1 preparing listkeys for "phases" @@ -224,7 +224,7 @@ 45f8b879de922f6a6e620ba04205730335b6fc7e sending unbundle command bundle2-output-bundle: "HG20", 4 parts total - bundle2-output-part: "replycaps" 172 bytes payload + bundle2-output-part: "replycaps" * bytes payload (glob) bundle2-output-part: "changegroup" (params: 1 mandatory) streamed payload bundle2-output-part: "pushkey" (params: 4 mandatory) empty payload bundle2-output-part: "obsmarkers" streamed payload diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-divergent.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-drop.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-cycles.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-effectflags.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-obshistory-complex.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-obshistory.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-order.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-phase.t --- a/tests/test-evolve-phase.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-evolve-phase.t Wed Mar 21 16:35:18 2018 +0100 @@ -115,8 +115,10 @@ $ echo c2 > a $ hg resolve -m (no more unresolved files) + continue: hg evolve --continue $ hg evolve -c evolving 2:13833940840c "c" + working directory is now at 3d2080c198e5 $ hg glog @ 5 - 3d2080c198e5 c (secret) diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-split.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-templates.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve-topic.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-evolve.t --- a/tests/test-evolve.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-evolve.t Wed Mar 21 16:35:18 2018 +0100 @@ -794,10 +794,10 @@ user: 10 marker size: format v1: - smallest length: 75 - longer length: 76 - median length: 76 - mean length: 75 + smallest length: * (glob) + longer length: * (glob) + median length: * (glob) + mean length: * (glob) format v0: smallest length: * (glob) longer length: * (glob) @@ -1243,6 +1243,20 @@ (use 'hg help' for the full list of commands or 'hg -v' for details) [255] +Shows "use 'hg evolve' to..." hints iff the evolve command is enabled + + $ hg --hidden up 14 + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory parent is obsolete! (cce26b684bfe) + $ cat >> $HGRCPATH < [experimental] + > evolutioncommands=evolve + > EOF + $ hg --hidden up 15 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory parent is obsolete! (27247fcb2df6) + (use 'hg evolve' to update to its successor: 24e63b319adf) + Restore all of the evolution features $ cat >> $HGRCPATH < [extensions] > evolve= > [ui] - > logtemplate = '{rev} - {node|short} {desc|firstline} [{author}] ({phase})\n' + > logtemplate = '{rev} - {node|short} {desc|firstline} [{author}] ({phase}) {bookmarks}\n' > EOF $ hg init fold-tests @@ -16,6 +16,7 @@ $ hg debugbuilddag .+3:branchpoint+4*branchpoint+2 $ hg up 'desc("r7")' 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg bookmark bm1 $ hg log -G o 10 - a8407f9a3dc1 r10 [debugbuilddag] (draft) | @@ -23,7 +24,7 @@ | o 8 - abf57d94268b r8 [debugbuilddag] (draft) | - | @ 7 - 4de32a90b66c r7 [debugbuilddag] (draft) + | @ 7 - 4de32a90b66c r7 [debugbuilddag] (draft) bm1 | | | o 6 - f69452c5b1af r6 [debugbuilddag] (draft) | | @@ -87,6 +88,27 @@ 3 changesets folded 0 files updated, 0 files merged, 0 files removed, 0 files unresolved +Checking whether the bookmarks are moved or not + + $ hg log -G + @ 11 - 198b5c405d01 r5 [debugbuilddag] (draft) bm1 + | + | o 10 - a8407f9a3dc1 r10 [debugbuilddag] (draft) + | | + | o 9 - 529dfc5bb875 r9 [debugbuilddag] (draft) + | | + | o 8 - abf57d94268b r8 [debugbuilddag] (draft) + | | + o | 4 - bebd167eb94d r4 [debugbuilddag] (draft) + |/ + o 3 - 2dc09a01254d r3 [debugbuilddag] (draft) + | + o 2 - 01241442b3c2 r2 [debugbuilddag] (draft) + | + o 1 - 66f7d451a68b r1 [debugbuilddag] (draft) + | + o 0 - 1ea73414a91b r0 [debugbuilddag] (public) + (test inherited from test-evolve.t) $ hg fold --from 6 # want to run hg fold 6 @@ -95,7 +117,7 @@ [255] $ hg log -G - @ 11 - 198b5c405d01 r5 [debugbuilddag] (draft) + @ 11 - 198b5c405d01 r5 [debugbuilddag] (draft) bm1 | | o 10 - a8407f9a3dc1 r10 [debugbuilddag] (draft) | | @@ -124,7 +146,7 @@ $ hg log -G o 12 - b568edbee6e0 r8 [debugbuilddag] (draft) | - | @ 11 - 198b5c405d01 r5 [debugbuilddag] (draft) + | @ 11 - 198b5c405d01 r5 [debugbuilddag] (draft) bm1 | | | o 4 - bebd167eb94d r4 [debugbuilddag] (draft) |/ @@ -144,8 +166,9 @@ $ hg commit '-m r11' $ hg up '.^' 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + (leaving bookmark bm1) $ hg log -G - o 13 - 14d0e0da8e91 r11 [test] (draft) + o 13 - 14d0e0da8e91 r11 [test] (draft) bm1 | | o 12 - b568edbee6e0 r8 [debugbuilddag] (draft) | | @@ -190,7 +213,7 @@ 2 changesets folded 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg log -G - @ 14 - 29b470a33594 r5 [Victor Rataxes ] (draft) + @ 14 - 29b470a33594 r5 [Victor Rataxes ] (draft) bm1 | | o 12 - b568edbee6e0 r8 [debugbuilddag] (draft) | | @@ -209,7 +232,7 @@ 2 changesets folded 0 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg log -G - @ 15 - 91880abed0f2 r4 [test] (draft) + @ 15 - 91880abed0f2 r4 [test] (draft) bm1 | | o 12 - b568edbee6e0 r8 [debugbuilddag] (draft) |/ diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-issue-5720.t --- a/tests/test-issue-5720.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-issue-5720.t Wed Mar 21 16:35:18 2018 +0100 @@ -70,10 +70,12 @@ $ echo c2 > a $ hg resolve -m (no more unresolved files) + continue: hg evolve --continue Continue the evolution $ hg evolve --continue evolving 2:13833940840c "c" + working directory is now at 3d2080c198e5 Tip should stay in secret phase $ hg log -G -T "{rev}: {phase}" diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-metaedit.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-obsolete.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-prev-next.t --- a/tests/test-prev-next.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-prev-next.t Wed Mar 21 16:35:18 2018 +0100 @@ -1,4 +1,6 @@ $ cat >> $HGRCPATH < [ui] + > interactive = True > [extensions] > color = > EOF @@ -36,8 +38,8 @@ hg next -B should move active bookmark $ hg next -B --dry-run - hg update 1; - hg bookmark mark -r 1; + hg update 6e742c9127b3; + hg bookmark mark -r 6e742c9127b3; [1] added b $ hg next -B 1 files updated, 0 files merged, 0 files removed, 0 files unresolved @@ -47,7 +49,7 @@ hg prev should unset active bookmark $ hg prev --dry-run - hg update 0; + hg update a154386e50d1; [0] added a $ hg prev 0 files updated, 0 files merged, 1 files removed, 0 files unresolved @@ -61,7 +63,7 @@ mark 1:6e742c9127b3 * mark2 0:a154386e50d1 $ hg next --dry-run --color=debug - hg update 1; + hg update 6e742c9127b3; [[evolve.rev|1]] added b $ hg next 1 files updated, 0 files merged, 0 files removed, 0 files unresolved @@ -173,7 +175,7 @@ no children [1] $ hg prev --dry-run --color=debug - hg update 1; + hg update 6e742c9127b3; [[evolve.rev|1]] added b $ hg prev 0 files updated, 0 files merged, 1 files removed, 0 files unresolved @@ -191,12 +193,11 @@ move:[2] added c atop:[3] added b (2) hg rebase -r 4e26ef31f919 -d 9ad178109a19 - working directory now at 9ad178109a19 (add color output for smoke testing) $ hg next --evolve --color debug - move:[[evolve.rev|2]] added c + [evolve.operation|move:][[evolve.rev|2]] added c atop:[[evolve.rev|3]] added b (2) [ ui.status|working directory now at [evolve.node|e3b6d5df389b]] @@ -212,12 +213,20 @@ $ hg prev 0 files updated, 0 files merged, 1 files removed, 0 files unresolved [3] added b (2) - $ hg next - ambiguous next changeset: - [4] added c + $ hg next < 1 + > EOF + ambiguous next changeset, choose one to update: + 0: [e3b6d5df389b] added c + 1: [9df671ccd2c7] added d + q: quit the prompt + enter the index of the revision you want to select: 1 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved [5] added d - explicitly update to one of them - [1] + + $ hg prev + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [3] added b (2) next with ambiguity in aspiring children @@ -227,24 +236,100 @@ no children (2 unstable changesets to be evolved here, do you want --evolve?) [1] - $ hg next --evolve - ambiguous next (unstable) changeset: - [4] added c - [5] added d - (run 'hg evolve --rev REV' on one of them) - [1] + $ hg next --evolve < 0 + > EOF + ambiguous next (unstable) changeset, choose one to evolve and update: + 0: [e3b6d5df389b] added c + 1: [9df671ccd2c7] added d + q: quit the prompt + enter the index of the revision you want to select: 0 + move:[4] added c + atop:[6] added b (3) + working directory now at 5ce67c2407b0 + + $ hg log -GT "{rev}:{node|short} {desc}\n" + @ 7:5ce67c2407b0 added c + | + o 6:d7f119adc759 added b (3) + | + | o 5:9df671ccd2c7 added d + | | + | x 3:9ad178109a19 added b (2) + |/ + o 0:a154386e50d1 added a + + $ hg evolve -r 5 move:[5] added d atop:[6] added b (3) working directory is now at 47ea25be8aea +prev with multiple parents + + $ hg log -GT "{rev}:{node|short} {desc}\n" + @ 8:47ea25be8aea added d + | + | o 7:5ce67c2407b0 added c + |/ + o 6:d7f119adc759 added b (3) + | + o 0:a154386e50d1 added a + + $ hg merge -r 5ce67c2407b0 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg ci -m "merge commit" + + $ hg prev < q + > EOF + multiple parents, choose one to update: + 0: [47ea25be8aea] added d + 1: [5ce67c2407b0] added c + q: quit the prompt + enter the index of the revision you want to select: q + [8] added d + [7] added c + multiple parents, explicitly update to one + [1] + + $ hg prev --config ui.interactive=False + [8] added d + [7] added c + multiple parents, explicitly update to one + [1] + + $ hg prev < 1 + > EOF + multiple parents, choose one to update: + 0: [47ea25be8aea] added d + 1: [5ce67c2407b0] added c + q: quit the prompt + enter the index of the revision you want to select: 1 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [7] added c + + $ hg log -GT "{rev}:{node|short} {desc}\n" + o 9:a4b8c25a87d3 merge commit + |\ + | o 8:47ea25be8aea added d + | | + @ | 7:5ce67c2407b0 added c + |/ + o 6:d7f119adc759 added b (3) + | + o 0:a154386e50d1 added a + + $ cd .. prev and next should lock properly against other commands $ hg init repo $ cd repo - $ HGEDITOR=${TESTDIR}/fake-editor.sh + $ HGEDITOR="sh ${TESTDIR}/fake-editor.sh" $ echo hi > foo $ hg ci -Am 'one' adding foo diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-prune.t --- a/tests/test-prune.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-prune.t Wed Mar 21 16:35:18 2018 +0100 @@ -358,10 +358,10 @@ user: 7 marker size: format v1: - smallest length: 75 - longer length: 75 - median length: 75 - mean length: 75 + smallest length: * (glob) + longer length: * (glob) + median length: * (glob) + mean length: * (glob) format v0: smallest length: * (glob) longer length: * (glob) diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-partial-C2.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-partial-C4.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-pruned-B2.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-pruned-B3.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-pruned-B4.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-pruned-B5.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-pruned-B8.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-superceed-A2.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-superceed-A3.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-superceed-A6.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-superceed-A7.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-unpushed-D2.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-unpushed-D3.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-unpushed-D4.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-push-checkheads-unpushed-D5.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-sharing.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-split.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-stabilize-conflict.t --- a/tests/test-stabilize-conflict.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-stabilize-conflict.t Wed Mar 21 16:35:18 2018 +0100 @@ -166,8 +166,10 @@ $ safesed 's/dix/ten/' babar $ hg resolve --all -m (no more unresolved files) + continue: hg evolve --continue $ hg evolve --continue evolving 5:71c18f70c34f "babar count up to fifteen" + working directory is now at 1836b91c6c1d $ hg resolve -l $ hg log -G @ changeset: 8:1836b91c6c1d diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-stabilize-order.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-stabilize-result.t --- a/tests/test-stabilize-result.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-stabilize-result.t Wed Mar 21 16:35:18 2018 +0100 @@ -101,8 +101,10 @@ [255] $ hg resolve -m a (no more unresolved files) + continue: hg evolve --continue $ hg evolve --continue evolving 5:3655f0f50885 "newer a" + working directory is now at 1cf0aacfd363 Stabilize latecomer with different parent ========================================= @@ -130,8 +132,7 @@ Get a successors of 8 on it $ hg grab 1cf0aacfd363 - rebasing 8:1cf0aacfd363 "newer a" - ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) + grabbing 8:1cf0aacfd363 "newer a" Add real change to the successors @@ -143,7 +144,7 @@ $ hg phase --hidden --public 1cf0aacfd363 1 new bumped changesets $ glog - @ 12:(73b15c7566e9|d5c7ef82d003)@default\(draft\) bk:\[\] newer a (re) + @ 12:99c21c89bcef@default(draft) bk:[] newer a | o 9:7bc2f5967f5e@default(draft) bk:[] add c | @@ -159,10 +160,10 @@ $ hg evolve --any --dry-run --phase-divergent recreate:[12] newer a atop:[8] newer a - hg rebase --rev (73b15c7566e9|d5c7ef82d003) --dest 66719795a494; (re) + hg rebase --rev 99c21c89bcef --dest 66719795a494; hg update 1cf0aacfd363; - hg revert --all --rev (73b15c7566e9|d5c7ef82d003); (re) - hg commit --msg "bumped update to d5c7ef82d003" + hg revert --all --rev 99c21c89bcef; + hg commit --msg "bumped update to 99c21c89bcef" $ hg evolve --any --confirm --phase-divergent recreate:[12] newer a atop:[8] newer a @@ -175,10 +176,10 @@ perform evolve? [Ny] y rebasing to destination parent: 66719795a494 computing new diff - committed as c2c1151aa854 - working directory is now at c2c1151aa854 + committed as e34e87ea7b83 + working directory is now at e34e87ea7b83 $ glog - @ 14:c2c1151aa854@default(draft) bk:[] bumped update to 1cf0aacfd363: + @ 14:e34e87ea7b83@default(draft) bk:[] bumped update to 1cf0aacfd363: | | o 9:7bc2f5967f5e@default(draft) bk:[] add c | | diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-stack-branch.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-topic-change.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-topic-stack-complex.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-topic-stack-data.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-topic-stack.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-topic-tutorial.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-topic.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-touch.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-tutorial.t --- a/tests/test-tutorial.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-tutorial.t Wed Mar 21 16:35:18 2018 +0100 @@ -637,11 +637,10 @@ $ hg up 'p1(10b8aeaa8cc8)' # going on "bathroom stuff" parent 1 files updated, 0 files merged, 0 files removed, 0 files unresolved $ hg grab fac207dec9f5 # moving "SPAM SPAM" to the working directory parent - rebasing 10:fac207dec9f5 "SPAM SPAM" (tip) + grabbing 10:fac207dec9f5 "SPAM SPAM" merging shopping - ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) $ hg log -G - @ a224f2a4fb9f (draft): SPAM SPAM + @ 57e9caedbcb8 (draft): SPAM SPAM | | o 10b8aeaa8cc8 (draft): bathroom stuff |/ @@ -709,7 +708,7 @@ # User test # Date 0 0 # Thu Jan 01 00:00:00 1970 +0000 - # Node ID a224f2a4fb9f9f828f608959912229d7b38b26de + # Node ID 57e9caedbcb8575a01c128db9d1bcbd624ef2115 # Parent 41aff6a42b7578ec7ec3cb2041633f1ca43cca96 SPAM SPAM @@ -742,14 +741,13 @@ for simplicity sake we get the bathroom change in line again $ hg grab 10b8aeaa8cc8 - rebasing 9:10b8aeaa8cc8 "bathroom stuff" + grabbing 9:10b8aeaa8cc8 "bathroom stuff" merging shopping - ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) $ hg phase --draft . $ hg log -G - @ 75954b8cd933 (draft): bathroom stuff + @ 4710c0968793 (draft): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -914,9 +912,9 @@ 1 new obsolescence markers (run 'hg update' to get a working copy) $ hg log -G - o 75954b8cd933 (public): bathroom stuff + o 4710c0968793 (public): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -934,7 +932,7 @@ $ hg rollback repository tip rolled back to revision 4 (undo pull) $ hg log -G - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -969,9 +967,9 @@ 1 new obsolescence markers (run 'hg update' to get a working copy) $ hg log -G - o 75954b8cd933 (draft): bathroom stuff + o 4710c0968793 (draft): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -987,7 +985,7 @@ Remotely someone add a new changeset on top of the mutable "bathroom" on. - $ hg up 75954b8cd933 -q + $ hg up 4710c0968793 -q $ cat >> shopping << EOF > Giraffe > Rhino @@ -1000,12 +998,14 @@ $ cd ../local $ hg up 75954b8cd933 -q + abort: unknown revision '75954b8cd933'! + [255] $ sed -i'' -e 's/... More bathroom stuff to come/Bath Robe/' shopping $ hg commit --amend $ hg log -G - @ a44c85f957d3 (draft): bathroom stuff + @ 682004e81e71 (draft): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -1071,13 +1071,13 @@ see both version showing up in the log. $ hg log -G - o bf1b0d202029 (draft): animals + o e4e4fa805d92 (draft): animals | - | @ a44c85f957d3 (draft): bathroom stuff + | @ 682004e81e71 (draft): bathroom stuff | | - x | 75954b8cd933 (draft): bathroom stuff + x | 4710c0968793 (draft): bathroom stuff |/ - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -1121,7 +1121,7 @@ $ hg push other pushing to $TESTTMP/other (glob) searching for changes - abort: push includes unstable changeset: bf1b0d202029! + abort: push includes unstable changeset: e4e4fa805d92! (use 'hg evolve' to get a stable history or --force to ignore warnings) [255] @@ -1134,7 +1134,7 @@ $ hg evolve --dry-run move:[15] animals atop:[14] bathroom stuff - hg rebase -r bf1b0d202029 -d a44c85f957d3 + hg rebase -r e4e4fa805d92 -d 682004e81e71 Let's do it @@ -1142,16 +1142,16 @@ move:[15] animals atop:[14] bathroom stuff merging shopping - working directory is now at ee942144f952 + working directory is now at 2a2b36e14660 The old version of bathroom is hidden again. $ hg log -G - @ ee942144f952 (draft): animals + @ 2a2b36e14660 (draft): animals | - o a44c85f957d3 (draft): bathroom stuff + o 682004e81e71 (draft): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -1220,13 +1220,13 @@ Now let's see where we are, and update to the successor. $ hg parents - bf1b0d202029 (draft): animals - working directory parent is obsolete! (bf1b0d202029) - (use 'hg evolve' to update to its successor: ee942144f952) + e4e4fa805d92 (draft): animals + working directory parent is obsolete! (e4e4fa805d92) + (use 'hg evolve' to update to its successor: 2a2b36e14660) $ hg evolve update:[8] animals 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - working directory is now at ee942144f952 + working directory is now at 2a2b36e14660 Relocating unstable change after prune ---------------------------------------------- @@ -1248,13 +1248,13 @@ added 1 changesets with 1 changes to 1 files (run 'hg update' to get a working copy) $ hg log -G - o 99f039c5ec9e (draft): SPAM SPAM SPAM + o fc41faf45288 (draft): SPAM SPAM SPAM | - @ ee942144f952 (draft): animals + @ 2a2b36e14660 (draft): animals | - o a44c85f957d3 (draft): bathroom stuff + o 682004e81e71 (draft): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -1340,9 +1340,9 @@ In the mean time I noticed you can't buy animals in a super market and I prune the animal changeset: - $ hg prune ee942144f952 + $ hg prune 2a2b36e14660 1 files updated, 0 files merged, 0 files removed, 0 files unresolved - working directory now at a44c85f957d3 + working directory now at 682004e81e71 1 changesets pruned 1 new unstable changesets @@ -1351,13 +1351,13 @@ is neither dead or obsolete. My repository is in an unstable state again. $ hg log -G - o 99f039c5ec9e (draft): SPAM SPAM SPAM + o fc41faf45288 (draft): SPAM SPAM SPAM | - x ee942144f952 (draft): animals + x 2a2b36e14660 (draft): animals | - @ a44c85f957d3 (draft): bathroom stuff + @ 682004e81e71 (draft): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | @@ -1453,7 +1453,7 @@ #endif $ hg log -r "orphan()" - 99f039c5ec9e (draft): SPAM SPAM SPAM + fc41faf45288 (draft): SPAM SPAM SPAM #if docgraph-ext $ hg docgraph -r "orphan()" --sphinx-directive --rankdir LR #rest-ignore @@ -1479,14 +1479,14 @@ move:[17] SPAM SPAM SPAM atop:[14] bathroom stuff merging shopping - working directory is now at 40aa40daeefb + working directory is now at e6cfcb672150 $ hg log -G - @ 40aa40daeefb (draft): SPAM SPAM SPAM + @ e6cfcb672150 (draft): SPAM SPAM SPAM | - o a44c85f957d3 (draft): bathroom stuff + o 682004e81e71 (draft): bathroom stuff | - o a224f2a4fb9f (public): SPAM SPAM + o 57e9caedbcb8 (public): SPAM SPAM | o 41aff6a42b75 (public): adding fruit | diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-uncommit.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-unstable.t --- a/tests/test-unstable.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-unstable.t Wed Mar 21 16:35:18 2018 +0100 @@ -153,18 +153,17 @@ $ hg evo --all --any --orphan - warning: no support for evolving merge changesets with two obsolete parents yet - (Redo the merge (6b4280e33286) and use `hg prune --succ ` to obsolete the old one) + move:[3] merge + atop:[4] aprime + move:[6] merge + atop:[5] cprime + working directory is now at 2d30b910830b $ hg log -G - @ 5:2db39fda7e2f@default(draft) cprime - | - | o 4:47127ea62e5f@default(draft) aprime - |/ - | o 3:6b4280e33286@default(draft) merge - | |\ - +---x 2:474da87dd33b@default(draft) add _c + @ 7:2d30b910830b@default(draft) merge + |\ + | o 5:2db39fda7e2f@default(draft) cprime | | - | x 1:b3264cec9506@default(draft) add _a + o | 4:47127ea62e5f@default(draft) aprime |/ o 0:b4952fcf48cf@default(draft) add base @@ -302,7 +301,7 @@ $ hg evo --all --any --orphan - cannot solve split across two branches + could not solve instability, ambiguous destination: parent split across two branches $ hg log -G @ 4:3c69ea6aa93e@default(draft) add bprimesplit2 | diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-userguide.t diff -r 7ac98f83ae6d -r b92114f201c9 tests/test-wireproto.t --- a/tests/test-wireproto.t Sat Jan 20 12:38:11 2018 +0100 +++ b/tests/test-wireproto.t Wed Mar 21 16:35:18 2018 +0100 @@ -194,6 +194,7 @@ $ hg debugpushkey http://localhost:$HGPORT/ obsolete abort: HTTP Error 410: won't exchange obsmarkers through pushkey [255] + $ cat errors.log $ hg debugpushkey ssh://user@dummy/server obsolete remote: abort: won't exchange obsmarkers through pushkey remote: (upgrade your client or server to use the bundle2 protocol)