--- 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)
----------------------
--- 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 <rev> 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 <old> "
- "--succ <new>` 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)
--- 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
--- /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 <peter.arrenbrecht@gmail.com>
+# Logilab SA <contact@logilab.fr>
+# Pierre-Yves David <pierre-yves.david@ens-lyon.org>
+# Patrick Mezard <patrick@mezard.eu>
+#
+# 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 <old> "
+ "--succ <new>` 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)
--- 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)
--- 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/'
--- 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.
--- /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)
--- 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()
--- 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'
--- 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"
--- /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 <<EOF
+ > [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
--- 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
--- 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)
--- 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
--- 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
| |
--- 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
|