# HG changeset patch # User Pierre-Yves David # Date 1517918428 -3600 # Node ID 1baf32675ec60d622bd2c3cdce9dbf5a1020c06d # Parent 14b9cded3c81fb1e8900581ddbb931a84bba39bd# Parent bee9fee8f36b3b1ef042d3f67ed7254164b974e6 merge with stable diff -r bee9fee8f36b -r 1baf32675ec6 CHANGELOG --- a/CHANGELOG Sun Jan 21 16:55:02 2018 -0500 +++ b/CHANGELOG Tue Feb 06 13:00:28 2018 +0100 @@ -1,6 +1,13 @@ Changelog ========= +7.3.0 --(in-progress) +--------------------- + + * 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 + 7.2.2 -- (in-progress) ---------------------- diff -r bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/__init__.py --- a/hgext3rd/evolve/__init__.py Sun Jan 21 16:55:02 2018 -0500 +++ b/hgext3rd/evolve/__init__.py Tue Feb 06 13:00:28 2018 +0100 @@ -252,11 +252,7 @@ evolution=all """.strip() - -import os import sys -import re -import collections import struct try: @@ -287,23 +283,19 @@ 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 +304,8 @@ compat, debugcmd, cmdrewrite, - evolvestate, + state, + evolvecmd, exthelper, metadata, obscache, @@ -330,8 +323,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' @@ -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) @@ -520,18 +511,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 @@ -854,8 +833,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')) @@ -889,79 +868,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 ############################# @@ -1039,1009 +945,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 = compat.changesetdisplayer(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 = compat.changesetdisplayer(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 = compat.changesetdisplayer(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 = compat.changesetdisplayer(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: '')() @@ -2087,7 +990,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] @@ -2233,7 +1136,7 @@ ui.warn(_('explicitly update to one of them\n')) result = 1 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) @@ -2258,12 +1161,15 @@ return 1 else: cmdutil.bailifchanged(repo) - result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt, - False, lambda: None, category='orphan') - if not result: + evolvestate = state.cmdstate(repo) + result = evolvecmd._solveone(ui, repo, repo[aspchildren[0]], + evolvestate, dryrunopt, 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 result + return 0 return 1 return result finally: @@ -2435,47 +1341,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 @@ -2484,32 +1349,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 bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/cmdrewrite.py --- a/hgext3rd/evolve/cmdrewrite.py Sun Jan 21 16:55:02 2018 -0500 +++ b/hgext3rd/evolve/cmdrewrite.py Tue Feb 06 13:00:28 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, @@ -1148,3 +1150,96 @@ tr.close() finally: lockmod.release(tr, lock, wlock) + +@eh.command( + 'grab', + [('r', 'rev', '', 'revision to grab'), + ('', 'continue', False, 'continue interrupted grab'), + ('', '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') + + 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()] + pctx = repo['.'] + + if origctx in pctx.ancestors(): + 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() + if newnode: + obsolete.createmarkers(repo, [(origctx, (repo[newnode],))]) + else: + obsolete.createmarkers(repo, [(origctx, (pctx,))]) + + 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 bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/evolvecmd.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/evolvecmd.py Tue Feb 06 13:00:28 2018 +0100 @@ -0,0 +1,1169 @@ +# 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, + 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 = cmdutil.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: + 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 (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: + msg = "cannot solve split across two branches\n" + ui.write_err(msg) + return (False, '') + 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:')) + 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).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:')) + 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, 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) + 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:')) + 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['.'] + obsolete.createmarkers(repo, [(other, (new,))]) + 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: + 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) + +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): + 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']: + 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')), + ('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 = 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 evolve to continue') + evolvestate.load() + 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} + + with repo.ui.configoverride(overrides, 'evolve-continue'): + node = repo.commit(text=message, user=user, + date=date, extra=extra) + + obsolete.createmarkers(repo, [(ctx, (repo[node],))]) + evolvestate.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) + + stateopts = {'category': targetcat, 'replacements': {}, 'revs': revs} + 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]] + progresscb() + _cleanup(ui, repo, startnode, showprogress) diff -r bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/evolvestate.py --- a/hgext3rd/evolve/evolvestate.py Sun Jan 21 16:55:02 2018 -0500 +++ /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 bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/metadata.py --- a/hgext3rd/evolve/metadata.py Sun Jan 21 16:55:02 2018 -0500 +++ b/hgext3rd/evolve/metadata.py Tue Feb 06 13:00:28 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.2.dev' +__version__ = '7.3.0.dev' testedwith = '4.1.3 4.2.3 4.3.2 4.4.2' minimumhgversion = '4.1' buglink = 'https://bz.mercurial-scm.org/' diff -r bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/obshistory.py --- a/hgext3rd/evolve/obshistory.py Sun Jan 21 16:55:02 2018 -0500 +++ b/hgext3rd/evolve/obshistory.py Tue Feb 06 13:00:28 2018 +0100 @@ -57,7 +57,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. diff -r bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/state.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/state.py Tue Feb 06 13:00:28 2018 +0100 @@ -0,0 +1,74 @@ +# 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 + +from .thirdparty import cbor + +from mercurial import ( + util, +) + +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 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 bee9fee8f36b -r 1baf32675ec6 hgext3rd/evolve/utility.py --- a/hgext3rd/evolve/utility.py Sun Jan 21 16:55:02 2018 -0500 +++ b/hgext3rd/evolve/utility.py Tue Feb 06 13:00:28 2018 +0100 @@ -5,8 +5,14 @@ # 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.node import nullrev +from . import ( + compat, +) + shorttemplate = "[{label('evolve.rev', rev)}] {desc|firstline}\n" def obsexcmsg(ui, message, important=False): @@ -66,3 +72,60 @@ 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() diff -r bee9fee8f36b -r 1baf32675ec6 hgext3rd/topic/__init__.py --- a/hgext3rd/topic/__init__.py Sun Jan 21 16:55:02 2018 -0500 +++ b/hgext3rd/topic/__init__.py Tue Feb 06 13:00:28 2018 +0100 @@ -175,7 +175,7 @@ 'topic.active': 'green', } -__version__ = '0.7.1.dev' +__version__ = '0.8.0.dev' testedwith = '4.1.3 4.2.3 4.3.3 4.4.2' minimumhgversion = '4.1' diff -r bee9fee8f36b -r 1baf32675ec6 tests/test-evolve-phase.t --- a/tests/test-evolve-phase.t Sun Jan 21 16:55:02 2018 -0500 +++ b/tests/test-evolve-phase.t Tue Feb 06 13:00:28 2018 +0100 @@ -115,6 +115,7 @@ $ echo c2 > a $ hg resolve -m (no more unresolved files) + continue: hg evolve --continue $ hg evolve -c evolving 2:13833940840c "c" diff -r bee9fee8f36b -r 1baf32675ec6 tests/test-grab.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-grab.t Tue Feb 06 13:00:28 2018 +0100 @@ -0,0 +1,302 @@ +Test for the grab command + + $ cat >> $HGRCPATH < [alias] + > glog = log -G -T "{rev}:{node|short} {desc}\n" + > [extensions] + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH + + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + + $ hg init repo + $ cd repo + $ hg help grab + hg grab [-r] rev + + grabs a commit, move it on the top of working directory parent and + updates to it. + + options: + + -r --rev VALUE revision to grab + --continue continue interrupted grab + --abort abort interrupted grab + + (some details hidden, use --verbose to show complete help) + + $ mkcommit a + $ mkcommit b + $ mkcommit c + + $ hg glog + @ 2:4538525df7e2 add c + | + o 1:7c3bad9141dc add b + | + o 0:1f0dee641bb7 add a + + +Grabbing an ancestor + + $ hg grab -r 7c3bad9141dc + abort: cannot grab an ancestor revision + [255] + +Specifying multiple revisions to grab + + $ hg grab 1f0dee641bb7 -r 7c3bad9141dc + abort: specify just one revision + [255] + +Specifying no revisions to grab + + $ hg grab + abort: empty revision set + [255] + +Continuing without interrupted grab + + $ hg grab --continue + abort: no interrupted grab state exists + [255] + +Aborting without interrupted grab + + $ hg grab --abort + abort: no interrupted grab state exists + [255] + +Specifying both continue and revs + + $ hg up 1f0dee641bb7 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg grab -r 4538525df7e2 --continue + abort: cannot specify both --continue and revision + [255] + +Making new branch heads + + $ mkcommit x + created new head + $ mkcommit y + + $ hg glog + @ 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + | o 2:4538525df7e2 add c + | | + | o 1:7c3bad9141dc add b + |/ + o 0:1f0dee641bb7 add a + +Grabbing a revision + + $ hg grab 7c3bad9141dc + grabbing 1:7c3bad9141dc "add b" + 1 new orphan changesets + $ hg glog + @ 5:7c15c05db6fa add b + | + o 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + | * 2:4538525df7e2 add c + | | + | x 1:7c3bad9141dc add b + |/ + o 0:1f0dee641bb7 add a + + +When grab does not create any changes + + $ hg graft -r 4538525df7e2 + grafting 2:4538525df7e2 "add c" + + $ hg glog + @ 6:c4636a81ebeb add c + | + o 5:7c15c05db6fa add b + | + o 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + | * 2:4538525df7e2 add c + | | + | x 1:7c3bad9141dc add b + |/ + o 0:1f0dee641bb7 add a + + $ hg grab -r 4538525df7e2 + grabbing 2:4538525df7e2 "add c" + note: grab of 2:4538525df7e2 created no changes to commit + + $ hg glog + @ 6:c4636a81ebeb add c + | + o 5:7c15c05db6fa add b + | + o 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + o 0:1f0dee641bb7 add a + +interrupted grab + + $ hg up d46dc301d92f + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ echo foo > c + $ hg ci -Aqm "foo to c" + $ hg grab -r c4636a81ebeb + grabbing 6:c4636a81ebeb "add c" + merging c + warning: conflicts while merging c! (edit, then use 'hg resolve --mark') + unresolved merge conflicts (see hg help resolve) + [1] + + $ echo foobar > c + $ hg resolve --all --mark + (no more unresolved files) + continue: hg grab --continue + $ hg grab --continue + $ hg glog + @ 8:44e155eb95c7 add c + | + o 7:2ccc03d1d096 foo to c + | + | o 5:7c15c05db6fa add b + |/ + o 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + o 0:1f0dee641bb7 add a + +Testing the abort functionality of hg grab + + $ echo foo > b + $ hg ci -Aqm "foo to b" + $ hg glog -r .^:: + @ 9:902d4f4602bb foo to b + | + o 8:44e155eb95c7 add c + | + ~ + + $ hg grab -r 7c15c05db6fa + grabbing 5:7c15c05db6fa "add b" + merging b + warning: conflicts while merging b! (edit, then use 'hg resolve --mark') + unresolved merge conflicts (see hg help resolve) + [1] + + $ hg grab --abort + aborting grab, updating to 902d4f4602bb + + $ hg glog + @ 9:902d4f4602bb foo to b + | + o 8:44e155eb95c7 add c + | + o 7:2ccc03d1d096 foo to c + | + | o 5:7c15c05db6fa add b + |/ + o 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + o 0:1f0dee641bb7 add a + + +Trying to grab a public changeset + + $ hg phase -r 7c15c05db6fa -p + + $ hg grab -r 7c15c05db6fa + abort: cannot grab public changesets: 7c15c05db6fa + (see 'hg help phases' for details) + [255] + + $ hg glog + @ 9:902d4f4602bb foo to b + | + o 8:44e155eb95c7 add c + | + o 7:2ccc03d1d096 foo to c + | + | o 5:7c15c05db6fa add b + |/ + o 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + o 0:1f0dee641bb7 add a + +Checking phase preservation while grabbing secret changeset + +In case of merge conflicts + + $ hg phase -r 7c15c05db6fa -s -f + + $ hg grab -r 7c15c05db6fa + grabbing 5:7c15c05db6fa "add b" + merging b + warning: conflicts while merging b! (edit, then use 'hg resolve --mark') + unresolved merge conflicts (see hg help resolve) + [1] + + $ echo bar > b + $ hg resolve -m + (no more unresolved files) + continue: hg grab --continue + + $ hg grab --continue + $ hg phase -r . + 10: secret + +No merge conflicts + + $ hg up d46dc301d92f + 0 files updated, 0 files merged, 3 files removed, 0 files unresolved + $ echo foo > l + $ hg add l + $ hg ci -qm "added l" --secret + + $ hg phase -r . + 11: secret + + $ hg glog + @ 11:508d572e7053 added l + | + | o 10:cd90ed194449 add b + | | + | o 9:902d4f4602bb foo to b + | | + | o 8:44e155eb95c7 add c + | | + | o 7:2ccc03d1d096 foo to c + |/ + o 4:d46dc301d92f add y + | + o 3:8e224524cd09 add x + | + o 0:1f0dee641bb7 add a + + $ hg up cd90ed194449 + 3 files updated, 0 files merged, 1 files removed, 0 files unresolved + + $ hg grab -r 508d572e7053 + grabbing 11:508d572e7053 "added l" + + $ hg phase -r . + 12: secret diff -r bee9fee8f36b -r 1baf32675ec6 tests/test-issue-5720.t --- a/tests/test-issue-5720.t Sun Jan 21 16:55:02 2018 -0500 +++ b/tests/test-issue-5720.t Tue Feb 06 13:00:28 2018 +0100 @@ -70,6 +70,7 @@ $ echo c2 > a $ hg resolve -m (no more unresolved files) + continue: hg evolve --continue Continue the evolution $ hg evolve --continue diff -r bee9fee8f36b -r 1baf32675ec6 tests/test-prev-next.t --- a/tests/test-prev-next.t Sun Jan 21 16:55:02 2018 -0500 +++ b/tests/test-prev-next.t Tue Feb 06 13:00:28 2018 +0100 @@ -190,7 +190,6 @@ 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) diff -r bee9fee8f36b -r 1baf32675ec6 tests/test-stabilize-conflict.t --- a/tests/test-stabilize-conflict.t Sun Jan 21 16:55:02 2018 -0500 +++ b/tests/test-stabilize-conflict.t Tue Feb 06 13:00:28 2018 +0100 @@ -167,6 +167,7 @@ $ safesed 's/dix/ten/' babar $ hg resolve --all -m (no more unresolved files) + continue: hg evolve --continue $ hg evolve --continue evolving 4:71c18f70c34f "babar count up to fifteen" $ hg resolve -l diff -r bee9fee8f36b -r 1baf32675ec6 tests/test-stabilize-result.t --- a/tests/test-stabilize-result.t Sun Jan 21 16:55:02 2018 -0500 +++ b/tests/test-stabilize-result.t Tue Feb 06 13:00:28 2018 +0100 @@ -98,6 +98,7 @@ [255] $ hg resolve -m a (no more unresolved files) + continue: hg evolve --continue $ hg evolve --continue evolving 4:3655f0f50885 "newer a" @@ -127,8 +128,7 @@ Get a successors of 8 on it $ hg grab 1cf0aacfd363 - rebasing 6:1cf0aacfd363 "newer a" - ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) + grabbing 6:1cf0aacfd363 "newer a" Add real change to the successors @@ -140,7 +140,7 @@ $ hg phase --hidden --public 1cf0aacfd363 1 new phase-divergent changesets $ glog - @ 9:(73b15c7566e9|d5c7ef82d003)@default\(draft\) bk:\[\] newer a (re) + @ 9:99c21c89bcef@default(draft) bk:[] newer a | o 7:7bc2f5967f5e@default(draft) bk:[] add c | @@ -156,10 +156,10 @@ $ hg evolve --any --dry-run --phase-divergent recreate:[9] newer a atop:[6] newer a - hg rebase --rev d5c7ef82d003 --dest 66719795a494; + hg rebase --rev 99c21c89bcef --dest 66719795a494; hg update 1cf0aacfd363; - hg revert --all --rev d5c7ef82d003; - hg commit --msg "phase-divergent update to d5c7ef82d003" + hg revert --all --rev 99c21c89bcef; + hg commit --msg "phase-divergent update to 99c21c89bcef" $ hg evolve --any --confirm --phase-divergent recreate:[9] newer a atop:[6] newer a @@ -172,10 +172,10 @@ perform evolve? [Ny] y rebasing to destination parent: 66719795a494 computing new diff - committed as 8c986e77913c - working directory is now at 8c986e77913c + committed as 3d968e0b3097 + working directory is now at 3d968e0b3097 $ glog - @ 11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: + @ 11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: | | o 7:7bc2f5967f5e@default(draft) bk:[] add c | | @@ -204,7 +204,7 @@ $ glog @ 12:3932c176bbaa@default(draft) bk:[] More addition | - | o 11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: + | o 11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: | | o | 7:7bc2f5967f5e@default(draft) bk:[] add c | | @@ -233,7 +233,7 @@ | | * 13:d2f173e25686@default(draft) bk:[] More addition |/ - | o 11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: + | o 11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: | | o | 7:7bc2f5967f5e@default(draft) bk:[] add c | | @@ -283,7 +283,7 @@ $ glog @ 15:f344982e63c4@default(draft) bk:[] More addition | - | o 11:8c986e77913c@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: + | o 11:3d968e0b3097@default(draft) bk:[] phase-divergent update to 1cf0aacfd363: | | o | 7:7bc2f5967f5e@default(draft) bk:[] add c | | diff -r bee9fee8f36b -r 1baf32675ec6 tests/test-tutorial.t --- a/tests/test-tutorial.t Sun Jan 21 16:55:02 2018 -0500 +++ b/tests/test-tutorial.t Tue Feb 06 13:00:28 2018 +0100 @@ -665,11 +665,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 9:fac207dec9f5 "SPAM SPAM" (tip) + grabbing 9: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 |/ @@ -775,7 +774,7 @@ # User test # Date 0 0 # Thu Jan 01 00:00:00 1970 +0000 - # Node ID a224f2a4fb9f9f828f608959912229d7b38b26de + # Node ID 57e9caedbcb8575a01c128db9d1bcbd624ef2115 # Parent 41aff6a42b7578ec7ec3cb2041633f1ca43cca96 SPAM SPAM @@ -808,14 +807,13 @@ for simplicity sake we get the bathroom change in line again $ hg grab 10b8aeaa8cc8 - rebasing 8:10b8aeaa8cc8 "bathroom stuff" + grabbing 8: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 | @@ -1016,12 +1014,12 @@ adding file changes added 1 changesets with 1 changes to 1 files 1 new obsolescence markers - new changesets 75954b8cd933 + new changesets 4710c0968793 (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 | @@ -1039,7 +1037,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 | @@ -1072,12 +1070,12 @@ adding file changes added 1 changesets with 1 changes to 1 files 1 new obsolescence markers - new changesets 75954b8cd933 + new changesets 4710c0968793 (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 | @@ -1093,7 +1091,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 @@ -1105,13 +1103,13 @@ But at the same time, locally, this same "bathroom changeset" was updated. $ cd ../local - $ hg up 75954b8cd933 -q + $ hg up 4710c0968793 -q $ 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 | @@ -1214,21 +1212,20 @@ adding file changes added 1 changesets with 1 changes to 1 files 1 new orphan changesets - new changesets bf1b0d202029 + new changesets e4e4fa805d92 (run 'hg update' to get a working copy) - The new changeset "animal" is based on an old changeset of "bathroom". You can see both version showing up in the log. $ hg log -G - * bf1b0d202029 (draft): animals + * 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 | @@ -1361,7 +1358,7 @@ $ hg push other pushing to $TESTTMP/other (glob) searching for changes - abort: push includes orphan changeset: bf1b0d202029! + abort: push includes orphan changeset: e4e4fa805d92! (use 'hg evolve' to get a stable history or --force to ignore warnings) [255] @@ -1374,7 +1371,7 @@ $ hg evolve --dry-run move:[13] animals atop:[12] bathroom stuff - hg rebase -r bf1b0d202029 -d a44c85f957d3 + hg rebase -r e4e4fa805d92 -d 682004e81e71 Let's do it @@ -1382,16 +1379,16 @@ move:[13] animals atop:[12] 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 | @@ -1520,13 +1517,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 ---------------------------------------------- @@ -1546,16 +1543,16 @@ adding manifests adding file changes added 1 changesets with 1 changes to 1 files - new changesets 99f039c5ec9e + new changesets fc41faf45288 (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 | @@ -1674,9 +1671,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 orphan changesets @@ -1685,13 +1682,13 @@ is neither dead or obsolete. My repository is in an unstable state again. $ hg log -G - * 99f039c5ec9e (draft): SPAM SPAM SPAM + * 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 | @@ -1809,7 +1806,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 @@ -1837,14 +1834,14 @@ move:[15] SPAM SPAM SPAM atop:[12] 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 |