# HG changeset patch # User Pierre-Yves David # Date 1455150760 0 # Node ID e359d33856c382b264facbe5c4c8d16b1e191578 # Parent 3c7f98753e3760c781d9f5f931b8ff366d30a5a1# Parent c2739551ea4e7b356c3f4b7f54b5c04b96d1e78d merge with new stable through 3.5 and 3.6 diff -r 3c7f98753e37 -r e359d33856c3 Makefile --- a/Makefile Thu Feb 04 11:01:35 2016 +0000 +++ b/Makefile Thu Feb 11 00:32:40 2016 +0000 @@ -1,5 +1,3 @@ -PYTHON=python -HG=`which hg` VERSION=$(shell python setup.py --version) @@ -11,21 +9,6 @@ all: help -tests: - cd tests && $(PYTHON) run-tests.py --with-hg=$(HG) $(TESTFLAGS) - -test-%: - cd tests && $(PYTHON) run-tests.py --with-hg=$(HG) $(TESTFLAGS) $@ - -tests-%: - @echo "Path to crew repo is $(CREW) - set this with CREW= if needed." - hg -R $(CREW) checkout $$(echo $@ | sed s/tests-//) && \ - (cd $(CREW) ; $(MAKE) clean ) && \ - cd tests && $(PYTHON) $(CREW)/tests/run-tests.py $(TESTFLAGS) - -all-version-tests: tests-1.3.1 tests-1.4.3 tests-1.5.4 \ - tests-1.6.4 tests-1.7.5 tests-1.8 tests-tip - deb-prepare: python setup.py sdist --dist-dir .. mv -f ../hg-evolve-$(VERSION).tar.gz ../mercurial-evolve_$(VERSION).orig.tar.gz @@ -34,5 +17,3 @@ mv hg-evolve-$(VERSION) ../mercurial-evolve_$(VERSION).orig cp -r debian/ ../mercurial-evolve_$(VERSION).orig/ @cd ../mercurial-evolve_$(VERSION).orig && echo 'debian build directory ready at' `pwd` - -.PHONY: tests all-version-tests diff -r 3c7f98753e37 -r e359d33856c3 README --- a/README Thu Feb 04 11:01:35 2016 +0000 +++ b/README Thu Feb 11 00:32:40 2016 +0000 @@ -28,17 +28,17 @@ Contribute ========== -Bugs are to be reported on the mercurial's bug tracker: http://bz.selenic.com/ +Bugs are to be reported on the mercurial's bug tracker: http://bz.mercurial-scm.com/ Use the the "evolution" component. Please use the patchbomb extension to send email to mercurial devel. Please make sure to use the evolve-ext flag when doing so. You can use a command like this: - hg email --to mercurial-devel@selenic.com --flag evolve-ext --rev '' + hg email --to mercurial-devel@mercurial-scm.org --flag evolve-ext --rev '' See also -http://mercurial.selenic.com/wiki/ContributingChanges#Patch_descriptions +http://mercurial-scm.org/wiki/ContributingChanges#Patch_descriptions for guidelines on the patch description. Please don't forget to update and run the tests when you fix a bug or @@ -58,7 +58,14 @@ 5.3.0 -- -- split: add a new command to split changesets +- split: add a new command to split changesets, +- tests: drop our copy of 'run-tests.py' use core one instead, +- bookmark: do all bookmark movement within a transaction. +- evolve: compatibility with Mercurial 3.7 +- evolve: support merge with a single obsolete parent. +- evolve: prevent added file to be marked as unknown if evolve fails (issue4966) +- evolve: stop relying on graftstate file for save evolve state + (for `hg evolve --continue`) 5.2.2 -- diff -r 3c7f98753e37 -r e359d33856c3 debian/rules --- a/debian/rules Thu Feb 04 11:01:35 2016 +0000 +++ b/debian/rules Thu Feb 11 00:32:40 2016 +0000 @@ -8,9 +8,13 @@ dh_auto_build $(MAKE) -C docs +hgsrc_defined: + # Use "! -z" instead of "-n", because "-n" without arguments is true + test ! -z $(HGSRC) && test -d $(HGSRC) || (echo "$(HGSRC) is not a directory"; false) + ifeq (,$(filter nocheck, $(DEB_BUILD_OPTIONS))) -override_dh_auto_test: - cd tests && python run-tests.py --with-hg=`which hg` --blacklist=$(CURDIR)/debian/test-blacklist +override_dh_auto_test: hgsrc_defined + cd tests && python $(HGSRC)/tests/run-tests.py --with-hg=$(HGSRC)/hg --blacklist=$(CURDIR)/debian/test-blacklist endif override_dh_python2: diff -r 3c7f98753e37 -r e359d33856c3 debian/test-blacklist --- a/debian/test-blacklist Thu Feb 04 11:01:35 2016 +0000 +++ b/debian/test-blacklist Thu Feb 11 00:32:40 2016 +0000 @@ -1,3 +1,4 @@ test-drop.t +test-inhibit.t test-simple4server.t tests/test-simple4server-bundle2.t diff -r 3c7f98753e37 -r e359d33856c3 hgext/drophack.py --- a/hgext/drophack.py Thu Feb 04 11:01:35 2016 +0000 +++ b/hgext/drophack.py Thu Feb 11 00:32:40 2016 +0000 @@ -150,7 +150,8 @@ stripmarker(ui, repo, markers) # strip the changeset with timed(ui, 'strip nodes'): - repair.strip(ui, repo, list(allnodes), backup="all", topic='drophack') + repair.strip(ui, repo, list(allnodes), backup="all", + topic='drophack') finally: lockmod.release(lock, wlock) diff -r 3c7f98753e37 -r e359d33856c3 hgext/evolve.py --- a/hgext/evolve.py Thu Feb 04 11:01:35 2016 +0000 +++ b/hgext/evolve.py Thu Feb 11 00:32:40 2016 +0000 @@ -67,6 +67,7 @@ import collections import socket import errno +import struct sha1re = re.compile(r'\b[0-9a-f]{6,40}\b') import mercurial @@ -86,7 +87,7 @@ # Flags for enabling optional parts of evolve commandopt = 'allnewcommands' -from mercurial import bookmarks +from mercurial import bookmarks as bookmarksmod from mercurial import cmdutil from mercurial import commands from mercurial import context @@ -116,6 +117,7 @@ command = cmdutil.command(cmdtable) _pack = struct.pack +_unpack = struct.unpack if gboptsmap is not None: memfilectx = context.memfilectx @@ -124,7 +126,8 @@ def memfilectx(repo, *args, **kwargs): return oldmemfilectx(*args, **kwargs) else: - raise ImportError('evolve needs version %s or above' % min(testedwith.split())) + raise ImportError('evolve needs version %s or above' % + min(testedwith.split())) aliases, entry = cmdutil.findcmd('commit', commands.table) hasinteractivemode = any(['interactive' in e for e in entry[1]]) @@ -415,8 +418,8 @@ if not matchingevolvecommands: raise error.Abort(_('unknown command: %s') % cmd) elif len(matchingevolvecommands) > 1: - raise error.Abort(_('ambiguous command specification: "%s" matches %r') - % (cmd, matchingevolvecommands)) + msg = _('ambiguous command specification: "%s" matches %r') + raise error.Abort(msg % (cmd, matchingevolvecommands)) else: whitelist.add(matchingevolvecommands[0]) for disabledcmd in set(cmdtable) - whitelist: @@ -747,7 +750,7 @@ """ try: return orig(repo, *args, **opts) - except util.Abort, ex: + except error.Abort as ex: hint = _("use 'hg evolve' to get a stable history " "or --force to ignore warnings") if (len(ex.args) >= 1 @@ -764,12 +767,17 @@ else: ui.note(s) - nbunstable = len(getrevs(repo, 'unstable')) - nbbumped = len(getrevs(repo, 'bumped')) - nbdivergent = len(getrevs(repo, 'divergent')) - write('unstable: %i changesets\n', nbunstable) - write('bumped: %i changesets\n', nbbumped) - write('divergent: %i changesets\n', nbdivergent) + # util.versiontuple was introduced in 3.6.2 + if not util.safehasattr(util, 'versiontuple'): + nbunstable = len(getrevs(repo, 'unstable')) + nbbumped = len(getrevs(repo, 'bumped')) + nbdivergent = len(getrevs(repo, 'divergent')) + write('unstable: %i changesets\n', nbunstable) + write('bumped: %i changesets\n', nbbumped) + write('divergent: %i changesets\n', nbdivergent) + else: + # In 3.6.2, summary in core gained this feature, no need to display it + pass @eh.extsetup def obssummarysetup(ui): @@ -789,13 +797,13 @@ if rebase: extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) except KeyError: - pass # rebase not found + pass # rebase not found try: histedit = extensions.find('histedit') if histedit: extensions.wrapcommand(histedit.cmdtable, 'histedit', warnobserrors) except KeyError: - pass # histedit not found + pass # histedit not found ##################################################################### ### Old Evolve extension content ### @@ -890,24 +898,27 @@ tr.close() return newid, created finally: - lockmod.release(lock, wlock, tr) - -class MergeFailure(util.Abort): + lockmod.release(tr, lock, wlock) + +class MergeFailure(error.Abort): pass -def relocate(repo, orig, dest, keepbranch=False): +def relocate(repo, orig, dest, pctx=None, keepbranch=False): """rewrite on dest""" if orig.rev() == dest.rev(): - raise util.Abort(_('tried to relocate a node on top of itself'), + 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 not orig.p2().rev() == node.nullrev: - raise util.Abort( - 'no support for evolving merge changesets yet', - hint="Redo the merge and use `hg prune --succ ` to obsolete the old one") + 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() + destbookmarks = repo.nodebookmarks(dest.node()) nodesrc = orig.node() destphase = repo[nodesrc].phase() @@ -937,37 +948,16 @@ repo.ui.note(_('The stale commit message reference to %s could ' 'not be updated\n') % sha1) - tr = repo.transaction('relocate') + tr = repo.currenttransaction() + assert tr is not None try: try: - if repo['.'].rev() != dest.rev(): - merge.update(repo, dest, False, True, False) - if bmactive(repo): - repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo)) - bmdeactivate(repo) - if keepbranch: - repo.dirstate.setbranch(orig.branch()) - r = merge.graft(repo, orig, orig.p1(), ['local', 'graft']) + r = _evolvemerge(repo, orig, dest, pctx, keepbranch) if r[-1]: #some conflict - raise util.Abort( + raise error.Abort( 'unresolved merge conflicts (see hg help resolve)') - 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, 'rebase') - # 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) - except util.Abort, exc: + nodenew = _relocatecommit(repo, orig, commitmsg) + except error.Abort as exc: repo.dirstate.beginparentchange() repo.setparents(repo['.'].node(), nullid) writedirstate(repo.dirstate, tr) @@ -977,25 +967,12 @@ class LocalMergeFailure(MergeFailure, exc.__class__): pass exc.__class__ = LocalMergeFailure + tr.close() # to keep changes in this transaction (e.g. dirstate) raise oldbookmarks = repo.nodebookmarks(nodesrc) - if nodenew is not None: - phases.retractboundary(repo, tr, destphase, [nodenew]) - obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))]) - for book in oldbookmarks: - repo._bookmarks[book] = nodenew - else: - obsolete.createmarkers(repo, [(repo[nodesrc], ())]) - # Behave like rebase, move bookmarks to dest - for book in oldbookmarks: - repo._bookmarks[book] = dest.node() - for book in destbookmarks: # restore bookmark that rebase move - repo._bookmarks[book] = dest.node() - if oldbookmarks or destbookmarks: - repo._bookmarks.recordchange(tr) - tr.close() + _finalizerelocate(repo, orig, dest, nodenew, tr) finally: - tr.release() + pass # TODO: remove this redundant try/finally block return nodenew def _bookmarksupdater(repo, oldid, tr): @@ -1016,14 +993,14 @@ ### bookmarks api compatibility layer ### def bmdeactivate(repo): try: - return bookmarks.deactivate(repo) + return bookmarksmod.deactivate(repo) except AttributeError: - return bookmarks.unsetcurrent(repo) + return bookmarksmod.unsetcurrent(repo) def bmactivate(repo, book): try: - return bookmarks.activate(repo, book) + return bookmarksmod.activate(repo, book) except AttributeError: - return bookmarks.setcurrent(repo, book) + return bookmarksmod.setcurrent(repo, book) def bmactive(repo): try: @@ -1156,7 +1133,7 @@ store.create(tr, mark[0], mark[1], mark[2], marks[3], parents=parents) if len(store._all) - before: - ui.write('created new markers for %i\n' % rev) + ui.write(_('created new markers for %i\n') % rev) ui.progress(pgop, idx, total=pgtotal) tr.close() ui.progress(pgop, None) @@ -1186,7 +1163,7 @@ store = repo.obsstore unfi = repo.unfiltered() nm = unfi.changelog.nodemap - ui.write('markers total: %9i\n' % len(store._all)) + ui.write(_('markers total: %9i\n') % len(store._all)) sucscount = [0, 0 , 0, 0] known = 0 parentsdata = 0 @@ -1195,7 +1172,7 @@ # a cluster is a (set(nodes), set(markers)) tuple clustersmap = {} # same data using parent information - pclustersmap= {} + pclustersmap = {} for mark in store: if mark[0] in nm: known += 1 @@ -1228,47 +1205,50 @@ fc = (frozenset(c[0]), frozenset(c[1])) for n in fc[0]: pclustersmap[n] = fc - ui.write(' for known precursors: %9i\n' % known) - ui.write(' with parents data: %9i\n' % parentsdata) + ui.write((' for known precursors: %9i\n' % known)) + ui.write((' with parents data: %9i\n' % parentsdata)) # successors data - ui.write('markers with no successors: %9i\n' % sucscount[0]) - ui.write(' 1 successors: %9i\n' % sucscount[1]) - ui.write(' 2 successors: %9i\n' % sucscount[2]) - ui.write(' more than 2 successors: %9i\n' % sucscount[3]) + ui.write(('markers with no successors: %9i\n' % sucscount[0])) + ui.write((' 1 successors: %9i\n' % sucscount[1])) + ui.write((' 2 successors: %9i\n' % sucscount[2])) + ui.write((' more than 2 successors: %9i\n' % sucscount[3])) # meta data info - ui.write(' available keys:\n') + ui.write((' available keys:\n')) for key in sorted(metakeys): - ui.write(' %15s: %9i\n' % (key, metakeys[key])) + ui.write((' %15s: %9i\n' % (key, metakeys[key]))) allclusters = list(set(clustersmap.values())) allclusters.sort(key=lambda x: len(x[1])) - ui.write('disconnected clusters: %9i\n' % len(allclusters)) + ui.write(('disconnected clusters: %9i\n' % len(allclusters))) ui.write(' any known node: %9i\n' % len([c for c in allclusters if [n for n in c[0] if nm.get(n) is not None]])) if allclusters: nbcluster = len(allclusters) - ui.write(' smallest length: %9i\n' % len(allclusters[0][1])) - ui.write(' longer length: %9i\n' % len(allclusters[-1][1])) + ui.write((' smallest length: %9i\n' % len(allclusters[0][1]))) + ui.write((' longer length: %9i\n' + % len(allclusters[-1][1]))) median = len(allclusters[nbcluster//2][1]) - ui.write(' median length: %9i\n' % median) + ui.write((' median length: %9i\n' % median)) mean = sum(len(x[1]) for x in allclusters) // nbcluster - ui.write(' mean length: %9i\n' % mean) + ui.write((' mean length: %9i\n' % mean)) allpclusters = list(set(pclustersmap.values())) allpclusters.sort(key=lambda x: len(x[1])) - ui.write(' using parents data: %9i\n' % len(allpclusters)) + ui.write((' using parents data: %9i\n' % len(allpclusters))) ui.write(' any known node: %9i\n' % len([c for c in allclusters if [n for n in c[0] if nm.get(n) is not None]])) if allpclusters: nbcluster = len(allpclusters) - ui.write(' smallest length: %9i\n' % len(allpclusters[0][1])) - ui.write(' longer length: %9i\n' % len(allpclusters[-1][1])) + ui.write((' smallest length: %9i\n' + % len(allpclusters[0][1]))) + ui.write((' longer length: %9i\n' + % len(allpclusters[-1][1]))) median = len(allpclusters[nbcluster//2][1]) - ui.write(' median length: %9i\n' % median) + ui.write((' median length: %9i\n' % median)) mean = sum(len(x[1]) for x in allpclusters) // nbcluster - ui.write(' mean length: %9i\n' % mean) + ui.write((' mean length: %9i\n' % mean)) def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category): """Resolve the troubles affecting one revision""" @@ -1367,8 +1347,8 @@ else: l = len(troubled[targetcat]) if l: - hint = (_("%d other %s in the repository, do you want --any or --rev") - % (l, targetcat)) + hint = _("%d other %s in the repository, do you want --any " + "or --rev") % (l, targetcat) else: othertroubles = [] for cat in unselectedcategories: @@ -1389,7 +1369,7 @@ def _cleanup(ui, repo, startnode, showprogress): if showprogress: - ui.progress('evolve', None) + ui.progress(_('evolve'), None) if repo['.'] != startnode: ui.status(_('working directory is now at %s\n') % repo['.']) @@ -1442,7 +1422,7 @@ for p in repo[r].parents(): try: succ = _singlesuccessor(repo, p) - except MultipleSuccessorsError, exc: + except MultipleSuccessorsError as exc: dependencies[r] = exc.successorssets continue if succ in revs: @@ -1476,7 +1456,8 @@ if revopt: revs = scmutil.revrange(repo, revopt) & revs elif not anyopt and targetcat == 'unstable': - revs = set(_aspiringdescendant(repo, repo.revs('(.::) - obsolete()::'))) + revs = set(_aspiringdescendant(repo, + repo.revs('(.::) - obsolete()::'))) if targetcat == 'divergent': # Pick one divergent per group of divergents revs = _dedupedivergents(repo, revs) @@ -1534,22 +1515,24 @@ _('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')), + ('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')), ('', 'divergent', False, _('solves only divergent changesets')), ('', 'unstable', False, _('solves only unstable changesets (default)')), - ('a', 'all', False, _('evolve all troubled changesets related to the current ' - 'working directory and its descendants')), + ('a', 'all', False, _('evolve all troubled changesets related to the ' + 'current working directory and its descendants')), ('c', 'continue', False, _('continue an interrupted evolution')), ] + 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: + 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 @@ -1557,53 +1540,57 @@ - 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. + 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.) + 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 explicity request such behavior (see below). + 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 explicity + 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. + 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. + 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 + 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``. + 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``. """ @@ -1620,15 +1607,16 @@ targetcat = 'unstable' if 1 < len(specifiedcategories): msg = _('cannot specify more than one trouble category to solve (yet)') - raise util.Abort(msg) + raise error.Abort(msg) elif len(specifiedcategories) == 1: targetcat = specifiedcategories[0] elif repo['.'].obsolete(): - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + displayer = cmdutil.show_changeset(ui, repo, + {'template': shorttemplate}) # no args and parent is obsolete, update to successors try: ctx = repo[_singlesuccessor(repo, repo['.'])] - except MultipleSuccessorsError, exc: + except MultipleSuccessorsError as exc: repo.ui.write_err('parent is obsolete with multiple successors:\n') for ln in exc.successorssets: for n in ln: @@ -1657,23 +1645,34 @@ def progresscb(): if revopt or allopt: - ui.progress('evolve', seen, unit='changesets', total=count) + ui.progress(_('evolve'), seen, unit='changesets', total=count) # Continuation handling if contopt: if anyopt: - raise util.Abort('cannot specify both "--any" and "--continue"') + raise error.Abort('cannot specify both "--any" and "--continue"') if allopt: - raise util.Abort('cannot specify both "--all" and "--continue"') - graftcmd = commands.table['graft'][0] - return graftcmd(ui, repo, old_obsolete=True, **{'continue': True}) + raise error.Abort('cannot specify both "--all" and "--continue"') + state = _evolvestateread(repo) + if state is None: + raise error.Abort('no evolve to continue') + orig = repo[state['current']] + # XXX This is a terrible terrible hack, please get rid of it. + repo.opener.write('graftstate', orig.hex() + '\n') + try: + graftcmd = commands.table['graft'][0] + ret = graftcmd(ui, repo, old_obsolete=True, **{'continue': True}) + _evolvestatedelete(repo) + return ret + finally: + util.unlinkpath(repo.join('graftstate'), ignoremissing=True) cmdutil.bailifchanged(repo) if revopt and allopt: - raise util.Abort('cannot specify both "--rev" and "--all"') + raise error.Abort('cannot specify both "--rev" and "--all"') if revopt and anyopt: - raise util.Abort('cannot specify both "--rev" and "--any"') + raise error.Abort('cannot specify both "--rev" and "--any"') revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat) @@ -1749,13 +1748,20 @@ def _solveunstable(ui, repo, orig, dryrun=False, confirm=False, progresscb=None): """Stabilize an unstable changeset""" - obs = orig.parents()[0] - if not obs.obsolete() and len(orig.parents()) == 2: - obs = orig.parents()[1] # second parent is obsolete ? - - if not obs.obsolete(): - ui.warn("cannot solve instability of %s, skipping\n" % orig) + pctx = orig.p1() + if len(orig.parents()) == 2: + if not pctx.obsolete(): + pctx = orig.p2() # second parent is obsolete ? + elif orig.p2().obsolete(): + raise error.Abort(_("no support for evolving merge changesets " + "with two obsolete parents yet"), + hint=_("Redo the merge and use `hg prune " + "--succ ` to obsolete the old one")) + + if not pctx.obsolete(): + ui.warn(_("cannot solve instability of %s, skipping\n") % orig) return False + obs = pctx newer = obsolete.successorssets(repo, obs.node()) # search of a parent which is not killed while not newer or newer == [()]: @@ -1765,7 +1771,8 @@ obs = obs.parents()[0] newer = obsolete.successorssets(repo, obs.node()) if len(newer) > 1: - msg = _("skipping %s: divergent rewriting. can't choose destination\n") % obs + msg = _("skipping %s: divergent rewriting. can't choose " + "destination\n") % obs ui.write_err(msg) return 2 targets = newer[0] @@ -1790,7 +1797,7 @@ repo.ui.write(_('atop:')) displayer.show(target) if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': - raise util.Abort(_('evolve aborted by user')) + raise error.Abort(_('evolve aborted by user')) if progresscb: progresscb() todo = 'hg rebase -r %s -d %s\n' % (orig, target) if dryrun: @@ -1800,9 +1807,9 @@ if progresscb: progresscb() keepbranch = orig.p1().branch() != orig.branch() try: - relocate(repo, orig, target, keepbranch) + relocate(repo, orig, target, pctx, keepbranch) except MergeFailure: - repo.opener.write('graftstate', orig.hex() + '\n') + _evolvestatewrite(repo, {'current': orig.node()}) repo.ui.write_err(_('evolve failed!\n')) repo.ui.write_err( _('fix conflict and run "hg evolve --continue"' @@ -1822,7 +1829,8 @@ 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 not handled yet\n') % prec + msg = _('skipping: %s: public version is a merge, ' + 'this is not handled yet\n') % prec ui.write_err(msg) return 2 @@ -1833,19 +1841,20 @@ repo.ui.write(_('atop:')) displayer.show(prec) if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': - raise util.Abort(_('evolve aborted by user')) + 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 "bumped update to %s"') + repo.ui.write(('hg update %s;\n' % prec)) + repo.ui.write(('hg revert --all --rev %s;\n' % bumped)) + repo.ui.write(('hg commit --msg "bumped update to %s"')) return 0 if progresscb: progresscb() newid = tmpctx = None tmpctx = bumped # Basic check for common parent. Far too complicated and fragile - tr = repo.transaction('bumped-stabilize') + tr = repo.currenttransaction() + assert tr is not None bmupdate = _bookmarksupdater(repo, bumped.node(), tr) try: if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)): @@ -1911,10 +1920,9 @@ obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))], flag=obsolete.bumpedfix) bmupdate(newid) - tr.close() repo.ui.status(_('committed as %s\n') % node.short(newid)) finally: - tr.release() + pass # TODO: remove this redundant try/finally block # reroute the working copy parent to the new changeset repo.dirstate.beginparentchange() repo.dirstate.setparents(newid, node.nullid) @@ -1927,7 +1935,8 @@ base, others = divergentdata(divergent) if len(others) > 1: othersstr = "[%s]" % (','.join([str(i) for i in others])) - msg = _("skipping %d:divergent with a changeset that got splitted into multiple ones:\n" + msg = _("skipping %d:divergent with a changeset that got splitted" + " into multiple ones:\n" "|[%s]\n" "| This is not handled by automatic evolution yet\n" "| You have to fallback to manual handling with commands " @@ -1936,12 +1945,13 @@ "| - hg prune\n" "| \n" "| You should contact your local evolution Guru for help.\n" - % (divergent, othersstr)) + ) % (divergent, othersstr) ui.write_err(msg) return 2 other = others[0] if len(other.parents()) > 1: - msg = _("skipping %s: divergent changeset can't be a merge (yet)\n" % divergent) + msg = _("skipping %s: divergent changeset can't be " + "a merge (yet)\n") % divergent ui.write_err(msg) hint = _("You have to fallback to solving this by hand...\n" "| This probably means redoing the merge and using \n" @@ -1949,7 +1959,8 @@ 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) + 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" @@ -1957,7 +1968,7 @@ "| 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}) + ) % {'d': divergent, 'o': other} ui.write_err(msg) ui.write_err(hint) return 2 @@ -1971,35 +1982,45 @@ ui.write(_('base: ')) displayer.show(base) if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y': - raise util.Abort(_('evolve aborted by user')) + 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) + 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 divergent changeset\n')) if progresscb: progresscb() - stats = merge.update(repo, - other.node(), - branchmerge=True, - force=False, - partial=None, - ancestor=base.node(), - mergeancestor=True) + if 'partial' in merge.update.__doc__: + # Mercurial < 43c00ca887d1 (3.7) + stats = merge.update(repo, + other.node(), + branchmerge=True, + force=False, + partial=None, + ancestor=base.node(), + mergeancestor=True) + else: + 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 abandon\n")) if stats[3] > 0: - raise util.Abort('merge conflict between several amendments ' + raise error.Abort('merge conflict between several amendments ' '(this is not automated yet)', hint="""/!\ You can try: /!\ * manual merge + resolve => new cset X @@ -2009,8 +2030,11 @@ /!\ * hg kill -n Y W Z """) if progresscb: progresscb() - tr = repo.transaction('stabilize-divergent') + emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit') + tr = repo.currenttransaction() + assert tr is not None try: + repo.ui.setconfig('ui', 'allowemptycommit', True) repo.dirstate.beginparentchange() repo.dirstate.setparents(divergent.node(), node.nullid) repo.dirstate.endparentchange() @@ -2023,9 +2047,8 @@ new = repo['.'] obsolete.createmarkers(repo, [(other, (new,))]) phases.retractboundary(repo, tr, other.phase(), [new.node()]) - tr.close() finally: - tr.release() + repo.ui.restoreconfig(emtpycommitallowed) def divergentdata(ctx): """return base, other part of a conflict @@ -2041,7 +2064,7 @@ 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 util.Abort("base of divergent changeset %s not found" % ctx, + raise error.Abort("base of divergent changeset %s not found" % ctx, hint='this case is not yet handled') @@ -2052,7 +2075,8 @@ [('B', 'move-bookmark', False, _('move active bookmark after update')), ('', 'merge', False, _('bring uncommitted change along')), - ('n', 'dry-run', False, _('do not perform actions, just print what would be done'))], + ('n', 'dry-run', False, + _('do not perform actions, just print what would be done'))], '[OPTION]...') def cmdprevious(ui, repo, **opts): """update to parent revision @@ -2062,11 +2086,11 @@ wparents = wkctx.parents() dryrunopt = opts['dry_run'] if len(wparents) != 1: - raise util.Abort('merge in progress') + raise error.Abort('merge in progress') if not opts['merge']: try: cmdutil.bailifchanged(repo) - except error.Abort, exc: + except error.Abort as exc: exc.hint = _('do you want --merge?') raise @@ -2077,21 +2101,25 @@ bm = bmactive(repo) shouldmove = opts.get('move_bookmark') and bm is not None if dryrunopt: - ui.write('hg update %s;\n' % p.rev()) + ui.write(('hg update %s;\n' % p.rev())) if shouldmove: - ui.write('hg bookmark %s -r %s;\n' % (bm, p.rev())) + ui.write(('hg bookmark %s -r %s;\n' % (bm, p.rev()))) else: ret = hg.update(repo, p.rev()) if not ret: + tr = lock = None wlock = repo.wlock() try: + lock = repo.lock() + tr = repo.transaction('previous') if shouldmove: repo._bookmarks[bm] = p.node() - repo._bookmarks.write() + repo._bookmarks.recordchange(tr) else: bmdeactivate(repo) + tr.close() finally: - wlock.release() + lockmod.release(tr, lock, wlock) displayer.show(p) return 0 else: @@ -2105,8 +2133,9 @@ _('move active bookmark after update')), ('', 'merge', False, _('bring uncommitted change along')), ('', 'evolve', False, _('evolve the next changeset if necessary')), - ('n', 'dry-run', False, _('do not perform actions, just print what would be done'))], - '[OPTION]...') + ('n', 'dry-run', False, + _('do not perform actions, just print what would be done'))], + '[OPTION]...') def cmdnext(ui, repo, **opts): """update to next child revision @@ -2118,11 +2147,11 @@ wparents = wkctx.parents() dryrunopt = opts['dry_run'] if len(wparents) != 1: - raise util.Abort('merge in progress') + raise error.Abort('merge in progress') if not opts['merge']: try: cmdutil.bailifchanged(repo) - except error.Abort, exc: + except error.Abort as exc: exc.hint = _('do you want --merge?') raise @@ -2133,25 +2162,29 @@ bm = bmactive(repo) shouldmove = opts.get('move_bookmark') and bm is not None if dryrunopt: - ui.write('hg update %s;\n' % c.rev()) + ui.write(('hg update %s;\n' % c.rev())) if shouldmove: - ui.write('hg bookmark %s -r %s;\n' % (bm, c.rev())) + ui.write(('hg bookmark %s -r %s;\n' % (bm, c.rev()))) else: ret = hg.update(repo, c.rev()) if not ret: + lock = tr = None wlock = repo.wlock() try: + lock = repo.lock() + tr = repo.transaction('next') if shouldmove: repo._bookmarks[bm] = c.node() - repo._bookmarks.write() + repo._bookmarks.recordchange(tr) else: bmdeactivate(repo) + tr.close() finally: - wlock.release() + lockmod.release(tr, lock, wlock) displayer.show(c) result = 0 elif children: - ui.warn("ambigious next changeset:\n") + ui.warn(_("ambigious next changeset:\n")) for c in children: displayer.show(c) ui.warn(_('explicitly update to one of them\n')) @@ -2181,46 +2214,48 @@ return 1 return result -def _reachablefrombookmark(repo, revs, mark): +def _reachablefrombookmark(repo, revs, bookmarks): """filter revisions and bookmarks reachable from the given bookmark yoinked from mq.py """ - marks = repo._bookmarks - if mark not in marks: - raise util.Abort(_("bookmark '%s' not found") % mark) + repomarks = repo._bookmarks + if not bookmarks.issubset(repomarks): + raise error.Abort(_("bookmark '%s' not found") % + ','.join(sorted(bookmarks - set(repomarks.keys())))) # If the requested bookmark is not the only one pointing to a # a revision we have to only delete the bookmark and not strip # anything. revsets cannot detect that case. - uniquebm = True - for m, n in marks.iteritems(): - if m != mark and n == repo[mark].node(): - uniquebm = False - break - if uniquebm: - if util.safehasattr(repair, 'stripbmrevset'): - rsrevs = repair.stripbmrevset(repo, mark) - else: - rsrevs = repo.revs("ancestors(bookmark(%s)) - " - "ancestors(head() and not bookmark(%s)) - " - "ancestors(bookmark() and not bookmark(%s)) - " - "obsolete()", - mark, mark, mark) - revs = set(revs) - revs.update(set(rsrevs)) - revs = sorted(revs) - return marks, revs - -def _deletebookmark(repo, marks, mark): + nodetobookmarks = {} + for mark, node in repomarks.iteritems(): + nodetobookmarks.setdefault(node, []).append(mark) + for marks in nodetobookmarks.values(): + if bookmarks.issuperset(marks): + if util.safehasattr(repair, 'stripbmrevset'): + rsrevs = repair.stripbmrevset(repo, marks[0]) + else: + rsrevs = repo.revs("ancestors(bookmark(%s)) - " + "ancestors(head() and not bookmark(%s)) - " + "ancestors(bookmark() and not bookmark(%s)) - " + "obsolete()", + marks[0], marks[0], marks[0]) + revs = set(revs) + revs.update(set(rsrevs)) + revs = sorted(revs) + return repomarks, revs + +def _deletebookmark(repo, repomarks, bookmarks): wlock = lock = tr = None try: wlock = repo.wlock() lock = repo.lock() tr = repo.transaction('prune') - del marks[mark] - marks.recordchange(tr) + for bookmark in bookmarks: + del repomarks[bookmark] + repomarks.recordchange(tr) tr.close() - repo.ui.write(_("bookmark '%s' deleted\n") % mark) + for bookmark in sorted(bookmarks): + repo.ui.write(_("bookmark '%s' deleted\n") % bookmark) finally: lockmod.release(tr, lock, wlock) @@ -2243,9 +2278,11 @@ ('r', 'rev', [], _("revisions to prune")), ('k', 'keep', None, _("does not modify working copy during prune")), ('', 'biject', False, _("do a 1-1 map between rev and successor ranges")), - ('', 'fold', False, _("record a fold (multiple precursors, one successors)")), - ('', 'split', False, _("record a split (on precursor, multiple successors)")), - ('B', 'bookmark', '', _("remove revs only reachable from given" + ('', 'fold', False, + _("record a fold (multiple precursors, one successors)")), + ('', 'split', False, + _("record a split (on precursor, multiple successors)")), + ('B', 'bookmark', [], _("remove revs only reachable from given" " bookmark"))] + metadataopts, _('[OPTION] [-r] REV...')) # -U --noupdate option to prevent wc update and or bookmarks update ? @@ -2275,7 +2312,7 @@ """ revs = scmutil.revrange(repo, list(revs) + opts.get('rev')) succs = opts['new'] + opts['succ'] - bookmark = opts.get('bookmark') + bookmarks = set(opts.get('bookmark')) metadata = _getmetadata(**opts) biject = opts.get('biject') fold = opts.get('fold') @@ -2283,16 +2320,16 @@ options = [o for o in ('biject', 'fold', 'split') if opts.get(o)] if 1 < len(options): - raise util.Abort(_("can only specify one of %s") % ', '.join(options)) - - if bookmark: - marks,revs = _reachablefrombookmark(repo, revs, bookmark) + raise error.Abort(_("can only specify one of %s") % ', '.join(options)) + + if bookmarks: + repomarks, revs = _reachablefrombookmark(repo, revs, bookmarks) if not revs: # no revisions to prune - delete bookmark immediately - _deletebookmark(repo, marks, bookmark) + _deletebookmark(repo, repomarks, bookmarks) if not revs: - raise util.Abort(_('nothing to prune')) + raise error.Abort(_('nothing to prune')) wlock = lock = tr = None try: @@ -2306,15 +2343,15 @@ cp = repo[p] if not cp.mutable(): # note: createmarkers() would have raised something anyway - raise util.Abort('cannot prune immutable changeset: %s' % cp, + raise error.Abort('cannot prune immutable changeset: %s' % cp, hint='see "hg help phases" for details') precs.append(cp) if not precs: - raise util.Abort('nothing to prune') + raise error.Abort('nothing to prune') if not obsolete.isenabled(repo, obsolete.allowunstableopt): if repo.revs("(%ld::) - %ld", revs, revs): - raise util.Abort(_("cannot prune in the middle of a stack")) + raise error.Abort(_("cannot prune in the middle of a stack")) # defines successors changesets sucs = scmutil.revrange(repo, succs) @@ -2322,17 +2359,17 @@ sucs = tuple(repo[n] for n in sucs) if not biject and len(sucs) > 1 and len(precs) > 1: msg = "Can't use multiple successors for multiple precursors" - raise util.Abort(msg) + raise error.Abort(msg) elif biject and len(sucs) != len(precs): msg = "Can't use %d successors for %d precursors" \ % (len(sucs), len(precs)) - raise util.Abort(msg) + raise error.Abort(msg) elif (len(precs) == 1 and len(sucs) > 1) and not split: msg = "please add --split if you want to do a split" - raise util.Abort(msg) + raise error.Abort(msg) elif len(sucs) == 1 and len(precs) > 1 and not fold: msg = "please add --fold if you want to do a fold" - raise util.Abort(msg) + raise error.Abort(msg) elif biject: relations = [(p, (s,)) for p, s in zip(precs, sucs)] else: @@ -2362,33 +2399,35 @@ descendantrevs = repo.revs("%d::." % newnode.rev()) changedfiles = [] for rev in descendantrevs: - # blindly reset the files, regardless of what actually changed + # blindly reset the files, regardless of what actually + # changed changedfiles.extend(repo[rev].files()) # reset files that only changed in the dirstate too dirstate = repo.dirstate dirchanges = [f for f in dirstate if dirstate[f] != 'n'] changedfiles.extend(dirchanges) - repo.dirstate.rebuild(newnode.node(), newnode.manifest(), changedfiles) + repo.dirstate.rebuild(newnode.node(), newnode.manifest(), + changedfiles) writedirstate(dirstate, tr) else: bookactive = bmactive(repo) # Active bookmark that we don't want to delete (with -B option) # we deactivate and move it before the update and reactivate it # after - movebookmark = bookactive and not bookmark + movebookmark = bookactive and not bookmarks if movebookmark: bmdeactivate(repo) repo._bookmarks[bookactive] = newnode.node() - repo._bookmarks.write() + repo._bookmarks.recordchange(tr) commands.update(ui, repo, newnode.rev()) ui.status(_('working directory now at %s\n') % newnode) if movebookmark: bmactivate(repo, bookactive) # update bookmarks - if bookmark: - _deletebookmark(repo, marks, bookmark) + if bookmarks: + _deletebookmark(repo, repomarks, bookmarks) # create markers obsolete.createmarkers(repo, relations, metadata=metadata) @@ -2583,14 +2622,14 @@ lock = repo.lock() wctx = repo[None] if len(wctx.parents()) <= 0: - raise util.Abort(_("cannot uncommit null changeset")) + raise error.Abort(_("cannot uncommit null changeset")) if len(wctx.parents()) > 1: - raise util.Abort(_("cannot uncommit while merging")) + raise error.Abort(_("cannot uncommit while merging")) old = repo['.'] if old.phase() == phases.public: - raise util.Abort(_("cannot rewrite immutable changeset")) + raise error.Abort(_("cannot rewrite immutable changeset")) if len(old.parents()) > 1: - raise util.Abort(_("cannot uncommit merge changeset")) + raise error.Abort(_("cannot uncommit merge changeset")) oldphase = old.phase() @@ -2599,12 +2638,13 @@ rev = scmutil.revsingle(repo, opts.get('rev')) ctx = repo[None] if ctx.p1() == rev or ctx.p2() == rev: - raise util.Abort(_("cannot uncommit to parent changeset")) + raise error.Abort(_("cannot uncommit to parent changeset")) onahead = old.rev() in repo.changelog.headrevs() - disallowunstable = not obsolete.isenabled(repo, obsolete.allowunstableopt) + disallowunstable = not obsolete.isenabled(repo, + obsolete.allowunstableopt) if disallowunstable and not onahead: - raise util.Abort(_("cannot uncommit in the middle of a stack")) + raise error.Abort(_("cannot uncommit in the middle of a stack")) # Recommit the filtered changeset tr = repo.transaction('uncommit') @@ -2615,7 +2655,7 @@ match = scmutil.match(old, pats, opts) newid = _commitfiltered(repo, old, match, target=rev) if newid is None: - raise util.Abort(_('nothing to uncommit'), + raise error.Abort(_('nothing to uncommit'), hint=_("use --all to uncommit all files")) # Move local changes on filtered changeset obsolete.createmarkers(repo, [(old, (repo[newid],))]) @@ -2634,6 +2674,7 @@ @eh.wrapcommand('commit') def commitwrapper(orig, ui, repo, *arg, **kwargs): + tr = None if kwargs.get('amend', False): wlock = lock = None else: @@ -2656,17 +2697,23 @@ for book in oldbookmarks: repo._bookmarks[book] = new.node() if oldbookmarks: - repo._bookmarks.write() + if not wlock: + wlock = repo.wlock() + if not lock: + lock = repo.lock() + tr = repo.transaction('commit') + repo._bookmarks.recordchange(tr) + tr.close() return result finally: - lockmod.release(lock, wlock) + lockmod.release(tr, lock, wlock) @command('^split', [('r', 'rev', [], _("revision to fold")), ] + commitopts + commitopts2, _('hg split [OPTION]... [-r] REV')) def cmdsplit(ui, repo, *revs, **opts): - """split a changeset into smaller changesets (EXPERIMENTAL) + """split a changeset into smaller changesets By default, split the current revision by prompting for all its hunks to be redistributed into new changesets. @@ -2676,16 +2723,13 @@ tr = wlock = lock = None newcommits = [] - revopt = opts.get('rev') - if revopt: - revs = scmutil.revrange(repo, revopt) - if len(revs) != 1: - raise util.Abort(_("you can only specify one revision to split")) - else: - rev = list(revs)[0] - else: - rev = '.' - + revarg = (list(revs) + opts.get('rev')) or ['.'] + if len(revarg) != 1: + msg = _("more than one revset is given") + hnt = _("use either `hg split ` or `hg split --rev `, not both") + raise error.Abort(msg, hint=hnt) + + rev = scmutil.revsingle(repo, revarg[0]) try: wlock = repo.wlock() lock = repo.lock() @@ -2698,10 +2742,10 @@ if disallowunstable: # XXX We should check head revs if repo.revs("(%d::) - %d", rev, rev): - raise util.Abort(_("cannot split commit: %s not a head") % ctx) + raise error.Abort(_("cannot split commit: %s not a head") % ctx) if len(ctx.parents()) > 1: - raise util.Abort(_("cannot split merge commits")) + raise error.Abort(_("cannot split merge commits")) prev = ctx.p1() bmupdate = _bookmarksupdater(repo, ctx.node(), tr) bookactive = bmactive(repo) @@ -2714,6 +2758,10 @@ def haschanges(): modified, added, removed, deleted = repo.status()[:4] return modified or added or removed or deleted + msg = 'HG: Please, edit the original changeset description.\n\n' + msg += ctx.description() + opts['message'] = msg + opts['edit'] = True while haschanges(): pats = () cmdutil.dorecord(ui, repo, commands.commit, 'commit', False, @@ -2727,7 +2775,7 @@ newcommits.append(repo['.']) break else: - ui.status("no more change to split\n") + ui.status(_("no more change to split\n")) tip = repo[newcommits[-1]] bmupdate(tip.node()) @@ -2767,15 +2815,20 @@ @command('^touch', [('r', 'rev', [], 'revision to update'), ('D', 'duplicate', False, - 'do not mark the new revision as successor of the old one')], + 'do not mark the new revision as successor of the old one'), + ('A', 'allowdivergence', False, + 'mark the new revision as successor of the old one potentially creating ' + 'divergence')], # allow to choose the seed ? _('[-r] revs')) def touch(ui, repo, *revs, **opts): - """create successors that are identical to their predecessors except for the changeset ID + """create successors that are identical to their predecessors except + for the changeset ID This is used to "resurrect" changesets """ duplicate = opts['duplicate'] + allowdivergence = opts['allowdivergence'] revs = list(revs) revs.extend(opts['rev']) if not revs: @@ -2785,7 +2838,8 @@ ui.write_err('no revision to touch\n') return 1 if not duplicate and repo.revs('public() and %ld', revs): - raise util.Abort("can't touch public revision") + raise error.Abort("can't touch public revision") + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) wlock = lock = tr = None try: wlock = repo.wlock() @@ -2802,11 +2856,37 @@ p2 = ctx.p2().node() p1 = newmapping.get(p1, p1) p2 = newmapping.get(p2, p2) + + if not (duplicate or allowdivergence): + # The user hasn't yet decided what to do with the revived + # cset, let's ask + sset = obsolete.successorssets(repo, ctx.node()) + nodivergencerisk = len(sset) == 0 or ( + len(sset) == 1 and + len(sset[0]) == 1 and + repo[sset[0][0]].rev() == ctx.rev() + ) + if nodivergencerisk: + duplicate = False + else: + displayer.show(ctx) + index = ui.promptchoice( + _("reviving this changeset will create divergence" + " unless you make a duplicate.\n(a)llow divergence or" + " (d)uplicate the changeset? $$ &Allowdivergence $$ " + "&Duplicate"), 0) + choice = ['allowdivergence', 'duplicate'][index] + if choice == 'allowdivergence': + duplicate = False + else: + duplicate = True + new, unusedvariable = rewrite(repo, ctx, [], ctx, [p1, p2], commitopts={'extra': extra}) # store touched version to help potential children newmapping[ctx.node()] = new + if not duplicate: obsolete.createmarkers(repo, [(ctx, (repo[new],))]) phases.retractboundary(repo, tr, ctx.phase(), [new]) @@ -2863,7 +2943,7 @@ revs = list(revs) revs.extend(opts['rev']) if not revs: - raise util.Abort(_('no revisions specified')) + raise error.Abort(_('no revisions specified')) revs = scmutil.revrange(repo, revs) @@ -2872,7 +2952,7 @@ extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs) discardedrevs = [r for r in revs if r not in extrevs] if discardedrevs: - raise util.Abort(_("cannot fold non-linear revisions"), + raise error.Abort(_("cannot fold non-linear revisions"), hint=_("given revisions are unrelated to parent " "of working directory")) revs = extrevs @@ -2883,20 +2963,20 @@ roots = repo.revs('roots(%ld)', revs) if len(roots) > 1: - raise util.Abort(_("cannot fold non-linear revisions " + raise error.Abort(_("cannot fold non-linear revisions " "(multiple roots given)")) root = repo[roots.first()] if root.phase() <= phases.public: - raise util.Abort(_("cannot fold public revisions")) + raise error.Abort(_("cannot fold public revisions")) heads = repo.revs('heads(%ld)', revs) if len(heads) > 1: - raise util.Abort(_("cannot fold non-linear revisions " + raise error.Abort(_("cannot fold non-linear revisions " "(multiple heads given)")) head = repo[heads.first()] disallowunstable = not obsolete.isenabled(repo, obsolete.allowunstableopt) if disallowunstable: if repo.revs("(%ld::) - %ld", revs, revs): - raise util.Abort(_("cannot fold chain not ending with a head "\ + raise error.Abort(_("cannot fold chain not ending with a head "\ "or with branching")) wlock = lock = None try: @@ -2918,7 +2998,8 @@ commitopts['edit'] = True newid, unusedvariable = rewrite(repo, root, allctx, head, - [root.p1().node(), root.p2().node()], + [root.p1().node(), + root.p2().node()], commitopts=commitopts) phases.retractboundary(repo, tr, targetphase, [newid]) obsolete.createmarkers(repo, [(ctx, (repo[newid],)) @@ -3026,7 +3107,8 @@ obsexcmsg(repo.ui, "looking for common markers in %i nodes\n" % len(revs)) commonrevs = list(unfi.revs('::%ln', pushop.outgoing.commonheads)) - common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote, commonrevs) + common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote, + commonrevs) revs = list(unfi.revs('%ld - (::%ln)', revs, common)) nodes = [cl.node(r) for r in revs] @@ -3058,8 +3140,9 @@ wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes') wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes') if getattr(exchange, '_pushdiscoveryobsmarkers', None) is None: - ui.warn('evolve: your mercurial version is too old\n' - 'evolve: (running in degraded mode, push will includes all markers)\n') + ui.warn(_('evolve: your mercurial version is too old\n' + 'evolve: (running in degraded mode, push will ' + 'includes all markers)\n')) else: olddisco = exchange.pushdiscoverymapping['obsmarker'] def newdisco(pushop): @@ -3086,7 +3169,8 @@ return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes))) def srv_obshash1(repo, proto, nodes): - return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), version=1)) + return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), + version=1)) @eh.addattr(localrepo.localpeer, 'evoext_obshash') def local_obshash(peer, nodes): @@ -3123,7 +3207,7 @@ common = set() undecided = set(probeset) totalnb = len(undecided) - ui.progress("comparing with other", 0, total=totalnb) + ui.progress(_("comparing with other"), 0, total=totalnb) _takefullsample = setdiscovery._takefullsample if remote.capable('_evoext_obshash_1'): getremotehash = remote.evoext_obshash1 @@ -3141,7 +3225,7 @@ sample = _takefullsample(dag, undecided, size=fullsamplesize) roundtrips += 1 - ui.progress("comparing with other", totalnb - len(undecided), + ui.progress(_("comparing with other"), totalnb - len(undecided), total=totalnb) ui.debug("query %i; still undecided: %i, sample size is: %i\n" % (roundtrips, len(undecided), len(sample))) @@ -3162,7 +3246,7 @@ undecided.difference_update(common) - ui.progress("comparing with other", None, total=totalnb) + ui.progress(_("comparing with other"), None, total=totalnb) result = dag.headsetofconnecteds(common) ui.debug("%d total queries\n" % roundtrips) @@ -3226,10 +3310,11 @@ else: rslts = [] remotedata = _pushkeyescape(markers).items() - totalbytes = sum(len(d) for k,d in remotedata) + totalbytes = sum(len(d) for k, d in remotedata) sentbytes = 0 - obsexcmsg(repo.ui, "pushing %i obsolescence markers in %i pushkey payload (%i bytes)\n" - % (len(markers), len(remotedata), totalbytes), + obsexcmsg(repo.ui, "pushing %i obsolescence markers in %i " + "pushkey payload (%i bytes)\n" + % (len(markers), len(remotedata), totalbytes), True) for key, data in remotedata: obsexcprg(repo.ui, sentbytes, item=key, unit="bytes", @@ -3271,10 +3356,10 @@ if l.strip(): self.ui.status(_('remote: '), l) return vals[0] - except socket.error, err: + except socket.error as err: if err.args[0] in (errno.ECONNRESET, errno.EPIPE): - raise util.Abort(_('push failed: %s') % err.args[1]) - raise util.Abort(err.args[1]) + raise error.Abort(_('push failed: %s') % err.args[1]) + raise error.Abort(err.args[1]) @eh.wrapfunction(localrepo.localrepository, '_restrictcapabilities') def local_pushobsmarker_capabilities(orig, repo, caps): @@ -3333,7 +3418,8 @@ @eh.wrapfunction(exchange, '_pullbundle2extraprepare') def _addobscommontob2pull(orig, pullop, kwargs): ret = orig(pullop, kwargs) - if 'obsmarkers' in kwargs and pullop.remote.capable('_evoext_getbundle_obscommon'): + if ('obsmarkers' in kwargs and + pullop.remote.capable('_evoext_getbundle_obscommon')): boundaries = _buildpullobsmarkersboundaries(pullop) common = boundaries['common'] if common != [nullid]: @@ -3446,7 +3532,7 @@ ui = self.ui obsexcprg(ui, current, unit="bytes", total=length) while current < length: - readsize = min(length-current, chunk) + readsize = min(length - current, chunk) data.write(f.read(readsize)) current += readsize obsexcprg(ui, current, unit="bytes", total=length) @@ -3481,7 +3567,7 @@ cache = [] unfi = repo.unfiltered() markercache = {} - repo.ui.progress("preparing locally", 0, total=len(unfi)) + repo.ui.progress(_("preparing locally"), 0, total=len(unfi)) for i in unfi: ctx = unfi[i] entry = 0 @@ -3511,14 +3597,13 @@ cache.append((ctx.node(), sha.digest())) else: cache.append((ctx.node(), nullid)) - repo.ui.progress("preparing locally", i, total=len(unfi)) - repo.ui.progress("preparing locally", None) + repo.ui.progress(_("preparing locally"), i, total=len(unfi)) + repo.ui.progress(_("preparing locally"), None) return cache @command('debugobsrelsethashtree', [('', 'v0', None, 'hash on marker format "0"'), - ('', 'v1', None, 'hash on marker format "1" (default)') - ,] , _('')) + ('', 'v1', None, 'hash on marker format "1" (default)')] , _('')) def debugobsrelsethashtree(ui, repo, v0=False, v1=False): """display Obsolete markers, Relevant Set, Hash Tree changeset-node obsrelsethashtree-node @@ -3526,7 +3611,7 @@ It computed form the "orsht" of its parent and markers relevant to the changeset itself.""" if v0 and v1: - raise util.Abort('cannot only specify one format') + raise error.Abort('cannot only specify one format') elif v0: treefunc = _obsrelsethashtreefm0 else: @@ -3549,7 +3634,7 @@ return for mark in markers: if node.nullid in mark[1]: - raise util.Abort(_('bad obsolescence marker detected: ' + raise error.Abort(_('bad obsolescence marker detected: ' 'invalid successors nullid'), hint=_('You should run `hg debugobsconvert`')) @@ -3561,7 +3646,7 @@ origmarkers = repo.obsstore._all # settle version if new_format == repo.obsstore._version: msg = _('New format is the same as the old format, not upgrading!') - raise util.Abort(msg) + raise error.Abort(msg) f = repo.svfs('obsstore', 'wb', atomictemp=True) known = set() markers = [] @@ -3621,3 +3706,144 @@ help.helptable.append((["evolution"], _("Safely Rewriting History"), _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, 'rebase') + # 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) + if nodenew is not None: + phases.retractboundary(repo, tr, destphase, [nodenew]) + obsolete.createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))]) + for book in oldbookmarks: + repo._bookmarks[book] = nodenew + else: + obsolete.createmarkers(repo, [(repo[nodesrc], ())]) + # Behave like rebase, move bookmarks to dest + for book in oldbookmarks: + repo._bookmarks[book] = dest.node() + for book in destbookmarks: # restore bookmark that rebase move + repo._bookmarks[book] = dest.node() + if oldbookmarks or destbookmarks: + repo._bookmarks.recordchange(tr) + +evolvestateversion = 0 + +@eh.uisetup +def setupevolveunfinished(ui): + data = ('evolvestate', True, False, _('evolve in progress'), + _("use 'hg evolve --continue' or 'hg update' to abort")) + cmdutil.unfinishedstates.append(data) + +@eh.wrapfunction(hg, 'clean') +def clean(orig, repo, *args, **kwargs): + ret = orig(repo, *args, **kwargs) + util.unlinkpath(repo.join('evolvestate'), ignoremissing=True) + return ret + +def _evolvestatewrite(repo, state): + # [version] + # [type][length][content] + # + # `version` is a 4 bytes integer (handled at higher level) + # `type` is a single character, `length` is a 4 byte integer, and + # `content` is an arbitrary byte sequence of length `length`. + f = repo.vfs('evolvestate', 'w') + try: + f.write(_pack('>I', evolvestateversion)) + current = state['current'] + key = 'C' # as in 'current' + format = '>sI%is' % len(current) + f.write(_pack(format, key, len(current), current)) + finally: + f.close() + +def _evolvestateread(repo): + try: + f = repo.vfs('evolvestate') + except IOError, err: + if err.errno != errno.ENOENT: + raise + return None + try: + versionblob = f.read(4) + if len(versionblob) < 4: + repo.ui.debug('ignoring corrupted evolvestte (file contains %i bits)' + % len(versionblob)) + return None + version = _unpack('>I', versionblob)[0] + if version != evolvestateversion: + raise error.Abort(_('unknown evolvestate version %i') + % version, hint=_('upgrade your evolve')) + records = [] + data = f.read() + off = 0 + end = len(data) + while off < end: + rtype = data[off] + off += 1 + length = _unpack('>I', data[off:(off + 4)])[0] + off += 4 + record = data[off:(off + length)] + off += length + if rtype == 't': + rtype, record = record[0], record[1:] + records.append((rtype, record)) + state = {} + for rtype, rdata in records: + if rtype == 'C': + state['current'] = rdata + elif rtype.lower(): + repo.ui.debug('ignore evolve state record type %s' % rtype) + else: + raise error.Abort(_('unknown evolvestate field type %r') + % rtype, hint=_('upgrade your evolve')) + return state + finally: + f.close() + +def _evolvestatedelete(repo): + util.unlinkpath(repo.join('evolvestate'), ignoremissing=True) + +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, False, True, False) + if bmactive(repo): + repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo)) + bmdeactivate(repo) + if keepbranch: + repo.dirstate.setbranch(orig.branch()) + + try: + r = merge.graft(repo, orig, pctx, ['local', 'graft'], True) + except TypeError: + # not using recent enough mercurial + if len(orig.parents()) == 2: + raise error.Abort( + _("no support for evolving merge changesets yet"), + hint=_("Redo the merge and use `hg prune --succ " + "` to obsolete the old one")) + + r = merge.graft(repo, orig, pctx, ['local', 'graft']) + return r diff -r 3c7f98753e37 -r e359d33856c3 hgext/obsolete.py --- a/hgext/obsolete.py Thu Feb 04 11:01:35 2016 +0000 +++ b/hgext/obsolete.py Thu Feb 11 00:32:40 2016 +0000 @@ -5,9 +5,12 @@ # GNU General Public License version 2 or any later version. """Deprecated extension that formely introduces "Changeset Obsolescence". -This concept is now partially in Mercurial core (starting with mercurial 2.3). The remaining logic have been grouped with the evolve extension. +This concept is now partially in Mercurial core (starting with mercurial 2.3). +The remaining logic have been grouped with the evolve extension. -Some code cemains in this extensions to detect and convert prehistoric format of obsolete marker than early user may have create. Keep it enabled if you were such user. +Some code cemains in this extensions to detect and convert prehistoric format +of obsolete marker than early user may have create. Keep it enabled if you +were such user. """ from mercurial import util @@ -15,13 +18,14 @@ try: from mercurial import obsolete except ImportError: - raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)') + raise error.Abort('Obsolete extension requires Mercurial 2.3 (or later)') import sys import json from mercurial import cmdutil from mercurial import error +from mercurial.i18n import _ from mercurial.node import bin, nullid @@ -50,8 +54,8 @@ if not data: data = repo.svfs.tryread('obsoletemarkers') if data: - raise util.Abort('old format of obsolete marker detected!\n' - 'run `hg debugconvertobsolete` once.') + raise error.Abort('old format of obsolete marker detected!\n' + 'run `hg debugconvertobsolete` once.') def _obsdeserialise(flike): """read a file like object serialised with _obsserialise @@ -65,7 +69,7 @@ subnode = bin(subhex) if subnode == nullid: subnode = None - rels.setdefault( subnode, set()).add(bin(objhex)) + rels.setdefault(subnode, set()).add(bin(objhex)) return rels cmdtable = {} @@ -153,7 +157,7 @@ del repo._importoldobsolete l.release() if not some: - ui.warn('nothing to do\n') + ui.warn(_('nothing to do\n')) ui.status('%i obsolete marker converted\n' % cnt) if err: ui.write_err('%i conversion failed. check you graph!\n' % err) diff -r 3c7f98753e37 -r e359d33856c3 hgext/pushexperiment.py --- a/hgext/pushexperiment.py Thu Feb 04 11:01:35 2016 +0000 +++ b/hgext/pushexperiment.py Thu Feb 11 00:32:40 2016 +0000 @@ -3,7 +3,8 @@ - Add a new wire protocol command to exchange obsolescence markers. Sending the raw file as a binary instead of using pushkey hack. - Add a "push done" notification -- Push obsolescence marker before anything else (This works around the lack of global transaction) +- Push obsolescence marker before anything else (This works around the lack +of global transaction) """ @@ -61,7 +62,8 @@ def client_notifypushend(self): """wire peer command to notify a push is done""" - self.requirecap('_push_experiment_notifypushend_0', _('hook once push is all done')) + self.requirecap('_push_experiment_notifypushend_0', + _('hook once push is all done')) return self._call('push_experiment_notifypushend_0') @@ -75,7 +77,7 @@ def augmented_push(orig, repo, remote, *args, **kwargs): """push wrapped that call the wire protocol command""" if not remote.canpush(): - raise util.Abort(_("destination does not support push")) + raise error.Abort(_("destination does not support push")) if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore and remote.capable('_push_experiment_pushobsmarkers_0')): # push marker early to limit damage of pushing too early. @@ -104,8 +106,10 @@ def extsetup(ui): wireproto.wirepeer.push_experiment_pushobsmarkers_0 = client_pushobsmarkers wireproto.wirepeer.push_experiment_notifypushend_0 = client_notifypushend - wireproto.commands['push_experiment_pushobsmarkers_0'] = (srv_pushobsmarkers, '') - wireproto.commands['push_experiment_notifypushend_0'] = (srv_notifypushend, '') + wireproto.commands['push_experiment_pushobsmarkers_0'] = \ + (srv_pushobsmarkers, '') + wireproto.commands['push_experiment_notifypushend_0'] = \ + (srv_notifypushend, '') extensions.wrapfunction(wireproto, 'capabilities', capabilities) extensions.wrapfunction(obsolete, 'syncpush', syncpush) extensions.wrapfunction(localrepo.localrepository, 'push', augmented_push) diff -r 3c7f98753e37 -r e359d33856c3 hgext/simple4server.py --- a/hgext/simple4server.py Thu Feb 04 11:01:35 2016 +0000 +++ b/hgext/simple4server.py Thu Feb 11 00:32:40 2016 +0000 @@ -34,8 +34,8 @@ gboptslist = getattr(wireproto, 'gboptslist', None) gboptsmap = getattr(wireproto, 'gboptsmap', None) except (ImportError, AttributeError): - raise util.Abort('Your Mercurial is too old for this version of Evolve\n' - 'requires version 3.0.1 or above') + raise error.Abort('Your Mercurial is too old for this version of Evolve\n' + 'requires version 3.0.1 or above') # Start of simple4server specific content @@ -101,10 +101,11 @@ if not util.safehasattr(obsolete.obsstore, 'relevantmarkers'): # from evolve extension: 1a23c7c52a43 class pruneobsstore(obsolete.obsstore): - """And extended obsstore class that read parent information from v1 format + """And extended obsstore class that read parent information from v1 + format - Evolve extension adds parent information in prune marker. We use it to make - markers relevant to pushed changeset.""" + Evolve extension adds parent information in prune marker. + We use it to make markers relevant to pushed changeset.""" def __init__(self, *args, **kwargs): self.prunedchildren = {} @@ -237,7 +238,8 @@ # from evolve extension: 3249814dabd1 def srv_obshash1(repo, proto, nodes): - return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), version=1)) + return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), + version=1)) # from evolve extension: 3249814dabd1 def capabilities(orig, repo, proto): diff -r 3c7f98753e37 -r e359d33856c3 tests/run-tests.py --- a/tests/run-tests.py Thu Feb 04 11:01:35 2016 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1128 +0,0 @@ -#!/usr/bin/env python -# -# run-tests.py - Run a set of tests on Mercurial -# -# Copyright 2006 Matt Mackall -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -# Modifying this script is tricky because it has many modes: -# - serial (default) vs parallel (-jN, N > 1) -# - no coverage (default) vs coverage (-c, -C, -s) -# - temp install (default) vs specific hg script (--with-hg, --local) -# - tests are a mix of shell scripts and Python scripts -# -# If you change this script, it is recommended that you ensure you -# haven't broken it by running it in various modes with a representative -# sample of test scripts. For example: -# -# 1) serial, no coverage, temp install: -# ./run-tests.py test-s* -# 2) serial, no coverage, local hg: -# ./run-tests.py --local test-s* -# 3) serial, coverage, temp install: -# ./run-tests.py -c test-s* -# 4) serial, coverage, local hg: -# ./run-tests.py -c --local test-s* # unsupported -# 5) parallel, no coverage, temp install: -# ./run-tests.py -j2 test-s* -# 6) parallel, no coverage, local hg: -# ./run-tests.py -j2 --local test-s* -# 7) parallel, coverage, temp install: -# ./run-tests.py -j2 -c test-s* # currently broken -# 8) parallel, coverage, local install: -# ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) -# 9) parallel, custom tmp dir: -# ./run-tests.py -j2 --tmpdir /tmp/myhgtests -# -# (You could use any subset of the tests: test-s* happens to match -# enough that it's worth doing parallel runs, few enough that it -# completes fairly quickly, includes both shell and Python scripts, and -# includes some scripts that run daemon processes.) - -from distutils import version -import difflib -import errno -import optparse -import os -import shutil -import subprocess -import signal -import sys -import tempfile -import time -import re - -closefds = os.name == 'posix' -def Popen4(cmd, bufsize=-1): - p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, - close_fds=closefds, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - p.fromchild = p.stdout - p.tochild = p.stdin - p.childerr = p.stderr - return p - -# reserved exit code to skip test (used by hghave) -SKIPPED_STATUS = 80 -SKIPPED_PREFIX = 'skipped: ' -FAILED_PREFIX = 'hghave check failed: ' -PYTHON = sys.executable -IMPL_PATH = 'PYTHONPATH' -if 'java' in sys.platform: - IMPL_PATH = 'JYTHONPATH' - -requiredtools = ["python", "diff", "grep", "sed"] - -defaults = { - 'jobs': ('HGTEST_JOBS', 1), - 'timeout': ('HGTEST_TIMEOUT', 180), - 'port': ('HGTEST_PORT', 20059), -} - -def parseargs(): - parser = optparse.OptionParser("%prog [options] [tests]") - - # keep these sorted - parser.add_option("--blacklist", action="append", - help="skip tests listed in the specified blacklist file") - parser.add_option("-C", "--annotate", action="store_true", - help="output files annotated with coverage") - parser.add_option("--child", type="int", - help="run as child process, summary to given fd") - parser.add_option("-c", "--cover", action="store_true", - help="print a test coverage report") - parser.add_option("-d", "--debug", action="store_true", - help="debug mode: write output of test scripts to console" - " rather than capturing and diff'ing it (disables timeout)") - parser.add_option("-f", "--first", action="store_true", - help="exit on the first test failure") - parser.add_option("--inotify", action="store_true", - help="enable inotify extension when running tests") - parser.add_option("-i", "--interactive", action="store_true", - help="prompt to accept changed output") - parser.add_option("-j", "--jobs", type="int", - help="number of jobs to run in parallel" - " (default: $%s or %d)" % defaults['jobs']) - parser.add_option("--keep-tmpdir", action="store_true", - help="keep temporary directory after running tests") - parser.add_option("-k", "--keywords", - help="run tests matching keywords") - parser.add_option("-l", "--local", action="store_true", - help="shortcut for --with-hg=/../hg") - parser.add_option("-n", "--nodiff", action="store_true", - help="skip showing test changes") - parser.add_option("-p", "--port", type="int", - help="port on which servers should listen" - " (default: $%s or %d)" % defaults['port']) - parser.add_option("--pure", action="store_true", - help="use pure Python code instead of C extensions") - parser.add_option("-R", "--restart", action="store_true", - help="restart at last error") - parser.add_option("-r", "--retest", action="store_true", - help="retest failed tests") - parser.add_option("-S", "--noskips", action="store_true", - help="don't report skip tests verbosely") - parser.add_option("-t", "--timeout", type="int", - help="kill errant tests after TIMEOUT seconds" - " (default: $%s or %d)" % defaults['timeout']) - parser.add_option("--tmpdir", type="string", - help="run tests in the given temporary directory" - " (implies --keep-tmpdir)") - parser.add_option("-v", "--verbose", action="store_true", - help="output verbose messages") - parser.add_option("--view", type="string", - help="external diff viewer") - parser.add_option("--with-hg", type="string", - metavar="HG", - help="test using specified hg script rather than a " - "temporary installation") - parser.add_option("-3", "--py3k-warnings", action="store_true", - help="enable Py3k warnings on Python 2.6+") - - for option, default in defaults.items(): - defaults[option] = int(os.environ.get(*default)) - parser.set_defaults(**defaults) - (options, args) = parser.parse_args() - - # jython is always pure - if 'java' in sys.platform or '__pypy__' in sys.modules: - options.pure = True - - if options.with_hg: - if not (os.path.isfile(options.with_hg) and - os.access(options.with_hg, os.X_OK)): - parser.error('--with-hg must specify an executable hg script') - if not os.path.basename(options.with_hg) == 'hg': - sys.stderr.write('warning: --with-hg should specify an hg script') - if options.local: - testdir = os.path.dirname(os.path.realpath(sys.argv[0])) - hgbin = os.path.join(os.path.dirname(testdir), 'hg') - if not os.access(hgbin, os.X_OK): - parser.error('--local specified, but %r not found or not executable' - % hgbin) - options.with_hg = hgbin - - options.anycoverage = options.cover or options.annotate - if options.anycoverage: - try: - import coverage - covver = version.StrictVersion(coverage.__version__).version - if covver < (3, 3): - parser.error('coverage options require coverage 3.3 or later') - except ImportError: - parser.error('coverage options now require the coverage package') - - if options.anycoverage and options.local: - # this needs some path mangling somewhere, I guess - parser.error("sorry, coverage options do not work when --local " - "is specified") - - global vlog - if options.verbose: - if options.jobs > 1 or options.child is not None: - pid = "[%d]" % os.getpid() - else: - pid = None - def vlog(*msg): - if pid: - print pid, - for m in msg: - print m, - print - sys.stdout.flush() - else: - vlog = lambda *msg: None - - if options.tmpdir: - options.tmpdir = os.path.expanduser(options.tmpdir) - - if options.jobs < 1: - parser.error('--jobs must be positive') - if options.interactive and options.jobs > 1: - print '(--interactive overrides --jobs)' - options.jobs = 1 - if options.interactive and options.debug: - parser.error("-i/--interactive and -d/--debug are incompatible") - if options.debug: - if options.timeout != defaults['timeout']: - sys.stderr.write( - 'warning: --timeout option ignored with --debug\n') - options.timeout = 0 - if options.py3k_warnings: - if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): - parser.error('--py3k-warnings can only be used on Python 2.6+') - if options.blacklist: - blacklist = dict() - for filename in options.blacklist: - try: - path = os.path.expanduser(os.path.expandvars(filename)) - f = open(path, "r") - except IOError, err: - if err.errno != errno.ENOENT: - raise - print "warning: no such blacklist file: %s" % filename - continue - - for line in f.readlines(): - line = line.strip() - if line and not line.startswith('#'): - blacklist[line] = filename - - f.close() - - options.blacklist = blacklist - - return (options, args) - -def rename(src, dst): - """Like os.rename(), trade atomicity and opened files friendliness - for existing destination support. - """ - shutil.copy(src, dst) - os.remove(src) - -def splitnewlines(text): - '''like str.splitlines, but only split on newlines. - keep line endings.''' - i = 0 - lines = [] - while True: - n = text.find('\n', i) - if n == -1: - last = text[i:] - if last: - lines.append(last) - return lines - lines.append(text[i:n + 1]) - i = n + 1 - -def parsehghaveoutput(lines): - '''Parse hghave log lines. - Return tuple of lists (missing, failed): - * the missing/unknown features - * the features for which existence check failed''' - missing = [] - failed = [] - for line in lines: - if line.startswith(SKIPPED_PREFIX): - line = line.splitlines()[0] - missing.append(line[len(SKIPPED_PREFIX):]) - elif line.startswith(FAILED_PREFIX): - line = line.splitlines()[0] - failed.append(line[len(FAILED_PREFIX):]) - - return missing, failed - -def showdiff(expected, output, ref, err): - try: - for line in difflib.unified_diff(expected, output, ref, err): - sys.stdout.write(line) - except IOError, ex: - print >>sys.stderr, 'BORKEN PIPE', ex.errno - pass - -def findprogram(program): - """Search PATH for a executable program""" - for p in os.environ.get('PATH', os.defpath).split(os.pathsep): - name = os.path.join(p, program) - if os.access(name, os.X_OK): - return name - return None - -def checktools(): - # Before we go any further, check for pre-requisite tools - # stuff from coreutils (cat, rm, etc) are not tested - for p in requiredtools: - if os.name == 'nt': - p += '.exe' - found = findprogram(p) - if found: - vlog("# Found prerequisite", p, "at", found) - else: - print "WARNING: Did not find prerequisite tool: "+p - -def killdaemons(): - # Kill off any leftover daemon processes - try: - fp = open(DAEMON_PIDS) - for line in fp: - try: - pid = int(line) - except ValueError: - continue - try: - os.kill(pid, 0) - vlog('# Killing daemon process %d' % pid) - os.kill(pid, signal.SIGTERM) - time.sleep(0.25) - os.kill(pid, 0) - vlog('# Daemon process %d is stuck - really killing it' % pid) - os.kill(pid, signal.SIGKILL) - except OSError, err: - if err.errno != errno.ESRCH: - raise - fp.close() - os.unlink(DAEMON_PIDS) - except IOError: - pass - -def cleanup(options): - if not options.keep_tmpdir: - vlog("# Cleaning up HGTMP", HGTMP) - shutil.rmtree(HGTMP, True) - -def usecorrectpython(): - # some tests run python interpreter. they must use same - # interpreter we use or bad things will happen. - exedir, exename = os.path.split(sys.executable) - if exename == 'python': - path = findprogram('python') - if os.path.dirname(path) == exedir: - return - vlog('# Making python executable in test path use correct Python') - mypython = os.path.join(BINDIR, 'python') - try: - os.symlink(sys.executable, mypython) - except AttributeError: - # windows fallback - shutil.copyfile(sys.executable, mypython) - shutil.copymode(sys.executable, mypython) - -def installhg(options): - vlog("# Performing temporary installation of HG") - installerrs = os.path.join("tests", "install.err") - pure = options.pure and "--pure" or "" - - # Run installer in hg root - script = os.path.realpath(sys.argv[0]) - hgroot = os.path.dirname(os.path.dirname(script)) - os.chdir(hgroot) - nohome = '--home=""' - if os.name == 'nt': - # The --home="" trick works only on OS where os.sep == '/' - # because of a distutils convert_path() fast-path. Avoid it at - # least on Windows for now, deal with .pydistutils.cfg bugs - # when they happen. - nohome = '' - cmd = ('%s setup.py %s clean --all' - ' build --build-base="%s"' - ' install --force --prefix="%s" --install-lib="%s"' - ' --install-scripts="%s" %s >%s 2>&1' - % (sys.executable, pure, os.path.join(HGTMP, "build"), - INST, PYTHONDIR, BINDIR, nohome, installerrs)) - vlog("# Running", cmd) - if os.system(cmd) == 0: - if not options.verbose: - os.remove(installerrs) - else: - f = open(installerrs) - for line in f: - print line, - f.close() - sys.exit(1) - os.chdir(TESTDIR) - - usecorrectpython() - - vlog("# Installing dummy diffstat") - f = open(os.path.join(BINDIR, 'diffstat'), 'w') - f.write('#!' + sys.executable + '\n' - 'import sys\n' - 'files = 0\n' - 'for line in sys.stdin:\n' - ' if line.startswith("diff "):\n' - ' files += 1\n' - 'sys.stdout.write("files patched: %d\\n" % files)\n') - f.close() - os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) - - if options.py3k_warnings and not options.anycoverage: - vlog("# Updating hg command to enable Py3k Warnings switch") - f = open(os.path.join(BINDIR, 'hg'), 'r') - lines = [line.rstrip() for line in f] - lines[0] += ' -3' - f.close() - f = open(os.path.join(BINDIR, 'hg'), 'w') - for line in lines: - f.write(line + '\n') - f.close() - - if options.anycoverage: - custom = os.path.join(TESTDIR, 'sitecustomize.py') - target = os.path.join(PYTHONDIR, 'sitecustomize.py') - vlog('# Installing coverage trigger to %s' % target) - shutil.copyfile(custom, target) - rc = os.path.join(TESTDIR, '.coveragerc') - vlog('# Installing coverage rc to %s' % rc) - os.environ['COVERAGE_PROCESS_START'] = rc - fn = os.path.join(INST, '..', '.coverage') - os.environ['COVERAGE_FILE'] = fn - -def outputcoverage(options): - - vlog('# Producing coverage report') - os.chdir(PYTHONDIR) - - def covrun(*args): - cmd = 'coverage %s' % ' '.join(args) - vlog('# Running: %s' % cmd) - os.system(cmd) - - if options.child: - return - - covrun('-c') - omit = ','.join([BINDIR, TESTDIR]) - covrun('-i', '-r', '"--omit=%s"' % omit) # report - if options.annotate: - adir = os.path.join(TESTDIR, 'annotated') - if not os.path.isdir(adir): - os.mkdir(adir) - covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit) - -class Timeout(Exception): - pass - -def alarmed(signum, frame): - raise Timeout - -def pytest(test, options, replacements): - py3kswitch = options.py3k_warnings and ' -3' or '' - cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test) - vlog("# Running", cmd) - return run(cmd, options, replacements) - -def shtest(test, options, replacements): - cmd = '"%s"' % test - vlog("# Running", cmd) - return run(cmd, options, replacements) - -needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search -escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub -escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256)) -escapemap.update({'\\': '\\\\', '\r': r'\r'}) -def escapef(m): - return escapemap[m.group(0)] -def stringescape(s): - return escapesub(escapef, s) - -def tsttest(test, options, replacements): - t = open(test) - out = [] - script = [] - salt = "SALT" + str(time.time()) - - pos = prepos = -1 - after = {} - expected = {} - for n, l in enumerate(t): - if not l.endswith('\n'): - l += '\n' - if l.startswith(' $ '): # commands - after.setdefault(pos, []).append(l) - prepos = pos - pos = n - script.append('echo %s %s $?\n' % (salt, n)) - script.append(l[4:]) - elif l.startswith(' > '): # continuations - after.setdefault(prepos, []).append(l) - script.append(l[4:]) - elif l.startswith(' '): # results - # queue up a list of expected results - expected.setdefault(pos, []).append(l[2:]) - else: - # non-command/result - queue up for merged output - after.setdefault(pos, []).append(l) - - t.close() - - script.append('echo %s %s $?\n' % (salt, n + 1)) - - fd, name = tempfile.mkstemp(suffix='hg-tst') - - try: - for l in script: - os.write(fd, l) - os.close(fd) - - cmd = '/bin/sh "%s"' % name - vlog("# Running", cmd) - exitcode, output = run(cmd, options, replacements) - # do not merge output if skipped, return hghave message instead - # similarly, with --debug, output is None - if exitcode == SKIPPED_STATUS or output is None: - return exitcode, output - finally: - os.remove(name) - - def rematch(el, l): - try: - # ensure that the regex matches to the end of the string - return re.match(el + r'\Z', l) - except re.error: - # el is an invalid regex - return False - - def globmatch(el, l): - # The only supported special characters are * and ?. Escaping is - # supported. - i, n = 0, len(el) - res = '' - while i < n: - c = el[i] - i += 1 - if c == '\\' and el[i] in '*?\\': - res += el[i - 1:i + 1] - i += 1 - elif c == '*': - res += '.*' - elif c == '?': - res += '.' - else: - res += re.escape(c) - return rematch(res, l) - - pos = -1 - postout = [] - ret = 0 - for n, l in enumerate(output): - lout, lcmd = l, None - if salt in l: - lout, lcmd = l.split(salt, 1) - - if lout: - if lcmd: - lout += ' (no-eol)\n' - - el = None - if pos in expected and expected[pos]: - el = expected[pos].pop(0) - - if el == lout: # perfect match (fast) - postout.append(" " + lout) - elif (el and - (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or - el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout) - or el.endswith(" (esc)\n") and - el.decode('string-escape') == l)): - postout.append(" " + el) # fallback regex/glob/esc match - else: - if needescape(lout): - lout = stringescape(lout.rstrip('\n')) + " (esc)\n" - postout.append(" " + lout) # let diff deal with it - - if lcmd: - # add on last return code - ret = int(lcmd.split()[1]) - if ret != 0: - postout.append(" [%s]\n" % ret) - if pos in after: - postout += after.pop(pos) - pos = int(lcmd.split()[0]) - - if pos in after: - postout += after.pop(pos) - - return exitcode, postout - -wifexited = getattr(os, "WIFEXITED", lambda x: False) -def run(cmd, options, replacements): - """Run command in a sub-process, capturing the output (stdout and stderr). - Return a tuple (exitcode, output). output is None in debug mode.""" - # TODO: Use subprocess.Popen if we're running on Python 2.4 - if options.debug: - proc = subprocess.Popen(cmd, shell=True) - ret = proc.wait() - return (ret, None) - - if os.name == 'nt' or sys.platform.startswith('java'): - tochild, fromchild = os.popen4(cmd) - tochild.close() - output = fromchild.read() - ret = fromchild.close() - if ret is None: - ret = 0 - else: - proc = Popen4(cmd) - def cleanup(): - os.kill(proc.pid, signal.SIGTERM) - ret = proc.wait() - if ret == 0: - ret = signal.SIGTERM << 8 - killdaemons() - return ret - - try: - output = '' - proc.tochild.close() - output = proc.fromchild.read() - ret = proc.wait() - if wifexited(ret): - ret = os.WEXITSTATUS(ret) - except Timeout: - vlog('# Process %d timed out - killing it' % proc.pid) - ret = cleanup() - output += ("\n### Abort: timeout after %d seconds.\n" - % options.timeout) - except KeyboardInterrupt: - vlog('# Handling keyboard interrupt') - cleanup() - raise - - for s, r in replacements: - output = re.sub(s, r, output) - return ret, splitnewlines(output) - -def runone(options, test, skips, fails): - '''tristate output: - None -> skipped - True -> passed - False -> failed''' - - def skip(msg): - if not options.verbose: - skips.append((test, msg)) - else: - print "\nSkipping %s: %s" % (testpath, msg) - return None - - def fail(msg): - fails.append((test, msg)) - if not options.nodiff: - print "\nERROR: %s %s" % (testpath, msg) - return None - - vlog("# Test", test) - - # create a fresh hgrc - hgrc = open(HGRCPATH, 'w+') - hgrc.write('[ui]\n') - hgrc.write('slash = True\n') - hgrc.write('[defaults]\n') - hgrc.write('backout = -d "0 0"\n') - hgrc.write('commit = -d "0 0"\n') - hgrc.write('tag = -d "0 0"\n') - if options.inotify: - hgrc.write('[extensions]\n') - hgrc.write('inotify=\n') - hgrc.write('[inotify]\n') - hgrc.write('pidfile=%s\n' % DAEMON_PIDS) - hgrc.write('appendpid=True\n') - hgrc.close() - - testpath = os.path.join(TESTDIR, test) - ref = os.path.join(TESTDIR, test+".out") - err = os.path.join(TESTDIR, test+".err") - if os.path.exists(err): - os.remove(err) # Remove any previous output files - try: - tf = open(testpath) - firstline = tf.readline().rstrip() - tf.close() - except: - firstline = '' - lctest = test.lower() - - if lctest.endswith('.py') or firstline == '#!/usr/bin/env python': - runner = pytest - elif lctest.endswith('.t'): - runner = tsttest - ref = testpath - else: - # do not try to run non-executable programs - if not os.access(testpath, os.X_OK): - return skip("not executable") - runner = shtest - - # Make a tmp subdirectory to work in - testtmp = os.environ["TESTTMP"] = os.path.join(HGTMP, test) - os.mkdir(testtmp) - os.chdir(testtmp) - - if options.timeout > 0: - signal.alarm(options.timeout) - - ret, out = runner(testpath, options, [ - (re.escape(testtmp), '$TESTTMP'), - (r':%s\b' % options.port, ':$HGPORT'), - (r':%s\b' % (options.port + 1), ':$HGPORT1'), - (r':%s\b' % (options.port + 2), ':$HGPORT2'), - ]) - vlog("# Ret was:", ret) - - if options.timeout > 0: - signal.alarm(0) - - mark = '.' - - skipped = (ret == SKIPPED_STATUS) - - # If we're not in --debug mode and reference output file exists, - # check test output against it. - if options.debug: - refout = None # to match "out is None" - elif os.path.exists(ref): - f = open(ref, "r") - refout = splitnewlines(f.read()) - f.close() - else: - refout = [] - - if (ret != 0 or out != refout) and not skipped and not options.debug: - # Save errors to a file for diagnosis - f = open(err, "wb") - for line in out: - f.write(line) - f.close() - - if skipped: - mark = 's' - if out is None: # debug mode: nothing to parse - missing = ['unknown'] - failed = None - else: - missing, failed = parsehghaveoutput(out) - if not missing: - missing = ['irrelevant'] - if failed: - fail("hghave failed checking for %s" % failed[-1]) - skipped = False - else: - skip(missing[-1]) - elif out != refout: - mark = '!' - if ret: - fail("output changed and returned error code %d" % ret) - else: - fail("output changed") - if not options.nodiff: - if options.view: - os.system("%s %s %s" % (options.view, ref, err)) - else: - showdiff(refout, out, ref, err) - ret = 1 - elif ret: - mark = '!' - fail("returned error code %d" % ret) - - if not options.verbose: - try: - sys.stdout.write(mark) - sys.stdout.flush() - except IOError, ex: - print >>sys.stderr, 'BORKEN PIPE', ex.errno - pass - - killdaemons() - - os.chdir(TESTDIR) - if not options.keep_tmpdir: - shutil.rmtree(testtmp, True) - if skipped: - return None - return ret == 0 - -_hgpath = None - -def _gethgpath(): - """Return the path to the mercurial package that is actually found by - the current Python interpreter.""" - global _hgpath - if _hgpath is not None: - return _hgpath - - cmd = '%s -c "import mercurial; print mercurial.__path__[0]"' - pipe = os.popen(cmd % PYTHON) - try: - _hgpath = pipe.read().strip() - finally: - pipe.close() - return _hgpath - -def _checkhglib(verb): - """Ensure that the 'mercurial' package imported by python is - the one we expect it to be. If not, print a warning to stderr.""" - expecthg = os.path.join(PYTHONDIR, 'mercurial') - actualhg = _gethgpath() - if actualhg != expecthg: - sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n' - ' (expected %s)\n' - % (verb, actualhg, expecthg)) - -def runchildren(options, tests): - if INST: - installhg(options) - _checkhglib("Testing") - - optcopy = dict(options.__dict__) - optcopy['jobs'] = 1 - del optcopy['blacklist'] - if optcopy['with_hg'] is None: - optcopy['with_hg'] = os.path.join(BINDIR, "hg") - optcopy.pop('anycoverage', None) - - opts = [] - for opt, value in optcopy.iteritems(): - name = '--' + opt.replace('_', '-') - if value is True: - opts.append(name) - elif value is not None: - opts.append(name + '=' + str(value)) - - tests.reverse() - jobs = [[] for j in xrange(options.jobs)] - while tests: - for job in jobs: - if not tests: - break - job.append(tests.pop()) - fps = {} - - for j, job in enumerate(jobs): - if not job: - continue - rfd, wfd = os.pipe() - childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] - childtmp = os.path.join(HGTMP, 'child%d' % j) - childopts += ['--tmpdir', childtmp] - cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job - vlog(' '.join(cmdline)) - fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') - os.close(wfd) - signal.signal(signal.SIGINT, signal.SIG_IGN) - failures = 0 - tested, skipped, failed = 0, 0, 0 - skips = [] - fails = [] - while fps: - pid, status = os.wait() - fp = fps.pop(pid) - l = fp.read().splitlines() - try: - test, skip, fail = map(int, l[:3]) - except ValueError: - test, skip, fail = 0, 0, 0 - split = -fail or len(l) - for s in l[3:split]: - skips.append(s.split(" ", 1)) - for s in l[split:]: - fails.append(s.split(" ", 1)) - tested += test - skipped += skip - failed += fail - vlog('pid %d exited, status %d' % (pid, status)) - failures |= status - print - if not options.noskips: - for s in skips: - print "Skipped %s: %s" % (s[0], s[1]) - for s in fails: - print "Failed %s: %s" % (s[0], s[1]) - - _checkhglib("Tested") - print "# Ran %d tests, %d skipped, %d failed." % ( - tested, skipped, failed) - - if options.anycoverage: - outputcoverage(options) - sys.exit(failures != 0) - -def runtests(options, tests): - global DAEMON_PIDS, HGRCPATH - DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') - HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') - - try: - if INST: - installhg(options) - _checkhglib("Testing") - - if options.timeout > 0: - try: - signal.signal(signal.SIGALRM, alarmed) - vlog('# Running each test with %d second timeout' % - options.timeout) - except AttributeError: - print 'WARNING: cannot run tests with timeouts' - options.timeout = 0 - - tested = 0 - failed = 0 - skipped = 0 - - if options.restart: - orig = list(tests) - while tests: - if os.path.exists(tests[0] + ".err"): - break - tests.pop(0) - if not tests: - print "running all tests" - tests = orig - - skips = [] - fails = [] - - for test in tests: - if options.blacklist: - filename = options.blacklist.get(test) - if filename is not None: - skips.append((test, "blacklisted (%s)" % filename)) - skipped += 1 - continue - - if options.retest and not os.path.exists(test + ".err"): - skipped += 1 - continue - - if options.keywords: - fp = open(test) - t = fp.read().lower() + test.lower() - fp.close() - for k in options.keywords.lower().split(): - if k in t: - break - else: - skipped += 1 - continue - - ret = runone(options, test, skips, fails) - if ret is None: - skipped += 1 - elif not ret: - if options.interactive: - print "Accept this change? [n] ", - answer = sys.stdin.readline().strip() - if answer.lower() in "y yes".split(): - if test.endswith(".t"): - rename(test + ".err", test) - else: - rename(test + ".err", test + ".out") - tested += 1 - fails.pop() - continue - failed += 1 - if options.first: - break - tested += 1 - - if options.child: - fp = os.fdopen(options.child, 'w') - fp.write('%d\n%d\n%d\n' % (tested, skipped, failed)) - for s in skips: - fp.write("%s %s\n" % s) - for s in fails: - fp.write("%s %s\n" % s) - fp.close() - else: - print - for s in skips: - print "Skipped %s: %s" % s - for s in fails: - print "Failed %s: %s" % s - _checkhglib("Tested") - print "# Ran %d tests, %d skipped, %d failed." % ( - tested, skipped, failed) - - if options.anycoverage: - outputcoverage(options) - except KeyboardInterrupt: - failed = True - print "\ninterrupted!" - - if failed: - sys.exit(1) - -def main(): - (options, args) = parseargs() - if not options.child: - os.umask(022) - - checktools() - - if len(args) == 0: - args = os.listdir(".") - args.sort() - - tests = [] - skipped = [] - for test in args: - if (test.startswith("test-") and '~' not in test and - ('.' not in test or test.endswith('.py') or - test.endswith('.bat') or test.endswith('.t'))): - if not os.path.exists(test): - skipped.append(test) - else: - tests.append(test) - if not tests: - for test in skipped: - print 'Skipped %s: does not exist' % test - print "# Ran 0 tests, %d skipped, 0 failed." % len(skipped) - return - tests = tests + skipped - - # Reset some environment variables to well-known values so that - # the tests produce repeatable output. - os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C' - os.environ['TZ'] = 'GMT' - os.environ["EMAIL"] = "Foo Bar " - os.environ['CDPATH'] = '' - os.environ['COLUMNS'] = '80' - os.environ['GREP_OPTIONS'] = '' - os.environ['http_proxy'] = '' - - # unset env related to hooks - for k in os.environ.keys(): - if k.startswith('HG_'): - # can't remove on solaris - os.environ[k] = '' - del os.environ[k] - - global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE - TESTDIR = os.environ["TESTDIR"] = os.getcwd() - if options.tmpdir: - options.keep_tmpdir = True - tmpdir = options.tmpdir - if os.path.exists(tmpdir): - # Meaning of tmpdir has changed since 1.3: we used to create - # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if - # tmpdir already exists. - sys.exit("error: temp dir %r already exists" % tmpdir) - - # Automatically removing tmpdir sounds convenient, but could - # really annoy anyone in the habit of using "--tmpdir=/tmp" - # or "--tmpdir=$HOME". - #vlog("# Removing temp dir", tmpdir) - #shutil.rmtree(tmpdir) - os.makedirs(tmpdir) - else: - tmpdir = tempfile.mkdtemp('', 'hgtests.') - HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir) - DAEMON_PIDS = None - HGRCPATH = None - - os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' - os.environ["HGMERGE"] = "internal:merge" - os.environ["HGUSER"] = "test" - os.environ["HGENCODING"] = "ascii" - os.environ["HGENCODINGMODE"] = "strict" - os.environ["HGPORT"] = str(options.port) - os.environ["HGPORT1"] = str(options.port + 1) - os.environ["HGPORT2"] = str(options.port + 2) - - if options.with_hg: - INST = None - BINDIR = os.path.dirname(os.path.realpath(options.with_hg)) - - # This looks redundant with how Python initializes sys.path from - # the location of the script being executed. Needed because the - # "hg" specified by --with-hg is not the only Python script - # executed in the test suite that needs to import 'mercurial' - # ... which means it's not really redundant at all. - PYTHONDIR = BINDIR - else: - INST = os.path.join(HGTMP, "install") - BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin") - PYTHONDIR = os.path.join(INST, "lib", "python") - - os.environ["BINDIR"] = BINDIR - os.environ["PYTHON"] = PYTHON - - if not options.child: - path = [BINDIR] + os.environ["PATH"].split(os.pathsep) - os.environ["PATH"] = os.pathsep.join(path) - - # Include TESTDIR in PYTHONPATH so that out-of-tree extensions - # can run .../tests/run-tests.py test-foo where test-foo - # adds an extension to HGRC - pypath = [PYTHONDIR, TESTDIR] - # We have to augment PYTHONPATH, rather than simply replacing - # it, in case external libraries are only available via current - # PYTHONPATH. (In particular, the Subversion bindings on OS X - # are in /opt/subversion.) - oldpypath = os.environ.get(IMPL_PATH) - if oldpypath: - pypath.append(oldpypath) - os.environ[IMPL_PATH] = os.pathsep.join(pypath) - - COVERAGE_FILE = os.path.join(TESTDIR, ".coverage") - - vlog("# Using TESTDIR", TESTDIR) - vlog("# Using HGTMP", HGTMP) - vlog("# Using PATH", os.environ["PATH"]) - vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH]) - - try: - if len(tests) > 1 and options.jobs > 1: - runchildren(options, tests) - else: - runtests(options, tests) - finally: - time.sleep(1) - cleanup(options) - -if __name__ == '__main__': - main() diff -r 3c7f98753e37 -r e359d33856c3 tests/test-corrupt.t --- a/tests/test-corrupt.t Thu Feb 04 11:01:35 2016 +0000 +++ b/tests/test-corrupt.t Thu Feb 11 00:32:40 2016 +0000 @@ -110,7 +110,7 @@ adding manifests adding file changes added 1 changesets with 2 changes to 2 files - pushing 2 obsolescence markers (161 bytes) + pushing 2 obsolescence markers (16? bytes) (glob) 2 obsolescence markers added $ hg -R ../other verify checking changesets diff -r 3c7f98753e37 -r e359d33856c3 tests/test-divergent.t --- a/tests/test-divergent.t Thu Feb 04 11:01:35 2016 +0000 +++ b/tests/test-divergent.t Thu Feb 11 00:32:40 2016 +0000 @@ -67,4 +67,47 @@ | o 0:135f39f4bd78@default(draft) add _a [] +Test divergence resolution when it yields to an empty commit (issue4950) +cdivergent2 contains the same content than cdivergent1 and they are divergent +versions of the revision _c + + $ hg up "desc(_a)" + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit _c + created new head + $ hg up "desc(_a)" + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit cdivergent1 + created new head + $ hg up "desc(_a)" + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo "cdivergent1" > cdivergent1 + $ hg add cdivergent1 + $ hg ci -m "cdivergent2" + created new head + $ hg prune -s "desc(cdivergent1)" "desc(_c)" + 1 changesets pruned + $ hg prune -s "desc(cdivergent2)" "desc(_c)" --hidden + 1 changesets pruned + 2 new divergent changesets + $ hg log -G + @ 8:0a768ef678d9@default(draft) cdivergent2 [divergent] + | + | o 7:26c7705fee96@default(draft) add cdivergent1 [divergent] + |/ + | o 5:c26f1d3baed2@default(draft) add bdivergent1 [] + |/ + o 0:135f39f4bd78@default(draft) add _a [] + +## Fixed only for 3.5+ +# +# $ hg evolve --all --any --divergent +# merge:[7] add cdivergent1 +# with: [8] cdivergent2 +# base: [6] add _c +# updating to "local" conflict +# 0 files updated, 0 files merged, 0 files removed, 0 files unresolved +# 0 files updated, 0 files merged, 0 files removed, 0 files unresolved +# working directory is now at 6602ff5a79dc + $ cd .. diff -r 3c7f98753e37 -r e359d33856c3 tests/test-evolve.t --- a/tests/test-evolve.t Thu Feb 04 11:01:35 2016 +0000 +++ b/tests/test-evolve.t Thu Feb 11 00:32:40 2016 +0000 @@ -1400,3 +1400,55 @@ working directory is now at 43c3f5ef149f +Check that dirstate changes are kept at failure for conflicts (issue4966) +---------------------------------------- + + $ echo "will be amended" > newfile + $ hg commit -m "will be amended" + $ hg parents + 37 : will be amended - test + + $ echo "will be evolved safely" >> a + $ hg commit -m "will be evolved safely" + + $ echo "will cause conflict at evolve" > newfile + $ echo "newly added" > newlyadded + $ hg add newlyadded + $ hg commit -m "will cause conflict at evolve" + + $ hg update -q 37 + $ echo "amended" > newfile + $ hg amend -m "amended" + 2 new unstable changesets + + $ hg evolve --rev "37::" + move:[38] will be evolved safely + atop:[41] amended + move:[39] will cause conflict at evolve + atop:[42] will be evolved safely + merging newfile + warning: conflicts during merge. + merging newfile incomplete! (edit conflicts, then use 'hg resolve --mark') + evolve failed! + fix conflict and run "hg evolve --continue" or use "hg update -C" to abort + abort: unresolved merge conflicts (see hg help resolve) + [255] + + $ glog -r "36::" --hidden + @ 42:c904da5245b0@default(draft) will be evolved safely + | + o 41:34ae045ec400@default(draft) amended + | + | x 40:e88bee38ffc2@default(draft) temporary amend commit for 36030b147271 + | | + | | o 39:02e943732647@default(draft) will cause conflict at evolve + | | | + | | x 38:f8e30e9317aa@default(draft) will be evolved safely + | |/ + | x 37:36030b147271@default(draft) will be amended + |/ + o 36:43c3f5ef149f@default(draft) add uu + | + + $ hg status newlyadded + A newlyadded diff -r 3c7f98753e37 -r e359d33856c3 tests/test-prune.t --- a/tests/test-prune.t Thu Feb 04 11:01:35 2016 +0000 +++ b/tests/test-prune.t Thu Feb 11 00:32:40 2016 +0000 @@ -279,11 +279,15 @@ $ cd .. $ hg init bookmarks $ cd bookmarks - $ hg debugbuilddag '..<2.*1/2:m<2+3:c evolution=createmarkers > evolutioncommands=split > EOF + $ hg up -qC tip $ hg split -r "desc(split3)" abort: cannot split commit: ced8fbcce3a7 not a head [255] +Changing evolution level to createmarkers + $ echo "[experimental]" >> $HGRCPATH + $ echo "evolution=createmarkers" >> $HGRCPATH +Running split without any revision operates on the parent of the working copy + $ hg up -qC tip + $ hg split << EOF + > q + > EOF + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + adding _d + diff --git a/_d b/_d + new file mode 100644 + examine changes to '_d'? [Ynesfdaq?] q + + abort: user quit + [255] + +Running split with tip revision, specified as unnamed argument + $ hg up -qC tip + $ hg split . << EOF + > q + > EOF + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + adding _d + diff --git a/_d b/_d + new file mode 100644 + examine changes to '_d'? [Ynesfdaq?] q + + abort: user quit + [255] + +Running split with both unnamed and named revision arguments shows an error msg + $ hg split . --rev .^ << EOF + > q + > EOF + abort: more than one revset is given + (use either `hg split ` or `hg split --rev `, not both) + [255] + diff -r 3c7f98753e37 -r e359d33856c3 tests/test-stabilize-result.t diff -r 3c7f98753e37 -r e359d33856c3 tests/test-touch.t --- a/tests/test-touch.t Thu Feb 04 11:01:35 2016 +0000 +++ b/tests/test-touch.t Thu Feb 11 00:32:40 2016 +0000 @@ -41,6 +41,9 @@ @ 1:[0-9a-f]{12} a (re) $ hg touch . + [1] a + reviving this changeset will create divergence unless you make a duplicate. + (a)llow divergence or (d)uplicate the changeset? a 2 new divergent changesets $ hg log -G @ 4:[0-9a-f]{12} a (re) @@ -110,3 +113,15 @@ A gna2 gna1 R gna1 + +check that the --duplicate option does not create divergence + + $ hg touch --duplicate 11 --hidden + 1 new unstable changesets + +check that reviving a changeset with no successor does not show the prompt + + $ hg prune 14 + 1 changesets pruned + $ hg touch 14 --hidden + 1 new unstable changesets diff -r 3c7f98753e37 -r e359d33856c3 tests/test-unstable.t --- a/tests/test-unstable.t Thu Feb 04 11:01:35 2016 +0000 +++ b/tests/test-unstable.t Thu Feb 11 00:32:40 2016 +0000 @@ -100,23 +100,21 @@ o 0:b4952fcf48cf@default(draft) add base - $ hg evo --all --any --unstable - move:[3] merge - atop:[4] aprime - abort: no support for evolving merge changesets yet - (Redo the merge and use `hg prune --succ ` to obsolete the old one) - [255] - $ hg log -G - @ 4:47127ea62e5f@default(draft) aprime - | - | o 3:6b4280e33286@default(draft) merge - | |\ - +---o 2:474da87dd33b@default(draft) add _c - | | - | x 1:b3264cec9506@default(draft) add _a - |/ - o 0:b4952fcf48cf@default(draft) add base - +# Unsupported before 3.7 +# +# $ hg evo --all --any --unstable +# move:[3] merge +# atop:[4] aprime +# working directory is now at 0bf3f3a59c8c +# $ hg log -G +# @ 5:0bf3f3a59c8c@default(draft) merge +# |\ +# | o 4:47127ea62e5f@default(draft) aprime +# | | +# o | 2:474da87dd33b@default(draft) add _c +# |/ +# o 0:b4952fcf48cf@default(draft) add base +# $ cd .. @@ -158,9 +156,7 @@ $ hg evo --all --any --unstable - move:[3] merge - atop:[5] cprime - abort: no support for evolving merge changesets yet + abort: no support for evolving merge changesets with two obsolete parents yet (Redo the merge and use `hg prune --succ ` to obsolete the old one) [255] $ hg log -G diff -r 3c7f98753e37 -r e359d33856c3 tests/test-wireproto-bundle1.t diff -r 3c7f98753e37 -r e359d33856c3 tests/test-wireproto.t