# HG changeset patch # User Pierre-Yves David # Date 1490967239 -7200 # Node ID 94432e742a0211a4d8ffcf620ea2da17bf8e93bd # Parent efda653c96a78107a52a0a5b988cbbf405a1cd20# Parent 446824c5e0b8d6752f890f61b0fe59f6a4652a86 merge with default We are getting close to cutting a 6.0.0 diff -r efda653c96a7 -r 94432e742a02 .hgignore --- a/.hgignore Tue Feb 28 17:22:21 2017 +0100 +++ b/.hgignore Fri Mar 31 15:33:59 2017 +0200 @@ -9,6 +9,7 @@ \.err$ ^tests/easy_run.sh$ ^build/ +^dist/ ^MANIFEST$ ^docs/tutorials/.*\.rst$ \.ico$ diff -r efda653c96a7 -r 94432e742a02 COPYING --- a/COPYING Tue Feb 28 17:22:21 2017 +0100 +++ b/COPYING Fri Mar 31 15:33:59 2017 +0200 @@ -1,12 +1,12 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble + Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public @@ -55,8 +55,8 @@ The precise terms and conditions for copying, distribution and modification follow. - - GNU GENERAL PUBLIC LICENSE + + GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains @@ -110,7 +110,7 @@ License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) - + These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in @@ -168,7 +168,7 @@ access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. - + 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is @@ -225,7 +225,7 @@ This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. - + 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License @@ -255,7 +255,7 @@ of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. - NO WARRANTY + NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN @@ -277,9 +277,9 @@ PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it @@ -313,7 +313,7 @@ If the program is interactive, make it output a short notice like this when it starts in an interactive mode: - Gnomovision version 69, Copyright (C) year name of author + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff -r efda653c96a7 -r 94432e742a02 MANIFEST.in --- a/MANIFEST.in Tue Feb 28 17:22:21 2017 +0100 +++ b/MANIFEST.in Fri Mar 31 15:33:59 2017 +0200 @@ -1,8 +1,8 @@ -exclude contrib/nopushpublish.py -exclude hgext/directaccess.py -exclude hgext/drophack.py -exclude hgext/inhibit.py -exclude hgext/obsolete.py +exclude contrib +recursive-exclude contrib * +exclude hgext3rd/evolve/hack +recursive-exclude hgext3rd/evolve/hack * +exclude hgext3rd/evolve/legacy.py exclude Makefile exclude tests/test-drop.t exclude tests/test-inhibit.t @@ -15,15 +15,16 @@ include docs/*.rst include docs/static/*.svg include docs/tutorials/*.t -include hgext/evolve.py -include hgext/__init__.py -include hgext/pushexperiment.py -include hgext/simple4server.py +include hgext3rd/__init__.py +include hgext3rd/evolve/*.py +include hgext3rd/topic/*.py include MANIFEST.in include README +include README-topic include setup.py include tests/*.py include tests/*.sh +include tests/testlib/*.sh include tests/*.t prune debian recursive-include docs/figures *.svg diff -r efda653c96a7 -r 94432e742a02 Makefile --- a/Makefile Tue Feb 28 17:22:21 2017 +0100 +++ b/Makefile Fri Mar 31 15:33:59 2017 +0200 @@ -1,5 +1,6 @@ VERSION=$(shell python setup.py --version) +PYTHON=python help: @echo 'Commonly used make targets:' @@ -15,3 +16,42 @@ 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` + +install-home: + $(PYTHON) setup.py install --home="$(HOME)" --prefix="" --force + +# test targets +TESTFLAGS ?= $(shell echo $$HGTESTFLAGS) + +HGTESTS=$(HGROOT)/tests + +help: + @echo 'Commonly used make targets:' + @echo ' tests - run all tests in the automatic test suite' + @echo ' all-version-tests - run all tests against many hg versions' + @echo ' tests-%s - run all tests in the specified hg version' + +all: help + +_check_hgroot: + ifeq ($(HGROOT),) + $(error HGROOT is not set to the root of the hg source tree) + endif + +tests: _check_hgroot + cd tests && $(PYTHON) $(HGTESTS)/run-tests.py $(TESTFLAGS) + +# /!\ run outside of the compatibility branch output test will likely fails + +test-%: _check_hgroot + cd tests && $(PYTHON) $(HGTESTS)/run-tests.py $(TESTFLAGS) $@ + +tests-%: _check_hgroot + hg -R $(HGROOT) checkout $$(echo $@ | sed s/tests-//) && \ + (cd $(HGROOT) ; $(MAKE) clean ) && \ + cd tests && $(PYTHON) $(HGTESTS)/run-tests.py $(TESTFLAGS) + +# build a script to extract declared version +all-version-tests: tests-@ + +.PHONY: tests all-version-tests diff -r efda653c96a7 -r 94432e742a02 README --- a/README Tue Feb 28 17:22:21 2017 +0100 +++ b/README Fri Mar 31 15:33:59 2017 +0200 @@ -2,94 +2,188 @@ Mutable History For Mercurial ============================= -This package supplies the ``evolve`` extension for Mercurial, which -provides several commands to mutate history and deal with the -resulting issues. +Evolve Extension +================= + +This package supplies the ``evolve`` extension for Mercurial, -It also: +**The full implementation of the changeset evolution concept is still in +progress.** Please subscribe to the `evolve-testers mailing list +`_ to stay up to +date with changes. + +This extension: - - enables the "changeset obsolescence" feature of Mercurial - - issues several warning messages when trouble appears in your repository +* enables the “changeset evolution” feature of Mercurial core, -**This extension is experimental and not yet meant for production.** +* provides a set of commands to mutate your history, + +* issues several warning messages when troubles from some mutable appears in + your repository, -You can enable it by adding the line below to the ``extensions`` -section of your hgrc:: +* provides an ``hg evolve`` command to deal with such "troubles", - evolve = PATH/TO/evolve-main/hgext/evolve.py +* improves performance of obsolescence marker exchanges and discovery during + push and pull. + +Documentation +------------- We recommend reading the documentation first. An online version is available here: https://www.mercurial-scm.org/doc/evolution/ -Or see the ``doc/`` directory for a local copy. +How to Install +============== + +Using Pip +--------- + +You can install the latest evolution version usin pip:: + + $ pip install --user hg-evolve + +Then just enable it in you hgrc:: -Contribute -========== + $ hg config --edit # adds the two line below: + [extensions] + evolve = + +From Source +----------- + +To install a local version from source:: -Bugs are to be reported on the mercurial's bug tracker (component: evolution): -https://bz.mercurial-scm.org/buglist.cgi?component=evolution&query_format=advanced&resolution=--- + $ hg clone https://www.mercurial-scm.org/repo/evolve/ + $ cd evolve + $ pip install --user . + +Then just enable it in you hgrc:: + + $ hg config --edit # adds the two line below: + [extensions] + evolve = -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: +Documentation lives in ``doc/``. + +Server Only Setup +================= + +It is possible to enable a smaller subset of the extensions aimed at server +serving repository. It skips the additions of the new commands and local UI +messages that might add performance overheads. To use the server only +extension, install the package and use:: + + $ hg config --edit # adds the two line below: + [extensions] + evolve.serveronly = + - hg email --to mercurial-devel@mercurial-scm.org --flag evolve-ext --rev '' +How to Contribute +================= + +Bugs are to be reported on the mercurial's bug tracker (component: `evolution`_): + +.. _evolution: https://bz.mercurial-scm.org/buglist.cgi?component=evolution&query_format=advanced&resolution=--- -See also -https://mercurial-scm.org/wiki/ContributingChanges#Patch_descriptions -for guidelines on the patch description. +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@mercurial-scm.org --flag evolve-ext --rev '' + +For guidelines on the patch description, see the `official Mercurial guideline`_. + +.. _`official Mercurial guideline`: https://mercurial-scm.org/wiki/ContributingChanges#Patch_descriptions Please don't forget to update and run the tests when you fix a bug or add a feature. To run the tests, you need a working copy of Mercurial, -say in $HGSRC: +say in $HGSRC:: - cd tests - python $HGSRC/tests/run-tests.py + $ cd tests + $ python $HGSRC/tests/run-tests.py (evolve's stable and default branches correspond to Mercurial's stable and default branches. So to test evolve from default, you need -Mercurial on default.) - +Mercurial on default.), Older version of Mercurial are supportd on their respective ``mercurial-x.y`` branches. Changelog ========= -5.6.1 -- 2017-02-28 +6.0.0 -- In progress +-------------------- + +- push: improved detection of obsoleted remote branch (issue4354), +- drop compatibility for Mercurial < 3.8, +- removed old (unpackaged) pushexperiment extension, +- move all extensions in the official 'hgext3rd' namespace package, +- add the "topic" experimental extensions. See the README.topic file for details +- officially ship 'evolve.serveronly' extensions. That extensions contains + only the part related to exchange and is intended to be used by server. + + Using the extension will enable evolution, use 'experimental.evolution=!' + to disable obsmarkers echange. The old '__temporary__.advertiseobsolete' + option is no longer supported. + +- a new prototype of obsmarker discovery is available. The prototype is still + at early stage and not recommended for production. + Examples of current limitations: - - fix a crash that sometime happened when evolving merges. + - write access to the repo is highly recommanded for all operation, + - large memory footprint, + - initial caching is slow, + - unusable on large repo (because of various issue pointed earlier), + - likely to constains various bugs. + + It can be tested by setting `experimental.obshashrange=1` on both client and + server. It is recommanded to get in touch with the evolve maintainer if you + decide to test it. + +- the 'debugrecordpruneparents' have been moved into the 'evolve.legacy' + separate extension. enable that extentions if you need to convert/update + markers in an old repository. + +5.6.1 -- 2017-02-28 +------------------- + +- fix a crash that sometime happened when evolving merges. 5.6.0 -- 2017-02-01 +------------------- - - compatibility with Mercurial 4.1. - - improvement of prune error message. - - fold: require --from flag for folding revisions to working copy - - fix crash when trying to fold an empty revision set (issue5453) - - uncommit: preserve copy information of remaining files (issue5403) +- compatibility with Mercurial 4.1. +- improvement of prune error message. +- fold: require --from flag for folding revisions to working copy +- fix crash when trying to fold an empty revision set (issue5453) +- uncommit: preserve copy information of remaining files (issue5403) 5.5.0 -- 2016-10-30 +------------------- - - The {obsolete} template now yield "obsolete" or "". - - compatibility with Mercurial 4.0 - - Fix erroneous manifest computation when solving 'bumped' changeset. - - split: avoid crash on empty commit (issue5191), - - next: improve locking to avoid issue with working copy parent (issue5244) - - prev: improve locking to avoid issue with working copy parent (issue5244) - - evolve: fix abort suggestion to include '.' in 'hg update -C .' +- The {obsolete} template now yield "obsolete" or "". +- compatibility with Mercurial 4.0 +- Fix erroneous manifest computation when solving 'bumped' changeset. +- split: avoid crash on empty commit (issue5191), +- next: improve locking to avoid issue with working copy parent (issue5244) +- prev: improve locking to avoid issue with working copy parent (issue5244) +- evolve: fix abort suggestion to include '.' in 'hg update -C .' 5.4.1 -- 2016-08-01 +------------------- - compat with Mercurial 3.9 5.4.0 -- 2016-05-06 +------------------- - Some collaboration with the topic experimental extensions, - hg evolve --all with consider all troubles in your current topic, - preserve 'topic' during evolve, - 'next' and 'prev' restrict themself to the current topic by default, - remove the dangerous 'kill' alias for 'prune' (because 'hg kill -1' without -the leading 'hg' will give you an hardtime) + the leading 'hg' will give you an hardtime) - during 'hg evolve' skip unsupported merge instead of aborting - various documentation fix and update - hg summary now suggest 'hg evolve --continue when appropriate` @@ -98,6 +192,7 @@ - add a 'metaedit' command to rewrite changeset meta data. 5.3.0 -- 2016-02-11 +------------------- - split: add a new command to split changesets, - tests: drop our copy of 'run-tests.py' use core one instead, @@ -113,6 +208,7 @@ - updated help for the `evolve` command 5.2.1 -- 2015-11-02 +------------------- - add compatibility with Mercurial 3.6 - prune: fixed possible issue with lock and bookmark @@ -121,6 +217,7 @@ - take advantage of dirstate/transaction collaboration 5.2.0 -- 2015-06-25 +------------------- - evolve: gain a --rev option to control what revisions to evolve (issue4391) - evolve: revision are processed in the order they stack on destination @@ -140,6 +237,7 @@ - next: add a --evolve flag to evolve aspiring children when on a head 5.1.5 -- 2015-06-23 +------------------- - minor documentation cleanup - support -i option for `hg amend` if commit supports it (3.4) @@ -154,6 +252,7 @@ parent. 5.1.4 -- 2015-04-23 +------------------- - significant documentation update - fix issue4616: pulling with bundle2 would crash if common marker when @@ -161,6 +260,7 @@ - fix the debugobsrelsethashtree command 5.1.3 -- 2015-04-20 +------------------- - discovery: fix misbehaving discovery across python version - pull: properly install the bundle2 par generator @@ -169,26 +269,31 @@ - graft: avoid potential deadlock (acquires wlock before lock) 5.1.2 -- 2015-04-01 +------------------- - evolve: prevent a crash in httpclient_pushobsmarkers() when pushing 5.1.1 -- 2015-03-05 +------------------- - debugobsconvert: fix invalid markers during conversion - discovery: cache some of the obs hash computation to improve performance (issue4518) - revset: fix some crash with (issue4515) 5.1 -- 2015-01-30 +------------------- - evolve: explicitly disable bookmark on evolve (issue4432) - evolve: don't abort Mercurial on version mismatch - compatibility with mercurial 3.3 5.0.2 -- 2014-12-14 +------------------- - evolve: remove dependency to the rebase extension 5.0.1 -- 2014-11-25 +------------------- - amend: fix --logfile argument - evolve: preserve branch change when evolving @@ -199,9 +304,8 @@ - evolve: make next/prev only move bookmarks optionally - evolve: tell user which "base of divergent changeset" is not found - - 5.0.0 -- 2014-10-22 +------------------- - drop compat with Mercurial pre 3.2 - uncommit: add a --rev argument @@ -217,6 +321,7 @@ 4.1.0 -- 2014-08-08 +------------------- - amend: add -D/--current-date option - amend: add -U/--current-user option @@ -230,10 +335,12 @@ `experimental.verbose-obsolescence-exchange` variable (default to False). 4.0.1 -- 2014-08-08 +------------------- - createmarkers() accept an iterable (for compat with other extension) 4.0.0 -- 2014-06-03 +------------------- - require Mercurial version 3.0.1 or above - some compatibility fixes with future 3.1.0 @@ -247,13 +354,16 @@ - added multiple output during obsolescence markers exchange - only push markers relevant to pushed subset - add a new experimental way to exchange marker (when server support): + - added progress when pulling obsmarkers - only pull markers relevant to pulled subset - avoid exchanging common markers in some case - use bundle2 as transport when available. + - add a hook related to the new commands 3.3.2 -- 2014-05-14 +------------------- - fix a bug where evolve were creating changeset with 2 parents on windows (fix issues #16, #35 and #42) @@ -263,6 +373,7 @@ - fold: add squash as an alias 3.3.1 -- 2014-04-23 +------------------- - various language fix - active bookmark now move when using prev/next (#37) @@ -271,6 +382,7 @@ - fold: enable --date and --user options 3.3.0 -- 2014-03-04 +------------------- - raise Mercurial's minimal requirement to 2.7 - drop `latercomer` and `conflicting` compatibility. Those old alias are @@ -283,6 +395,7 @@ - report troubles creation from `hg import` 3.2.0 -- 2013-11-15 +------------------- - conform to the Mercurial custom of lowercase messages - added a small extension to experiment with obsolescence marker push @@ -296,6 +409,7 @@ - Tested with 2.6, 2.7 and 2.8 3.1.0 -- 2013-02-11 +------------------- - amend: drop deprecated --change option for amend - alias: add a grab alias to be used instead of graft -O @@ -307,23 +421,28 @@ - prune: add -u and -d option to control metadata 3.0.0 -- 2013-02-02 +------------------- - compatibility with 2.5 2.2.0 -- +------------------- - make evolve smarter at picking next troubled to solved without --any 2.1.0 -- 2012-12-03 +------------------- - qsync fixes - have qfold ask for commit message 2.0.0 -- 2012-10-26 +------------------- - compat with mercurial 2.4 1.1.0 -- 2012-10-26 +------------------- - fix troubles creation reporting from rebase - rename latecomer to bumped @@ -331,6 +450,7 @@ - smarter divergent handling 1.0.2 -- 2012-09-19 +------------------- - fix hg fold bug - fix hg pull --rebase @@ -338,11 +458,13 @@ - adapt to core movement (caches and --amend) 1.0.1 -- 2012-08-31 +------------------- - documentation improvement - fix a performance bug with hgweb 1.0 -- 2012-08-29 +------------------- - Align with Mercurial version 2.3 (drop 2.2 support). - stabilize handle killed parent @@ -363,6 +485,7 @@ 0.7 -- 2012-08-06 +------------------- - hook: work around insanely huge value in obsolete pushkey call - pushkey: properly handle abort during obsolete markers push @@ -374,12 +497,14 @@ - evolve: graft --continue is optional, test 0.6 -- 2012-07-31 +------------------- - obsolete: change warning output to match mercurial core on - qsync: ignore nonexistent nodes - make compat server both compatible with "dump" and "dump%i" version 0.5 -- 2012-07-16 +------------------- - obsolete: Detect conflicting changeset! - obsolete: adapt to core: marker are written in transaction now @@ -389,12 +514,14 @@ 0.4.1 -- 2012-07-10 +------------------- - [convert] properly exclude null successors from conversion - Ignore buggy marker in newerversion 0.4.0 -- 2012-07-06 +------------------- - obsolete: public changeset are no longer latecomer. - obsolete: move to official binary format @@ -402,6 +529,7 @@ - obsolete: we are not compatible with 2.1 any more 0.3.0 -- 2012-06-27 +------------------- - obsolete: Add "latecomer" error detection (stabilize does not handle resolution yet) - evolve: Introduce a new `uncommit` command to remove change from a changeset @@ -415,6 +543,7 @@ - template: add an {obsolete} keyword 0.2.0 -- 2012-06-20 +------------------- - stabilize: improve choice of the next changeset to stabilize - stabilize: improve resolution of several corner case @@ -422,5 +551,3 @@ - rebase: handle --collapse - evolve: add `obsolete` alias to `kill` - evolve: add `evolve` alias to `stabilize` - - diff -r efda653c96a7 -r 94432e742a02 README-topic --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README-topic Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,53 @@ +Topic Extension +================ + +This packages also provides the ``topic`` experiment in an independent +extension. It implements a new experimental concept to provide lightweight +feature branches for the mutable parts of the history. The experiments is still +at an early stage and have significant usability and performance issues when +enabled. + +How to Install +============== + +The ``topic`` extension is included into the ``evolve` package, so the same instruction apply. + +Using Pip +--------- + +You can install the latest version using pip:: + + $ pip install --user hg-evolve + +Then just enable it in you hgrc:: + + $ hg config --edit # adds the two line below: + [extensions] + topic = + +From Source +----------- + +To install a local version from source:: + + $ hg clone https://www.mercurial-scm.org/repo/evolve/ + $ cd evolve + $ make install-home + +Enable +------ + +The topic extensions is included in the evolve package. See the install instruction for evolve. + +Then enable it in you configuration:: + + $ hg config --edit # adds the two line below: + [extensions] + topic = + +Documentation +------------- + +* See 'hg help -e topic' for a generic help. +* See 'hg help topics' and 'hg help stack' for help on specific commands. +* See the 'tests/test-topic-tutorial.t' file for a quick tutorial. diff -r efda653c96a7 -r 94432e742a02 debian/rules --- a/debian/rules Tue Feb 28 17:22:21 2017 +0100 +++ b/debian/rules Fri Mar 31 15:33:59 2017 +0200 @@ -18,8 +18,8 @@ endif override_dh_python2: - # avoid conflict with mercurial's own hgext/__init__.py - find debian -name __init__.py -delete + # avoid conflict with mercurial's own hgext3rd/__init__.py + find debian -path '*/hgext3rd/__init__.py' -delete dh_python2 override_dh_auto_clean: clean-docs diff -r efda653c96a7 -r 94432e742a02 docs/conf.py --- a/docs/conf.py Tue Feb 28 17:22:21 2017 +0100 +++ b/docs/conf.py Fri Mar 31 15:33:59 2017 +0200 @@ -1,9 +1,9 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] -#autoclass_content = 'both' +# autoclass_content = 'both' # Add any paths that contain templates here, relative to this directory. -#templates_path = [] +# templates_path = [] # The suffix of source filenames. source_suffix = '.rst' @@ -25,7 +25,7 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' @@ -34,21 +34,21 @@ # List of directories, relative to source directories, that shouldn't be searched # for source files. -#exclude_dirs = [] +# exclude_dirs = [] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' @@ -60,7 +60,7 @@ # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. -#html_style = 'sphinx-default.css' +# html_style = 'sphinx-default.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -69,11 +69,11 @@ html_theme_path = ['.'] # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. -#html_logo = 'logo-evolve.svg' +# html_logo = 'logo-evolve.svg' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 @@ -91,34 +91,34 @@ # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. html_use_modindex = False # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. -#html_copy_source = True +# html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = '.html' +# html_file_suffix = '.html' # Output file base name for HTML help builder. -#htmlhelp_basename = '' +# htmlhelp_basename = '' diff -r efda653c96a7 -r 94432e742a02 docs/index.rst --- a/docs/index.rst Tue Feb 28 17:22:21 2017 +0100 +++ b/docs/index.rst Fri Mar 31 15:33:59 2017 +0200 @@ -125,7 +125,7 @@ Then add :: - evolve=~/src/evolve-main/hgext/evolve.py + evolve=~/src/evolve-main/hgext3rd/evolve/ in the ``[extensions]`` section (adding the section if necessary). Use the directory that you actually cloned to, of course. diff -r efda653c96a7 -r 94432e742a02 docs/sharing.rst --- a/docs/sharing.rst Tue Feb 28 17:22:21 2017 +0100 +++ b/docs/sharing.rst Fri Mar 31 15:33:59 2017 +0200 @@ -102,7 +102,7 @@ publish = false [extensions] - evolve = /path/to/evolve-main/hgext/evolve.py + evolve = /path/to/evolve-main/hgext3rd/evolve/ Then edit the configuration for ``dev-repo``:: @@ -111,7 +111,7 @@ and add :: [extensions] - evolve = /path/to/evolve-main/hgext/evolve.py + evolve = /path/to/evolve-main/hgext3rd/evolve/ Keep in mind that in real life, these repositories would probably be on separate computers, so you'd have to login to each one to configure @@ -331,7 +331,7 @@ and add :: [extensions] - evolve = /path/to/evolve-main/hgext/evolve.py + evolve = /path/to/evolve-main/hgext3rd/evolve/ Then edit Bob's repository configuration:: @@ -545,7 +545,7 @@ [extensions] rebase = - evolve = /path/to/evolve-main/hgext/evolve.py + evolve = /path/to/evolve-main/hgext3rd/evolve/ [phases] publish = false diff -r efda653c96a7 -r 94432e742a02 docs/test2rst.py --- a/docs/test2rst.py Tue Feb 28 17:22:21 2017 +0100 +++ b/docs/test2rst.py Fri Mar 31 15:33:59 2017 +0200 @@ -1,6 +1,9 @@ #!/usr/bin/env python -import os, os.path as op, re, sys +import os +import os.path as op +import re +import sys # line starts with two chars one of which is not a space (and both are not # newlines obviously) and ends with one or more newlines followed by two spaces @@ -32,7 +35,7 @@ def one_dir(base): index = INDEX - #doc = lambda x: op.join(op.dirname(__file__), 'docs', x) + # doc = lambda x: op.join(op.dirname(__file__), 'docs', x) for fn in sorted(os.listdir(base)): if not fn.endswith('.t'): @@ -40,23 +43,23 @@ name = os.path.splitext(fn)[0] content = one_file(op.join(base, fn)) target = op.join(base, name + '.rst') - #with file(doc(name + '.rst'), 'w') as f: - with file(target, 'w') as f: + # with file(doc(name + '.rst'), 'w') as f: + with open(target, 'w') as f: f.write(content) index += '\n ' + name - #with file(doc('index.rst'), 'w') as f: - # f.write(index) + # with file(doc('index.rst'), 'w') as f: + # f.write(index) def one_file(path): name = os.path.basename(path)[:-2] - return rstify(file(path).read(), name) + return rstify(open(path).read(), name) if __name__ == '__main__': if len(sys.argv) != 2: - print 'Please supply a path to tests dir as parameter' + print('Please supply a path to tests dir as parameter') sys.exit() main(sys.argv[1]) diff -r efda653c96a7 -r 94432e742a02 hgext/__init__.py --- a/hgext/__init__.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4 +0,0 @@ -from __future__ import absolute_import -import pkgutil -__path__ = pkgutil.extend_path(__path__, __name__) - diff -r efda653c96a7 -r 94432e742a02 hgext/directaccess.py --- a/hgext/directaccess.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,194 +0,0 @@ -""" This extension provides direct access -It is the ability to refer and access hidden sha in commands provided that you -know their value. -For example hg log -r xxx where xxx is a commit has should work whether xxx is -hidden or not as we assume that the user knows what he is doing when referring -to xxx. -""" -from mercurial import extensions -from mercurial import cmdutil -from mercurial import repoview -from mercurial import branchmap -from mercurial import revset -from mercurial import error -from mercurial import commands -from mercurial import hg -from mercurial import util -from mercurial.i18n import _ - -cmdtable = {} -command = cmdutil.command(cmdtable) - -# By default, all the commands have directaccess with warnings -# List of commands that have no directaccess and directaccess with no warning -directaccesslevel = [ - # Format: - # ('nowarning', 'evolve', 'prune'), - # means: no directaccess warning, for the command in evolve named prune - # - # ('error', None, 'serve'), - # means: no directaccess for the command in core named serve - # - # The list is ordered alphabetically by command names, starting with all - # the commands in core then all the commands in the extensions - # - # The general guideline is: - # - remove directaccess warnings for read only commands - # - no direct access for commands with consequences outside of the repo - # - leave directaccess warnings for all the other commands - # - ('nowarning', None, 'annotate'), - ('nowarning', None, 'archive'), - ('nowarning', None, 'bisect'), - ('nowarning', None, 'bookmarks'), - ('nowarning', None, 'bundle'), - ('nowarning', None, 'cat'), - ('nowarning', None, 'diff'), - ('nowarning', None, 'export'), - ('nowarning', None, 'identify'), - ('nowarning', None, 'incoming'), - ('nowarning', None, 'log'), - ('nowarning', None, 'manifest'), - ('error', None, 'outgoing'), # confusing if push errors and not outgoing - ('error', None, 'push'), # destructive - ('nowarning', None, 'revert'), - ('error', None, 'serve'), - ('nowarning', None, 'tags'), - ('nowarning', None, 'unbundle'), - ('nowarning', None, 'update'), -] - -def reposetup(ui, repo): - repo._explicitaccess = set() - -def _computehidden(repo): - hidden = repoview.filterrevs(repo, 'visible') - cl = repo.changelog - dynamic = hidden & repo._explicitaccess - if dynamic: - blocked = cl.ancestors(dynamic, inclusive=True) - hidden = frozenset(r for r in hidden if r not in blocked) - return hidden - -def setupdirectaccess(): - """ Add two new filtername that behave like visible to provide direct access - and direct access with warning. Wraps the commands to setup direct access - """ - repoview.filtertable.update({'visible-directaccess-nowarn': _computehidden}) - repoview.filtertable.update({'visible-directaccess-warn': _computehidden}) - branchmap.subsettable['visible-directaccess-nowarn'] = 'visible' - branchmap.subsettable['visible-directaccess-warn'] = 'visible' - - for warn, ext, cmd in directaccesslevel: - try: - cmdtable = extensions.find(ext).cmdtable if ext else commands.table - wrapper = wrapwitherror if warn == 'error' else wrapwithoutwarning - extensions.wrapcommand(cmdtable, cmd, wrapper) - except (error.UnknownCommand, KeyError): - pass - -def wrapwitherror(orig, ui, repo, *args, **kwargs): - if repo and repo.filtername == 'visible-directaccess-warn': - repo = repo.filtered('visible') - return orig(ui, repo, *args, **kwargs) - -def wrapwithoutwarning(orig, ui, repo, *args, **kwargs): - if repo and repo.filtername == 'visible-directaccess-warn': - repo = repo.filtered("visible-directaccess-nowarn") - return orig(ui, repo, *args, **kwargs) - -def uisetup(ui): - """ Change ordering of extensions to ensure that directaccess extsetup comes - after the one of the extensions in the loadsafter list """ - loadsafter = ui.configlist('directaccess','loadsafter') - order = list(extensions._order) - directaccesidx = order.index('directaccess') - - # The min idx for directaccess to load after all the extensions in loadafter - minidxdirectaccess = directaccesidx - - for ext in loadsafter: - try: - minidxdirectaccess = max(minidxdirectaccess, order.index(ext)) - except ValueError: - pass # extension not loaded - - if minidxdirectaccess > directaccesidx: - order.insert(minidxdirectaccess + 1, 'directaccess') - order.remove('directaccess') - extensions._order = order - -def _repository(orig, *args, **kwargs): - """Make visible-directaccess-warn the default filter for new repos""" - repo = orig(*args, **kwargs) - return repo.filtered("visible-directaccess-warn") - -def extsetup(ui): - extensions.wrapfunction(revset, 'posttreebuilthook', _posttreebuilthook) - extensions.wrapfunction(hg, 'repository', _repository) - setupdirectaccess() - -hashre = util.re.compile('[0-9a-fA-F]{1,40}') - -_listtuple = ('symbol', '_list') - -def _ishashsymbol(symbol, maxrev): - # Returns true if symbol looks like a hash - try: - n = int(symbol) - if n <= maxrev: - # It's a rev number - return False - except ValueError: - pass - return hashre.match(symbol) - -def gethashsymbols(tree, maxrev): - # Returns the list of symbols of the tree that look like hashes - # for example for the revset 3::abe3ff it will return ('abe3ff') - if not tree: - return [] - - results = [] - if len(tree) == 2 and tree[0] == "symbol": - results.append(tree[1]) - elif tree[0] == "func" and tree[1] == _listtuple: - # the optimiser will group sequence of hash request - results += tree[2][1].split('\0') - elif len(tree) >= 3: - for subtree in tree[1:]: - results += gethashsymbols(subtree, maxrev) - # return directly, we don't need to filter symbols again - return results - return [s for s in results if _ishashsymbol(s, maxrev)] - -def _posttreebuilthook(orig, tree, repo): - # This is use to enabled direct hash access - # We extract the symbols that look like hashes and add them to the - # explicitaccess set - orig(tree, repo) - filternm = "" - if repo is not None: - filternm = repo.filtername - if filternm is not None and filternm.startswith('visible-directaccess'): - prelength = len(repo._explicitaccess) - accessbefore = set(repo._explicitaccess) - cl = repo.unfiltered().changelog - repo.symbols = gethashsymbols(tree, len(cl)) - for node in repo.symbols: - try: - node = cl._partialmatch(node) - except error.LookupError: - node = None - if node is not None: - rev = cl.rev(node) - if rev not in repo.changelog: - repo._explicitaccess.add(rev) - if prelength != len(repo._explicitaccess): - if repo.filtername != 'visible-directaccess-nowarn': - unhiddencommits = repo._explicitaccess - accessbefore - repo.ui.warn(_("Warning: accessing hidden changesets %s " - "for write operation\n") % - (",".join([str(repo.unfiltered()[l]) - for l in unhiddencommits]))) - repo.invalidatevolatilesets() diff -r efda653c96a7 -r 94432e742a02 hgext/drophack.py --- a/hgext/drophack.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,163 +0,0 @@ -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. -'''This extension add a hacky command to drop changeset during review - -This extension is intended as a temporary hack to allow Matt Mackall to use -evolve in the Mercurial review it self. You should probably not use it if your -name is not Matt Mackall. -''' - -import os -import time -import contextlib - -from mercurial.i18n import _ -from mercurial import cmdutil -from mercurial import repair -from mercurial import scmutil -from mercurial import lock as lockmod -from mercurial import util -from mercurial import commands - -cmdtable = {} -command = cmdutil.command(cmdtable) - - -@contextlib.contextmanager -def timed(ui, caption): - ostart = os.times() - cstart = time.time() - yield - cstop = time.time() - ostop = os.times() - wall = cstop - cstart - user = ostop[0] - ostart[0] - sys = ostop[1] - ostart[1] - comb = user + sys - ui.write("%s: wall %f comb %f user %f sys %f\n" - % (caption, wall, comb, user, sys)) - -def obsmarkerchainfrom(obsstore, nodes): - """return all marker chain starting from node - - Starting from mean "use as successors".""" - # XXX need something smarter for descendant of bumped changeset - seennodes = set(nodes) - seenmarkers = set() - pendingnodes = set(nodes) - precursorsmarkers = obsstore.precursors - while pendingnodes: - current = pendingnodes.pop() - new = set() - for precmark in precursorsmarkers.get(current, ()): - if precmark in seenmarkers: - continue - seenmarkers.add(precmark) - new.add(precmark[0]) - yield precmark - new -= seennodes - pendingnodes |= new - -def stripmarker(ui, repo, markers): - """remove from the repo obsstore - - The old obsstore content is saved in a `obsstore.prestrip` file - """ - repo = repo.unfiltered() - repo.destroying() - oldmarkers = list(repo.obsstore._all) - util.rename(repo.sjoin('obsstore'), - repo.join('obsstore.prestrip')) - del repo.obsstore # drop the cache - newstore = repo.obsstore - assert not newstore # should be empty after rename - newmarkers = [m for m in oldmarkers if m not in markers] - tr = repo.transaction('drophack') - try: - newstore.add(tr, newmarkers) - tr.close() - finally: - tr.release() - repo.destroyed() - - -@command('drop', [('r', 'rev', [], 'revision to update')], _('[-r] revs')) -def cmddrop(ui, repo, *revs, **opts): - """I'm hacky do not use me! - - This command strip a changeset, its precursors and all obsolescence marker - associated to its chain. - - There is no way to limit the extend of the purge yet. You may have to - repull from other source to get some changeset and obsolescence marker - back. - - This intended for Matt Mackall usage only. do not use me. - """ - revs = list(revs) - revs.extend(opts['rev']) - if not revs: - revs = ['.'] - # get the changeset - revs = scmutil.revrange(repo, revs) - if not revs: - ui.write_err('no revision to drop\n') - return 1 - # lock from the beginning to prevent race - wlock = lock = None - try: - wlock = repo.wlock() - lock = repo.lock() - # check they have no children - if repo.revs('%ld and public()', revs): - ui.write_err('cannot drop public revision') - return 1 - if repo.revs('children(%ld) - %ld', revs, revs): - ui.write_err('cannot drop revision with children') - return 1 - if repo.revs('. and %ld', revs): - newrevs = repo.revs('max(::. - %ld)', revs) - if newrevs: - assert len(newrevs) == 1 - newrev = newrevs.first() - else: - newrev = -1 - commands.update(ui, repo, newrev) - ui.status(_('working directory now at %s\n') % repo[newrev]) - # get all markers and successors up to root - nodes = [repo[r].node() for r in revs] - with timed(ui, 'search obsmarker'): - markers = set(obsmarkerchainfrom(repo.obsstore, nodes)) - ui.write('%i obsmarkers found\n' % len(markers)) - cl = repo.unfiltered().changelog - with timed(ui, 'search nodes'): - allnodes = set(nodes) - allnodes.update(m[0] for m in markers if cl.hasnode(m[0])) - ui.write('%i nodes found\n' % len(allnodes)) - cl = repo.changelog - visiblenodes = set(n for n in allnodes if cl.hasnode(n)) - # check constraint again - if repo.revs('%ln and public()', visiblenodes): - ui.write_err('cannot drop public revision') - return 1 - if repo.revs('children(%ln) - %ln', visiblenodes, visiblenodes): - ui.write_err('cannot drop revision with children') - return 1 - - if markers: - # strip them - with timed(ui, 'strip obsmarker'): - stripmarker(ui, repo, markers) - # strip the changeset - with timed(ui, 'strip nodes'): - repair.strip(ui, repo, list(allnodes), backup="all", - topic='drophack') - - finally: - lockmod.release(lock, wlock) - - # rewrite the whole file. - # print data. - # - time to compute the chain - # - time to strip the changeset - # - time to strip the obs marker. diff -r efda653c96a7 -r 94432e742a02 hgext/evolve.py --- a/hgext/evolve.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,4187 +0,0 @@ -# Copyright 2011 Peter Arrenbrecht -# Logilab SA -# Pierre-Yves David -# Patrick Mezard -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. - -'''extends Mercurial feature related to Changeset Evolution - -This extension provides several commands to mutate history and deal with -resulting issues. - -It also: - - - enables the "Changeset Obsolescence" feature of Mercurial, - - alters core commands and extensions that rewrite history to use - this feature, - - improves some aspect of the early implementation in Mercurial core -''' - -__version__ = '5.6.0' -testedwith = '3.4.3 3.5.2 3.6.2 3.7.3 3.8.1 3.9 4.0 4.1' -buglink = 'https://bz.mercurial-scm.org/' - - -evolutionhelptext = """ -Obsolescence markers make it possible to mark changesets that have been -deleted or superset in a new version of the changeset. - -Unlike the previous way of handling such changes, by stripping the old -changesets from the repository, obsolescence markers can be propagated -between repositories. This allows for a safe and simple way of exchanging -mutable history and altering it after the fact. Changeset phases are -respected, such that only draft and secret changesets can be altered (see -:hg:`help phases` for details). - -Obsolescence is tracked using "obsolete markers", a piece of metadata -tracking which changesets have been made obsolete, potential successors for -a given changeset, the moment the changeset was marked as obsolete, and the -user who performed the rewriting operation. The markers are stored -separately from standard changeset data can be exchanged without any of the -precursor changesets, preventing unnecessary exchange of obsolescence data. - -The complete set of obsolescence markers describes a history of changeset -modifications that is orthogonal to the repository history of file -modifications. This changeset history allows for detection and automatic -resolution of edge cases arising from multiple users rewriting the same part -of history concurrently. - -Current feature status -====================== - -This feature is still in development. If you see this help, you have enabled an -extension that turned this feature on. - -Obsolescence markers will be exchanged between repositories that explicitly -assert support for the obsolescence feature (this can currently only be done -via an extension).""".strip() - - -import sys, os -import random -try: - import StringIO as io - StringIO = io.StringIO -except ImportError: - import io - StringIO = io.StringIO -import re -import collections -import socket -import errno -import hashlib -import struct -sha1re = re.compile(r'\b[0-9a-f]{6,40}\b') - -import mercurial -from mercurial import util -from mercurial import repair - -try: - from mercurial import obsolete - if not obsolete._enabled: - obsolete._enabled = True - from mercurial import wireproto - gboptslist = getattr(wireproto, 'gboptslist', None) - gboptsmap = getattr(wireproto, 'gboptsmap', None) -except (ImportError, AttributeError): - gboptslist = gboptsmap = None - -# Flags for enabling optional parts of evolve -commandopt = 'allnewcommands' - -from mercurial import bookmarks as bookmarksmod -from mercurial import cmdutil -from mercurial import commands -from mercurial import context -from mercurial import copies -from mercurial import error -from mercurial import exchange -from mercurial import extensions -from mercurial import help -from mercurial import httppeer -from mercurial import hg -from mercurial import lock as lockmod -from mercurial import merge -from mercurial import node -from mercurial import phases -from mercurial import patch -from mercurial import revset -from mercurial import scmutil -from mercurial import templatekw -from mercurial.i18n import _ -from mercurial.commands import walkopts, commitopts, commitopts2, mergetoolopts -from mercurial.node import nullid -from mercurial import wireproto -from mercurial import localrepo -from mercurial.hgweb import hgweb_mod - -cmdtable = {} -command = cmdutil.command(cmdtable) - -_pack = struct.pack -_unpack = struct.unpack - -if gboptsmap is not None: - memfilectx = context.memfilectx -elif gboptslist is not None: - oldmemfilectx = context.memfilectx - def memfilectx(repo, *args, **kwargs): - return oldmemfilectx(*args, **kwargs) -else: - 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]]) -if hasinteractivemode: - interactiveopt = [['i', 'interactive', None, _('use interactive mode')]] -else: - interactiveopt = [] -# This extension contains the following code -# -# - Extension Helper code -# - Obsolescence cache -# - ... -# - Older format compat - - -##################################################################### -### Extension helper ### -##################################################################### - -class exthelper(object): - """Helper for modular extension setup - - A single helper should be instantiated for each extension. Helper - methods are then used as decorators for various purpose. - - All decorators return the original function and may be chained. - """ - - def __init__(self): - self._uicallables = [] - self._extcallables = [] - self._repocallables = [] - self._revsetsymbols = [] - self._templatekws = [] - self._commandwrappers = [] - self._extcommandwrappers = [] - self._functionwrappers = [] - self._duckpunchers = [] - - def final_uisetup(self, ui): - """Method to be used as the extension uisetup - - The following operations belong here: - - - Changes to ui.__class__ . The ui object that will be used to run the - command has not yet been created. Changes made here will affect ui - objects created after this, and in particular the ui that will be - passed to runcommand - - Command wraps (extensions.wrapcommand) - - Changes that need to be visible to other extensions: because - initialization occurs in phases (all extensions run uisetup, then all - run extsetup), a change made here will be visible to other extensions - during extsetup - - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch - module members - - Setup of pre-* and post-* hooks - - pushkey setup - """ - for cont, funcname, func in self._duckpunchers: - setattr(cont, funcname, func) - for command, wrapper, opts in self._commandwrappers: - entry = extensions.wrapcommand(commands.table, command, wrapper) - if opts: - for short, long, val, msg in opts: - entry[1].append((short, long, val, msg)) - for cont, funcname, wrapper in self._functionwrappers: - extensions.wrapfunction(cont, funcname, wrapper) - for c in self._uicallables: - c(ui) - - def final_extsetup(self, ui): - """Method to be used as a the extension extsetup - - The following operations belong here: - - - Changes depending on the status of other extensions. (if - extensions.find('mq')) - - Add a global option to all commands - - Register revset functions - """ - knownexts = {} - for name, symbol in self._revsetsymbols: - revset.symbols[name] = symbol - for name, kw in self._templatekws: - templatekw.keywords[name] = kw - for ext, command, wrapper, opts in self._extcommandwrappers: - if ext not in knownexts: - try: - e = extensions.find(ext) - except KeyError: - # Extension isn't enabled, so don't bother trying to wrap - # it. - continue - knownexts[ext] = e.cmdtable - entry = extensions.wrapcommand(knownexts[ext], command, wrapper) - if opts: - for short, long, val, msg in opts: - entry[1].append((short, long, val, msg)) - - for c in self._extcallables: - c(ui) - - def final_reposetup(self, ui, repo): - """Method to be used as the extension reposetup - - The following operations belong here: - - - All hooks but pre-* and post-* - - Modify configuration variables - - Changes to repo.__class__, repo.dirstate.__class__ - """ - for c in self._repocallables: - c(ui, repo) - - def uisetup(self, call): - """Decorated function will be executed during uisetup - - example:: - - @eh.uisetup - def setupbabar(ui): - print 'this is uisetup!' - """ - self._uicallables.append(call) - return call - - def extsetup(self, call): - """Decorated function will be executed during extsetup - - example:: - - @eh.extsetup - def setupcelestine(ui): - print 'this is extsetup!' - """ - self._extcallables.append(call) - return call - - def reposetup(self, call): - """Decorated function will be executed during reposetup - - example:: - - @eh.reposetup - def setupzephir(ui, repo): - print 'this is reposetup!' - """ - self._repocallables.append(call) - return call - - def revset(self, symbolname): - """Decorated function is a revset symbol - - The name of the symbol must be given as the decorator argument. - The symbol is added during `extsetup`. - - example:: - - @eh.revset('hidden') - def revsetbabar(repo, subset, x): - args = revset.getargs(x, 0, 0, 'babar accept no argument') - return [r for r in subset if 'babar' in repo[r].description()] - """ - def dec(symbol): - self._revsetsymbols.append((symbolname, symbol)) - return symbol - return dec - - - def templatekw(self, keywordname): - """Decorated function is a template keyword - - The name of the keyword must be given as the decorator argument. - The symbol is added during `extsetup`. - - example:: - - @eh.templatekw('babar') - def kwbabar(ctx): - return 'babar' - """ - def dec(keyword): - self._templatekws.append((keywordname, keyword)) - return keyword - return dec - - def wrapcommand(self, command, extension=None, opts=[]): - """Decorated function is a command wrapper - - The name of the command must be given as the decorator argument. - The wrapping is installed during `uisetup`. - - If the second option `extension` argument is provided, the wrapping - will be applied in the extension commandtable. This argument must be a - string that will be searched using `extension.find` if not found and - Abort error is raised. If the wrapping applies to an extension, it is - installed during `extsetup`. - - example:: - - @eh.wrapcommand('summary') - def wrapsummary(orig, ui, repo, *args, **kwargs): - ui.note('Barry!') - return orig(ui, repo, *args, **kwargs) - - The `opts` argument allows specifying additional arguments for the - command. - - """ - def dec(wrapper): - if extension is None: - self._commandwrappers.append((command, wrapper, opts)) - else: - self._extcommandwrappers.append((extension, command, wrapper, - opts)) - return wrapper - return dec - - def wrapfunction(self, container, funcname): - """Decorated function is a function wrapper - - This function takes two arguments, the container and the name of the - function to wrap. The wrapping is performed during `uisetup`. - (there is no extension support) - - example:: - - @eh.function(discovery, 'checkheads') - def wrapfunction(orig, *args, **kwargs): - ui.note('His head smashed in and his heart cut out') - return orig(*args, **kwargs) - """ - def dec(wrapper): - self._functionwrappers.append((container, funcname, wrapper)) - return wrapper - return dec - - def addattr(self, container, funcname): - """Decorated function is to be added to the container - - This function takes two arguments, the container and the name of the - function to wrap. The wrapping is performed during `uisetup`. - - example:: - - @eh.function(context.changectx, 'babar') - def babar(ctx): - return 'babar' in ctx.description - """ - def dec(func): - self._duckpunchers.append((container, funcname, func)) - return func - return dec - -eh = exthelper() -uisetup = eh.final_uisetup -extsetup = eh.final_extsetup -reposetup = eh.final_reposetup - -##################################################################### -### Option configuration ### -##################################################################### - -@eh.reposetup # must be the first of its kin. -def _configureoptions(ui, repo): - # If no capabilities are specified, enable everything. - # This is so existing evolve users don't need to change their config. - evolveopts = ui.configlist('experimental', 'evolution') - if not evolveopts: - evolveopts = ['all'] - ui.setconfig('experimental', 'evolution', evolveopts, 'evolve') - -@eh.uisetup -def _configurecmdoptions(ui): - # Unregister evolve commands if the command capability is not specified. - # - # This must be in the same function as the option configuration above to - # guarantee it happens after the above configuration, but before the - # extsetup functions. - evolvecommands = ui.configlist('experimental', 'evolutioncommands') - evolveopts = ui.configlist('experimental', 'evolution') - if evolveopts and (commandopt not in evolveopts and - 'all' not in evolveopts): - # We build whitelist containing the commands we want to enable - whitelist = set() - for cmd in evolvecommands: - matchingevolvecommands = [e for e in cmdtable.keys() if cmd in e] - if not matchingevolvecommands: - raise error.Abort(_('unknown command: %s') % cmd) - elif len(matchingevolvecommands) > 1: - msg = _('ambiguous command specification: "%s" matches %r') - raise error.Abort(msg % (cmd, matchingevolvecommands)) - else: - whitelist.add(matchingevolvecommands[0]) - for disabledcmd in set(cmdtable) - whitelist: - del cmdtable[disabledcmd] - -##################################################################### -### experimental behavior ### -##################################################################### - -commitopts3 = [ - ('D', 'current-date', None, - _('record the current date as commit date')), - ('U', 'current-user', None, - _('record the current user as committer')), -] - -def _resolveoptions(ui, opts): - """modify commit options dict to handle related options - - For now, all it does is figure out the commit date: respect -D unless - -d was supplied. - """ - # N.B. this is extremely similar to setupheaderopts() in mq.py - if not opts.get('date') and opts.get('current_date'): - opts['date'] = '%d %d' % util.makedate() - if not opts.get('user') and opts.get('current_user'): - opts['user'] = ui.username() - -getrevs = obsolete.getrevs - -##################################################################### -### Additional Utilities ### -##################################################################### - -# This section contains a lot of small utility function and method - -# - Function to create markers -# - useful alias pstatus and pdiff (should probably go in evolve) -# - "troubles" method on changectx -# - function to travel through the obsolescence graph -# - function to find useful changeset to stabilize - - -### Useful alias - -@eh.uisetup -def _installalias(ui): - if ui.config('alias', 'pstatus', None) is None: - ui.setconfig('alias', 'pstatus', 'status --rev .^', 'evolve') - if ui.config('alias', 'pdiff', None) is None: - ui.setconfig('alias', 'pdiff', 'diff --rev .^', 'evolve') - if ui.config('alias', 'olog', None) is None: - ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden", - 'evolve') - if ui.config('alias', 'odiff', None) is None: - ui.setconfig('alias', 'odiff', - "diff --hidden --rev 'limit(precursors(.),1)' --rev .", - 'evolve') - if ui.config('alias', 'grab', None) is None: - if os.name == 'nt': - ui.setconfig('alias', 'grab', - "! " + util.hgexecutable() + " rebase --dest . --rev $@ && " - + util.hgexecutable() + " up tip", - 'evolve') - else: - ui.setconfig('alias', 'grab', - "! $HG rebase --dest . --rev $@ && $HG up tip", - 'evolve') - - -### Troubled revset symbol - -@eh.revset('troubled') -def revsettroubled(repo, subset, x): - """``troubled()`` - Changesets with troubles. - """ - revset.getargs(x, 0, 0, 'troubled takes no arguments') - troubled = set() - troubled.update(getrevs(repo, 'unstable')) - troubled.update(getrevs(repo, 'bumped')) - troubled.update(getrevs(repo, 'divergent')) - troubled = revset.baseset(troubled) - troubled.sort() # set is non-ordered, enforce order - return subset & troubled - -### Obsolescence graph - -# XXX SOME MAJOR CLEAN UP TO DO HERE XXX - -def _precursors(repo, s): - """Precursor of a changeset""" - cs = set() - nm = repo.changelog.nodemap - markerbysubj = repo.obsstore.precursors - node = repo.changelog.node - for r in s: - for p in markerbysubj.get(node(r), ()): - pr = nm.get(p[0]) - if pr is not None: - cs.add(pr) - cs -= repo.changelog.filteredrevs # nodemap has no filtering - return cs - -def _allprecursors(repo, s): # XXX we need a better naming - """transitive precursors of a subset""" - node = repo.changelog.node - toproceed = [node(r) for r in s] - seen = set() - allsubjects = repo.obsstore.precursors - while toproceed: - nc = toproceed.pop() - for mark in allsubjects.get(nc, ()): - np = mark[0] - if np not in seen: - seen.add(np) - toproceed.append(np) - nm = repo.changelog.nodemap - cs = set() - for p in seen: - pr = nm.get(p) - if pr is not None: - cs.add(pr) - cs -= repo.changelog.filteredrevs # nodemap has no filtering - return cs - -def _successors(repo, s): - """Successors of a changeset""" - cs = set() - node = repo.changelog.node - nm = repo.changelog.nodemap - markerbyobj = repo.obsstore.successors - for r in s: - for p in markerbyobj.get(node(r), ()): - for sub in p[1]: - sr = nm.get(sub) - if sr is not None: - cs.add(sr) - cs -= repo.changelog.filteredrevs # nodemap has no filtering - return cs - -def _allsuccessors(repo, s, haltonflags=0): # XXX we need a better naming - """transitive successors of a subset - - haltonflags allows to provide flags which prevent the evaluation of a - marker. """ - node = repo.changelog.node - toproceed = [node(r) for r in s] - seen = set() - allobjects = repo.obsstore.successors - while toproceed: - nc = toproceed.pop() - for mark in allobjects.get(nc, ()): - if mark[2] & haltonflags: - continue - for sub in mark[1]: - if sub == nullid: - continue # should not be here! - if sub not in seen: - seen.add(sub) - toproceed.append(sub) - nm = repo.changelog.nodemap - cs = set() - for s in seen: - sr = nm.get(s) - if sr is not None: - cs.add(sr) - cs -= repo.changelog.filteredrevs # nodemap has no filtering - return cs - - - - -##################################################################### -### Extending revset and template ### -##################################################################### - -# this section add several useful revset symbol not yet in core. -# they are subject to changes - - -### XXX I'm not sure this revset is useful -@eh.revset('suspended') -def revsetsuspended(repo, subset, x): - """``suspended()`` - Obsolete changesets with non-obsolete descendants. - """ - revset.getargs(x, 0, 0, 'suspended takes no arguments') - suspended = revset.baseset(getrevs(repo, 'suspended')) - suspended.sort() - return subset & suspended - - -@eh.revset('precursors') -def revsetprecursors(repo, subset, x): - """``precursors(set)`` - Immediate precursors of changesets in set. - """ - s = revset.getset(repo, revset.fullreposet(repo), x) - s = revset.baseset(_precursors(repo, s)) - s.sort() - return subset & s - - -@eh.revset('allprecursors') -def revsetallprecursors(repo, subset, x): - """``allprecursors(set)`` - Transitive precursors of changesets in set. - """ - s = revset.getset(repo, revset.fullreposet(repo), x) - s = revset.baseset(_allprecursors(repo, s)) - s.sort() - return subset & s - - -@eh.revset('successors') -def revsetsuccessors(repo, subset, x): - """``successors(set)`` - Immediate successors of changesets in set. - """ - s = revset.getset(repo, revset.fullreposet(repo), x) - s = revset.baseset(_successors(repo, s)) - s.sort() - return subset & s - -@eh.revset('allsuccessors') -def revsetallsuccessors(repo, subset, x): - """``allsuccessors(set)`` - Transitive successors of changesets in set. - """ - s = revset.getset(repo, revset.fullreposet(repo), x) - s = revset.baseset(_allsuccessors(repo, s)) - s.sort() - return subset & s - -### template keywords -# XXX it does not handle troubles well :-/ - -@eh.templatekw('obsolete') -def obsoletekw(repo, ctx, templ, **args): - """:obsolete: String. Whether the changeset is ``obsolete``. - """ - if ctx.obsolete(): - return 'obsolete' - return '' - -@eh.templatekw('troubles') -def showtroubles(repo, ctx, **args): - """:troubles: List of strings. Evolution troubles affecting the changeset - (zero or more of "unstable", "divergent" or "bumped").""" - return templatekw.showlist('trouble', ctx.troubles(), plural='troubles', - **args) - -##################################################################### -### Various trouble warning ### -##################################################################### - -# This section take care of issue warning to the user when troubles appear - - -def _warnobsoletewc(ui, repo): - if repo['.'].obsolete(): - ui.warn(_('working directory parent is obsolete!\n')) - if (not ui.quiet) and obsolete.isenabled(repo, commandopt): - ui.warn(_("(use 'hg evolve' to update to its successor)\n")) - -@eh.wrapcommand("update") -@eh.wrapcommand("pull") -def wrapmayobsoletewc(origfn, ui, repo, *args, **opts): - """Warn that the working directory parent is an obsolete changeset""" - def warnobsolete(): - _warnobsoletewc(ui, repo) - wlock = None - try: - wlock = repo.wlock() - repo._afterlock(warnobsolete) - res = origfn(ui, repo, *args, **opts) - finally: - lockmod.release(wlock) - return res - -@eh.wrapcommand("parents") -def wrapparents(origfn, ui, repo, *args, **opts): - res = origfn(ui, repo, *args, **opts) - _warnobsoletewc(ui, repo) - return res - -# XXX this could wrap transaction code -# XXX (but this is a bit a layer violation) -@eh.wrapcommand("commit") -@eh.wrapcommand("import") -@eh.wrapcommand("push") -@eh.wrapcommand("pull") -@eh.wrapcommand("graft") -@eh.wrapcommand("phase") -@eh.wrapcommand("unbundle") -def warnobserrors(orig, ui, repo, *args, **kwargs): - """display warning is the command resulted in more instable changeset""" - # part of the troubled stuff may be filtered (stash ?) - # This needs a better implementation but will probably wait for core. - filtered = repo.changelog.filteredrevs - priorunstables = len(set(getrevs(repo, 'unstable')) - filtered) - priorbumpeds = len(set(getrevs(repo, 'bumped')) - filtered) - priordivergents = len(set(getrevs(repo, 'divergent')) - filtered) - ret = orig(ui, repo, *args, **kwargs) - # workaround phase stupidity - #phases._filterunknown(ui, repo.changelog, repo._phasecache.phaseroots) - filtered = repo.changelog.filteredrevs - newunstables = \ - len(set(getrevs(repo, 'unstable')) - filtered) - priorunstables - newbumpeds = \ - len(set(getrevs(repo, 'bumped')) - filtered) - priorbumpeds - newdivergents = \ - len(set(getrevs(repo, 'divergent')) - filtered) - priordivergents - if newunstables > 0: - ui.warn(_('%i new unstable changesets\n') % newunstables) - if newbumpeds > 0: - ui.warn(_('%i new bumped changesets\n') % newbumpeds) - if newdivergents > 0: - ui.warn(_('%i new divergent changesets\n') % newdivergents) - return ret - -@eh.wrapfunction(mercurial.exchange, 'push') -def push(orig, repo, *args, **opts): - """Add a hint for "hg evolve" when troubles make push fails - """ - try: - return orig(repo, *args, **opts) - except error.Abort as ex: - hint = _("use 'hg evolve' to get a stable history " - "or --force to ignore warnings") - if (len(ex.args) >= 1 - and ex.args[0].startswith('push includes ') - and ex.hint is None): - ex.hint = hint - raise - -def summaryhook(ui, repo): - def write(fmt, count): - s = fmt % count - if count: - ui.write(s) - else: - ui.note(s) - - # 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 - state = _evolvestateread(repo) - if state is not None: - # i18n: column positioning for "hg summary" - ui.write(_('evolve: (evolve --continue)\n')) - -@eh.extsetup -def obssummarysetup(ui): - cmdutil.summaryhooks.add('evolve', summaryhook) - - -##################################################################### -### Core Other extension compat ### -##################################################################### - - -@eh.extsetup -def _rebasewrapping(ui): - # warning about more obsolete - try: - rebase = extensions.find('rebase') - if rebase: - extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) - except KeyError: - pass # rebase not found - try: - histedit = extensions.find('histedit') - if histedit: - extensions.wrapcommand(histedit.cmdtable, 'histedit', warnobserrors) - except KeyError: - pass # histedit not found - -##################################################################### -### Old Evolve extension content ### -##################################################################### - -# XXX need clean up and proper sorting in other section - -### util function -############################# - -### changeset rewriting logic -############################# - -def rewrite(repo, old, updates, head, newbases, commitopts): - """Return (nodeid, created) where nodeid is the identifier of the - changeset generated by the rewrite process, and created is True if - nodeid was actually created. If created is False, nodeid - references a changeset existing before the rewrite call. - """ - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - tr = repo.transaction('rewrite') - if len(old.parents()) > 1: #XXX remove this unnecessary limitation. - raise error.Abort(_('cannot amend merge changesets')) - base = old.p1() - updatebookmarks = _bookmarksupdater(repo, old.node(), tr) - - # commit a new version of the old changeset, including the update - # collect all files which might be affected - files = set(old.files()) - for u in updates: - files.update(u.files()) - - # Recompute copies (avoid recording a -> b -> a) - copied = copies.pathcopies(base, head) - - - # prune files which were reverted by the updates - def samefile(f): - if f in head.manifest(): - a = head.filectx(f) - if f in base.manifest(): - b = base.filectx(f) - return (a.data() == b.data() - and a.flags() == b.flags()) - else: - return False - else: - return f not in base.manifest() - files = [f for f in files if not samefile(f)] - # commit version of these files as defined by head - headmf = head.manifest() - def filectxfn(repo, ctx, path): - if path in headmf: - fctx = head[path] - flags = fctx.flags() - mctx = memfilectx(repo, fctx.path(), fctx.data(), - islink='l' in flags, - isexec='x' in flags, - copied=copied.get(path)) - return mctx - return None - - message = cmdutil.logmessage(repo.ui, commitopts) - if not message: - message = old.description() - - user = commitopts.get('user') or old.user() - date = commitopts.get('date') or None # old.date() - extra = dict(commitopts.get('extra', old.extra())) - extra['branch'] = head.branch() - - new = context.memctx(repo, - parents=newbases, - text=message, - files=files, - filectxfn=filectxfn, - user=user, - date=date, - extra=extra) - - if commitopts.get('edit'): - new._text = cmdutil.commitforceeditor(repo, new, []) - revcount = len(repo) - newid = repo.commitctx(new) - new = repo[newid] - created = len(repo) != revcount - updatebookmarks(newid) - - tr.close() - return newid, created - finally: - lockmod.release(tr, lock, wlock) - -class MergeFailure(error.Abort): - pass - -def relocate(repo, orig, dest, pctx=None, keepbranch=False): - """rewrite on dest""" - if orig.rev() == dest.rev(): - raise error.Abort(_('tried to relocate a node on top of itself'), - hint=_("This shouldn't happen. If you still " - "need to move changesets, please do so " - "manually with nothing to rebase - working " - "directory parent is also destination")) - - if pctx is None: - if len(orig.parents()) == 2: - raise error.Abort(_("tried to relocate a merge commit without " - "specifying which parent should be moved"), - hint=_("Specify the parent by passing in pctx")) - pctx = orig.p1() - - destbookmarks = repo.nodebookmarks(dest.node()) - nodesrc = orig.node() - destphase = repo[nodesrc].phase() - commitmsg = orig.description() - - cache = {} - sha1s = re.findall(sha1re, commitmsg) - unfi = repo.unfiltered() - for sha1 in sha1s: - ctx = None - try: - ctx = unfi[sha1] - except error.RepoLookupError: - continue - - if not ctx.obsolete(): - continue - - successors = obsolete.successorssets(repo, ctx.node(), cache) - - # We can't make any assumptions about how to update the hash if the - # cset in question was split or diverged. - if len(successors) == 1 and len(successors[0]) == 1: - newsha1 = node.hex(successors[0][0]) - commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)]) - else: - repo.ui.note(_('The stale commit message reference to %s could ' - 'not be updated\n') % sha1) - - tr = repo.currenttransaction() - assert tr is not None - try: - r = _evolvemerge(repo, orig, dest, pctx, keepbranch) - if r[-1]: #some conflict - raise error.Abort( - 'unresolved merge conflicts (see hg help resolve)') - nodenew = _relocatecommit(repo, orig, commitmsg) - except error.Abort as exc: - repo.dirstate.beginparentchange() - repo.setparents(repo['.'].node(), nullid) - writedirstate(repo.dirstate, tr) - # fix up dirstate for copies and renames - copies.duplicatecopies(repo, dest.rev(), orig.p1().rev()) - repo.dirstate.endparentchange() - 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) - _finalizerelocate(repo, orig, dest, nodenew, tr) - return nodenew - -def _bookmarksupdater(repo, oldid, tr): - """Return a callable update(newid) updating the current bookmark - and bookmarks bound to oldid to newid. - """ - def updatebookmarks(newid): - dirty = False - oldbookmarks = repo.nodebookmarks(oldid) - if oldbookmarks: - for b in oldbookmarks: - repo._bookmarks[b] = newid - dirty = True - if dirty: - repo._bookmarks.recordchange(tr) - return updatebookmarks - -### bookmarks api compatibility layer ### -def bmdeactivate(repo): - try: - return bookmarksmod.deactivate(repo) - except AttributeError: - return bookmarksmod.unsetcurrent(repo) -def bmactivate(repo, book): - try: - return bookmarksmod.activate(repo, book) - except AttributeError: - return bookmarksmod.setcurrent(repo, book) - -def bmactive(repo): - try: - return repo._activebookmark - except AttributeError: - return repo._bookmarkcurrent - -### dirstate compatibility layer < hg 3.6 - -def writedirstate(dirstate, tr): - if dirstate.write.func_code.co_argcount != 1: # mercurial 3.6 and above - return dirstate.write(tr) - return dirstate.write() - - - -### new command -############################# -metadataopts = [ - ('d', 'date', '', - _('record the specified date in metadata'), _('DATE')), - ('u', 'user', '', - _('record the specified user in metadata'), _('USER')), -] - -@eh.uisetup -def _installimportobsolete(ui): - entry = cmdutil.findcmd('import', commands.table)[1] - entry[1].append(('', 'obsolete', False, - _('mark the old node as obsoleted by ' - 'the created commit'))) - -@eh.wrapfunction(mercurial.cmdutil, 'tryimportone') -def tryimportone(orig, ui, repo, hunk, parents, opts, *args, **kwargs): - extracted = patch.extract(ui, hunk) - if util.safehasattr(extracted, 'get'): - # mercurial 3.6 return a dictionary there - expected = extracted.get('nodeid') - else: - expected = extracted[5] - if expected is not None: - expected = node.bin(expected) - oldextract = patch.extract - try: - patch.extract = lambda ui, hunk: extracted - ret = orig(ui, repo, hunk, parents, opts, *args, **kwargs) - finally: - patch.extract = oldextract - created = ret[1] - if (opts['obsolete'] and None not in (created, expected) - and created != expected): - tr = repo.transaction('import-obs') - try: - metadata = {'user': ui.username()} - repo.obsstore.create(tr, expected, (created,), - metadata=metadata) - tr.close() - finally: - tr.release() - return ret - - -def _deprecatealias(oldalias, newalias): - '''Deprecates an alias for a command in favour of another - - Creates a new entry in the command table for the old alias. It creates a - wrapper that has its synopsis set to show that is has been deprecated. - The documentation will be replace with a pointer to the new alias. - If a user invokes the command a deprecation warning will be printed and - the command of the *new* alias will be invoked. - - This function is loosely based on the extensions.wrapcommand function. - ''' - try: - aliases, entry = cmdutil.findcmd(newalias, cmdtable) - except error.UnknownCommand: - # Commands may be disabled - return - for alias, e in cmdtable.items(): - if e is entry: - break - - synopsis = '(DEPRECATED)' - if len(entry) > 2: - fn, opts, _syn = entry - else: - fn, opts, = entry - deprecationwarning = _('%s have been deprecated in favor of %s\n') % ( - oldalias, newalias) - def newfn(*args, **kwargs): - ui = args[0] - ui.warn(deprecationwarning) - util.checksignature(fn)(*args, **kwargs) - newfn.__doc__ = deprecationwarning - cmdwrapper = command(oldalias, opts, synopsis) - cmdwrapper(newfn) - -@eh.extsetup -def deprecatealiases(ui): - _deprecatealias('gup', 'next') - _deprecatealias('gdown', 'previous') - -@command('debugrecordpruneparents', [], '') -def cmddebugrecordpruneparents(ui, repo): - """add parent data to prune markers when possible - - This command searches the repo for prune markers without parent information. - If the pruned node is locally known, it creates a new marker with parent - data. - """ - pgop = 'reading markers' - - # lock from the beginning to prevent race - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - tr = repo.transaction('recordpruneparents') - unfi = repo.unfiltered() - nm = unfi.changelog.nodemap - store = repo.obsstore - pgtotal = len(store._all) - for idx, mark in enumerate(list(store._all)): - if not mark[1]: - rev = nm.get(mark[0]) - if rev is not None: - ctx = unfi[rev] - parents = tuple(p.node() for p in ctx.parents()) - before = len(store._all) - store.create(tr, mark[0], mark[1], mark[2], mark[3], - parents=parents) - if len(store._all) - before: - ui.write(_('created new markers for %i\n') % rev) - ui.progress(pgop, idx, total=pgtotal) - tr.close() - ui.progress(pgop, None) - finally: - lockmod.release(tr, lock, wlock) - -@command('debugobsstorestat', [], '') -def cmddebugobsstorestat(ui, repo): - """print statistics about obsolescence markers in the repo""" - def _updateclustermap(nodes, mark, clustersmap): - c = (set(nodes), set([mark])) - toproceed = set(nodes) - while toproceed: - n = toproceed.pop() - other = clustersmap.get(n) - if (other is not None - and other is not c): - other[0].update(c[0]) - other[1].update(c[1]) - for on in c[0]: - if on in toproceed: - continue - clustersmap[on] = other - c = other - clustersmap[n] = c - - store = repo.obsstore - unfi = repo.unfiltered() - nm = unfi.changelog.nodemap - ui.write(_('markers total: %9i\n') % len(store._all)) - sucscount = [0, 0 , 0, 0] - known = 0 - parentsdata = 0 - metakeys = {} - # node -> cluster mapping - # a cluster is a (set(nodes), set(markers)) tuple - clustersmap = {} - # same data using parent information - pclustersmap = {} - for mark in store: - if mark[0] in nm: - known += 1 - nbsucs = len(mark[1]) - sucscount[min(nbsucs, 3)] += 1 - meta = mark[3] - for key, value in meta: - metakeys.setdefault(key, 0) - metakeys[key] += 1 - meta = dict(meta) - parents = [meta.get('p1'), meta.get('p2')] - parents = [node.bin(p) for p in parents if p is not None] - if parents: - parentsdata += 1 - # cluster handling - nodes = set(mark[1]) - nodes.add(mark[0]) - _updateclustermap(nodes, mark, clustersmap) - # same with parent data - nodes.update(parents) - _updateclustermap(nodes, mark, pclustersmap) - - # freezing the result - for c in clustersmap.values(): - fc = (frozenset(c[0]), frozenset(c[1])) - for n in fc[0]: - clustersmap[n] = fc - # same with parent data - for c in pclustersmap.values(): - 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)) - # 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])) - # meta data info - ui.write((' available keys:\n')) - for key in sorted(metakeys): - 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(' 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]))) - median = len(allclusters[nbcluster//2][1]) - ui.write((' median length: %9i\n' % median)) - mean = sum(len(x[1]) for x in allclusters) // nbcluster - 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(' 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]))) - median = len(allpclusters[nbcluster//2][1]) - ui.write((' median length: %9i\n' % median)) - mean = sum(len(x[1]) for x in allpclusters) // nbcluster - ui.write((' mean length: %9i\n' % mean)) - -def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category): - """Resolve the troubles affecting one revision""" - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - tr = repo.transaction("evolve") - if 'unstable' == category: - result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb) - elif 'bumped' == category: - result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb) - elif 'divergent' == category: - result = _solvedivergent(ui, repo, ctx, dryrun, confirm, - progresscb) - else: - assert False, "unknown trouble category: %s" % (category) - tr.close() - return result - finally: - lockmod.release(tr, lock, wlock) - -def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat): - """Used by the evolve function to display an error message when - no troubles can be resolved""" - troublecategories = ['bumped', 'divergent', 'unstable'] - unselectedcategories = [c for c in troublecategories if c != targetcat] - msg = None - hint = None - - troubled = { - "unstable": repo.revs("unstable()"), - "divergent": repo.revs("divergent()"), - "bumped": repo.revs("bumped()"), - "all": repo.revs("troubled()"), - } - - - hintmap = { - 'bumped': _("do you want to use --bumped"), - 'bumped+divergent': _("do you want to use --bumped or --divergent"), - 'bumped+unstable': _("do you want to use --bumped or --unstable"), - 'divergent': _("do you want to use --divergent"), - 'divergent+unstable': _("do you want to use --divergent" - " or --unstable"), - 'unstable': _("do you want to use --unstable"), - 'any+bumped': _("do you want to use --any (or --rev) and --bumped"), - 'any+bumped+divergent': _("do you want to use --any (or --rev) and" - " --bumped or --divergent"), - 'any+bumped+unstable': _("do you want to use --any (or --rev) and" - "--bumped or --unstable"), - 'any+divergent': _("do you want to use --any (or --rev) and" - " --divergent"), - 'any+divergent+unstable': _("do you want to use --any (or --rev)" - " and --divergent or --unstable"), - 'any+unstable': _("do you want to use --any (or --rev)" - "and --unstable"), - } - - if revopt: - revs = scmutil.revrange(repo, revopt) - if not revs: - msg = _("set of specified revisions is empty") - else: - msg = _("no %s changesets in specified revisions") % targetcat - othertroubles = [] - for cat in unselectedcategories: - if revs & troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['+'.join(othertroubles)] - - elif anyopt: - msg = _("no %s changesets to evolve") % targetcat - othertroubles = [] - for cat in unselectedcategories: - if troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['+'.join(othertroubles)] - - else: - # evolve without any option = relative to the current wdir - if targetcat == 'unstable': - msg = _("nothing to evolve on current working copy parent") - else: - msg = _("current working copy parent is not %s") % targetcat - - p1 = repo['.'].rev() - othertroubles = [] - for cat in unselectedcategories: - if p1 in troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['+'.join(othertroubles)] - else: - l = len(troubled[targetcat]) - if l: - hint = _("%d other %s in the repository, do you want --any " - "or --rev") % (l, targetcat) - else: - othertroubles = [] - for cat in unselectedcategories: - if troubled[cat]: - othertroubles.append(cat) - if othertroubles: - hint = hintmap['any+'+('+'.join(othertroubles))] - else: - msg = _("no troubled changesets") - - assert msg is not None - ui.write_err(msg+"\n") - if hint: - ui.write_err("("+hint+")\n") - return 2 - else: - return 1 - -def _cleanup(ui, repo, startnode, showprogress): - if showprogress: - ui.progress(_('evolve'), None) - if repo['.'] != startnode: - ui.status(_('working directory is now at %s\n') % repo['.']) - -class MultipleSuccessorsError(RuntimeError): - """Exception raised by _singlesuccessor when multiple successor sets exists - - The object contains the list of successorssets in its 'successorssets' - attribute to call to easily recover. - """ - - def __init__(self, successorssets): - self.successorssets = successorssets - -def _singlesuccessor(repo, p): - """returns p (as rev) if not obsolete or its unique latest successors - - fail if there are no such successor""" - - if not p.obsolete(): - return p.rev() - obs = repo[p] - ui = repo.ui - newer = obsolete.successorssets(repo, obs.node()) - # search of a parent which is not killed - while not newer: - ui.debug("stabilize target %s is plain dead," - " trying to stabilize on its parent\n" % - obs) - obs = obs.parents()[0] - newer = obsolete.successorssets(repo, obs.node()) - if len(newer) > 1 or len(newer[0]) > 1: - raise MultipleSuccessorsError(newer) - - return repo[newer[0][0]].rev() - -def builddependencies(repo, revs): - """returns dependency graphs giving an order to solve instability of revs - (see _orderrevs for more information on usage)""" - - # For each troubled revision we keep track of what instability if any should - # be resolved in order to resolve it. Example: - # dependencies = {3: [6], 6:[]} - # Means that: 6 has no dependency, 3 depends on 6 to be solved - dependencies = {} - # rdependencies is the inverted dict of dependencies - rdependencies = collections.defaultdict(set) - - for r in revs: - dependencies[r] = set() - for p in repo[r].parents(): - try: - succ = _singlesuccessor(repo, p) - except MultipleSuccessorsError as exc: - dependencies[r] = exc.successorssets - continue - if succ in revs: - dependencies[r].add(succ) - rdependencies[succ].add(r) - return dependencies, rdependencies - -def _dedupedivergents(repo, revs): - """Dedupe the divergents revs in revs to get one from each group with the - lowest revision numbers - """ - repo = repo.unfiltered() - res = set() - # To not reevaluate divergents of the same group once one is encountered - discarded = set() - for rev in revs: - if rev in discarded: - continue - divergent = repo[rev] - base, others = divergentdata(divergent) - othersrevs = [o.rev() for o in others] - res.add(min([divergent.rev()] + othersrevs)) - discarded.update(othersrevs) - return res - -def _selectrevs(repo, allopt, revopt, anyopt, targetcat): - """select troubles in repo matching according to given options""" - revs = set() - if allopt or revopt: - revs = repo.revs(targetcat+'()') - if revopt: - revs = scmutil.revrange(repo, revopt) & revs - elif not anyopt: - topic = getattr(repo, 'currenttopic', '') - if topic: - revs = repo.revs('topic(%s)', topic) & revs - elif targetcat == 'unstable': - revs = _aspiringdescendant(repo, - repo.revs('(.::) - obsolete()::')) - revs = set(revs) - if targetcat == 'divergent': - # Pick one divergent per group of divergents - revs = _dedupedivergents(repo, revs) - elif anyopt: - revs = repo.revs('first(%s())' % (targetcat)) - elif targetcat == 'unstable': - revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::'))) - if 1 < len(revs): - msg = "multiple evolve candidates" - hint = (_("select one of %s with --rev") - % ', '.join([str(repo[r]) for r in sorted(revs)])) - raise error.Abort(msg, hint=hint) - elif targetcat in repo['.'].troubles(): - revs = set([repo['.'].rev()]) - return revs - - -def _orderrevs(repo, revs): - """Compute an ordering to solve instability for the given revs - - revs is a list of unstable revisions. - - Returns the same revisions ordered to solve their instability from the - bottom to the top of the stack that the stabilization process will produce - eventually. - - This ensures the minimal number of stabilizations, as we can stabilize each - revision on its final stabilized destination. - """ - # Step 1: Build the dependency graph - dependencies, rdependencies = builddependencies(repo, revs) - # Step 2: Build the ordering - # Remove the revisions with no dependency(A) and add them to the ordering. - # Removing these revisions leads to new revisions with no dependency (the - # one depending on A) that we can remove from the dependency graph and add - # to the ordering. We progress in a similar fashion until the ordering is - # built - solvablerevs = collections.deque([r for r in sorted(dependencies.keys()) - if not dependencies[r]]) - ordering = [] - while solvablerevs: - rev = solvablerevs.popleft() - for dependent in rdependencies[rev]: - dependencies[dependent].remove(rev) - if not dependencies[dependent]: - solvablerevs.append(dependent) - del dependencies[rev] - ordering.append(rev) - - ordering.extend(sorted(dependencies)) - return ordering - -def divergentsets(repo, ctx): - """Compute sets of commits divergent with a given one""" - cache = {} - succsets = {} - base = {} - for n in obsolete.allprecursors(repo.obsstore, [ctx.node()]): - if n == ctx.node(): - # a node can't be a base for divergence with itself - continue - nsuccsets = obsolete.successorssets(repo, n, cache) - for nsuccset in nsuccsets: - if ctx.node() in nsuccset: - # we are only interested in *other* successor sets - continue - if tuple(nsuccset) in base: - # we already know the latest base for this divergency - continue - base[tuple(nsuccset)] = n - divergence = [] - for divset, b in base.iteritems(): - divergence.append({ - 'divergentnodes': divset, - 'commonprecursor': b - }) - - return divergence - -def _preparelistctxs(items, condition): - return [item.hex() for item in items if condition(item)] - -def _formatctx(fm, ctx): - fm.data(node=ctx.hex()) - fm.data(desc=ctx.description()) - fm.data(date=ctx.date()) - fm.data(user=ctx.user()) - -def listtroubles(ui, repo, troublecategories, **opts): - """Print all the troubles for the repo (or given revset)""" - troublecategories = troublecategories or ['divergent', 'unstable', 'bumped'] - showunstable = 'unstable' in troublecategories - showbumped = 'bumped' in troublecategories - showdivergent = 'divergent' in troublecategories - - revs = repo.revs('+'.join("%s()" % t for t in troublecategories)) - if opts.get('rev'): - revs = revs & repo.revs(opts.get('rev')) - - fm = ui.formatter('evolvelist', opts) - for rev in revs: - ctx = repo[rev] - unpars = _preparelistctxs(ctx.parents(), lambda p: p.unstable()) - obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete()) - imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()), - lambda p: not p.mutable()) - dsets = divergentsets(repo, ctx) - - fm.startitem() - # plain formatter section - hashlen, desclen = 12, 60 - desc = ctx.description() - if desc: - desc = desc.splitlines()[0] - desc = (desc[:desclen] + '...') if len(desc) > desclen else desc - fm.plain('%s: ' % ctx.hex()[:hashlen]) - fm.plain('%s\n' % desc) - fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr()) - - for unpar in unpars if showunstable else []: - fm.plain(' unstable: %s (unstable parent)\n' % unpar[:hashlen]) - for obspar in obspars if showunstable else []: - fm.plain(' unstable: %s (obsolete parent)\n' % obspar[:hashlen]) - for imprec in imprecs if showbumped else []: - fm.plain(' bumped: %s (immutable precursor)\n' % imprec[:hashlen]) - - if dsets and showdivergent: - for dset in dsets: - fm.plain(' divergent: ') - first = True - for n in dset['divergentnodes']: - t = "%s (%s)" if first else " %s (%s)" - first = False - fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr())) - comprec = node.hex(dset['commonprecursor'])[:hashlen] - fm.plain(" (precursor %s)\n" % comprec) - fm.plain("\n") - - # templater-friendly section - _formatctx(fm, ctx) - troubles = [] - for unpar in unpars: - troubles.append({'troubletype': 'unstable', 'sourcenode': unpar, - 'sourcetype': 'unstableparent'}) - for obspar in obspars: - troubles.append({'troubletype': 'unstable', 'sourcenode': obspar, - 'sourcetype': 'obsoleteparent'}) - for imprec in imprecs: - troubles.append({'troubletype': 'bumped', 'sourcenode': imprec, - 'sourcetype': 'immutableprecursor'}) - for dset in dsets: - divnodes = [{'node': node.hex(n), - 'phase': repo[n].phasestr(), - } for n in dset['divergentnodes']] - troubles.append({'troubletype': 'divergent', - 'commonprecursor': node.hex(dset['commonprecursor']), - 'divergentnodes': divnodes}) - fm.data(troubles=troubles) - - fm.end() - -@command('^evolve|stabilize|solve', - [('n', 'dry-run', False, - _('do not perform actions, just print what would be done')), - ('', 'confirm', False, - _('ask for confirmation before performing the action')), - ('A', 'any', False, - _('also consider troubled changesets unrelated to current working ' - 'directory')), - ('r', 'rev', [], _('solves troubles of these revisions')), - ('', 'bumped', False, _('solves only bumped changesets')), - ('', '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')), - ('c', 'continue', False, _('continue an interrupted evolution')), - ('l', 'list', False, 'provide details on troubled changesets in the repo'), - ] + mergetoolopts, - _('[OPTIONS]...')) -def evolve(ui, repo, **opts): - """solve troubled changesets in your repository - - Modifying history can lead to various types of troubled changesets: - unstable, bumped, or divergent. The evolve command resolves your troubles - by executing one of the following actions: - - - update working copy to a successor - - rebase an unstable changeset - - extract the desired changes from a bumped changeset - - fuse divergent changesets back together - - If you pass no arguments, evolve works in automatic mode: it will execute a - single action to reduce instability related to your working copy. There are - two cases for this action. First, if the parent of your working copy is - obsolete, evolve updates to the parent's successor. Second, if the working - copy parent is not obsolete but has obsolete predecessors, then evolve - determines if there is an unstable changeset that can be rebased onto the - working copy parent in order to reduce instability. - If so, evolve rebases that changeset. If not, evolve refuses to guess your - intention, and gives a hint about what you might want to do next. - - Any time evolve creates a changeset, it updates the working copy to the new - changeset. (Currently, every successful evolve operation involves an update - as well; this may change in future.) - - Automatic mode only handles common use cases. For example, it avoids taking - action in the case of ambiguity, and it ignores unstable changesets that - are not related to your working copy. - It also refuses to solve bumped or divergent changesets unless you 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. - This is more powerful than successive invocations, since ``--all`` handles - ambiguous cases (e.g. unstable changesets with multiple children) by - evolving all branches. - - When your repository cannot be handled by automatic mode, you might need to - use ``--rev`` to specify a changeset to evolve. For example, if you have - an unstable changeset that is not related to the working copy parent, - you could use ``--rev`` to evolve it. Or, if some changeset has multiple - unstable children, evolve in automatic mode refuses to guess which one to - evolve; you have to use ``--rev`` in that case. - - Alternately, ``--any`` makes evolve search for the next evolvable changeset - regardless of whether it is related to the working copy parent. - - You can supply multiple revisions to evolve multiple troubled changesets - in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev - first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are - ``--rev`` and ``--any``. - - ``hg evolve --any --all`` is useful for cleaning up instability across all - branches, letting evolve figure out the appropriate order and destination. - - When you have troubled changesets that are not unstable, :hg:`evolve` - refuses to consider them unless you specify the category of trouble you - wish to resolve, with ``--bumped`` or ``--divergent``. These options are - currently mutually exclusive with each other and with ``--unstable`` - (the default). You can combine ``--bumped`` or ``--divergent`` with - ``--rev``, ``--all``, or ``--any``. - - You can also use the evolve command to list the troubles affecting your - repository by using the --list flag. You can choose to display only some - categories of troubles with the --unstable, --divergent or --bumped flags. - """ - - # Options - listopt = opts['list'] - contopt = opts['continue'] - anyopt = opts['any'] - allopt = opts['all'] - startnode = repo['.'] - dryrunopt = opts['dry_run'] - confirmopt = opts['confirm'] - revopt = opts['rev'] - troublecategories = ['bumped', 'divergent', 'unstable'] - specifiedcategories = [t for t in troublecategories if opts[t]] - if listopt: - listtroubles(ui, repo, specifiedcategories, **opts) - return - - targetcat = 'unstable' - if 1 < len(specifiedcategories): - msg = _('cannot specify more than one trouble category to solve (yet)') - raise error.Abort(msg) - elif len(specifiedcategories) == 1: - targetcat = specifiedcategories[0] - elif repo['.'].obsolete(): - displayer = cmdutil.show_changeset(ui, repo, - {'template': shorttemplate}) - # no args and parent is obsolete, update to successors - try: - ctx = repo[_singlesuccessor(repo, repo['.'])] - except MultipleSuccessorsError as exc: - repo.ui.write_err('parent is obsolete with multiple successors:\n') - for ln in exc.successorssets: - for n in ln: - displayer.show(repo[n]) - return 2 - - - ui.status(_('update:')) - if not ui.quiet: - displayer.show(ctx) - - if dryrunopt: - return 0 - res = hg.update(repo, ctx.rev()) - if ctx != startnode: - ui.status(_('working directory is now at %s\n') % ctx) - return res - - ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve') - troubled = set(repo.revs('troubled()')) - - # Progress handling - seen = 1 - count = allopt and len(troubled) or 1 - showprogress = allopt - - def progresscb(): - if revopt or allopt: - ui.progress(_('evolve'), seen, unit=_('changesets'), total=count) - - # Continuation handling - if contopt: - if anyopt: - raise error.Abort('cannot specify both "--any" and "--continue"') - if allopt: - 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. - lock = repo.wlock() - try: - 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) - finally: - lock.release() - cmdutil.bailifchanged(repo) - - - if revopt and allopt: - raise error.Abort('cannot specify both "--rev" and "--all"') - if revopt and anyopt: - raise error.Abort('cannot specify both "--rev" and "--any"') - - revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat) - - if not revs: - return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat) - - # For the progress bar to show - count = len(revs) - # Order the revisions - if targetcat == 'unstable': - revs = _orderrevs(repo, revs) - for rev in revs: - progresscb() - _solveone(ui, repo, repo[rev], dryrunopt, confirmopt, - progresscb, targetcat) - seen += 1 - progresscb() - _cleanup(ui, repo, startnode, showprogress) - -def _possibledestination(repo, rev): - """return all changesets that may be a new parent for REV""" - tonode = repo.changelog.node - parents = repo.changelog.parentrevs - torev = repo.changelog.rev - dest = set() - tovisit = list(parents(rev)) - while tovisit: - r = tovisit.pop() - succsets = obsolete.successorssets(repo, tonode(r)) - if not succsets: - tovisit.extend(parents(r)) - else: - # We should probably pick only one destination from split - # (case where '1 < len(ss)'), This could be the currently tipmost - # but logic is less clear when result of the split are now on - # multiple branches. - for ss in succsets: - for n in ss: - dest.add(torev(n)) - return dest - -def _aspiringchildren(repo, revs): - """Return a list of changectx which can be stabilized on top of pctx or - one of its descendants. Empty list if none can be found.""" - target = set(revs) - result = [] - for r in repo.revs('unstable() - %ld', revs): - dest = _possibledestination(repo, r) - if target & dest: - result.append(r) - return result - -def _aspiringdescendant(repo, revs): - """Return a list of changectx which can be stabilized on top of pctx or - one of its descendants recursively. Empty list if none can be found.""" - target = set(revs) - result = set(target) - paths = collections.defaultdict(set) - for r in repo.revs('unstable() - %ld', revs): - for d in _possibledestination(repo, r): - paths[d].add(r) - - result = set(target) - tovisit = list(revs) - while tovisit: - base = tovisit.pop() - for unstable in paths[base]: - if unstable not in result: - tovisit.append(unstable) - result.add(unstable) - return sorted(result - target) - -def _solveunstable(ui, repo, orig, dryrun=False, confirm=False, - progresscb=None): - """Stabilize an unstable changeset""" - pctx = orig.p1() - if len(orig.parents()) == 2: - if not pctx.obsolete(): - pctx = orig.p2() # second parent is obsolete ? - elif orig.p2().obsolete(): - hint = _("Redo the merge (%s) and use `hg prune " - "--succ ` to obsolete the old one") % orig.hex()[:12] - ui.warn(_("warning: no support for evolving merge changesets " - "with two obsolete parents yet\n") + - _("(%s)\n") % hint) - return False - - if not pctx.obsolete(): - ui.warn(_("cannot solve instability of %s, skipping\n") % orig) - return False - obs = pctx - newer = obsolete.successorssets(repo, obs.node()) - # search of a parent which is not killed - while not newer or newer == [()]: - ui.debug("stabilize target %s is plain dead," - " trying to stabilize on its parent\n" % - obs) - obs = obs.parents()[0] - newer = obsolete.successorssets(repo, obs.node()) - if len(newer) > 1: - msg = _("skipping %s: divergent rewriting. can't choose " - "destination\n") % obs - ui.write_err(msg) - return 2 - targets = newer[0] - assert targets - if len(targets) > 1: - # split target, figure out which one to pick, are they all in line? - targetrevs = [repo[r].rev() for r in targets] - roots = repo.revs('roots(%ld)', targetrevs) - heads = repo.revs('heads(%ld)', targetrevs) - if len(roots) > 1 or len(heads) > 1: - msg = "cannot solve split accross two branches\n" - ui.write_err(msg) - return 2 - target = repo[heads.first()] - else: - target = targets[0] - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - target = repo[target] - if not ui.quiet or confirm: - repo.ui.write(_('move:')) - displayer.show(orig) - repo.ui.write(_('atop:')) - displayer.show(target) - if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': - raise error.Abort(_('evolve aborted by user')) - if progresscb: progresscb() - todo = 'hg rebase -r %s -d %s\n' % (orig, target) - if dryrun: - repo.ui.write(todo) - else: - repo.ui.note(todo) - if progresscb: progresscb() - keepbranch = orig.p1().branch() != orig.branch() - try: - relocate(repo, orig, target, pctx, keepbranch) - except MergeFailure: - _evolvestatewrite(repo, {'current': orig.node()}) - repo.ui.write_err(_('evolve failed!\n')) - repo.ui.write_err( - _("fix conflict and run 'hg evolve --continue'" - " or use 'hg update -C .' to abort\n")) - raise - -def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False, - progresscb=None): - """Stabilize a bumped changeset""" - repo = repo.unfiltered() - bumped = repo[bumped.rev()] - # For now we deny bumped merge - if len(bumped.parents()) > 1: - msg = _('skipping %s : we do not handle merge yet\n') % bumped - ui.write_err(msg) - return 2 - prec = repo.set('last(allprecursors(%d) and public())', bumped).next() - # For now we deny target merge - if len(prec.parents()) > 1: - msg = _('skipping: %s: public version is a merge, ' - 'this is not handled yet\n') % prec - ui.write_err(msg) - return 2 - - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - if not ui.quiet or confirm: - repo.ui.write(_('recreate:')) - displayer.show(bumped) - repo.ui.write(_('atop:')) - displayer.show(prec) - if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': - raise error.Abort(_('evolve aborted by user')) - if dryrun: - todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1()) - repo.ui.write(todo) - repo.ui.write(('hg update %s;\n' % prec)) - repo.ui.write(('hg revert --all --rev %s;\n' % bumped)) - repo.ui.write(('hg commit --msg "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.currenttransaction() - assert tr is not None - bmupdate = _bookmarksupdater(repo, bumped.node(), tr) - if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)): - # Need to rebase the changeset at the right place - repo.ui.status( - _('rebasing to destination parent: %s\n') % prec.p1()) - try: - tmpid = relocate(repo, bumped, prec.p1()) - if tmpid is not None: - tmpctx = repo[tmpid] - obsolete.createmarkers(repo, [(bumped, (tmpctx,))]) - except MergeFailure: - repo.opener.write('graftstate', bumped.hex() + '\n') - repo.ui.write_err(_('evolution failed!\n')) - repo.ui.write_err( - _("fix conflict and run 'hg evolve --continue'\n")) - raise - # Create the new commit context - repo.ui.status(_('computing new diff\n')) - files = set() - copied = copies.pathcopies(prec, bumped) - precmanifest = prec.manifest().copy() - # 3.3.2 needs a list. - # future 3.4 don't detect the size change during iteration - # this is fishy - for key, val in list(bumped.manifest().iteritems()): - precvalue = precmanifest.get(key, None) - if precvalue is not None: - del precmanifest[key] - if precvalue != val: - files.add(key) - files.update(precmanifest) # add missing files - # commit it - if files: # something to commit! - def filectxfn(repo, ctx, path): - if path in bumped: - fctx = bumped[path] - flags = fctx.flags() - mctx = memfilectx(repo, fctx.path(), fctx.data(), - islink='l' in flags, - isexec='x' in flags, - copied=copied.get(path)) - return mctx - return None - text = 'bumped update to %s:\n\n' % prec - text += bumped.description() - - new = context.memctx(repo, - parents=[prec.node(), node.nullid], - text=text, - files=files, - filectxfn=filectxfn, - user=bumped.user(), - date=bumped.date(), - extra=bumped.extra()) - - newid = repo.commitctx(new) - if newid is None: - obsolete.createmarkers(repo, [(tmpctx, ())]) - newid = prec.node() - else: - phases.retractboundary(repo, tr, bumped.phase(), [newid]) - obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))], - flag=obsolete.bumpedfix) - bmupdate(newid) - repo.ui.status(_('committed as %s\n') % node.short(newid)) - # reroute the working copy parent to the new changeset - repo.dirstate.beginparentchange() - repo.dirstate.setparents(newid, node.nullid) - repo.dirstate.endparentchange() - -def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False, - progresscb=None): - repo = repo.unfiltered() - divergent = repo[divergent.rev()] - base, others = divergentdata(divergent) - if len(others) > 1: - othersstr = "[%s]" % (','.join([str(i) for i in others])) - msg = _("skipping %d: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 " - "such as:\n" - "| - hg touch -D\n" - "| - hg prune\n" - "| \n" - "| You should contact your local evolution Guru for help.\n" - ) % (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 - ui.write_err(msg) - hint = _("You have to fallback to solving this by hand...\n" - "| This probably means redoing the merge and using \n" - "| `hg prune` to kill older version.\n") - ui.write_err(hint) - return 2 - if other.p1() not in divergent.parents(): - msg = _("skipping %s: have a different parent than %s " - "(not handled yet)\n") % (divergent, other) - hint = _("| %(d)s, %(o)s are not based on the same changeset.\n" - "| With the current state of its implementation, \n" - "| evolve does not work in that case.\n" - "| rebase one of them next to the other and run \n" - "| this command again.\n" - "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n" - "| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n" - ) % {'d': divergent, 'o': other} - ui.write_err(msg) - ui.write_err(hint) - return 2 - - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - if not ui.quiet or confirm: - ui.write(_('merge:')) - displayer.show(divergent) - ui.write(_('with: ')) - displayer.show(other) - ui.write(_('base: ')) - displayer.show(base) - if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y': - raise error.Abort(_('evolve aborted by user')) - if dryrun: - ui.write(('hg update -c %s &&\n' % divergent)) - ui.write(('hg merge %s &&\n' % other)) - ui.write(('hg commit -m "auto merge resolving conflict between ' - '%s and %s"&&\n' % (divergent, other))) - ui.write(('hg up -C %s &&\n' % base)) - ui.write(('hg revert --all --rev tip &&\n')) - ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n' - % divergent)) - return - if divergent not in repo[None].parents(): - repo.ui.status(_('updating to "local" conflict\n')) - hg.update(repo, divergent.rev()) - repo.ui.note(_('merging divergent changeset\n')) - if progresscb: progresscb() - try: - stats = merge.update(repo, - other.node(), - branchmerge=True, - force=False, - ancestor=base.node(), - mergeancestor=True) - except TypeError: - # Mercurial < 43c00ca887d1 (3.7) - stats = merge.update(repo, - other.node(), - branchmerge=True, - force=False, - partial=None, - ancestor=base.node(), - mergeancestor=True) - - hg._showstats(repo, stats) - if stats[3]: - repo.ui.status(_("use 'hg resolve' to retry unresolved file merges " - "or 'hg update -C .' to abort\n")) - if stats[3] > 0: - raise error.Abort('merge conflict between several amendments ' - '(this is not automated yet)', - hint="""/!\ You can try: -/!\ * manual merge + resolve => new cset X -/!\ * hg up to the parent of the amended changeset (which are named W and Z) -/!\ * hg revert --all -r X -/!\ * hg ci -m "same message as the amended changeset" => new cset Y -/!\ * hg prune -n Y W Z -""") - if progresscb: progresscb() - emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit') - tr = repo.currenttransaction() - assert tr is not None - try: - repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve') - repo.dirstate.beginparentchange() - repo.dirstate.setparents(divergent.node(), node.nullid) - repo.dirstate.endparentchange() - oldlen = len(repo) - amend(ui, repo, message='', logfile='') - if oldlen == len(repo): - new = divergent - # no changes - else: - new = repo['.'] - obsolete.createmarkers(repo, [(other, (new,))]) - phases.retractboundary(repo, tr, other.phase(), [new.node()]) - finally: - repo.ui.restoreconfig(emtpycommitallowed) - -def divergentdata(ctx): - """return base, other part of a conflict - - This only return the first one. - - XXX this woobly function won't survive XXX - """ - repo = ctx._repo.unfiltered() - for base in repo.set('reverse(allprecursors(%d))', ctx): - newer = obsolete.successorssets(ctx._repo, base.node()) - # drop filter and solution including the original ctx - newer = [n for n in newer if n and ctx.node() not in n] - if newer: - return base, tuple(ctx._repo[o] for o in newer[0]) - raise error.Abort("base of divergent changeset %s not found" % ctx, - hint='this case is not yet handled') - - - -shorttemplate = '[{rev}] {desc|firstline}\n' - -@command('^previous', - [('B', 'move-bookmark', False, - _('move active bookmark after update')), - ('', 'merge', False, _('bring uncommitted change along')), - ('', 'no-topic', False, _('ignore topic and move topologically')), - ('n', 'dry-run', False, - _('do not perform actions, just print what would be done'))], - '[OPTION]...') -def cmdprevious(ui, repo, **opts): - """update to parent revision - - Displays the summary line of the destination for clarity.""" - wlock = None - dryrunopt = opts['dry_run'] - if not dryrunopt: - wlock = repo.wlock() - try: - wkctx = repo[None] - wparents = wkctx.parents() - if len(wparents) != 1: - raise error.Abort('merge in progress') - if not opts['merge']: - try: - cmdutil.bailifchanged(repo) - except error.Abort as exc: - exc.hint = _('do you want --merge?') - raise - - parents = wparents[0].parents() - topic = getattr(repo, 'currenttopic', '') - if topic and not opts.get("no_topic", False): - parents = [ctx for ctx in parents if ctx.topic() == topic] - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - if not parents: - ui.warn(_('no parent in topic "%s"\n') % topic) - ui.warn(_('(do you want --no-topic)\n')) - elif len(parents) == 1: - p = parents[0] - bm = bmactive(repo) - shouldmove = opts.get('move_bookmark') and bm is not None - if dryrunopt: - ui.write(('hg update %s;\n' % p.rev())) - if shouldmove: - ui.write(('hg bookmark %s -r %s;\n' % (bm, p.rev()))) - else: - ret = hg.update(repo, p.rev()) - if not ret: - tr = lock = None - try: - lock = repo.lock() - tr = repo.transaction('previous') - if shouldmove: - repo._bookmarks[bm] = p.node() - repo._bookmarks.recordchange(tr) - else: - bmdeactivate(repo) - tr.close() - finally: - lockmod.release(tr, lock) - - displayer.show(p) - return 0 - else: - for p in parents: - displayer.show(p) - ui.warn(_('multiple parents, explicitly update to one\n')) - return 1 - finally: - lockmod.release(wlock) - -@command('^next', - [('B', 'move-bookmark', False, - _('move active bookmark after update')), - ('', 'merge', False, _('bring uncommitted change along')), - ('', 'evolve', False, _('evolve the next changeset if necessary')), - ('', 'no-topic', False, _('ignore topic and move topologically')), - ('n', 'dry-run', False, - _('do not perform actions, just print what would be done'))], - '[OPTION]...') -def cmdnext(ui, repo, **opts): - """update to next child revision - - Use the ``--evolve`` flag to evolve unstable children on demand. - - Displays the summary line of the destination for clarity. - """ - wlock = None - dryrunopt = opts['dry_run'] - if not dryrunopt: - wlock = repo.wlock() - try: - wkctx = repo[None] - wparents = wkctx.parents() - if len(wparents) != 1: - raise error.Abort('merge in progress') - if not opts['merge']: - try: - cmdutil.bailifchanged(repo) - except error.Abort as exc: - exc.hint = _('do you want --merge?') - raise - - children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()] - topic = getattr(repo, 'currenttopic', '') - filtered = [] - if topic and not opts.get("no_topic", False): - filtered = [ctx for ctx in children if ctx.topic() != topic] - # XXX N-square membership on children - children = [ctx for ctx in children if ctx not in filtered] - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - if len(children) == 1: - c = children[0] - bm = bmactive(repo) - shouldmove = opts.get('move_bookmark') and bm is not None - if dryrunopt: - ui.write(('hg update %s;\n' % c.rev())) - if shouldmove: - ui.write(('hg bookmark %s -r %s;\n' % (bm, c.rev()))) - else: - ret = hg.update(repo, c.rev()) - if not ret: - lock = tr = None - try: - lock = repo.lock() - tr = repo.transaction('next') - if shouldmove: - repo._bookmarks[bm] = c.node() - repo._bookmarks.recordchange(tr) - else: - bmdeactivate(repo) - tr.close() - finally: - lockmod.release(tr, lock) - displayer.show(c) - result = 0 - elif children: - ui.warn(_("ambigious next changeset:\n")) - for c in children: - displayer.show(c) - ui.warn(_('explicitly update to one of them\n')) - result = 1 - else: - aspchildren = _aspiringchildren(repo, [repo['.'].rev()]) - if topic: - filtered.extend(repo[c] for c in children - if repo[c].topic() != topic) - # XXX N-square membership on children - aspchildren = [ctx for ctx in aspchildren if ctx not in filtered] - if not opts['evolve'] or not aspchildren: - if filtered: - ui.warn(_('no children on topic "%s"\n') % topic) - ui.warn(_('do you want --no-topic\n')) - else: - ui.warn(_('no children\n')) - if aspchildren: - msg = _('(%i unstable changesets to be evolved here, ' - 'do you want --evolve?)\n') - ui.warn(msg % len(aspchildren)) - result = 1 - elif 1 < len(aspchildren): - ui.warn(_("ambigious next (unstable) changeset:\n")) - for c in aspchildren: - displayer.show(repo[c]) - ui.warn(_("(run 'hg evolve --rev REV' on one of them)\n")) - return 1 - else: - cmdutil.bailifchanged(repo) - result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt, - False, lambda:None, category='unstable') - if not result: - ui.status(_('working directory now at %s\n') % repo['.']) - return result - return 1 - return result - finally: - lockmod.release(wlock) - -def _reachablefrombookmark(repo, revs, bookmarks): - """filter revisions and bookmarks reachable from the given bookmark - yoinked from mq.py - """ - 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. - 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') - for bookmark in bookmarks: - del repomarks[bookmark] - repomarks.recordchange(tr) - tr.close() - for bookmark in sorted(bookmarks): - repo.ui.write(_("bookmark '%s' deleted\n") % bookmark) - finally: - lockmod.release(tr, lock, wlock) - - - -def _getmetadata(**opts): - metadata = {} - date = opts.get('date') - user = opts.get('user') - if date: - metadata['date'] = '%i %i' % util.parsedate(date) - if user: - metadata['user'] = user - return metadata - - -@command('^prune|obsolete', - [('n', 'new', [], _("successor changeset (DEPRECATED)")), - ('s', 'succ', [], _("successor changeset")), - ('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" - " bookmark"))] + metadataopts, - _('[OPTION] [-r] REV...')) - # -U --noupdate option to prevent wc update and or bookmarks update ? -def cmdprune(ui, repo, *revs, **opts): - """hide changesets by marking them obsolete - - Pruned changesets are obsolete with no successors. If they also have no - descendants, they are hidden (invisible to all commands). - - Non-obsolete descendants of pruned changesets become "unstable". Use :hg:`evolve` - to handle this situation. - - When you prune the parent of your working copy, Mercurial updates the working - copy to a non-obsolete parent. - - You can use ``--succ`` to tell Mercurial that a newer version (successor) of the - pruned changeset exists. Mercurial records successor revisions in obsolescence - markers. - - You can use the ``--biject`` option to specify a 1-1 mapping (bijection) between - revisions to pruned (precursor) and successor changesets. This option may be - removed in a future release (with the functionality provided automatically). - - If you specify multiple revisions in ``--succ``, you are recording a "split" and - must acknowledge it by passing ``--split``. Similarly, when you prune multiple - changesets with a single successor, you must pass the ``--fold`` option. - """ - revs = scmutil.revrange(repo, list(revs) + opts.get('rev')) - succs = opts['new'] + opts['succ'] - bookmarks = set(opts.get('bookmark')) - metadata = _getmetadata(**opts) - biject = opts.get('biject') - fold = opts.get('fold') - split = opts.get('split') - - options = [o for o in ('biject', 'fold', 'split') if opts.get(o)] - if 1 < len(options): - 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, repomarks, bookmarks) - - if not revs: - raise error.Abort(_('nothing to prune')) - - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - tr = repo.transaction('prune') - # defines pruned changesets - precs = [] - revs.sort() - for p in revs: - cp = repo[p] - if not cp.mutable(): - # note: createmarkers() would have raised something anyway - raise error.Abort('cannot prune immutable changeset: %s' % cp, - hint="see 'hg help phases' for details") - precs.append(cp) - if not precs: - raise error.Abort('nothing to prune') - - if _disallowednewunstable(repo, revs): - raise error.Abort(_("cannot prune in the middle of a stack"), - hint = _("new unstable changesets are not allowed")) - - # defines successors changesets - sucs = scmutil.revrange(repo, succs) - sucs.sort() - 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" - hint = _("use --biject to mark a series as a replacement" - " for another") - raise error.Abort(msg, hint=hint) - elif biject and len(sucs) != len(precs): - msg = "Can't use %d successors for %d precursors" \ - % (len(sucs), len(precs)) - 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 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 error.Abort(msg) - elif biject: - relations = [(p, (s,)) for p, s in zip(precs, sucs)] - else: - relations = [(p, sucs) for p in precs] - - wdp = repo['.'] - - if len(sucs) == 1 and len(precs) == 1 and wdp in precs: - # '.' killed, so update to the successor - newnode = sucs[0] - else: - # update to an unkilled parent - newnode = wdp - - while newnode in precs or newnode.obsolete(): - newnode = newnode.parents()[0] - - - if newnode.node() != wdp.node(): - if opts.get('keep', False): - # This is largely the same as the implementation in - # strip.stripcmd(). We might want to refactor this somewhere - # common at some point. - - # only reset the dirstate for files that would actually change - # between the working context and uctx - descendantrevs = repo.revs("%d::." % newnode.rev()) - changedfiles = [] - for rev in descendantrevs: - # 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) - 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 bookmarks - if movebookmark: - bmdeactivate(repo) - repo._bookmarks[bookactive] = newnode.node() - 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 bookmarks: - _deletebookmark(repo, repomarks, bookmarks) - - # create markers - obsolete.createmarkers(repo, relations, metadata=metadata) - - # informs that changeset have been pruned - ui.status(_('%i changesets pruned\n') % len(precs)) - - for ctx in repo.unfiltered().set('bookmark() and %ld', precs): - # used to be: - # - # ldest = list(repo.set('max((::%d) - obsolete())', ctx)) - # if ldest: - # c = ldest[0] - # - # but then revset took a lazy arrow in the knee and became much - # slower. The new forms makes as much sense and a much faster. - for dest in ctx.ancestors(): - if not dest.obsolete(): - updatebookmarks = _bookmarksupdater(repo, ctx.node(), tr) - updatebookmarks(dest.node()) - break - - tr.close() - finally: - lockmod.release(tr, lock, wlock) - -@command('amend|refresh', - [('A', 'addremove', None, - _('mark new/missing files as added/removed before committing')), - ('e', 'edit', False, _('invoke editor on commit messages')), - ('', 'close-branch', None, - _('mark a branch as closed, hiding it from the branch list')), - ('s', 'secret', None, _('use the secret phase for committing')), - ] + walkopts + commitopts + commitopts2 + commitopts3 + interactiveopt, - _('[OPTION]... [FILE]...')) -def amend(ui, repo, *pats, **opts): - """combine a changeset with updates and replace it with a new one - - Commits a new changeset incorporating both the changes to the given files - and all the changes from the current parent changeset into the repository. - - See :hg:`commit` for details about committing changes. - - If you don't specify -m, the parent's message will be reused. - - Behind the scenes, Mercurial first commits the update as a regular child - of the current parent. Then it creates a new commit on the parent's parents - with the updated contents. Then it changes the working copy parent to this - new combined changeset. Finally, the old changeset and its update are hidden - from :hg:`log` (unless you use --hidden with log). - - Returns 0 on success, 1 if nothing changed. - """ - opts = opts.copy() - edit = opts.pop('edit', False) - log = opts.get('logfile') - opts['amend'] = True - if not (edit or opts['message'] or log): - opts['message'] = repo['.'].description() - _resolveoptions(ui, opts) - _alias, commitcmd = cmdutil.findcmd('commit', commands.table) - return commitcmd[0](ui, repo, *pats, **opts) - - -def _touchedbetween(repo, source, dest, match=None): - touched = set() - for files in repo.status(source, dest, match=match)[:3]: - touched.update(files) - return touched - -def _commitfiltered(repo, ctx, match, target=None): - """Recommit ctx with changed files not in match. Return the new - node identifier, or None if nothing changed. - """ - base = ctx.p1() - if target is None: - target = base - # ctx - initialfiles = _touchedbetween(repo, base, ctx) - if base == target: - affected = set(f for f in initialfiles if match(f)) - newcontent = set() - else: - affected = _touchedbetween(repo, target, ctx, match=match) - newcontent = _touchedbetween(repo, target, base, match=match) - # The commit touchs all existing files - # + all file that needs a new content - # - the file affected bny uncommit with the same content than base. - files = (initialfiles - affected) | newcontent - if not newcontent and files == initialfiles: - return None - - # Filter copies - copied = copies.pathcopies(target, ctx) - copied = dict((dst, src) for dst, src in copied.iteritems() - if dst in files) - def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent): - if path in redirect: - return filectxfn(repo, memctx, path, contentctx=target, redirect=()) - if path not in contentctx: - return None - fctx = contentctx[path] - flags = fctx.flags() - mctx = memfilectx(repo, fctx.path(), fctx.data(), - islink='l' in flags, - isexec='x' in flags, - copied=copied.get(path)) - return mctx - - new = context.memctx(repo, - parents=[base.node(), node.nullid], - text=ctx.description(), - files=files, - filectxfn=filectxfn, - user=ctx.user(), - date=ctx.date(), - extra=ctx.extra()) - # commitctx always create a new revision, no need to check - newid = repo.commitctx(new) - return newid - -def _uncommitdirstate(repo, oldctx, match): - """Fix the dirstate after switching the working directory from - oldctx to a copy of oldctx not containing changed files matched by - match. - """ - ctx = repo['.'] - ds = repo.dirstate - copies = dict(ds.copies()) - m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3] - for f in m: - if ds[f] == 'r': - # modified + removed -> removed - continue - ds.normallookup(f) - - for f in a: - if ds[f] == 'r': - # added + removed -> unknown - ds.drop(f) - elif ds[f] != 'a': - ds.add(f) - - for f in r: - if ds[f] == 'a': - # removed + added -> normal - ds.normallookup(f) - elif ds[f] != 'r': - ds.remove(f) - - # Merge old parent and old working dir copies - oldcopies = {} - for f in (m + a): - src = oldctx[f].renamed() - if src: - oldcopies[f] = src[0] - oldcopies.update(copies) - copies = dict((dst, oldcopies.get(src, src)) - for dst, src in oldcopies.iteritems()) - # Adjust the dirstate copies - for dst, src in copies.iteritems(): - if (src not in ctx or dst in ctx or ds[dst] != 'a'): - src = None - ds.copy(src, dst) - -@command('^uncommit', - [('a', 'all', None, _('uncommit all changes when no arguments given')), - ('r', 'rev', '', _('revert commit content to REV instead')), - ] + commands.walkopts, - _('[OPTION]... [NAME]')) -def uncommit(ui, repo, *pats, **opts): - """move changes from parent revision to working directory - - Changes to selected files in the checked out revision appear again as - uncommitted changed in the working directory. A new revision - without the selected changes is created, becomes the checked out - revision, and obsoletes the previous one. - - The --include option specifies patterns to uncommit. - The --exclude option specifies patterns to keep in the commit. - - The --rev argument let you change the commit file to a content of another - revision. It still does not change the content of your file in the working - directory. - - Return 0 if changed files are uncommitted. - """ - - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - wctx = repo[None] - if len(wctx.parents()) <= 0: - raise error.Abort(_("cannot uncommit null changeset")) - if len(wctx.parents()) > 1: - raise error.Abort(_("cannot uncommit while merging")) - old = repo['.'] - if old.phase() == phases.public: - raise error.Abort(_("cannot rewrite immutable changeset")) - if len(old.parents()) > 1: - raise error.Abort(_("cannot uncommit merge changeset")) - oldphase = old.phase() - - - rev = None - if opts.get('rev'): - rev = scmutil.revsingle(repo, opts.get('rev')) - ctx = repo[None] - if ctx.p1() == rev or ctx.p2() == rev: - raise error.Abort(_("cannot uncommit to parent changeset")) - - onahead = old.rev() in repo.changelog.headrevs() - disallowunstable = not obsolete.isenabled(repo, - obsolete.allowunstableopt) - if disallowunstable and not onahead: - raise error.Abort(_("cannot uncommit in the middle of a stack")) - - # Recommit the filtered changeset - tr = repo.transaction('uncommit') - updatebookmarks = _bookmarksupdater(repo, old.node(), tr) - newid = None - includeorexclude = opts.get('include') or opts.get('exclude') - if (pats or includeorexclude or opts.get('all')): - match = scmutil.match(old, pats, opts) - newid = _commitfiltered(repo, old, match, target=rev) - if newid is None: - 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],))]) - phases.retractboundary(repo, tr, oldphase, [newid]) - repo.dirstate.beginparentchange() - repo.dirstate.setparents(newid, node.nullid) - _uncommitdirstate(repo, old, match) - repo.dirstate.endparentchange() - updatebookmarks(newid) - if not repo[newid].files(): - ui.warn(_("new changeset is empty\n")) - ui.status(_("(use 'hg prune .' to remove it)\n")) - tr.close() - finally: - lockmod.release(tr, lock, wlock) - -@eh.wrapcommand('commit') -def commitwrapper(orig, ui, repo, *arg, **kwargs): - tr = None - if kwargs.get('amend', False): - wlock = lock = None - else: - wlock = repo.wlock() - lock = repo.lock() - try: - obsoleted = kwargs.get('obsolete', []) - if obsoleted: - obsoleted = repo.set('%lr', obsoleted) - result = orig(ui, repo, *arg, **kwargs) - if not result: # commit succeeded - new = repo['-1'] - oldbookmarks = [] - markers = [] - for old in obsoleted: - oldbookmarks.extend(repo.nodebookmarks(old.node())) - markers.append((old, (new,))) - if markers: - obsolete.createmarkers(repo, markers) - for book in oldbookmarks: - repo._bookmarks[book] = new.node() - if oldbookmarks: - 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(tr, lock, wlock) - -@command('^split', - [('r', 'rev', [], _("revision to split")), - ] + commitopts + commitopts2, - _('hg split [OPTION]... [-r] REV')) -def cmdsplit(ui, repo, *revs, **opts): - """split a changeset into smaller changesets - - By default, split the current revision by prompting for all its hunks to be - redistributed into new changesets. - - Use --rev to split a given changeset instead. - """ - tr = wlock = lock = None - newcommits = [] - - 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() - cmdutil.bailifchanged(repo) - tr = repo.transaction('split') - ctx = repo[rev] - r = ctx.rev() - disallowunstable = not obsolete.isenabled(repo, - obsolete.allowunstableopt) - if disallowunstable: - # XXX We should check head revs - if repo.revs("(%d::) - %d", rev, rev): - raise error.Abort(_("cannot split commit: %s not a head") % ctx) - - if len(ctx.parents()) > 1: - raise error.Abort(_("cannot split merge commits")) - prev = ctx.p1() - bmupdate = _bookmarksupdater(repo, ctx.node(), tr) - bookactive = bmactive(repo) - if bookactive is not None: - repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo)) - bmdeactivate(repo) - hg.update(repo, prev) - - commands.revert(ui, repo, rev=r, all=True) - def haschanges(): - modified, added, removed, deleted = repo.status()[:4] - return modified or added or removed or deleted - msg = ("HG: This is the original pre-split commit message. " - "Edit it as appropriate.\n\n") - msg += ctx.description() - opts['message'] = msg - opts['edit'] = True - while haschanges(): - pats = () - cmdutil.dorecord(ui, repo, commands.commit, 'commit', False, - cmdutil.recordfilter, *pats, **opts) - # TODO: Does no seem like the best way to do this - # We should make dorecord return the newly created commit - newcommits.append(repo['.']) - if haschanges(): - if ui.prompt('Done splitting? [yN]', default='n') == 'y': - commands.commit(ui, repo, **opts) - newcommits.append(repo['.']) - break - else: - ui.status(_("no more change to split\n")) - - if newcommits: - tip = repo[newcommits[-1]] - bmupdate(tip.node()) - if bookactive is not None: - bmactivate(repo, bookactive) - obsolete.createmarkers(repo, [(repo[r], newcommits)]) - tr.close() - finally: - lockmod.release(tr, lock, wlock) - - -@eh.wrapcommand('strip', extension='strip', opts=[ - ('', 'bundle', None, _("delete the commit entirely and move it to a " - "backup bundle")), - ]) -def stripwrapper(orig, ui, repo, *revs, **kwargs): - if (not ui.configbool('experimental', 'prunestrip') or - kwargs.get('bundle', False)): - return orig(ui, repo, *revs, **kwargs) - - if kwargs.get('force'): - ui.warn(_("warning: --force has no effect during strip with evolve " - "enabled\n")) - if kwargs.get('no_backup', False): - ui.warn(_("warning: --no-backup has no effect during strips with " - "evolve enabled\n")) - - revs = list(revs) + kwargs.pop('rev', []) - revs = set(scmutil.revrange(repo, revs)) - revs = repo.revs("(%ld)::", revs) - kwargs['rev'] = [] - kwargs['new'] = [] - kwargs['succ'] = [] - kwargs['biject'] = False - return cmdprune(ui, repo, *revs, **kwargs) - -@command('^touch', - [('r', 'rev', [], 'revision to update'), - ('D', 'duplicate', False, - '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 - - This is used to "resurrect" changesets - """ - duplicate = opts['duplicate'] - allowdivergence = opts['allowdivergence'] - revs = list(revs) - revs.extend(opts['rev']) - if not revs: - revs = ['.'] - revs = scmutil.revrange(repo, revs) - if not revs: - ui.write_err('no revision to touch\n') - return 1 - if not duplicate and repo.revs('public() and %ld', revs): - raise error.Abort("can't touch public revision") - displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) - wlock = lock = tr = None - try: - wlock = repo.wlock() - lock = repo.lock() - tr = repo.transaction('touch') - revs.sort() # ensure parent are run first - newmapping = {} - for r in revs: - ctx = repo[r] - extra = ctx.extra().copy() - extra['__touch-noise__'] = random.randint(0, 0xffffffff) - # search for touched parent - p1 = ctx.p1().node() - 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]) - if ctx in repo[None].parents(): - repo.dirstate.beginparentchange() - repo.dirstate.setparents(new, node.nullid) - repo.dirstate.endparentchange() - tr.close() - finally: - lockmod.release(tr, lock, wlock) - -@command('^fold|squash', - [('r', 'rev', [], _("revision to fold")), - ('', 'exact', None, _("only fold specified revisions")), - ('', 'from', None, _("fold revisions linearly to working copy parent")) - ] + commitopts + commitopts2, - _('hg fold [OPTION]... [-r] REV')) -def fold(ui, repo, *revs, **opts): - """fold multiple revisions into a single one - - With --from, folds all the revisions linearly between the given revisions - and the parent of the working directory. - - With --exact, folds only the specified revisions while ignoring the - parent of the working directory. In this case, the given revisions must - form a linear unbroken chain. - - .. container:: verbose - - Some examples: - - - Fold the current revision with its parent:: - - hg fold --from .^ - - - Fold all draft revisions with working directory parent:: - - hg fold --from 'draft()' - - See :hg:`help phases` for more about draft revisions and - :hg:`help revsets` for more about the `draft()` keyword - - - Fold revisions between 3 and 6 with the working directory parent:: - - hg fold --from 3::6 - - - Fold revisions 3 and 4: - - hg fold "3 + 4" --exact - - - Only fold revisions linearly between foo and @:: - - hg fold foo::@ --exact - """ - revs = list(revs) - revs.extend(opts['rev']) - if not revs: - raise error.Abort(_('no revisions specified')) - - revs = scmutil.revrange(repo, revs) - - if opts['from'] and opts['exact']: - raise error.Abort(_('cannot use both --from and --exact')) - elif opts['from']: - # Try to extend given revision starting from the working directory - extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs) - discardedrevs = [r for r in revs if r not in extrevs] - if discardedrevs: - raise error.Abort(_("cannot fold non-linear revisions"), - hint=_("given revisions are unrelated to parent " - "of working directory")) - revs = extrevs - elif opts['exact']: - # Nothing to do; "revs" is already set correctly - pass - else: - raise error.Abort(_('must specify either --from or --exact')) - - if not revs: - raise error.Abort(_('specified revisions evaluate to an empty set'), - hint=_('use different revision arguments')) - elif len(revs) == 1: - ui.write_err(_('single revision specified, nothing to fold\n')) - return 1 - - wlock = lock = None - try: - wlock = repo.wlock() - lock = repo.lock() - - root, head = _foldcheck(repo, revs) - - tr = repo.transaction('fold') - try: - commitopts = opts.copy() - allctx = [repo[r] for r in revs] - targetphase = max(c.phase() for c in allctx) - - if commitopts.get('message') or commitopts.get('logfile'): - commitopts['edit'] = False - else: - msgs = ["HG: This is a fold of %d changesets." % len(allctx)] - msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % - (c.rev(), c.description()) for c in allctx] - commitopts['message'] = "\n".join(msgs) - commitopts['edit'] = True - - newid, unusedvariable = rewrite(repo, root, allctx, head, - [root.p1().node(), - root.p2().node()], - commitopts=commitopts) - phases.retractboundary(repo, tr, targetphase, [newid]) - obsolete.createmarkers(repo, [(ctx, (repo[newid],)) - for ctx in allctx]) - tr.close() - finally: - tr.release() - ui.status('%i changesets folded\n' % len(revs)) - if repo['.'].rev() in revs: - hg.update(repo, newid) - finally: - lockmod.release(lock, wlock) - -@command('^metaedit', - [('r', 'rev', [], _("revision to edit")), - ('', 'fold', None, _("also fold specified revisions into one")), - ] + commitopts + commitopts2, - _('hg metaedit [OPTION]... [-r] [REV]')) -def metaedit(ui, repo, *revs, **opts): - """edit commit information - - Edits the commit information for the specified revisions. By default, edits - commit information for the working directory parent. - - With --fold, also folds multiple revisions into one if necessary. In this - case, the given revisions must form a linear unbroken chain. - - .. container:: verbose - - Some examples: - - - Edit the commit message for the working directory parent:: - - hg metaedit - - - Change the username for the working directory parent:: - - hg metaedit --user 'New User ' - - - Combine all draft revisions that are ancestors of foo but not of @ into - one:: - - hg metaedit --fold 'draft() and only(foo,@)' - - See :hg:`help phases` for more about draft revisions, and - :hg:`help revsets` for more about the `draft()` and `only()` keywords. - """ - revs = list(revs) - revs.extend(opts['rev']) - if not revs: - if opts['fold']: - raise error.Abort(_('revisions must be specified with --fold')) - revs = ['.'] - - wlock = lock = None - try: - wlock = repo.wlock() - lock = repo.lock() - - revs = scmutil.revrange(repo, revs) - if not opts['fold'] and len(revs) > 1: - # TODO: handle multiple revisions. This is somewhat tricky because - # if we want to edit a series of commits: - # - # a ---- b ---- c - # - # we need to rewrite a first, then directly rewrite b on top of the - # new a, then rewrite c on top of the new b. So we need to handle - # revisions in topological order. - raise error.Abort(_('editing multiple revisions without --fold is ' - 'not currently supported')) - - if opts['fold']: - root, head = _foldcheck(repo, revs) - else: - if repo.revs("%ld and public()", revs): - raise error.Abort(_('cannot edit commit information for public ' - 'revisions')) - newunstable = _disallowednewunstable(repo, revs) - if newunstable: - raise error.Abort( - _('cannot edit commit information in the middle of a '\ - 'stack'), hint=_('%s will become unstable and new unstable'\ - ' changes are not allowed') % repo[newunstable.first()]) - root = head = repo[revs.first()] - - wctx = repo[None] - p1 = wctx.p1() - tr = repo.transaction('metaedit') - newp1 = None - try: - commitopts = opts.copy() - allctx = [repo[r] for r in revs] - targetphase = max(c.phase() for c in allctx) - - if commitopts.get('message') or commitopts.get('logfile'): - commitopts['edit'] = False - else: - if opts['fold']: - msgs = ["HG: This is a fold of %d changesets." % len(allctx)] - msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % - (c.rev(), c.description()) for c in allctx] - else: - msgs = [head.description()] - commitopts['message'] = "\n".join(msgs) - commitopts['edit'] = True - - # TODO: if the author and message are the same, don't create a new - # hash. Right now we create a new hash because the date can be - # different. - newid, created = rewrite(repo, root, allctx, head, - [root.p1().node(), root.p2().node()], - commitopts=commitopts) - if created: - if p1.rev() in revs: - newp1 = newid - phases.retractboundary(repo, tr, targetphase, [newid]) - obsolete.createmarkers(repo, [(ctx, (repo[newid],)) - for ctx in allctx]) - else: - ui.status(_("nothing changed\n")) - tr.close() - finally: - tr.release() - - if opts['fold']: - ui.status('%i changesets folded\n' % len(revs)) - if newp1 is not None: - hg.update(repo, newp1) - finally: - lockmod.release(lock, wlock) - -def _foldcheck(repo, revs): - roots = repo.revs('roots(%ld)', revs) - if len(roots) > 1: - raise error.Abort(_("cannot fold non-linear revisions " - "(multiple roots given)")) - root = repo[roots.first()] - if root.phase() <= phases.public: - raise error.Abort(_("cannot fold public revisions")) - heads = repo.revs('heads(%ld)', revs) - if len(heads) > 1: - raise error.Abort(_("cannot fold non-linear revisions " - "(multiple heads given)")) - head = repo[heads.first()] - if _disallowednewunstable(repo, revs): - raise error.Abort(_("cannot fold chain not ending with a head "\ - "or with branching"), hint = _("new unstable"\ - " changesets are not allowed")) - return root, head - -def _disallowednewunstable(repo, revs): - allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt) - if allowunstable: - return revset.baseset() - return repo.revs("(%ld::) - %ld", revs, revs) - -@eh.wrapcommand('graft') -def graftwrapper(orig, ui, repo, *revs, **kwargs): - kwargs = dict(kwargs) - revs = list(revs) + kwargs.get('rev', []) - kwargs['rev'] = [] - obsoleted = kwargs.setdefault('obsolete', []) - - wlock = lock = None - try: - wlock = repo.wlock() - lock = repo.lock() - if kwargs.get('old_obsolete'): - if kwargs.get('continue'): - obsoleted.extend(repo.opener.read('graftstate').splitlines()) - else: - obsoleted.extend(revs) - # convert obsolete target into revs to avoid alias joke - obsoleted[:] = [str(i) for i in repo.revs('%lr', obsoleted)] - if obsoleted and len(revs) > 1: - - raise error.Abort(_('cannot graft multiple revisions while ' - 'obsoleting (for now).')) - - return commitwrapper(orig, ui, repo,*revs, **kwargs) - finally: - lockmod.release(lock, wlock) - -@eh.extsetup -def oldevolveextsetup(ui): - for cmd in ['prune', 'uncommit', 'touch', 'fold']: - try: - entry = extensions.wrapcommand(cmdtable, cmd, - warnobserrors) - except error.UnknownCommand: - # Commands may be disabled - continue - - entry = cmdutil.findcmd('commit', commands.table)[1] - entry[1].append(('o', 'obsolete', [], - _("make commit obsolete this revision (DEPRECATED)"))) - entry = cmdutil.findcmd('graft', commands.table)[1] - entry[1].append(('o', 'obsolete', [], - _("make graft obsoletes this revision (DEPRECATED)"))) - entry[1].append(('O', 'old-obsolete', False, - _("make graft obsoletes its source (DEPRECATED)"))) - -##################################################################### -### Obsolescence marker exchange experimenation ### -##################################################################### - -def obsexcmsg(ui, message, important=False): - verbose = ui.configbool('experimental', 'verbose-obsolescence-exchange', - False) - if verbose: - message = 'OBSEXC: ' + message - if important or verbose: - ui.status(message) - -def obsexcprg(ui, *args, **kwargs): - topic = 'obsmarkers exchange' - if ui.configbool('experimental', 'verbose-obsolescence-exchange', False): - topic = 'OBSEXC' - ui.progress(topic, *args, **kwargs) - -@eh.wrapfunction(exchange, '_pushdiscoveryobsmarkers') -def _pushdiscoveryobsmarkers(orig, pushop): - if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt) - and pushop.repo.obsstore - and 'obsolete' in pushop.remote.listkeys('namespaces')): - repo = pushop.repo - obsexcmsg(repo.ui, "computing relevant nodes\n") - revs = list(repo.revs('::%ln', pushop.futureheads)) - unfi = repo.unfiltered() - cl = unfi.changelog - if not pushop.remote.capable('_evoext_obshash_0'): - # do not trust core yet - # return orig(pushop) - nodes = [cl.node(r) for r in revs] - if nodes: - obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n" - % len(nodes)) - pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) - else: - obsexcmsg(repo.ui, "markers already in sync\n") - pushop.outobsmarkers = [] - pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) - return - - common = [] - 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) - - revs = list(unfi.revs('%ld - (::%ln)', revs, common)) - nodes = [cl.node(r) for r in revs] - if nodes: - obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n" - % len(nodes)) - pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) - else: - obsexcmsg(repo.ui, "markers already in sync\n") - pushop.outobsmarkers = [] - -@eh.wrapfunction(wireproto, 'capabilities') -def discocapabilities(orig, repo, proto): - """wrapper to advertise new capability""" - caps = orig(repo, proto) - if obsolete.isenabled(repo, obsolete.exchangeopt): - caps += ' _evoext_obshash_0' - return caps - -@eh.extsetup -def _installobsmarkersdiscovery(ui): - hgweb_mod.perms['evoext_obshash'] = 'pull' - hgweb_mod.perms['evoext_obshash1'] = 'pull' - # wrap command content - oldcap, args = wireproto.commands['capabilities'] - def newcap(repo, proto): - return discocapabilities(oldcap, repo, proto) - wireproto.commands['capabilities'] = (newcap, args) - 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')) - else: - olddisco = exchange.pushdiscoverymapping['obsmarker'] - def newdisco(pushop): - _pushdiscoveryobsmarkers(olddisco, pushop) - exchange.pushdiscoverymapping['obsmarker'] = newdisco - -### Set discovery START - -from mercurial import dagutil -from mercurial import setdiscovery - -def _obshash(repo, nodes, version=0): - if version == 0: - hashs = _obsrelsethashtreefm0(repo) - elif version ==1: - hashs = _obsrelsethashtreefm1(repo) - else: - assert False - nm = repo.changelog.nodemap - revs = [nm.get(n) for n in nodes] - return [r is None and nullid or hashs[r][1] for r in revs] - -def srv_obshash(repo, proto, nodes): - return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes))) - -def srv_obshash1(repo, proto, nodes): - return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), - version=1)) - -@eh.addattr(localrepo.localpeer, 'evoext_obshash') -def local_obshash(peer, nodes): - return _obshash(peer._repo, nodes) - -@eh.addattr(localrepo.localpeer, 'evoext_obshash1') -def local_obshash1(peer, nodes): - return _obshash(peer._repo, nodes, version=1) - -@eh.addattr(wireproto.wirepeer, 'evoext_obshash') -def peer_obshash(self, nodes): - d = self._call("evoext_obshash", nodes=wireproto.encodelist(nodes)) - try: - return wireproto.decodelist(d) - except ValueError: - self._abort(error.ResponseError(_("unexpected response:"), d)) - -@eh.addattr(wireproto.wirepeer, 'evoext_obshash1') -def peer_obshash1(self, nodes): - d = self._call("evoext_obshash1", nodes=wireproto.encodelist(nodes)) - try: - return wireproto.decodelist(d) - except ValueError: - self._abort(error.ResponseError(_("unexpected response:"), d)) - -def findcommonobsmarkers(ui, local, remote, probeset, - initialsamplesize=100, - fullsamplesize=200): - # from discovery - roundtrips = 0 - cl = local.changelog - dag = dagutil.revlogdag(cl) - missing = set() - common = set() - undecided = set(probeset) - totalnb = len(undecided) - ui.progress(_("comparing with other"), 0, total=totalnb) - _takefullsample = setdiscovery._takefullsample - if remote.capable('_evoext_obshash_1'): - getremotehash = remote.evoext_obshash1 - localhash = _obsrelsethashtreefm1(local) - else: - getremotehash = remote.evoext_obshash - localhash = _obsrelsethashtreefm0(local) - - while undecided: - - ui.note(_("sampling from both directions\n")) - if len(undecided) < fullsamplesize: - sample = set(undecided) - else: - sample = _takefullsample(dag, undecided, size=fullsamplesize) - - roundtrips += 1 - 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))) - # indices between sample and externalized version must match - sample = list(sample) - remotehash = getremotehash(dag.externalizeall(sample)) - - yesno = [localhash[ix][1] == remotehash[si] - for si, ix in enumerate(sample)] - - commoninsample = set(n for i, n in enumerate(sample) if yesno[i]) - common.update(dag.ancestorset(commoninsample, common)) - - missinginsample = [n for i, n in enumerate(sample) if not yesno[i]] - missing.update(dag.descendantset(missinginsample, missing)) - - undecided.difference_update(missing) - undecided.difference_update(common) - - - ui.progress(_("comparing with other"), None) - result = dag.headsetofconnecteds(common) - ui.debug("%d total queries\n" % roundtrips) - - if not result: - return set([nullid]) - return dag.externalizeall(result) - - -_pushkeyescape = getattr(obsolete, '_pushkeyescape', None) - -class pushobsmarkerStringIO(StringIO): - """hacky string io for progress""" - - @util.propertycache - def length(self): - return len(self.getvalue()) - - def read(self, size=None): - obsexcprg(self.ui, self.tell(), unit=_("bytes"), total=self.length) - return StringIO.read(self, size) - - def __iter__(self): - d = self.read(4096) - while d: - yield d - d = self.read(4096) - -@eh.wrapfunction(exchange, '_pushobsolete') -def _pushobsolete(orig, pushop): - """utility function to push obsolete markers to a remote""" - stepsdone = getattr(pushop, 'stepsdone', None) - if stepsdone is not None: - if 'obsmarkers' in stepsdone: - return - stepsdone.add('obsmarkers') - if util.safehasattr(pushop, 'cgresult'): - cgresult = pushop.cgresult - else: - cgresult = pushop.ret - if cgresult == 0: - return - pushop.ui.debug('try to push obsolete markers to remote\n') - repo = pushop.repo - remote = pushop.remote - if (obsolete.isenabled(repo, obsolete.exchangeopt) and repo.obsstore and - 'obsolete' in remote.listkeys('namespaces')): - markers = pushop.outobsmarkers - if not markers: - obsexcmsg(repo.ui, "no marker to push\n") - elif remote.capable('_evoext_pushobsmarkers_0'): - obsdata = pushobsmarkerStringIO() - for chunk in obsolete.encodemarkers(markers, True): - obsdata.write(chunk) - obsdata.seek(0) - obsdata.ui = repo.ui - obsexcmsg(repo.ui, "pushing %i obsolescence markers (%i bytes)\n" - % (len(markers), len(obsdata.getvalue())), - True) - remote.evoext_pushobsmarkers_0(obsdata) - obsexcprg(repo.ui, None) - else: - rslts = [] - remotedata = _pushkeyescape(markers).items() - 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), - True) - for key, data in remotedata: - obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"), - total=totalbytes) - rslts.append(remote.pushkey('obsolete', key, '', data)) - sentbytes += len(data) - obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"), - total=totalbytes) - obsexcprg(repo.ui, None) - if [r for r in rslts if not r]: - msg = _('failed to push some obsolete markers!\n') - repo.ui.warn(msg) - obsexcmsg(repo.ui, "DONE\n") - - -@eh.addattr(wireproto.wirepeer, 'evoext_pushobsmarkers_0') -def client_pushobsmarkers(self, obsfile): - """wireprotocol peer method""" - self.requirecap('_evoext_pushobsmarkers_0', - _('push obsolete markers faster')) - ret, output = self._callpush('evoext_pushobsmarkers_0', obsfile) - for l in output.splitlines(True): - self.ui.status(_('remote: '), l) - return ret - -@eh.addattr(httppeer.httppeer, 'evoext_pushobsmarkers_0') -def httpclient_pushobsmarkers(self, obsfile): - """httpprotocol peer method - (Cannot simply use _callpush as http is doing some special handling)""" - self.requirecap('_evoext_pushobsmarkers_0', - _('push obsolete markers faster')) - try: - r = self._call('evoext_pushobsmarkers_0', data=obsfile) - vals = r.split('\n', 1) - if len(vals) < 2: - raise error.ResponseError(_("unexpected response:"), r) - - for l in vals[1].splitlines(True): - if l.strip(): - self.ui.status(_('remote: '), l) - return vals[0] - except socket.error as err: - if err.args[0] in (errno.ECONNRESET, errno.EPIPE): - 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): - caps = orig(repo, caps) - caps.add('_evoext_pushobsmarkers_0') - return caps - -def _pushobsmarkers(repo, data): - tr = lock = None - try: - lock = repo.lock() - tr = repo.transaction('pushkey: obsolete markers') - new = repo.obsstore.mergemarkers(tr, data) - if new is not None: - obsexcmsg(repo.ui, "%i obsolescence markers added\n" % new, True) - tr.close() - finally: - lockmod.release(tr, lock) - repo.hook('evolve_pushobsmarkers') - -@eh.addattr(localrepo.localpeer, 'evoext_pushobsmarkers_0') -def local_pushobsmarkers(peer, obsfile): - data = obsfile.read() - _pushobsmarkers(peer._repo, data) - -def srv_pushobsmarkers(repo, proto): - """wireprotocol command""" - fp = StringIO() - proto.redirect() - proto.getfile(fp) - data = fp.getvalue() - fp.close() - _pushobsmarkers(repo, data) - return wireproto.pushres(0) - -def _buildpullobsmarkersboundaries(pullop): - """small funtion returning the argument for pull markers call - may to contains 'heads' and 'common'. skip the key for None. - - Its a separed functio to play around with strategy for that.""" - repo = pullop.repo - remote = pullop.remote - unfi = repo.unfiltered() - revs = unfi.revs('::(%ln - null)', pullop.common) - common = [nullid] - if remote.capable('_evoext_obshash_0'): - obsexcmsg(repo.ui, "looking for common markers in %i nodes\n" - % len(revs)) - common = findcommonobsmarkers(repo.ui, repo, remote, revs) - return {'heads': pullop.pulledsubset, 'common': common} - -@eh.uisetup -def addgetbundleargs(self): - gboptsmap['evo_obscommon'] = 'nodes' - -@eh.wrapfunction(exchange, '_pullbundle2extraprepare') -def _addobscommontob2pull(orig, pullop, kwargs): - ret = orig(pullop, kwargs) - if ('obsmarkers' in kwargs and - pullop.remote.capable('_evoext_getbundle_obscommon')): - boundaries = _buildpullobsmarkersboundaries(pullop) - common = boundaries['common'] - if common != [nullid]: - kwargs['evo_obscommon'] = common - return ret - -@eh.wrapfunction(exchange, '_getbundleobsmarkerpart') -def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs): - if 'evo_obscommon' not in kwargs: - return orig(bundler, repo, source, **kwargs) - - heads = kwargs.get('heads') - if kwargs.get('obsmarkers', False): - if heads is None: - heads = repo.heads() - obscommon = kwargs.get('evo_obscommon', ()) - assert obscommon - obsset = repo.unfiltered().set('::%ln - ::%ln', heads, obscommon) - subset = [c.node() for c in obsset] - markers = repo.obsstore.relevantmarkers(subset) - exchange.buildobsmarkerspart(bundler, markers) - -@eh.uisetup -def installgetbundlepartgen(ui): - origfunc = exchange.getbundle2partsmapping['obsmarkers'] - def newfunc(*args, **kwargs): - return _getbundleobsmarkerpart(origfunc, *args, **kwargs) - exchange.getbundle2partsmapping['obsmarkers'] = newfunc - -@eh.wrapfunction(exchange, '_pullobsolete') -def _pullobsolete(orig, pullop): - if not obsolete.isenabled(pullop.repo, obsolete.exchangeopt): - return None - if 'obsmarkers' not in getattr(pullop, 'todosteps', ['obsmarkers']): - return None - if 'obsmarkers' in getattr(pullop, 'stepsdone', []): - return None - wirepull = pullop.remote.capable('_evoext_pullobsmarkers_0') - if not wirepull: - return orig(pullop) - if 'obsolete' not in pullop.remote.listkeys('namespaces'): - return None # remote opted out of obsolescence marker exchange - tr = None - ui = pullop.repo.ui - boundaries = _buildpullobsmarkersboundaries(pullop) - if not set(boundaries['heads']) - set(boundaries['common']): - obsexcmsg(ui, "nothing to pull\n") - return None - - obsexcmsg(ui, "pull obsolescence markers\n", True) - new = 0 - - if wirepull: - obsdata = pullop.remote.evoext_pullobsmarkers_0(**boundaries) - obsdata = obsdata.read() - if len(obsdata) > 5: - obsexcmsg(ui, "merging obsolescence markers (%i bytes)\n" - % len(obsdata)) - tr = pullop.gettransaction() - old = len(pullop.repo.obsstore._all) - pullop.repo.obsstore.mergemarkers(tr, obsdata) - new = len(pullop.repo.obsstore._all) - old - obsexcmsg(ui, "%i obsolescence markers added\n" % new, True) - else: - obsexcmsg(ui, "no unknown remote markers\n") - obsexcmsg(ui, "DONE\n") - if new: - pullop.repo.invalidatevolatilesets() - return tr - -def _getobsmarkersstream(repo, heads=None, common=None): - revset = '' - args = [] - repo = repo.unfiltered() - if heads is None: - revset = 'all()' - elif heads: - revset += "(::%ln)" - args.append(heads) - else: - assert False, 'pulling no heads?' - if common: - revset += ' - (::%ln)' - args.append(common) - nodes = [c.node() for c in repo.set(revset, *args)] - markers = repo.obsstore.relevantmarkers(nodes) - obsdata = StringIO() - for chunk in obsolete.encodemarkers(markers, True): - obsdata.write(chunk) - obsdata.seek(0) - return obsdata - -@eh.addattr(wireproto.wirepeer, 'evoext_pullobsmarkers_0') -def client_pullobsmarkers(self, heads=None, common=None): - self.requirecap('_evoext_pullobsmarkers_0', _('look up remote obsmarkers')) - opts = {} - if heads is not None: - opts['heads'] = wireproto.encodelist(heads) - if common is not None: - opts['common'] = wireproto.encodelist(common) - if util.safehasattr(self, '_callcompressable'): - f = self._callcompressable("evoext_pullobsmarkers_0", **opts) - else: - f = self._callstream("evoext_pullobsmarkers_0", **opts) - f = self._decompress(f) - length = int(f.read(20)) - chunk = 4096 - current = 0 - data = StringIO() - ui = self.ui - obsexcprg(ui, current, unit=_("bytes"), total=length) - while current < length: - readsize = min(length - current, chunk) - data.write(f.read(readsize)) - current += readsize - obsexcprg(ui, current, unit=_("bytes"), total=length) - obsexcprg(ui, None) - data.seek(0) - return data - -@eh.addattr(localrepo.localpeer, 'evoext_pullobsmarkers_0') -def local_pullobsmarkers(self, heads=None, common=None): - return _getobsmarkersstream(self._repo, heads=heads, common=common) - -# The wireproto.streamres API changed, handling chunking and compression -# directly. Handle either case. -if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'): - # We need to handle chunking and compression directly - def streamres(d, proto): - return wireproto.streamres(proto.groupchunks(d)) -else: - # Leave chunking and compression to streamres - def streamres(d, proto): - return wireproto.streamres(reader=d, v1compressible=True) - -def srv_pullobsmarkers(repo, proto, others): - opts = wireproto.options('', ['heads', 'common'], others) - for k, v in opts.iteritems(): - if k in ('heads', 'common'): - opts[k] = wireproto.decodelist(v) - obsdata = _getobsmarkersstream(repo, **opts) - finaldata = StringIO() - obsdata = obsdata.getvalue() - finaldata.write('%20i' % len(obsdata)) - finaldata.write(obsdata) - finaldata.seek(0) - return streamres(finaldata, proto) - -def _obsrelsethashtreefm0(repo): - return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker) - -def _obsrelsethashtreefm1(repo): - return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker) - -def _obsrelsethashtree(repo, encodeonemarker): - cache = [] - unfi = repo.unfiltered() - markercache = {} - repo.ui.progress(_("preparing locally"), 0, total=len(unfi)) - for i in unfi: - ctx = unfi[i] - entry = 0 - sha = hashlib.sha1() - # add data from p1 - for p in ctx.parents(): - p = p.rev() - if p < 0: - p = nullid - else: - p = cache[p][1] - if p != nullid: - entry += 1 - sha.update(p) - tmarkers = repo.obsstore.relevantmarkers([ctx.node()]) - if tmarkers: - bmarkers = [] - for m in tmarkers: - if not m in markercache: - markercache[m] = encodeonemarker(m) - bmarkers.append(markercache[m]) - bmarkers.sort() - for m in bmarkers: - entry += 1 - sha.update(m) - if entry: - 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) - return cache - -@command('debugobsrelsethashtree', - [('', 'v0', None, 'hash on marker format "0"'), - ('', '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 - - It computed form the "orsht" of its parent and markers - relevant to the changeset itself.""" - if v0 and v1: - raise error.Abort('cannot only specify one format') - elif v0: - treefunc = _obsrelsethashtreefm0 - else: - treefunc = _obsrelsethashtreefm1 - - for chg, obs in treefunc(repo): - ui.status('%s %s\n' % (node.hex(chg), node.hex(obs))) - -_bestformat = max(obsolete.formats.keys()) - - -@eh.wrapfunction(obsolete, '_checkinvalidmarkers') -def _checkinvalidmarkers(orig, markers): - """search for marker with invalid data and raise error if needed - - Exist as a separated function to allow the evolve extension for a more - subtle handling. - """ - if 'debugobsconvert' in sys.argv: - return - for mark in markers: - if node.nullid in mark[1]: - raise error.Abort(_('bad obsolescence marker detected: ' - 'invalid successors nullid'), - hint=_('You should run `hg debugobsconvert`')) - -@command( - 'debugobsconvert', - [('', 'new-format', _bestformat, _('Destination format for markers.'))], - '') -def debugobsconvert(ui, repo, new_format): - 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 error.Abort(msg) - f = repo.svfs('obsstore', 'wb', atomictemp=True) - known = set() - markers = [] - for m in origmarkers: - # filter out invalid markers - if nullid in m[1]: - m = list(m) - m[1] = tuple(s for s in m[1] if s != nullid) - m = tuple(m) - if m in known: - continue - known.add(m) - markers.append(m) - ui.write(_('Old store is version %d, will rewrite in version %d\n') % ( - repo.obsstore._version, new_format)) - map(f.write, obsolete.encodemarkers(markers, True, new_format)) - f.close() - ui.write(_('Done!\n')) - - -@eh.wrapfunction(wireproto, 'capabilities') -def capabilities(orig, repo, proto): - """wrapper to advertise new capability""" - caps = orig(repo, proto) - if obsolete.isenabled(repo, obsolete.exchangeopt): - caps += ' _evoext_pushobsmarkers_0' - caps += ' _evoext_pullobsmarkers_0' - caps += ' _evoext_obshash_0' - caps += ' _evoext_obshash_1' - caps += ' _evoext_getbundle_obscommon' - return caps - - -@eh.extsetup -def _installwireprotocol(ui): - localrepo.moderncaps.add('_evoext_pullobsmarkers_0') - hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push' - hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull' - wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '') - wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*') - # wrap command content - oldcap, args = wireproto.commands['capabilities'] - def newcap(repo, proto): - return capabilities(oldcap, repo, proto) - wireproto.commands['capabilities'] = (newcap, args) - -# Mercurial >= 3.6 passes ui -def _helploader(ui=None): - return help.gettext(evolutionhelptext) - -@eh.uisetup -def _setuphelp(ui): - for entry in help.helptable: - if entry[0] == "evolution": - break - else: - 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, 'evolve') - # Commit might fail if unresolved files exist - nodenew = repo.commit(text=commitmsg, user=orig.user(), - date=orig.date(), extra=extra) - finally: - repo.ui.restoreconfig(backup) - return nodenew - -def _finalizerelocate(repo, orig, dest, nodenew, tr): - destbookmarks = repo.nodebookmarks(dest.node()) - nodesrc = orig.node() - destphase = repo[nodesrc].phase() - oldbookmarks = repo.nodebookmarks(nodesrc) - 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 -C .' 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 as 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(): - #assert False - try: - merge.update(repo, - dest, - branchmerge=False, - force=True) - except TypeError: - # Mercurial < 43c00ca887d1 (3.7) - merge.update(repo, - dest, - branchmerge=False, - force=True, - partial=False) - if bmactive(repo): - repo.ui.status(_("(leaving bookmark %s)\n") % bmactive(repo)) - bmdeactivate(repo) - if keepbranch: - repo.dirstate.setbranch(orig.branch()) - if util.safehasattr(repo, 'currenttopic'): - # uurrgs - # there no other topic setter yet - if not orig.topic() and repo.vfs.exists('topic'): - repo.vfs.unlink('topic') - else: - with repo.vfs.open('topic', 'w') as f: - f.write(orig.topic()) - - 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 efda653c96a7 -r 94432e742a02 hgext/inhibit.py --- a/hgext/inhibit.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,310 +0,0 @@ -"""reduce the changesets evolution feature scope for early and noob friendly ui - -the full scale changeset evolution have some massive bleeding edge and it is -very easy for people not very intimate with the concept to end up in intricate -situation. in order to get some of the benefit sooner, this extension is -disabling some of the less polished aspect of evolution. it should gradually -get thinner and thinner as changeset evolution will get more polished. this -extension is only recommended for large scale organisations. individual user -should probably stick on using evolution in its current state, understand its -concept and provide feedback - -This extension provides the ability to "inhibit" obsolescence markers. obsolete -revision can be cheaply brought back to life that way. -However as the inhibitor are not fitting in an append only model, this is -incompatible with sharing mutable history. -""" -from mercurial import localrepo -from mercurial import obsolete -from mercurial import extensions -from mercurial import cmdutil -from mercurial import error -from mercurial import scmutil -from mercurial import commands -from mercurial import lock as lockmod -from mercurial import bookmarks -from mercurial import util -from mercurial.i18n import _ - -cmdtable = {} -command = cmdutil.command(cmdtable) - -def _inhibitenabled(repo): - return util.safehasattr(repo, '_obsinhibit') - -def reposetup(ui, repo): - - class obsinhibitedrepo(repo.__class__): - - @localrepo.storecache('obsinhibit') - def _obsinhibit(self): - # XXX we should make sure it is invalidated by transaction failure - obsinhibit = set() - raw = self.svfs.tryread('obsinhibit') - for i in xrange(0, len(raw), 20): - obsinhibit.add(raw[i:i + 20]) - return obsinhibit - - def commit(self, *args, **kwargs): - newnode = super(obsinhibitedrepo, self).commit(*args, **kwargs) - if newnode is not None: - _inhibitmarkers(repo, [newnode]) - return newnode - - repo.__class__ = obsinhibitedrepo - -def _update(orig, ui, repo, *args, **kwargs): - """ - When moving to a commit we want to inhibit any obsolete commit affecting - the changeset we are updating to. In other words we don't want any visible - commit to be obsolete. - """ - wlock = None - try: - # Evolve is running a hook on lock release to display a warning message - # if the workind dir's parent is obsolete. - # We take the lock here to make sure that we inhibit the parent before - # that hook get a chance to run. - wlock = repo.wlock() - res = orig(ui, repo, *args, **kwargs) - newhead = repo['.'].node() - _inhibitmarkers(repo, [newhead]) - return res - finally: - lockmod.release(wlock) - -def _bookmarkchanged(orig, bkmstoreinst, *args, **kwargs): - """ Add inhibition markers to every obsolete bookmarks """ - repo = bkmstoreinst._repo - bkmstorenodes = [repo[v].node() for v in bkmstoreinst.values()] - _inhibitmarkers(repo, bkmstorenodes) - return orig(bkmstoreinst, *args, **kwargs) - -def _bookmark(orig, ui, repo, *bookmarks, **opts): - """ Add a -D option to the bookmark command, map it to prune -B """ - haspruneopt = opts.get('prune', False) - if not haspruneopt: - return orig(ui, repo, *bookmarks, **opts) - elif opts.get('rename'): - raise error.Abort('Cannot use both -m and -D') - elif len(bookmarks) == 0: - hint = _('make sure to put a space between -D and your bookmark name') - raise error.Abort(_('Error, please check your command'), hint=hint) - - # Call prune -B - evolve = extensions.find('evolve') - optsdict = { - 'new': [], - 'succ': [], - 'rev': [], - 'bookmark': bookmarks, - 'keep': None, - 'biject': False, - } - evolve.cmdprune(ui, repo, **optsdict) - -# obsolescence inhibitor -######################## - -def _schedulewrite(tr, obsinhibit): - """Make sure on disk content will be updated on transaction commit""" - def writer(fp): - """Serialize the inhibited list to disk. - """ - raw = ''.join(obsinhibit) - fp.write(raw) - tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer) - tr.hookargs['obs_inbihited'] = '1' - -def _filterpublic(repo, nodes): - """filter out inhibitor on public changeset - - Public changesets are already immune to obsolescence""" - getrev = repo.changelog.nodemap.get - getphase = repo._phasecache.phase - return (n for n in nodes - if getrev(n) is not None and getphase(repo, getrev(n))) - -def _inhibitmarkers(repo, nodes): - """add marker inhibitor for all obsolete revision under - - Content of and all mutable ancestors are considered. Marker for - obsolete revision only are created. - """ - if not _inhibitenabled(repo): - return - - # we add (non public()) as a lower boundary to - # - use the C code in 3.6 (no ancestors in C as this is written) - # - restrict the search space. Otherwise, the ancestors can spend a lot of - # time iterating if you have a check very low in the repo. We do not need - # to iterate over tens of thousand of public revisions with higher - # revision number - # - # In addition, the revset logic could be made significantly smarter here. - newinhibit = repo.revs('(not public())::%ln and obsolete()', nodes) - if newinhibit: - node = repo.changelog.node - lock = tr = None - try: - lock = repo.lock() - tr = repo.transaction('obsinhibit') - repo._obsinhibit.update(node(r) for r in newinhibit) - _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit)) - repo.invalidatevolatilesets() - tr.close() - finally: - lockmod.release(tr, lock) - -def _deinhibitmarkers(repo, nodes): - """lift obsolescence inhibition on a set of nodes - - This will be triggered when inhibited nodes received new obsolescence - markers. Otherwise the new obsolescence markers would also be inhibited. - """ - if not _inhibitenabled(repo): - return - - deinhibited = repo._obsinhibit & set(nodes) - if deinhibited: - tr = repo.transaction('obsinhibit') - try: - repo._obsinhibit -= deinhibited - _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit)) - repo.invalidatevolatilesets() - tr.close() - finally: - tr.release() - -def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None): - """wrap markers create to make sure we de-inhibit target nodes""" - # wrapping transactio to unify the one in each function - lock = tr = None - try: - lock = repo.lock() - tr = repo.transaction('add-obsolescence-marker') - orig(repo, relations, flag, date, metadata) - precs = (r[0].node() for r in relations) - _deinhibitmarkers(repo, precs) - tr.close() - finally: - lockmod.release(tr, lock) - -def _filterobsoleterevswrap(orig, repo, rebasesetrevs, *args, **kwargs): - repo._notinhibited = rebasesetrevs - try: - repo.invalidatevolatilesets() - r = orig(repo, rebasesetrevs, *args, **kwargs) - finally: - del repo._notinhibited - repo.invalidatevolatilesets() - return r - -def transactioncallback(orig, repo, desc, *args, **kwargs): - """ Wrap localrepo.transaction to inhibit new obsolete changes """ - def inhibitposttransaction(transaction): - # At the end of the transaction we catch all the new visible and - # obsolete commit to inhibit them - visibleobsolete = repo.revs('obsolete() - hidden()') - ignoreset = set(getattr(repo, '_rebaseset', [])) - ignoreset |= set(getattr(repo, '_obsoletenotrebased', [])) - visibleobsolete = list(r for r in visibleobsolete if r not in ignoreset) - if visibleobsolete: - _inhibitmarkers(repo, [repo[r].node() for r in visibleobsolete]) - transaction = orig(repo, desc, *args, **kwargs) - if desc != 'strip' and _inhibitenabled(repo): - transaction.addpostclose('inhibitposttransaction', - inhibitposttransaction) - return transaction - - -# We wrap these two functions to address the following scenario: -# - Assuming that we have markers between commits in the rebase set and -# destination and that these markers are inhibited -# - At the end of the rebase the nodes are still visible because rebase operate -# without inhibition and skip these nodes -# We keep track in repo._obsoletenotrebased of the obsolete commits skipped by -# the rebase and lift the inhibition in the end of the rebase. - -def _computeobsoletenotrebased(orig, repo, *args, **kwargs): - r = orig(repo, *args, **kwargs) - repo._obsoletenotrebased = r.keys() - return r - -def _clearrebased(orig, ui, repo, *args, **kwargs): - r = orig(ui, repo, *args, **kwargs) - tonode = repo.changelog.node - if util.safehasattr(repo, '_obsoletenotrebased'): - _deinhibitmarkers(repo, [tonode(k) for k in repo._obsoletenotrebased]) - return r - - -def extsetup(ui): - # lets wrap the computation of the obsolete set - # We apply inhibition there - obsfunc = obsolete.cachefuncs['obsolete'] - def _computeobsoleteset(repo): - """remove any inhibited nodes from the obsolete set - - This will trickle down to other part of mercurial (hidden, log, etc)""" - obs = obsfunc(repo) - if _inhibitenabled(repo): - getrev = repo.changelog.nodemap.get - blacklist = getattr(repo, '_notinhibited', set()) - for n in repo._obsinhibit: - if getrev(n) not in blacklist: - obs.discard(getrev(n)) - return obs - try: - extensions.find('directaccess') - except KeyError: - errormsg = _('cannot use inhibit without the direct access extension\n') - hint = _("(please enable it or inhibit won\'t work)\n") - ui.warn(errormsg) - ui.warn(hint) - return - - # Wrapping this to inhibit obsolete revs resulting from a transaction - extensions.wrapfunction(localrepo.localrepository, - 'transaction', transactioncallback) - - obsolete.cachefuncs['obsolete'] = _computeobsoleteset - # wrap create marker to make it able to lift the inhibition - extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers) - # drop divergence computation since it is incompatible with "light revive" - obsolete.cachefuncs['divergent'] = lambda repo: set() - # drop bumped computation since it is incompatible with "light revive" - obsolete.cachefuncs['bumped'] = lambda repo: set() - # wrap update to make sure that no obsolete commit is visible after an - # update - extensions.wrapcommand(commands.table, 'update', _update) - try: - rebase = extensions.find('rebase') - if rebase: - if util.safehasattr(rebase, '_filterobsoleterevs'): - extensions.wrapfunction(rebase, - '_filterobsoleterevs', - _filterobsoleterevswrap) - extensions.wrapfunction(rebase, 'clearrebased', _clearrebased) - if util.safehasattr(rebase, '_computeobsoletenotrebased'): - extensions.wrapfunction(rebase, - '_computeobsoletenotrebased', - _computeobsoletenotrebased) - - except KeyError: - pass - # There are two ways to save bookmark changes during a transation, we - # wrap both to add inhibition markers. - extensions.wrapfunction(bookmarks.bmstore, 'recordchange', _bookmarkchanged) - if getattr(bookmarks.bmstore, 'write', None) is not None:# mercurial < 3.9 - extensions.wrapfunction(bookmarks.bmstore, 'write', _bookmarkchanged) - # Add bookmark -D option - entry = extensions.wrapcommand(commands.table, 'bookmark', _bookmark) - entry[1].append(('D','prune',None, - _('delete the bookmark and prune the commits underneath'))) - -@command('debugobsinhibit', [], '') -def cmddebugobsinhibit(ui, repo, *revs): - """inhibit obsolescence markers effect on a set of revs""" - nodes = (repo[r].node() for r in scmutil.revrange(repo, revs)) - _inhibitmarkers(repo, nodes) diff -r efda653c96a7 -r 94432e742a02 hgext/obsolete.py --- a/hgext/obsolete.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,163 +0,0 @@ -# Copyright 2011 Pierre-Yves David -# Logilab SA -# -# This software may be used and distributed according to the terms of the -# GNU General Public License version 2 or any later version. -"""Deprecated extension that formerly introduced "Changeset Obsolescence". - -This concept is now partially in Mercurial core (starting with Mercurial 2.3). -The remaining logic has been grouped with the evolve extension. - -Some code remains 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 error - -try: - from mercurial import obsolete -except ImportError: - raise error.Abort('Obsolete extension requires Mercurial 2.3 (or later)') - -import sys -import json - -from mercurial import cmdutil -from mercurial.i18n import _ -from mercurial.node import bin, nullid -from mercurial import util - - -##################################################################### -### Older format management ### -##################################################################### - -# Code related to detection and management of older legacy format never -# handled by core - - -def reposetup(ui, repo): - """Detect that a repo still contains some old obsolete format - """ - if not repo.local(): - return - evolveopts = ui.configlist('experimental', 'evolution') - if not evolveopts: - evolveopts = 'all' - ui.setconfig('experimental', 'evolution', evolveopts) - for arg in sys.argv: - if 'debugc' in arg: - break - else: - data = repo.opener.tryread('obsolete-relations') - if not data: - data = repo.svfs.tryread('obsoletemarkers') - if data: - raise error.Abort('old format of obsolete marker detected!\n' - 'run `hg debugconvertobsolete` once.') - -def _obsdeserialize(flike): - """read a file like object serialized with _obsserialize - - this deserialize into a {subject -> objects} mapping - - this was the very first format ever.""" - rels = {} - for line in flike: - subhex, objhex = line.split() - subnode = bin(subhex) - if subnode == nullid: - subnode = None - rels.setdefault(subnode, set()).add(bin(objhex)) - return rels - -cmdtable = {} -command = cmdutil.command(cmdtable) -@command('debugconvertobsolete', [], '') -def cmddebugconvertobsolete(ui, repo): - """import markers from an .hg/obsolete-relations file""" - cnt = 0 - err = 0 - l = repo.lock() - some = False - try: - unlink = [] - tr = repo.transaction('convert-obsolete') - try: - repo._importoldobsolete = True - store = repo.obsstore - ### very first format - try: - f = repo.opener('obsolete-relations') - try: - some = True - for line in f: - subhex, objhex = line.split() - suc = bin(subhex) - prec = bin(objhex) - sucs = (suc==nullid) and [] or [suc] - meta = { - 'date': '%i %i' % util.makedate(), - 'user': ui.username(), - } - try: - store.create(tr, prec, sucs, 0, metadata=meta) - cnt += 1 - except ValueError: - repo.ui.write_err("invalid old marker line: %s" - % (line)) - err += 1 - finally: - f.close() - unlink.append(repo.join('obsolete-relations')) - except IOError: - pass - ### second (json) format - data = repo.svfs.tryread('obsoletemarkers') - if data: - some = True - for oldmark in json.loads(data): - del oldmark['id'] # dropped for now - del oldmark['reason'] # unused until then - oldobject = str(oldmark.pop('object')) - oldsubjects = [str(s) for s in oldmark.pop('subjects', [])] - LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError) - if len(oldobject) != 40: - try: - oldobject = repo[oldobject].node() - except LOOKUP_ERRORS: - pass - if any(len(s) != 40 for s in oldsubjects): - try: - oldsubjects = [repo[s].node() for s in oldsubjects] - except LOOKUP_ERRORS: - pass - - oldmark['date'] = '%i %i' % tuple(oldmark['date']) - meta = dict((k.encode('utf-8'), v.encode('utf-8')) - for k, v in oldmark.iteritems()) - try: - succs = [bin(n) for n in oldsubjects] - succs = [n for n in succs if n != nullid] - store.create(tr, bin(oldobject), succs, - 0, metadata=meta) - cnt += 1 - except ValueError: - repo.ui.write_err("invalid marker %s -> %s\n" - % (oldobject, oldsubjects)) - err += 1 - unlink.append(repo.sjoin('obsoletemarkers')) - tr.close() - for path in unlink: - util.unlink(path) - finally: - tr.release() - finally: - del repo._importoldobsolete - l.release() - if not some: - 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 efda653c96a7 -r 94432e742a02 hgext/pushexperiment.py --- a/hgext/pushexperiment.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,118 +0,0 @@ -"""Small extension altering some push behavior - -- 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) - -""" - -import errno -from StringIO import StringIO - -from mercurial.i18n import _ -from mercurial import error -from mercurial import extensions -from mercurial import wireproto -from mercurial import obsolete -from mercurial import localrepo - - -def client_pushobsmarkers(self, obsfile): - """wireprotocol peer method""" - self.requirecap('_push_experiment_pushobsmarkers_0', - _('push obsolete markers faster')) - ret, output = self._callpush('push_experiment_pushobsmarkers_0', obsfile) - for l in output.splitlines(True): - self.ui.status(_('remote: '), l) - return ret - - -def srv_pushobsmarkers(repo, proto): - """wireprotocol command""" - fp = StringIO() - proto.redirect() - proto.getfile(fp) - data = fp.getvalue() - fp.close() - lock = repo.lock() - try: - tr = repo.transaction('pushkey: obsolete markers') - try: - repo.obsstore.mergemarkers(tr, data) - tr.close() - finally: - tr.release() - finally: - lock.release() - return wireproto.pushres(0) - - -def syncpush(orig, repo, remote): - """wraper for obsolete.syncpush to use the fast way if possible""" - if not (obsolete.isenabled(repo, obsolete.exchangeopt) and - repo.obsstore): - return - if remote.capable('_push_experiment_pushobsmarkers_0'): - return # already pushed before changeset - remote.push_experiment_pushobsmarkers_0(obsfp) - return - return orig(repo, remote) - - -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')) - return self._call('push_experiment_notifypushend_0') - - -def srv_notifypushend(repo, proto): - """wire protocol command to notify a push is done""" - proto.redirect() - repo.hook('notifypushend') - return wireproto.pushres(0) - - -def augmented_push(orig, repo, remote, *args, **kwargs): - """push wrapped that call the wire protocol command""" - if not remote.canpush(): - 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. - try: - obsfp = repo.svfs('obsstore') - except IOError as e: - if e.errno != errno.ENOENT: - raise - else: - remote.push_experiment_pushobsmarkers_0(obsfp) - ret = orig(repo, remote, *args, **kwargs) - if remote.capable('_push_experiment_notifypushend_0'): - remote.push_experiment_notifypushend_0() - return ret - - -def capabilities(orig, repo, proto): - """wrapper to advertise new capability""" - caps = orig(repo, proto) - if obsolete.isenabled(repo, obsolete.exchangeopt): - caps += ' _push_experiment_pushobsmarkers_0' - caps += ' _push_experiment_notifypushend_0' - return caps - - -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, '') - extensions.wrapfunction(wireproto, 'capabilities', capabilities) - extensions.wrapfunction(obsolete, 'syncpush', syncpush) - extensions.wrapfunction(localrepo.localrepository, 'push', augmented_push) - - diff -r efda653c96a7 -r 94432e742a02 hgext/simple4server.py --- a/hgext/simple4server.py Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,321 +0,0 @@ -'''enable experimental obsolescence feature of Mercurial - -OBSOLESCENCE IS AN EXPERIMENTAL FEATURE MAKE SURE YOU UNDERSTOOD THE INVOLVED -CONCEPT BEFORE USING IT. - -/!\ THIS EXTENSION IS INTENDED FOR SERVER SIDE ONLY USAGE /!\ - -For client side usages it is recommended to use the evolve extension for -improved user interface.''' - -testedwith = '3.3 3.4-rc' -buglink = 'https://bz.mercurial-scm.org/' - -import mercurial.obsolete - -import hashlib -import struct -from mercurial import error -from mercurial import util -from mercurial import wireproto -from mercurial import extensions -from mercurial import obsolete -from cStringIO import StringIO -from mercurial import node -from mercurial.hgweb import hgweb_mod -from mercurial import bundle2 -from mercurial import localrepo -from mercurial import exchange -from mercurial import node -_pack = struct.pack - -gboptslist = gboptsmap = None -try: - from mercurial import obsolete - from mercurial import wireproto - gboptslist = getattr(wireproto, 'gboptslist', None) - gboptsmap = getattr(wireproto, 'gboptsmap', None) -except (ImportError, AttributeError): - 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 - -from mercurial import pushkey - -# specific content also include the wrapping int extsetup -def _nslist(orig, repo): - rep = orig(repo) - if not repo.ui.configbool('__temporary__', 'advertiseobsolete', True): - rep.pop('obsolete') - return rep - -# End of simple4server specific content - - - -# from evolve extension: 1a23c7c52a43 -def srv_pushobsmarkers(repo, proto): - """That receives a stream of markers and apply then to the repo""" - fp = StringIO() - proto.redirect() - proto.getfile(fp) - data = fp.getvalue() - fp.close() - lock = repo.lock() - try: - tr = repo.transaction('pushkey: obsolete markers') - try: - repo.obsstore.mergemarkers(tr, data) - tr.close() - finally: - tr.release() - finally: - lock.release() - repo.hook('evolve_pushobsmarkers') - return wireproto.pushres(0) - -# from evolve extension: 1a23c7c52a43 -def _getobsmarkersstream(repo, heads=None, common=None): - """Get a binary stream for all markers relevant to `:: - ::` - """ - revset = '' - args = [] - repo = repo.unfiltered() - if heads is None: - revset = 'all()' - elif heads: - revset += "(::%ln)" - args.append(heads) - else: - assert False, 'pulling no heads?' - if common: - revset += ' - (::%ln)' - args.append(common) - nodes = [c.node() for c in repo.set(revset, *args)] - markers = repo.obsstore.relevantmarkers(nodes) - obsdata = StringIO() - for chunk in obsolete.encodemarkers(markers, True): - obsdata.write(chunk) - obsdata.seek(0) - return obsdata - -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 - - 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 = {} - return super(pruneobsstore, self).__init__(*args, **kwargs) - - def _load(self, markers): - markers = self._prunedetectingmarkers(markers) - return super(pruneobsstore, self)._load(markers) - - - def _prunedetectingmarkers(self, markers): - for m in markers: - if not m[1]: # no successors - meta = obsolete.decodemeta(m[3]) - if 'p1' in meta: - p1 = node.bin(meta['p1']) - self.prunedchildren.setdefault(p1, set()).add(m) - if 'p2' in meta: - p2 = node.bin(meta['p2']) - self.prunedchildren.setdefault(p2, set()).add(m) - yield m - - # from evolve extension: 1a23c7c52a43 - def relevantmarkers(self, nodes): - """return a set of all obsolescence marker relevant to a set of node. - - "relevant" to a set of node mean: - - - marker that use this changeset as successors - - prune marker of direct children on this changeset. - - recursive application of the two rules on precursors of these markers - - It is a set so you cannot rely on order""" - seennodes = set(nodes) - seenmarkers = set() - pendingnodes = set(nodes) - precursorsmarkers = self.precursors - prunedchildren = self.prunedchildren - while pendingnodes: - direct = set() - for current in pendingnodes: - direct.update(precursorsmarkers.get(current, ())) - direct.update(prunedchildren.get(current, ())) - direct -= seenmarkers - pendingnodes = set([m[0] for m in direct]) - seenmarkers |= direct - pendingnodes -= seennodes - seennodes |= pendingnodes - return seenmarkers - -# The wireproto.streamres API changed, handling chunking and compression -# directly. Handle either case. -if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'): - # We need to handle chunking and compression directly - def streamres(d, proto): - return wireproto.streamres(proto.groupchunks(d)) -else: - # Leave chunking and compression to streamres - def streamres(d, proto): - return wireproto.streamres(reader=d, v1compressible=True) - -# from evolve extension: cf35f38d6a10 -def srv_pullobsmarkers(repo, proto, others): - """serves a binary stream of markers. - - Serves relevant to changeset between heads and common. The stream is prefix - by a -string- representation of an integer. This integer is the size of the - stream.""" - opts = wireproto.options('', ['heads', 'common'], others) - for k, v in opts.iteritems(): - if k in ('heads', 'common'): - opts[k] = wireproto.decodelist(v) - obsdata = _getobsmarkersstream(repo, **opts) - finaldata = StringIO() - obsdata = obsdata.getvalue() - finaldata.write('%20i' % len(obsdata)) - finaldata.write(obsdata) - finaldata.seek(0) - return streamres(finaldata, proto) - - -# from evolve extension: 3249814dabd1 -def _obsrelsethashtreefm0(repo): - return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker) - -# from evolve extension: 3249814dabd1 -def _obsrelsethashtreefm1(repo): - return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker) - -# from evolve extension: 3249814dabd1 -def _obsrelsethashtree(repo, encodeonemarker): - cache = [] - unfi = repo.unfiltered() - markercache = {} - for i in unfi: - ctx = unfi[i] - entry = 0 - sha = hashlib.sha1() - # add data from p1 - for p in ctx.parents(): - p = p.rev() - if p < 0: - p = node.nullid - else: - p = cache[p][1] - if p != node.nullid: - entry += 1 - sha.update(p) - tmarkers = repo.obsstore.relevantmarkers([ctx.node()]) - if tmarkers: - bmarkers = [] - for m in tmarkers: - if not m in markercache: - markercache[m] = encodeonemarker(m) - bmarkers.append(markercache[m]) - bmarkers.sort() - for m in bmarkers: - entry += 1 - sha.update(m) - if entry: - cache.append((ctx.node(), sha.digest())) - else: - cache.append((ctx.node(), node.nullid)) - return cache - -# from evolve extension: 3249814dabd1 -def _obshash(repo, nodes, version=0): - if version == 0: - hashs = _obsrelsethashtreefm0(repo) - elif version ==1: - hashs = _obsrelsethashtreefm1(repo) - else: - assert False - nm = repo.changelog.nodemap - revs = [nm.get(n) for n in nodes] - return [r is None and node.nullid or hashs[r][1] for r in revs] - -# from evolve extension: 3249814dabd1 -def srv_obshash(repo, proto, nodes): - return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes))) - -# from evolve extension: 3249814dabd1 -def srv_obshash1(repo, proto, nodes): - return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), - version=1)) - -# from evolve extension: 3249814dabd1 -def capabilities(orig, repo, proto): - """wrapper to advertise new capability""" - caps = orig(repo, proto) - advertise = repo.ui.configbool('__temporary__', 'advertiseobsolete', True) - if obsolete.isenabled(repo, obsolete.exchangeopt) and advertise: - caps += ' _evoext_pushobsmarkers_0' - caps += ' _evoext_pullobsmarkers_0' - caps += ' _evoext_obshash_0' - caps += ' _evoext_obshash_1' - caps += ' _evoext_getbundle_obscommon' - return caps - -def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs): - if 'evo_obscommon' not in kwargs: - return orig(bundler, repo, source, **kwargs) - - heads = kwargs.get('heads') - if 'evo_obscommon' not in kwargs: - return orig(bundler, repo, source, **kwargs) - - if kwargs.get('obsmarkers', False): - if heads is None: - heads = repo.heads() - obscommon = kwargs.get('evo_obscommon', ()) - obsset = repo.set('::%ln - ::%ln', heads, obscommon) - subset = [c.node() for c in obsset] - markers = repo.obsstore.relevantmarkers(subset) - exchange.buildobsmarkerspart(bundler, markers) - -# from evolve extension: 10867a8e27c6 -# heavily modified -def extsetup(ui): - localrepo.moderncaps.add('_evoext_b2x_obsmarkers_0') - gboptsmap['evo_obscommon'] = 'nodes' - if not util.safehasattr(obsolete.obsstore, 'relevantmarkers'): - obsolete.obsstore = pruneobsstore - obsolete.obsstore.relevantmarkers = relevantmarkers - hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push' - hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull' - hgweb_mod.perms['evoext_obshash'] = 'pull' - wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '') - wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*') - # wrap module content - origfunc = exchange.getbundle2partsmapping['obsmarkers'] - def newfunc(*args, **kwargs): - return _getbundleobsmarkerpart(origfunc, *args, **kwargs) - exchange.getbundle2partsmapping['obsmarkers'] = newfunc - extensions.wrapfunction(wireproto, 'capabilities', capabilities) - # wrap command content - oldcap, args = wireproto.commands['capabilities'] - def newcap(repo, proto): - return capabilities(oldcap, repo, proto) - wireproto.commands['capabilities'] = (newcap, args) - wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes') - wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes') - # specific simple4server content - extensions.wrapfunction(pushkey, '_nslist', _nslist) - pushkey._namespaces['namespaces'] = (lambda *x: False, pushkey._nslist) - -def reposetup(ui, repo): - evolveopts = ui.configlist('experimental', 'evolution') - if not evolveopts: - evolveopts = 'all' - ui.setconfig('experimental', 'evolution', evolveopts) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/__init__.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,4 @@ +# name space package to host third party extensions +from __future__ import absolute_import +import pkgutil +__path__ = pkgutil.extend_path(__path__, __name__) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/__init__.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,3123 @@ +# Copyright 2011 Peter Arrenbrecht +# Logilab SA +# Pierre-Yves David +# Patrick Mezard +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +"""extends Mercurial feature related to Changeset Evolution + +This extension provides several commands to mutate history and deal with +resulting issues. + +It also: + + - enables the "Changeset Obsolescence" feature of Mercurial, + - alters core commands and extensions that rewrite history to use + this feature, + - improves some aspect of the early implementation in Mercurial core +""" + + +evolutionhelptext = """ +Obsolescence markers make it possible to mark changesets that have been +deleted or superset in a new version of the changeset. + +Unlike the previous way of handling such changes, by stripping the old +changesets from the repository, obsolescence markers can be propagated +between repositories. This allows for a safe and simple way of exchanging +mutable history and altering it after the fact. Changeset phases are +respected, such that only draft and secret changesets can be altered (see +:hg:`help phases` for details). + +Obsolescence is tracked using "obsolete markers", a piece of metadata +tracking which changesets have been made obsolete, potential successors for +a given changeset, the moment the changeset was marked as obsolete, and the +user who performed the rewriting operation. The markers are stored +separately from standard changeset data can be exchanged without any of the +precursor changesets, preventing unnecessary exchange of obsolescence data. + +The complete set of obsolescence markers describes a history of changeset +modifications that is orthogonal to the repository history of file +modifications. This changeset history allows for detection and automatic +resolution of edge cases arising from multiple users rewriting the same part +of history concurrently. + +Current feature status +====================== + +This feature is still in development. If you see this help, you have enabled an +extension that turned this feature on. + +Obsolescence markers will be exchanged between repositories that explicitly +assert support for the obsolescence feature (this can currently only be done +via an extension).""".strip() + + +import os +import sys +import random +import re +import collections +import errno +import struct + +try: + import StringIO as io + StringIO = io.StringIO +except ImportError: + import io + StringIO = io.StringIO + + +try: + from mercurial import registrar + registrar.templatekeyword # new in hg-3.8 +except ImportError: + from . import metadata + raise ImportError('evolve needs version %s or above' % + min(metadata.testedwith.split())) + +import mercurial +from mercurial import util +from mercurial import repair + +from mercurial import obsolete +if not obsolete._enabled: + obsolete._enabled = True + +from mercurial import ( + bookmarks as bookmarksmod, + cmdutil, + commands, + context, + copies, + error, + extensions, + help, + hg, + lock as lockmod, + merge, + node, + patch, + phases, + revset, + scmutil, + templatekw, +) + +from mercurial.commands import walkopts, commitopts, commitopts2, mergetoolopts +from mercurial.i18n import _ +from mercurial.node import nullid + +from . import ( + checkheads, + debugcmd, + obsdiscovery, + obsexchange, + exthelper, + metadata, + utility, +) + +__version__ = metadata.__version__ +testedwith = metadata.testedwith +minimumhgversion = metadata.minimumhgversion +buglink = metadata.buglink + +sha1re = re.compile(r'\b[0-9a-f]{6,40}\b') + +# Flags for enabling optional parts of evolve +commandopt = 'allnewcommands' + +obsexcmsg = utility.obsexcmsg + +_pack = struct.pack +_unpack = struct.unpack + +aliases, entry = cmdutil.findcmd('commit', commands.table) +interactiveopt = [['i', 'interactive', None, _('use interactive mode')]] +# This extension contains the following code +# +# - Extension Helper code +# - Obsolescence cache +# - ... +# - Older format compat + +eh = exthelper.exthelper() +eh.merge(debugcmd.eh) +eh.merge(obsdiscovery.eh) +eh.merge(obsexchange.eh) +eh.merge(checkheads.eh) +uisetup = eh.final_uisetup +extsetup = eh.final_extsetup +reposetup = eh.final_reposetup +cmdtable = eh.cmdtable + +##################################################################### +### Option configuration ### +##################################################################### + +@eh.reposetup # must be the first of its kin. +def _configureoptions(ui, repo): + # If no capabilities are specified, enable everything. + # This is so existing evolve users don't need to change their config. + evolveopts = ui.configlist('experimental', 'evolution') + if not evolveopts: + evolveopts = ['all'] + ui.setconfig('experimental', 'evolution', evolveopts, 'evolve') + +@eh.uisetup +def _configurecmdoptions(ui): + # Unregister evolve commands if the command capability is not specified. + # + # This must be in the same function as the option configuration above to + # guarantee it happens after the above configuration, but before the + # extsetup functions. + evolvecommands = ui.configlist('experimental', 'evolutioncommands') + evolveopts = ui.configlist('experimental', 'evolution') + if evolveopts and (commandopt not in evolveopts and + 'all' not in evolveopts): + # We build whitelist containing the commands we want to enable + whitelist = set() + for cmd in evolvecommands: + matchingevolvecommands = [e for e in cmdtable.keys() if cmd in e] + if not matchingevolvecommands: + raise error.Abort(_('unknown command: %s') % cmd) + elif len(matchingevolvecommands) > 1: + msg = _('ambiguous command specification: "%s" matches %r') + raise error.Abort(msg % (cmd, matchingevolvecommands)) + else: + whitelist.add(matchingevolvecommands[0]) + for disabledcmd in set(cmdtable) - whitelist: + del cmdtable[disabledcmd] + +##################################################################### +### experimental behavior ### +##################################################################### + +commitopts3 = [ + ('D', 'current-date', None, + _('record the current date as commit date')), + ('U', 'current-user', None, + _('record the current user as committer')), +] + +def _resolveoptions(ui, opts): + """modify commit options dict to handle related options + + For now, all it does is figure out the commit date: respect -D unless + -d was supplied. + """ + # N.B. this is extremely similar to setupheaderopts() in mq.py + if not opts.get('date') and opts.get('current_date'): + opts['date'] = '%d %d' % util.makedate() + if not opts.get('user') and opts.get('current_user'): + opts['user'] = ui.username() + +getrevs = obsolete.getrevs + +##################################################################### +### Additional Utilities ### +##################################################################### + +# This section contains a lot of small utility function and method + +# - Function to create markers +# - useful alias pstatus and pdiff (should probably go in evolve) +# - "troubles" method on changectx +# - function to travel through the obsolescence graph +# - function to find useful changeset to stabilize + + +### Useful alias + +@eh.uisetup +def _installalias(ui): + if ui.config('alias', 'pstatus', None) is None: + ui.setconfig('alias', 'pstatus', 'status --rev .^', 'evolve') + if ui.config('alias', 'pdiff', None) is None: + ui.setconfig('alias', 'pdiff', 'diff --rev .^', 'evolve') + if ui.config('alias', 'olog', None) is None: + ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden", + 'evolve') + if ui.config('alias', 'odiff', None) is None: + ui.setconfig('alias', 'odiff', + "diff --hidden --rev 'limit(precursors(.),1)' --rev .", + 'evolve') + if ui.config('alias', 'grab', None) is None: + if os.name == 'nt': + ui.setconfig('alias', 'grab', + "! " + util.hgexecutable() + + " rebase --dest . --rev $@ && " + + util.hgexecutable() + " up tip", + 'evolve') + else: + ui.setconfig('alias', 'grab', + "! $HG rebase --dest . --rev $@ && $HG up tip", + 'evolve') + + +### Troubled revset symbol + +@eh.revset('troubled') +def revsettroubled(repo, subset, x): + """``troubled()`` + Changesets with troubles. + """ + revset.getargs(x, 0, 0, 'troubled takes no arguments') + troubled = set() + troubled.update(getrevs(repo, 'unstable')) + troubled.update(getrevs(repo, 'bumped')) + troubled.update(getrevs(repo, 'divergent')) + troubled = revset.baseset(troubled) + troubled.sort() # set is non-ordered, enforce order + return subset & troubled + +### Obsolescence graph + +# XXX SOME MAJOR CLEAN UP TO DO HERE XXX + +def _precursors(repo, s): + """Precursor of a changeset""" + cs = set() + nm = repo.changelog.nodemap + markerbysubj = repo.obsstore.precursors + node = repo.changelog.node + for r in s: + for p in markerbysubj.get(node(r), ()): + pr = nm.get(p[0]) + if pr is not None: + cs.add(pr) + cs -= repo.changelog.filteredrevs # nodemap has no filtering + return cs + +def _allprecursors(repo, s): # XXX we need a better naming + """transitive precursors of a subset""" + node = repo.changelog.node + toproceed = [node(r) for r in s] + seen = set() + allsubjects = repo.obsstore.precursors + while toproceed: + nc = toproceed.pop() + for mark in allsubjects.get(nc, ()): + np = mark[0] + if np not in seen: + seen.add(np) + toproceed.append(np) + nm = repo.changelog.nodemap + cs = set() + for p in seen: + pr = nm.get(p) + if pr is not None: + cs.add(pr) + cs -= repo.changelog.filteredrevs # nodemap has no filtering + return cs + +def _successors(repo, s): + """Successors of a changeset""" + cs = set() + node = repo.changelog.node + nm = repo.changelog.nodemap + markerbyobj = repo.obsstore.successors + for r in s: + for p in markerbyobj.get(node(r), ()): + for sub in p[1]: + sr = nm.get(sub) + if sr is not None: + cs.add(sr) + cs -= repo.changelog.filteredrevs # nodemap has no filtering + return cs + +def _allsuccessors(repo, s, haltonflags=0): # XXX we need a better naming + """transitive successors of a subset + + haltonflags allows to provide flags which prevent the evaluation of a + marker. """ + node = repo.changelog.node + toproceed = [node(r) for r in s] + seen = set() + allobjects = repo.obsstore.successors + while toproceed: + nc = toproceed.pop() + for mark in allobjects.get(nc, ()): + if mark[2] & haltonflags: + continue + for sub in mark[1]: + if sub == nullid: + continue # should not be here! + if sub not in seen: + seen.add(sub) + toproceed.append(sub) + nm = repo.changelog.nodemap + cs = set() + for s in seen: + sr = nm.get(s) + if sr is not None: + cs.add(sr) + cs -= repo.changelog.filteredrevs # nodemap has no filtering + return cs + +##################################################################### +### Extending revset and template ### +##################################################################### + +# this section add several useful revset symbol not yet in core. +# they are subject to changes + + +### XXX I'm not sure this revset is useful +@eh.revset('suspended') +def revsetsuspended(repo, subset, x): + """``suspended()`` + Obsolete changesets with non-obsolete descendants. + """ + revset.getargs(x, 0, 0, 'suspended takes no arguments') + suspended = revset.baseset(getrevs(repo, 'suspended')) + suspended.sort() + return subset & suspended + + +@eh.revset('precursors') +def revsetprecursors(repo, subset, x): + """``precursors(set)`` + Immediate precursors of changesets in set. + """ + s = revset.getset(repo, revset.fullreposet(repo), x) + s = revset.baseset(_precursors(repo, s)) + s.sort() + return subset & s + + +@eh.revset('allprecursors') +def revsetallprecursors(repo, subset, x): + """``allprecursors(set)`` + Transitive precursors of changesets in set. + """ + s = revset.getset(repo, revset.fullreposet(repo), x) + s = revset.baseset(_allprecursors(repo, s)) + s.sort() + return subset & s + + +@eh.revset('successors') +def revsetsuccessors(repo, subset, x): + """``successors(set)`` + Immediate successors of changesets in set. + """ + s = revset.getset(repo, revset.fullreposet(repo), x) + s = revset.baseset(_successors(repo, s)) + s.sort() + return subset & s + +@eh.revset('allsuccessors') +def revsetallsuccessors(repo, subset, x): + """``allsuccessors(set)`` + Transitive successors of changesets in set. + """ + s = revset.getset(repo, revset.fullreposet(repo), x) + s = revset.baseset(_allsuccessors(repo, s)) + s.sort() + return subset & s + +### template keywords +# XXX it does not handle troubles well :-/ + +@eh.templatekw('obsolete') +def obsoletekw(repo, ctx, templ, **args): + """:obsolete: String. Whether the changeset is ``obsolete``. + """ + if ctx.obsolete(): + return 'obsolete' + return '' + +@eh.templatekw('troubles') +def showtroubles(repo, ctx, **args): + """:troubles: List of strings. Evolution troubles affecting the changeset + (zero or more of "unstable", "divergent" or "bumped").""" + return templatekw.showlist('trouble', ctx.troubles(), plural='troubles', + **args) + +##################################################################### +### Various trouble warning ### +##################################################################### + +# This section take care of issue warning to the user when troubles appear + + +def _warnobsoletewc(ui, repo): + if repo['.'].obsolete(): + ui.warn(_('working directory parent is obsolete!\n')) + if (not ui.quiet) and obsolete.isenabled(repo, commandopt): + ui.warn(_("(use 'hg evolve' to update to its successor)\n")) + +@eh.wrapcommand("update") +@eh.wrapcommand("pull") +def wrapmayobsoletewc(origfn, ui, repo, *args, **opts): + """Warn that the working directory parent is an obsolete changeset""" + def warnobsolete(): + _warnobsoletewc(ui, repo) + wlock = None + try: + wlock = repo.wlock() + repo._afterlock(warnobsolete) + res = origfn(ui, repo, *args, **opts) + finally: + lockmod.release(wlock) + return res + +@eh.wrapcommand("parents") +def wrapparents(origfn, ui, repo, *args, **opts): + res = origfn(ui, repo, *args, **opts) + _warnobsoletewc(ui, repo) + return res + +# XXX this could wrap transaction code +# XXX (but this is a bit a layer violation) +@eh.wrapcommand("commit") +@eh.wrapcommand("import") +@eh.wrapcommand("push") +@eh.wrapcommand("pull") +@eh.wrapcommand("graft") +@eh.wrapcommand("phase") +@eh.wrapcommand("unbundle") +def warnobserrors(orig, ui, repo, *args, **kwargs): + """display warning is the command resulted in more instable changeset""" + # part of the troubled stuff may be filtered (stash ?) + # This needs a better implementation but will probably wait for core. + filtered = repo.changelog.filteredrevs + priorunstables = len(set(getrevs(repo, 'unstable')) - filtered) + priorbumpeds = len(set(getrevs(repo, 'bumped')) - filtered) + priordivergents = len(set(getrevs(repo, 'divergent')) - filtered) + ret = orig(ui, repo, *args, **kwargs) + filtered = repo.changelog.filteredrevs + newunstables = \ + len(set(getrevs(repo, 'unstable')) - filtered) - priorunstables + newbumpeds = \ + len(set(getrevs(repo, 'bumped')) - filtered) - priorbumpeds + newdivergents = \ + len(set(getrevs(repo, 'divergent')) - filtered) - priordivergents + if newunstables > 0: + ui.warn(_('%i new unstable changesets\n') % newunstables) + if newbumpeds > 0: + ui.warn(_('%i new bumped changesets\n') % newbumpeds) + if newdivergents > 0: + ui.warn(_('%i new divergent changesets\n') % newdivergents) + return ret + +@eh.wrapfunction(mercurial.exchange, 'push') +def push(orig, repo, *args, **opts): + """Add a hint for "hg evolve" when troubles make push fails + """ + try: + return orig(repo, *args, **opts) + except error.Abort as ex: + hint = _("use 'hg evolve' to get a stable history " + "or --force to ignore warnings") + if (len(ex.args) >= 1 + and ex.args[0].startswith('push includes ') + and ex.hint is None): + ex.hint = hint + raise + +def summaryhook(ui, repo): + def write(fmt, count): + s = fmt % count + if count: + ui.write(s) + else: + ui.note(s) + + state = _evolvestateread(repo) + if state is not None: + # i18n: column positioning for "hg summary" + ui.write(_('evolve: (evolve --continue)\n')) + +@eh.extsetup +def obssummarysetup(ui): + cmdutil.summaryhooks.add('evolve', summaryhook) + + +##################################################################### +### Core Other extension compat ### +##################################################################### + + +@eh.extsetup +def _rebasewrapping(ui): + # warning about more obsolete + try: + rebase = extensions.find('rebase') + if rebase: + extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) + except KeyError: + pass # rebase not found + try: + histedit = extensions.find('histedit') + if histedit: + extensions.wrapcommand(histedit.cmdtable, 'histedit', warnobserrors) + except KeyError: + pass # histedit not found + +##################################################################### +### Old Evolve extension content ### +##################################################################### + +# XXX need clean up and proper sorting in other section + +### changeset rewriting logic +############################# + +def rewrite(repo, old, updates, head, newbases, commitopts): + """Return (nodeid, created) where nodeid is the identifier of the + changeset generated by the rewrite process, and created is True if + nodeid was actually created. If created is False, nodeid + references a changeset existing before the rewrite call. + """ + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction('rewrite') + if len(old.parents()) > 1: # XXX remove this unnecessary limitation. + raise error.Abort(_('cannot amend merge changesets')) + base = old.p1() + updatebookmarks = _bookmarksupdater(repo, old.node(), tr) + + # commit a new version of the old changeset, including the update + # collect all files which might be affected + files = set(old.files()) + for u in updates: + files.update(u.files()) + + # Recompute copies (avoid recording a -> b -> a) + copied = copies.pathcopies(base, head) + + # prune files which were reverted by the updates + def samefile(f): + if f in head.manifest(): + a = head.filectx(f) + if f in base.manifest(): + b = base.filectx(f) + return (a.data() == b.data() + and a.flags() == b.flags()) + else: + return False + else: + return f not in base.manifest() + files = [f for f in files if not samefile(f)] + # commit version of these files as defined by head + headmf = head.manifest() + + def filectxfn(repo, ctx, path): + if path in headmf: + fctx = head[path] + flags = fctx.flags() + mctx = context.memfilectx(repo, fctx.path(), fctx.data(), + islink='l' in flags, + isexec='x' in flags, + copied=copied.get(path)) + return mctx + return None + + message = cmdutil.logmessage(repo.ui, commitopts) + if not message: + message = old.description() + + user = commitopts.get('user') or old.user() + date = commitopts.get('date') or None # old.date() + extra = dict(commitopts.get('extra', old.extra())) + extra['branch'] = head.branch() + + new = context.memctx(repo, + parents=newbases, + text=message, + files=files, + filectxfn=filectxfn, + user=user, + date=date, + extra=extra) + + if commitopts.get('edit'): + new._text = cmdutil.commitforceeditor(repo, new, []) + revcount = len(repo) + newid = repo.commitctx(new) + new = repo[newid] + created = len(repo) != revcount + updatebookmarks(newid) + + tr.close() + return newid, created + finally: + lockmod.release(tr, lock, wlock) + +class MergeFailure(error.Abort): + pass + +def relocate(repo, orig, dest, pctx=None, keepbranch=False): + """rewrite on dest""" + if orig.rev() == dest.rev(): + raise error.Abort(_('tried to relocate a node on top of itself'), + hint=_("This shouldn't happen. If you still " + "need to move changesets, please do so " + "manually with nothing to rebase - working " + "directory parent is also destination")) + + if pctx is None: + if len(orig.parents()) == 2: + raise error.Abort(_("tried to relocate a merge commit without " + "specifying which parent should be moved"), + hint=_("Specify the parent by passing in pctx")) + pctx = orig.p1() + + commitmsg = orig.description() + + cache = {} + sha1s = re.findall(sha1re, commitmsg) + unfi = repo.unfiltered() + for sha1 in sha1s: + ctx = None + try: + ctx = unfi[sha1] + except error.RepoLookupError: + continue + + if not ctx.obsolete(): + continue + + successors = obsolete.successorssets(repo, ctx.node(), cache) + + # We can't make any assumptions about how to update the hash if the + # cset in question was split or diverged. + if len(successors) == 1 and len(successors[0]) == 1: + newsha1 = node.hex(successors[0][0]) + commitmsg = commitmsg.replace(sha1, newsha1[:len(sha1)]) + else: + repo.ui.note(_('The stale commit message reference to %s could ' + 'not be updated\n') % sha1) + + tr = repo.currenttransaction() + assert tr is not None + try: + r = _evolvemerge(repo, orig, dest, pctx, keepbranch) + if r[-1]: # some conflict + raise error.Abort(_('unresolved merge conflicts ' + '(see hg help resolve)')) + nodenew = _relocatecommit(repo, orig, commitmsg) + except error.Abort as exc: + repo.dirstate.beginparentchange() + repo.setparents(repo['.'].node(), nullid) + repo.dirstate.write(tr) + # fix up dirstate for copies and renames + copies.duplicatecopies(repo, dest.rev(), orig.p1().rev()) + repo.dirstate.endparentchange() + + class LocalMergeFailure(MergeFailure, exc.__class__): + pass + exc.__class__ = LocalMergeFailure + tr.close() # to keep changes in this transaction (e.g. dirstate) + raise + _finalizerelocate(repo, orig, dest, nodenew, tr) + return nodenew + +def _bookmarksupdater(repo, oldid, tr): + """Return a callable update(newid) updating the current bookmark + and bookmarks bound to oldid to newid. + """ + def updatebookmarks(newid): + dirty = False + oldbookmarks = repo.nodebookmarks(oldid) + if oldbookmarks: + for b in oldbookmarks: + repo._bookmarks[b] = newid + dirty = True + if dirty: + repo._bookmarks.recordchange(tr) + return updatebookmarks + +### new command +############################# +metadataopts = [ + ('d', 'date', '', + _('record the specified date in metadata'), _('DATE')), + ('u', 'user', '', + _('record the specified user in metadata'), _('USER')), +] + +@eh.uisetup +def _installimportobsolete(ui): + entry = cmdutil.findcmd('import', commands.table)[1] + entry[1].append(('', 'obsolete', False, + _('mark the old node as obsoleted by ' + 'the created commit'))) + +@eh.wrapfunction(mercurial.cmdutil, 'tryimportone') +def tryimportone(orig, ui, repo, hunk, parents, opts, *args, **kwargs): + extracted = patch.extract(ui, hunk) + expected = extracted.get('nodeid') + if expected is not None: + expected = node.bin(expected) + oldextract = patch.extract + try: + patch.extract = lambda ui, hunk: extracted + ret = orig(ui, repo, hunk, parents, opts, *args, **kwargs) + finally: + patch.extract = oldextract + created = ret[1] + if (opts['obsolete'] and None not in (created, expected) + and created != expected): + tr = repo.transaction('import-obs') + try: + metadata = {'user': ui.username()} + repo.obsstore.create(tr, expected, (created,), + metadata=metadata) + tr.close() + finally: + tr.release() + return ret + + +def _deprecatealias(oldalias, newalias): + '''Deprecates an alias for a command in favour of another + + Creates a new entry in the command table for the old alias. It creates a + wrapper that has its synopsis set to show that is has been deprecated. + The documentation will be replace with a pointer to the new alias. + If a user invokes the command a deprecation warning will be printed and + the command of the *new* alias will be invoked. + + This function is loosely based on the extensions.wrapcommand function. + ''' + try: + aliases, entry = cmdutil.findcmd(newalias, cmdtable) + except error.UnknownCommand: + # Commands may be disabled + return + for alias, e in cmdtable.items(): + if e is entry: + break + + synopsis = '(DEPRECATED)' + if len(entry) > 2: + fn, opts, _syn = entry + else: + fn, opts, = entry + deprecationwarning = _('%s have been deprecated in favor of %s\n') % ( + oldalias, newalias) + + def newfn(*args, **kwargs): + ui = args[0] + ui.warn(deprecationwarning) + util.checksignature(fn)(*args, **kwargs) + newfn.__doc__ = deprecationwarning + cmdwrapper = eh.command(oldalias, opts, synopsis) + cmdwrapper(newfn) + +@eh.extsetup +def deprecatealiases(ui): + _deprecatealias('gup', 'next') + _deprecatealias('gdown', 'previous') + +def _solveone(ui, repo, ctx, dryrun, confirm, progresscb, category): + """Resolve the troubles affecting one revision""" + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction("evolve") + if 'unstable' == category: + result = _solveunstable(ui, repo, ctx, dryrun, confirm, progresscb) + elif 'bumped' == category: + result = _solvebumped(ui, repo, ctx, dryrun, confirm, progresscb) + elif 'divergent' == category: + result = _solvedivergent(ui, repo, ctx, dryrun, confirm, + progresscb) + else: + assert False, "unknown trouble category: %s" % (category) + tr.close() + return result + finally: + lockmod.release(tr, lock, wlock) + +def _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat): + """Used by the evolve function to display an error message when + no troubles can be resolved""" + troublecategories = ['bumped', 'divergent', 'unstable'] + unselectedcategories = [c for c in troublecategories if c != targetcat] + msg = None + hint = None + + troubled = { + "unstable": repo.revs("unstable()"), + "divergent": repo.revs("divergent()"), + "bumped": repo.revs("bumped()"), + "all": repo.revs("troubled()"), + } + + hintmap = { + 'bumped': _("do you want to use --bumped"), + 'bumped+divergent': _("do you want to use --bumped or --divergent"), + 'bumped+unstable': _("do you want to use --bumped or --unstable"), + 'divergent': _("do you want to use --divergent"), + 'divergent+unstable': _("do you want to use --divergent" + " or --unstable"), + 'unstable': _("do you want to use --unstable"), + 'any+bumped': _("do you want to use --any (or --rev) and --bumped"), + 'any+bumped+divergent': _("do you want to use --any (or --rev) and" + " --bumped or --divergent"), + 'any+bumped+unstable': _("do you want to use --any (or --rev) and" + "--bumped or --unstable"), + 'any+divergent': _("do you want to use --any (or --rev) and" + " --divergent"), + 'any+divergent+unstable': _("do you want to use --any (or --rev)" + " and --divergent or --unstable"), + 'any+unstable': _("do you want to use --any (or --rev)" + "and --unstable"), + } + + if revopt: + revs = scmutil.revrange(repo, revopt) + if not revs: + msg = _("set of specified revisions is empty") + else: + msg = _("no %s changesets in specified revisions") % targetcat + othertroubles = [] + for cat in unselectedcategories: + if revs & troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['+'.join(othertroubles)] + + elif anyopt: + msg = _("no %s changesets to evolve") % targetcat + othertroubles = [] + for cat in unselectedcategories: + if troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['+'.join(othertroubles)] + + else: + # evolve without any option = relative to the current wdir + if targetcat == 'unstable': + msg = _("nothing to evolve on current working copy parent") + else: + msg = _("current working copy parent is not %s") % targetcat + + p1 = repo['.'].rev() + othertroubles = [] + for cat in unselectedcategories: + if p1 in troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['+'.join(othertroubles)] + else: + l = len(troubled[targetcat]) + if l: + hint = _("%d other %s in the repository, do you want --any " + "or --rev") % (l, targetcat) + else: + othertroubles = [] + for cat in unselectedcategories: + if troubled[cat]: + othertroubles.append(cat) + if othertroubles: + hint = hintmap['any+' + ('+'.join(othertroubles))] + else: + msg = _("no troubled changesets") + + assert msg is not None + ui.write_err("%s\n" % msg) + if hint: + ui.write_err("(%s)\n" % hint) + return 2 + else: + return 1 + +def _cleanup(ui, repo, startnode, showprogress): + if showprogress: + ui.progress(_('evolve'), None) + if repo['.'] != startnode: + ui.status(_('working directory is now at %s\n') % repo['.']) + +class MultipleSuccessorsError(RuntimeError): + """Exception raised by _singlesuccessor when multiple successor sets exists + + The object contains the list of successorssets in its 'successorssets' + attribute to call to easily recover. + """ + + def __init__(self, successorssets): + self.successorssets = successorssets + +def _singlesuccessor(repo, p): + """returns p (as rev) if not obsolete or its unique latest successors + + fail if there are no such successor""" + + if not p.obsolete(): + return p.rev() + obs = repo[p] + ui = repo.ui + newer = obsolete.successorssets(repo, obs.node()) + # search of a parent which is not killed + while not newer: + ui.debug("stabilize target %s is plain dead," + " trying to stabilize on its parent\n" % + obs) + obs = obs.parents()[0] + newer = obsolete.successorssets(repo, obs.node()) + if len(newer) > 1 or len(newer[0]) > 1: + raise MultipleSuccessorsError(newer) + + return repo[newer[0][0]].rev() + +def builddependencies(repo, revs): + """returns dependency graphs giving an order to solve instability of revs + (see _orderrevs for more information on usage)""" + + # For each troubled revision we keep track of what instability if any should + # be resolved in order to resolve it. Example: + # dependencies = {3: [6], 6:[]} + # Means that: 6 has no dependency, 3 depends on 6 to be solved + dependencies = {} + # rdependencies is the inverted dict of dependencies + rdependencies = collections.defaultdict(set) + + for r in revs: + dependencies[r] = set() + for p in repo[r].parents(): + try: + succ = _singlesuccessor(repo, p) + except MultipleSuccessorsError as exc: + dependencies[r] = exc.successorssets + continue + if succ in revs: + dependencies[r].add(succ) + rdependencies[succ].add(r) + return dependencies, rdependencies + +def _dedupedivergents(repo, revs): + """Dedupe the divergents revs in revs to get one from each group with the + lowest revision numbers + """ + repo = repo.unfiltered() + res = set() + # To not reevaluate divergents of the same group once one is encountered + discarded = set() + for rev in revs: + if rev in discarded: + continue + divergent = repo[rev] + base, others = divergentdata(divergent) + othersrevs = [o.rev() for o in others] + res.add(min([divergent.rev()] + othersrevs)) + discarded.update(othersrevs) + return res + +def _selectrevs(repo, allopt, revopt, anyopt, targetcat): + """select troubles in repo matching according to given options""" + revs = set() + if allopt or revopt: + revs = repo.revs("%s()" % targetcat) + if revopt: + revs = scmutil.revrange(repo, revopt) & revs + elif not anyopt: + topic = getattr(repo, 'currenttopic', '') + if topic: + revs = repo.revs('topic(%s)', topic) & revs + elif targetcat == 'unstable': + revs = _aspiringdescendant(repo, + repo.revs('(.::) - obsolete()::')) + revs = set(revs) + if targetcat == 'divergent': + # Pick one divergent per group of divergents + revs = _dedupedivergents(repo, revs) + elif anyopt: + revs = repo.revs('first(%s())' % (targetcat)) + elif targetcat == 'unstable': + revs = set(_aspiringchildren(repo, repo.revs('(.::) - obsolete()::'))) + if 1 < len(revs): + msg = "multiple evolve candidates" + hint = (_("select one of %s with --rev") + % ', '.join([str(repo[r]) for r in sorted(revs)])) + raise error.Abort(msg, hint=hint) + elif targetcat in repo['.'].troubles(): + revs = set([repo['.'].rev()]) + return revs + + +def _orderrevs(repo, revs): + """Compute an ordering to solve instability for the given revs + + revs is a list of unstable revisions. + + Returns the same revisions ordered to solve their instability from the + bottom to the top of the stack that the stabilization process will produce + eventually. + + This ensures the minimal number of stabilizations, as we can stabilize each + revision on its final stabilized destination. + """ + # Step 1: Build the dependency graph + dependencies, rdependencies = builddependencies(repo, revs) + # Step 2: Build the ordering + # Remove the revisions with no dependency(A) and add them to the ordering. + # Removing these revisions leads to new revisions with no dependency (the + # one depending on A) that we can remove from the dependency graph and add + # to the ordering. We progress in a similar fashion until the ordering is + # built + solvablerevs = collections.deque([r for r in sorted(dependencies.keys()) + if not dependencies[r]]) + ordering = [] + while solvablerevs: + rev = solvablerevs.popleft() + for dependent in rdependencies[rev]: + dependencies[dependent].remove(rev) + if not dependencies[dependent]: + solvablerevs.append(dependent) + del dependencies[rev] + ordering.append(rev) + + ordering.extend(sorted(dependencies)) + return ordering + +def divergentsets(repo, ctx): + """Compute sets of commits divergent with a given one""" + cache = {} + base = {} + for n in obsolete.allprecursors(repo.obsstore, [ctx.node()]): + if n == ctx.node(): + # a node can't be a base for divergence with itself + continue + nsuccsets = obsolete.successorssets(repo, n, cache) + for nsuccset in nsuccsets: + if ctx.node() in nsuccset: + # we are only interested in *other* successor sets + continue + if tuple(nsuccset) in base: + # we already know the latest base for this divergency + continue + base[tuple(nsuccset)] = n + divergence = [] + for divset, b in base.iteritems(): + divergence.append({ + 'divergentnodes': divset, + 'commonprecursor': b + }) + + return divergence + +def _preparelistctxs(items, condition): + return [item.hex() for item in items if condition(item)] + +def _formatctx(fm, ctx): + fm.data(node=ctx.hex()) + fm.data(desc=ctx.description()) + fm.data(date=ctx.date()) + fm.data(user=ctx.user()) + +def listtroubles(ui, repo, troublecategories, **opts): + """Print all the troubles for the repo (or given revset)""" + troublecategories = troublecategories or ['divergent', 'unstable', 'bumped'] + showunstable = 'unstable' in troublecategories + showbumped = 'bumped' in troublecategories + showdivergent = 'divergent' in troublecategories + + revs = repo.revs('+'.join("%s()" % t for t in troublecategories)) + if opts.get('rev'): + revs = revs & repo.revs(opts.get('rev')) + + fm = ui.formatter('evolvelist', opts) + for rev in revs: + ctx = repo[rev] + unpars = _preparelistctxs(ctx.parents(), lambda p: p.unstable()) + obspars = _preparelistctxs(ctx.parents(), lambda p: p.obsolete()) + imprecs = _preparelistctxs(repo.set("allprecursors(%n)", ctx.node()), + lambda p: not p.mutable()) + dsets = divergentsets(repo, ctx) + + fm.startitem() + # plain formatter section + hashlen, desclen = 12, 60 + desc = ctx.description() + if desc: + desc = desc.splitlines()[0] + desc = (desc[:desclen] + '...') if len(desc) > desclen else desc + fm.plain('%s: ' % ctx.hex()[:hashlen]) + fm.plain('%s\n' % desc) + fm.data(node=ctx.hex(), rev=ctx.rev(), desc=desc, phase=ctx.phasestr()) + + for unpar in unpars if showunstable else []: + fm.plain(' unstable: %s (unstable parent)\n' % unpar[:hashlen]) + for obspar in obspars if showunstable else []: + fm.plain(' unstable: %s (obsolete parent)\n' % obspar[:hashlen]) + for imprec in imprecs if showbumped else []: + fm.plain(' bumped: %s (immutable precursor)\n' % imprec[:hashlen]) + + if dsets and showdivergent: + for dset in dsets: + fm.plain(' divergent: ') + first = True + for n in dset['divergentnodes']: + t = "%s (%s)" if first else " %s (%s)" + first = False + fm.plain(t % (node.hex(n)[:hashlen], repo[n].phasestr())) + comprec = node.hex(dset['commonprecursor'])[:hashlen] + fm.plain(" (precursor %s)\n" % comprec) + fm.plain("\n") + + # templater-friendly section + _formatctx(fm, ctx) + troubles = [] + for unpar in unpars: + troubles.append({'troubletype': 'unstable', 'sourcenode': unpar, + 'sourcetype': 'unstableparent'}) + for obspar in obspars: + troubles.append({'troubletype': 'unstable', 'sourcenode': obspar, + 'sourcetype': 'obsoleteparent'}) + for imprec in imprecs: + troubles.append({'troubletype': 'bumped', 'sourcenode': imprec, + 'sourcetype': 'immutableprecursor'}) + for dset in dsets: + divnodes = [{'node': node.hex(n), + 'phase': repo[n].phasestr(), + } for n in dset['divergentnodes']] + troubles.append({'troubletype': 'divergent', + 'commonprecursor': node.hex(dset['commonprecursor']), + 'divergentnodes': divnodes}) + fm.data(troubles=troubles) + + fm.end() + +@eh.command( + '^evolve|stabilize|solve', + [('n', 'dry-run', False, + _('do not perform actions, just print what would be done')), + ('', 'confirm', False, + _('ask for confirmation before performing the action')), + ('A', 'any', False, + _('also consider troubled changesets unrelated to current working ' + 'directory')), + ('r', 'rev', [], _('solves troubles of these revisions')), + ('', 'bumped', False, _('solves only bumped changesets')), + ('', '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')), + ('c', 'continue', False, _('continue an interrupted evolution')), + ('l', 'list', False, 'provide details on troubled changesets in the repo'), + ] + mergetoolopts, + _('[OPTIONS]...') +) +def evolve(ui, repo, **opts): + """solve troubled changesets in your repository + + Modifying history can lead to various types of troubled changesets: + unstable, bumped, or divergent. The evolve command resolves your troubles + by executing one of the following actions: + + - update working copy to a successor + - rebase an unstable changeset + - extract the desired changes from a bumped changeset + - fuse divergent changesets back together + + If you pass no arguments, evolve works in automatic mode: it will execute a + single action to reduce instability related to your working copy. There are + two cases for this action. First, if the parent of your working copy is + obsolete, evolve updates to the parent's successor. Second, if the working + copy parent is not obsolete but has obsolete predecessors, then evolve + determines if there is an unstable changeset that can be rebased onto the + working copy parent in order to reduce instability. + If so, evolve rebases that changeset. If not, evolve refuses to guess your + intention, and gives a hint about what you might want to do next. + + Any time evolve creates a changeset, it updates the working copy to the new + changeset. (Currently, every successful evolve operation involves an update + as well; this may change in future.) + + Automatic mode only handles common use cases. For example, it avoids taking + action in the case of ambiguity, and it ignores unstable changesets that + are not related to your working copy. + It also refuses to solve bumped or divergent changesets unless you 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. + This is more powerful than successive invocations, since ``--all`` handles + ambiguous cases (e.g. unstable changesets with multiple children) by + evolving all branches. + + When your repository cannot be handled by automatic mode, you might need to + use ``--rev`` to specify a changeset to evolve. For example, if you have + an unstable changeset that is not related to the working copy parent, + you could use ``--rev`` to evolve it. Or, if some changeset has multiple + unstable children, evolve in automatic mode refuses to guess which one to + evolve; you have to use ``--rev`` in that case. + + Alternately, ``--any`` makes evolve search for the next evolvable changeset + regardless of whether it is related to the working copy parent. + + You can supply multiple revisions to evolve multiple troubled changesets + in a single invocation. In revset terms, ``--any`` is equivalent to ``--rev + first(unstable())``. ``--rev`` and ``--all`` are mutually exclusive, as are + ``--rev`` and ``--any``. + + ``hg evolve --any --all`` is useful for cleaning up instability across all + branches, letting evolve figure out the appropriate order and destination. + + When you have troubled changesets that are not unstable, :hg:`evolve` + refuses to consider them unless you specify the category of trouble you + wish to resolve, with ``--bumped`` or ``--divergent``. These options are + currently mutually exclusive with each other and with ``--unstable`` + (the default). You can combine ``--bumped`` or ``--divergent`` with + ``--rev``, ``--all``, or ``--any``. + + You can also use the evolve command to list the troubles affecting your + repository by using the --list flag. You can choose to display only some + categories of troubles with the --unstable, --divergent or --bumped flags. + """ + + # Options + listopt = opts['list'] + contopt = opts['continue'] + anyopt = opts['any'] + allopt = opts['all'] + startnode = repo['.'] + dryrunopt = opts['dry_run'] + confirmopt = opts['confirm'] + revopt = opts['rev'] + troublecategories = ['bumped', 'divergent', 'unstable'] + specifiedcategories = [t for t in troublecategories if opts[t]] + if listopt: + listtroubles(ui, repo, specifiedcategories, **opts) + return + + targetcat = 'unstable' + if 1 < len(specifiedcategories): + msg = _('cannot specify more than one trouble category to solve (yet)') + raise error.Abort(msg) + elif len(specifiedcategories) == 1: + targetcat = specifiedcategories[0] + elif repo['.'].obsolete(): + displayer = cmdutil.show_changeset(ui, repo, + {'template': shorttemplate}) + # no args and parent is obsolete, update to successors + try: + ctx = repo[_singlesuccessor(repo, repo['.'])] + except MultipleSuccessorsError as exc: + repo.ui.write_err('parent is obsolete with multiple successors:\n') + for ln in exc.successorssets: + for n in ln: + displayer.show(repo[n]) + return 2 + + ui.status(_('update:')) + if not ui.quiet: + displayer.show(ctx) + + if dryrunopt: + return 0 + res = hg.update(repo, ctx.rev()) + if ctx != startnode: + ui.status(_('working directory is now at %s\n') % ctx) + return res + + ui.setconfig('ui', 'forcemerge', opts.get('tool', ''), 'evolve') + troubled = set(repo.revs('troubled()')) + + # Progress handling + seen = 1 + count = allopt and len(troubled) or 1 + showprogress = allopt + + def progresscb(): + if revopt or allopt: + ui.progress(_('evolve'), seen, unit=_('changesets'), total=count) + + # Continuation handling + if contopt: + if anyopt: + raise error.Abort('cannot specify both "--any" and "--continue"') + if allopt: + 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. + lock = repo.wlock() + try: + repo.vfs.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.vfs.join('graftstate'), ignoremissing=True) + finally: + lock.release() + cmdutil.bailifchanged(repo) + + if revopt and allopt: + raise error.Abort('cannot specify both "--rev" and "--all"') + if revopt and anyopt: + raise error.Abort('cannot specify both "--rev" and "--any"') + + revs = _selectrevs(repo, allopt, revopt, anyopt, targetcat) + + if not revs: + return _handlenotrouble(ui, repo, allopt, revopt, anyopt, targetcat) + + # For the progress bar to show + count = len(revs) + # Order the revisions + if targetcat == 'unstable': + revs = _orderrevs(repo, revs) + for rev in revs: + progresscb() + _solveone(ui, repo, repo[rev], dryrunopt, confirmopt, + progresscb, targetcat) + seen += 1 + progresscb() + _cleanup(ui, repo, startnode, showprogress) + +def _possibledestination(repo, rev): + """return all changesets that may be a new parent for REV""" + tonode = repo.changelog.node + parents = repo.changelog.parentrevs + torev = repo.changelog.rev + dest = set() + tovisit = list(parents(rev)) + while tovisit: + r = tovisit.pop() + succsets = obsolete.successorssets(repo, tonode(r)) + if not succsets: + tovisit.extend(parents(r)) + else: + # We should probably pick only one destination from split + # (case where '1 < len(ss)'), This could be the currently tipmost + # but logic is less clear when result of the split are now on + # multiple branches. + for ss in succsets: + for n in ss: + dest.add(torev(n)) + return dest + +def _aspiringchildren(repo, revs): + """Return a list of changectx which can be stabilized on top of pctx or + one of its descendants. Empty list if none can be found.""" + target = set(revs) + result = [] + for r in repo.revs('unstable() - %ld', revs): + dest = _possibledestination(repo, r) + if target & dest: + result.append(r) + return result + +def _aspiringdescendant(repo, revs): + """Return a list of changectx which can be stabilized on top of pctx or + one of its descendants recursively. Empty list if none can be found.""" + target = set(revs) + result = set(target) + paths = collections.defaultdict(set) + for r in repo.revs('unstable() - %ld', revs): + for d in _possibledestination(repo, r): + paths[d].add(r) + + result = set(target) + tovisit = list(revs) + while tovisit: + base = tovisit.pop() + for unstable in paths[base]: + if unstable not in result: + tovisit.append(unstable) + result.add(unstable) + return sorted(result - target) + +def _solveunstable(ui, repo, orig, dryrun=False, confirm=False, + progresscb=None): + """Stabilize an unstable changeset""" + pctx = orig.p1() + if len(orig.parents()) == 2: + if not pctx.obsolete(): + pctx = orig.p2() # second parent is obsolete ? + elif orig.p2().obsolete(): + hint = _("Redo the merge (%s) and use `hg prune " + "--succ ` to obsolete the old one") % orig.hex()[:12] + ui.warn(_("warning: no support for evolving merge changesets " + "with two obsolete parents yet\n") + + _("(%s)\n") % hint) + return False + + if not pctx.obsolete(): + ui.warn(_("cannot solve instability of %s, skipping\n") % orig) + return False + obs = pctx + newer = obsolete.successorssets(repo, obs.node()) + # search of a parent which is not killed + while not newer or newer == [()]: + ui.debug("stabilize target %s is plain dead," + " trying to stabilize on its parent\n" % + obs) + obs = obs.parents()[0] + newer = obsolete.successorssets(repo, obs.node()) + if len(newer) > 1: + msg = _("skipping %s: divergent rewriting. can't choose " + "destination\n") % obs + ui.write_err(msg) + return 2 + targets = newer[0] + assert targets + if len(targets) > 1: + # split target, figure out which one to pick, are they all in line? + targetrevs = [repo[r].rev() for r in targets] + roots = repo.revs('roots(%ld)', targetrevs) + heads = repo.revs('heads(%ld)', targetrevs) + if len(roots) > 1 or len(heads) > 1: + msg = "cannot solve split accross two branches\n" + ui.write_err(msg) + return 2 + target = repo[heads.first()] + else: + target = targets[0] + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + target = repo[target] + if not ui.quiet or confirm: + repo.ui.write(_('move:')) + displayer.show(orig) + repo.ui.write(_('atop:')) + displayer.show(target) + if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': + raise error.Abort(_('evolve aborted by user')) + if progresscb: + progresscb() + todo = 'hg rebase -r %s -d %s\n' % (orig, target) + if dryrun: + repo.ui.write(todo) + else: + repo.ui.note(todo) + if progresscb: + progresscb() + keepbranch = orig.p1().branch() != orig.branch() + try: + relocate(repo, orig, target, pctx, keepbranch) + except MergeFailure: + _evolvestatewrite(repo, {'current': orig.node()}) + repo.ui.write_err(_('evolve failed!\n')) + repo.ui.write_err( + _("fix conflict and run 'hg evolve --continue'" + " or use 'hg update -C .' to abort\n")) + raise + +def _solvebumped(ui, repo, bumped, dryrun=False, confirm=False, + progresscb=None): + """Stabilize a bumped changeset""" + repo = repo.unfiltered() + bumped = repo[bumped.rev()] + # For now we deny bumped merge + if len(bumped.parents()) > 1: + msg = _('skipping %s : we do not handle merge yet\n') % bumped + ui.write_err(msg) + return 2 + prec = repo.set('last(allprecursors(%d) and public())', bumped).next() + # For now we deny target merge + if len(prec.parents()) > 1: + msg = _('skipping: %s: public version is a merge, ' + 'this is not handled yet\n') % prec + ui.write_err(msg) + return 2 + + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if not ui.quiet or confirm: + repo.ui.write(_('recreate:')) + displayer.show(bumped) + repo.ui.write(_('atop:')) + displayer.show(prec) + if confirm and ui.prompt('perform evolve? [Ny]', 'n') != 'y': + raise error.Abort(_('evolve aborted by user')) + if dryrun: + todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1()) + repo.ui.write(todo) + repo.ui.write(('hg update %s;\n' % prec)) + repo.ui.write(('hg revert --all --rev %s;\n' % bumped)) + repo.ui.write(('hg commit --msg "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.currenttransaction() + assert tr is not None + bmupdate = _bookmarksupdater(repo, bumped.node(), tr) + if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)): + # Need to rebase the changeset at the right place + repo.ui.status( + _('rebasing to destination parent: %s\n') % prec.p1()) + try: + tmpid = relocate(repo, bumped, prec.p1()) + if tmpid is not None: + tmpctx = repo[tmpid] + obsolete.createmarkers(repo, [(bumped, (tmpctx,))]) + except MergeFailure: + repo.vfs.write('graftstate', bumped.hex() + '\n') + repo.ui.write_err(_('evolution failed!\n')) + msg = _("fix conflict and run 'hg evolve --continue'\n") + repo.ui.write_err(msg) + raise + # Create the new commit context + repo.ui.status(_('computing new diff\n')) + files = set() + copied = copies.pathcopies(prec, bumped) + precmanifest = prec.manifest().copy() + # 3.3.2 needs a list. + # future 3.4 don't detect the size change during iteration + # this is fishy + for key, val in list(bumped.manifest().iteritems()): + precvalue = precmanifest.get(key, None) + if precvalue is not None: + del precmanifest[key] + if precvalue != val: + files.add(key) + files.update(precmanifest) # add missing files + # commit it + if files: # something to commit! + def filectxfn(repo, ctx, path): + if path in bumped: + fctx = bumped[path] + flags = fctx.flags() + mctx = context.memfilectx(repo, fctx.path(), fctx.data(), + islink='l' in flags, + isexec='x' in flags, + copied=copied.get(path)) + return mctx + return None + text = 'bumped update to %s:\n\n' % prec + text += bumped.description() + + new = context.memctx(repo, + parents=[prec.node(), node.nullid], + text=text, + files=files, + filectxfn=filectxfn, + user=bumped.user(), + date=bumped.date(), + extra=bumped.extra()) + + newid = repo.commitctx(new) + if newid is None: + obsolete.createmarkers(repo, [(tmpctx, ())]) + newid = prec.node() + else: + phases.retractboundary(repo, tr, bumped.phase(), [newid]) + obsolete.createmarkers(repo, [(tmpctx, (repo[newid],))], + flag=obsolete.bumpedfix) + bmupdate(newid) + repo.ui.status(_('committed as %s\n') % node.short(newid)) + # reroute the working copy parent to the new changeset + repo.dirstate.beginparentchange() + repo.dirstate.setparents(newid, node.nullid) + repo.dirstate.endparentchange() + +def _solvedivergent(ui, repo, divergent, dryrun=False, confirm=False, + progresscb=None): + repo = repo.unfiltered() + divergent = repo[divergent.rev()] + base, others = divergentdata(divergent) + if len(others) > 1: + othersstr = "[%s]" % (','.join([str(i) for i in others])) + msg = _("skipping %d: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 " + "such as:\n" + "| - hg touch -D\n" + "| - hg prune\n" + "| \n" + "| You should contact your local evolution Guru for help.\n" + ) % (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 + ui.write_err(msg) + hint = _("You have to fallback to solving this by hand...\n" + "| This probably means redoing the merge and using \n" + "| `hg prune` to kill older version.\n") + ui.write_err(hint) + return 2 + if other.p1() not in divergent.parents(): + msg = _("skipping %s: have a different parent than %s " + "(not handled yet)\n") % (divergent, other) + hint = _("| %(d)s, %(o)s are not based on the same changeset.\n" + "| With the current state of its implementation, \n" + "| evolve does not work in that case.\n" + "| rebase one of them next to the other and run \n" + "| this command again.\n" + "| - either: hg rebase --dest 'p1(%(d)s)' -r %(o)s\n" + "| - or: hg rebase --dest 'p1(%(o)s)' -r %(d)s\n" + ) % {'d': divergent, 'o': other} + ui.write_err(msg) + ui.write_err(hint) + return 2 + + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if not ui.quiet or confirm: + ui.write(_('merge:')) + displayer.show(divergent) + ui.write(_('with: ')) + displayer.show(other) + ui.write(_('base: ')) + displayer.show(base) + if confirm and ui.prompt(_('perform evolve? [Ny]'), 'n') != 'y': + raise error.Abort(_('evolve aborted by user')) + if dryrun: + ui.write(('hg update -c %s &&\n' % divergent)) + ui.write(('hg merge %s &&\n' % other)) + ui.write(('hg commit -m "auto merge resolving conflict between ' + '%s and %s"&&\n' % (divergent, other))) + ui.write(('hg up -C %s &&\n' % base)) + ui.write(('hg revert --all --rev tip &&\n')) + ui.write(('hg commit -m "`hg log -r %s --template={desc}`";\n' + % divergent)) + return + if divergent not in repo[None].parents(): + repo.ui.status(_('updating to "local" conflict\n')) + hg.update(repo, divergent.rev()) + repo.ui.note(_('merging divergent changeset\n')) + if progresscb: + progresscb() + stats = merge.update(repo, + other.node(), + branchmerge=True, + force=False, + ancestor=base.node(), + mergeancestor=True) + hg._showstats(repo, stats) + if stats[3]: + repo.ui.status(_("use 'hg resolve' to retry unresolved file merges " + "or 'hg update -C .' to abort\n")) + if stats[3] > 0: + raise error.Abort('merge conflict between several amendments ' + '(this is not automated yet)', + hint="""/!\ You can try: +/!\ * manual merge + resolve => new cset X +/!\ * hg up to the parent of the amended changeset (which are named W and Z) +/!\ * hg revert --all -r X +/!\ * hg ci -m "same message as the amended changeset" => new cset Y +/!\ * hg prune -n Y W Z +""") + if progresscb: + progresscb() + emtpycommitallowed = repo.ui.backupconfig('ui', 'allowemptycommit') + tr = repo.currenttransaction() + assert tr is not None + try: + repo.ui.setconfig('ui', 'allowemptycommit', True, 'evolve') + repo.dirstate.beginparentchange() + repo.dirstate.setparents(divergent.node(), node.nullid) + repo.dirstate.endparentchange() + oldlen = len(repo) + amend(ui, repo, message='', logfile='') + if oldlen == len(repo): + new = divergent + # no changes + else: + new = repo['.'] + obsolete.createmarkers(repo, [(other, (new,))]) + phases.retractboundary(repo, tr, other.phase(), [new.node()]) + finally: + repo.ui.restoreconfig(emtpycommitallowed) + +def divergentdata(ctx): + """return base, other part of a conflict + + This only return the first one. + + XXX this woobly function won't survive XXX + """ + repo = ctx._repo.unfiltered() + for base in repo.set('reverse(allprecursors(%d))', ctx): + newer = obsolete.successorssets(ctx._repo, base.node()) + # drop filter and solution including the original ctx + newer = [n for n in newer if n and ctx.node() not in n] + if newer: + return base, tuple(ctx._repo[o] for o in newer[0]) + raise error.Abort("base of divergent changeset %s not found" % ctx, + hint='this case is not yet handled') + +shorttemplate = '[{rev}] {desc|firstline}\n' + +@eh.command( + '^previous', + [('B', 'move-bookmark', False, + _('move active bookmark after update')), + ('', 'merge', False, _('bring uncommitted change along')), + ('', 'no-topic', False, _('ignore topic and move topologically')), + ('n', 'dry-run', False, + _('do not perform actions, just print what would be done'))], + '[OPTION]...') +def cmdprevious(ui, repo, **opts): + """update to parent revision + + Displays the summary line of the destination for clarity.""" + wlock = None + dryrunopt = opts['dry_run'] + if not dryrunopt: + wlock = repo.wlock() + try: + wkctx = repo[None] + wparents = wkctx.parents() + if len(wparents) != 1: + raise error.Abort('merge in progress') + if not opts['merge']: + try: + cmdutil.bailifchanged(repo) + except error.Abort as exc: + exc.hint = _('do you want --merge?') + raise + + parents = wparents[0].parents() + topic = getattr(repo, 'currenttopic', '') + if topic and not opts.get("no_topic", False): + parents = [ctx for ctx in parents if ctx.topic() == topic] + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if not parents: + ui.warn(_('no parent in topic "%s"\n') % topic) + ui.warn(_('(do you want --no-topic)\n')) + elif len(parents) == 1: + p = parents[0] + bm = repo._activebookmark + shouldmove = opts.get('move_bookmark') and bm is not None + if dryrunopt: + ui.write(('hg update %s;\n' % p.rev())) + if shouldmove: + ui.write(('hg bookmark %s -r %s;\n' % (bm, p.rev()))) + else: + ret = hg.update(repo, p.rev()) + if not ret: + tr = lock = None + try: + lock = repo.lock() + tr = repo.transaction('previous') + if shouldmove: + repo._bookmarks[bm] = p.node() + repo._bookmarks.recordchange(tr) + else: + bookmarksmod.deactivate(repo) + tr.close() + finally: + lockmod.release(tr, lock) + + displayer.show(p) + return 0 + else: + for p in parents: + displayer.show(p) + ui.warn(_('multiple parents, explicitly update to one\n')) + return 1 + finally: + lockmod.release(wlock) + +@eh.command( + '^next', + [('B', 'move-bookmark', False, + _('move active bookmark after update')), + ('', 'merge', False, _('bring uncommitted change along')), + ('', 'evolve', False, _('evolve the next changeset if necessary')), + ('', 'no-topic', False, _('ignore topic and move topologically')), + ('n', 'dry-run', False, + _('do not perform actions, just print what would be done'))], + '[OPTION]...') +def cmdnext(ui, repo, **opts): + """update to next child revision + + Use the ``--evolve`` flag to evolve unstable children on demand. + + Displays the summary line of the destination for clarity. + """ + wlock = None + dryrunopt = opts['dry_run'] + if not dryrunopt: + wlock = repo.wlock() + try: + wkctx = repo[None] + wparents = wkctx.parents() + if len(wparents) != 1: + raise error.Abort('merge in progress') + if not opts['merge']: + try: + cmdutil.bailifchanged(repo) + except error.Abort as exc: + exc.hint = _('do you want --merge?') + raise + + children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()] + topic = getattr(repo, 'currenttopic', '') + filtered = [] + if topic and not opts.get("no_topic", False): + filtered = [ctx for ctx in children if ctx.topic() != topic] + # XXX N-square membership on children + children = [ctx for ctx in children if ctx not in filtered] + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if len(children) == 1: + c = children[0] + bm = repo._activebookmark + shouldmove = opts.get('move_bookmark') and bm is not None + if dryrunopt: + ui.write(('hg update %s;\n' % c.rev())) + if shouldmove: + ui.write(('hg bookmark %s -r %s;\n' % (bm, c.rev()))) + else: + ret = hg.update(repo, c.rev()) + if not ret: + lock = tr = None + try: + lock = repo.lock() + tr = repo.transaction('next') + if shouldmove: + repo._bookmarks[bm] = c.node() + repo._bookmarks.recordchange(tr) + else: + bookmarksmod.deactivate(repo) + tr.close() + finally: + lockmod.release(tr, lock) + displayer.show(c) + result = 0 + elif children: + ui.warn(_("ambigious next changeset:\n")) + for c in children: + displayer.show(c) + ui.warn(_('explicitly update to one of them\n')) + result = 1 + else: + aspchildren = _aspiringchildren(repo, [repo['.'].rev()]) + if topic: + filtered.extend(repo[c] for c in children + if repo[c].topic() != topic) + # XXX N-square membership on children + aspchildren = [ctx for ctx in aspchildren if ctx not in filtered] + if not opts['evolve'] or not aspchildren: + if filtered: + ui.warn(_('no children on topic "%s"\n') % topic) + ui.warn(_('do you want --no-topic\n')) + else: + ui.warn(_('no children\n')) + if aspchildren: + msg = _('(%i unstable changesets to be evolved here, ' + 'do you want --evolve?)\n') + ui.warn(msg % len(aspchildren)) + result = 1 + elif 1 < len(aspchildren): + ui.warn(_("ambigious next (unstable) changeset:\n")) + for c in aspchildren: + displayer.show(repo[c]) + ui.warn(_("(run 'hg evolve --rev REV' on one of them)\n")) + return 1 + else: + cmdutil.bailifchanged(repo) + result = _solveone(ui, repo, repo[aspchildren[0]], dryrunopt, + False, lambda: None, category='unstable') + if not result: + ui.status(_('working directory now at %s\n') % repo['.']) + return result + return 1 + return result + finally: + lockmod.release(wlock) + +def _reachablefrombookmark(repo, revs, bookmarks): + """filter revisions and bookmarks reachable from the given bookmark + yoinked from mq.py + """ + 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. + nodetobookmarks = {} + for mark, bnode in repomarks.iteritems(): + nodetobookmarks.setdefault(bnode, []).append(mark) + for marks in nodetobookmarks.values(): + if bookmarks.issuperset(marks): + rsrevs = repair.stripbmrevset(repo, 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') + for bookmark in bookmarks: + del repomarks[bookmark] + repomarks.recordchange(tr) + tr.close() + for bookmark in sorted(bookmarks): + repo.ui.write(_("bookmark '%s' deleted\n") % bookmark) + finally: + lockmod.release(tr, lock, wlock) + +def _getmetadata(**opts): + metadata = {} + date = opts.get('date') + user = opts.get('user') + if date: + metadata['date'] = '%i %i' % util.parsedate(date) + if user: + metadata['user'] = user + return metadata + +@eh.command( + '^prune|obsolete', + [('n', 'new', [], _("successor changeset (DEPRECATED)")), + ('s', 'succ', [], _("successor changeset")), + ('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" + " bookmark"))] + metadataopts, + _('[OPTION] [-r] REV...')) +# XXX -U --noupdate option to prevent wc update and or bookmarks update ? +def cmdprune(ui, repo, *revs, **opts): + """hide changesets by marking them obsolete + + Pruned changesets are obsolete with no successors. If they also have no + descendants, they are hidden (invisible to all commands). + + Non-obsolete descendants of pruned changesets become "unstable". Use :hg:`evolve` + to handle this situation. + + When you prune the parent of your working copy, Mercurial updates the working + copy to a non-obsolete parent. + + You can use ``--succ`` to tell Mercurial that a newer version (successor) of the + pruned changeset exists. Mercurial records successor revisions in obsolescence + markers. + + You can use the ``--biject`` option to specify a 1-1 mapping (bijection) between + revisions to pruned (precursor) and successor changesets. This option may be + removed in a future release (with the functionality provided automatically). + + If you specify multiple revisions in ``--succ``, you are recording a "split" and + must acknowledge it by passing ``--split``. Similarly, when you prune multiple + changesets with a single successor, you must pass the ``--fold`` option. + """ + revs = scmutil.revrange(repo, list(revs) + opts.get('rev')) + succs = opts['new'] + opts['succ'] + bookmarks = set(opts.get('bookmark')) + metadata = _getmetadata(**opts) + biject = opts.get('biject') + fold = opts.get('fold') + split = opts.get('split') + + options = [o for o in ('biject', 'fold', 'split') if opts.get(o)] + if 1 < len(options): + 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, repomarks, bookmarks) + + if not revs: + raise error.Abort(_('nothing to prune')) + + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction('prune') + # defines pruned changesets + precs = [] + revs.sort() + for p in revs: + cp = repo[p] + if not cp.mutable(): + # note: createmarkers() would have raised something anyway + raise error.Abort('cannot prune immutable changeset: %s' % cp, + hint="see 'hg help phases' for details") + precs.append(cp) + if not precs: + raise error.Abort('nothing to prune') + + if _disallowednewunstable(repo, revs): + raise error.Abort(_("cannot prune in the middle of a stack"), + hint=_("new unstable changesets are not allowed")) + + # defines successors changesets + sucs = scmutil.revrange(repo, succs) + sucs.sort() + 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" + hint = _("use --biject to mark a series as a replacement" + " for another") + raise error.Abort(msg, hint=hint) + elif biject and len(sucs) != len(precs): + msg = "Can't use %d successors for %d precursors" \ + % (len(sucs), len(precs)) + 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 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 error.Abort(msg) + elif biject: + relations = [(p, (s,)) for p, s in zip(precs, sucs)] + else: + relations = [(p, sucs) for p in precs] + + wdp = repo['.'] + + if len(sucs) == 1 and len(precs) == 1 and wdp in precs: + # '.' killed, so update to the successor + newnode = sucs[0] + else: + # update to an unkilled parent + newnode = wdp + + while newnode in precs or newnode.obsolete(): + newnode = newnode.parents()[0] + + if newnode.node() != wdp.node(): + if opts.get('keep', False): + # This is largely the same as the implementation in + # strip.stripcmd(). We might want to refactor this somewhere + # common at some point. + + # only reset the dirstate for files that would actually change + # between the working context and uctx + descendantrevs = repo.revs("%d::." % newnode.rev()) + changedfiles = [] + for rev in descendantrevs: + # 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) + dirstate.write(tr) + else: + bookactive = repo._activebookmark + # 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 bookmarks + if movebookmark: + bookmarksmod.deactivate(repo) + repo._bookmarks[bookactive] = newnode.node() + repo._bookmarks.recordchange(tr) + commands.update(ui, repo, newnode.rev()) + ui.status(_('working directory now at %s\n') % newnode) + if movebookmark: + bookmarksmod.activate(repo, bookactive) + + # update bookmarks + if bookmarks: + _deletebookmark(repo, repomarks, bookmarks) + + # create markers + obsolete.createmarkers(repo, relations, metadata=metadata) + + # informs that changeset have been pruned + ui.status(_('%i changesets pruned\n') % len(precs)) + + for ctx in repo.unfiltered().set('bookmark() and %ld', precs): + # used to be: + # + # ldest = list(repo.set('max((::%d) - obsolete())', ctx)) + # if ldest: + # c = ldest[0] + # + # but then revset took a lazy arrow in the knee and became much + # slower. The new forms makes as much sense and a much faster. + for dest in ctx.ancestors(): + if not dest.obsolete(): + updatebookmarks = _bookmarksupdater(repo, ctx.node(), tr) + updatebookmarks(dest.node()) + break + + tr.close() + finally: + lockmod.release(tr, lock, wlock) + +@eh.command( + 'amend|refresh', + [('A', 'addremove', None, + _('mark new/missing files as added/removed before committing')), + ('e', 'edit', False, _('invoke editor on commit messages')), + ('', 'close-branch', None, + _('mark a branch as closed, hiding it from the branch list')), + ('s', 'secret', None, _('use the secret phase for committing')), + ] + walkopts + commitopts + commitopts2 + commitopts3 + interactiveopt, + _('[OPTION]... [FILE]...')) +def amend(ui, repo, *pats, **opts): + """combine a changeset with updates and replace it with a new one + + Commits a new changeset incorporating both the changes to the given files + and all the changes from the current parent changeset into the repository. + + See :hg:`commit` for details about committing changes. + + If you don't specify -m, the parent's message will be reused. + + Behind the scenes, Mercurial first commits the update as a regular child + of the current parent. Then it creates a new commit on the parent's parents + with the updated contents. Then it changes the working copy parent to this + new combined changeset. Finally, the old changeset and its update are hidden + from :hg:`log` (unless you use --hidden with log). + + Returns 0 on success, 1 if nothing changed. + """ + opts = opts.copy() + edit = opts.pop('edit', False) + log = opts.get('logfile') + opts['amend'] = True + if not (edit or opts['message'] or log): + opts['message'] = repo['.'].description() + _resolveoptions(ui, opts) + _alias, commitcmd = cmdutil.findcmd('commit', commands.table) + return commitcmd[0](ui, repo, *pats, **opts) + + +def _touchedbetween(repo, source, dest, match=None): + touched = set() + for files in repo.status(source, dest, match=match)[:3]: + touched.update(files) + return touched + +def _commitfiltered(repo, ctx, match, target=None): + """Recommit ctx with changed files not in match. Return the new + node identifier, or None if nothing changed. + """ + base = ctx.p1() + if target is None: + target = base + # ctx + initialfiles = _touchedbetween(repo, base, ctx) + if base == target: + affected = set(f for f in initialfiles if match(f)) + newcontent = set() + else: + affected = _touchedbetween(repo, target, ctx, match=match) + newcontent = _touchedbetween(repo, target, base, match=match) + # The commit touchs all existing files + # + all file that needs a new content + # - the file affected bny uncommit with the same content than base. + files = (initialfiles - affected) | newcontent + if not newcontent and files == initialfiles: + return None + + # Filter copies + copied = copies.pathcopies(target, ctx) + copied = dict((dst, src) for dst, src in copied.iteritems() + if dst in files) + + def filectxfn(repo, memctx, path, contentctx=ctx, redirect=newcontent): + if path in redirect: + return filectxfn(repo, memctx, path, contentctx=target, redirect=()) + if path not in contentctx: + return None + fctx = contentctx[path] + flags = fctx.flags() + mctx = context.memfilectx(repo, fctx.path(), fctx.data(), + islink='l' in flags, + isexec='x' in flags, + copied=copied.get(path)) + return mctx + + new = context.memctx(repo, + parents=[base.node(), node.nullid], + text=ctx.description(), + files=files, + filectxfn=filectxfn, + user=ctx.user(), + date=ctx.date(), + extra=ctx.extra()) + # commitctx always create a new revision, no need to check + newid = repo.commitctx(new) + return newid + +def _uncommitdirstate(repo, oldctx, match): + """Fix the dirstate after switching the working directory from + oldctx to a copy of oldctx not containing changed files matched by + match. + """ + ctx = repo['.'] + ds = repo.dirstate + copies = dict(ds.copies()) + m, a, r = repo.status(oldctx.p1(), oldctx, match=match)[:3] + for f in m: + if ds[f] == 'r': + # modified + removed -> removed + continue + ds.normallookup(f) + + for f in a: + if ds[f] == 'r': + # added + removed -> unknown + ds.drop(f) + elif ds[f] != 'a': + ds.add(f) + + for f in r: + if ds[f] == 'a': + # removed + added -> normal + ds.normallookup(f) + elif ds[f] != 'r': + ds.remove(f) + + # Merge old parent and old working dir copies + oldcopies = {} + for f in (m + a): + src = oldctx[f].renamed() + if src: + oldcopies[f] = src[0] + oldcopies.update(copies) + copies = dict((dst, oldcopies.get(src, src)) + for dst, src in oldcopies.iteritems()) + # Adjust the dirstate copies + for dst, src in copies.iteritems(): + if (src not in ctx or dst in ctx or ds[dst] != 'a'): + src = None + ds.copy(src, dst) + +@eh.command( + '^uncommit', + [('a', 'all', None, _('uncommit all changes when no arguments given')), + ('r', 'rev', '', _('revert commit content to REV instead')), + ] + commands.walkopts, + _('[OPTION]... [NAME]')) +def uncommit(ui, repo, *pats, **opts): + """move changes from parent revision to working directory + + Changes to selected files in the checked out revision appear again as + uncommitted changed in the working directory. A new revision + without the selected changes is created, becomes the checked out + revision, and obsoletes the previous one. + + The --include option specifies patterns to uncommit. + The --exclude option specifies patterns to keep in the commit. + + The --rev argument let you change the commit file to a content of another + revision. It still does not change the content of your file in the working + directory. + + Return 0 if changed files are uncommitted. + """ + + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + wctx = repo[None] + if len(wctx.parents()) <= 0: + raise error.Abort(_("cannot uncommit null changeset")) + if len(wctx.parents()) > 1: + raise error.Abort(_("cannot uncommit while merging")) + old = repo['.'] + if old.phase() == phases.public: + raise error.Abort(_("cannot rewrite immutable changeset")) + if len(old.parents()) > 1: + raise error.Abort(_("cannot uncommit merge changeset")) + oldphase = old.phase() + + rev = None + if opts.get('rev'): + rev = scmutil.revsingle(repo, opts.get('rev')) + ctx = repo[None] + if ctx.p1() == rev or ctx.p2() == rev: + raise error.Abort(_("cannot uncommit to parent changeset")) + + onahead = old.rev() in repo.changelog.headrevs() + disallowunstable = not obsolete.isenabled(repo, + obsolete.allowunstableopt) + if disallowunstable and not onahead: + raise error.Abort(_("cannot uncommit in the middle of a stack")) + + # Recommit the filtered changeset + tr = repo.transaction('uncommit') + updatebookmarks = _bookmarksupdater(repo, old.node(), tr) + newid = None + includeorexclude = opts.get('include') or opts.get('exclude') + if (pats or includeorexclude or opts.get('all')): + match = scmutil.match(old, pats, opts) + newid = _commitfiltered(repo, old, match, target=rev) + if newid is None: + 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],))]) + phases.retractboundary(repo, tr, oldphase, [newid]) + repo.dirstate.beginparentchange() + repo.dirstate.setparents(newid, node.nullid) + _uncommitdirstate(repo, old, match) + repo.dirstate.endparentchange() + updatebookmarks(newid) + if not repo[newid].files(): + ui.warn(_("new changeset is empty\n")) + ui.status(_("(use 'hg prune .' to remove it)\n")) + tr.close() + finally: + lockmod.release(tr, lock, wlock) + +@eh.wrapcommand('commit') +def commitwrapper(orig, ui, repo, *arg, **kwargs): + tr = None + if kwargs.get('amend', False): + wlock = lock = None + else: + wlock = repo.wlock() + lock = repo.lock() + try: + obsoleted = kwargs.get('obsolete', []) + if obsoleted: + obsoleted = repo.set('%lr', obsoleted) + result = orig(ui, repo, *arg, **kwargs) + if not result: # commit succeeded + new = repo['-1'] + oldbookmarks = [] + markers = [] + for old in obsoleted: + oldbookmarks.extend(repo.nodebookmarks(old.node())) + markers.append((old, (new,))) + if markers: + obsolete.createmarkers(repo, markers) + for book in oldbookmarks: + repo._bookmarks[book] = new.node() + if oldbookmarks: + 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(tr, lock, wlock) + +@eh.command( + '^split', + [('r', 'rev', [], _("revision to split")), + ] + commitopts + commitopts2, + _('hg split [OPTION]... [-r] REV')) +def cmdsplit(ui, repo, *revs, **opts): + """split a changeset into smaller changesets + + By default, split the current revision by prompting for all its hunks to be + redistributed into new changesets. + + Use --rev to split a given changeset instead. + """ + tr = wlock = lock = None + newcommits = [] + + 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() + cmdutil.bailifchanged(repo) + tr = repo.transaction('split') + ctx = repo[rev] + r = ctx.rev() + disallowunstable = not obsolete.isenabled(repo, + obsolete.allowunstableopt) + if disallowunstable: + # XXX We should check head revs + if repo.revs("(%d::) - %d", rev, rev): + raise error.Abort(_("cannot split commit: %s not a head") % ctx) + + if len(ctx.parents()) > 1: + raise error.Abort(_("cannot split merge commits")) + prev = ctx.p1() + bmupdate = _bookmarksupdater(repo, ctx.node(), tr) + bookactive = repo._activebookmark + if bookactive is not None: + repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark) + bookmarksmod.deactivate(repo) + hg.update(repo, prev) + + commands.revert(ui, repo, rev=r, all=True) + + def haschanges(): + modified, added, removed, deleted = repo.status()[:4] + return modified or added or removed or deleted + msg = ("HG: This is the original pre-split commit message. " + "Edit it as appropriate.\n\n") + msg += ctx.description() + opts['message'] = msg + opts['edit'] = True + while haschanges(): + pats = () + cmdutil.dorecord(ui, repo, commands.commit, 'commit', False, + cmdutil.recordfilter, *pats, **opts) + # TODO: Does no seem like the best way to do this + # We should make dorecord return the newly created commit + newcommits.append(repo['.']) + if haschanges(): + if ui.prompt('Done splitting? [yN]', default='n') == 'y': + commands.commit(ui, repo, **opts) + newcommits.append(repo['.']) + break + else: + ui.status(_("no more change to split\n")) + + if newcommits: + tip = repo[newcommits[-1]] + bmupdate(tip.node()) + if bookactive is not None: + bookmarksmod.activate(repo, bookactive) + obsolete.createmarkers(repo, [(repo[r], newcommits)]) + tr.close() + finally: + lockmod.release(tr, lock, wlock) + + +@eh.wrapcommand('strip', extension='strip', opts=[ + ('', 'bundle', None, _("delete the commit entirely and move it to a " + "backup bundle")), + ]) +def stripwrapper(orig, ui, repo, *revs, **kwargs): + if (not ui.configbool('experimental', 'prunestrip') or + kwargs.get('bundle', False)): + return orig(ui, repo, *revs, **kwargs) + + if kwargs.get('force'): + ui.warn(_("warning: --force has no effect during strip with evolve " + "enabled\n")) + if kwargs.get('no_backup', False): + ui.warn(_("warning: --no-backup has no effect during strips with " + "evolve enabled\n")) + + revs = list(revs) + kwargs.pop('rev', []) + revs = set(scmutil.revrange(repo, revs)) + revs = repo.revs("(%ld)::", revs) + kwargs['rev'] = [] + kwargs['new'] = [] + kwargs['succ'] = [] + kwargs['biject'] = False + return cmdprune(ui, repo, *revs, **kwargs) + +@eh.command( + '^touch', + [('r', 'rev', [], 'revision to update'), + ('D', 'duplicate', False, + '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 + + This is used to "resurrect" changesets + """ + duplicate = opts['duplicate'] + allowdivergence = opts['allowdivergence'] + revs = list(revs) + revs.extend(opts['rev']) + if not revs: + revs = ['.'] + revs = scmutil.revrange(repo, revs) + if not revs: + ui.write_err('no revision to touch\n') + return 1 + if not duplicate and repo.revs('public() and %ld', revs): + raise error.Abort("can't touch public revision") + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction('touch') + revs.sort() # ensure parent are run first + newmapping = {} + for r in revs: + ctx = repo[r] + extra = ctx.extra().copy() + extra['__touch-noise__'] = random.randint(0, 0xffffffff) + # search for touched parent + p1 = ctx.p1().node() + 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]) + if ctx in repo[None].parents(): + repo.dirstate.beginparentchange() + repo.dirstate.setparents(new, node.nullid) + repo.dirstate.endparentchange() + tr.close() + finally: + lockmod.release(tr, lock, wlock) + +@eh.command( + '^fold|squash', + [('r', 'rev', [], _("revision to fold")), + ('', 'exact', None, _("only fold specified revisions")), + ('', 'from', None, _("fold revisions linearly to working copy parent")) + ] + commitopts + commitopts2, + _('hg fold [OPTION]... [-r] REV')) +def fold(ui, repo, *revs, **opts): + """fold multiple revisions into a single one + + With --from, folds all the revisions linearly between the given revisions + and the parent of the working directory. + + With --exact, folds only the specified revisions while ignoring the + parent of the working directory. In this case, the given revisions must + form a linear unbroken chain. + + .. container:: verbose + + Some examples: + + - Fold the current revision with its parent:: + + hg fold --from .^ + + - Fold all draft revisions with working directory parent:: + + hg fold --from 'draft()' + + See :hg:`help phases` for more about draft revisions and + :hg:`help revsets` for more about the `draft()` keyword + + - Fold revisions between 3 and 6 with the working directory parent:: + + hg fold --from 3::6 + + - Fold revisions 3 and 4: + + hg fold "3 + 4" --exact + + - Only fold revisions linearly between foo and @:: + + hg fold foo::@ --exact + """ + revs = list(revs) + revs.extend(opts['rev']) + if not revs: + raise error.Abort(_('no revisions specified')) + + revs = scmutil.revrange(repo, revs) + + if opts['from'] and opts['exact']: + raise error.Abort(_('cannot use both --from and --exact')) + elif opts['from']: + # Try to extend given revision starting from the working directory + extrevs = repo.revs('(%ld::.) or (.::%ld)', revs, revs) + discardedrevs = [r for r in revs if r not in extrevs] + if discardedrevs: + msg = _("cannot fold non-linear revisions") + hint = _("given revisions are unrelated to parent of working" + " directory") + raise error.Abort(msg, hint=hint) + revs = extrevs + elif opts['exact']: + # Nothing to do; "revs" is already set correctly + pass + else: + raise error.Abort(_('must specify either --from or --exact')) + + if not revs: + raise error.Abort(_('specified revisions evaluate to an empty set'), + hint=_('use different revision arguments')) + elif len(revs) == 1: + ui.write_err(_('single revision specified, nothing to fold\n')) + return 1 + + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + + root, head = _foldcheck(repo, revs) + + tr = repo.transaction('fold') + try: + commitopts = opts.copy() + allctx = [repo[r] for r in revs] + targetphase = max(c.phase() for c in allctx) + + if commitopts.get('message') or commitopts.get('logfile'): + commitopts['edit'] = False + else: + msgs = ["HG: This is a fold of %d changesets." % len(allctx)] + msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % + (c.rev(), c.description()) for c in allctx] + commitopts['message'] = "\n".join(msgs) + commitopts['edit'] = True + + newid, unusedvariable = rewrite(repo, root, allctx, head, + [root.p1().node(), + root.p2().node()], + commitopts=commitopts) + phases.retractboundary(repo, tr, targetphase, [newid]) + obsolete.createmarkers(repo, [(ctx, (repo[newid],)) + for ctx in allctx]) + tr.close() + finally: + tr.release() + ui.status('%i changesets folded\n' % len(revs)) + if repo['.'].rev() in revs: + hg.update(repo, newid) + finally: + lockmod.release(lock, wlock) + +@eh.command( + '^metaedit', + [('r', 'rev', [], _("revision to edit")), + ('', 'fold', None, _("also fold specified revisions into one")), + ] + commitopts + commitopts2, + _('hg metaedit [OPTION]... [-r] [REV]')) +def metaedit(ui, repo, *revs, **opts): + """edit commit information + + Edits the commit information for the specified revisions. By default, edits + commit information for the working directory parent. + + With --fold, also folds multiple revisions into one if necessary. In this + case, the given revisions must form a linear unbroken chain. + + .. container:: verbose + + Some examples: + + - Edit the commit message for the working directory parent:: + + hg metaedit + + - Change the username for the working directory parent:: + + hg metaedit --user 'New User ' + + - Combine all draft revisions that are ancestors of foo but not of @ into + one:: + + hg metaedit --fold 'draft() and only(foo,@)' + + See :hg:`help phases` for more about draft revisions, and + :hg:`help revsets` for more about the `draft()` and `only()` keywords. + """ + revs = list(revs) + revs.extend(opts['rev']) + if not revs: + if opts['fold']: + raise error.Abort(_('revisions must be specified with --fold')) + revs = ['.'] + + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + + revs = scmutil.revrange(repo, revs) + if not opts['fold'] and len(revs) > 1: + # TODO: handle multiple revisions. This is somewhat tricky because + # if we want to edit a series of commits: + # + # a ---- b ---- c + # + # we need to rewrite a first, then directly rewrite b on top of the + # new a, then rewrite c on top of the new b. So we need to handle + # revisions in topological order. + raise error.Abort(_('editing multiple revisions without --fold is ' + 'not currently supported')) + + if opts['fold']: + root, head = _foldcheck(repo, revs) + else: + if repo.revs("%ld and public()", revs): + raise error.Abort(_('cannot edit commit information for public ' + 'revisions')) + newunstable = _disallowednewunstable(repo, revs) + if newunstable: + msg = _('cannot edit commit information in the middle' + ' of a stack') + hint = _('%s will become unstable and new unstable changes' + ' are not allowed') + hint %= repo[newunstable.first()] + raise error.Abort(msg, hint=hint) + root = head = repo[revs.first()] + + wctx = repo[None] + p1 = wctx.p1() + tr = repo.transaction('metaedit') + newp1 = None + try: + commitopts = opts.copy() + allctx = [repo[r] for r in revs] + targetphase = max(c.phase() for c in allctx) + + if commitopts.get('message') or commitopts.get('logfile'): + commitopts['edit'] = False + else: + if opts['fold']: + msgs = ["HG: This is a fold of %d changesets." % len(allctx)] + msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % + (c.rev(), c.description()) for c in allctx] + else: + msgs = [head.description()] + commitopts['message'] = "\n".join(msgs) + commitopts['edit'] = True + + # TODO: if the author and message are the same, don't create a new + # hash. Right now we create a new hash because the date can be + # different. + newid, created = rewrite(repo, root, allctx, head, + [root.p1().node(), root.p2().node()], + commitopts=commitopts) + if created: + if p1.rev() in revs: + newp1 = newid + phases.retractboundary(repo, tr, targetphase, [newid]) + obsolete.createmarkers(repo, [(ctx, (repo[newid],)) + for ctx in allctx]) + else: + ui.status(_("nothing changed\n")) + tr.close() + finally: + tr.release() + + if opts['fold']: + ui.status('%i changesets folded\n' % len(revs)) + if newp1 is not None: + hg.update(repo, newp1) + finally: + lockmod.release(lock, wlock) + +def _foldcheck(repo, revs): + roots = repo.revs('roots(%ld)', revs) + if len(roots) > 1: + raise error.Abort(_("cannot fold non-linear revisions " + "(multiple roots given)")) + root = repo[roots.first()] + if root.phase() <= phases.public: + raise error.Abort(_("cannot fold public revisions")) + heads = repo.revs('heads(%ld)', revs) + if len(heads) > 1: + raise error.Abort(_("cannot fold non-linear revisions " + "(multiple heads given)")) + head = repo[heads.first()] + if _disallowednewunstable(repo, revs): + msg = _("cannot fold chain not ending with a head or with branching") + hint = _("new unstable changesets are not allowed") + raise error.Abort(msg, hint=hint) + return root, head + +def _disallowednewunstable(repo, revs): + allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt) + if allowunstable: + return revset.baseset() + return repo.revs("(%ld::) - %ld", revs, revs) + +@eh.wrapcommand('graft') +def graftwrapper(orig, ui, repo, *revs, **kwargs): + kwargs = dict(kwargs) + revs = list(revs) + kwargs.get('rev', []) + kwargs['rev'] = [] + obsoleted = kwargs.setdefault('obsolete', []) + + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + if kwargs.get('old_obsolete'): + if kwargs.get('continue'): + obsoleted.extend(repo.vfs.read('graftstate').splitlines()) + else: + obsoleted.extend(revs) + # convert obsolete target into revs to avoid alias joke + obsoleted[:] = [str(i) for i in repo.revs('%lr', obsoleted)] + if obsoleted and len(revs) > 1: + + raise error.Abort(_('cannot graft multiple revisions while ' + 'obsoleting (for now).')) + + return commitwrapper(orig, ui, repo, *revs, **kwargs) + finally: + lockmod.release(lock, wlock) + +@eh.extsetup +def oldevolveextsetup(ui): + for cmd in ['prune', 'uncommit', 'touch', 'fold']: + try: + entry = extensions.wrapcommand(cmdtable, cmd, + warnobserrors) + except error.UnknownCommand: + # Commands may be disabled + continue + + entry = cmdutil.findcmd('commit', commands.table)[1] + entry[1].append(('o', 'obsolete', [], + _("make commit obsolete this revision (DEPRECATED)"))) + entry = cmdutil.findcmd('graft', commands.table)[1] + entry[1].append(('o', 'obsolete', [], + _("make graft obsoletes this revision (DEPRECATED)"))) + entry[1].append(('O', 'old-obsolete', False, + _("make graft obsoletes its source (DEPRECATED)"))) + +@eh.wrapfunction(obsolete, '_checkinvalidmarkers') +def _checkinvalidmarkers(orig, markers): + """search for marker with invalid data and raise error if needed + + Exist as a separated function to allow the evolve extension for a more + subtle handling. + """ + if 'debugobsconvert' in sys.argv: + return + for mark in markers: + if node.nullid in mark[1]: + msg = _('bad obsolescence marker detected: invalid successors nullid') + hint = _('You should run `hg debugobsconvert`') + raise error.Abort(msg, hint=hint) + +@eh.command( + 'debugobsconvert', + [('', 'new-format', obsexchange._bestformat, _('Destination format for markers.'))], + '') +def debugobsconvert(ui, repo, new_format): + 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 error.Abort(msg) + f = repo.svfs('obsstore', 'wb', atomictemp=True) + known = set() + markers = [] + for m in origmarkers: + # filter out invalid markers + if nullid in m[1]: + m = list(m) + m[1] = tuple(s for s in m[1] if s != nullid) + m = tuple(m) + if m in known: + continue + known.add(m) + markers.append(m) + ui.write(_('Old store is version %d, will rewrite in version %d\n') % ( + repo.obsstore._version, new_format)) + map(f.write, obsolete.encodemarkers(markers, True, new_format)) + f.close() + ui.write(_('Done!\n')) + + +def _helploader(ui): + return help.gettext(evolutionhelptext) + +@eh.uisetup +def _setuphelp(ui): + for entry in help.helptable: + if entry[0] == "evolution": + break + else: + 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, 'evolve') + # Commit might fail if unresolved files exist + nodenew = repo.commit(text=commitmsg, user=orig.user(), + date=orig.date(), extra=extra) + finally: + repo.ui.restoreconfig(backup) + return nodenew + +def _finalizerelocate(repo, orig, dest, nodenew, tr): + destbookmarks = repo.nodebookmarks(dest.node()) + nodesrc = orig.node() + destphase = repo[nodesrc].phase() + oldbookmarks = repo.nodebookmarks(nodesrc) + 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 -C .' to abort")) + cmdutil.unfinishedstates.append(data) + +@eh.wrapfunction(hg, 'clean') +def clean(orig, repo, *args, **kwargs): + ret = orig(repo, *args, **kwargs) + util.unlinkpath(repo.vfs.join('evolvestate'), ignoremissing=True) + return ret + +def _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 as 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: + msg = _('unknown evolvestate version %i') % version + raise error.Abort(msg, hint=_('upgrade your evolve')) + records = [] + data = f.read() + off = 0 + end = len(data) + while off < end: + rtype = data[off] + off += 1 + length = _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.vfs.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, + branchmerge=False, + force=True) + if repo._activebookmark: + repo.ui.status(_("(leaving bookmark %s)\n") % repo._activebookmark) + bookmarksmod.deactivate(repo) + if keepbranch: + repo.dirstate.setbranch(orig.branch()) + if util.safehasattr(repo, 'currenttopic'): + # uurrgs + # there no other topic setter yet + if not orig.topic() and repo.vfs.exists('topic'): + repo.vfs.unlink('topic') + else: + with repo.vfs.open('topic', 'w') as f: + f.write(orig.topic()) + + return merge.graft(repo, orig, pctx, ['local', 'graft'], True) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/checkheads.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/checkheads.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,281 @@ +# Code dedicated to the postprocessing new heads check with obsolescence +# +# Copyright 2017 Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import functools + +from mercurial import ( + discovery, + error, + extensions, + node as nodemod, + phases, + util, +) + +from mercurial.i18n import _ + +from . import exthelper + +nullid = nodemod.nullid +short = nodemod.short +_headssummary = discovery._headssummary +_oldheadssummary = discovery._oldheadssummary +_nowarnheads = discovery._nowarnheads + +eh = exthelper.exthelper() + +@eh.uisetup +def setupcheckheadswrapper(ui): + if util.safehasattr(discovery, '_postprocessobsolete'): + extensions.wrapfunction(discovery, '_postprocessobsolete', + checkheadslightoverlay) + else: + extensions.wrapfunction(discovery, 'checkheads', + checkheadsfulloverlay) + +# have dedicated wrapper to keep the rest as close as core as possible +def checkheadsfulloverlay(orig, pushop): + if pushop.repo.obsstore: + return corecheckheads(pushop) + else: + return orig(pushop) + +def checkheadslightoverlay(orig, *args, **kwargs): + return _postprocessobsolete(*args, **kwargs) + +# copied from mercurial.discovery.checkheads as in a5bad127128d (4.1) +# +# The only differences are: +# * the _postprocessobsolete section have been extracted, +# * minor test adjustment to please flake8 +def corecheckheads(pushop): + """Check that a push won't add any outgoing head + + raise Abort error and display ui message as needed. + """ + + repo = pushop.repo.unfiltered() + remote = pushop.remote + outgoing = pushop.outgoing + remoteheads = pushop.remoteheads + newbranch = pushop.newbranch + inc = bool(pushop.incoming) + + # Check for each named branch if we're creating new remote heads. + # To be a remote head after push, node must be either: + # - unknown locally + # - a local outgoing head descended from update + # - a remote head that's known locally and not + # ancestral to an outgoing head + if remoteheads == [nullid]: + # remote is empty, nothing to check. + return + + if remote.capable('branchmap'): + headssum = _headssummary(repo, remote, outgoing) + else: + headssum = _oldheadssummary(repo, remoteheads, outgoing, inc) + newbranches = [branch for branch, heads in headssum.iteritems() + if heads[0] is None] + # 1. Check for new branches on the remote. + if newbranches and not newbranch: # new branch requires --new-branch + branchnames = ', '.join(sorted(newbranches)) + raise error.Abort(_("push creates new remote branches: %s!") + % branchnames, + hint=_("use 'hg push --new-branch' to create" + " new remote branches")) + + # 2. Find heads that we need not warn about + nowarnheads = _nowarnheads(pushop) + + # 3. Check for new heads. + # If there are more heads after the push than before, a suitable + # error message, depending on unsynced status, is displayed. + errormsg = None + # If there is no obsstore, allfuturecommon won't be used, so no + # need to compute it. + if repo.obsstore: + allmissing = set(outgoing.missing) + cctx = repo.set('%ld', outgoing.common) + allfuturecommon = set(c.node() for c in cctx) + allfuturecommon.update(allmissing) + for branch, heads in sorted(headssum.iteritems()): + remoteheads, newheads, unsyncedheads = heads + candidate_newhs = set(newheads) + # add unsynced data + if remoteheads is None: + oldhs = set() + else: + oldhs = set(remoteheads) + oldhs.update(unsyncedheads) + candidate_newhs.update(unsyncedheads) + dhs = None # delta heads, the new heads on branch + if not repo.obsstore: + discardedheads = set() + newhs = candidate_newhs + else: + newhs, discardedheads = _postprocessobsolete(pushop, + allfuturecommon, + candidate_newhs) + unsynced = sorted(h for h in unsyncedheads if h not in discardedheads) + if unsynced: + if None in unsynced: + # old remote, no heads data + heads = None + elif len(unsynced) <= 4 or repo.ui.verbose: + heads = ' '.join(short(h) for h in unsynced) + else: + heads = (' '.join(short(h) for h in unsynced[:4]) + + ' ' + _("and %s others") % (len(unsynced) - 4)) + if heads is None: + repo.ui.status(_("remote has heads that are " + "not known locally\n")) + elif branch is None: + repo.ui.status(_("remote has heads that are " + "not known locally: %s\n") % heads) + else: + repo.ui.status(_("remote has heads on branch '%s' that are " + "not known locally: %s\n") % (branch, heads)) + if remoteheads is None: + if len(newhs) > 1: + dhs = list(newhs) + if errormsg is None: + errormsg = (_("push creates new branch '%s' " + "with multiple heads") % (branch)) + hint = _("merge or" + " see 'hg help push' for details about" + " pushing new heads") + elif len(newhs) > len(oldhs): + # remove bookmarked or existing remote heads from the new heads list + dhs = sorted(newhs - nowarnheads - oldhs) + if dhs: + if errormsg is None: + if branch not in ('default', None): + errormsg = _("push creates new remote head %s " + "on branch '%s'!") % (short(dhs[0]), branch) + elif repo[dhs[0]].bookmarks(): + errormsg = (_("push creates new remote head %s " + "with bookmark '%s'!") + % (short(dhs[0]), repo[dhs[0]].bookmarks()[0])) + else: + errormsg = _("push creates new remote head %s!" + ) % short(dhs[0]) + if unsyncedheads: + hint = _("pull and merge or" + " see 'hg help push' for details about" + " pushing new heads") + else: + hint = _("merge or" + " see 'hg help push' for details about" + " pushing new heads") + if branch is None: + repo.ui.note(_("new remote heads:\n")) + else: + repo.ui.note(_("new remote heads on branch '%s':\n") % branch) + for h in dhs: + repo.ui.note((" %s\n") % short(h)) + if errormsg: + raise error.Abort(errormsg, hint=hint) + +def _postprocessobsolete(pushop, futurecommon, candidate): + """post process the list of new heads with obsolescence information + + Exist as a subfunction to contains the complexity and allow extensions to + experiment with smarter logic. + Returns (newheads, discarded_heads) tuple + """ + # remove future heads which are actually obsoleted by another + # pushed element: + # + # known issue + # + # * We "silently" skip processing on all changeset unknown locally + # + # * if is public on the remote, it won't be affected by obsolete + # marker and a new is created + repo = pushop.repo + unfi = repo.unfiltered() + tonode = unfi.changelog.node + public = phases.public + getphase = unfi._phasecache.phase + ispublic = (lambda r: getphase(unfi, r) == public) + hasoutmarker = functools.partial(pushingmarkerfor, unfi.obsstore, futurecommon) + successorsmarkers = unfi.obsstore.successors + newhs = set() + discarded = set() + # I leave the print in the code because they are so handy at debugging + # and I keep getting back to this piece of code. + # + localcandidate = set() + unknownheads = set() + for h in candidate: + if h in unfi: + localcandidate.add(h) + else: + if successorsmarkers.get(h) is not None: + msg = ('checkheads: remote head unknown locally has' + ' local marker: %s\n') + repo.ui.debug(msg % nodemod.hex(h)) + unknownheads.add(h) + if len(localcandidate) == 1: + return unknownheads | set(candidate), set() + while localcandidate: + nh = localcandidate.pop() + # run this check early to skip the revset on the whole branch + if (nh in futurecommon + or unfi[nh].phase() <= public): + newhs.add(nh) + continue + # XXX there is a corner case if there is a merge in the branch. we + # might end up with -more- heads. However, these heads are not "added" + # by the push, but more by the "removal" on the remote so I think is a + # okay to ignore them, + branchrevs = unfi.revs('only(%n, (%ln+%ln))', + nh, localcandidate, newhs) + branchnodes = [tonode(r) for r in branchrevs] + + # The branch will still exist on the remote if + # * any part of it is public, + # * any part of it is considered part of the result by previous logic, + # * if we have no markers to push to obsolete it. + if (any(ispublic(r) for r in branchrevs) + or any(n in futurecommon for n in branchnodes) + or any(not hasoutmarker(n) for n in branchnodes)): + newhs.add(nh) + else: + discarded.add(nh) + newhs |= unknownheads + return newhs, discarded + +def pushingmarkerfor(obsstore, pushset, node): + """True if some markers are to be pushed for node + + We cannot just look in to the pushed obsmarkers from the pushop because + discover might have filtered relevant markers. In addition listing all + markers relevant to all changeset in the pushed set would be too expensive. + + The is probably some cache opportunity in this function. but it would + requires a two dimentions stack. + """ + successorsmarkers = obsstore.successors + stack = [node] + seen = set(stack) + while stack: + current = stack.pop() + if current in pushset: + return True + markers = successorsmarkers.get(current, ()) + # markers fields = ('prec', 'succs', 'flag', 'meta', 'date', 'parents') + for m in markers: + nexts = m[1] # successors + if not nexts: # this is a prune marker + nexts = m[5] # parents + for n in nexts: + if n not in seen: + seen.add(n) + stack.append(n) + return False diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/debugcmd.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/debugcmd.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,129 @@ +# Code dedicated to debug commands around evolution +# +# Copyright 2017 Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +# Status: Ready to Upstream +# +# * We could have the same code in core as `hg debugobsolete --stat`, +# * We probably want a way for the extension to hook in for extra data. + +from mercurial import node + +from mercurial.i18n import _ + +from . import exthelper + +eh = exthelper.exthelper() + +@eh.command('debugobsstorestat', [], '') +def cmddebugobsstorestat(ui, repo): + """print statistics about obsolescence markers in the repo""" + def _updateclustermap(nodes, mark, clustersmap): + c = (set(nodes), set([mark])) + toproceed = set(nodes) + while toproceed: + n = toproceed.pop() + other = clustersmap.get(n) + if (other is not None + and other is not c): + other[0].update(c[0]) + other[1].update(c[1]) + for on in c[0]: + if on in toproceed: + continue + clustersmap[on] = other + c = other + clustersmap[n] = c + + store = repo.obsstore + unfi = repo.unfiltered() + nm = unfi.changelog.nodemap + ui.write(_('markers total: %9i\n') % len(store._all)) + sucscount = [0, 0, 0, 0] + known = 0 + parentsdata = 0 + metakeys = {} + # node -> cluster mapping + # a cluster is a (set(nodes), set(markers)) tuple + clustersmap = {} + # same data using parent information + pclustersmap = {} + for mark in store: + if mark[0] in nm: + known += 1 + nbsucs = len(mark[1]) + sucscount[min(nbsucs, 3)] += 1 + meta = mark[3] + for key, value in meta: + metakeys.setdefault(key, 0) + metakeys[key] += 1 + meta = dict(meta) + parents = [meta.get('p1'), meta.get('p2')] + parents = [node.bin(p) for p in parents if p is not None] + if parents: + parentsdata += 1 + # cluster handling + nodes = set(mark[1]) + nodes.add(mark[0]) + _updateclustermap(nodes, mark, clustersmap) + # same with parent data + nodes.update(parents) + _updateclustermap(nodes, mark, pclustersmap) + + # freezing the result + for c in clustersmap.values(): + fc = (frozenset(c[0]), frozenset(c[1])) + for n in fc[0]: + clustersmap[n] = fc + # same with parent data + for c in pclustersmap.values(): + 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)) + # 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])) + # meta data info + ui.write((' available keys:\n')) + for key in sorted(metakeys): + 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(' 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]))) + median = len(allclusters[nbcluster // 2][1]) + ui.write((' median length: %9i\n' % median)) + mean = sum(len(x[1]) for x in allclusters) // nbcluster + 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(' 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]))) + median = len(allpclusters[nbcluster // 2][1]) + ui.write((' median length: %9i\n' % median)) + mean = sum(len(x[1]) for x in allpclusters) // nbcluster + ui.write((' mean length: %9i\n' % mean)) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/exthelper.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/exthelper.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,259 @@ +##################################################################### +### Extension helper ### +##################################################################### + +from mercurial import ( + cmdutil, + commands, + extensions, + revset, + templatekw, +) + +class exthelper(object): + """Helper for modular extension setup + + A single helper should be instantiated for each extension. Helper + methods are then used as decorators for various purpose. + + All decorators return the original function and may be chained. + """ + + def __init__(self): + self._uicallables = [] + self._extcallables = [] + self._repocallables = [] + self._revsetsymbols = [] + self._templatekws = [] + self._commandwrappers = [] + self._extcommandwrappers = [] + self._functionwrappers = [] + self._duckpunchers = [] + self.cmdtable = {} + self.command = cmdutil.command(self.cmdtable) + + def merge(self, other): + self._uicallables.extend(other._uicallables) + self._extcallables.extend(other._extcallables) + self._repocallables.extend(other._repocallables) + self._revsetsymbols.extend(other._revsetsymbols) + self._templatekws.extend(other._templatekws) + self._commandwrappers.extend(other._commandwrappers) + self._extcommandwrappers.extend(other._extcommandwrappers) + self._functionwrappers.extend(other._functionwrappers) + self._duckpunchers.extend(other._duckpunchers) + self.cmdtable.update(other.cmdtable) + + def final_uisetup(self, ui): + """Method to be used as the extension uisetup + + The following operations belong here: + + - Changes to ui.__class__ . The ui object that will be used to run the + command has not yet been created. Changes made here will affect ui + objects created after this, and in particular the ui that will be + passed to runcommand + - Command wraps (extensions.wrapcommand) + - Changes that need to be visible to other extensions: because + initialization occurs in phases (all extensions run uisetup, then all + run extsetup), a change made here will be visible to other extensions + during extsetup + - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch + module members + - Setup of pre-* and post-* hooks + - pushkey setup + """ + for cont, funcname, func in self._duckpunchers: + setattr(cont, funcname, func) + for command, wrapper, opts in self._commandwrappers: + entry = extensions.wrapcommand(commands.table, command, wrapper) + if opts: + for short, long, val, msg in opts: + entry[1].append((short, long, val, msg)) + for cont, funcname, wrapper in self._functionwrappers: + extensions.wrapfunction(cont, funcname, wrapper) + for c in self._uicallables: + c(ui) + + def final_extsetup(self, ui): + """Method to be used as a the extension extsetup + + The following operations belong here: + + - Changes depending on the status of other extensions. (if + extensions.find('mq')) + - Add a global option to all commands + - Register revset functions + """ + knownexts = {} + for name, symbol in self._revsetsymbols: + revset.symbols[name] = symbol + for name, kw in self._templatekws: + templatekw.keywords[name] = kw + for ext, command, wrapper, opts in self._extcommandwrappers: + if ext not in knownexts: + try: + e = extensions.find(ext) + except KeyError: + # Extension isn't enabled, so don't bother trying to wrap + # it. + continue + knownexts[ext] = e.cmdtable + entry = extensions.wrapcommand(knownexts[ext], command, wrapper) + if opts: + for short, long, val, msg in opts: + entry[1].append((short, long, val, msg)) + + for c in self._extcallables: + c(ui) + + def final_reposetup(self, ui, repo): + """Method to be used as the extension reposetup + + The following operations belong here: + + - All hooks but pre-* and post-* + - Modify configuration variables + - Changes to repo.__class__, repo.dirstate.__class__ + """ + for c in self._repocallables: + c(ui, repo) + + def uisetup(self, call): + """Decorated function will be executed during uisetup + + example:: + + @eh.uisetup + def setupbabar(ui): + print 'this is uisetup!' + """ + self._uicallables.append(call) + return call + + def extsetup(self, call): + """Decorated function will be executed during extsetup + + example:: + + @eh.extsetup + def setupcelestine(ui): + print 'this is extsetup!' + """ + self._extcallables.append(call) + return call + + def reposetup(self, call): + """Decorated function will be executed during reposetup + + example:: + + @eh.reposetup + def setupzephir(ui, repo): + print 'this is reposetup!' + """ + self._repocallables.append(call) + return call + + def revset(self, symbolname): + """Decorated function is a revset symbol + + The name of the symbol must be given as the decorator argument. + The symbol is added during `extsetup`. + + example:: + + @eh.revset('hidden') + def revsetbabar(repo, subset, x): + args = revset.getargs(x, 0, 0, 'babar accept no argument') + return [r for r in subset if 'babar' in repo[r].description()] + """ + def dec(symbol): + self._revsetsymbols.append((symbolname, symbol)) + return symbol + return dec + + def templatekw(self, keywordname): + """Decorated function is a template keyword + + The name of the keyword must be given as the decorator argument. + The symbol is added during `extsetup`. + + example:: + + @eh.templatekw('babar') + def kwbabar(ctx): + return 'babar' + """ + def dec(keyword): + self._templatekws.append((keywordname, keyword)) + return keyword + return dec + + def wrapcommand(self, command, extension=None, opts=[]): + """Decorated function is a command wrapper + + The name of the command must be given as the decorator argument. + The wrapping is installed during `uisetup`. + + If the second option `extension` argument is provided, the wrapping + will be applied in the extension commandtable. This argument must be a + string that will be searched using `extension.find` if not found and + Abort error is raised. If the wrapping applies to an extension, it is + installed during `extsetup`. + + example:: + + @eh.wrapcommand('summary') + def wrapsummary(orig, ui, repo, *args, **kwargs): + ui.note('Barry!') + return orig(ui, repo, *args, **kwargs) + + The `opts` argument allows specifying additional arguments for the + command. + + """ + def dec(wrapper): + if extension is None: + self._commandwrappers.append((command, wrapper, opts)) + else: + self._extcommandwrappers.append((extension, command, wrapper, + opts)) + return wrapper + return dec + + def wrapfunction(self, container, funcname): + """Decorated function is a function wrapper + + This function takes two arguments, the container and the name of the + function to wrap. The wrapping is performed during `uisetup`. + (there is no extension support) + + example:: + + @eh.function(discovery, 'checkheads') + def wrapfunction(orig, *args, **kwargs): + ui.note('His head smashed in and his heart cut out') + return orig(*args, **kwargs) + """ + def dec(wrapper): + self._functionwrappers.append((container, funcname, wrapper)) + return wrapper + return dec + + def addattr(self, container, funcname): + """Decorated function is to be added to the container + + This function takes two arguments, the container and the name of the + function to wrap. The wrapping is performed during `uisetup`. + + example:: + + @eh.function(context.changectx, 'babar') + def babar(ctx): + return 'babar' in ctx.description + """ + def dec(func): + self._duckpunchers.append((container, funcname, func)) + return func + return dec diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/hack/__init__.py diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/hack/directaccess.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/hack/directaccess.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,194 @@ +""" This extension provides direct access +It is the ability to refer and access hidden sha in commands provided that you +know their value. +For example hg log -r xxx where xxx is a commit has should work whether xxx is +hidden or not as we assume that the user knows what he is doing when referring +to xxx. +""" +from mercurial import extensions +from mercurial import cmdutil +from mercurial import repoview +from mercurial import branchmap +from mercurial import revset +from mercurial import error +from mercurial import commands +from mercurial import hg +from mercurial import util +from mercurial.i18n import _ + +cmdtable = {} +command = cmdutil.command(cmdtable) + +# By default, all the commands have directaccess with warnings +# List of commands that have no directaccess and directaccess with no warning +directaccesslevel = [ + # Format: + # ('nowarning', 'evolve', 'prune'), + # means: no directaccess warning, for the command in evolve named prune + # + # ('error', None, 'serve'), + # means: no directaccess for the command in core named serve + # + # The list is ordered alphabetically by command names, starting with all + # the commands in core then all the commands in the extensions + # + # The general guideline is: + # - remove directaccess warnings for read only commands + # - no direct access for commands with consequences outside of the repo + # - leave directaccess warnings for all the other commands + # + ('nowarning', None, 'annotate'), + ('nowarning', None, 'archive'), + ('nowarning', None, 'bisect'), + ('nowarning', None, 'bookmarks'), + ('nowarning', None, 'bundle'), + ('nowarning', None, 'cat'), + ('nowarning', None, 'diff'), + ('nowarning', None, 'export'), + ('nowarning', None, 'identify'), + ('nowarning', None, 'incoming'), + ('nowarning', None, 'log'), + ('nowarning', None, 'manifest'), + ('error', None, 'outgoing'), # confusing if push errors and not outgoing + ('error', None, 'push'), # destructive + ('nowarning', None, 'revert'), + ('error', None, 'serve'), + ('nowarning', None, 'tags'), + ('nowarning', None, 'unbundle'), + ('nowarning', None, 'update'), +] + +def reposetup(ui, repo): + repo._explicitaccess = set() + +def _computehidden(repo): + hidden = repoview.filterrevs(repo, 'visible') + cl = repo.changelog + dynamic = hidden & repo._explicitaccess + if dynamic: + blocked = cl.ancestors(dynamic, inclusive=True) + hidden = frozenset(r for r in hidden if r not in blocked) + return hidden + +def setupdirectaccess(): + """ Add two new filtername that behave like visible to provide direct access + and direct access with warning. Wraps the commands to setup direct access + """ + repoview.filtertable.update({'visible-directaccess-nowarn': _computehidden}) + repoview.filtertable.update({'visible-directaccess-warn': _computehidden}) + branchmap.subsettable['visible-directaccess-nowarn'] = 'visible' + branchmap.subsettable['visible-directaccess-warn'] = 'visible' + + for warn, ext, cmd in directaccesslevel: + try: + cmdtable = extensions.find(ext).cmdtable if ext else commands.table + wrapper = wrapwitherror if warn == 'error' else wrapwithoutwarning + extensions.wrapcommand(cmdtable, cmd, wrapper) + except (error.UnknownCommand, KeyError): + pass + +def wrapwitherror(orig, ui, repo, *args, **kwargs): + if repo and repo.filtername == 'visible-directaccess-warn': + repo = repo.filtered('visible') + return orig(ui, repo, *args, **kwargs) + +def wrapwithoutwarning(orig, ui, repo, *args, **kwargs): + if repo and repo.filtername == 'visible-directaccess-warn': + repo = repo.filtered("visible-directaccess-nowarn") + return orig(ui, repo, *args, **kwargs) + +def uisetup(ui): + """ Change ordering of extensions to ensure that directaccess extsetup comes + after the one of the extensions in the loadsafter list """ + loadsafter = ui.configlist('directaccess', 'loadsafter') + order = list(extensions._order) + directaccesidx = order.index('directaccess') + + # The min idx for directaccess to load after all the extensions in loadafter + minidxdirectaccess = directaccesidx + + for ext in loadsafter: + try: + minidxdirectaccess = max(minidxdirectaccess, order.index(ext)) + except ValueError: + pass # extension not loaded + + if minidxdirectaccess > directaccesidx: + order.insert(minidxdirectaccess + 1, 'directaccess') + order.remove('directaccess') + extensions._order = order + +def _repository(orig, *args, **kwargs): + """Make visible-directaccess-warn the default filter for new repos""" + repo = orig(*args, **kwargs) + return repo.filtered("visible-directaccess-warn") + +def extsetup(ui): + extensions.wrapfunction(revset, 'posttreebuilthook', _posttreebuilthook) + extensions.wrapfunction(hg, 'repository', _repository) + setupdirectaccess() + +hashre = util.re.compile('[0-9a-fA-F]{1,40}') + +_listtuple = ('symbol', '_list') + +def _ishashsymbol(symbol, maxrev): + # Returns true if symbol looks like a hash + try: + n = int(symbol) + if n <= maxrev: + # It's a rev number + return False + except ValueError: + pass + return hashre.match(symbol) + +def gethashsymbols(tree, maxrev): + # Returns the list of symbols of the tree that look like hashes + # for example for the revset 3::abe3ff it will return ('abe3ff') + if not tree: + return [] + + results = [] + if len(tree) == 2 and tree[0] == "symbol": + results.append(tree[1]) + elif tree[0] == "func" and tree[1] == _listtuple: + # the optimiser will group sequence of hash request + results += tree[2][1].split('\0') + elif len(tree) >= 3: + for subtree in tree[1:]: + results += gethashsymbols(subtree, maxrev) + # return directly, we don't need to filter symbols again + return results + return [s for s in results if _ishashsymbol(s, maxrev)] + +def _posttreebuilthook(orig, tree, repo): + # This is use to enabled direct hash access + # We extract the symbols that look like hashes and add them to the + # explicitaccess set + orig(tree, repo) + filternm = "" + if repo is not None: + filternm = repo.filtername + if filternm is not None and filternm.startswith('visible-directaccess'): + prelength = len(repo._explicitaccess) + accessbefore = set(repo._explicitaccess) + cl = repo.unfiltered().changelog + repo.symbols = gethashsymbols(tree, len(cl)) + for node in repo.symbols: + try: + node = cl._partialmatch(node) + except error.LookupError: + node = None + if node is not None: + rev = cl.rev(node) + if rev not in repo.changelog: + repo._explicitaccess.add(rev) + if prelength != len(repo._explicitaccess): + if repo.filtername != 'visible-directaccess-nowarn': + unhiddencommits = repo._explicitaccess - accessbefore + repo.ui.warn(_("Warning: accessing hidden changesets %s " + "for write operation\n") % + (",".join([str(repo.unfiltered()[l]) + for l in unhiddencommits]))) + repo.invalidatevolatilesets() diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/hack/drophack.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/hack/drophack.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,163 @@ +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +'''This extension add a hacky command to drop changeset during review + +This extension is intended as a temporary hack to allow Matt Mackall to use +evolve in the Mercurial review it self. You should probably not use it if your +name is not Matt Mackall. +''' + +import os +import time +import contextlib + +from mercurial.i18n import _ +from mercurial import cmdutil +from mercurial import repair +from mercurial import scmutil +from mercurial import lock as lockmod +from mercurial import util +from mercurial import commands + +cmdtable = {} +command = cmdutil.command(cmdtable) + + +@contextlib.contextmanager +def timed(ui, caption): + ostart = os.times() + cstart = time.time() + yield + cstop = time.time() + ostop = os.times() + wall = cstop - cstart + user = ostop[0] - ostart[0] + sys = ostop[1] - ostart[1] + comb = user + sys + ui.write("%s: wall %f comb %f user %f sys %f\n" + % (caption, wall, comb, user, sys)) + +def obsmarkerchainfrom(obsstore, nodes): + """return all marker chain starting from node + + Starting from mean "use as successors".""" + # XXX need something smarter for descendant of bumped changeset + seennodes = set(nodes) + seenmarkers = set() + pendingnodes = set(nodes) + precursorsmarkers = obsstore.precursors + while pendingnodes: + current = pendingnodes.pop() + new = set() + for precmark in precursorsmarkers.get(current, ()): + if precmark in seenmarkers: + continue + seenmarkers.add(precmark) + new.add(precmark[0]) + yield precmark + new -= seennodes + pendingnodes |= new + +def stripmarker(ui, repo, markers): + """remove from the repo obsstore + + The old obsstore content is saved in a `obsstore.prestrip` file + """ + repo = repo.unfiltered() + repo.destroying() + oldmarkers = list(repo.obsstore._all) + util.rename(repo.svfs.join('obsstore'), + repo.vfs.join('obsstore.prestrip')) + del repo.obsstore # drop the cache + newstore = repo.obsstore + assert not newstore # should be empty after rename + newmarkers = [m for m in oldmarkers if m not in markers] + tr = repo.transaction('drophack') + try: + newstore.add(tr, newmarkers) + tr.close() + finally: + tr.release() + repo.destroyed() + + +@command('drop', [('r', 'rev', [], 'revision to update')], _('[-r] revs')) +def cmddrop(ui, repo, *revs, **opts): + """I'm hacky do not use me! + + This command strip a changeset, its precursors and all obsolescence marker + associated to its chain. + + There is no way to limit the extend of the purge yet. You may have to + repull from other source to get some changeset and obsolescence marker + back. + + This intended for Matt Mackall usage only. do not use me. + """ + revs = list(revs) + revs.extend(opts['rev']) + if not revs: + revs = ['.'] + # get the changeset + revs = scmutil.revrange(repo, revs) + if not revs: + ui.write_err('no revision to drop\n') + return 1 + # lock from the beginning to prevent race + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + # check they have no children + if repo.revs('%ld and public()', revs): + ui.write_err('cannot drop public revision') + return 1 + if repo.revs('children(%ld) - %ld', revs, revs): + ui.write_err('cannot drop revision with children') + return 1 + if repo.revs('. and %ld', revs): + newrevs = repo.revs('max(::. - %ld)', revs) + if newrevs: + assert len(newrevs) == 1 + newrev = newrevs.first() + else: + newrev = -1 + commands.update(ui, repo, newrev) + ui.status(_('working directory now at %s\n') % repo[newrev]) + # get all markers and successors up to root + nodes = [repo[r].node() for r in revs] + with timed(ui, 'search obsmarker'): + markers = set(obsmarkerchainfrom(repo.obsstore, nodes)) + ui.write('%i obsmarkers found\n' % len(markers)) + cl = repo.unfiltered().changelog + with timed(ui, 'search nodes'): + allnodes = set(nodes) + allnodes.update(m[0] for m in markers if cl.hasnode(m[0])) + ui.write('%i nodes found\n' % len(allnodes)) + cl = repo.changelog + visiblenodes = set(n for n in allnodes if cl.hasnode(n)) + # check constraint again + if repo.revs('%ln and public()', visiblenodes): + ui.write_err('cannot drop public revision') + return 1 + if repo.revs('children(%ln) - %ln', visiblenodes, visiblenodes): + ui.write_err('cannot drop revision with children') + return 1 + + if markers: + # strip them + with timed(ui, 'strip obsmarker'): + stripmarker(ui, repo, markers) + # strip the changeset + with timed(ui, 'strip nodes'): + repair.strip(ui, repo, list(allnodes), backup="all", + topic='drophack') + + finally: + lockmod.release(lock, wlock) + + # rewrite the whole file. + # print data. + # - time to compute the chain + # - time to strip the changeset + # - time to strip the obs marker. diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/hack/inhibit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/hack/inhibit.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,311 @@ +"""reduce the changesets evolution feature scope for early and noob friendly ui + +the full scale changeset evolution have some massive bleeding edge and it is +very easy for people not very intimate with the concept to end up in intricate +situation. in order to get some of the benefit sooner, this extension is +disabling some of the less polished aspect of evolution. it should gradually +get thinner and thinner as changeset evolution will get more polished. this +extension is only recommended for large scale organisations. individual user +should probably stick on using evolution in its current state, understand its +concept and provide feedback + +This extension provides the ability to "inhibit" obsolescence markers. obsolete +revision can be cheaply brought back to life that way. +However as the inhibitor are not fitting in an append only model, this is +incompatible with sharing mutable history. +""" +from mercurial import localrepo +from mercurial import obsolete +from mercurial import extensions +from mercurial import cmdutil +from mercurial import error +from mercurial import scmutil +from mercurial import commands +from mercurial import lock as lockmod +from mercurial import bookmarks +from mercurial import util +from mercurial.i18n import _ + +cmdtable = {} +command = cmdutil.command(cmdtable) + +def _inhibitenabled(repo): + return util.safehasattr(repo, '_obsinhibit') + +def reposetup(ui, repo): + + class obsinhibitedrepo(repo.__class__): + + @localrepo.storecache('obsinhibit') + def _obsinhibit(self): + # XXX we should make sure it is invalidated by transaction failure + obsinhibit = set() + raw = self.svfs.tryread('obsinhibit') + for i in xrange(0, len(raw), 20): + obsinhibit.add(raw[i:i + 20]) + return obsinhibit + + def commit(self, *args, **kwargs): + newnode = super(obsinhibitedrepo, self).commit(*args, **kwargs) + if newnode is not None: + _inhibitmarkers(repo, [newnode]) + return newnode + + repo.__class__ = obsinhibitedrepo + +def _update(orig, ui, repo, *args, **kwargs): + """ + When moving to a commit we want to inhibit any obsolete commit affecting + the changeset we are updating to. In other words we don't want any visible + commit to be obsolete. + """ + wlock = None + try: + # Evolve is running a hook on lock release to display a warning message + # if the workind dir's parent is obsolete. + # We take the lock here to make sure that we inhibit the parent before + # that hook get a chance to run. + wlock = repo.wlock() + res = orig(ui, repo, *args, **kwargs) + newhead = repo['.'].node() + _inhibitmarkers(repo, [newhead]) + return res + finally: + lockmod.release(wlock) + +def _bookmarkchanged(orig, bkmstoreinst, *args, **kwargs): + """ Add inhibition markers to every obsolete bookmarks """ + repo = bkmstoreinst._repo + bkmstorenodes = [repo[v].node() for v in bkmstoreinst.values()] + _inhibitmarkers(repo, bkmstorenodes) + return orig(bkmstoreinst, *args, **kwargs) + +def _bookmark(orig, ui, repo, *bookmarks, **opts): + """ Add a -D option to the bookmark command, map it to prune -B """ + haspruneopt = opts.get('prune', False) + if not haspruneopt: + return orig(ui, repo, *bookmarks, **opts) + elif opts.get('rename'): + raise error.Abort('Cannot use both -m and -D') + elif len(bookmarks) == 0: + hint = _('make sure to put a space between -D and your bookmark name') + raise error.Abort(_('Error, please check your command'), hint=hint) + + # Call prune -B + evolve = extensions.find('evolve') + optsdict = { + 'new': [], + 'succ': [], + 'rev': [], + 'bookmark': bookmarks, + 'keep': None, + 'biject': False, + } + evolve.cmdprune(ui, repo, **optsdict) + +# obsolescence inhibitor +######################## + +def _schedulewrite(tr, obsinhibit): + """Make sure on disk content will be updated on transaction commit""" + def writer(fp): + """Serialize the inhibited list to disk. + """ + raw = ''.join(obsinhibit) + fp.write(raw) + tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer) + tr.hookargs['obs_inbihited'] = '1' + +def _filterpublic(repo, nodes): + """filter out inhibitor on public changeset + + Public changesets are already immune to obsolescence""" + getrev = repo.changelog.nodemap.get + getphase = repo._phasecache.phase + return (n for n in nodes + if getrev(n) is not None and getphase(repo, getrev(n))) + +def _inhibitmarkers(repo, nodes): + """add marker inhibitor for all obsolete revision under + + Content of and all mutable ancestors are considered. Marker for + obsolete revision only are created. + """ + if not _inhibitenabled(repo): + return + + # we add (non public()) as a lower boundary to + # - use the C code in 3.6 (no ancestors in C as this is written) + # - restrict the search space. Otherwise, the ancestors can spend a lot of + # time iterating if you have a check very low in the repo. We do not need + # to iterate over tens of thousand of public revisions with higher + # revision number + # + # In addition, the revset logic could be made significantly smarter here. + newinhibit = repo.revs('(not public())::%ln and obsolete()', nodes) + if newinhibit: + node = repo.changelog.node + lock = tr = None + try: + lock = repo.lock() + tr = repo.transaction('obsinhibit') + repo._obsinhibit.update(node(r) for r in newinhibit) + _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit)) + repo.invalidatevolatilesets() + tr.close() + finally: + lockmod.release(tr, lock) + +def _deinhibitmarkers(repo, nodes): + """lift obsolescence inhibition on a set of nodes + + This will be triggered when inhibited nodes received new obsolescence + markers. Otherwise the new obsolescence markers would also be inhibited. + """ + if not _inhibitenabled(repo): + return + + deinhibited = repo._obsinhibit & set(nodes) + if deinhibited: + tr = repo.transaction('obsinhibit') + try: + repo._obsinhibit -= deinhibited + _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit)) + repo.invalidatevolatilesets() + tr.close() + finally: + tr.release() + +def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None): + """wrap markers create to make sure we de-inhibit target nodes""" + # wrapping transactio to unify the one in each function + lock = tr = None + try: + lock = repo.lock() + tr = repo.transaction('add-obsolescence-marker') + orig(repo, relations, flag, date, metadata) + precs = (r[0].node() for r in relations) + _deinhibitmarkers(repo, precs) + tr.close() + finally: + lockmod.release(tr, lock) + +def _filterobsoleterevswrap(orig, repo, rebasesetrevs, *args, **kwargs): + repo._notinhibited = rebasesetrevs + try: + repo.invalidatevolatilesets() + r = orig(repo, rebasesetrevs, *args, **kwargs) + finally: + del repo._notinhibited + repo.invalidatevolatilesets() + return r + +def transactioncallback(orig, repo, desc, *args, **kwargs): + """ Wrap localrepo.transaction to inhibit new obsolete changes """ + def inhibitposttransaction(transaction): + # At the end of the transaction we catch all the new visible and + # obsolete commit to inhibit them + visibleobsolete = repo.revs('obsolete() - hidden()') + ignoreset = set(getattr(repo, '_rebaseset', [])) + ignoreset |= set(getattr(repo, '_obsoletenotrebased', [])) + visibleobsolete = list(r for r in visibleobsolete if r not in ignoreset) + if visibleobsolete: + _inhibitmarkers(repo, [repo[r].node() for r in visibleobsolete]) + transaction = orig(repo, desc, *args, **kwargs) + if desc != 'strip' and _inhibitenabled(repo): + transaction.addpostclose('inhibitposttransaction', + inhibitposttransaction) + return transaction + + +# We wrap these two functions to address the following scenario: +# - Assuming that we have markers between commits in the rebase set and +# destination and that these markers are inhibited +# - At the end of the rebase the nodes are still visible because rebase operate +# without inhibition and skip these nodes +# We keep track in repo._obsoletenotrebased of the obsolete commits skipped by +# the rebase and lift the inhibition in the end of the rebase. + +def _computeobsoletenotrebased(orig, repo, *args, **kwargs): + r = orig(repo, *args, **kwargs) + repo._obsoletenotrebased = r.keys() + return r + +def _clearrebased(orig, ui, repo, *args, **kwargs): + r = orig(ui, repo, *args, **kwargs) + tonode = repo.changelog.node + if util.safehasattr(repo, '_obsoletenotrebased'): + _deinhibitmarkers(repo, [tonode(k) for k in repo._obsoletenotrebased]) + return r + + +def extsetup(ui): + # lets wrap the computation of the obsolete set + # We apply inhibition there + obsfunc = obsolete.cachefuncs['obsolete'] + + def _computeobsoleteset(repo): + """remove any inhibited nodes from the obsolete set + + This will trickle down to other part of mercurial (hidden, log, etc)""" + obs = obsfunc(repo) + if _inhibitenabled(repo): + getrev = repo.changelog.nodemap.get + blacklist = getattr(repo, '_notinhibited', set()) + for n in repo._obsinhibit: + if getrev(n) not in blacklist: + obs.discard(getrev(n)) + return obs + try: + extensions.find('directaccess') + except KeyError: + errormsg = _('cannot use inhibit without the direct access extension\n') + hint = _("(please enable it or inhibit won\'t work)\n") + ui.warn(errormsg) + ui.warn(hint) + return + + # Wrapping this to inhibit obsolete revs resulting from a transaction + extensions.wrapfunction(localrepo.localrepository, + 'transaction', transactioncallback) + + obsolete.cachefuncs['obsolete'] = _computeobsoleteset + # wrap create marker to make it able to lift the inhibition + extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers) + # drop divergence computation since it is incompatible with "light revive" + obsolete.cachefuncs['divergent'] = lambda repo: set() + # drop bumped computation since it is incompatible with "light revive" + obsolete.cachefuncs['bumped'] = lambda repo: set() + # wrap update to make sure that no obsolete commit is visible after an + # update + extensions.wrapcommand(commands.table, 'update', _update) + try: + rebase = extensions.find('rebase') + if rebase: + if util.safehasattr(rebase, '_filterobsoleterevs'): + extensions.wrapfunction(rebase, + '_filterobsoleterevs', + _filterobsoleterevswrap) + extensions.wrapfunction(rebase, 'clearrebased', _clearrebased) + if util.safehasattr(rebase, '_computeobsoletenotrebased'): + extensions.wrapfunction(rebase, + '_computeobsoletenotrebased', + _computeobsoletenotrebased) + + except KeyError: + pass + # There are two ways to save bookmark changes during a transation, we + # wrap both to add inhibition markers. + extensions.wrapfunction(bookmarks.bmstore, 'recordchange', _bookmarkchanged) + if getattr(bookmarks.bmstore, 'write', None) is not None:# mercurial < 3.9 + extensions.wrapfunction(bookmarks.bmstore, 'write', _bookmarkchanged) + # Add bookmark -D option + entry = extensions.wrapcommand(commands.table, 'bookmark', _bookmark) + entry[1].append(('D', 'prune', None, + _('delete the bookmark and prune the commits underneath'))) + +@command('debugobsinhibit', [], '') +def cmddebugobsinhibit(ui, repo, *revs): + """inhibit obsolescence markers effect on a set of revs""" + nodes = (repo[r].node() for r in scmutil.revrange(repo, revs)) + _inhibitmarkers(repo, nodes) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/legacy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/legacy.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,203 @@ +# Copyright 2011 Pierre-Yves David +# Logilab SA +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +"""Deprecated extension that formerly introduced "Changeset Obsolescence". + +This concept is now partially in Mercurial core (starting with Mercurial 2.3). +The remaining logic has been grouped with the evolve extension. + +Some code remains 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 error + +try: + from mercurial import obsolete + obsolete._enabled +except ImportError: + raise error.Abort('Obsolete extension requires Mercurial 2.3 (or later)') + +import sys +import json + +from mercurial import cmdutil +from mercurial.i18n import _ +from mercurial import lock as lockmod +from mercurial.node import bin, nullid +from mercurial import util + + +##################################################################### +### Older format management ### +##################################################################### + +# Code related to detection and management of older legacy format never +# handled by core + + +def reposetup(ui, repo): + """Detect that a repo still contains some old obsolete format + """ + if not repo.local(): + return + evolveopts = ui.configlist('experimental', 'evolution') + if not evolveopts: + evolveopts = 'all' + ui.setconfig('experimental', 'evolution', evolveopts) + for arg in sys.argv: + if 'debugc' in arg: + break + else: + data = repo.vfs.tryread('obsolete-relations') + if not data: + data = repo.svfs.tryread('obsoletemarkers') + if data: + raise error.Abort('old format of obsolete marker detected!\n' + 'run `hg debugconvertobsolete` once.') + +def _obsdeserialize(flike): + """read a file like object serialized with _obsserialize + + this deserialize into a {subject -> objects} mapping + + this was the very first format ever.""" + rels = {} + for line in flike: + subhex, objhex = line.split() + subnode = bin(subhex) + if subnode == nullid: + subnode = None + rels.setdefault(subnode, set()).add(bin(objhex)) + return rels + +cmdtable = {} +command = cmdutil.command(cmdtable) +@command('debugconvertobsolete', [], '') +def cmddebugconvertobsolete(ui, repo): + """import markers from an .hg/obsolete-relations file""" + cnt = 0 + err = 0 + l = repo.lock() + some = False + try: + unlink = [] + tr = repo.transaction('convert-obsolete') + try: + repo._importoldobsolete = True + store = repo.obsstore + ### very first format + try: + f = repo.vfs('obsolete-relations') + try: + some = True + for line in f: + subhex, objhex = line.split() + suc = bin(subhex) + prec = bin(objhex) + sucs = (suc == nullid) and [] or [suc] + meta = { + 'date': '%i %i' % util.makedate(), + 'user': ui.username(), + } + try: + store.create(tr, prec, sucs, 0, metadata=meta) + cnt += 1 + except ValueError: + repo.ui.write_err("invalid old marker line: %s" + % (line)) + err += 1 + finally: + f.close() + unlink.append(repo.vfs.join('obsolete-relations')) + except IOError: + pass + ### second (json) format + data = repo.svfs.tryread('obsoletemarkers') + if data: + some = True + for oldmark in json.loads(data): + del oldmark['id'] # dropped for now + del oldmark['reason'] # unused until then + oldobject = str(oldmark.pop('object')) + oldsubjects = [str(s) for s in oldmark.pop('subjects', [])] + LOOKUP_ERRORS = (error.RepoLookupError, error.LookupError) + if len(oldobject) != 40: + try: + oldobject = repo[oldobject].node() + except LOOKUP_ERRORS: + pass + if any(len(s) != 40 for s in oldsubjects): + try: + oldsubjects = [repo[s].node() for s in oldsubjects] + except LOOKUP_ERRORS: + pass + + oldmark['date'] = '%i %i' % tuple(oldmark['date']) + meta = dict((k.encode('utf-8'), v.encode('utf-8')) + for k, v in oldmark.iteritems()) + try: + succs = [bin(n) for n in oldsubjects] + succs = [n for n in succs if n != nullid] + store.create(tr, bin(oldobject), succs, + 0, metadata=meta) + cnt += 1 + except ValueError: + msg = "invalid marker %s -> %s\n" + msg %= (oldobject, oldsubjects) + repo.ui.write_err(msg) + err += 1 + unlink.append(repo.svfs.join('obsoletemarkers')) + tr.close() + for path in unlink: + util.unlink(path) + finally: + tr.release() + finally: + del repo._importoldobsolete + l.release() + if not some: + 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) + +@command('debugrecordpruneparents', [], '') +def cmddebugrecordpruneparents(ui, repo): + """add parent data to prune markers when possible + + This command searches the repo for prune markers without parent information. + If the pruned node is locally known, it creates a new marker with parent + data. + """ + pgop = 'reading markers' + + # lock from the beginning to prevent race + wlock = lock = tr = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction('recordpruneparents') + unfi = repo.unfiltered() + nm = unfi.changelog.nodemap + store = repo.obsstore + pgtotal = len(store._all) + for idx, mark in enumerate(list(store._all)): + if not mark[1]: + rev = nm.get(mark[0]) + if rev is not None: + ctx = unfi[rev] + parents = tuple(p.node() for p in ctx.parents()) + before = len(store._all) + store.create(tr, mark[0], mark[1], mark[2], mark[3], + parents=parents) + if len(store._all) - before: + ui.write(_('created new markers for %i\n') % rev) + ui.progress(pgop, idx, total=pgtotal) + tr.close() + ui.progress(pgop, None) + finally: + lockmod.release(tr, lock, wlock) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/metadata.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/metadata.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,11 @@ +# define Mercurial extension metadata for evolution +# +# Copyright 2017 Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +__version__ = '6.0.0.dev' +testedwith = '3.8.4 3.9.2 4.0.2 4.1' +minimumhgversion = '3.8' +buglink = 'https://bz.mercurial-scm.org/' diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/obsdiscovery.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/obsdiscovery.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,783 @@ +# Code dedicated to the discovery of obsolescence marker "over the wire" +# +# Copyright 2017 Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +# Status: Experiment in progress // open question +# +# The final discovery algorithm and protocol will go into core when we'll be +# happy with it. +# +# Some of the code in this module is for compatiblity with older version +# of evolve and will be eventually dropped. + +from __future__ import absolute_import + +try: + import StringIO as io + StringIO = io.StringIO +except ImportError: + import io + StringIO = io.StringIO + +import hashlib +import heapq +import sqlite3 +import struct +import weakref + +from mercurial import ( + dagutil, + error, + exchange, + extensions, + localrepo, + node, + obsolete, + scmutil, + setdiscovery, + util, + wireproto, +) +from mercurial.hgweb import hgweb_mod +from mercurial.i18n import _ + +from . import ( + exthelper, + utility, + stablerange, +) + +_pack = struct.pack +_unpack = struct.unpack +_calcsize = struct.calcsize + +eh = exthelper.exthelper() +eh.merge(stablerange.eh) +obsexcmsg = utility.obsexcmsg + +########################################## +### trigger discovery during exchange ### +########################################## + +@eh.wrapfunction(exchange, '_pushdiscoveryobsmarkers') +def _pushdiscoveryobsmarkers(orig, pushop): + if (obsolete.isenabled(pushop.repo, obsolete.exchangeopt) + and pushop.repo.obsstore + and 'obsolete' in pushop.remote.listkeys('namespaces')): + repo = pushop.repo + obsexcmsg(repo.ui, "computing relevant nodes\n") + revs = list(repo.revs('::%ln', pushop.futureheads)) + unfi = repo.unfiltered() + cl = unfi.changelog + if not pushop.remote.capable('_evoext_obshash_0'): + # do not trust core yet + # return orig(pushop) + nodes = [cl.node(r) for r in revs] + if nodes: + obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n" + % len(nodes)) + pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) + else: + obsexcmsg(repo.ui, "markers already in sync\n") + pushop.outobsmarkers = [] + pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) + return + + common = [] + missing = None + obsexcmsg(repo.ui, "looking for common markers in %i nodes\n" + % len(revs)) + commonrevs = list(unfi.revs('::%ln', pushop.outgoing.commonheads)) + if _canobshashrange(repo, pushop.remote): + missing = findmissingrange(pushop.ui, unfi, pushop.remote, + commonrevs) + else: + common = findcommonobsmarkers(pushop.ui, unfi, pushop.remote, + commonrevs) + if missing is None: + revs = list(unfi.revs('%ld - (::%ln)', revs, common)) + nodes = [cl.node(r) for r in revs] + else: + revs = list(repo.revs('only(%ln, %ln)', pushop.futureheads, + pushop.outgoing.commonheads)) + nodes = [cl.node(r) for r in revs] + nodes += missing + + if nodes: + obsexcmsg(repo.ui, "computing markers relevant to %i nodes\n" + % len(nodes)) + pushop.outobsmarkers = repo.obsstore.relevantmarkers(nodes) + else: + obsexcmsg(repo.ui, "markers already in sync\n") + pushop.outobsmarkers = [] + +@eh.extsetup +def _installobsmarkersdiscovery(ui): + olddisco = exchange.pushdiscoverymapping['obsmarker'] + + def newdisco(pushop): + _pushdiscoveryobsmarkers(olddisco, pushop) + exchange.pushdiscoverymapping['obsmarker'] = newdisco + +def buildpullobsmarkersboundaries(pullop, bundle2=True): + """small function returning the argument for pull markers call + may to contains 'heads' and 'common'. skip the key for None. + + It is a separed function to play around with strategy for that.""" + repo = pullop.repo + remote = pullop.remote + unfi = repo.unfiltered() + revs = unfi.revs('::(%ln - null)', pullop.common) + boundaries = {'heads': pullop.pulledsubset} + if not revs: # nothing common + boundaries['common'] = [node.nullid] + return boundaries + + if bundle2 and _canobshashrange(repo, remote): + obsexcmsg(repo.ui, "looking for common markers in %i nodes\n" + % len(revs)) + boundaries['missing'] = findmissingrange(repo.ui, repo, pullop.remote, + revs) + elif remote.capable('_evoext_obshash_0'): + obsexcmsg(repo.ui, "looking for common markers in %i nodes\n" + % len(revs)) + boundaries['common'] = findcommonobsmarkers(repo.ui, repo, remote, revs) + else: + boundaries['common'] = [node.nullid] + return boundaries + +################################## +### Code performing discovery ### +################################## + +def findcommonobsmarkers(ui, local, remote, probeset, + initialsamplesize=100, + fullsamplesize=200): + # from discovery + roundtrips = 0 + cl = local.changelog + dag = dagutil.revlogdag(cl) + missing = set() + common = set() + undecided = set(probeset) + totalnb = len(undecided) + ui.progress(_("comparing with other"), 0, total=totalnb) + _takefullsample = setdiscovery._takefullsample + if remote.capable('_evoext_obshash_1'): + getremotehash = remote.evoext_obshash1 + localhash = _obsrelsethashtreefm1(local) + else: + getremotehash = remote.evoext_obshash + localhash = _obsrelsethashtreefm0(local) + + while undecided: + + ui.note(_("sampling from both directions\n")) + if len(undecided) < fullsamplesize: + sample = set(undecided) + else: + sample = _takefullsample(dag, undecided, size=fullsamplesize) + + roundtrips += 1 + 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))) + # indices between sample and externalized version must match + sample = list(sample) + remotehash = getremotehash(dag.externalizeall(sample)) + + yesno = [localhash[ix][1] == remotehash[si] + for si, ix in enumerate(sample)] + + commoninsample = set(n for i, n in enumerate(sample) if yesno[i]) + common.update(dag.ancestorset(commoninsample, common)) + + missinginsample = [n for i, n in enumerate(sample) if not yesno[i]] + missing.update(dag.descendantset(missinginsample, missing)) + + undecided.difference_update(missing) + undecided.difference_update(common) + + ui.progress(_("comparing with other"), None) + result = dag.headsetofconnecteds(common) + ui.debug("%d total queries\n" % roundtrips) + + if not result: + return set([node.nullid]) + return dag.externalizeall(result) + +def findmissingrange(ui, local, remote, probeset, + initialsamplesize=100, + fullsamplesize=200): + missing = set() + + heads = local.revs('heads(%ld)', probeset) + local.stablerange.warmup(local) + + rangelength = local.stablerange.rangelength + subranges = local.stablerange.subranges + # size of slice ? + heappop = heapq.heappop + heappush = heapq.heappush + heapify = heapq.heapify + + tested = set() + + sample = [] + samplesize = initialsamplesize + + def addentry(entry): + if entry in tested: + return False + sample.append(entry) + tested.add(entry) + return True + + for h in heads: + entry = (h, 0) + addentry(entry) + + querycount = 0 + ui.progress(_("comparing obsmarker with other"), querycount) + overflow = [] + while sample or overflow: + if overflow: + sample.extend(overflow) + overflow = [] + + if samplesize < len(sample): + # too much sample already + overflow = sample[samplesize:] + sample = sample[:samplesize] + elif len(sample) < samplesize: + ui.debug("query %i; add more sample (target %i, current %i)\n" + % (querycount, samplesize, len(sample))) + # we need more sample ! + needed = samplesize - len(sample) + sliceme = [] + heapify(sliceme) + for entry in sample: + if 1 < rangelength(local, entry): + heappush(sliceme, (-rangelength(local, entry), entry)) + + while sliceme and 0 < needed: + _key, target = heappop(sliceme) + for new in subranges(local, target): + # XXX we could record hierarchy to optimise drop + if addentry(new): + if 1 < len(new): + heappush(sliceme, (-rangelength(local, new), new)) + needed -= 1 + if needed <= 0: + break + + # no longer the first interation + samplesize = fullsamplesize + + nbsample = len(sample) + maxsize = max([rangelength(local, r) for r in sample]) + ui.debug("query %i; sample size is %i, largest range %i\n" + % (querycount, nbsample, maxsize)) + nbreplies = 0 + replies = list(_queryrange(ui, local, remote, sample)) + sample = [] + n = local.changelog.node + for entry, remotehash in replies: + nbreplies += 1 + if remotehash == _obshashrange(local, entry): + continue + elif 1 == rangelength(local, entry): + missing.add(n(entry[0])) + else: + for new in subranges(local, entry): + addentry(new) + assert nbsample == nbreplies + querycount += 1 + ui.progress(_("comparing obsmarker with other"), querycount) + ui.progress(_("comparing obsmarker with other"), None) + local.obsstore.rangeobshashcache.save(local) + return sorted(missing) + +def _queryrange(ui, repo, remote, allentries): + # question are asked with node + n = repo.changelog.node + noderanges = [(n(entry[0]), entry[1]) for entry in allentries] + replies = remote.evoext_obshashrange_v0(noderanges) + result = [] + for idx, entry in enumerate(allentries): + result.append((entry, replies[idx])) + return result + +############################## +### Range Hash computation ### +############################## + +@eh.command( + 'debugobshashrange', + [ + ('', 'rev', [], 'display obshash for all (rev, 0) range in REVS'), + ('', 'subranges', False, 'display all subranges'), + ], + _('')) +def debugobshashrange(ui, repo, **opts): + """display the ::REVS set topologically sorted in a stable way + """ + s = node.short + revs = scmutil.revrange(repo, opts['rev']) + # prewarm depth cache + if revs: + repo.stablerange.warmup(repo, max(revs)) + cl = repo.changelog + rangelength = repo.stablerange.rangelength + depthrev = repo.stablerange.depthrev + if opts['subranges']: + ranges = stablerange.subrangesclosure(repo, revs) + else: + ranges = [(r, 0) for r in revs] + headers = ('rev', 'node', 'index', 'size', 'depth', 'obshash') + linetemplate = '%12d %12s %12d %12d %12d %12s\n' + headertemplate = linetemplate.replace('d', 's') + ui.status(headertemplate % headers) + for r in ranges: + d = (r[0], + s(cl.node(r[0])), + r[1], + rangelength(repo, r), + depthrev(repo, r[0]), + node.short(_obshashrange(repo, r))) + ui.status(linetemplate % d) + repo.obsstore.rangeobshashcache.save(repo) + +def _obshashrange(repo, rangeid): + """return the obsolete hash associated to a range""" + cache = repo.obsstore.rangeobshashcache + cl = repo.changelog + obshash = cache.get(rangeid) + if obshash is not None: + return obshash + pieces = [] + nullid = node.nullid + if repo.stablerange.rangelength(repo, rangeid) == 1: + rangenode = cl.node(rangeid[0]) + tmarkers = repo.obsstore.relevantmarkers([rangenode]) + pieces = [] + for m in tmarkers: + mbin = obsolete._fm1encodeonemarker(m) + pieces.append(mbin) + pieces.sort() + else: + for subrange in repo.stablerange.subranges(repo, rangeid): + obshash = _obshashrange(repo, subrange) + if obshash != nullid: + pieces.append(obshash) + + sha = hashlib.sha1() + # note: if there is only one subrange with actual data, we'll just + # reuse the same hash. + if not pieces: + obshash = node.nullid + elif len(pieces) != 1 or obshash is None: + sha = hashlib.sha1() + for p in pieces: + sha.update(p) + obshash = sha.digest() + cache[rangeid] = obshash + return obshash + +### sqlite caching + +_sqliteschema = [ + """CREATE TABLE meta(schemaversion INTEGER NOT NULL, + nbobsmarker INTEGER NOT NULL, + obstipdata BLOB NOT NULL, + tiprev INTEGER NOT NULL, + tipnode BLOB NOT NULL + );""", + """CREATE TABLE obshashrange(rev INTEGER NOT NULL, + idx INTEGER NOT NULL, + obshash BLOB NOT NULL, + PRIMARY KEY(rev, idx));""", + "CREATE INDEX range_index ON obshashrange(rev, idx);", +] +_queryexist = "SELECT name FROM sqlite_master WHERE type='table' AND name='meta';" +_newmeta = """INSERT INTO meta (schemaversion, nbobsmarker, obstipdata, tiprev, tipnode) + VALUES (?,?,?,?,?);""" +_updateobshash = "INSERT INTO obshashrange(rev, idx, obshash) VALUES (?,?,?);" +_querymeta = "SELECT schemaversion, nbobsmarker, obstipdata, tiprev, tipnode FROM meta;" +_queryobshash = "SELECT obshash FROM obshashrange WHERE (rev = ? AND idx = ?);" + +class _obshashcache(dict): + + _schemaversion = 0 + + def __init__(self, repo): + super(_obshashcache, self).__init__() + self._path = repo.vfs.join('cache/evoext_obshashrange_v0.sqlite') + self._new = set() + self._valid = True + self._repo = weakref.ref(repo.unfiltered()) + # cache status + self._ondiskcachekey = None + + def clear(self): + self._valid = False + super(_obshashcache, self).clear() + self._new.clear() + + def get(self, rangeid): + value = super(_obshashcache, self).get(rangeid) + if value is None and self._con is not None: + nrange = (rangeid[0], rangeid[1]) + obshash = self._con.execute(_queryobshash, nrange).fetchone() + if obshash is not None: + value = obshash[0] + return value + + def __setitem__(self, rangeid, obshash): + self._new.add(rangeid) + super(_obshashcache, self).__setitem__(rangeid, obshash) + + def _cachekey(self, repo): + # XXX for now the cache is very volatile, but this is still a win + nbobsmarker = len(repo.obsstore._all) + if nbobsmarker: + tipdata = obsolete._fm1encodeonemarker(repo.obsstore._all[-1]) + else: + tipdata = node.nullid + tiprev = len(repo.changelog) - 1 + tipnode = repo.changelog.node(tiprev) + return (self._schemaversion, nbobsmarker, tipdata, tiprev, tipnode) + + @util.propertycache + def _con(self): + if not self._valid: + return None + repo = self._repo() + if repo is None: + return None + cachekey = self._cachekey(repo) + con = sqlite3.connect(self._path) + con.text_factory = str + cur = con.execute(_queryexist) + if cur.fetchone() is None: + self._valid = False + return None + meta = con.execute(_querymeta).fetchone() + if meta != cachekey: + self._valid = False + return None + self._ondiskcachekey = meta + return con + + def save(self, repo): + repo = repo.unfiltered() + try: + if not self._new: + return + with repo.lock(): + self._save(repo) + except error.LockError: + # Exceptionnally we are noisy about it since performance impact + # is large We should address that before using this more + # widely. + msg = _('obshashrange cache: skipping save unable to lock repo\n') + repo.ui.warn(msg) + + def _save(self, repo): + if self._con is None: + util.unlinkpath(self._path, ignoremissing=True) + if '_con' in vars(self): + del self._con + + con = sqlite3.connect(self._path) + con.text_factory = str + with con: + for req in _sqliteschema: + con.execute(req) + + con.execute(_newmeta, self._cachekey(repo)) + else: + con = self._con + if self._ondiskcachekey is not None: + meta = con.execute(_querymeta).fetchone() + if meta != self._ondiskcachekey: + # drifting is currently an issue because this means another + # process might have already added the cache line we are about + # to add. This will confuse sqlite + msg = _('obshashrange cache: skipping write, ' + 'database drifted under my feet\n') + data = (meta[2], meta[1], self._ondisktiprev, self._ondisktipnode) + repo.ui.warn(msg) + data = ((rangeid[0], rangeid[1], self[rangeid]) for rangeid in self._new) + con.executemany(_updateobshash, data) + cachekey = self._cachekey(repo) + con.execute(_newmeta, cachekey) + con.commit() + self._new.clear() + self._ondiskcachekey = cachekey + +@eh.wrapfunction(obsolete.obsstore, '_addmarkers') +def _addmarkers(orig, obsstore, *args, **kwargs): + obsstore.rangeobshashcache.clear() + return orig(obsstore, *args, **kwargs) + +try: + obsstorefilecache = localrepo.localrepository.obsstore +except AttributeError: + # XXX hg-3.8 compat + # + # mercurial 3.8 has issue with accessing file cache property from their + # cache. This is fix by 36fbd72c2f39fef8ad52d7c559906c2bc388760c in core + # and shipped in 3.9 + obsstorefilecache = localrepo.localrepository.__dict__['obsstore'] + + +# obsstore is a filecache so we have do to some spacial dancing +@eh.wrapfunction(obsstorefilecache, 'func') +def obsstorewithcache(orig, repo): + obsstore = orig(repo) + obsstore.rangeobshashcache = _obshashcache(repo.unfiltered()) + return obsstore + +@eh.reposetup +def setupcache(ui, repo): + + class obshashrepo(repo.__class__): + @localrepo.unfilteredmethod + def destroyed(self): + if 'stablerange' in vars(self): + del self.stablerange + + repo.__class__ = obshashrepo + +### wire protocol commands + +def _obshashrange_v0(repo, ranges): + """return a list of hash from a list of range + + The range have the id encoded as a node + + return 'wdirid' for unknown range""" + nm = repo.changelog.nodemap + ranges = [(nm.get(n), idx) for n, idx in ranges] + if ranges: + maxrev = max(r for r, i in ranges) + if maxrev is not None: + repo.stablerange.warmup(repo, upto=maxrev) + result = [] + for r in ranges: + if r[0] is None: + result.append(node.wdirid) + else: + result.append(_obshashrange(repo, r)) + repo.obsstore.rangeobshashcache.save(repo) + return result + +@eh.addattr(localrepo.localpeer, 'evoext_obshashrange_v0') +def local_obshashrange_v0(peer, ranges): + return _obshashrange_v0(peer._repo, ranges) + + +_indexformat = '>I' +_indexsize = _calcsize(_indexformat) +def _encrange(node_rangeid): + """encode a (node) range""" + headnode, index = node_rangeid + return headnode + _pack(_indexformat, index) + +def _decrange(data): + """encode a (node) range""" + assert _indexsize < len(data), len(data) + headnode = data[:-_indexsize] + index = _unpack(_indexformat, data[-_indexsize:])[0] + return (headnode, index) + +@eh.addattr(wireproto.wirepeer, 'evoext_obshashrange_v0') +def peer_obshashrange_v0(self, ranges): + binranges = [_encrange(r) for r in ranges] + encranges = wireproto.encodelist(binranges) + d = self._call("evoext_obshashrange_v0", ranges=encranges) + try: + return wireproto.decodelist(d) + except ValueError: + self._abort(error.ResponseError(_("unexpected response:"), d)) + +def srv_obshashrange_v0(repo, proto, ranges): + ranges = wireproto.decodelist(ranges) + ranges = [_decrange(r) for r in ranges] + hashes = _obshashrange_v0(repo, ranges) + return wireproto.encodelist(hashes) + + +def _canobshashrange(local, remote): + return (local.ui.configbool('experimental', 'obshashrange', False) + and remote.capable('_evoext_obshashrange_v0')) + +def _obshashrange_capabilities(orig, repo, proto): + """wrapper to advertise new capability""" + caps = orig(repo, proto) + enabled = repo.ui.configbool('experimental', 'obshashrange', False) + if obsolete.isenabled(repo, obsolete.exchangeopt) and enabled: + caps = caps.split() + caps.append('_evoext_obshashrange_v0') + caps.sort() + caps = ' '.join(caps) + return caps + +@eh.extsetup +def obshashrange_extsetup(ui): + hgweb_mod.perms['evoext_obshashrange_v0'] = 'pull' + + wireproto.commands['evoext_obshashrange_v0'] = (srv_obshashrange_v0, 'ranges') + ### + extensions.wrapfunction(wireproto, 'capabilities', _obshashrange_capabilities) + # wrap command content + oldcap, args = wireproto.commands['capabilities'] + + def newcap(repo, proto): + return _obshashrange_capabilities(oldcap, repo, proto) + wireproto.commands['capabilities'] = (newcap, args) + +############################# +### Tree Hash computation ### +############################# + +# Dash computed from a given changesets using all markers relevant to it and +# the obshash of its parents. This is similar to what happend for changeset +# node where the parent is used in the computation + +@eh.command( + 'debugobsrelsethashtree', + [('', 'v0', None, 'hash on marker format "0"'), + ('', '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 + + It computed form the "orsht" of its parent and markers + relevant to the changeset itself.""" + if v0 and v1: + raise error.Abort('cannot only specify one format') + elif v0: + treefunc = _obsrelsethashtreefm0 + else: + treefunc = _obsrelsethashtreefm1 + + for chg, obs in treefunc(repo): + ui.status('%s %s\n' % (node.hex(chg), node.hex(obs))) + +def _obsrelsethashtreefm0(repo): + return _obsrelsethashtree(repo, obsolete._fm0encodeonemarker) + +def _obsrelsethashtreefm1(repo): + return _obsrelsethashtree(repo, obsolete._fm1encodeonemarker) + +def _obsrelsethashtree(repo, encodeonemarker): + cache = [] + unfi = repo.unfiltered() + markercache = {} + repo.ui.progress(_("preparing locally"), 0, total=len(unfi)) + for i in unfi: + ctx = unfi[i] + entry = 0 + sha = hashlib.sha1() + # add data from p1 + for p in ctx.parents(): + p = p.rev() + if p < 0: + p = node.nullid + else: + p = cache[p][1] + if p != node.nullid: + entry += 1 + sha.update(p) + tmarkers = repo.obsstore.relevantmarkers([ctx.node()]) + if tmarkers: + bmarkers = [] + for m in tmarkers: + if m not in markercache: + markercache[m] = encodeonemarker(m) + bmarkers.append(markercache[m]) + bmarkers.sort() + for m in bmarkers: + entry += 1 + sha.update(m) + if entry: + cache.append((ctx.node(), sha.digest())) + else: + cache.append((ctx.node(), node.nullid)) + repo.ui.progress(_("preparing locally"), i, total=len(unfi)) + repo.ui.progress(_("preparing locally"), None) + return cache + +def _obshash(repo, nodes, version=0): + if version == 0: + hashs = _obsrelsethashtreefm0(repo) + elif version == 1: + hashs = _obsrelsethashtreefm1(repo) + else: + assert False + nm = repo.changelog.nodemap + revs = [nm.get(n) for n in nodes] + return [r is None and node.nullid or hashs[r][1] for r in revs] + +@eh.addattr(localrepo.localpeer, 'evoext_obshash') +def local_obshash(peer, nodes): + return _obshash(peer._repo, nodes) + +@eh.addattr(localrepo.localpeer, 'evoext_obshash1') +def local_obshash1(peer, nodes): + return _obshash(peer._repo, nodes, version=1) + +@eh.addattr(wireproto.wirepeer, 'evoext_obshash') +def peer_obshash(self, nodes): + d = self._call("evoext_obshash", nodes=wireproto.encodelist(nodes)) + try: + return wireproto.decodelist(d) + except ValueError: + self._abort(error.ResponseError(_("unexpected response:"), d)) + +@eh.addattr(wireproto.wirepeer, 'evoext_obshash1') +def peer_obshash1(self, nodes): + d = self._call("evoext_obshash1", nodes=wireproto.encodelist(nodes)) + try: + return wireproto.decodelist(d) + except ValueError: + self._abort(error.ResponseError(_("unexpected response:"), d)) + +def srv_obshash(repo, proto, nodes): + return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes))) + +def srv_obshash1(repo, proto, nodes): + return wireproto.encodelist(_obshash(repo, wireproto.decodelist(nodes), + version=1)) + +def _obshash_capabilities(orig, repo, proto): + """wrapper to advertise new capability""" + caps = orig(repo, proto) + if obsolete.isenabled(repo, obsolete.exchangeopt): + caps = caps.split() + caps.append('_evoext_obshash_0') + caps.append('_evoext_obshash_1') + caps.sort() + caps = ' '.join(caps) + return caps + +@eh.extsetup +def obshash_extsetup(ui): + hgweb_mod.perms['evoext_obshash'] = 'pull' + hgweb_mod.perms['evoext_obshash1'] = 'pull' + + wireproto.commands['evoext_obshash'] = (srv_obshash, 'nodes') + wireproto.commands['evoext_obshash1'] = (srv_obshash1, 'nodes') + extensions.wrapfunction(wireproto, 'capabilities', _obshash_capabilities) + # wrap command content + oldcap, args = wireproto.commands['capabilities'] + + def newcap(repo, proto): + return _obshash_capabilities(oldcap, repo, proto) + wireproto.commands['capabilities'] = (newcap, args) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/obsexchange.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/obsexchange.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,447 @@ +# Code dedicated to the exchange of obsolescence markers +# +# Copyright 2017 Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +from __future__ import absolute_import + +try: + import StringIO as io + StringIO = io.StringIO +except ImportError: + import io + StringIO = io.StringIO + +import errno +import socket + +from mercurial import ( + error, + exchange, + extensions, + httppeer, + localrepo, + lock as lockmod, + node, + obsolete, + util, + wireproto, +) +from mercurial.hgweb import hgweb_mod +from mercurial.i18n import _ + +from . import ( + exthelper, + utility, + obsdiscovery, +) + +eh = exthelper.exthelper() +obsexcmsg = utility.obsexcmsg +obsexcprg = utility.obsexcprg + + +_bestformat = max(obsolete.formats.keys()) + +##################################################### +### Support for subset specification in getbundle ### +##################################################### + +# Adds support for the 'evo_obscommon' argument to getbundle This argument use +# the data recovered from the discovery to request only a subpart of the +# obsolete subtree. + +@eh.uisetup +def addgetbundleargs(self): + wireproto.gboptsmap['evo_obscommon'] = 'nodes' + wireproto.gboptsmap['evo_missing_nodes'] = 'nodes' + +@eh.wrapfunction(exchange, '_pullbundle2extraprepare') +def _addobscommontob2pull(orig, pullop, kwargs): + ret = orig(pullop, kwargs) + ui = pullop.repo.ui + if ('obsmarkers' in kwargs and + pullop.remote.capable('_evoext_getbundle_obscommon')): + boundaries = obsdiscovery.buildpullobsmarkersboundaries(pullop) + if 'common' in boundaries: + common = boundaries['common'] + if common != pullop.common: + obsexcmsg(ui, 'request obsmarkers for some common nodes\n') + if common != [node.nullid]: + kwargs['evo_obscommon'] = common + elif 'missing' in boundaries: + missing = boundaries['missing'] + if missing: + obsexcmsg(ui, 'request obsmarkers for %d common nodes\n' + % len(missing)) + kwargs['evo_missing_nodes'] = missing + return ret + +def _getbundleobsmarkerpart(orig, bundler, repo, source, **kwargs): + if not (set(['evo_obscommon', 'evo_missing_nodes']) & set(kwargs)): + return orig(bundler, repo, source, **kwargs) + + if kwargs.get('obsmarkers', False): + heads = kwargs.get('heads') + if 'evo_obscommon' in kwargs: + if heads is None: + heads = repo.heads() + obscommon = kwargs.get('evo_obscommon', ()) + assert obscommon + obsset = repo.unfiltered().set('::%ln - ::%ln', heads, obscommon) + subset = [c.node() for c in obsset] + else: + common = kwargs.get('common') + subset = [c.node() for c in repo.unfiltered().set('only(%ln, %ln)', heads, common)] + subset += kwargs['evo_missing_nodes'] + markers = repo.obsstore.relevantmarkers(subset) + exchange.buildobsmarkerspart(bundler, markers) + +# manual wrap up in extsetup because of the wireproto.commands mapping +def _obscommon_capabilities(orig, repo, proto): + """wrapper to advertise new capability""" + caps = orig(repo, proto) + if obsolete.isenabled(repo, obsolete.exchangeopt): + caps = caps.split() + caps.append('_evoext_getbundle_obscommon') + caps.sort() + caps = ' '.join(caps) + return caps + +@eh.extsetup +def extsetup_obscommon(ui): + wireproto.gboptsmap['evo_obscommon'] = 'nodes' + + # wrap module content + origfunc = exchange.getbundle2partsmapping['obsmarkers'] + + def newfunc(*args, **kwargs): + return _getbundleobsmarkerpart(origfunc, *args, **kwargs) + exchange.getbundle2partsmapping['obsmarkers'] = newfunc + + extensions.wrapfunction(wireproto, 'capabilities', _obscommon_capabilities) + # wrap command content + oldcap, args = wireproto.commands['capabilities'] + + def newcap(repo, proto): + return _obscommon_capabilities(oldcap, repo, proto) + wireproto.commands['capabilities'] = (newcap, args) + +def _pushobsmarkers(repo, data): + tr = lock = None + try: + lock = repo.lock() + tr = repo.transaction('pushkey: obsolete markers') + new = repo.obsstore.mergemarkers(tr, data) + if new is not None: + obsexcmsg(repo.ui, "%i obsolescence markers added\n" % new, True) + tr.close() + finally: + lockmod.release(tr, lock) + repo.hook('evolve_pushobsmarkers') + +def srv_pushobsmarkers(repo, proto): + """wireprotocol command""" + fp = StringIO() + proto.redirect() + proto.getfile(fp) + data = fp.getvalue() + fp.close() + _pushobsmarkers(repo, data) + return wireproto.pushres(0) + +def _getobsmarkersstream(repo, heads=None, common=None): + """Get a binary stream for all markers relevant to `:: - ::` + """ + revset = '' + args = [] + repo = repo.unfiltered() + if heads is None: + revset = 'all()' + elif heads: + revset += "(::%ln)" + args.append(heads) + else: + assert False, 'pulling no heads?' + if common: + revset += ' - (::%ln)' + args.append(common) + nodes = [c.node() for c in repo.set(revset, *args)] + markers = repo.obsstore.relevantmarkers(nodes) + obsdata = StringIO() + for chunk in obsolete.encodemarkers(markers, True): + obsdata.write(chunk) + obsdata.seek(0) + return obsdata + +# The wireproto.streamres API changed, handling chunking and compression +# directly. Handle either case. +if util.safehasattr(wireproto.abstractserverproto, 'groupchunks'): + # We need to handle chunking and compression directly + def streamres(d, proto): + return wireproto.streamres(proto.groupchunks(d)) +else: + # Leave chunking and compression to streamres + def streamres(d, proto): + return wireproto.streamres(reader=d, v1compressible=True) + +def srv_pullobsmarkers(repo, proto, others): + """serves a binary stream of markers. + + Serves relevant to changeset between heads and common. The stream is prefix + by a -string- representation of an integer. This integer is the size of the + stream.""" + opts = wireproto.options('', ['heads', 'common'], others) + for k, v in opts.iteritems(): + if k in ('heads', 'common'): + opts[k] = wireproto.decodelist(v) + obsdata = _getobsmarkersstream(repo, **opts) + finaldata = StringIO() + obsdata = obsdata.getvalue() + finaldata.write('%20i' % len(obsdata)) + finaldata.write(obsdata) + finaldata.seek(0) + return streamres(finaldata, proto) + +############################################### +### Support for old legacy exchange methods ### +############################################### + +class pushobsmarkerStringIO(StringIO): + """hacky string io for progress""" + + @util.propertycache + def length(self): + return len(self.getvalue()) + + def read(self, size=None): + obsexcprg(self.ui, self.tell(), unit=_("bytes"), total=self.length) + return StringIO.read(self, size) + + def __iter__(self): + d = self.read(4096) + while d: + yield d + d = self.read(4096) + +# compat-code: _pushobsolete +# +# the _pushobsolete function is a core function used to exchange +# obsmarker with repository that does not support bundle2 + +@eh.wrapfunction(exchange, '_pushobsolete') +def _pushobsolete(orig, pushop): + """utility function to push obsolete markers to a remote""" + if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt): + return + if 'obsmarkers' in pushop.stepsdone: + return + pushop.stepsdone.add('obsmarkers') + if pushop.cgresult == 0: + return + pushop.ui.debug('try to push obsolete markers to remote\n') + repo = pushop.repo + remote = pushop.remote + if (repo.obsstore and 'obsolete' in remote.listkeys('namespaces')): + markers = pushop.outobsmarkers + if not markers: + obsexcmsg(repo.ui, "no marker to push\n") + elif remote.capable('_evoext_pushobsmarkers_0'): + msg = ('the remote repository use years old versions of Mercurial' + ' and evolve\npushing obsmarker using legacy method\n') + repo.ui.warn(msg) + repo.ui.warn('(please upgrade your server)\n') + obsdata = pushobsmarkerStringIO() + for chunk in obsolete.encodemarkers(markers, True): + obsdata.write(chunk) + obsdata.seek(0) + obsdata.ui = repo.ui + obsexcmsg(repo.ui, "pushing %i obsolescence markers (%i bytes)\n" + % (len(markers), len(obsdata.getvalue())), + True) + remote.evoext_pushobsmarkers_0(obsdata) + obsexcprg(repo.ui, None) + + else: + # XXX core could be able do the same things but without the debug + # and progress output. + msg = ('the remote repository usea years old version of Mercurial' + ' and not evolve extension\n') + repo.ui.warn(msg) + msg = 'pushing obsmarker using and extremely slow legacy method\n' + repo.ui.warn(msg) + repo.ui.warn('(please upgrade your server and enable evolve.serveronly on it)\n') + rslts = [] + remotedata = obsolete._pushkeyescape(markers).items() + 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), + True) + for key, data in remotedata: + obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"), + total=totalbytes) + rslts.append(remote.pushkey('obsolete', key, '', data)) + sentbytes += len(data) + obsexcprg(repo.ui, sentbytes, item=key, unit=_("bytes"), + total=totalbytes) + obsexcprg(repo.ui, None) + if [r for r in rslts if not r]: + msg = _('failed to push some obsolete markers!\n') + repo.ui.warn(msg) + obsexcmsg(repo.ui, "DONE\n") + +# Supporting legacy way to push obsmarker so that old client can still push +# them somewhat efficiently + +@eh.addattr(wireproto.wirepeer, 'evoext_pushobsmarkers_0') +def client_pushobsmarkers(self, obsfile): + """wireprotocol peer method""" + self.requirecap('_evoext_pushobsmarkers_0', + _('push obsolete markers faster')) + ret, output = self._callpush('evoext_pushobsmarkers_0', obsfile) + for l in output.splitlines(True): + self.ui.status(_('remote: '), l) + return ret + +@eh.addattr(httppeer.httppeer, 'evoext_pushobsmarkers_0') +def httpclient_pushobsmarkers(self, obsfile): + """httpprotocol peer method + (Cannot simply use _callpush as http is doing some special handling)""" + self.requirecap('_evoext_pushobsmarkers_0', + _('push obsolete markers faster')) + try: + r = self._call('evoext_pushobsmarkers_0', data=obsfile) + vals = r.split('\n', 1) + if len(vals) < 2: + raise error.ResponseError(_("unexpected response:"), r) + + for l in vals[1].splitlines(True): + if l.strip(): + self.ui.status(_('remote: '), l) + return vals[0] + except socket.error as err: + if err.args[0] in (errno.ECONNRESET, errno.EPIPE): + 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): + caps = orig(repo, caps) + caps.add('_evoext_pushobsmarkers_0') + return caps + +@eh.addattr(localrepo.localpeer, 'evoext_pushobsmarkers_0') +def local_pushobsmarkers(peer, obsfile): + data = obsfile.read() + _pushobsmarkers(peer._repo, data) + +# compat-code: _pullobsolete +# +# the _pullobsolete function is a core function used to exchange +# obsmarker with repository that does not support bundle2 + +@eh.wrapfunction(exchange, '_pullobsolete') +def _pullobsolete(orig, pullop): + if not obsolete.isenabled(pullop.repo, obsolete.exchangeopt): + return None + if 'obsmarkers' in pullop.stepsdone: + return None + wirepull = pullop.remote.capable('_evoext_pullobsmarkers_0') + if 'obsolete' not in pullop.remote.listkeys('namespaces'): + return None # remote opted out of obsolescence marker exchange + if not wirepull: + return orig(pullop) + tr = None + ui = pullop.repo.ui + boundaries = obsdiscovery.buildpullobsmarkersboundaries(pullop, bundle2=False) + if 'missing' in boundaries and not boundaries['missing']: + obsexcmsg(ui, "nothing to pull\n") + return None + elif not set(boundaries['heads']) - set(boundaries['common']): + obsexcmsg(ui, "nothing to pull\n") + return None + + obsexcmsg(ui, "pull obsolescence markers\n", True) + new = 0 + + msg = ('the remote repository use years old versions of Mercurial and evolve\n' + 'pulling obsmarker using legacy method\n') + ui.warn(msg) + ui.warn('(please upgrade your server)\n') + + obsdata = pullop.remote.evoext_pullobsmarkers_0(**boundaries) + obsdata = obsdata.read() + if len(obsdata) > 5: + msg = "merging obsolescence markers (%i bytes)\n" % len(obsdata) + obsexcmsg(ui, msg) + tr = pullop.gettransaction() + old = len(pullop.repo.obsstore._all) + pullop.repo.obsstore.mergemarkers(tr, obsdata) + new = len(pullop.repo.obsstore._all) - old + obsexcmsg(ui, "%i obsolescence markers added\n" % new, True) + else: + obsexcmsg(ui, "no unknown remote markers\n") + obsexcmsg(ui, "DONE\n") + if new: + pullop.repo.invalidatevolatilesets() + return tr + +@eh.addattr(wireproto.wirepeer, 'evoext_pullobsmarkers_0') +def client_pullobsmarkers(self, heads=None, common=None): + self.requirecap('_evoext_pullobsmarkers_0', _('look up remote obsmarkers')) + opts = {} + if heads is not None: + opts['heads'] = wireproto.encodelist(heads) + if common is not None: + opts['common'] = wireproto.encodelist(common) + f = self._callcompressable("evoext_pullobsmarkers_0", **opts) + length = int(f.read(20)) + chunk = 4096 + current = 0 + data = StringIO() + ui = self.ui + obsexcprg(ui, current, unit=_("bytes"), total=length) + while current < length: + readsize = min(length - current, chunk) + data.write(f.read(readsize)) + current += readsize + obsexcprg(ui, current, unit=_("bytes"), total=length) + obsexcprg(ui, None) + data.seek(0) + return data + +@eh.addattr(localrepo.localpeer, 'evoext_pullobsmarkers_0') +def local_pullobsmarkers(self, heads=None, common=None): + return _getobsmarkersstream(self._repo, heads=heads, + common=common) + +def _legacypush_capabilities(orig, repo, proto): + """wrapper to advertise new capability""" + caps = orig(repo, proto) + if obsolete.isenabled(repo, obsolete.exchangeopt): + caps = caps.split() + caps.append('_evoext_pushobsmarkers_0') + caps.append('_evoext_pullobsmarkers_0') + caps.sort() + caps = ' '.join(caps) + return caps + +@eh.extsetup +def extsetup(ui): + # legacy standalone method + hgweb_mod.perms['evoext_pushobsmarkers_0'] = 'push' + hgweb_mod.perms['evoext_pullobsmarkers_0'] = 'pull' + wireproto.commands['evoext_pushobsmarkers_0'] = (srv_pushobsmarkers, '') + wireproto.commands['evoext_pullobsmarkers_0'] = (srv_pullobsmarkers, '*') + + extensions.wrapfunction(wireproto, 'capabilities', _legacypush_capabilities) + # wrap command content + oldcap, args = wireproto.commands['capabilities'] + + def newcap(repo, proto): + return _legacypush_capabilities(oldcap, repo, proto) + wireproto.commands['capabilities'] = (newcap, args) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/serveronly.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/serveronly.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,53 @@ +'''enable experimental obsolescence feature of Mercurial + +OBSOLESCENCE IS AN EXPERIMENTAL FEATURE MAKE SURE YOU UNDERSTOOD THE INVOLVED +CONCEPT BEFORE USING IT. + +/!\ THIS EXTENSION IS INTENDED FOR SERVER SIDE ONLY USAGE /!\ + +For client side usages it is recommended to use the evolve extension for +improved user interface.''' + +from __future__ import absolute_import + +import sys +import os + +try: + from . import ( + exthelper, + metadata, + obsdiscovery, + obsexchange, + ) +except ValueError as exc: + if str(exc) != 'Attempted relative import in non-package': + raise + # extension imported using direct path + sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) + from evolve import ( + exthelper, + metadata, + obsdiscovery, + obsexchange, + ) + +__version__ = metadata.__version__ +testedwith = metadata.testedwith +minimumhgversion = metadata.minimumhgversion +buglink = metadata.buglink + +eh = exthelper.exthelper() +eh.merge(obsdiscovery.eh) +eh.merge(obsexchange.eh) +uisetup = eh.final_uisetup +extsetup = eh.final_extsetup +reposetup = eh.final_reposetup +cmdtable = eh.cmdtable + +@eh.reposetup +def reposetup(ui, repo): + evolveopts = ui.configlist('experimental', 'evolution') + if not evolveopts: + evolveopts = 'all' + ui.setconfig('experimental', 'evolution', evolveopts) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/stablerange.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/stablerange.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,907 @@ +# Code dedicated to the computation and properties of "stable ranges" +# +# These stable ranges are use for obsolescence markers discovery +# +# Copyright 2017 Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +import collections +import heapq +import math +import sqlite3 +import weakref + +from mercurial import ( + commands, + cmdutil, + error, + localrepo, + node as nodemod, + scmutil, + util, +) + +from mercurial.i18n import _ + +from . import ( + exthelper, +) + +eh = exthelper.exthelper() + +################################## +### Stable topological sorting ### +################################## +@eh.command( + 'debugstablesort', + [ + ('', 'rev', [], 'heads to start from'), + ] + commands.formatteropts, + _('')) +def debugstablesort(ui, repo, **opts): + """display the ::REVS set topologically sorted in a stable way + """ + revs = scmutil.revrange(repo, opts['rev']) + displayer = cmdutil.show_changeset(ui, repo, opts, buffered=True) + for r in stablesort(repo, revs): + ctx = repo[r] + displayer.show(ctx) + displayer.flush(ctx) + displayer.close() + +def stablesort(repo, revs, mergecallback=None): + """return '::revs' topologically sorted in "stable" order + + This is a depth first traversal starting from 'nullrev', using node as a + tie breaker. + """ + # Various notes: + # + # * Bitbucket is used dates as tie breaker, that might be a good idea. + # + # * It seemds we can traverse in the same order from (one) head to bottom, + # if we the following record data for each merge: + # + # - highest (stablesort-wise) common ancestors, + # - order of parents (tablesort-wise) + cl = repo.changelog + parents = cl.parentrevs + nullrev = nodemod.nullrev + n = cl.node + # step 1: We need a parents -> children mapping for 2 reasons. + # + # * we build the order from nullrev to tip + # + # * we need to detect branching + children = collections.defaultdict(list) + for r in cl.ancestors(revs, inclusive=True): + p1, p2 = parents(r) + children[p1].append(r) + if p2 != nullrev: + children[p2].append(r) + # step two: walk back up + # * pick lowest node in case of branching + # * stack disregarded part of the branching + # * process merge when both parents are yielded + + # track what changeset has been + seen = [0] * (max(revs) + 2) + seen[-1] = True # nullrev is known + # starts from repository roots + # reuse the list form the mapping as we won't need it again anyway + stack = children[nullrev] + if not stack: + return [] + if 1 < len(stack): + stack.sort(key=n, reverse=True) + + # list of rev, maybe we should yield, but since we built a children mapping we are 'O(N)' already + result = [] + + current = stack.pop() + while current is not None or stack: + if current is None: + # previous iteration reached a merge or an unready merge, + current = stack.pop() + if seen[current]: + current = None + continue + p1, p2 = parents(current) + if not (seen[p1] and seen[p2]): + # we can't iterate on this merge yet because other child is not + # yielded yet (and we are topo sorting) we can discard it for now + # because it will be reached from the other child. + current = None + continue + assert not seen[current] + seen[current] = True + result.append(current) # could be yield, cf earlier comment + if mergecallback is not None and p2 != nullrev: + mergecallback(result, current) + cs = children[current] + if not cs: + current = None + elif 1 == len(cs): + current = cs[0] + else: + cs.sort(key=n, reverse=True) + current = cs.pop() # proceed on smallest + stack.extend(cs) # stack the rest for later + assert len(result) == len(set(result)) + return result + +################################# +### Stable Range computation ### +################################# + +def _hlp2(i): + """return highest power of two lower than 'i'""" + return 2 ** int(math.log(i - 1, 2)) + +def subrangesclosure(repo, heads): + """set of all standard subrange under heads + + This is intended for debug purposes. Range are returned from largest to + smallest in terms of number of revision it contains.""" + subranges = repo.stablerange.subranges + toproceed = [(r, 0, ) for r in heads] + ranges = set(toproceed) + while toproceed: + entry = toproceed.pop() + for r in subranges(repo, entry): + if r not in ranges: + ranges.add(r) + toproceed.append(r) + ranges = list(ranges) + n = repo.changelog.node + rangelength = repo.stablerange.rangelength + ranges.sort(key=lambda r: (-rangelength(repo, r), n(r[0]))) + return ranges + +@eh.command( + 'debugstablerange', + [ + ('', 'rev', [], 'operate on (rev, 0) ranges for rev in REVS'), + ('', 'subranges', False, 'recursively display data for subranges too'), + ('', 'verify', False, 'checks subranges content (EXPENSIVE)'), + ], + _('')) +def debugstablerange(ui, repo, **opts): + """display standard stable subrange for a set of ranges + + Range as displayed as '- (, , )', use + --verbose to get the extra details in (). + """ + short = nodemod.short + revs = scmutil.revrange(repo, opts['rev']) + # prewarm depth cache + unfi = repo.unfiltered() + node = unfi.changelog.node + stablerange = unfi.stablerange + depth = stablerange.depthrev + length = stablerange.rangelength + subranges = stablerange.subranges + repo.stablerange.warmup(repo, max(revs)) + if opts['subranges']: + ranges = subrangesclosure(repo, revs) + else: + ranges = [(r, 0) for r in revs] + if ui.verbose: + template = '%s-%d (%d, %d, %d)' + + def _rangestring(repo, rangeid): + return template % ( + short(node(rangeid[0])), + rangeid[1], + rangeid[0], + depth(unfi, rangeid[0]), + length(unfi, rangeid) + ) + else: + template = '%s-%d' + + def _rangestring(repo, rangeid): + return template % ( + short(node(rangeid[0])), + rangeid[1], + ) + + for r in ranges: + subs = subranges(unfi, r) + subsstr = ', '.join(_rangestring(unfi, s) for s in subs) + rstr = _rangestring(unfi, r) + if opts['verify']: + status = 'leaf' + if 1 < length(unfi, r): + status = 'complete' + revs = set(stablerange.revsfromrange(unfi, r)) + subrevs = set() + for s in subs: + subrevs.update(stablerange.revsfromrange(unfi, s)) + if revs != subrevs: + status = 'missing' + ui.status('%s [%s] - %s\n' % (rstr, status, subsstr)) + else: + ui.status('%s - %s\n' % (rstr, subsstr)) + +class stablerange(object): + + def __init__(self): + # The point up to which we have data in cache + self._tiprev = None + self._tipnode = None + # cache the 'depth' of a changeset, the size of '::rev' + self._depthcache = {} + # cache the standard stable subranges or a range + self._subrangescache = {} + # To slices merge, we need to walk their descendant in reverse stable + # sort order. For now we perform a full stable sort their descendant + # and then use the relevant top most part. This order is going to be + # the same for all ranges headed at the same merge. So we cache these + # value to reuse them accross the same invocation. + self._stablesortcache = {} + # something useful to compute the above + # mergerev -> stablesort, length + self._stablesortprepared = {} + # caching parent call # as we do so many of them + self._parentscache = {} + # The first part of the stable sorted list of revision of a merge will + # shared with the one of others. This means we can reuse subranges + # computed from that point to compute some of the subranges from the + # merge. + self._inheritancecache = {} + + def warmup(self, repo, upto=None): + """warm the cache up""" + repo = repo.unfiltered() + cl = repo.changelog + # subrange should be warmed from head to range to be able to benefit + # from revsfromrange cache. otherwise each merge will trigger its own + # stablesort. + # + # we use the revnumber as an approximation for depth + ui = repo.ui + + if upto is None: + upto = len(cl) - 1 + if self._tiprev is None: + revs = cl.revs(stop=upto) + nbrevs = upto + 1 + else: + assert cl.node(self._tiprev) == self._tipnode + if upto <= self._tiprev: + return + revs = cl.revs(start=self._tiprev + 1, stop=upto) + nbrevs = upto - self._tiprev + rangeheap = [] + for idx, r in enumerate(revs): + if not idx % 1000: + ui.progress(_("filling depth cache"), idx, total=nbrevs) + # warm up depth + self.depthrev(repo, r) + rangeheap.append((-r, (r, 0))) + ui.progress(_("filling depth cache"), None, total=nbrevs) + + heappop = heapq.heappop + heappush = heapq.heappush + heapify = heapq.heapify + + original = set(rangeheap) + seen = 0 + heapify(rangeheap) + while rangeheap: + value = heappop(rangeheap) + if value in original: + if not seen % 1000: + ui.progress(_("filling stablerange cache"), seen, total=nbrevs) + seen += 1 + original.remove(value) # might have been added from other source + __, rangeid = value + if self._getsub(rangeid) is None: + for sub in self.subranges(repo, rangeid): + if self._getsub(sub) is None: + heappush(rangeheap, (-sub[0], sub)) + ui.progress(_("filling stablerange cache"), None, total=nbrevs) + + self._tiprev = upto + self._tipnode = cl.node(upto) + + def depthrev(self, repo, rev): + repo = repo.unfiltered() + cl = repo.changelog + depth = self._getdepth + nullrev = nodemod.nullrev + stack = [rev] + while stack: + revdepth = None + current = stack[-1] + revdepth = depth(current) + if revdepth is not None: + stack.pop() + continue + p1, p2 = self._parents(current, cl.parentrevs) + if p1 == nullrev: + # root case + revdepth = 1 + elif p2 == nullrev: + # linear commit case + parentdepth = depth(p1) + if parentdepth is None: + stack.append(p1) + else: + revdepth = parentdepth + 1 + else: + # merge case + revdepth = self._depthmerge(cl, current, p1, p2, stack) + if revdepth is not None: + self._setdepth(current, revdepth) + stack.pop() + # actual_depth = len(list(cl.ancestors([rev], inclusive=True))) + # assert revdepth == actual_depth, (rev, revdepth, actual_depth) + return revdepth + + def rangelength(self, repo, rangeid): + headrev, index = rangeid[0], rangeid[1] + return self.depthrev(repo, headrev) - index + + def subranges(self, repo, rangeid): + cached = self._getsub(rangeid) + if cached is not None: + return cached + value = self._subranges(repo, rangeid) + self._setsub(rangeid, value) + return value + + def revsfromrange(self, repo, rangeid): + headrev, index = rangeid + rangelength = self.rangelength(repo, rangeid) + if rangelength == 1: + revs = [headrev] + else: + # get all revs under heads in stable order + # + # note: In the general case we can just walk down and then request + # data about the merge. But I'm not sure this function will be even + # call for the general case. + allrevs = self._stablesortcache.get(headrev) + if allrevs is None: + allrevs = self._getrevsfrommerge(repo, headrev) + if allrevs is None: + allrevs = stablesort(repo, [headrev], + mergecallback=self._filestablesortcache) + self._stablesortcache[headrev] = allrevs + # takes from index + revs = allrevs[index:] + # sanity checks + assert len(revs) == rangelength + return revs + + def _parents(self, rev, func): + parents = self._parentscache.get(rev) + if parents is None: + parents = func(rev) + self._parentscache[rev] = parents + return parents + + def _getdepth(self, rev): + """utility function used to access the depth cache + + This mostly exist to help the on disk persistence.""" + return self._depthcache.get(rev) + + def _setdepth(self, rev, value): + """utility function used to set the depth cache + + This mostly exist to help the on disk persistence.""" + self._depthcache[rev] = value + + def _getsub(self, rev): + """utility function used to access the subranges cache + + This mostly exist to help the on disk persistence""" + return self._subrangescache.get(rev) + + def _setsub(self, rev, value): + """utility function used to set the subranges cache + + This mostly exist to help the on disk persistence.""" + self._subrangescache[rev] = value + + def _filestablesortcache(self, sortedrevs, merge): + if merge not in self._stablesortprepared: + self._stablesortprepared[merge] = (sortedrevs, len(sortedrevs)) + + def _getrevsfrommerge(self, repo, merge): + prepared = self._stablesortprepared.get(merge) + if prepared is None: + return None + + mergedepth = self.depthrev(repo, merge) + allrevs = prepared[0][:prepared[1]] + nbextrarevs = prepared[1] - mergedepth + if not nbextrarevs: + return allrevs + + anc = repo.changelog.ancestors([merge], inclusive=True) + top = [] + counter = nbextrarevs + for rev in reversed(allrevs): + if rev in anc: + top.append(rev) + else: + counter -= 1 + if counter <= 0: + break + + bottomidx = prepared[1] - (nbextrarevs + len(top)) + revs = allrevs[:bottomidx] + revs.extend(reversed(top)) + return revs + + def _inheritancepoint(self, repo, merge): + """Find the inheritance point of a Merge + + The first part of the stable sorted list of revision of a merge will shared with + the one of others. This means we can reuse subranges computed from that point to + compute some of the subranges from the merge. + + That point is latest point in the stable sorted list where the depth of the + revisions match its index (that means all revision earlier in the stable sorted + list are its ancestors, no dangling unrelated branches exists). + """ + value = self._inheritancecache.get(merge) + if value is None: + revs = self.revsfromrange(repo, (merge, 0)) + i = reversed(revs) + i.next() # pop the merge + expected = len(revs) - 1 + # Since we do warmup properly, we can expect the cache to be hot + # for everythin under the merge we investigate + cache = self._depthcache + # note: we cannot do a binary search because element under the + # inherited point might have mismatching depth because of inner + # branching. + for rev in i: + if cache[rev] == expected: + break + expected -= 1 + value = (expected - 1, rev) + self._inheritancecache[merge] = value + return value + + def _depthmerge(self, cl, rev, p1, p2, stack): + # sub method to simplify the main 'depthrev' one + revdepth = None + depth = self._getdepth + depth_p1 = depth(p1) + depth_p2 = depth(p2) + missingparent = False + if depth_p1 is None: + stack.append(p1) + missingparent = True + if depth_p2 is None: + stack.append(p2) + missingparent = True + if missingparent: + return None + # computin depth of a merge + # XXX the common ancestors heads could be cached + ancnodes = cl.commonancestorsheads(cl.node(p1), cl.node(p2)) + ancrevs = [cl.rev(a) for a in ancnodes] + anyunkown = False + ancdepth = [] + for r in ancrevs: + d = depth(r) + if d is None: + anyunkown = True + stack.append(r) + ancdepth.append((r, d)) + if anyunkown: + return None + if not ancrevs: + # unrelated branch, (no common root) + revdepth = depth_p1 + depth_p2 + 1 + elif len(ancrevs) == 1: + # one unique branch point: + # we can compute depth without any walk + depth_anc = ancdepth[0][1] + revdepth = depth_p1 + (depth_p2 - depth_anc) + 1 + else: + # multiple ancestors, we pick one that is + # * the deepest (less changeset outside of it), + # * lowest revs because more chance to have descendant of other "above" + anc, revdepth = max(ancdepth, key=lambda x: (x[1], -x[0])) + revdepth += len(cl.findmissingrevs(common=[anc], heads=[rev])) + return revdepth + + def _subranges(self, repo, rangeid): + if self.rangelength(repo, rangeid) == 1: + return [] + slicepoint = self._slicepoint(repo, rangeid) + + # make sure we have cache for all relevant parent first to prevent + # recursion (python is bad with recursion + stack = [] + current = rangeid + while current is not None: + current = self._cold_reusable(repo, current, slicepoint) + if current is not None: + stack.append(current) + while stack: + # these call will directly compute the subranges + self.subranges(repo, stack.pop()) + return self._slicesrangeat(repo, rangeid, slicepoint) + + def _cold_reusable(self, repo, rangeid, slicepoint): + """return parent range that it would be useful to prepare to slice + rangeid at slicepoint + + This function also have the important task to update the revscache of + the parent rev s if possible and needed""" + p1, p2 = self._parents(rangeid[0], repo.changelog.parentrevs) + if p2 == nodemod.nullrev: + # regular changesets, we pick the parent + reusablerev = p1 + else: + # merge, we try the inheritance point + # if it is too low, it will be ditched by the depth check anyway + index, reusablerev = self._inheritancepoint(repo, rangeid[0]) + + # if we reached the slicepoint, no need to go further + if self.depthrev(repo, reusablerev) <= slicepoint: + return None + + reurange = (reusablerev, rangeid[1]) + # if we have an entry for the current range, lets update the cache + # if we already have subrange for this range, no need to prepare it. + if self._getsub(reurange) is not None: + return None + + # look like we found a relevent parentrange with no cache yet + return reurange + + def _slicepoint(self, repo, rangeid): + rangedepth = self.depthrev(repo, rangeid[0]) + step = _hlp2(rangedepth) + standard_start = 0 + while standard_start < rangeid[1] and 0 < step: + if standard_start + step < rangedepth: + standard_start += step + step //= 2 + if rangeid[1] == standard_start: + slicesize = _hlp2(self.rangelength(repo, rangeid)) + slicepoint = rangeid[1] + slicesize + else: + assert standard_start < rangedepth + slicepoint = standard_start + return slicepoint + + def _slicesrangeat(self, repo, rangeid, globalindex): + p1, p2 = self._parents(rangeid[0], repo.changelog.parentrevs) + if p2 == nodemod.nullrev: + reuserev = p1 + else: + index, reuserev = self._inheritancepoint(repo, rangeid[0]) + if index < globalindex: + return self._slicesrangeatmerge(repo, rangeid, globalindex) + + assert reuserev != nodemod.nullrev + + reuserange = (reuserev, rangeid[1]) + top = (rangeid[0], globalindex) + + if rangeid[1] + self.rangelength(repo, reuserange) == globalindex: + return [reuserange, top] + # This will not initiate a recursion since we took appropriate + # precaution in the caller of this method to ensure it will be so. + # It the parent is a merge that will not be the case but computing + # subranges from a merge will not recurse. + reusesubranges = self.subranges(repo, reuserange) + slices = reusesubranges[:-1] # pop the top + slices.append(top) + return slices + + def _slicesrangeatmerge(self, repo, rangeid, globalindex): + localindex = globalindex - rangeid[1] + cl = repo.changelog + + result = [] + allrevs = self.revsfromrange(repo, rangeid) + bottomrevs = allrevs[:localindex] + + if globalindex == self.depthrev(repo, bottomrevs[-1]): + # simple case, top revision in the bottom set contains exactly the + # revision we needs + result.append((bottomrevs[-1], rangeid[1])) + else: + parentrevs = cl.parentrevs + parents = self._parents + bheads = set(bottomrevs) + du = bheads.difference_update + reachableroots = repo.changelog.reachableroots + minrev = min(bottomrevs) + for r in bottomrevs: + du(parents(r, parentrevs)) + for h in bheads: + # reachable roots is fast because is C + # + # It is worth noting that will use this kind of filtering from + # "h" multiple time in a warming run. So using "ancestors" and + # caching that should be faster. But python code filtering on + # the ancestors end up being slower. + hrevs = reachableroots(minrev, [h], bottomrevs, True) + start = self.depthrev(repo, h) - len(hrevs) + entry = (h, start) + result.append(entry) + + # Talking about python code being slow, the following code is an + # alternative implementation. + # + # It complexity is better since is does a single traversal on the + # bottomset. However since it is all python it end up being + # slower. + # I'm keeping it here as an inspiration for a future C version + # branches = [] + # for current in reversed(bottomrevs): + # ps = parents(current, parentrevs) + # found = False + # for brevs, bexpect in branches: + # if current in bexpect: + # found = True + # brevs.append(current) + # bexpect.discard(current) + # bexpect.update(ps) + # if not found: + # branches.append(([current], set(ps))) + # for revs, __ in reversed(branches): + # head = revs[0] + # index = self.depthrev(repo, head) - len(revs) + # result.append((head, index)) + + # top part is trivial + top = (rangeid[0], globalindex) + result.append(top) + return result + +############################# +### simple sqlite caching ### +############################# + +_sqliteschema = [ + """CREATE TABLE meta(schemaversion INTEGER NOT NULL, + tiprev INTEGER NOT NULL, + tipnode BLOB NOT NULL + );""", + "CREATE TABLE depth(rev INTEGER NOT NULL PRIMARY KEY, depth INTEGER NOT NULL);", + """CREATE TABLE range(rev INTEGER NOT NULL, + idx INTEGER NOT NULL, + PRIMARY KEY(rev, idx));""", + """CREATE TABLE subranges(listidx INTEGER NOT NULL, + suprev INTEGER NOT NULL, + supidx INTEGER NOT NULL, + subrev INTEGER NOT NULL, + subidx INTEGER NOT NULL, + PRIMARY KEY(listidx, suprev, supidx), + FOREIGN KEY (suprev, supidx) REFERENCES range(rev, idx), + FOREIGN KEY (subrev, subidx) REFERENCES range(rev, idx) + );""", + "CREATE INDEX subranges_index ON subranges (suprev, supidx);", + "CREATE INDEX range_index ON range (rev, idx);", + "CREATE INDEX depth_index ON depth (rev);" +] +_newmeta = "INSERT INTO meta (schemaversion, tiprev, tipnode) VALUES (?,?,?);" +_updatemeta = "UPDATE meta SET tiprev = ?, tipnode = ?;" +_updatedepth = "INSERT INTO depth(rev, depth) VALUES (?,?);" +_updaterange = "INSERT INTO range(rev, idx) VALUES (?,?);" +_updatesubranges = """INSERT + INTO subranges(listidx, suprev, supidx, subrev, subidx) + VALUES (?,?,?,?,?);""" +_queryexist = "SELECT name FROM sqlite_master WHERE type='table' AND name='meta';" +_querymeta = "SELECT schemaversion, tiprev, tipnode FROM meta;" +_querydepth = "SELECT depth FROM depth WHERE rev = ?;" +_batchdepth = "SELECT rev, depth FROM depth;" +_queryrange = "SELECT * FROM range WHERE (rev = ? AND idx = ?);" +_querysubranges = """SELECT subrev, subidx + FROM subranges + WHERE (suprev = ? AND supidx = ?) + ORDER BY listidx;""" + +class sqlstablerange(stablerange): + + _schemaversion = 0 + + def __init__(self, repo): + super(sqlstablerange, self).__init__() + self._path = repo.vfs.join('cache/evoext_stablerange_v0.sqlite') + self._cl = repo.unfiltered().changelog # (okay to keep an old one) + self._ondisktiprev = None + self._ondisktipnode = None + self._unsaveddepth = {} + self._unsavedsubranges = {} + self._fulldepth = False + + def warmup(self, repo, upto=None): + self._con # make sure the data base is loaded + try: + # samelessly lock the repo to ensure nobody will update the repo + # concurently. This should not be too much of an issue if we warm + # at the end of the transaction. + # + # XXX However, we lock even if we are up to date so we should check + # before locking + with repo.lock(): + super(sqlstablerange, self).warmup(repo, upto) + self._save(repo) + except error.LockError: + # Exceptionnally we are noisy about it since performance impact is + # large We should address that before using this more widely. + repo.ui.warn('stable-range cache: unable to lock repo while warming\n') + repo.ui.warn('(cache will not be saved)\n') + super(sqlstablerange, self).warmup(repo, upto) + + def _getdepth(self, rev): + cache = self._depthcache + if rev not in cache and rev <= self._ondisktiprev and self._con is not None: + value = None + result = self._con.execute(_querydepth, (rev,)).fetchone() + if result is not None: + value = result[0] + # in memory caching of the value + cache[rev] = value + return cache.get(rev) + + def _setdepth(self, rev, depth): + assert rev not in self._unsaveddepth + self._unsaveddepth[rev] = depth + super(sqlstablerange, self)._setdepth(rev, depth) + + def _getsub(self, rangeid): + cache = self._subrangescache + if rangeid not in cache and rangeid[0] <= self._ondisktiprev and self._con is not None: + value = None + result = self._con.execute(_queryrange, rangeid).fetchone() + if result is not None: # database know about this node (skip in the future?) + value = self._con.execute(_querysubranges, rangeid).fetchall() + # in memory caching of the value + cache[rangeid] = value + return cache.get(rangeid) + + def _setsub(self, rangeid, value): + assert rangeid not in self._unsavedsubranges + self._unsavedsubranges[rangeid] = value + super(sqlstablerange, self)._setsub(rangeid, value) + + def _inheritancepoint(self, *args, **kwargs): + self._loaddepth() + return super(sqlstablerange, self)._inheritancepoint(*args, **kwargs) + + @util.propertycache + def _con(self): + con = sqlite3.connect(self._path) + con.text_factory = str + cur = con.execute(_queryexist) + if cur.fetchone() is None: + return None + meta = con.execute(_querymeta).fetchone() + if meta is None: + return None + if meta[0] != self._schemaversion: + return None + if len(self._cl) <= meta[1]: + return None + if self._cl.node(meta[1]) != meta[2]: + return None + self._ondisktiprev = meta[1] + self._ondisktipnode = meta[2] + if self._tiprev < self._ondisktiprev: + self._tiprev = self._ondisktiprev + self._tipnode = self._ondisktipnode + return con + + def _save(self, repo): + repo = repo.unfiltered() + if not (self._unsavedsubranges or self._unsaveddepth): + return # no new data + + if self._con is None: + util.unlinkpath(self._path, ignoremissing=True) + if '_con' in vars(self): + del self._con + + con = sqlite3.connect(self._path) + con.text_factory = str + with con: + for req in _sqliteschema: + con.execute(req) + + meta = [self._schemaversion, + self._tiprev, + self._tipnode, + ] + con.execute(_newmeta, meta) + else: + con = self._con + meta = con.execute(_querymeta).fetchone() + if meta[2] != self._ondisktipnode or meta[1] != self._ondisktiprev: + # drifting is currently an issue because this means another + # process might have already added the cache line we are about + # to add. This will confuse sqlite + msg = _('stable-range cache: skipping write, ' + 'database drifted under my feet\n') + hint = _('(disk: %s-%s vs mem: %s%s)\n') + data = (meta[2], meta[1], self._ondisktiprev, self._ondisktipnode) + repo.ui.warn(msg) + repo.ui.warn(hint % data) + return + meta = [self._tiprev, + self._tipnode, + ] + con.execute(_updatemeta, meta) + + self._savedepth(con, repo) + self._saverange(con, repo) + con.commit() + self._ondisktiprev = self._tiprev + self._ondisktipnode = self._tipnode + self._unsaveddepth.clear() + self._unsavedsubranges.clear() + + def _savedepth(self, con, repo): + repo = repo.unfiltered() + data = self._unsaveddepth.items() + con.executemany(_updatedepth, data) + + def _loaddepth(self): + """batch load all data about depth""" + if not (self._fulldepth or self._con is None): + result = self._con.execute(_batchdepth) + self._depthcache.update(result.fetchall()) + self._fulldepth = True + + def _saverange(self, con, repo): + repo = repo.unfiltered() + data = [] + allranges = set() + for key, value in self._unsavedsubranges.items(): + allranges.add(key) + for idx, sub in enumerate(value): + data.append((idx, key[0], key[1], sub[0], sub[1])) + + con.executemany(_updaterange, allranges) + con.executemany(_updatesubranges, data) + + +@eh.reposetup +def setupcache(ui, repo): + + class stablerangerepo(repo.__class__): + + @localrepo.unfilteredpropertycache + def stablerange(self): + return sqlstablerange(repo) + + @localrepo.unfilteredmethod + def destroyed(self): + if 'stablerange' in vars(self): + del self.stablerange + + def transaction(self, *args, **kwargs): + tr = super(stablerangerepo, self).transaction(*args, **kwargs) + if not repo.ui.configbool('experimental', 'obshashrange', False): + return tr + reporef = weakref.ref(self) + + def _warmcache(tr): + repo = reporef() + if repo is None: + return + if 'node' in tr.hookargs: + # new nodes ! + repo.stablerange.warmup(repo) + + tr.addpostclose('warmcache-stablerange', _warmcache) + return tr + + repo.__class__ = stablerangerepo diff -r efda653c96a7 -r 94432e742a02 hgext3rd/evolve/utility.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/evolve/utility.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,20 @@ +# Various utility function for the evolve extension +# +# Copyright 2017 Pierre-Yves David +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +def obsexcmsg(ui, message, important=False): + verbose = ui.configbool('experimental', 'verbose-obsolescence-exchange', + False) + if verbose: + message = 'OBSEXC: ' + message + if important or verbose: + ui.status(message) + +def obsexcprg(ui, *args, **kwargs): + topic = 'obsmarkers exchange' + if ui.configbool('experimental', 'verbose-obsolescence-exchange', False): + topic = 'OBSEXC' + ui.progress(topic, *args, **kwargs) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/__init__.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,475 @@ +# __init__.py - topic extension +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +"""support for topic branches + +Topic branches are lightweight branches which disappear when changes are +finalized (move to the public phase). + +Compared to bookmark, topic is reference carried by each changesets of the +series instead of just the single head revision. Topic are quite similar to +the way named branch work, except they eventualy fade away when the changeset +becomes part of the immutable history. Changeset can below to both a topic and +a named branch, but as long as it is mutable, its topic identity will prevail. +As a result, default destination for 'update', 'merge', etc... will take topic +into account. When a topic is active these operations will only consider other +changesets on that topic (and, in some occurence, bare changeset on same +branch). When no topic is active, changeset with topic will be ignored and +only bare one on the same branch will be taken in account. + +There is currently two commands to be used with that extension: 'topics' and +'stack'. + +The 'hg topics' command is used to set the current topic and list existing one. +'hg topics --verbose' will list various information related to each topic. + +The 'stack' will show you in formation about the stack of commit belonging to +your current topic. + +Topic is offering you aliases reference to changeset in your current topic +stack as 't#'. For example, 't1' refers to the root of your stack, 't2' to the +second commits, etc. The 'hg stack' command show these number. + +Push behavior will change a bit with topic. When pushing to a publishing +repository the changesets will turn public and the topic data on them will fade +away. The logic regarding pushing new heads will behave has before, ignore any +topic related data. When pushing to a non-publishing repository (supporting +topic), the head checking will be done taking topic data into account. +Push will complain about multiple heads on a branch if you push multiple heads +with no topic information on them (or multiple public heads). But pushing a new +topic will not requires any specific flag. However, pushing multiple heads on a +topic will be met with the usual warning. + +The 'evolve' extension takes 'topic' into account. 'hg evolve --all' +will evolve all changesets in the active topic. In addition, by default. 'hg +next' and 'hg prev' will stick to the current topic. + +Be aware that this extension is still an experiment, commands and other features +are likely to be change/adjusted/dropped over time as we refine the concept. +""" + +from __future__ import absolute_import + +import re + +from mercurial.i18n import _ +from mercurial import ( + branchmap, + cmdutil, + commands, + context, + error, + extensions, + localrepo, + lock, + merge, + namespaces, + node, + obsolete, + patch, + phases, + util, +) + +from . import ( + constants, + revset as topicrevset, + destination, + stack, + topicmap, + discovery, +) + +cmdtable = {} +command = cmdutil.command(cmdtable) +colortable = {'topic.active': 'green', + 'topic.list.troubledcount': 'red', + 'topic.list.headcount.multiple': 'yellow', + 'topic.list.behindcount': 'cyan', + 'topic.list.behinderror': 'red', + 'topic.stack.index': 'yellow', + 'topic.stack.index.base': 'none dim', + 'topic.stack.desc.base': 'none dim', + 'topic.stack.state.base': 'dim', + 'topic.stack.state.clean': 'green', + 'topic.stack.index.current': 'cyan', # random pick + 'topic.stack.state.current': 'cyan bold', # random pick + 'topic.stack.desc.current': 'cyan', # random pick + 'topic.stack.state.unstable': 'red', + 'topic.stack.summary.behindcount': 'cyan', + 'topic.stack.summary.behinderror': 'red', + 'topic.stack.summary.headcount.multiple': 'yellow', + } + +testedwith = '3.9' + +def _contexttopic(self): + return self.extra().get(constants.extrakey, '') +context.basectx.topic = _contexttopic + +topicrev = re.compile(r'^t\d+$') + +def _namemap(repo, name): + if topicrev.match(name): + idx = int(name[1:]) + topic = repo.currenttopic + if not topic: + raise error.Abort(_('cannot resolve "%s": no active topic') % name) + revs = list(stack.getstack(repo, topic)) + try: + r = revs[idx - 1] + except IndexError: + msg = _('cannot resolve "%s": topic "%s" has only %d changesets') + raise error.Abort(msg % (name, topic, len(revs))) + return [repo[r].node()] + if name not in repo.topics: + return [] + return [ctx.node() for ctx in + repo.set('not public() and extra(topic, %s)', name)] + +def _nodemap(repo, node): + ctx = repo[node] + t = ctx.topic() + if t and ctx.phase() > phases.public: + return [t] + return [] + +def uisetup(ui): + destination.modsetup(ui) + topicrevset.modsetup(ui) + discovery.modsetup(ui) + topicmap.modsetup(ui) + setupimportexport(ui) + + extensions.afterloaded('rebase', _fixrebase) + + entry = extensions.wrapcommand(commands.table, 'commit', commitwrap) + entry[1].append(('t', 'topic', '', + _("use specified topic"), _('TOPIC'))) + + extensions.wrapfunction(cmdutil, 'buildcommittext', committextwrap) + extensions.wrapfunction(merge, 'update', mergeupdatewrap) + cmdutil.summaryhooks.add('topic', summaryhook) + + +def reposetup(ui, repo): + orig = repo.__class__ + if not isinstance(repo, localrepo.localrepository): + return # this can be a peer in the ssh case (puzzling) + + class topicrepo(repo.__class__): + + def _restrictcapabilities(self, caps): + caps = super(topicrepo, self)._restrictcapabilities(caps) + caps.add('topics') + return caps + + def commit(self, *args, **kwargs): + backup = self.ui.backupconfig('ui', 'allowemptycommit') + try: + if repo.currenttopic != repo['.'].topic(): + # bypass the core "nothing changed" logic + self.ui.setconfig('ui', 'allowemptycommit', True) + return orig.commit(self, *args, **kwargs) + finally: + self.ui.restoreconfig(backup) + + def commitctx(self, ctx, error=None): + if isinstance(ctx, context.workingcommitctx): + current = self.currenttopic + if current: + ctx.extra()[constants.extrakey] = current + if (isinstance(ctx, context.memctx) and + ctx.extra().get('amend_source') and + ctx.topic() and + not self.currenttopic): + # we are amending and need to remove a topic + del ctx.extra()[constants.extrakey] + with topicmap.usetopicmap(self): + return orig.commitctx(self, ctx, error=error) + + @property + def topics(self): + if self._topics is not None: + return self._topics + topics = set(['', self.currenttopic]) + for c in self.set('not public()'): + topics.add(c.topic()) + topics.remove('') + self._topics = topics + return topics + + @property + def currenttopic(self): + return self.vfs.tryread('topic') + + def branchmap(self, topic=True): + if not topic: + super(topicrepo, self).branchmap() + with topicmap.usetopicmap(self): + branchmap.updatecache(self) + return self._topiccaches[self.filtername] + + def destroyed(self, *args, **kwargs): + with topicmap.usetopicmap(self): + return super(topicrepo, self).destroyed(*args, **kwargs) + + def invalidatevolatilesets(self): + # XXX we might be able to move this to something invalidated less often + super(topicrepo, self).invalidatevolatilesets() + self._topics = None + if '_topiccaches' in vars(self.unfiltered()): + self.unfiltered()._topiccaches.clear() + + def peer(self): + peer = super(topicrepo, self).peer() + if getattr(peer, '_repo', None) is not None: # localpeer + class topicpeer(peer.__class__): + def branchmap(self): + usetopic = not self._repo.publishing() + return self._repo.branchmap(topic=usetopic) + peer.__class__ = topicpeer + return peer + + repo.__class__ = topicrepo + repo._topics = None + if util.safehasattr(repo, 'names'): + repo.names.addnamespace(namespaces.namespace( + 'topics', 'topic', namemap=_namemap, nodemap=_nodemap, + listnames=lambda repo: repo.topics)) + +@command('topics [TOPIC]', [ + ('', 'clear', False, 'clear active topic if any'), + ('', 'change', '', 'revset of existing revisions to change topic'), + ('l', 'list', False, 'show the stack of changeset in the topic'), + ] + commands.formatteropts) +def topics(ui, repo, topic='', clear=False, change=None, list=False, **opts): + """View current topic, set current topic, or see all topics. + + The --verbose version of this command display various information on the state of each topic.""" + if list: + if clear or change: + raise error.Abort(_("cannot use --clear or --change with --list")) + if not topic: + topic = repo.currenttopic + if not topic: + raise error.Abort(_('no active topic to list')) + return stack.showstack(ui, repo, topic, opts) + + if change: + if not obsolete.isenabled(repo, obsolete.createmarkersopt): + raise error.Abort(_('must have obsolete enabled to use --change')) + if not topic and not clear: + raise error.Abort('changing topic requires a topic name or --clear') + if any(not c.mutable() for c in repo.set('%r and public()', change)): + raise error.Abort("can't change topic of a public change") + rewrote = 0 + needevolve = False + l = repo.lock() + txn = repo.transaction('rewrite-topics') + try: + for c in repo.set('%r', change): + def filectxfn(repo, ctx, path): + try: + return c[path] + except error.ManifestLookupError: + return None + fixedextra = dict(c.extra()) + ui.debug('old node id is %s\n' % node.hex(c.node())) + ui.debug('origextra: %r\n' % fixedextra) + newtopic = None if clear else topic + oldtopic = fixedextra.get(constants.extrakey, None) + if oldtopic == newtopic: + continue + if clear: + del fixedextra[constants.extrakey] + else: + fixedextra[constants.extrakey] = topic + if 'amend_source' in fixedextra: + # TODO: right now the commitctx wrapper in + # topicrepo overwrites the topic in extra if + # amend_source is set to support 'hg commit + # --amend'. Support for amend should be adjusted + # to not be so invasive. + del fixedextra['amend_source'] + ui.debug('changing topic of %s from %s to %s\n' % ( + c, oldtopic, newtopic)) + ui.debug('fixedextra: %r\n' % fixedextra) + mc = context.memctx( + repo, (c.p1().node(), c.p2().node()), c.description(), + c.files(), filectxfn, + user=c.user(), date=c.date(), extra=fixedextra) + newnode = repo.commitctx(mc) + ui.debug('new node id is %s\n' % node.hex(newnode)) + needevolve = needevolve or (len(c.children()) > 0) + obsolete.createmarkers(repo, [(c, (repo[newnode],))]) + rewrote += 1 + txn.close() + except: + try: + txn.abort() + finally: + repo.invalidate() + raise + finally: + lock.release(txn, l) + ui.status('changed topic on %d changes\n' % rewrote) + if needevolve: + evolvetarget = 'topic(%s)' % topic if topic else 'not topic()' + ui.status('please run hg evolve --rev "%s" now\n' % evolvetarget) + if clear: + if repo.vfs.exists('topic'): + repo.vfs.unlink('topic') + return + if topic: + with repo.wlock(): + with repo.vfs.open('topic', 'w') as f: + f.write(topic) + return + _listtopics(ui, repo, opts) + +@command('stack [TOPIC]', [] + commands.formatteropts) +def cmdstack(ui, repo, topic='', **opts): + """list all changesets in a topic and other information + + List the current topic by default.""" + if not topic: + topic = repo.currenttopic + if not topic: + raise error.Abort(_('no active topic to list')) + return stack.showstack(ui, repo, topic, opts) + +def _listtopics(ui, repo, opts): + fm = ui.formatter('bookmarks', opts) + activetopic = repo.currenttopic + namemask = '%s' + if repo.topics and ui.verbose: + maxwidth = max(len(t) for t in repo.topics) + namemask = '%%-%is' % maxwidth + for topic in sorted(repo.topics): + fm.startitem() + marker = ' ' + label = 'topic' + active = (topic == activetopic) + if active: + marker = '*' + label = 'topic.active' + if not ui.quiet: + # registering the active data is made explicitly later + fm.plain(' %s ' % marker, label=label) + fm.write('topic', namemask, topic, label=label) + fm.data(active=active) + if ui.verbose: + # XXX we should include the data even when not verbose + data = stack.stackdata(repo, topic) + fm.plain(' (') + fm.write('branches+', 'on branch: %s', + '+'.join(data['branches']), # XXX use list directly after 4.0 is released + label='topic.list.branches') + fm.plain(', ') + fm.write('changesetcount', '%d changesets', data['changesetcount'], + label='topic.list.changesetcount') + if data['troubledcount']: + fm.plain(', ') + fm.write('troubledcount', '%d troubled', + data['troubledcount'], + label='topic.list.troubledcount') + if 1 < data['headcount']: + fm.plain(', ') + fm.write('headcount', '%d heads', + data['headcount'], + label='topic.list.headcount.multiple') + if 0 < data['behindcount']: + fm.plain(', ') + fm.write('behindcount', '%d behind', + data['behindcount'], + label='topic.list.behindcount') + elif -1 == data['behindcount']: + fm.plain(', ') + fm.write('behinderror', '%s', + _('ambiguous destination'), + label='topic.list.behinderror') + fm.plain(')') + fm.plain('\n') + fm.end() + +def summaryhook(ui, repo): + t = repo.currenttopic + if not t: + return + # i18n: column positioning for "hg summary" + ui.write(_("topic: %s\n") % ui.label(t, 'topic.active')) + +def commitwrap(orig, ui, repo, *args, **opts): + with repo.wlock(): + if opts.get('topic'): + t = opts['topic'] + with repo.vfs.open('topic', 'w') as f: + f.write(t) + return orig(ui, repo, *args, **opts) + +def committextwrap(orig, repo, ctx, subs, extramsg): + ret = orig(repo, ctx, subs, extramsg) + t = repo.currenttopic + if t: + ret = ret.replace("\nHG: branch", + "\nHG: topic '%s'\nHG: branch" % t) + return ret + +def mergeupdatewrap(orig, repo, node, branchmerge, force, *args, **kwargs): + matcher = kwargs.get('matcher') + partial = not (matcher is None or matcher.always()) + wlock = repo.wlock() + try: + ret = orig(repo, node, branchmerge, force, *args, **kwargs) + if not partial and not branchmerge: + ot = repo.currenttopic + t = '' + pctx = repo[node] + if pctx.phase() > phases.public: + t = pctx.topic() + with repo.vfs.open('topic', 'w') as f: + f.write(t) + if t and t != ot: + repo.ui.status(_("switching to topic %s\n") % t) + return ret + finally: + wlock.release() + +def _fixrebase(loaded): + if not loaded: + return + + def savetopic(ctx, extra): + if ctx.topic(): + extra[constants.extrakey] = ctx.topic() + + def newmakeextrafn(orig, copiers): + return orig(copiers + [savetopic]) + + try: + rebase = extensions.find("rebase") + extensions.wrapfunction(rebase, '_makeextrafn', newmakeextrafn) + except KeyError: + pass + +## preserve topic during import/export + +def _exporttopic(seq, ctx): + topic = ctx.topic() + if topic: + return 'EXP-Topic %s' % topic + return None + +def _importtopic(repo, patchdata, extra, opts): + if 'topic' in patchdata: + extra['topic'] = patchdata['topic'] + +def setupimportexport(ui): + """run at ui setup time to install import/export logic""" + cmdutil.extraexport.append('topic') + cmdutil.extraexportmap['topic'] = _exporttopic + cmdutil.extrapreimport.append('topic') + cmdutil.extrapreimportmap['topic'] = _importtopic + patch.patchheadermap.append(('EXP-Topic', 'topic')) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/constants.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,1 @@ +extrakey = 'topic' diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/destination.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/destination.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,118 @@ +from __future__ import absolute_import + +from mercurial.i18n import _ +from mercurial import ( + bookmarks, + destutil, + error, + extensions, + util, +) +from . import topicmap +from .evolvebits import builddependencies + +def _destmergebranch(orig, repo, action='merge', sourceset=None, + onheadcheck=True, destspace=None): + # XXX: take destspace into account + if sourceset is None: + p1 = repo['.'] + else: + # XXX: using only the max here is flacky. That code should eventually + # be updated to take care of the whole sourceset. + p1 = repo[max(sourceset)] + top = p1.topic() + if top: + revs = repo.revs('topic(%s) - obsolete()', top) + deps, rdeps = builddependencies(repo, revs) + heads = [r for r in revs if not rdeps[r]] + if onheadcheck and p1.rev() not in heads: + raise error.Abort(_("not at topic head, update or explicit")) + + # prune heads above the source + otherheads = set(heads) + pool = set([p1.rev()]) + while pool: + current = pool.pop() + otherheads.discard(current) + pool.update(rdeps[current]) + if not otherheads: + # nothing to do at the topic level + bhead = ngtip(repo, p1.branch(), all=True) + if not bhead: + raise error.NoMergeDestAbort(_("nothing to merge")) + elif 1 == len(bhead): + return bhead[0] + else: + msg = _("branch '%s' has %d heads " + "- please merge with an explicit rev") + hint = _("run 'hg heads .' to see heads") + raise error.ManyMergeDestAbort(msg % (p1.branch(), len(bhead)), + hint=hint) + elif len(otherheads) == 1: + return otherheads.pop() + else: + msg = _("topic '%s' has %d heads " + "- please merge with an explicit rev") % (top, len(heads)) + raise error.ManyMergeDestAbort(msg) + if len(getattr(orig, 'func_defaults', ())) == 3: # version hg-3.7 + return orig(repo, action, sourceset, onheadcheck) + if 3 < len(getattr(orig, 'func_defaults', ())): # version hg-3.8 and above + return orig(repo, action, sourceset, onheadcheck, destspace=destspace) + else: + return orig(repo) + +def _destupdatetopic(repo, clean, check=None): + """decide on an update destination from current topic""" + movemark = node = None + topic = repo.currenttopic + revs = repo.revs('.::topic("%s")' % topic) + if not revs: + return None, None, None + node = revs.last() + if bookmarks.isactivewdirparent(repo): + movemark = repo['.'].node() + return node, movemark, None + +def desthistedit(orig, ui, repo): + if not (ui.config('histedit', 'defaultrev', None) is None + and repo.currenttopic): + return orig(ui, repo) + revs = repo.revs('::. and stack()') + if revs: + return revs.min() + return None + +def ngtip(repo, branch, all=False): + """tip new generation""" + ## search for untopiced heads of branch + # could be heads((::branch(x) - topic())) + # but that is expensive + # + # we should write plain code instead + with topicmap.usetopicmap(repo): + tmap = repo.branchmap() + if branch not in tmap: + return [] + elif all: + return tmap.branchheads(branch) + else: + return [tmap.branchtip(branch)] + +def modsetup(ui): + """run a uisetup time to install all destinations wrapping""" + if util.safehasattr(destutil, '_destmergebranch'): + extensions.wrapfunction(destutil, '_destmergebranch', _destmergebranch) + try: + rebase = extensions.find('rebase') + except KeyError: + rebase = None + if (util.safehasattr(rebase, '_destrebase') + # logic not shared with merge yet < hg-3.8 + and not util.safehasattr(rebase, '_definesets')): + extensions.wrapfunction(rebase, '_destrebase', _destmergebranch) + if util.safehasattr(destutil, 'destupdatesteps'): + bridx = destutil.destupdatesteps.index('branch') + destutil.destupdatesteps.insert(bridx, 'topic') + destutil.destupdatestepmap['topic'] = _destupdatetopic + if util.safehasattr(destutil, 'desthistedit'): + extensions.wrapfunction(destutil, 'desthistedit', desthistedit) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/discovery.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/discovery.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,135 @@ +from __future__ import absolute_import + +import weakref + +from mercurial.i18n import _ +from mercurial import ( + branchmap, + bundle2, + discovery, + error, + exchange, + extensions, + wireproto, +) + +from . import topicmap + +def _headssummary(orig, repo, remote, outgoing): + publishing = ('phases' not in remote.listkeys('namespaces') + or bool(remote.listkeys('phases').get('publishing', False))) + if publishing or not remote.capable('topics'): + return orig(repo, remote, outgoing) + oldrepo = repo.__class__ + oldbranchcache = branchmap.branchcache + oldfilename = branchmap._filename + try: + class repocls(repo.__class__): + def __getitem__(self, key): + ctx = super(repocls, self).__getitem__(key) + oldbranch = ctx.branch + + def branch(): + branch = oldbranch() + topic = ctx.topic() + if topic: + branch = "%s:%s" % (branch, topic) + return branch + + ctx.branch = branch + return ctx + + repo.__class__ = repocls + branchmap.branchcache = topicmap.topiccache + branchmap._filename = topicmap._filename + summary = orig(repo, remote, outgoing) + for key, value in summary.iteritems(): + if ':' in key: # This is a topic + if value[0] is None and value[1]: + summary[key] = ([value[1].pop(0)], ) + value[1:] + return summary + finally: + repo.__class__ = oldrepo + branchmap.branchcache = oldbranchcache + branchmap._filename = oldfilename + +def wireprotobranchmap(orig, repo, proto): + oldrepo = repo.__class__ + try: + class repocls(repo.__class__): + def branchmap(self): + usetopic = not self.publishing() + return super(repocls, self).branchmap(topic=usetopic) + repo.__class__ = repocls + return orig(repo, proto) + finally: + repo.__class__ = oldrepo + + +# Discovery have deficiency around phases, branch can get new heads with pure +# phases change. This happened with a changeset was allowed to be pushed +# because it had a topic, but it later become public and create a new branch +# head. +# +# Handle this by doing an extra check for new head creation server side +def _nbheads(repo): + data = {} + for b in repo.branchmap().iterbranches(): + if ':' in b[0]: + continue + data[b[0]] = len(b[1]) + return data + +def handlecheckheads(orig, op, inpart): + orig(op, inpart) + if op.repo.publishing(): + return + tr = op.gettransaction() + if tr.hookargs['source'] not in ('push', 'serve'): # not a push + return + tr._prepushheads = _nbheads(op.repo) + reporef = weakref.ref(op.repo) + oldvalidator = tr.validator + + def validator(tr): + repo = reporef() + if repo is not None: + repo.invalidatecaches() + finalheads = _nbheads(repo) + for branch, oldnb in tr._prepushheads.iteritems(): + newnb = finalheads.pop(branch, 0) + if oldnb < newnb: + msg = _('push create a new head on branch "%s"' % branch) + raise error.Abort(msg) + for branch, newnb in finalheads.iteritems(): + if 1 < newnb: + msg = _('push create more than 1 head on new branch "%s"' + % branch) + raise error.Abort(msg) + return oldvalidator(tr) + tr.validator = validator +handlecheckheads.params = frozenset() + +def _pushb2phases(orig, pushop, bundler): + hascheck = any(p.type == 'check:heads' for p in bundler._parts) + if pushop.outdatedphases and not hascheck: + exchange._pushb2ctxcheckheads(pushop, bundler) + return orig(pushop, bundler) + +def wireprotocaps(orig, repo, proto): + caps = orig(repo, proto) + if repo.peer().capable('topics'): + caps.append('topics') + return caps + +def modsetup(ui): + """run at uisetup time to install all destinations wrapping""" + extensions.wrapfunction(discovery, '_headssummary', _headssummary) + extensions.wrapfunction(wireproto, 'branchmap', wireprotobranchmap) + extensions.wrapfunction(wireproto, '_capabilities', wireprotocaps) + extensions.wrapfunction(bundle2, 'handlecheckheads', handlecheckheads) + # we need a proper wrap b2 part stuff + bundle2.handlecheckheads.params = frozenset() + bundle2.parthandlermapping['check:heads'] = bundle2.handlecheckheads + extensions.wrapfunction(exchange, '_pushb2phases', _pushb2phases) + exchange.b2partsgenmapping['phase'] = exchange._pushb2phases diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/evolvebits.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/evolvebits.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,96 @@ +import collections +from mercurial import obsolete + +# Copied from evolve 081605c2e9b6 + +def _orderrevs(repo, revs): + """Compute an ordering to solve instability for the given revs + + revs is a list of unstable revisions. + + Returns the same revisions ordered to solve their instability from the + bottom to the top of the stack that the stabilization process will produce + eventually. + + This ensures the minimal number of stabilizations, as we can stabilize each + revision on its final stabilized destination. + """ + # Step 1: Build the dependency graph + dependencies, rdependencies = builddependencies(repo, revs) + # Step 2: Build the ordering + # Remove the revisions with no dependency(A) and add them to the ordering. + # Removing these revisions leads to new revisions with no dependency (the + # one depending on A) that we can remove from the dependency graph and add + # to the ordering. We progress in a similar fashion until the ordering is + # built + solvablerevs = [r for r in sorted(dependencies.keys()) + if not dependencies[r]] + ordering = [] + while solvablerevs: + rev = solvablerevs.pop() + for dependent in rdependencies[rev]: + dependencies[dependent].remove(rev) + if not dependencies[dependent]: + solvablerevs.append(dependent) + del dependencies[rev] + ordering.append(rev) + + ordering.extend(sorted(dependencies)) + return ordering + +def builddependencies(repo, revs): + """returns dependency graphs giving an order to solve instability of revs + (see _orderrevs for more information on usage)""" + + # For each troubled revision we keep track of what instability if any should + # be resolved in order to resolve it. Example: + # dependencies = {3: [6], 6:[]} + # Means that: 6 has no dependency, 3 depends on 6 to be solved + dependencies = {} + # rdependencies is the inverted dict of dependencies + rdependencies = collections.defaultdict(set) + + for r in revs: + dependencies[r] = set() + for p in repo[r].parents(): + try: + succ = _singlesuccessor(repo, p) + except MultipleSuccessorsError as exc: + dependencies[r] = exc.successorssets + continue + if succ in revs: + dependencies[r].add(succ) + rdependencies[succ].add(r) + return dependencies, rdependencies + +def _singlesuccessor(repo, p): + """returns p (as rev) if not obsolete or its unique latest successors + + fail if there are no such successor""" + + if not p.obsolete(): + return p.rev() + obs = repo[p] + ui = repo.ui + newer = obsolete.successorssets(repo, obs.node()) + # search of a parent which is not killed + while not newer: + ui.debug("stabilize target %s is plain dead," + " trying to stabilize on its parent\n" % + obs) + obs = obs.parents()[0] + newer = obsolete.successorssets(repo, obs.node()) + if len(newer) > 1 or len(newer[0]) > 1: + raise MultipleSuccessorsError(newer) + + return repo[newer[0][0]].rev() + +class MultipleSuccessorsError(RuntimeError): + """Exception raised by _singlesuccessor when multiple successor sets exists + + The object contains the list of successorssets in its 'successorssets' + attribute to call to easily recover. + """ + + def __init__(self, successorssets): + self.successorssets = successorssets diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/revset.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/revset.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,75 @@ +from __future__ import absolute_import + +from mercurial.i18n import _ +from mercurial import ( + error, + revset, + util, +) + +from . import ( + constants, + destination, + stack, +) + +try: + mkmatcher = revset._stringmatcher +except AttributeError: + mkmatcher = util.stringmatcher + + +def topicset(repo, subset, x): + """`topic([topic])` + Specified topic or all changes with any topic specified. + + If `topic` starts with `re:` the remainder of the name is treated + as a regular expression. + + TODO: make `topic(revset)` work the same as `branch(revset)`. + """ + args = revset.getargs(x, 0, 1, 'topic takes one or no arguments') + if args: + # match a specific topic + topic = revset.getstring(args[0], 'topic() argument must be a string') + if topic == '.': + topic = repo['.'].extra().get('topic', '') + _kind, _pattern, matcher = mkmatcher(topic) + else: + matcher = lambda t: bool(t) + drafts = subset.filter(lambda r: repo[r].mutable()) + return drafts.filter( + lambda r: matcher(repo[r].extra().get(constants.extrakey, ''))) + +def ngtipset(repo, subset, x): + """`ngtip([branch])` + + The untopiced tip. + + Name is horrible so that people change it. + """ + args = revset.getargs(x, 1, 1, 'topic takes one') + # match a specific topic + branch = revset.getstring(args[0], 'ngtip() argument must be a string') + if branch == '.': + branch = repo['.'].branch() + return subset & revset.baseset(destination.ngtip(repo, branch)) + +def stackset(repo, subset, x): + """`stack()` + All relevant changes in the current topic, + + This is roughly equivalent to 'topic(.) - obsolete' with a sorting moving + unstable changeset after there future parent (as if evolve where already + run).""" + topic = repo.currenttopic + if not topic: + raise error.Abort(_('no active topic to list')) + # ordering hack, boo + return revset.baseset(stack.getstack(repo, topic)) & subset + + +def modsetup(ui): + revset.symbols.update({'topic': topicset}) + revset.symbols.update({'ngtip': ngtipset}) + revset.symbols.update({'stack': stackset}) diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/stack.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/stack.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,121 @@ +# stack.py - code related to stack workflow +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +from mercurial.i18n import _ +from mercurial import ( + destutil, + error, + node, +) +from .evolvebits import builddependencies, _orderrevs, _singlesuccessor + +def getstack(repo, topic): + # XXX need sorting + trevs = repo.revs("topic(%s) - obsolete()", topic) + return _orderrevs(repo, trevs) + +def showstack(ui, repo, topic, opts): + fm = ui.formatter('topicstack', opts) + prev = None + entries = [] + idxmap = {} + + label = 'topic' + if topic == repo.currenttopic: + label = 'topic.active' + + data = stackdata(repo, topic) + fm.plain(_('### topic: %s') % ui.label(topic, label), + label='topic.stack.summary.topic') + + if 1 < data['headcount']: + fm.plain(' (') + fm.plain('%d heads' % data['headcount'], + label='topic.stack.summary.headcount.multiple') + fm.plain(')') + fm.plain('\n') + fm.plain(_('### branch: %s') + % '+'.join(data['branches']), # XXX handle multi branches + label='topic.stack.summary.branches') + if data['behindcount'] == -1: + fm.plain(', ') + fm.plain('ambigious rebase destination', label='topic.stack.summary.behinderror') + elif data['behindcount']: + fm.plain(', ') + fm.plain('%d behind' % data['behindcount'], label='topic.stack.summary.behindcount') + fm.plain('\n') + + for idx, r in enumerate(getstack(repo, topic), 1): + ctx = repo[r] + p1 = ctx.p1() + if p1.obsolete(): + p1 = repo[_singlesuccessor(repo, p1)] + if p1.rev() != prev and p1.node() != node.nullid: + entries.append((idxmap.get(p1.rev()), False, p1)) + entries.append((idx, True, ctx)) + idxmap[ctx.rev()] = idx + prev = r + + # super crude initial version + for idx, isentry, ctx in entries[::-1]: + if not isentry: + symbol = '^' + state = 'base' + elif repo.revs('%d and parents()', ctx.rev()): + symbol = '@' + state = 'current' + elif repo.revs('%d and unstable()', ctx.rev()): + symbol = '$' + state = 'unstable' + else: + symbol = ':' + state = 'clean' + fm.startitem() + fm.data(isentry=isentry) + if idx is None: + fm.plain(' ') + else: + fm.write('topic.stack.index', 't%d', idx, + label='topic.stack.index topic.stack.index.%s' % state) + fm.write('topic.stack.state.symbol', '%s', symbol, + label='topic.stack.state topic.stack.state.%s' % state) + fm.plain(' ') + fm.write('topic.stack.desc', '%s', ctx.description().splitlines()[0], + label='topic.stack.desc topic.stack.desc.%s' % state) + fm.condwrite(state != 'clean' and idx is not None, 'topic.stack.state', + ' (%s)', state, + label='topic.stack.state topic.stack.state.%s' % state) + fm.plain('\n') + fm.end() + +def stackdata(repo, topic): + """get various data about a stack + + :changesetcount: number of non-obsolete changesets in the stack + :troubledcount: number on troubled changesets + :headcount: number of heads on the topic + :behindcount: number of changeset on rebase destination + """ + data = {} + revs = repo.revs("topic(%s) - obsolete()", topic) + data['changesetcount'] = len(revs) + data['troubledcount'] = len([r for r in revs if repo[r].troubled()]) + deps, rdeps = builddependencies(repo, revs) + data['headcount'] = len([r for r in revs if not rdeps[r]]) + data['behindcount'] = 0 + if revs: + minroot = [min(r for r in revs if not deps[r])] + try: + dest = destutil.destmerge(repo, action='rebase', + sourceset=minroot, + onheadcheck=False) + data['behindcount'] = len(repo.revs("only(%d, %ld)", dest, + minroot)) + except error.NoMergeDestAbort: + data['behindcount'] = 0 + except error.ManyMergeDestAbort: + data['behindcount'] = -1 + data['branches'] = sorted(set(repo[r].branch() for r in revs)) + + return data diff -r efda653c96a7 -r 94432e742a02 hgext3rd/topic/topicmap.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext3rd/topic/topicmap.py Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,247 @@ +import contextlib +import hashlib + +from mercurial.node import hex, bin, nullid +from mercurial import ( + branchmap, + changegroup, + cmdutil, + encoding, + error, + extensions, + scmutil, +) + +def _filename(repo): + """name of a branchcache file for a given repo or repoview""" + filename = "cache/topicmap" + if repo.filtername: + filename = '%s-%s' % (filename, repo.filtername) + return filename + +oldbranchcache = branchmap.branchcache + +def _phaseshash(repo, maxrev): + revs = set() + cl = repo.changelog + fr = cl.filteredrevs + nm = cl.nodemap + for roots in repo._phasecache.phaseroots[1:]: + for n in roots: + r = nm.get(n) + if r not in fr and r < maxrev: + revs.add(r) + key = nullid + revs = sorted(revs) + if revs: + s = hashlib.sha1() + for rev in revs: + s.update('%s;' % rev) + key = s.digest() + return key + +@contextlib.contextmanager +def usetopicmap(repo): + """use awful monkey patching to ensure topic map usage + + During the extend of the context block, The topicmap should be used and + updated instead of the branchmap.""" + oldbranchcache = branchmap.branchcache + oldfilename = branchmap._filename + oldread = branchmap.read + oldcaches = getattr(repo, '_branchcaches', {}) + try: + branchmap.branchcache = topiccache + branchmap._filename = _filename + branchmap.read = readtopicmap + repo._branchcaches = getattr(repo, '_topiccaches', {}) + yield + repo._topiccaches = repo._branchcaches + finally: + repo._branchcaches = oldcaches + branchmap.branchcache = oldbranchcache + branchmap._filename = oldfilename + branchmap.read = oldread + +def cgapply(orig, repo, *args, **kwargs): + """make sure a topicmap is used when applying a changegroup""" + with usetopicmap(repo): + return orig(repo, *args, **kwargs) + +def commitstatus(orig, repo, node, branch, bheads=None, opts=None): + # wrap commit status use the topic branch heads + ctx = repo[node] + if ctx.topic() and ctx.branch() == branch: + bheads = repo.branchheads("%s:%s" % (branch, ctx.topic())) + return orig(repo, node, branch, bheads=bheads, opts=opts) + +class topiccache(oldbranchcache): + + def __init__(self, *args, **kwargs): + otherbranchcache = branchmap.branchcache + try: + # super() call may fail otherwise + branchmap.branchcache = oldbranchcache + super(topiccache, self).__init__(*args, **kwargs) + if self.filteredhash is None: + self.filteredhash = nullid + self.phaseshash = nullid + finally: + branchmap.branchcache = otherbranchcache + + def copy(self): + """return an deep copy of the branchcache object""" + new = topiccache(self, self.tipnode, self.tiprev, self.filteredhash, + self._closednodes) + if self.filteredhash is None: + self.filteredhash = nullid + new.phaseshash = self.phaseshash + return new + + def branchtip(self, branch, topic=''): + '''Return the tipmost open head on branch head, otherwise return the + tipmost closed head on branch. + Raise KeyError for unknown branch.''' + if topic: + branch = '%s:%s' % (branch, topic) + return super(topiccache, self).branchtip(branch) + + def branchheads(self, branch, closed=False, topic=''): + if topic: + branch = '%s:%s' % (branch, topic) + return super(topiccache, self).branchheads(branch, closed=closed) + + def validfor(self, repo): + """Is the cache content valid regarding a repo + + - False when cached tipnode is unknown or if we detect a strip. + - True when cache is up to date or a subset of current repo.""" + # This is copy paste of mercurial.branchmap.branchcache.validfor in + # 69077c65919d With a small changes to the cache key handling to + # include phase information that impact the topic cache. + # + # All code changes should be flagged on site. + try: + if (self.tipnode == repo.changelog.node(self.tiprev)): + fh = scmutil.filteredhash(repo, self.tiprev) + if fh is None: + fh = nullid + if ((self.filteredhash == fh) + and (self.phaseshash == _phaseshash(repo, self.tiprev))): + return True + return False + except IndexError: + return False + + def write(self, repo): + # This is copy paste of mercurial.branchmap.branchcache.write in + # 69077c65919d With a small changes to the cache key handling to + # include phase information that impact the topic cache. + # + # All code changes should be flagged on site. + try: + f = repo.vfs(_filename(repo), "w", atomictemp=True) + cachekey = [hex(self.tipnode), str(self.tiprev)] + # [CHANGE] we need a hash in all cases + assert self.filteredhash is not None + cachekey.append(hex(self.filteredhash)) + cachekey.append(hex(self.phaseshash)) + f.write(" ".join(cachekey) + '\n') + nodecount = 0 + for label, nodes in sorted(self.iteritems()): + for node in nodes: + nodecount += 1 + if node in self._closednodes: + state = 'c' + else: + state = 'o' + f.write("%s %s %s\n" % (hex(node), state, + encoding.fromlocal(label))) + f.close() + repo.ui.log('branchcache', + 'wrote %s branch cache with %d labels and %d nodes\n', + repo.filtername, len(self), nodecount) + except (IOError, OSError, error.Abort) as inst: + repo.ui.debug("couldn't write branch cache: %s\n" % inst) + # Abort may be raise by read only opener + pass + + def update(self, repo, revgen): + """Given a branchhead cache, self, that may have extra nodes or be + missing heads, and a generator of nodes that are strictly a superset of + heads missing, this function updates self to be correct. + """ + oldgetbranchinfo = repo.revbranchcache().branchinfo + try: + def branchinfo(r): + info = oldgetbranchinfo(r) + topic = '' + ctx = repo[r] + if ctx.mutable(): + topic = ctx.topic() + branch = info[0] + if topic: + branch = '%s:%s' % (branch, topic) + return (branch, info[1]) + repo.revbranchcache().branchinfo = branchinfo + super(topiccache, self).update(repo, revgen) + if self.filteredhash is None: + self.filteredhash = nullid + self.phaseshash = _phaseshash(repo, self.tiprev) + finally: + repo.revbranchcache().branchinfo = oldgetbranchinfo + +def readtopicmap(repo): + # This is copy paste of mercurial.branchmap.read in 69077c65919d + # With a small changes to the cache key handling to include phase + # information that impact the topic cache. + # + # All code changes should be flagged on site. + try: + f = repo.vfs(_filename(repo)) + lines = f.read().split('\n') + f.close() + except (IOError, OSError): + return None + + try: + cachekey = lines.pop(0).split(" ", 2) + last, lrev = cachekey[:2] + last, lrev = bin(last), int(lrev) + filteredhash = bin(cachekey[2]) # [CHANGE] unconditional filteredhash + partial = topiccache(tipnode=last, tiprev=lrev, + filteredhash=filteredhash) + partial.phaseshash = bin(cachekey[3]) # [CHANGE] read phaseshash + if not partial.validfor(repo): + # invalidate the cache + raise ValueError('tip differs') + cl = repo.changelog + for l in lines: + if not l: + continue + node, state, label = l.split(" ", 2) + if state not in 'oc': + raise ValueError('invalid branch state') + label = encoding.tolocal(label.strip()) + node = bin(node) + if not cl.hasnode(node): + raise ValueError('node %s does not exist' % hex(node)) + partial.setdefault(label, []).append(node) + if state == 'c': + partial._closednodes.add(node) + except KeyboardInterrupt: + raise + except Exception as inst: + if repo.ui.debugflag: + msg = 'invalid branchheads cache' + if repo.filtername is not None: + msg += ' (%s)' % repo.filtername + msg += ': %s\n' + repo.ui.debug(msg % inst) + partial = None + return partial + +def modsetup(ui): + """call at uisetup time to install various wrappings""" + extensions.wrapfunction(changegroup.cg1unpacker, 'apply', cgapply) + extensions.wrapfunction(cmdutil, 'commitstatus', commitstatus) diff -r efda653c96a7 -r 94432e742a02 setup.cfg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.cfg Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,28 @@ +[flake8] +ignore = + #closing bracket does not match indentation of opening bracket's line + E123, + # closing bracket does not match visual indentation + E124, + # visually indented line with same indent as next logical line + E129, + # at least two spaces before inline comment + E261, + # too many leading '#' for block comment + E266, + # expected 2 blank lines, found 0 + E302, + # expected 2 blank lines after end of function or class + E305, + # module level import not at top of file + E402, + # line too long (82 > 79 characters) + E501, + # do not assign a lambda expression, use a def + E731, + # class names should use CapWords convention + N801, + # line break occurred before a binary operator + W503 +builtins=xrange, execfile + diff -r efda653c96a7 -r 94432e742a02 setup.py --- a/setup.py Tue Feb 28 17:22:21 2017 +0100 +++ b/setup.py Fri Mar 31 15:33:59 2017 +0200 @@ -1,32 +1,40 @@ -# Copied from histedit setup.py -# Credit to Augie Fackler - import os from distutils.core import setup from os.path import dirname, join -def get_version(relpath): +META_PATH = 'hgext3rd/evolve/metadata.py' + +def get_metadata(): + meta = {} + fullpath = join(dirname(__file__), META_PATH) + execfile(fullpath, meta) + return meta + +def get_version(): '''Read version info from a file without importing it''' - for line in open(join(dirname(__file__), relpath), 'rb'): - # Decode to a fail-safe string for PY3 - # (gives unicode object in PY2) - line = line.decode('utf8') - if '__version__' in line: - if "'" in line: - return line.split("'")[1] + return get_metadata()['__version__'] + +def min_hg_version(): + '''Read version info from a file without importing it''' + return get_metadata()['minimumhgversion'] py_modules = [ - 'hgext.evolve', +] +py_packages = [ + 'hgext3rd', + 'hgext3rd.evolve', + 'hgext3rd.topic', ] if os.environ.get('INCLUDE_INHIBIT'): - py_modules.append('hgext.inhibit') - py_modules.append('hgext.directaccess') + py_modules.append('hgext3rd.evolve.hack.inhibit') + py_modules.append('hgext3rd.evolve.hack.directaccess') setup( name='hg-evolve', - version=get_version('hgext/evolve.py'), + version=get_version(), author='Pierre-Yves David', + author_email='pierre-yves.david@ens-lyon.org', maintainer='Pierre-Yves David', maintainer_email='pierre-yves.david@ens-lyon.org', url='https://www.mercurial-scm.org/doc/evolution/', @@ -34,5 +42,6 @@ long_description=open('README').read(), keywords='hg mercurial', license='GPLv2+', - py_modules=py_modules + py_modules=py_modules, + packages=py_packages ) diff -r efda653c96a7 -r 94432e742a02 tests/_exc-util.sh --- a/tests/_exc-util.sh Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,110 +0,0 @@ -#!/bin/sh - -cat >> $HGRCPATH <> $HGRCPATH - -mkcommit() { - echo "$1" > "$1" - hg add "$1" - hg ci -m "$1" -} -getid() { - hg log --hidden --template '{node}\n' --rev "$1" -} - -setuprepos() { - echo creating test repo for test case $1 - mkdir $1 - cd $1 - echo - pulldest - hg init pushdest - cd pushdest - mkcommit O - hg phase --public . - cd .. - echo - main - hg clone -q pushdest main - echo - pushdest - hg clone -q main pulldest - echo 'cd into `main` and proceed with env setup' -} - -dotest() { -# dotest TESTNAME [TARGETNODE] - - testcase=$1 - shift - target="$1" - if [ $# -gt 0 ]; then - shift - fi - targetnode="" - desccall="" - cd $testcase - echo "## Running testcase $testcase" - if [ -n "$target" ]; then - desccall="desc("\'"$target"\'")" - targetnode="`hg -R main id -qr \"$desccall\"`" - echo "# testing echange of \"$target\" ($targetnode)" - fi - echo "## initial state" - echo "# obstore: main" - hg -R main debugobsolete | sort - echo "# obstore: pushdest" - hg -R pushdest debugobsolete | sort - echo "# obstore: pulldest" - hg -R pulldest debugobsolete | sort - - if [ -n "$target" ]; then - echo "## pushing \"$target\"" from main to pushdest - hg -R main push -r "$desccall" $@ pushdest - else - echo "## pushing from main to pushdest" - hg -R main push pushdest $@ - fi - echo "## post push state" - echo "# obstore: main" - hg -R main debugobsolete | sort - echo "# obstore: pushdest" - hg -R pushdest debugobsolete | sort - echo "# obstore: pulldest" - hg -R pulldest debugobsolete | sort - if [ -n "$target" ]; then - echo "## pulling \"$targetnode\"" from main into pulldest - hg -R pulldest pull -r $targetnode $@ main - else - echo "## pulling from main into pulldest" - hg -R pulldest pull main $@ - fi - echo "## post pull state" - echo "# obstore: main" - hg -R main debugobsolete | sort - echo "# obstore: pushdest" - hg -R pushdest debugobsolete | sort - echo "# obstore: pulldest" - hg -R pulldest debugobsolete | sort - - cd .. - -} diff -r efda653c96a7 -r 94432e742a02 tests/test-amend.t --- a/tests/test-amend.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-amend.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ glog() { > hg glog --template '{rev}@{branch}({phase}) {desc|firstline}\n' "$@" diff -r efda653c96a7 -r 94432e742a02 tests/test-check-flake8.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-check-flake8.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,18 @@ +#require test-repo + + $ checkflake8() { + > if ! (which flake8 > /dev/null); then + > echo skipped: missing tool: flake8; + > exit 80; + > fi; + > }; + $ checkflake8 + +Copied from Mercurial core (60ee2593a270) + + $ cd "`dirname "$TESTDIR"`" + +run flake8 if it exists; if it doesn't, then just skip + + $ hg files -0 'set:(**.py or grep("^#!.*python")) - removed()' 2>/dev/null \ + > | xargs -0 flake8 diff -r efda653c96a7 -r 94432e742a02 tests/test-check-pyflakes.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-check-pyflakes.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,11 @@ +#require test-repo pyflakes + +Copied from Mercurial core (60ee2593a270) + + $ cd "`dirname "$TESTDIR"`" + +run pyflakes on all tracked files ending in .py or without a file ending +(skipping binary file random-seed) + + $ hg locate 'set:(**.py or grep("^#!.*python")) - removed()' 2>/dev/null \ + > | xargs pyflakes 2>/dev/null diff -r efda653c96a7 -r 94432e742a02 tests/test-check-setup-manifest.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-check-setup-manifest.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,19 @@ +#require test-repo + + $ checkcm() { + > if ! (which check-manifest > /dev/null); then + > echo skipped: missing tool: check-manifest; + > exit 80; + > fi; + > }; + $ checkcm + $ cat << EOF >> $HGRCPATH + > [experimental] + > evolution=all + > EOF + +Run check manifest: + + $ cd $TESTDIR/.. + $ check-manifest + lists of files in version control and sdist match diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-partial-C1.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-partial-C1.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,79 @@ +==================================== +Testing head checking code: Case C-1 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category C: checking case were the branch is only partially obsoleted. +TestCase 1: 2 changeset branch, only the head is rewritten + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * 1 new changesets branches superceeding only the head of the old one +.. * base of the old branch is still alive +.. +.. expected-result: +.. +.. * push denied +.. +.. graph-summary: +.. +.. B ø⇠◔ B' +.. | | +.. A ○ | +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B1 + created new head + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ 25c56d33e4c4 (draft): B1 + | + | x d73caddc5533 (draft): B0 + | | + | o 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 25c56d33e4c4! + (merge or see 'hg help push' for details about pushing new heads) + [255] diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-partial-C2.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-partial-C2.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,79 @@ +==================================== +Testing head checking code: Case C-2 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category C: checking case were the branch is only partially obsoleted. +TestCase 2: 2 changeset branch, only the base is rewritten + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * 1 new changesets branches superceeding only the base of the old one +.. * The old branch is still alive (base is obsolete, head is alive) +.. +.. expected-result: +.. +.. * push denied +.. +.. graph-summary: +.. +.. B ○ +.. | +.. A ø⇠◔ A' +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg log -G --hidden + @ f6082bc4ffef (draft): A1 + | + | o d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push --rev 'desc(A1)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head f6082bc4ffef! + (merge or see 'hg help push' for details about pushing new heads) + [255] diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-partial-C3.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-partial-C3.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,78 @@ +==================================== +Testing head checking code: Case C-3 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category C: checking case were the branch is only partially obsoleted. +TestCase 3: 2 changeset branch, only the head is pruned + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * old head is pruned +.. * 1 new unrelated branch +.. +.. expected-result: +.. +.. * push denied +.. +.. graph-summary: +.. +.. B ⊗ +.. | +.. A ◔ ◔ C +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ hg debugobsolete --record-parents `getid "desc(B0)"` + $ hg log -G --hidden + @ 0f88766e02d6 (draft): C0 + | + | x d73caddc5533 (draft): B0 + | | + | o 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 0f88766e02d6! + (merge or see 'hg help push' for details about pushing new heads) + [255] diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-partial-C4.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-partial-C4.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,78 @@ +==================================== +Testing head checking code: Case C-4 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category C: checking case were the branch is only partially obsoleted. +TestCase 4: 2 changeset branch, only the base is pruned + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * old base is pruned +.. * 1 new unrelated branch +.. +.. expected-result: +.. +.. * push denied +.. +.. graph-summary: +.. +.. B ◔ +.. | +.. A ⊗ ◔ C +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ hg debugobsolete --record-parents `getid "desc(A0)"` + $ hg log -G --hidden + @ 0f88766e02d6 (draft): C0 + | + | o d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push --rev 'desc(C0)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 0f88766e02d6! + (merge or see 'hg help push' for details about pushing new heads) + [255] diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B1.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B1.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,71 @@ +==================================== +Testing head checking code: Case B-1 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 1: single pruned changeset + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * old branch is pruned +.. * 1 new unrelated branch +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. ◔ B +.. | +.. A ⊗ | +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B0 + created new head + $ hg debugobsolete --record-parents `getid "desc(A0)"` + $ hg log -G --hidden + @ 74ff5441d343 (draft): B0 + | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 1 new obsolescence markers + + + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B2.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B2.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,82 @@ +==================================== +Testing head checking code: Case B-2 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 2: multi-changeset branch, head is pruned, rest is superceeded + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * old head is pruned +.. * 1 new branch succeeding to the other changeset in the old branch +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ⊗ +.. | +.. A ø⇠◔ A' +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete --record-parents `getid "desc(B0)"` + $ hg log -G --hidden + @ f6082bc4ffef (draft): A1 + | + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B3.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B3.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,82 @@ +==================================== +Testing head checking code: Case B-3 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 3: multi-changeset branch, other is pruned, rest is superceeded + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * old head is superceeded +.. * old other is pruned +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ø⇠◔ B' +.. | | +.. A ⊗ | +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B1 + created new head + $ hg debugobsolete --record-parents `getid "desc(A0)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ 25c56d33e4c4 (draft): B1 + | + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B4.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B4.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,83 @@ +==================================== +Testing head checking code: Case B-4 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 4: multi-changeset branch, all are pruned + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * old branch is pruned +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ⊗ +.. | +.. A ⊗ +.. | +.. | ◔ C +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ hg debugobsolete --record-parents `getid "desc(A0)"` + $ hg debugobsolete --record-parents `getid "desc(B0)"` + $ hg log -G --hidden + @ 0f88766e02d6 (draft): C0 + | + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B5.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B5.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,89 @@ +==================================== +Testing head checking code: Case B-5 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 5: multi-changeset branch, mix of pruned and superceeded + +.. old-state: +.. +.. * 3 changeset branch +.. +.. new-state: +.. +.. * old head is pruned +.. * old mid is superceeded +.. * old root is pruned +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ⊗ +.. | +.. A ø⇠◔ A' +.. | | +.. B ⊗ | +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ mkcommit C0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B1 + created new head + $ hg debugobsolete --record-parents `getid "desc(A0)"` + $ hg debugobsolete `getid "desc(B0)"` `getid "desc(B1)"` + $ hg debugobsolete --record-parents `getid "desc(C0)"` + $ hg log -G --hidden + @ 25c56d33e4c4 (draft): B1 + | + | x 821fb21d0dd2 (draft): C0 + | | + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 3 new obsolescence markers + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B6.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B6.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,74 @@ +==================================== +Testing head checking code: Case B-6 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 6: single changesets, pruned then superseeded (on a new changeset) + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * old branch is rewritten onto another one, +.. * the new version is then pruned. +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. A ø⇠⊗ A' +.. | | +.. | ◔ B +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B0 + created new head + $ mkcommit A1 + $ hg up 'desc(B0)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"` + $ hg debugobsolete --record-parents `getid "desc(A1)"` + $ hg log -G --hidden + x ba93660aff8d (draft): A1 + | + @ 74ff5441d343 (draft): B0 + | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B7.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B7.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,73 @@ +==================================== +Testing head checking code: Case B-7 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 7: single changesets, pruned then superseeded (on an existing changeset) + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * old branch is rewritten onto the common set, +.. * the new version is then pruned. +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. A ø⇠⊗ A' +.. B ◔ | | +.. \|/ +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B0 + created new head + $ mkcommit A1 + $ hg up 'desc(B0)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"` + $ hg debugobsolete --record-parents `getid "desc(A1)"` + $ hg log -G --hidden + x ba93660aff8d (draft): A1 + | + @ 74ff5441d343 (draft): B0 + | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-pruned-B8.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-pruned-B8.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,95 @@ +==================================== +Testing head checking code: Case B-2 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category B: checking simple case involving pruned changesets +TestCase 2: multi-changeset branch, head is pruned, rest is superceeded, through other + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * old head is rewritten then pruned +.. * 1 new branch succeeding to the other changeset in the old branch (through another obsolete branch) +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ø⇠⊗ B' +.. | | A' +.. A ø⇠ø⇠◔ A'' +.. |/ / +.. | / +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ mkcommit B1 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit A2 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg debugobsolete --record-parents `getid "desc(B1)"` + $ hg debugobsolete `getid "desc(A1)" ` `getid "desc(A2)"` + $ hg log -G --hidden + @ c1f8d089020f (draft): A2 + | + | x 262c8c798096 (draft): B1 + | | + | x f6082bc4ffef (draft): A1 + |/ + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 4 new obsolescence markers + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A1.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A1.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,68 @@ +==================================== +Testing head checking code: Case A-1 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 1: single-changeset branch + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * 1 changeset branch succeeding to A +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. A ø⇠◔ A' +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg log -G --hidden + @ f6082bc4ffef (draft): A1 + | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 1 new obsolescence markers + + + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A2.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A2.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,84 @@ +==================================== +Testing head checking code: Case A-2 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 2: multi-changeset branch + +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * 2 changeset branch succeeding the old one +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ø⇠◔ B' +.. | | +.. A ø⇠◔ A' +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ mkcommit B1 + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ 262c8c798096 (draft): B1 + | + o f6082bc4ffef (draft): A1 + | + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + 2 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A3.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A3.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,87 @@ +==================================== +Testing head checking code: Case A-3 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 3: multi-changeset branch with reordering + +Push should be allowed +.. old-state: +.. +.. * 2 changeset branch +.. +.. new-state: +.. +.. * 2 changeset branch succeeding the old one with reordering +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ø⇠⇠ +.. | ⇡ +.. A ø⇠⇠⇠○ A' +.. | ⇡/ +.. | ○ B' +.. |/ +.. ● O + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B1 + created new head + $ mkcommit A1 + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ c1c7524e9488 (draft): A1 + | + o 25c56d33e4c4 (draft): B1 + | + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + 2 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A4.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A4.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,74 @@ +==================================== +Testing head checking code: Case A-4 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 4: New changeset as children of the successor + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * 2 changeset branch, first is a successor, but head is new +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. ◔ B +.. | +.. A ø⇠◔ A' +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ mkcommit B0 + $ hg log -G --hidden + @ f40ded968333 (draft): B0 + | + o f6082bc4ffef (draft): A1 + | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + 1 new obsolescence markers + + + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A5.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A5.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,74 @@ +==================================== +Testing head checking code: Case A-5 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 5: New changeset as parent of the successor + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * 2 changeset branch, head is a successor, but other is new +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. A ø⇠◔ A' +.. | | +.. | ◔ B +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B0 + created new head + $ mkcommit A1 + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg log -G --hidden + @ ba93660aff8d (draft): A1 + | + o 74ff5441d343 (draft): B0 + | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + 1 new obsolescence markers + + + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A6.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A6.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,95 @@ +==================================== +Testing head checking code: Case A-6 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 6: multi-changeset branch, split on multiple other, (base on its own branch) + +.. old-state: +.. +.. * 2 branch (1 changeset, and 2 changesets) +.. +.. new-state: +.. +.. * 1 new branch superceeding the base of the old-2-changesets-branch, +.. * 1 new changesets on the old-1-changeset-branch superceeding the head of the other +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B'◔⇢ø B +.. | | +.. A | ø⇠◔ A' +.. | |/ +.. C ● | +.. \| +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg up 'desc(C0)' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B1 + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ d70a1f75a020 (draft): B1 + | + | o f6082bc4ffef (draft): A1 + | | + o | 0f88766e02d6 (draft): C0 + |/ + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + 2 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A7.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A7.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,95 @@ +==================================== +Testing head checking code: Case A-7 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 7: multi-changeset branch, split on multiple other, (head on its own branch) + +.. old-state: +.. +.. * 2 branch (1 changeset, and 2 changesets) +.. +.. new-state: +.. +.. * 1 new branch superceeding the head of the old-2-changesets-branch, +.. * 1 new changesets on the old-1-changeset-branch superceeding the base of the other +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. B ø⇠◔ B' +.. | | +.. A'◔⇢ø | +.. | |/ +.. C ● | +.. \| +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg up 'desc(C0)' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit B1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ 25c56d33e4c4 (draft): B1 + | + | o a0802eb7fc1b (draft): A1 + | | + | o 0f88766e02d6 (draft): C0 + |/ + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + 2 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-superceed-A8.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-superceed-A8.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,78 @@ +==================================== +Testing head checking code: Case A-8 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category A: checking simple case invoving a branch being superceeded by another. +TestCase 8: single-changeset branch indirect rewrite + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * 1 changeset branch succeeding to A, through another unpushed changesets +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. A' +.. A ø⇠ø⇠◔ A'' +.. |/ / +.. | / +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A2 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(A1)" ` `getid "desc(A2)"` + $ hg log -G --hidden + @ c1f8d089020f (draft): A2 + | + | x f6082bc4ffef (draft): A1 + |/ + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers + + + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-unpushed-D1.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-unpushed-D1.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,75 @@ +==================================== +Testing head checking code: Case D-1 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category D: remote head is "obs-affected" locally, but result is not part of the push. +TestCase 1: remote head is rewritten, but successors is not part of the push + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * 1 changeset branch succeeding the old branch +.. * 1 new unrelated branch +.. +.. expected-result: +.. +.. * pushing only the unrelated branch: denied +.. +.. graph-summary: +.. +.. A ø⇠○ A' +.. |/ +.. | ◔ B +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B0 + created new head + $ hg log -G --hidden + @ 74ff5441d343 (draft): B0 + | + | o f6082bc4ffef (draft): A1 + |/ + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push -r 'desc(B0)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 74ff5441d343! + (merge or see 'hg help push' for details about pushing new heads) + [255] + + + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-unpushed-D2.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-unpushed-D2.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,90 @@ +==================================== +Testing head checking code: Case D-2 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category D: remote head is "obs-affected" locally, but result is not part of the push. +TestCase 1: remote branch has 2 changes, head is pruned, second is rewritten but result is not pushed + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * old head is pruned +.. * 1 new branch succeeding to the other changeset in the old branch +.. * 1 new unrelated branch +.. +.. expected-result: +.. +.. * push allowed +.. * pushing only the unrelated branch: denied +.. +.. graph-summary: +.. +.. B ⊗ +.. | +.. A ø⇠○ A' +.. |/ +.. | ◔ C +.. |/ +.. ○ + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete --record-parents `getid "desc(B0)"` + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ hg log -G --hidden + @ 0f88766e02d6 (draft): C0 + | + | o f6082bc4ffef (draft): A1 + |/ + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push --rev 'desc(C0)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 0f88766e02d6! + (merge or see 'hg help push' for details about pushing new heads) + [255] + diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-unpushed-D3.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-unpushed-D3.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,107 @@ +==================================== +Testing head checking code: Case D-3 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category D: remote head is "obs-affected" locally, but result is not part of the push. +TestCase 3: multi-changeset branch, split on multiple new others, only one of them is pushed + +.. old-state: +.. +.. * 2 changesets branch +.. +.. new-state: +.. +.. * 2 new branches, each superseding one changeset in the old one. +.. +.. expected-result: +.. +.. * pushing only one of the resulting branch (either of them) +.. * push denied +.. +.. graph-summary: +.. +.. B'◔⇢ø B +.. | | +.. A | ø⇠◔ A' +.. | |/ +.. \| +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + (run 'hg update' to get a working copy) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg up '0' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ 25c56d33e4c4 (draft): B1 + | + | o f6082bc4ffef (draft): A1 + |/ + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push --rev 'desc(A1)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head f6082bc4ffef! + (merge or see 'hg help push' for details about pushing new heads) + [255] + $ hg push --rev 'desc(B1)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 25c56d33e4c4! + (merge or see 'hg help push' for details about pushing new heads) + [255] + +Extra testing +------------- + +In this case, even a bare push is creating more heads + + $ hg push + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 25c56d33e4c4! + (merge or see 'hg help push' for details about pushing new heads) + [255] diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-unpushed-D4.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-unpushed-D4.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,102 @@ +==================================== +Testing head checking code: Case D-4 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category D: remote head is "obs-affected" locally, but result is not part of the push. +TestCase 4: multi-changeset branch, split on multiple other, (base on its own new branch) + +.. old-state: +.. +.. * 2 branch (1 changeset, and 2 changesets) +.. +.. new-state: +.. +.. * 1 new branch superceeding the base of the old-2-changesets-branch, +.. * 1 new changesets on the old-1-changeset-branch superceeding the head of the other +.. +.. expected-result: +.. +.. * push the new branch only -> push denied +.. * push the existing branch only -> push allowed +.. +.. graph-summary: +.. +.. B'◔⇢ø B +.. | | +.. A | ø⇠◔ A' +.. | |/ +.. C ● | +.. \| +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + created new head + $ hg up 'desc(C0)' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B1 + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ d70a1f75a020 (draft): B1 + | + | o f6082bc4ffef (draft): A1 + | | + o | 0f88766e02d6 (draft): C0 + |/ + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push --rev 'desc(A1)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head f6082bc4ffef! + (merge or see 'hg help push' for details about pushing new heads) + [255] + $ hg push --rev 'desc(B1)' + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + 1 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-unpushed-D5.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-unpushed-D5.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,104 @@ +==================================== +Testing head checking code: Case D-5 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category D: remote head is "obs-affected" locally, but result is not part of the push. +TestCase 5: multi-changeset branch, split on multiple other, (head on its own new branch) + +.. old-state: +.. +.. * 2 branch (1 changeset, and 2 changesets) +.. +.. new-state: +.. +.. * 1 new branch superceeding the head of the old-2-changesets-branch, +.. * 1 new changesets on the old-1-changeset-branch superceeding the base of the other +.. +.. expected-result: +.. +.. * push the new branch only -> push denied +.. * push the existing branch only -> push allowed +.. /!\ This push create unstability/orphaning on the other hand and we should +.. probably detect/warn agains that. +.. +.. graph-summary: +.. +.. B ø⇠◔ B' +.. | | +.. A'◔⇢ø | +.. | |/ +.. C ● | +.. \| +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ mkcommit B0 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ cd ../client + $ hg pull + pulling from $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg up 'desc(C0)' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit A1 + $ hg up 0 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit B1 + created new head + $ hg debugobsolete `getid "desc(A0)" ` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(B0)" ` `getid "desc(B1)"` + $ hg log -G --hidden + @ 25c56d33e4c4 (draft): B1 + | + | o a0802eb7fc1b (draft): A1 + | | + | o 0f88766e02d6 (draft): C0 + |/ + | x d73caddc5533 (draft): B0 + | | + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + + +Actual testing +-------------- + + $ hg push --rev 'desc(B1)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 25c56d33e4c4! + (merge or see 'hg help push' for details about pushing new heads) + [255] + $ hg push --rev 'desc(A1)' + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + 1 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-unpushed-D6.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-unpushed-D6.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,78 @@ +==================================== +Testing head checking code: Case D-6 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category D: remote head is "obs-affected" locally, but result is not part of the push. +TestCase 6: single changesets, superseeded then pruned (on a new changeset unpushed) changeset + +This is a partial push variation of B6 + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * old branch is rewritten onto another one, +.. * the new version is then pruned. +.. +.. expected-result: +.. +.. * push denied +.. +.. graph-summary: +.. +.. A ø⇠⊗ A' +.. | | +.. C ◔ | ◔ B +.. \|/ +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B0 + created new head + $ mkcommit A1 + $ hg up '0' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"` + $ hg debugobsolete --record-parents `getid "desc(A1)"` + $ hg log -G --hidden + @ 0f88766e02d6 (draft): C0 + | + | x ba93660aff8d (draft): A1 + | | + | o 74ff5441d343 (draft): B0 + |/ + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push --rev 'desc(C0)' + pushing to $TESTTMP/server + searching for changes + abort: push creates new remote head 0f88766e02d6! + (merge or see 'hg help push' for details about pushing new heads) + [255] diff -r efda653c96a7 -r 94432e742a02 tests/test-checkheads-unpushed-D7.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-checkheads-unpushed-D7.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,92 @@ +==================================== +Testing head checking code: Case D-7 +==================================== + +Mercurial checks for the introduction of multiple heads on push. Evolution +comes into play to detect if existing heads on the server are being replaced by +some of the new heads we push. + +This test file is part of a series of tests checking this behavior. + +Category D: remote head is "obs-affected" locally, but result is not part of the push. +TestCase 7: single changesets, superseeded multiple time then pruned (on a new changeset unpushed) changeset + +This is a partial push variation of B6 + +.. old-state: +.. +.. * 1 changeset branch +.. +.. new-state: +.. +.. * old branch is rewritten onto another one, +.. * The rewriting it again rewritten on the root +.. * the new version is then pruned. +.. +.. expected-result: +.. +.. * push allowed +.. +.. graph-summary: +.. +.. A' +.. A ø⇠ø⇠⊗ A'' +.. | | | +.. C ◔ | ◔ | B +.. \|/ / +.. | / +.. |/ +.. | +.. ● + + $ . $TESTDIR/testlib/checkheads-util.sh + +Test setup +---------- + + $ setuprepos + creating basic server and client repo + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit B0 + created new head + $ mkcommit A1 + $ hg up '0' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit A2 + created new head + $ hg up '0' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit C0 + created new head + $ hg debugobsolete `getid "desc(A0)"` `getid "desc(A1)"` + $ hg debugobsolete `getid "desc(A1)"` `getid "desc(A2)"` + $ hg debugobsolete --record-parents `getid "desc(A2)"` + $ hg log -G --hidden + @ 0f88766e02d6 (draft): C0 + | + | x c1f8d089020f (draft): A2 + |/ + | x ba93660aff8d (draft): A1 + | | + | o 74ff5441d343 (draft): B0 + |/ + | x 8aaa48160adc (draft): A0 + |/ + o 1e4be0697311 (public): root + + +Actual testing +-------------- + + $ hg push --rev 'desc(C0)' + pushing to $TESTTMP/server + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 3 new obsolescence markers diff -r efda653c96a7 -r 94432e742a02 tests/test-corrupt.t --- a/tests/test-corrupt.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-corrupt.t Fri Mar 31 15:33:59 2017 +0200 @@ -15,7 +15,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" >> "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-discovery-obshashrange.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-discovery-obshashrange.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,211 @@ +test for range based discovery +============================== + + $ . $TESTDIR/testlib/pythonpath.sh + + $ cat << EOF >> $HGRCPATH + > [extensions] + > hgext3rd.evolve = + > [experimental] + > obshashrange=1 + > verbose-obsolescence-exchange=1 + > [ui] + > logtemplate = "{rev} {node|short} {desc} {tags}\n" + > ssh=python "$RUNTESTDIR/dummyssh" + > [alias] + > debugobsolete=debugobsolete -d '0 0' + > EOF + + $ getid() { + > hg log --hidden --template '{node}\n' --rev "$1" + > } + + $ hg init server + $ hg clone ssh://user@dummy/server client + no changes found + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd server + $ hg debugbuilddag '.+7' + $ hg log -G + o 7 4de32a90b66c r7 tip + | + o 6 f69452c5b1af r6 + | + o 5 c8d03c1b5e94 r5 + | + o 4 bebd167eb94d r4 + | + o 3 2dc09a01254d r3 + | + o 2 01241442b3c2 r2 + | + o 1 66f7d451a68b r1 + | + o 0 1ea73414a91b r0 + + + $ hg debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa `getid 'desc(r1)'` + $ hg debugobsolete bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb `getid 'desc(r2)'` + $ hg debugobsolete cccccccccccccccccccccccccccccccccccccccc `getid 'desc(r4)'` + $ hg debugobsolete dddddddddddddddddddddddddddddddddddddddd `getid 'desc(r5)'` + $ hg debugobsolete eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee `getid 'desc(r7)'` + $ hg debugobsolete + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee 4de32a90b66cd083ebf3c00b41277aa7abca51dd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + + $ hg debugobshashrange --subranges --rev tip + rev node index size depth obshash + 7 4de32a90b66c 0 8 8 38d1e7ad86ea + 3 2dc09a01254d 0 4 4 000000000000 + 7 4de32a90b66c 4 4 8 38d1e7ad86ea + 3 2dc09a01254d 2 2 4 000000000000 + 7 4de32a90b66c 6 2 8 033544c939f0 + 1 66f7d451a68b 0 2 2 17ff8dd63509 + 5 c8d03c1b5e94 4 2 6 57f6cf3757a2 + 2 01241442b3c2 2 1 3 1ed3c61fb39a + 0 1ea73414a91b 0 1 1 000000000000 + 3 2dc09a01254d 3 1 4 000000000000 + 7 4de32a90b66c 7 1 8 033544c939f0 + 1 66f7d451a68b 1 1 2 17ff8dd63509 + 4 bebd167eb94d 4 1 5 bbe4d7fe27a8 + 5 c8d03c1b5e94 5 1 6 446c2dc3bce5 + 6 f69452c5b1af 6 1 7 000000000000 + $ cd .. + +testing simple pull +=================== + + $ cd client + $ hg pull --rev 4 + pulling from ssh://user@dummy/server + adding changesets + adding manifests + adding file changes + added 5 changesets with 0 changes to 0 files + 3 new obsolescence markers + (run 'hg update' to get a working copy) + $ hg -R ../server/ debugobsolete --rev ::4 | sort + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsolete | sort + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + +testing simple push +=================== + + $ hg up + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo foo > foo + $ hg add foo + $ hg commit -m foo + $ hg debugobsolete ffffffffffffffffffffffffffffffffffffffff `getid '.'` + $ hg push -f + pushing to ssh://user@dummy/server + searching for changes + OBSEXC: computing relevant nodes + OBSEXC: looking for common markers in 6 nodes + OBSEXC: computing markers relevant to 1 nodes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files (+1 heads) + remote: 1 new obsolescence markers + +testing push with extra local markers +===================================== + + $ hg log -G + @ 5 45f8b879de92 foo tip + | + o 4 bebd167eb94d r4 + | + o 3 2dc09a01254d r3 + | + o 2 01241442b3c2 r2 + | + o 1 66f7d451a68b r1 + | + o 0 1ea73414a91b r0 + + $ hg debugobsolete 111111111111111aaaaaaaaa1111111111111111 `getid 'desc(r1)'` + $ hg debugobsolete 22222222222222222bbbbbbbbbbbbb2222222222 `getid 'desc(r3)'` + $ hg push + pushing to ssh://user@dummy/server + searching for changes + OBSEXC: computing relevant nodes + OBSEXC: looking for common markers in 6 nodes + OBSEXC: computing markers relevant to 2 nodes + no changes found + remote: 2 new obsolescence markers + [1] + $ hg -R ../server/ debugobsolete --rev ::tip | sort + 111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + 22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + ffffffffffffffffffffffffffffffffffffffff 45f8b879de922f6a6e620ba04205730335b6fc7e 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsolete | sort + 111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + 22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + ffffffffffffffffffffffffffffffffffffffff 45f8b879de922f6a6e620ba04205730335b6fc7e 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + +testing pull with extra remote markers +===================================== + + $ hg log -G + @ 5 45f8b879de92 foo tip + | + o 4 bebd167eb94d r4 + | + o 3 2dc09a01254d r3 + | + o 2 01241442b3c2 r2 + | + o 1 66f7d451a68b r1 + | + o 0 1ea73414a91b r0 + + $ hg -R ../server debugobsolete aaaaaaa11111111aaaaaaaaa1111111111111111 `getid 'desc(r1)'` + $ hg -R ../server debugobsolete bbbbbbb2222222222bbbbbbbbbbbbb2222222222 `getid 'desc(r4)'` + $ hg pull -r 6 + pulling from ssh://user@dummy/server + searching for changes + OBSEXC: looking for common markers in 6 nodes + OBSEXC: request obsmarkers for 2 common nodes + adding changesets + adding manifests + adding file changes + added 2 changesets with 0 changes to 0 files (+1 heads) + 3 new obsolescence markers + (run 'hg heads' to see heads, 'hg merge' to merge) + + $ hg -R ../server/ debugobsolete --rev '::6' | sort + 111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + 22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + aaaaaaa11111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbb2222222222bbbbbbbbbbbbb2222222222 bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsolete --rev '::6' | sort + 111111111111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + 22222222222222222bbbbbbbbbbbbb2222222222 2dc09a01254db841290af0538aa52f6f52c776e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + aaaaaaa11111111aaaaaaaaa1111111111111111 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 66f7d451a68b85ed82ff5fcc254daf50c74144bd 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbb2222222222bbbbbbbbbbbbb2222222222 bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 01241442b3c2bf3211e593b549c655ea65b295e3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + cccccccccccccccccccccccccccccccccccccccc bebd167eb94d257ace0e814aeb98e6972ed2970d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + dddddddddddddddddddddddddddddddddddddddd c8d03c1b5e94af74b772900c58259d2e08917735 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + diff -r efda653c96a7 -r 94432e742a02 tests/test-divergent.t --- a/tests/test-divergent.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-divergent.t Fri Mar 31 15:33:59 2017 +0200 @@ -17,7 +17,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-drop.t --- a/tests/test-drop.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-drop.t Fri Mar 31 15:33:59 2017 +0200 @@ -3,8 +3,8 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "drophack=$(echo $(dirname $TESTDIR))/hgext/drophack.py" >> $HGRCPATH - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "drophack=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/drophack.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-evolve-bumped.t --- a/tests/test-evolve-bumped.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-evolve-bumped.t Fri Mar 31 15:33:59 2017 +0200 @@ -10,7 +10,7 @@ adding a $ cd .. - $ evolvepath=$(echo $(dirname $TESTDIR))/hgext/evolve.py + $ evolvepath=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/ $ hg clone -U public private $ cd private $ cat >> .hg/hgrc < [extensions] > rebase= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH Test the instability listing $ hg init r2 diff -r efda653c96a7 -r 94432e742a02 tests/test-evolve-order.t --- a/tests/test-evolve-order.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-evolve-order.t Fri Mar 31 15:33:59 2017 +0200 @@ -18,7 +18,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-evolve-serveronly-bundle2.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-evolve-serveronly-bundle2.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,175 @@ + + $ . ${TESTDIR}/testlib/pythonpath.sh + + $ cat >> $HGRCPATH < [defaults] + > amend=-d "0 0" + > [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish = False + > [experimental] + > bundle2-exp=True + > EOF + + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + + $ hg init server + +Try the multiple ways to setup the extension + + $ hg -R server log --config 'extensions.evolve.serveronly=' + $ hg -R server log --config "extensions.evolve.serveronly=${SRCDIR}/hgext3rd/evolve/serveronly.py" + $ PYTHONPATH=$HGTEST_ORIG_PYTHONPATH hg -R server log --config "extensions.evolve.serveronly=${SRCDIR}/hgext3rd/evolve/serveronly.py" + +setup repo + + $ echo "[extensions]" >> ./server/.hg/hgrc + $ echo "evolve.serveronly=" >> ./server/.hg/hgrc + $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log + $ cat hg.pid >> $DAEMON_PIDS + + $ hg clone http://localhost:$HGPORT/ client + no changes found + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat ./errors.log + $ echo "[extensions]" >> ./client/.hg/hgrc + $ echo "evolve=" >> ./client/.hg/hgrc + $ cp -r client other + +Smoke testing +=============== + + $ cd client + $ mkcommit 0 + $ mkcommit a + $ hg push + pushing to http://localhost:$HGPORT/ + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 2 changesets with 2 changes to 2 files + $ hg pull + pulling from http://localhost:$HGPORT/ + searching for changes + no changes found + $ cat ../errors.log + $ hg pull -R ../other + pulling from http://localhost:$HGPORT/ + requesting all changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files + (run 'hg update' to get a working copy) + $ cat ../errors.log + $ hg push -R ../other + pushing to http://localhost:$HGPORT/ + searching for changes + no changes found + [1] + $ cat ../errors.log + +Capacity testing +=================== + + $ curl -s http://localhost:$HGPORT/?cmd=hello + capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (glob) + $ curl -s http://localhost:$HGPORT/?cmd=capabilities + _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (no-eol) (glob) + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + obsolete + phases + +Push +============= + + $ echo 'A' > a + $ hg amend + $ hg push + pushing to http://localhost:$HGPORT/ + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files (+1 heads) + remote: 2 new obsolescence markers + $ cat ../errors.log + $ hg push + pushing to http://localhost:$HGPORT/ + searching for changes + no changes found + [1] + $ cat ../errors.log + +Pull +============= + + $ hg -R ../other pull + pulling from http://localhost:$HGPORT/ + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to [12] files \(\+1 heads\) (re) + 2 new obsolescence markers + (run 'hg heads' to see heads, 'hg merge' to merge) + $ cat ../errors.log + $ hg -R ../other pull + pulling from http://localhost:$HGPORT/ + searching for changes + no changes found + $ cat ../errors.log + + $ cd .. + +Test disabling obsolete advertisement +=========================================== +(used by bitbucket to select which repo use evolve) + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + obsolete + phases + $ curl -s http://localhost:$HGPORT/?cmd=hello + capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (glob) + $ curl -s http://localhost:$HGPORT/?cmd=capabilities + _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (no-eol) (glob) + + $ echo '[experimental]' >> server/.hg/hgrc + $ echo 'evolution=!' >> server/.hg/hgrc + $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS + $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log + $ cat hg.pid >> $DAEMON_PIDS + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + phases + + $ echo 'evolution=all' >> server/.hg/hgrc + $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS + $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log + $ cat hg.pid >> $DAEMON_PIDS + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + obsolete + phases + + $ curl -s http://localhost:$HGPORT/?cmd=hello + capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (glob) + $ curl -s http://localhost:$HGPORT/?cmd=capabilities + _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (no-eol) (glob) diff -r efda653c96a7 -r 94432e742a02 tests/test-evolve-serveronly.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-evolve-serveronly.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,194 @@ + + $ . ${TESTDIR}/testlib/pythonpath.sh + + $ cat >> $HGRCPATH < [defaults] + > amend=-d "0 0" + > [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish = False + > [experimental] + > bundle2-exp=False # < Mercurial-4.0 + > [devel] + > legacy.exchange=bundle1 + > [extensions] + > EOF + + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + + + $ hg init server + +Try the multiple ways to setup the extension + + $ hg -R server log --config 'extensions.evolve.serveronly=' + $ hg -R server log --config "extensions.evolve.serveronly=${SRCDIR}/hgext3rd/evolve/serveronly.py" + $ PYTHONPATH=$HGTEST_ORIG_PYTHONPATH hg -R server log --config "extensions.evolve.serveronly=${SRCDIR}/hgext3rd/evolve/serveronly.py" + +setup repo + + $ echo "[extensions]" >> ./server/.hg/hgrc + $ echo "evolve.serveronly=" >> ./server/.hg/hgrc + $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log --traceback + $ cat hg.pid >> $DAEMON_PIDS + + $ hg clone http://localhost:$HGPORT/ client + no changes found + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat ./errors.log + $ echo "[extensions]" >> ./client/.hg/hgrc + $ echo "evolve=" >> ./client/.hg/hgrc + $ cp -r client other + +Smoke testing +=============== + + $ cd client + $ mkcommit 0 + $ mkcommit a + $ hg push + pushing to http://localhost:$HGPORT/ + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 2 changesets with 2 changes to 2 files + $ hg pull + pulling from http://localhost:$HGPORT/ + searching for changes + no changes found + $ cat ../errors.log + $ hg pull -R ../other + pulling from http://localhost:$HGPORT/ + requesting all changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 2 files + pull obsolescence markers + the remote repository use years old versions of Mercurial and evolve + pulling obsmarker using legacy method + (please upgrade your server) + (run 'hg update' to get a working copy) + $ cat ../errors.log + $ hg push -R ../other + pushing to http://localhost:$HGPORT/ + searching for changes + no changes found + [1] + $ cat ../errors.log + +Capacity testing +=================== + + $ curl -s http://localhost:$HGPORT/?cmd=hello + capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (glob) + $ curl -s http://localhost:$HGPORT/?cmd=capabilities + _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (no-eol) (glob) + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + obsolete + phases + +Push +============= + + $ echo 'A' > a + $ hg amend + $ hg push + pushing to http://localhost:$HGPORT/ + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files (+1 heads) + the remote repository use years old versions of Mercurial and evolve + pushing obsmarker using legacy method + (please upgrade your server) + pushing 2 obsolescence markers (* bytes) (glob) + remote: 2 obsolescence markers added + $ cat ../errors.log + $ hg push + pushing to http://localhost:$HGPORT/ + searching for changes + no changes found + [1] + $ cat ../errors.log + +Pull +============= + + $ hg -R ../other pull + pulling from http://localhost:$HGPORT/ + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to [12] files \(\+1 heads\) (re) + pull obsolescence markers + the remote repository use years old versions of Mercurial and evolve + pulling obsmarker using legacy method + (please upgrade your server) + 2 obsolescence markers added + (run 'hg heads' to see heads) + $ cat ../errors.log + $ hg -R ../other pull + pulling from http://localhost:$HGPORT/ + searching for changes + no changes found + $ cat ../errors.log + + $ cd .. + +Test disabling obsolete advertisement +=========================================== +(used by bitbucket to select which repo use evolve) + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + obsolete + phases + $ curl -s http://localhost:$HGPORT/?cmd=hello + capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (glob) + $ curl -s http://localhost:$HGPORT/?cmd=capabilities + _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (no-eol) (glob) + + $ echo '[experimental]' >> server/.hg/hgrc + $ echo 'evolution=!' >> server/.hg/hgrc + $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS + $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log + $ cat hg.pid >> $DAEMON_PIDS + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + phases + $ curl -s http://localhost:$HGPORT/?cmd=hello | grep _evoext_pushobsmarkers_0 + [1] + $ curl -s http://localhost:$HGPORT/?cmd=capabilities | grep _evoext_pushobsmarkers_0 + [1] + + $ echo 'evolution=' >> server/.hg/hgrc + $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS + $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log + $ cat hg.pid >> $DAEMON_PIDS + + $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort + bookmarks + namespaces + obsolete + phases + $ curl -s http://localhost:$HGPORT/?cmd=hello + capabilities: _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (glob) + $ curl -s http://localhost:$HGPORT/?cmd=capabilities + _evoext_getbundle_obscommon _evoext_obshash_0 _evoext_obshash_1 _evoext_pullobsmarkers_0 _evoext_pushobsmarkers_0 batch * (no-eol) (glob) diff -r efda653c96a7 -r 94432e742a02 tests/test-evolve-split.t --- a/tests/test-evolve-split.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-evolve-split.t Fri Mar 31 15:33:59 2017 +0200 @@ -17,7 +17,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-evolve-topic.t --- a/tests/test-evolve-topic.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-evolve-topic.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,10 +1,6 @@ Check we can find the topic extensions - $ [ -z "$HGTEST_TOPICROOT" ] && echo 'skipped: $HGTEST_TOPICROOT not set' >&2 && exit 80 - [1] - $ [ ! -e $HGTEST_TOPICROOT/hgext3rd/topic/__init__.py ] && echo 'skipped: no topic repo found at $HGTEST_TOPICROOT' >&2 && exit 80 - [1] $ cat >> $HGRCPATH < [defaults] > amend=-d "0 0" @@ -18,9 +14,9 @@ > unified = 0 > [extensions] > rebase = - > topic = $HGTEST_TOPICROOT/hgext3rd/topic/ > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH + $ echo "topic=$(echo $(dirname $TESTDIR))/hgext3rd/topic/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-evolve.t --- a/tests/test-evolve.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-evolve.t Fri Mar 31 15:33:59 2017 +0200 @@ -16,7 +16,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" @@ -169,6 +169,14 @@ fbb94e3a0ecf6d20c2cc31152ef162ce45af982f * (glob) e44648563c73f75950076031c6fdf06629de95f1 * (glob) +Smoketest stablerange.obshash: + + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 1 7c3bad9141dc 0 2 2 * (glob) + 0 1f0dee641bb7 0 1 1 000000000000 + 1 7c3bad9141dc 1 1 2 * (glob) + $ cd .. ########################## diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-A1.t --- a/tests/test-exchange-A1.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-A1.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh ==== A.1.1 pushing a single head ==== .. @@ -45,6 +45,14 @@ $ hg debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f5bc6836db60e308a17ba08bf050154ba9c4fad7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 50656e04a95ecdfed94659dd61f663b2caa55e98 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 1 f5bc6836db60 0 2 2 50656e04a95e + 0 a9bdc8b26820 0 1 1 000000000000 + 1 f5bc6836db60 1 1 2 50656e04a95e $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-A2.t --- a/tests/test-exchange-A2.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-A2.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === A.2 Two heads === @@ -57,6 +57,17 @@ $ hg debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f5bc6836db60e308a17ba08bf050154ba9c4fad7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 35b1839966785d5703a01607229eea932db42f87 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 50656e04a95ecdfed94659dd61f663b2caa55e98 + 35b1839966785d5703a01607229eea932db42f87 b9c8f20eef8938ebab939fe6a592587feacf3245 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 35b183996678 0 2 2 b9c8f20eef89 + 1 f5bc6836db60 0 2 2 50656e04a95e + 2 35b183996678 1 1 2 b9c8f20eef89 + 0 a9bdc8b26820 0 1 1 000000000000 + 1 f5bc6836db60 1 1 2 50656e04a95e $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-A3.t --- a/tests/test-exchange-A3.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-A3.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === A.3 new branch created === @@ -70,6 +70,19 @@ $ hg debugobsolete 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0000000000000000000000000000000000000000 + 6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 0000000000000000000000000000000000000000 + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 3bc2ee626e11a7cf8fee7a66d069271e17d5a597 + f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 91716bfd671b5a5854a47ac5d392edfdd25e431a + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 3 e5ea8f9c7314 0 2 2 3bc2ee626e11 + 4 f6298a8ac3a4 0 2 2 91716bfd671b + 0 a9bdc8b26820 0 1 1 000000000000 + 3 e5ea8f9c7314 1 1 2 3bc2ee626e11 + 4 f6298a8ac3a4 1 1 2 91716bfd671b $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-A4.t --- a/tests/test-exchange-A4.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-A4.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === A.4 Push in the middle of the obsolescence chain === @@ -63,6 +63,20 @@ $ hg debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 5d69322fad9eb1ba8f8f2c2312346ed347fdde76 + 06055a7959d4128e6e3bccfd01482e83a2db8a3a fd3e5712c9c2d216547d7a1b87ac815ee1fb7542 + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 cf518031fa753e9b049d727e6b0e19f645bab38f + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 06055a7959d4 0 3 3 000000000000 + 1 28b51eb45704 0 2 2 5d69322fad9e + 3 e5ea8f9c7314 0 2 2 cf518031fa75 + 2 06055a7959d4 2 1 3 000000000000 + 1 28b51eb45704 1 1 2 5d69322fad9e + 0 a9bdc8b26820 0 1 1 000000000000 + 3 e5ea8f9c7314 1 1 2 cf518031fa75 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-A5.t --- a/tests/test-exchange-A5.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-A5.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === A.5 partial reordering === @@ -69,6 +69,19 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 8c0a98c8372212c6efde4bfdcef006f27ff759d3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 5d69322fad9eb1ba8f8f2c2312346ed347fdde76 + 6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 fd3e5712c9c2d216547d7a1b87ac815ee1fb7542 + f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 91716bfd671b5a5854a47ac5d392edfdd25e431a + 8c0a98c8372212c6efde4bfdcef006f27ff759d3 6e8c8c71c47a2bfc27c7cf2b1f4174977ede9f21 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 4 8c0a98c83722 0 3 3 70185b996296 + 3 f6298a8ac3a4 0 2 2 91716bfd671b + 4 8c0a98c83722 2 1 3 4d835a45c1e9 + 0 a9bdc8b26820 0 1 1 000000000000 + 3 f6298a8ac3a4 1 1 2 91716bfd671b $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-A6.t --- a/tests/test-exchange-A6.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-A6.t Fri Mar 31 15:33:59 2017 +0200 @@ -3,7 +3,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === A.6 between existing changeset === @@ -59,6 +59,15 @@ $ hg debugobsolete 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0000000000000000000000000000000000000000 + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 3bc2ee626e11a7cf8fee7a66d069271e17d5a597 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 e5ea8f9c7314 0 2 2 3bc2ee626e11 + 0 a9bdc8b26820 0 1 1 000000000000 + 2 e5ea8f9c7314 1 1 2 3bc2ee626e11 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-A7.t --- a/tests/test-exchange-A7.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-A7.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === A.7 Non targeted common changeset === @@ -42,6 +42,14 @@ $ hg debugobsolete aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f5bc6836db60e308a17ba08bf050154ba9c4fad7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 50656e04a95ecdfed94659dd61f663b2caa55e98 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 1 f5bc6836db60 0 2 2 50656e04a95e + 0 a9bdc8b26820 0 1 1 000000000000 + 1 f5bc6836db60 1 1 2 50656e04a95e $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-B1.t --- a/tests/test-exchange-B1.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-B1.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === B.1 Prune on non-targeted common changeset === @@ -46,6 +46,15 @@ $ hg debugobsolete f6fbb35d8ac958bbe70035e4c789c18471cdc0af 0 {f5bc6836db60e308a17ba08bf050154ba9c4fad7} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 926d9d84b97b3483891ae983990ad87c1f7827e9 + f6fbb35d8ac958bbe70035e4c789c18471cdc0af e041f7ff1c7bd5501c7ab602baa35f0873128021 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 1 f5bc6836db60 0 2 2 926d9d84b97b + 0 a9bdc8b26820 0 1 1 000000000000 + 1 f5bc6836db60 1 1 2 926d9d84b97b $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-B2.t --- a/tests/test-exchange-B2.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-B2.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === B.2 Pruned changeset on head: nothing pushed === @@ -41,6 +41,12 @@ $ hg debugobsolete f5bc6836db60e308a17ba08bf050154ba9c4fad7 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 52a5380bc04783a9ad43bb2ab2f47a02ef02adcc + f5bc6836db60e308a17ba08bf050154ba9c4fad7 c5a567339e205e8cc4c494e4fb82944daaec449c + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 0 a9bdc8b26820 0 1 1 52a5380bc047 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-B3.t --- a/tests/test-exchange-B3.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-B3.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === B.3 Pruned changeset on non-pushed part of the history === @@ -57,6 +57,18 @@ $ hg debugobsolete e56289ab6378dc752fd7965f8bf66b58bda740bd 0 {35b1839966785d5703a01607229eea932db42f87} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 0000000000000000000000000000000000000000 + 35b1839966785d5703a01607229eea932db42f87 631ab4cd02ffa1d144dc8f32a18be574076031e3 + e56289ab6378dc752fd7965f8bf66b58bda740bd 47c9d2d8db5d4b1eddd0266329ad260ccc84772c + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 35b183996678 0 2 2 631ab4cd02ff + 1 f5bc6836db60 0 2 2 000000000000 + 2 35b183996678 1 1 2 631ab4cd02ff + 0 a9bdc8b26820 0 1 1 000000000000 + 1 f5bc6836db60 1 1 2 000000000000 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-B4.t --- a/tests/test-exchange-B4.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-B4.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === B.4 Pruned changeset on common part of history === @@ -67,6 +67,18 @@ $ hg debugobsolete 7f7f229b13a629a5b20581c6cb723f4e2ca54bed 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 1900882e85db10a1dc5bc7748f436a8a834356c6 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 c27e764c783f451ef3aa40daf2a3795e6674cd06 + f6fbb35d8ac958bbe70035e4c789c18471cdc0af 907beff79fdff2b82b5d3bed7989107a6d744508 + 7f7f229b13a629a5b20581c6cb723f4e2ca54bed c27e764c783f451ef3aa40daf2a3795e6674cd06 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 f6fbb35d8ac9 0 3 3 000000000000 + 1 f5bc6836db60 0 2 2 000000000000 + 0 a9bdc8b26820 0 1 1 1900882e85db + 1 f5bc6836db60 1 1 2 000000000000 + 2 f6fbb35d8ac9 2 1 3 000000000000 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-B5.t --- a/tests/test-exchange-B5.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-B5.t Fri Mar 31 15:33:59 2017 +0200 @@ -3,7 +3,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === B.5 Push of a children of changeset which successors is pruned === @@ -66,6 +66,18 @@ $ hg debugobsolete 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 554c0b12f7d9fff20cb904c26e12eee337e3309c + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 5c81c58ce0a8ad61dd9cf4c6949846b5990af30d + 06055a7959d4128e6e3bccfd01482e83a2db8a3a 201e20697f2a6b0752335af7cd813f140e9e653e + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 ae1ac676a5e6d6f4216595c53da763d588929970 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 06055a7959d4 0 3 3 000000000000 + 1 28b51eb45704 0 2 2 000000000000 + 2 06055a7959d4 2 1 3 000000000000 + 1 28b51eb45704 1 1 2 000000000000 + 0 a9bdc8b26820 0 1 1 554c0b12f7d9 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-B6.t --- a/tests/test-exchange-B6.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-B6.t Fri Mar 31 15:33:59 2017 +0200 @@ -4,7 +4,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh == B.6 Pruned changeset with ancestors not in pushed set === @@ -56,6 +56,16 @@ $ hg debugobsolete 962ecf6b1afc94e15c7e48fdfb76ef8abd11372b f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 86e41541149f4b6cccc5fd131d744d8e83a681e5 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 f2e05412d3f1d5bc1ae647cf9efc43e0399c26ca + 962ecf6b1afc94e15c7e48fdfb76ef8abd11372b 974507d1c466d0aa86d288836194339ed3b98736 + f6298a8ac3a4b78bbeae5f1d3dc5bc3c3812f0f3 04e03a8959d8a39984e6a8f4a16fba975b364747 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 1 f5bc6836db60 0 2 2 000000000000 + 0 a9bdc8b26820 0 1 1 86e41541149f + 1 f5bc6836db60 1 1 2 000000000000 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-B7.t --- a/tests/test-exchange-B7.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-B7.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === B.7 Prune on non-targeted common changeset === @@ -49,6 +49,15 @@ $ hg debugobsolete f6fbb35d8ac958bbe70035e4c789c18471cdc0af 0 {f5bc6836db60e308a17ba08bf050154ba9c4fad7} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 926d9d84b97b3483891ae983990ad87c1f7827e9 + f6fbb35d8ac958bbe70035e4c789c18471cdc0af e041f7ff1c7bd5501c7ab602baa35f0873128021 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 1 f5bc6836db60 0 2 2 926d9d84b97b + 0 a9bdc8b26820 0 1 1 000000000000 + 1 f5bc6836db60 1 1 2 926d9d84b97b $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-C1.t --- a/tests/test-exchange-C1.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-C1.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === C.1 Multiple pruned changeset atop each other === .. @@ -48,6 +48,13 @@ $ hg debugobsolete f5bc6836db60e308a17ba08bf050154ba9c4fad7 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} f6fbb35d8ac958bbe70035e4c789c18471cdc0af 0 {f5bc6836db60e308a17ba08bf050154ba9c4fad7} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 1ce18e5a71f78d443a80c819f2f7197c4706af70 + f5bc6836db60e308a17ba08bf050154ba9c4fad7 92af733686ce7e0469d8b2b87b4612a4c2d33468 + f6fbb35d8ac958bbe70035e4c789c18471cdc0af 3800aeba3728457abb9c508c94f6abc59e698c55 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 0 a9bdc8b26820 0 1 1 1ce18e5a71f7 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-C2.t --- a/tests/test-exchange-C2.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-C2.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === C.2 Pruned changeset on precursors === @@ -55,6 +55,16 @@ $ hg debugobsolete 06055a7959d4128e6e3bccfd01482e83a2db8a3a 0 {28b51eb45704506b5c603decd6bf7ac5e0f6a52f} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 72f95b7b9fa12243aeb90433d211f2c38263da31 + 06055a7959d4128e6e3bccfd01482e83a2db8a3a 58ecf9a107b10986d88da605eb0d03b7f24ae486 + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 289cb0d058c81c763eca8bb438657dba9a7ba646 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 3 e5ea8f9c7314 0 2 2 289cb0d058c8 + 0 a9bdc8b26820 0 1 1 000000000000 + 3 e5ea8f9c7314 1 1 2 289cb0d058c8 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-C3.t --- a/tests/test-exchange-C3.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-C3.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === C.3 Pruned changeset on precursors of another pruned one === @@ -60,6 +60,14 @@ 06055a7959d4128e6e3bccfd01482e83a2db8a3a 0 {28b51eb45704506b5c603decd6bf7ac5e0f6a52f} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 40be80b35671116f2c61ef25797806536a6eb5a0 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f beac7228bbe708bc7c9181c3c27f8a17f21dbd9f + 06055a7959d4128e6e3bccfd01482e83a2db8a3a 8b648bd67281e9e525919285ac7b3bb2836c2f02 + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 dcd2b566ad0983333be704afdc205066e1a6b742 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 0 a9bdc8b26820 0 1 1 40be80b35671 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-C4.t --- a/tests/test-exchange-C4.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-C4.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === C.4 multiple successors, one is pruned === @@ -70,6 +70,16 @@ f5bc6836db60e308a17ba08bf050154ba9c4fad7 35b1839966785d5703a01607229eea932db42f87 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} f5bc6836db60e308a17ba08bf050154ba9c4fad7 7f7f229b13a629a5b20581c6cb723f4e2ca54bed 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 7f7f229b13a629a5b20581c6cb723f4e2ca54bed 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 a9c02d134f5b98acc74d1dc4eb28fd59f958a2bd + f5bc6836db60e308a17ba08bf050154ba9c4fad7 619b4d13bd9878f04d7208dcfcf1e89da826f6be + 35b1839966785d5703a01607229eea932db42f87 ddeb7b7a87378f59cecb36d5146df0092b6b3327 + 7f7f229b13a629a5b20581c6cb723f4e2ca54bed 58ef2e726c5bd89bceffb6243294b38eadbf3d60 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 35b183996678 0 2 2 2a098b4a877f + 2 35b183996678 1 1 2 916e804c50de + 0 a9bdc8b26820 0 1 1 a9c02d134f5b $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-D1.t --- a/tests/test-exchange-D1.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-D1.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === D.1 Pruned changeset based on missing precursor of something not present === @@ -52,6 +52,14 @@ $ hg debugobsolete 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 06055a7959d4128e6e3bccfd01482e83a2db8a3a 0 {28b51eb45704506b5c603decd6bf7ac5e0f6a52f} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 289cb0d058c81c763eca8bb438657dba9a7ba646 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 1 e5ea8f9c7314 0 2 2 289cb0d058c8 + 0 a9bdc8b26820 0 1 1 000000000000 + 1 e5ea8f9c7314 1 1 2 289cb0d058c8 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-D2.t --- a/tests/test-exchange-D2.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-D2.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === D.2 missing prune target (prune in "pushed set") === @@ -49,6 +49,12 @@ $ hg debugobsolete 28b51eb45704506b5c603decd6bf7ac5e0f6a52f e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 {a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 554c0b12f7d9fff20cb904c26e12eee337e3309c + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 5c81c58ce0a8ad61dd9cf4c6949846b5990af30d + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 0 a9bdc8b26820 0 1 1 554c0b12f7d9 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-D3.t --- a/tests/test-exchange-D3.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-D3.t Fri Mar 31 15:33:59 2017 +0200 @@ -3,7 +3,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === D.2 missing prune target (prune in "pushed set") === @@ -53,6 +53,15 @@ $ hg debugobsolete 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 6aa67a7b4baa6fb41b06aed38d5b1201436546e2 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 6aa67a7b4baa6fb41b06aed38d5b1201436546e2 0 {35b1839966785d5703a01607229eea932db42f87} (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0000000000000000000000000000000000000000 + 35b1839966785d5703a01607229eea932db42f87 65a9f21dff0702355e973a8f31d3b3b7e59376fb + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 2 35b183996678 0 2 2 65a9f21dff07 + 2 35b183996678 1 1 2 65a9f21dff07 + 0 a9bdc8b26820 0 1 1 000000000000 $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-exchange-D4.t --- a/tests/test-exchange-D4.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-exchange-D4.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ Initial setup - $ . $TESTDIR/_exc-util.sh + $ . $TESTDIR/testlib/exchange-util.sh === D.4 Unknown changeset in between known one === @@ -65,6 +65,19 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} 6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 069b05c3876d56f62895e853a501ea58ea85f68d 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'} + $ hg debugobsrelsethashtree + a9bdc8b26820b1b87d585b82eb0ceb4a2ecdbc04 0000000000000000000000000000000000000000 + 28b51eb45704506b5c603decd6bf7ac5e0f6a52f 0000000000000000000000000000000000000000 + 6e72f0a95b5e01a7504743aa941f69cb1fbef8b0 0000000000000000000000000000000000000000 + e5ea8f9c73143125d36658e90ef70c6d2027a5b7 0aacc2f86e8fca29f2d5fd8d0790644620acd58a + 069b05c3876d56f62895e853a501ea58ea85f68d 40b98bc2b5b1152416ea8e9665ae1c6a3ce32ba0 + $ hg debugobshashrange --subranges --rev 'head()' + rev node index size depth obshash + 4 069b05c3876d 0 3 3 a2b2331da650 + 3 e5ea8f9c7314 0 2 2 0aacc2f86e8f + 4 069b05c3876d 2 1 3 901f118d4333 + 0 a9bdc8b26820 0 1 1 000000000000 + 3 e5ea8f9c7314 1 1 2 0aacc2f86e8f $ cd .. $ cd .. diff -r efda653c96a7 -r 94432e742a02 tests/test-import.t --- a/tests/test-import.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-import.t Fri Mar 31 15:33:59 2017 +0200 @@ -9,7 +9,7 @@ $ hg init auto-obsolete $ cd auto-obsolete $ echo '[extensions]' >> $HGRCPATH - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ echo A > a $ hg commit -Am A adding a diff -r efda653c96a7 -r 94432e742a02 tests/test-inhibit.t --- a/tests/test-inhibit.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-inhibit.t Fri Mar 31 15:33:59 2017 +0200 @@ -8,9 +8,9 @@ > rebase= > strip= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH - $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext/directaccess.py" >> $HGRCPATH - $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext/inhibit.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH + $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/directaccess.py" >> $HGRCPATH + $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/inhibit.py" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" @@ -728,7 +728,7 @@ $ cat >> $HGRCPATH < [extensions] > EOF - $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext/inhibit.py" >> $HGRCPATH + $ echo "inhibit=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/inhibit.py" >> $HGRCPATH Empty commit $ hg amend @@ -781,7 +781,7 @@ cannot use inhibit without the direct access extension (please enable it or inhibit won't work) 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext/directaccess.py" >> $HGRCPATH + $ echo "directaccess=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/hack/directaccess.py" >> $HGRCPATH $ cd .. hg push should not allow directaccess unless forced with --hidden @@ -807,7 +807,7 @@ [255] Visible commits can still be pushed - $ hg push -r 71eb4f100663 $pwd/inhibit2 + $ hg push -fr 71eb4f100663 $pwd/inhibit2 pushing to $TESTTMP/inhibit2 searching for changes adding changesets @@ -911,7 +911,7 @@ $ cd not-inhibit $ hg book -d foo $ hg pull - pulling from $TESTTMP/inhibit + pulling from $TESTTMP/inhibit (glob) searching for changes no changes found adding remote bookmark foo diff -r efda653c96a7 -r 94432e742a02 tests/test-obsconvert.t --- a/tests/test-obsconvert.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-obsconvert.t Fri Mar 31 15:33:59 2017 +0200 @@ -1,7 +1,7 @@ $ cat >> $HGRCPATH < [extensions] > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ hg init alpha $ cd alpha $ echo foo > foo diff -r efda653c96a7 -r 94432e742a02 tests/test-obsolete-push.t --- a/tests/test-obsolete-push.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-obsolete-push.t Fri Mar 31 15:33:59 2017 +0200 @@ -4,7 +4,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ template='{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n' $ glog() { diff -r efda653c96a7 -r 94432e742a02 tests/test-obsolete.t --- a/tests/test-obsolete.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-obsolete.t Fri Mar 31 15:33:59 2017 +0200 @@ -9,7 +9,7 @@ > [extensions] > hgext.rebase= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-oldconvert.t --- a/tests/test-oldconvert.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-oldconvert.t Fri Mar 31 15:33:59 2017 +0200 @@ -32,7 +32,7 @@ enable the extensions - $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/legacy.py" >> $HGRCPATH $ hg glog abort: old format of obsolete marker detected! diff -r efda653c96a7 -r 94432e742a02 tests/test-options.t --- a/tests/test-options.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-options.t Fri Mar 31 15:33:59 2017 +0200 @@ -3,7 +3,7 @@ > logtemplate={rev}:{node|short}[{bookmarks}] ({obsolete}/{phase}) {desc|firstline}\n > [extensions] > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-prev-next.t --- a/tests/test-prev-next.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-prev-next.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH hg prev -B should move active bookmark $ hg init diff -r efda653c96a7 -r 94432e742a02 tests/test-prune.t --- a/tests/test-prune.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-prune.t Fri Mar 31 15:33:59 2017 +0200 @@ -3,7 +3,7 @@ > logtemplate={rev}:{node|short}[{bookmarks}] ({separate('/', obsolete ,phase)}) {desc|firstline}\n > [extensions] > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-sharing.t --- a/tests/test-sharing.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-sharing.t Fri Mar 31 15:33:59 2017 +0200 @@ -9,7 +9,7 @@ > [extensions] > rebase = > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ hg init public $ hg clone public test-repo updating to branch default diff -r efda653c96a7 -r 94432e742a02 tests/test-simple4server-bundle2.t --- a/tests/test-simple4server-bundle2.t Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,166 +0,0 @@ - - $ cat >> $HGRCPATH < [defaults] - > amend=-d "0 0" - > [web] - > push_ssl = false - > allow_push = * - > [phases] - > publish = False - > [experimental] - > bundle2-exp=True - > EOF - - $ mkcommit() { - > echo "$1" > "$1" - > hg add "$1" - > hg ci -m "add $1" - > } - -setup repo - - $ hg init server - $ echo "[extensions]" >> ./server/.hg/hgrc - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/simple4server.py" >> ./server/.hg/hgrc - $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log - $ cat hg.pid >> $DAEMON_PIDS - - $ hg clone http://localhost:$HGPORT/ client - no changes found - updating to branch default - 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cat ./errors.log - $ echo "[extensions]" >> ./client/.hg/hgrc - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> ./client/.hg/hgrc - $ cp -r client other - -Smoke testing -=============== - - $ cd client - $ mkcommit 0 - $ mkcommit a - $ hg push - pushing to http://localhost:$HGPORT/ - searching for changes - remote: adding changesets - remote: adding manifests - remote: adding file changes - remote: added 2 changesets with 2 changes to 2 files - $ hg pull - pulling from http://localhost:$HGPORT/ - searching for changes - no changes found - $ cat ../errors.log - $ hg pull -R ../other - pulling from http://localhost:$HGPORT/ - requesting all changes - adding changesets - adding manifests - adding file changes - added 2 changesets with 2 changes to 2 files - (run 'hg update' to get a working copy) - $ cat ../errors.log - $ hg push -R ../other - pushing to http://localhost:$HGPORT/ - searching for changes - no changes found - [1] - $ cat ../errors.log - -Capacity testing -=================== - - $ curl -s http://localhost:$HGPORT/?cmd=hello - capabilities: * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (glob) - $ curl -s http://localhost:$HGPORT/?cmd=capabilities - * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (no-eol) (glob) - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - obsolete - phases - -Push -============= - - $ echo 'A' > a - $ hg amend - $ hg push - pushing to http://localhost:$HGPORT/ - searching for changes - remote: adding changesets - remote: adding manifests - remote: adding file changes - remote: added 1 changesets with 1 changes to 1 files (+1 heads) - remote: 2 new obsolescence markers - $ cat ../errors.log - $ hg push - pushing to http://localhost:$HGPORT/ - searching for changes - no changes found - [1] - $ cat ../errors.log - -Pull -============= - - $ hg -R ../other pull - pulling from http://localhost:$HGPORT/ - searching for changes - adding changesets - adding manifests - adding file changes - added 1 changesets with 1 changes to [12] files \(\+1 heads\) (re) - 2 new obsolescence markers - (run 'hg heads' to see heads, 'hg merge' to merge) - $ cat ../errors.log - $ hg -R ../other pull - pulling from http://localhost:$HGPORT/ - searching for changes - no changes found - $ cat ../errors.log - - $ cd .. - -Test disabling obsolete advertisement -=========================================== -(used by bitbucket to select which repo use evolve) - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - obsolete - phases - $ curl -s http://localhost:$HGPORT/?cmd=hello - capabilities: * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (glob) - $ curl -s http://localhost:$HGPORT/?cmd=capabilities - * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (no-eol) (glob) - - $ echo '[__temporary__]' >> server/.hg/hgrc - $ echo 'advertiseobsolete=False' >> server/.hg/hgrc - $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS - $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log - $ cat hg.pid >> $DAEMON_PIDS - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - phases - - $ echo 'advertiseobsolete=True' >> server/.hg/hgrc - $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS - $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log - $ cat hg.pid >> $DAEMON_PIDS - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - obsolete - phases - - $ curl -s http://localhost:$HGPORT/?cmd=hello - capabilities: * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (glob) - $ curl -s http://localhost:$HGPORT/?cmd=capabilities - * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (no-eol) (glob) diff -r efda653c96a7 -r 94432e742a02 tests/test-simple4server.t --- a/tests/test-simple4server.t Tue Feb 28 17:22:21 2017 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,174 +0,0 @@ - - $ cat >> $HGRCPATH < [defaults] - > amend=-d "0 0" - > [web] - > push_ssl = false - > allow_push = * - > [phases] - > publish = False - > [experimental] - > bundle2-exp=False # < Mercurial-4.0 - > [devel] - > legacy.exchange=bundle1 - > [extensions] - > EOF - - $ mkcommit() { - > echo "$1" > "$1" - > hg add "$1" - > hg ci -m "add $1" - > } - -setup repo - - $ hg init server - $ echo "[extensions]" >> ./server/.hg/hgrc - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/simple4server.py" >> ./server/.hg/hgrc - $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log - $ cat hg.pid >> $DAEMON_PIDS - - $ hg clone http://localhost:$HGPORT/ client - no changes found - updating to branch default - 0 files updated, 0 files merged, 0 files removed, 0 files unresolved - $ cat ./errors.log - $ echo "[extensions]" >> ./client/.hg/hgrc - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> ./client/.hg/hgrc - $ cp -r client other - -Smoke testing -=============== - - $ cd client - $ mkcommit 0 - $ mkcommit a - $ hg push - pushing to http://localhost:$HGPORT/ - searching for changes - remote: adding changesets - remote: adding manifests - remote: adding file changes - remote: added 2 changesets with 2 changes to 2 files - $ hg pull - pulling from http://localhost:$HGPORT/ - searching for changes - no changes found - $ cat ../errors.log - $ hg pull -R ../other - pulling from http://localhost:$HGPORT/ - requesting all changes - adding changesets - adding manifests - adding file changes - added 2 changesets with 2 changes to 2 files - pull obsolescence markers - (run 'hg update' to get a working copy) - $ cat ../errors.log - $ hg push -R ../other - pushing to http://localhost:$HGPORT/ - searching for changes - no changes found - [1] - $ cat ../errors.log - -Capacity testing -=================== - - $ curl -s http://localhost:$HGPORT/?cmd=hello - capabilities: * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (glob) - $ curl -s http://localhost:$HGPORT/?cmd=capabilities - * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (no-eol) (glob) - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - obsolete - phases - -Push -============= - - $ echo 'A' > a - $ hg amend - $ hg push - pushing to http://localhost:$HGPORT/ - searching for changes - remote: adding changesets - remote: adding manifests - remote: adding file changes - remote: added 1 changesets with 1 changes to 1 files (+1 heads) - pushing 2 obsolescence markers (* bytes) (glob) - $ cat ../errors.log - $ hg push - pushing to http://localhost:$HGPORT/ - searching for changes - no changes found - [1] - $ cat ../errors.log - -Pull -============= - - $ hg -R ../other pull - pulling from http://localhost:$HGPORT/ - searching for changes - adding changesets - adding manifests - adding file changes - added 1 changesets with 1 changes to [12] files \(\+1 heads\) (re) - pull obsolescence markers - 2 obsolescence markers added - (run 'hg heads' to see heads) - $ cat ../errors.log - $ hg -R ../other pull - pulling from http://localhost:$HGPORT/ - searching for changes - no changes found - $ cat ../errors.log - - $ cd .. - -Test disabling obsolete advertisement -=========================================== -(used by bitbucket to select which repo use evolve) - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - obsolete - phases - $ curl -s http://localhost:$HGPORT/?cmd=hello - capabilities: * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (glob) - $ curl -s http://localhost:$HGPORT/?cmd=capabilities - * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (no-eol) (glob) - - $ echo '[__temporary__]' >> server/.hg/hgrc - $ echo 'advertiseobsolete=False' >> server/.hg/hgrc - $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS - $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log - $ cat hg.pid >> $DAEMON_PIDS - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - phases - $ curl -s http://localhost:$HGPORT/?cmd=hello | grep _evoext_pushobsmarkers_0 - [1] - $ curl -s http://localhost:$HGPORT/?cmd=capabilities | grep _evoext_pushobsmarkers_0 - [1] - - $ echo 'advertiseobsolete=True' >> server/.hg/hgrc - $ $RUNTESTDIR/killdaemons.py $DAEMON_PIDS - $ hg serve -R server -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log - $ cat hg.pid >> $DAEMON_PIDS - - $ curl -s "http://localhost:$HGPORT/?cmd=listkeys&namespace=namespaces" | sort - bookmarks - namespaces - obsolete - phases - $ curl -s http://localhost:$HGPORT/?cmd=hello - capabilities: * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (glob) - $ curl -s http://localhost:$HGPORT/?cmd=capabilities - * _evoext_pushobsmarkers_0 _evoext_pullobsmarkers_0 _evoext_obshash_0 _evoext_obshash_1 _evoext_getbundle_obscommon (no-eol) (glob) diff -r efda653c96a7 -r 94432e742a02 tests/test-split.t --- a/tests/test-split.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-split.t Fri Mar 31 15:33:59 2017 +0200 @@ -20,7 +20,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-stabilize-conflict.t --- a/tests/test-stabilize-conflict.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-stabilize-conflict.t Fri Mar 31 15:33:59 2017 +0200 @@ -17,7 +17,7 @@ > touch.args=babar > [extensions] > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ safesed() { > sed "$1" "$2" > `pwd`/sed.temp diff -r efda653c96a7 -r 94432e742a02 tests/test-stabilize-order.t --- a/tests/test-stabilize-order.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-stabilize-order.t Fri Mar 31 15:33:59 2017 +0200 @@ -4,7 +4,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ glog() { > hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n' "$@" diff -r efda653c96a7 -r 94432e742a02 tests/test-stabilize-result.t --- a/tests/test-stabilize-result.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-stabilize-result.t Fri Mar 31 15:33:59 2017 +0200 @@ -5,7 +5,7 @@ > hgext.rebase= > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ glog() { > hg glog --template \ diff -r efda653c96a7 -r 94432e742a02 tests/test-stablerange.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-stablerange.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,677 @@ +Test for stable ordering capabilities +===================================== + + $ . $TESTDIR/testlib/pythonpath.sh + + $ cat << EOF >> $HGRCPATH + > [extensions] + > hgext3rd.evolve = + > [ui] + > logtemplate = "{rev} {node|short} {desc} {tags}\n" + > EOF + +Simple linear test +================== + + $ hg init repo_linear + $ cd repo_linear + $ hg debugbuilddag '.+6' + $ hg debugstablerange --verify --verbose --subranges --rev 1 + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 1 > 1.range + +bigger subset reuse most of the previous one + + $ hg debugstablerange --verify --verbose --subranges --rev 4 + bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 4 > 4.range + $ diff -u 1.range 4.range + --- 1.range * (glob) + +++ 4.range * (glob) + @@ -1,3 +1,9 @@ + +bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + +01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + +2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + +bebd167eb94d-4 (4, 5, 1) [leaf] - + [1] + +Using a range not ending on 2**N boundary +we fall back on 2**N as much as possible + + $ hg debugstablerange --verify --verbose --subranges --rev 5 + c8d03c1b5e94-0 (5, 6, 6) [complete] - 2dc09a01254d-0 (3, 4, 4), c8d03c1b5e94-4 (5, 6, 2) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + c8d03c1b5e94-5 (5, 6, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 5 > 5.range + $ diff -u 4.range 5.range + --- 4.range * (glob) + +++ 5.range * (glob) + @@ -1,9 +1,11 @@ + -bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + +c8d03c1b5e94-0 (5, 6, 6) [complete] - 2dc09a01254d-0 (3, 4, 4), c8d03c1b5e94-4 (5, 6, 2) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + +c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + +c8d03c1b5e94-5 (5, 6, 1) [leaf] - + [1] + +Even two unperfect range overlap a lot + + $ hg debugstablerange --verify --verbose --subranges --rev tip + f69452c5b1af-0 (6, 7, 7) [complete] - 2dc09a01254d-0 (3, 4, 4), f69452c5b1af-4 (6, 7, 3) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + f69452c5b1af-4 (6, 7, 3) [complete] - c8d03c1b5e94-4 (5, 6, 2), f69452c5b1af-6 (6, 7, 1) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + c8d03c1b5e94-5 (5, 6, 1) [leaf] - + f69452c5b1af-6 (6, 7, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev tip > tip.range + $ diff -u 5.range tip.range + --- 5.range * (glob) + +++ tip.range * (glob) + @@ -1,5 +1,6 @@ + -c8d03c1b5e94-0 (5, 6, 6) [complete] - 2dc09a01254d-0 (3, 4, 4), c8d03c1b5e94-4 (5, 6, 2) + +f69452c5b1af-0 (6, 7, 7) [complete] - 2dc09a01254d-0 (3, 4, 4), f69452c5b1af-4 (6, 7, 3) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + +f69452c5b1af-4 (6, 7, 3) [complete] - c8d03c1b5e94-4 (5, 6, 2), f69452c5b1af-6 (6, 7, 1) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + c8d03c1b5e94-4 (5, 6, 2) [complete] - bebd167eb94d-4 (4, 5, 1), c8d03c1b5e94-5 (5, 6, 1) + @@ -9,3 +10,4 @@ + 66f7d451a68b-1 (1, 2, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + c8d03c1b5e94-5 (5, 6, 1) [leaf] - + +f69452c5b1af-6 (6, 7, 1) [leaf] - + [1] + + $ cd .. + +Case with merge +=============== + +Simple case: branching is on a boundary +-------------------------------------------- + + $ hg init repo_merge_split_on_boundary + $ cd repo_merge_split_on_boundary + $ hg debugbuilddag '.:base + > +3:left + > +2:head + > ' + $ hg log -G + o 9 0338daf18215 r9 head tip + | + o 8 71b32fcf3f71 r8 + | + o 7 5f18015f9110 r7 merge + |\ + | o 6 a2f58e9c1e56 r6 right + | | + | o 5 3a367db1fabc r5 + | | + | o 4 e7bd5218ca15 r4 + | | + o | 3 2dc09a01254d r3 left + | | + o | 2 01241442b3c2 r2 + | | + o | 1 66f7d451a68b r1 + |/ + o 0 1ea73414a91b r0 base + + +Each of the linear branch reuse range internally + +(left branch) + + $ hg debugstablerange --verify --verbose --subranges --rev 'left~2' + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'left~2' > left-2.range + $ hg debugstablerange --verify --verbose --subranges --rev left + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'left' > left.range + $ diff -u left-2.range left.range + --- left-2.range * (glob) + +++ left.range * (glob) + @@ -1,3 +1,7 @@ + +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + +01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + +2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + [1] + +(right branch) + + $ hg debugstablerange --verify --verbose --subranges --rev right~2 + e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + e7bd5218ca15-1 (4, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'right~2' > right-2.range + $ hg debugstablerange --verify --verbose --subranges --rev right + a2f58e9c1e56-0 (6, 4, 4) [complete] - e7bd5218ca15-0 (4, 2, 2), a2f58e9c1e56-2 (6, 4, 2) + a2f58e9c1e56-2 (6, 4, 2) [complete] - 3a367db1fabc-2 (5, 3, 1), a2f58e9c1e56-3 (6, 4, 1) + e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 3a367db1fabc-2 (5, 3, 1) [leaf] - + a2f58e9c1e56-3 (6, 4, 1) [leaf] - + e7bd5218ca15-1 (4, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'right' > right.range + $ diff -u right-2.range right.range + --- right-2.range * (glob) + +++ right.range * (glob) + @@ -1,3 +1,7 @@ + +a2f58e9c1e56-0 (6, 4, 4) [complete] - e7bd5218ca15-0 (4, 2, 2), a2f58e9c1e56-2 (6, 4, 2) + +a2f58e9c1e56-2 (6, 4, 2) [complete] - 3a367db1fabc-2 (5, 3, 1), a2f58e9c1e56-3 (6, 4, 1) + e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + +3a367db1fabc-2 (5, 3, 1) [leaf] - + +a2f58e9c1e56-3 (6, 4, 1) [leaf] - + e7bd5218ca15-1 (4, 2, 1) [leaf] - + [1] + +The merge reuse as much of the slicing created for one of the branch + + $ hg debugstablerange --verify --verbose --subranges --rev merge + 5f18015f9110-0 (7, 8, 8) [complete] - 2dc09a01254d-0 (3, 4, 4), 5f18015f9110-4 (7, 8, 4) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 5f18015f9110-4 (7, 8, 4) [complete] - 3a367db1fabc-1 (5, 3, 2), 5f18015f9110-6 (7, 8, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 3a367db1fabc-1 (5, 3, 2) [complete] - e7bd5218ca15-1 (4, 2, 1), 3a367db1fabc-2 (5, 3, 1) + 5f18015f9110-6 (7, 8, 2) [complete] - a2f58e9c1e56-3 (6, 4, 1), 5f18015f9110-7 (7, 8, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 3a367db1fabc-2 (5, 3, 1) [leaf] - + 5f18015f9110-7 (7, 8, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + a2f58e9c1e56-3 (6, 4, 1) [leaf] - + e7bd5218ca15-1 (4, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'merge' > merge.range + $ diff -u left.range merge.range + --- left.range * (glob) + +++ merge.range * (glob) + @@ -1,7 +1,15 @@ + +5f18015f9110-0 (7, 8, 8) [complete] - 2dc09a01254d-0 (3, 4, 4), 5f18015f9110-4 (7, 8, 4) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + +5f18015f9110-4 (7, 8, 4) [complete] - 3a367db1fabc-1 (5, 3, 2), 5f18015f9110-6 (7, 8, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + +3a367db1fabc-1 (5, 3, 2) [complete] - e7bd5218ca15-1 (4, 2, 1), 3a367db1fabc-2 (5, 3, 1) + +5f18015f9110-6 (7, 8, 2) [complete] - a2f58e9c1e56-3 (6, 4, 1), 5f18015f9110-7 (7, 8, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + +3a367db1fabc-2 (5, 3, 1) [leaf] - + +5f18015f9110-7 (7, 8, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + +a2f58e9c1e56-3 (6, 4, 1) [leaf] - + +e7bd5218ca15-1 (4, 2, 1) [leaf] - + [1] + $ diff -u right.range merge.range + --- right.range * (glob) + +++ merge.range * (glob) + @@ -1,7 +1,15 @@ + -a2f58e9c1e56-0 (6, 4, 4) [complete] - e7bd5218ca15-0 (4, 2, 2), a2f58e9c1e56-2 (6, 4, 2) + -a2f58e9c1e56-2 (6, 4, 2) [complete] - 3a367db1fabc-2 (5, 3, 1), a2f58e9c1e56-3 (6, 4, 1) + -e7bd5218ca15-0 (4, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), e7bd5218ca15-1 (4, 2, 1) + +5f18015f9110-0 (7, 8, 8) [complete] - 2dc09a01254d-0 (3, 4, 4), 5f18015f9110-4 (7, 8, 4) + +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + +5f18015f9110-4 (7, 8, 4) [complete] - 3a367db1fabc-1 (5, 3, 2), 5f18015f9110-6 (7, 8, 2) + +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + +3a367db1fabc-1 (5, 3, 2) [complete] - e7bd5218ca15-1 (4, 2, 1), 3a367db1fabc-2 (5, 3, 1) + +5f18015f9110-6 (7, 8, 2) [complete] - a2f58e9c1e56-3 (6, 4, 1), 5f18015f9110-7 (7, 8, 1) + +66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + +01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + +2dc09a01254d-3 (3, 4, 1) [leaf] - + 3a367db1fabc-2 (5, 3, 1) [leaf] - + +5f18015f9110-7 (7, 8, 1) [leaf] - + +66f7d451a68b-1 (1, 2, 1) [leaf] - + a2f58e9c1e56-3 (6, 4, 1) [leaf] - + e7bd5218ca15-1 (4, 2, 1) [leaf] - + [1] + $ cd .. + +slice create multiple heads +--------------------------- + + $ hg init repo_merge_split_heads + $ cd repo_merge_split_heads + $ hg debugbuilddag '.:base + > +4:left + > +2:head + > ' + $ hg debugbuilddag '.:base + > +3:left + > +2:head + > ' + abort: repository is not empty + [255] + $ hg log -G + o 12 e6b8d5b46647 r12 head tip + | + o 11 485383494a89 r11 + | + o 10 8aca7f8c9bd2 r10 merge + |\ + | o 9 f4b7da68b467 r9 right + | | + | o 8 857477a9aebb r8 + | | + | o 7 42b07e8da27d r7 + | | + | o 6 b9bc20507e0b r6 + | | + | o 5 de561312eff4 r5 + | | + o | 4 bebd167eb94d r4 left + | | + o | 3 2dc09a01254d r3 + | | + o | 2 01241442b3c2 r2 + | | + o | 1 66f7d451a68b r1 + |/ + o 0 1ea73414a91b r0 base + + +Each of the linear branch reuse range internally + +(left branch) + + $ hg debugstablerange --verify --verbose --subranges --rev 'left~2' + 01241442b3c2-0 (2, 3, 3) [complete] - 66f7d451a68b-0 (1, 2, 2), 01241442b3c2-2 (2, 3, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'left~2' > left-2.range + $ hg debugstablerange --verify --verbose --subranges --rev left + bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'left' > left.range + $ diff -u left-2.range left.range + --- left-2.range * (glob) + +++ left.range * (glob) + @@ -1,5 +1,9 @@ + -01241442b3c2-0 (2, 3, 3) [complete] - 66f7d451a68b-0 (1, 2, 2), 01241442b3c2-2 (2, 3, 1) + +bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + +2dc09a01254d-3 (3, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + +bebd167eb94d-4 (4, 5, 1) [leaf] - + [1] + +(right branch) + + $ hg debugstablerange --verify --verbose --subranges --rev right~2 + 42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + 42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 42b07e8da27d-3 (7, 4, 1) [leaf] - + b9bc20507e0b-2 (6, 3, 1) [leaf] - + de561312eff4-1 (5, 2, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'right~2' > right-2.range + $ hg debugstablerange --verify --verbose --subranges --rev right + f4b7da68b467-0 (9, 6, 6) [complete] - 42b07e8da27d-0 (7, 4, 4), f4b7da68b467-4 (9, 6, 2) + 42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + 42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 42b07e8da27d-3 (7, 4, 1) [leaf] - + 857477a9aebb-4 (8, 5, 1) [leaf] - + b9bc20507e0b-2 (6, 3, 1) [leaf] - + de561312eff4-1 (5, 2, 1) [leaf] - + f4b7da68b467-5 (9, 6, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'right' > right.range + $ diff -u right-2.range right.range + --- right-2.range * (glob) + +++ right.range * (glob) + @@ -1,7 +1,11 @@ + +f4b7da68b467-0 (9, 6, 6) [complete] - 42b07e8da27d-0 (7, 4, 4), f4b7da68b467-4 (9, 6, 2) + 42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + 42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + +f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 42b07e8da27d-3 (7, 4, 1) [leaf] - + +857477a9aebb-4 (8, 5, 1) [leaf] - + b9bc20507e0b-2 (6, 3, 1) [leaf] - + de561312eff4-1 (5, 2, 1) [leaf] - + +f4b7da68b467-5 (9, 6, 1) [leaf] - + [1] + +In this case, the bottom of the split will have multiple heads, + +So we'll create more than 1 subrange out of it. + +We are still able to reuse one of the branch however + + $ hg debugstablerange --verify --verbose --subranges --rev merge + 8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3) + bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + 8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 42b07e8da27d-3 (7, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + 857477a9aebb-4 (8, 5, 1) [leaf] - + 8aca7f8c9bd2-10 (10, 11, 1) [leaf] - + b9bc20507e0b-2 (6, 3, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + de561312eff4-1 (5, 2, 1) [leaf] - + f4b7da68b467-5 (9, 6, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'merge' > merge.range + $ diff -u left.range merge.range + --- left.range * (glob) + +++ merge.range * (glob) + @@ -1,9 +1,21 @@ + +8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3) + bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + +42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + +8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + +42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + +de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + +f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + +42b07e8da27d-3 (7, 4, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + +857477a9aebb-4 (8, 5, 1) [leaf] - + +8aca7f8c9bd2-10 (10, 11, 1) [leaf] - + +b9bc20507e0b-2 (6, 3, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + +de561312eff4-1 (5, 2, 1) [leaf] - + +f4b7da68b467-5 (9, 6, 1) [leaf] - + [1] + $ diff -u right.range merge.range + --- right.range * (glob) + +++ merge.range * (glob) + @@ -1,11 +1,21 @@ + -f4b7da68b467-0 (9, 6, 6) [complete] - 42b07e8da27d-0 (7, 4, 4), f4b7da68b467-4 (9, 6, 2) + +8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3) + +bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + +2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + +8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1) + +2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + +66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1) + +01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + +2dc09a01254d-3 (3, 4, 1) [leaf] - + 42b07e8da27d-3 (7, 4, 1) [leaf] - + +66f7d451a68b-1 (1, 2, 1) [leaf] - + 857477a9aebb-4 (8, 5, 1) [leaf] - + +8aca7f8c9bd2-10 (10, 11, 1) [leaf] - + b9bc20507e0b-2 (6, 3, 1) [leaf] - + +bebd167eb94d-4 (4, 5, 1) [leaf] - + de561312eff4-1 (5, 2, 1) [leaf] - + f4b7da68b467-5 (9, 6, 1) [leaf] - + [1] + +Range above the merge, reuse subrange from the merge + + $ hg debugstablerange --verify --verbose --subranges --rev tip + e6b8d5b46647-0 (12, 13, 13) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), e6b8d5b46647-8 (12, 13, 5) + bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + e6b8d5b46647-8 (12, 13, 5) [complete] - 485383494a89-8 (11, 12, 4), e6b8d5b46647-12 (12, 13, 1) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + 485383494a89-8 (11, 12, 4) [complete] - f4b7da68b467-4 (9, 6, 2), 485383494a89-10 (11, 12, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + 485383494a89-10 (11, 12, 2) [complete] - 8aca7f8c9bd2-10 (10, 11, 1), 485383494a89-11 (11, 12, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1) + 01241442b3c2-2 (2, 3, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 42b07e8da27d-3 (7, 4, 1) [leaf] - + 485383494a89-11 (11, 12, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + 857477a9aebb-4 (8, 5, 1) [leaf] - + 8aca7f8c9bd2-10 (10, 11, 1) [leaf] - + b9bc20507e0b-2 (6, 3, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + de561312eff4-1 (5, 2, 1) [leaf] - + e6b8d5b46647-12 (12, 13, 1) [leaf] - + f4b7da68b467-5 (9, 6, 1) [leaf] - + $ hg debugstablerange --verify --verbose --subranges --rev 'tip' > tip.range + $ diff -u merge.range tip.range + --- merge.range * (glob) + +++ tip.range * (glob) + @@ -1,10 +1,12 @@ + -8aca7f8c9bd2-0 (10, 11, 11) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), 8aca7f8c9bd2-8 (10, 11, 3) + +e6b8d5b46647-0 (12, 13, 13) [complete] - bebd167eb94d-0 (4, 5, 5), 42b07e8da27d-0 (7, 4, 4), e6b8d5b46647-8 (12, 13, 5) + bebd167eb94d-0 (4, 5, 5) [complete] - 2dc09a01254d-0 (3, 4, 4), bebd167eb94d-4 (4, 5, 1) + +e6b8d5b46647-8 (12, 13, 5) [complete] - 485383494a89-8 (11, 12, 4), e6b8d5b46647-12 (12, 13, 1) + 2dc09a01254d-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2dc09a01254d-2 (3, 4, 2) + 42b07e8da27d-0 (7, 4, 4) [complete] - de561312eff4-0 (5, 2, 2), 42b07e8da27d-2 (7, 4, 2) + -8aca7f8c9bd2-8 (10, 11, 3) [complete] - f4b7da68b467-4 (9, 6, 2), 8aca7f8c9bd2-10 (10, 11, 1) + +485383494a89-8 (11, 12, 4) [complete] - f4b7da68b467-4 (9, 6, 2), 485383494a89-10 (11, 12, 2) + 2dc09a01254d-2 (3, 4, 2) [complete] - 01241442b3c2-2 (2, 3, 1), 2dc09a01254d-3 (3, 4, 1) + 42b07e8da27d-2 (7, 4, 2) [complete] - b9bc20507e0b-2 (6, 3, 1), 42b07e8da27d-3 (7, 4, 1) + +485383494a89-10 (11, 12, 2) [complete] - 8aca7f8c9bd2-10 (10, 11, 1), 485383494a89-11 (11, 12, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + de561312eff4-0 (5, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), de561312eff4-1 (5, 2, 1) + f4b7da68b467-4 (9, 6, 2) [complete] - 857477a9aebb-4 (8, 5, 1), f4b7da68b467-5 (9, 6, 1) + @@ -12,10 +14,12 @@ + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2dc09a01254d-3 (3, 4, 1) [leaf] - + 42b07e8da27d-3 (7, 4, 1) [leaf] - + +485383494a89-11 (11, 12, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + 857477a9aebb-4 (8, 5, 1) [leaf] - + 8aca7f8c9bd2-10 (10, 11, 1) [leaf] - + b9bc20507e0b-2 (6, 3, 1) [leaf] - + bebd167eb94d-4 (4, 5, 1) [leaf] - + de561312eff4-1 (5, 2, 1) [leaf] - + +e6b8d5b46647-12 (12, 13, 1) [leaf] - + f4b7da68b467-5 (9, 6, 1) [leaf] - + [1] + + $ cd .. + +Tests range with criss cross merge in the graph +=============================================== + + $ hg init repo_criss_cross + $ cd repo_criss_cross + $ hg debugbuilddag ' + > ..:g # 2 nodes, tagged "g" + > <2.:h # another node base one -2 -> 0, tagged "h" + > *1/2:m # merge -1 and -2 (1, 2), tagged "m" + > <2+2:i # 2 nodes based on -2, tag head as "i" + > .:c # 1 node tagged "c" + > <2.:b # 1 node based on -2; tagged "b" + > <2.:e # 1 node based on -2, tagged "e" + > ' + $ hg log -G + o 15 1d8d22637c2d r15 tip + |\ + | o 14 43227190fef8 r14 f + | | + | | o 13 b4594d867745 r13 e + | | | + | | | o 12 e46a4836065c r12 d + | | |/ + | | o 11 bab5d5bf48bd r11 + | |/ + | | o 10 ff43616e5d0f r10 b + | | | + | | | o 9 dcbb326fdec2 r9 a + | | |/ + | | o 8 d62d843c9a01 r8 + | | | + | | o 7 e7d9710d9fc6 r7 + | |/ + +---o 6 2702dd0c91e7 r6 c + | | + o | 5 f0f3ef9a6cd5 r5 i + | | + o | 4 4c748ffd1a46 r4 + | | + | o 3 2b6d669947cd r3 m + |/| + o | 2 fa942426a6fd r2 h + | | + | o 1 66f7d451a68b r1 g + |/ + o 0 1ea73414a91b r0 + + $ hg debugstablerange --verify --verbose --subranges --rev 'head()' + 1d8d22637c2d-0 (15, 8, 8) [complete] - 2b6d669947cd-0 (3, 4, 4), 1d8d22637c2d-4 (15, 8, 4) + dcbb326fdec2-0 (9, 7, 7) [complete] - 2b6d669947cd-0 (3, 4, 4), dcbb326fdec2-4 (9, 7, 3) + ff43616e5d0f-0 (10, 7, 7) [complete] - 2b6d669947cd-0 (3, 4, 4), ff43616e5d0f-4 (10, 7, 3) + b4594d867745-0 (13, 6, 6) [complete] - 2b6d669947cd-0 (3, 4, 4), b4594d867745-4 (13, 6, 2) + e46a4836065c-0 (12, 6, 6) [complete] - 2b6d669947cd-0 (3, 4, 4), e46a4836065c-4 (12, 6, 2) + 2702dd0c91e7-0 (6, 5, 5) [complete] - f0f3ef9a6cd5-0 (5, 4, 4), 2702dd0c91e7-4 (6, 5, 1) + 1d8d22637c2d-4 (15, 8, 4) [complete] - 4c748ffd1a46-2 (4, 3, 1), 43227190fef8-4 (14, 5, 1), 1d8d22637c2d-6 (15, 8, 2) + 2b6d669947cd-0 (3, 4, 4) [complete] - 66f7d451a68b-0 (1, 2, 2), 2b6d669947cd-2 (3, 4, 2) + f0f3ef9a6cd5-0 (5, 4, 4) [complete] - fa942426a6fd-0 (2, 2, 2), f0f3ef9a6cd5-2 (5, 4, 2) + dcbb326fdec2-4 (9, 7, 3) [complete] - d62d843c9a01-4 (8, 6, 2), dcbb326fdec2-6 (9, 7, 1) + ff43616e5d0f-4 (10, 7, 3) [complete] - d62d843c9a01-4 (8, 6, 2), ff43616e5d0f-6 (10, 7, 1) + 1d8d22637c2d-6 (15, 8, 2) [complete] - f0f3ef9a6cd5-3 (5, 4, 1), 1d8d22637c2d-7 (15, 8, 1) + 2b6d669947cd-2 (3, 4, 2) [complete] - fa942426a6fd-1 (2, 2, 1), 2b6d669947cd-3 (3, 4, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + b4594d867745-4 (13, 6, 2) [complete] - bab5d5bf48bd-4 (11, 5, 1), b4594d867745-5 (13, 6, 1) + d62d843c9a01-4 (8, 6, 2) [complete] - e7d9710d9fc6-4 (7, 5, 1), d62d843c9a01-5 (8, 6, 1) + e46a4836065c-4 (12, 6, 2) [complete] - bab5d5bf48bd-4 (11, 5, 1), e46a4836065c-5 (12, 6, 1) + f0f3ef9a6cd5-2 (5, 4, 2) [complete] - 4c748ffd1a46-2 (4, 3, 1), f0f3ef9a6cd5-3 (5, 4, 1) + fa942426a6fd-0 (2, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), fa942426a6fd-1 (2, 2, 1) + 1d8d22637c2d-7 (15, 8, 1) [leaf] - + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 2702dd0c91e7-4 (6, 5, 1) [leaf] - + 2b6d669947cd-3 (3, 4, 1) [leaf] - + 43227190fef8-4 (14, 5, 1) [leaf] - + 4c748ffd1a46-2 (4, 3, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + b4594d867745-5 (13, 6, 1) [leaf] - + bab5d5bf48bd-4 (11, 5, 1) [leaf] - + d62d843c9a01-5 (8, 6, 1) [leaf] - + dcbb326fdec2-6 (9, 7, 1) [leaf] - + e46a4836065c-5 (12, 6, 1) [leaf] - + e7d9710d9fc6-4 (7, 5, 1) [leaf] - + f0f3ef9a6cd5-3 (5, 4, 1) [leaf] - + fa942426a6fd-1 (2, 2, 1) [leaf] - + ff43616e5d0f-6 (10, 7, 1) [leaf] - + $ cd .. + +Tests range where a toprange is rooted on a merge +================================================= + + $ hg init slice_on_merge + $ cd slice_on_merge + $ hg debugbuilddag ' + > ..:a # 2 nodes, tagged "a" + > <2..:b # another branch with two node based on 0, tagged b + > *a/b:m # merge -1 and -2 (1, 2), tagged "m" + > ' + $ hg log -G + o 4 f37e476fba9a r4 m tip + |\ + | o 3 36315563e2fa r3 b + | | + | o 2 fa942426a6fd r2 + | | + o | 1 66f7d451a68b r1 a + |/ + o 0 1ea73414a91b r0 + + $ hg debugstablerange --verify --verbose --subranges --rev 'head()' + f37e476fba9a-0 (4, 5, 5) [complete] - 66f7d451a68b-0 (1, 2, 2), 36315563e2fa-0 (3, 3, 3), f37e476fba9a-4 (4, 5, 1) + 36315563e2fa-0 (3, 3, 3) [complete] - fa942426a6fd-0 (2, 2, 2), 36315563e2fa-2 (3, 3, 1) + 66f7d451a68b-0 (1, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), 66f7d451a68b-1 (1, 2, 1) + fa942426a6fd-0 (2, 2, 2) [complete] - 1ea73414a91b-0 (0, 1, 1), fa942426a6fd-1 (2, 2, 1) + 1ea73414a91b-0 (0, 1, 1) [leaf] - + 36315563e2fa-2 (3, 3, 1) [leaf] - + 66f7d451a68b-1 (1, 2, 1) [leaf] - + f37e476fba9a-4 (4, 5, 1) [leaf] - + fa942426a6fd-1 (2, 2, 1) [leaf] - + diff -r efda653c96a7 -r 94432e742a02 tests/test-stablesort.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-stablesort.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,1358 @@ +Test for stable ordering capabilities +===================================== + + $ . $TESTDIR/testlib/pythonpath.sh + + $ cat << EOF >> $HGRCPATH + > [extensions] + > hgext3rd.evolve = + > [ui] + > logtemplate = "{rev} {node|short} {desc} {tags}\n" + > [alias] + > showsort = debugstablesort --template="{node|short}\n" + > EOF + + + + $ checktopo () { + > seen='null'; + > for node in `hg showsort --rev "$1"`; do + > echo "=== checking $node ==="; + > hg log --rev "($seen) and $node::"; + > seen="${seen}+${node}"; + > done; + > } + +Basic tests +=========== +(no criss cross merge) + +Smoke tests +----------- + +Starts with a "simple case" + + $ hg init repo_A + $ cd repo_A + $ hg debugbuilddag ' + > ..:g # 2 nodes, tagged "g" + > <2.:h # another node base one -2 -> 0, tagged "h" + > *1/2:m # merge -1 and -2 (1, 2), tagged "m" + > <2+2:i # 2 nodes based on -2, tag head as "i" + > .:c # 1 node tagged "c" + > <2.:b # 1 node based on -2; tagged "b" + > <2.:e # 1 node based on -2, tagged "e" + > ' + $ hg log -G + o 15 1d8d22637c2d r15 tip + |\ + | o 14 43227190fef8 r14 f + | | + | | o 13 b4594d867745 r13 e + | | | + | | | o 12 e46a4836065c r12 d + | | |/ + | | o 11 bab5d5bf48bd r11 + | |/ + | | o 10 ff43616e5d0f r10 b + | | | + | | | o 9 dcbb326fdec2 r9 a + | | |/ + | | o 8 d62d843c9a01 r8 + | | | + | | o 7 e7d9710d9fc6 r7 + | |/ + +---o 6 2702dd0c91e7 r6 c + | | + o | 5 f0f3ef9a6cd5 r5 i + | | + o | 4 4c748ffd1a46 r4 + | | + | o 3 2b6d669947cd r3 m + |/| + o | 2 fa942426a6fd r2 h + | | + | o 1 66f7d451a68b r1 g + |/ + o 0 1ea73414a91b r0 + + $ hg showsort --rev 'all()' --traceback + 1ea73414a91b + 66f7d451a68b + fa942426a6fd + 2b6d669947cd + 43227190fef8 + bab5d5bf48bd + b4594d867745 + e46a4836065c + e7d9710d9fc6 + d62d843c9a01 + dcbb326fdec2 + ff43616e5d0f + 4c748ffd1a46 + f0f3ef9a6cd5 + 1d8d22637c2d + 2702dd0c91e7 + +Verify the topological order +---------------------------- + +Check we we did not issued a node before on ancestor + +output of log should be empty + + $ checktopo 'all()' + === checking 1ea73414a91b === + === checking 66f7d451a68b === + === checking fa942426a6fd === + === checking 2b6d669947cd === + === checking 43227190fef8 === + === checking bab5d5bf48bd === + === checking b4594d867745 === + === checking e46a4836065c === + === checking e7d9710d9fc6 === + === checking d62d843c9a01 === + === checking dcbb326fdec2 === + === checking ff43616e5d0f === + === checking 4c748ffd1a46 === + === checking f0f3ef9a6cd5 === + === checking 1d8d22637c2d === + === checking 2702dd0c91e7 === + +Check stability +=============== + +have repo with changesets in orders + + $ cd .. + $ hg -R repo_A log -G > A.log + $ hg clone repo_A repo_B --rev 5 + adding changesets + adding manifests + adding file changes + added 4 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg -R repo_B pull --rev 13 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg -R repo_B pull --rev 14 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + $ hg -R repo_B pull + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 7 changesets with 0 changes to 0 files (+3 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + $ hg -R repo_B log -G + o 15 1d8d22637c2d r15 tip + |\ + | | o 14 e46a4836065c r12 + | | | + | | | o 13 ff43616e5d0f r10 + | | | | + | | | | o 12 dcbb326fdec2 r9 + | | | |/ + | | | o 11 d62d843c9a01 r8 + | | | | + | | | o 10 e7d9710d9fc6 r7 + | | | | + +-------o 9 2702dd0c91e7 r6 + | | | | + | o---+ 8 43227190fef8 r14 + | / / + | +---o 7 b4594d867745 r13 + | | | + | o | 6 bab5d5bf48bd r11 + | |/ + | o 5 2b6d669947cd r3 + | |\ + | | o 4 66f7d451a68b r1 + | | | + @ | | 3 f0f3ef9a6cd5 r5 + | | | + o | | 2 4c748ffd1a46 r4 + |/ / + o / 1 fa942426a6fd r2 + |/ + o 0 1ea73414a91b r0 + + $ hg -R repo_B log -G > B.log + + $ hg clone repo_A repo_C --rev 10 + adding changesets + adding manifests + adding file changes + added 7 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg -R repo_C pull --rev 12 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg -R repo_C pull --rev 15 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + $ hg -R repo_C pull + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 0 changes to 0 files (+3 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + $ hg -R repo_C log -G + o 15 b4594d867745 r13 tip + | + | o 14 dcbb326fdec2 r9 + | | + | | o 13 2702dd0c91e7 r6 + | | | + | | | o 12 1d8d22637c2d r15 + | | |/| + | | | o 11 43227190fef8 r14 + | | | | + | | o | 10 f0f3ef9a6cd5 r5 + | | | | + | | o | 9 4c748ffd1a46 r4 + | | | | + +-------o 8 e46a4836065c r12 + | | | | + o-----+ 7 bab5d5bf48bd r11 + / / / + +-----@ 6 ff43616e5d0f r10 + | | | + o | | 5 d62d843c9a01 r8 + | | | + o---+ 4 e7d9710d9fc6 r7 + / / + | o 3 2b6d669947cd r3 + |/| + o | 2 fa942426a6fd r2 + | | + | o 1 66f7d451a68b r1 + |/ + o 0 1ea73414a91b r0 + + $ hg -R repo_C log -G > C.log + + $ hg clone repo_A repo_D --rev 2 + adding changesets + adding manifests + adding file changes + added 2 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg -R repo_D pull --rev 10 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 0 changes to 0 files + (run 'hg update' to get a working copy) + $ hg -R repo_D pull --rev 15 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg -R repo_D pull + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 0 changes to 0 files (+4 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + $ hg -R repo_D log -G + o 15 b4594d867745 r13 tip + | + | o 14 e46a4836065c r12 + |/ + o 13 bab5d5bf48bd r11 + | + | o 12 dcbb326fdec2 r9 + | | + | | o 11 2702dd0c91e7 r6 + | | | + | | | o 10 1d8d22637c2d r15 + | | |/| + +-----o 9 43227190fef8 r14 + | | | + | | o 8 f0f3ef9a6cd5 r5 + | | | + | | o 7 4c748ffd1a46 r4 + | | | + | +---o 6 ff43616e5d0f r10 + | | | + | o | 5 d62d843c9a01 r8 + | | | + | o | 4 e7d9710d9fc6 r7 + |/ / + o | 3 2b6d669947cd r3 + |\| + o | 2 66f7d451a68b r1 + | | + | @ 1 fa942426a6fd r2 + |/ + o 0 1ea73414a91b r0 + + $ hg -R repo_D log -G > D.log + +check the log output are different + + $ python "$RUNTESTDIR/md5sum.py" *.log + 55919ebc9c02f28070cf3255b1690f8c A.log + c6244b76a60d0707767dc71780e544f3 B.log + 4d8b08b8c50ecbdd2460a62e5852d84d C.log + 0f327003593b50b9591bea8ee28acb81 D.log + +bug stable ordering should be identical +--------------------------------------- + + $ repos="A B C D " + +for 'all()' + + $ for x in $repos; do + > echo $x + > hg -R repo_$x showsort --rev 'all()' > ${x}.all.order; + > done + A + B + C + D + + $ python "$RUNTESTDIR/md5sum.py" *.all.order + 0c6b2e6f15249c0359b0f93e28c5bd1c A.all.order + 0c6b2e6f15249c0359b0f93e28c5bd1c B.all.order + 0c6b2e6f15249c0359b0f93e28c5bd1c C.all.order + 0c6b2e6f15249c0359b0f93e28c5bd1c D.all.order + +one specific head + + $ for x in $repos; do + > hg -R repo_$x showsort --rev 'b4594d867745' > ${x}.b4594d867745.order; + > done + + $ python "$RUNTESTDIR/md5sum.py" *.b4594d867745.order + 5c40900a22008f24eab8dfe2f30ad79f A.b4594d867745.order + 5c40900a22008f24eab8dfe2f30ad79f B.b4594d867745.order + 5c40900a22008f24eab8dfe2f30ad79f C.b4594d867745.order + 5c40900a22008f24eab8dfe2f30ad79f D.b4594d867745.order + +one secific heads, that is a merge + + $ for x in $repos; do + > hg -R repo_$x showsort --rev '1d8d22637c2d' > ${x}.1d8d22637c2d.order; + > done + + $ python "$RUNTESTDIR/md5sum.py" *.1d8d22637c2d.order + 77dc20a6f86db9103df8edaae9ad2754 A.1d8d22637c2d.order + 77dc20a6f86db9103df8edaae9ad2754 B.1d8d22637c2d.order + 77dc20a6f86db9103df8edaae9ad2754 C.1d8d22637c2d.order + 77dc20a6f86db9103df8edaae9ad2754 D.1d8d22637c2d.order + +changeset that are not heads + + $ for x in $repos; do + > hg -R repo_$x showsort --rev 'e7d9710d9fc6+43227190fef8' > ${x}.non-heads.order; + > done + + $ python "$RUNTESTDIR/md5sum.py" *.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 A.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 B.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 C.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 D.non-heads.order + +Check with different subset + + $ hg clone repo_A repo_E --rev "43227190fef8" + adding changesets + adding manifests + adding file changes + added 5 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg -R repo_E pull --rev e7d9710d9fc6 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + + $ hg clone repo_A repo_F --rev "1d8d22637c2d" + adding changesets + adding manifests + adding file changes + added 8 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg -R repo_F pull --rev d62d843c9a01 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + + $ hg clone repo_A repo_G --rev "e7d9710d9fc6" + adding changesets + adding manifests + adding file changes + added 5 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg -R repo_G pull --rev 43227190fef8 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads' to see heads, 'hg merge' to merge) + $ hg -R repo_G pull --rev 2702dd0c91e7 + pulling from $TESTTMP/repo_A (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 0 changes to 0 files (+1 heads) + (run 'hg heads .' to see heads, 'hg merge' to merge) + + $ for x in E F G; do + > hg -R repo_$x showsort --rev 'e7d9710d9fc6+43227190fef8' > ${x}.non-heads.order; + > done + + $ python "$RUNTESTDIR/md5sum.py" *.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 A.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 B.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 C.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 D.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 E.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 F.non-heads.order + 94e0ea8cdade135dabde4ec5e9954329 G.non-heads.order + +Multiple recursions +=================== + + $ cat << EOF >> random_rev.py + > import random + > import sys + > + > loop = int(sys.argv[1]) + > var = int(sys.argv[2]) + > for x in range(loop): + > print(x + random.randint(0, var)) + > EOF + + $ hg init recursion_A + $ cd recursion_A + $ hg debugbuilddag ' + > .:base + > +3:A + > +2/A:C + > +3:F + > +2 + > ' + $ hg log -G + o 20 160a7a0adbf4 r20 tip + | + o 19 1c645e73dbc6 r19 + | + o 18 0496f0a6a143 r18 + |\ + | o 17 d64d500024d1 r17 + | | + | o 16 4dbf739dd63f r16 + | | + | o 15 9fff0871d230 r15 + | | + | | o 14 4bbfc6078919 r14 F + | | | + | | o 13 013b27f11536 r13 + | | | + +---o 12 a66b68853635 r12 + | | + o | 11 001194dd78d5 r11 E + |\ \ + | o | 10 6ee532b68cfa r10 + | | | + o | | 9 529dfc5bb875 r9 D + | | | + o | | 8 abf57d94268b r8 + | | | + +---o 7 5f18015f9110 r7 C + | | | + | | o 6 a2f58e9c1e56 r6 + | | | + | | o 5 3a367db1fabc r5 + | |/ + | o 4 e7bd5218ca15 r4 B + | | + o | 3 2dc09a01254d r3 A + | | + o | 2 01241442b3c2 r2 + | | + o | 1 66f7d451a68b r1 + |/ + o 0 1ea73414a91b r0 base + + $ hg showsort --rev 'all()' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 2dc09a01254d + abf57d94268b + 529dfc5bb875 + e7bd5218ca15 + 3a367db1fabc + a2f58e9c1e56 + 5f18015f9110 + 9fff0871d230 + 4dbf739dd63f + d64d500024d1 + 6ee532b68cfa + 001194dd78d5 + 0496f0a6a143 + 1c645e73dbc6 + 160a7a0adbf4 + a66b68853635 + 013b27f11536 + 4bbfc6078919 + $ checktopo 'all()' + === checking 1ea73414a91b === + === checking 66f7d451a68b === + === checking 01241442b3c2 === + === checking 2dc09a01254d === + === checking abf57d94268b === + === checking 529dfc5bb875 === + === checking e7bd5218ca15 === + === checking 3a367db1fabc === + === checking a2f58e9c1e56 === + === checking 5f18015f9110 === + === checking 9fff0871d230 === + === checking 4dbf739dd63f === + === checking d64d500024d1 === + === checking 6ee532b68cfa === + === checking 001194dd78d5 === + === checking 0496f0a6a143 === + === checking 1c645e73dbc6 === + === checking 160a7a0adbf4 === + === checking a66b68853635 === + === checking 013b27f11536 === + === checking 4bbfc6078919 === + $ hg showsort --rev 'all()' > ../multiple.source.order + $ hg log -r tip + 20 160a7a0adbf4 r20 tip + $ cd .. + + $ hg clone recursion_A recursion_random --rev 0 + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd recursion_random + $ for x in `python ../random_rev.py 15 5`; do + > # using python to benefit from the random seed + > hg pull -r $x --quiet + > done; + $ hg pull --quiet + $ hg showsort --rev 'all()' > ../multiple.random.order + $ python "$RUNTESTDIR/md5sum.py" ../multiple.*.order + 6ff802a0a5f0a3ddd82b25f860238fbd ../multiple.random.order + 6ff802a0a5f0a3ddd82b25f860238fbd ../multiple.source.order + $ hg showsort --rev 'all()' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 2dc09a01254d + abf57d94268b + 529dfc5bb875 + e7bd5218ca15 + 3a367db1fabc + a2f58e9c1e56 + 5f18015f9110 + 9fff0871d230 + 4dbf739dd63f + d64d500024d1 + 6ee532b68cfa + 001194dd78d5 + 0496f0a6a143 + 1c645e73dbc6 + 160a7a0adbf4 + a66b68853635 + 013b27f11536 + 4bbfc6078919 + $ cd .. + +Check criss cross merge +======================= + + $ hg init crisscross_A + $ cd crisscross_A + $ hg debugbuilddag ' + > ...:base # create some base + > # criss cross #1: simple + > +3:AbaseA # "A" branch for CC "A" + > # criss cross #2:multiple closes ones + > .:BbaseA + > # criss cross #2:many branches + > ' + $ hg log -G + o 94 01f771406cab r94 Cfinal tip + |\ + | o 93 84d6ec6a8e21 r93 CmergeZB + | |\ + o | | 92 721ba7c5f4ff r92 CmergeZA + |\| | + | | o 91 8ae32c3ed670 r91 CmergeYC + | | |\ + | o \ \ 90 8b79544bb56d r90 CmergeYB + | |\ \ \ + o \ \ \ \ 89 041e1188f5f1 r89 CmergeYA + |\ \ \ \ \ + | o \ \ \ \ 88 2472d042ec95 r88 CmergeXF + | |\ \ \ \ \ + | | | | o \ \ 87 c7d3029bf731 r87 CmergeXE + | | | | |\ \ \ + | | | | | | | o 86 469c700e9ed8 r86 CmergeXD + | | | | | | | |\ + | | | | | | o \ \ 85 28be96b80dc1 r85 CmergeXC + | | | | | | |\ \ \ + | | | o \ \ \ \ \ \ 84 dbde319d43a3 r84 CmergeXB + | | | |\ \ \ \ \ \ \ + o | | | | | | | | | | 83 b3cf98c3d587 r83 CmergeXA + |\| | | | | | | | | | + | | | | | | o | | | | 82 1da228afcf06 r82 CmergeWK + | | | | | | |\ \ \ \ \ + | | | | | | +-+-------o 81 0bab31f71a21 r81 CmergeWJ + | | | | | | | | | | | + | | | | | | | | | o | 80 cd345198cf12 r80 CmergeWI + | | | | | | | | | |\ \ + | | | | o \ \ \ \ \ \ \ 79 82238c0bc950 r79 CmergeWH + | | | | |\ \ \ \ \ \ \ \ + o \ \ \ \ \ \ \ \ \ \ \ \ 78 89a0fe204177 r78 CmergeWG + |\ \ \ \ \ \ \ \ \ \ \ \ \ + | | | o \ \ \ \ \ \ \ \ \ \ 77 97d19fc5236f r77 CmergeWF + | | | |\ \ \ \ \ \ \ \ \ \ \ + | | | | | | | | o \ \ \ \ \ \ 76 37ad3ab0cddf r76 CmergeWE + | | | | | | | | |\ \ \ \ \ \ \ + | | | | | | | | | | | | | | | o 75 790cdfecd168 r75 CmergeWD + | | | | | | | | | | | | | | | |\ + | | | | | | | | | | | | o \ \ \ \ 74 698970a2480b r74 CmergeWC + | | | | | | | | | | | | |\ \ \ \ \ + | | | | | o \ \ \ \ \ \ \ \ \ \ \ \ 73 31d7b43cc321 r73 CmergeWB + | | | | | |\ \ \ \ \ \ \ \ \ \ \ \ \ + | | o \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 72 eed373b0090d r72 CmergeWA + | | |\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ + | | | | | | | | | | | o \ \ \ \ \ \ \ \ 71 4f3b41956174 r71 CmergeT + | | | | | | | | | | | |\ \ \ \ \ \ \ \ \ + | | | | | o | | | | | | | | | | | | | | | 70 c3c7fa726f88 r70 CmergeS + | | | | | | | | | | | | | | | | | | | | | + | | | | | o-------------+ | | | | | | | | 69 d917f77a6439 r69 + | | | | | | | | | | | | | | | | | | | | | + | o | | | | | | | | | | | | | | | | | | | 68 fac9e582edd1 r68 CmergeR + | | | | | | | | | | | | | | | | | | | | | + | o | | | | | | | | | | | | | | | | | | | 67 e4cfd6264623 r67 + | | | | | | | | | | | | | | | | | | | | | + | o---------------------+ | | | | | | | | 66 d99e0f7dad5b r66 + | | | | | | | | | | | | | | | | | | | | | + | | | | | | | | | o-----+ | | | | | | | | 65 c713eae2d31f r65 CmergeQ + | | | | | | | | | | | | | | | | | | | | | + | | | | | | | | | | | +-+-----------o | | 64 b33fd5ad4c0c r64 CmergeP + | | | | | | | | | | | | | | | | | | / / + | | | | | +-----------+-----o | | | / / 63 bf6593f7e073 r63 CmergeO + | | | | | | | | | | | | | | / / / / / + | | | | | | | | | | | | | o | | | | | 62 3871506da61e r62 CmergeN + | | | | | | | | | | | | | | | | | | | + | | | | | | | | | | | | | o | | | | | 61 c84da74cf586 r61 + | | | | | | | | | | | | | | | | | | | + | | | | | | | | | | | | | o | | | | | 60 5eec91b12a58 r60 + | | | | | | | | | | | | | | | | | | | + | +-------------------+---o | | | | | 59 0484d39906c8 r59 + | | | | | | | | | | | | | / / / / / + | | | | | | | | | +---+-------o / / 58 29141354a762 r58 CmergeM + | | | | | | | | | | | | | | | / / + | | | | | | | | o | | | | | | | | 57 e7135b665740 r57 CmergeL + | | | | | | | | | | | | | | | | | + | | | | | | | | o | | | | | | | | 56 c7c1497fc270 r56 + | | | | | | | | | | | | | | | | | + | | | | | +-----o-------+ | | | | 55 76151e8066e1 r55 + | | | | | | | | / / / / / / / / + o | | | | | | | | | | | | | | | 54 9a67238ad1c4 r54 CmergeK + | | | | | | | | | | | | | | | | + o | | | | | | | | | | | | | | | 53 c37e7cd9f2bd r53 + | | | | | | | | | | | | | | | | + o | | | | | | | | | | | | | | | 52 0d153e3ad632 r52 + | | | | | | | | | | | | | | | | + o | | | | | | | | | | | | | | | 51 97ac964e34b7 r51 + | | | | | | | | | | | | | | | | + o | | | | | | | | | | | | | | | 50 900dd066a072 r50 + | | | | | | | | | | | | | | | | + o---------+---------+ | | | | | 49 673f5499c8c2 r49 + / / / / / / / / / / / / / / / + +-----o / / / / / / / / / / / 48 8ecb28746ec4 r48 CmergeJ + | | | |/ / / / / / / / / / / + | | | | | | | o | | | | | | 47 d6c9e2d27f14 r47 CmergeI + | | | | | | | | | | | | | | + | | | +-------o | | | | | | 46 bfcfd9a61e84 r46 + | | | | | | |/ / / / / / / + +---------------+-------o 45 40553f55397e r45 CmergeH + | | | | | | | | | | | | + | | o | | | | | | | | | 44 d94da36be176 r44 CmergeG + | | | | | | | | | | | | + +---o---------+ | | | | 43 4b39f229a0ce r43 + | | / / / / / / / / / + +---+---o / / / / / / 42 43fc0b77ff07 r42 CmergeF + | | | | / / / / / / + | | | | | | | | o | 41 88eace5ce682 r41 CmergeE + | | | | | | | | | | + | | | | | | | | o | 40 d928b4e8a515 r40 + | | | | | | | | | | + +-------+-------o | 39 88714f4125cb r39 + | | | | | | | | / + | | | | +---+---o 38 e3e6738c56ce r38 CmergeD + | | | | | | | | + | | | | | | | o 37 32b41ca704e1 r37 CmergeC + | | | | | | | | + | | | | +-+---o 36 01e29e20ea3f r36 + | | | | | | | + | | | o | | | 35 1f4a19f83a29 r35 CmergeB + | | |/|/ / / + | o | | | | 34 722d1b8b8942 r34 CmergeA + | | | | | | + | o | | | | 33 47c836a1f13e r33 + | | | | | | + | o | | | | 32 2ea3fbf151b5 r32 + | | | | | | + | o | | | | 31 0c3f2ba59eb7 r31 + | | | | | | + | o | | | | 30 f3441cd3e664 r30 + | | | | | | + | o | | | | 29 b9c3aa92fba5 r29 + | | | | | | + | o | | | | 28 3bdb00d5c818 r28 + | | | | | | + | o---+ | | 27 2bd677d0f13a r27 + |/ / / / / + | | | | o 26 de05b9c29ec7 r26 CbaseE + | | | | | + | | | o | 25 ad46a4a0fc10 r25 CbaseD + | | | | | + | | | o | 24 a457569c5306 r24 + | | | | | + | | | o | 23 f2bdd828a3aa r23 + | | | | | + | | | o | 22 5ce588c2b7c5 r22 + | | | | | + | | | o | 21 17b6e6bac221 r21 + | | | |/ + | o---+ 20 b115c694654e r20 CbaseC + | / / + o | | 19 884936b34999 r19 CbaseB + | | | + o---+ 18 9729470d9329 r18 + / / + o / 17 4f5078f7da8a r17 CbaseA + |/ + o 16 3e1560705803 r16 Bfinal + |\ + | o 15 55bf3fdb634f r15 BmergeD + | |\ + o---+ 14 39bab1cb1cbe r14 BmergeC + |/ / + | o 13 f7c6e7bfbcd0 r13 BmergeB + | |\ + o---+ 12 26f59ee8b1d7 r12 BmergeA + |/ / + | o 11 3e2da24aee59 r11 BbaseA + | | + | o 10 5ba9a53052ed r10 Afinal + |/| + o | 9 07c648efceeb r9 AmergeB BbaseB + |\ \ + +---o 8 c81423bf5a24 r8 AmergeA + | |/ + | o 7 65eb34ffc3a8 r7 AbaseB + | | + | o 6 0c1445abb33d r6 + | | + o | 5 c8d03c1b5e94 r5 AbaseA + | | + o | 4 bebd167eb94d r4 + | | + o | 3 2dc09a01254d r3 + |/ + o 2 01241442b3c2 r2 base + | + o 1 66f7d451a68b r1 + | + o 0 1ea73414a91b r0 + + +Basic check +----------- + + $ hg showsort --rev 'Afinal' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 0c1445abb33d + 65eb34ffc3a8 + 2dc09a01254d + bebd167eb94d + c8d03c1b5e94 + 07c648efceeb + c81423bf5a24 + 5ba9a53052ed + $ checktopo Afinal + === checking 1ea73414a91b === + === checking 66f7d451a68b === + === checking 01241442b3c2 === + === checking 0c1445abb33d === + === checking 65eb34ffc3a8 === + === checking 2dc09a01254d === + === checking bebd167eb94d === + === checking c8d03c1b5e94 === + === checking 07c648efceeb === + === checking c81423bf5a24 === + === checking 5ba9a53052ed === + $ hg showsort --rev 'AmergeA' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 0c1445abb33d + 65eb34ffc3a8 + 2dc09a01254d + bebd167eb94d + c8d03c1b5e94 + c81423bf5a24 + $ checktopo AmergeA + === checking 1ea73414a91b === + === checking 66f7d451a68b === + === checking 01241442b3c2 === + === checking 0c1445abb33d === + === checking 65eb34ffc3a8 === + === checking 2dc09a01254d === + === checking bebd167eb94d === + === checking c8d03c1b5e94 === + === checking c81423bf5a24 === + $ hg showsort --rev 'AmergeB' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 0c1445abb33d + 65eb34ffc3a8 + 2dc09a01254d + bebd167eb94d + c8d03c1b5e94 + 07c648efceeb + $ checktopo AmergeB + === checking 1ea73414a91b === + === checking 66f7d451a68b === + === checking 01241442b3c2 === + === checking 0c1445abb33d === + === checking 65eb34ffc3a8 === + === checking 2dc09a01254d === + === checking bebd167eb94d === + === checking c8d03c1b5e94 === + === checking 07c648efceeb === + +close criss cross + $ hg showsort --rev 'Bfinal' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 0c1445abb33d + 65eb34ffc3a8 + 2dc09a01254d + bebd167eb94d + c8d03c1b5e94 + 07c648efceeb + c81423bf5a24 + 5ba9a53052ed + 3e2da24aee59 + 26f59ee8b1d7 + f7c6e7bfbcd0 + 39bab1cb1cbe + 55bf3fdb634f + 3e1560705803 + $ checktopo Bfinal + === checking 1ea73414a91b === + === checking 66f7d451a68b === + === checking 01241442b3c2 === + === checking 0c1445abb33d === + === checking 65eb34ffc3a8 === + === checking 2dc09a01254d === + === checking bebd167eb94d === + === checking c8d03c1b5e94 === + === checking 07c648efceeb === + === checking c81423bf5a24 === + === checking 5ba9a53052ed === + === checking 3e2da24aee59 === + === checking 26f59ee8b1d7 === + === checking f7c6e7bfbcd0 === + === checking 39bab1cb1cbe === + === checking 55bf3fdb634f === + === checking 3e1560705803 === + +many branches criss cross + + $ hg showsort --rev 'Cfinal' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 0c1445abb33d + 65eb34ffc3a8 + 2dc09a01254d + bebd167eb94d + c8d03c1b5e94 + 07c648efceeb + c81423bf5a24 + 5ba9a53052ed + 3e2da24aee59 + 26f59ee8b1d7 + f7c6e7bfbcd0 + 39bab1cb1cbe + 55bf3fdb634f + 3e1560705803 + 17b6e6bac221 + 5ce588c2b7c5 + f2bdd828a3aa + a457569c5306 + ad46a4a0fc10 + 4f5078f7da8a + 01e29e20ea3f + 32b41ca704e1 + 29141354a762 + 9729470d9329 + 884936b34999 + 0484d39906c8 + 5eec91b12a58 + c84da74cf586 + 3871506da61e + 2bd677d0f13a + 3bdb00d5c818 + b9c3aa92fba5 + f3441cd3e664 + 0c3f2ba59eb7 + 2ea3fbf151b5 + 47c836a1f13e + 722d1b8b8942 + 4b39f229a0ce + d94da36be176 + eed373b0090d + 88714f4125cb + d928b4e8a515 + 88eace5ce682 + 698970a2480b + b115c694654e + 1f4a19f83a29 + 43fc0b77ff07 + 31d7b43cc321 + 673f5499c8c2 + 900dd066a072 + 97ac964e34b7 + 0d153e3ad632 + c37e7cd9f2bd + 9a67238ad1c4 + 8ecb28746ec4 + bf6593f7e073 + 0bab31f71a21 + 1da228afcf06 + bfcfd9a61e84 + d6c9e2d27f14 + de05b9c29ec7 + 40553f55397e + 4f3b41956174 + 37ad3ab0cddf + c7d3029bf731 + 76151e8066e1 + c7c1497fc270 + e7135b665740 + b33fd5ad4c0c + cd345198cf12 + 28be96b80dc1 + c713eae2d31f + 82238c0bc950 + dbde319d43a3 + 8b79544bb56d + d917f77a6439 + c3c7fa726f88 + 97d19fc5236f + 2472d042ec95 + d99e0f7dad5b + e4cfd6264623 + fac9e582edd1 + 89a0fe204177 + b3cf98c3d587 + 041e1188f5f1 + 721ba7c5f4ff + e3e6738c56ce + 790cdfecd168 + 469c700e9ed8 + 8ae32c3ed670 + 84d6ec6a8e21 + 01f771406cab + $ checktopo Cfinal + === checking 1ea73414a91b === + === checking 66f7d451a68b === + === checking 01241442b3c2 === + === checking 0c1445abb33d === + === checking 65eb34ffc3a8 === + === checking 2dc09a01254d === + === checking bebd167eb94d === + === checking c8d03c1b5e94 === + === checking 07c648efceeb === + === checking c81423bf5a24 === + === checking 5ba9a53052ed === + === checking 3e2da24aee59 === + === checking 26f59ee8b1d7 === + === checking f7c6e7bfbcd0 === + === checking 39bab1cb1cbe === + === checking 55bf3fdb634f === + === checking 3e1560705803 === + === checking 17b6e6bac221 === + === checking 5ce588c2b7c5 === + === checking f2bdd828a3aa === + === checking a457569c5306 === + === checking ad46a4a0fc10 === + === checking 4f5078f7da8a === + === checking 01e29e20ea3f === + === checking 32b41ca704e1 === + === checking 29141354a762 === + === checking 9729470d9329 === + === checking 884936b34999 === + === checking 0484d39906c8 === + === checking 5eec91b12a58 === + === checking c84da74cf586 === + === checking 3871506da61e === + === checking 2bd677d0f13a === + === checking 3bdb00d5c818 === + === checking b9c3aa92fba5 === + === checking f3441cd3e664 === + === checking 0c3f2ba59eb7 === + === checking 2ea3fbf151b5 === + === checking 47c836a1f13e === + === checking 722d1b8b8942 === + === checking 4b39f229a0ce === + === checking d94da36be176 === + === checking eed373b0090d === + === checking 88714f4125cb === + === checking d928b4e8a515 === + === checking 88eace5ce682 === + === checking 698970a2480b === + === checking b115c694654e === + === checking 1f4a19f83a29 === + === checking 43fc0b77ff07 === + === checking 31d7b43cc321 === + === checking 673f5499c8c2 === + === checking 900dd066a072 === + === checking 97ac964e34b7 === + === checking 0d153e3ad632 === + === checking c37e7cd9f2bd === + === checking 9a67238ad1c4 === + === checking 8ecb28746ec4 === + === checking bf6593f7e073 === + === checking 0bab31f71a21 === + === checking 1da228afcf06 === + === checking bfcfd9a61e84 === + === checking d6c9e2d27f14 === + === checking de05b9c29ec7 === + === checking 40553f55397e === + === checking 4f3b41956174 === + === checking 37ad3ab0cddf === + === checking c7d3029bf731 === + === checking 76151e8066e1 === + === checking c7c1497fc270 === + === checking e7135b665740 === + === checking b33fd5ad4c0c === + === checking cd345198cf12 === + === checking 28be96b80dc1 === + === checking c713eae2d31f === + === checking 82238c0bc950 === + === checking dbde319d43a3 === + === checking 8b79544bb56d === + === checking d917f77a6439 === + === checking c3c7fa726f88 === + === checking 97d19fc5236f === + === checking 2472d042ec95 === + === checking d99e0f7dad5b === + === checking e4cfd6264623 === + === checking fac9e582edd1 === + === checking 89a0fe204177 === + === checking b3cf98c3d587 === + === checking 041e1188f5f1 === + === checking 721ba7c5f4ff === + === checking e3e6738c56ce === + === checking 790cdfecd168 === + === checking 469c700e9ed8 === + === checking 8ae32c3ed670 === + === checking 84d6ec6a8e21 === + === checking 01f771406cab === + +Test stability of this mess +--------------------------- + + $ hg log -r tip + 94 01f771406cab r94 Cfinal tip + $ hg showsort --rev 'all()' > ../crisscross.source.order + $ cd .. + + $ hg clone crisscross_A crisscross_random --rev 0 + adding changesets + adding manifests + adding file changes + added 1 changesets with 0 changes to 0 files + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd crisscross_random + $ for x in `python ../random_rev.py 50 44`; do + > # using python to benefit from the random seed + > hg pull -r $x --quiet + > done; + $ hg pull --quiet + + $ hg showsort --rev 'all()' > ../crisscross.random.order + $ python "$RUNTESTDIR/md5sum.py" ../crisscross.*.order + d9aab0d1907d5cf64d205a8b9036e959 ../crisscross.random.order + d9aab0d1907d5cf64d205a8b9036e959 ../crisscross.source.order + $ diff -u ../crisscross.*.order + $ hg showsort --rev 'all()' + 1ea73414a91b + 66f7d451a68b + 01241442b3c2 + 0c1445abb33d + 65eb34ffc3a8 + 2dc09a01254d + bebd167eb94d + c8d03c1b5e94 + 07c648efceeb + c81423bf5a24 + 5ba9a53052ed + 3e2da24aee59 + 26f59ee8b1d7 + f7c6e7bfbcd0 + 39bab1cb1cbe + 55bf3fdb634f + 3e1560705803 + 17b6e6bac221 + 5ce588c2b7c5 + f2bdd828a3aa + a457569c5306 + ad46a4a0fc10 + 4f5078f7da8a + 01e29e20ea3f + 32b41ca704e1 + 29141354a762 + 9729470d9329 + 884936b34999 + 0484d39906c8 + 5eec91b12a58 + c84da74cf586 + 3871506da61e + 2bd677d0f13a + 3bdb00d5c818 + b9c3aa92fba5 + f3441cd3e664 + 0c3f2ba59eb7 + 2ea3fbf151b5 + 47c836a1f13e + 722d1b8b8942 + 4b39f229a0ce + d94da36be176 + eed373b0090d + 88714f4125cb + d928b4e8a515 + 88eace5ce682 + 698970a2480b + b115c694654e + 1f4a19f83a29 + 43fc0b77ff07 + 31d7b43cc321 + 673f5499c8c2 + 900dd066a072 + 97ac964e34b7 + 0d153e3ad632 + c37e7cd9f2bd + 9a67238ad1c4 + 8ecb28746ec4 + bf6593f7e073 + 0bab31f71a21 + 1da228afcf06 + bfcfd9a61e84 + d6c9e2d27f14 + de05b9c29ec7 + 40553f55397e + 4f3b41956174 + 37ad3ab0cddf + c7d3029bf731 + 76151e8066e1 + c7c1497fc270 + e7135b665740 + b33fd5ad4c0c + cd345198cf12 + 28be96b80dc1 + c713eae2d31f + 82238c0bc950 + dbde319d43a3 + 8b79544bb56d + d917f77a6439 + c3c7fa726f88 + 97d19fc5236f + 2472d042ec95 + d99e0f7dad5b + e4cfd6264623 + fac9e582edd1 + 89a0fe204177 + b3cf98c3d587 + 041e1188f5f1 + 721ba7c5f4ff + e3e6738c56ce + 790cdfecd168 + 469c700e9ed8 + 8ae32c3ed670 + 84d6ec6a8e21 + 01f771406cab + + +Test behavior with oedipus merges +================================= + + $ hg init recursion_oedipus + $ cd recursion_oedipus + $ echo base > base + $ hg add base + $ hg ci -m base + $ hg branch foo + marked working directory as branch foo + (branches are permanent and global, did you want a bookmark?) + $ echo foo1 > foo1 + $ hg add foo1 + $ hg ci -m foo1 + $ echo foo2 > foo2 + $ hg add foo2 + $ hg ci -m foo2 + $ hg up default + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg merge foo + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg ci -m oedipus_merge + $ echo default1 > default1 + $ hg add default1 + $ hg ci -m default1 + $ hg log -G + @ 4 7f2454f6b04f default1 tip + | + o 3 ed776db7ed63 oedipus_merge + |\ + | o 2 0dedbcd995b6 foo2 + | | + | o 1 47da0f2c25e2 foo1 + |/ + o 0 d20a80d4def3 base + + $ hg showsort --rev '.' + d20a80d4def3 + 47da0f2c25e2 + 0dedbcd995b6 + ed776db7ed63 + 7f2454f6b04f diff -r efda653c96a7 -r 94432e742a02 tests/test-topic-dest.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-topic-dest.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,492 @@ + $ . "$TESTDIR/testlib/topic_setup.sh" + + $ hg init jungle + $ cd jungle + $ cat <> .hg/hgrc + > [extensions] + > rebase= + > histedit= + > [phases] + > publish=false + > EOF + $ cat <> $HGRCPATH + > [ui] + > logtemplate = '{rev} ({topics}) {desc}\n' + > EOF + + $ for x in alpha beta gamma delta ; do + > echo file $x >> $x + > hg add $x + > hg ci -m "c_$x" + > done + +Test NGTip feature +================== + +Simple linear case + + $ echo babar >> jungle + $ hg add jungle + $ hg ci -t elephant -m babar + + $ hg log -G + @ 4 (elephant) babar + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + $ hg log -r 'ngtip(.)' + 3 () c_delta + $ hg log -r 'default' + 3 () c_delta + + +multiple heads with topic + + $ hg up "desc('c_beta')" + 0 files updated, 0 files merged, 3 files removed, 0 files unresolved + $ echo zephir >> jungle + $ hg add jungle + $ hg ci -t monkey -m zephir + $ hg log -G + @ 5 (monkey) zephir + | + | o 4 (elephant) babar + | | + | o 3 () c_delta + | | + | o 2 () c_gamma + |/ + o 1 () c_beta + | + o 0 () c_alpha + + $ hg log -r 'ngtip(.)' + 3 () c_delta + $ hg log -r 'default' + 3 () c_delta + +one of the head is a valid tip + + $ hg up "desc('c_delta')" + 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo epsilon >> epsilon + $ hg add epsilon + $ hg ci -m "c_epsilon" + $ hg log -G + @ 6 () c_epsilon + | + | o 5 (monkey) zephir + | | + +---o 4 (elephant) babar + | | + o | 3 () c_delta + | | + o | 2 () c_gamma + |/ + o 1 () c_beta + | + o 0 () c_alpha + + $ hg log -r 'ngtip(.)' + 6 () c_epsilon + $ hg log -r 'default' + 6 () c_epsilon + +rebase destination +================== + +rebase on branch ngtip + + $ hg up elephant + switching to topic elephant + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg rebase + rebasing 4:cb7ae72f4a80 "babar" + $ hg log -G + @ 7 (elephant) babar + | + o 6 () c_epsilon + | + | o 5 (monkey) zephir + | | + o | 3 () c_delta + | | + o | 2 () c_gamma + |/ + o 1 () c_beta + | + o 0 () c_alpha + + $ hg up monkey + switching to topic monkey + 1 files updated, 0 files merged, 3 files removed, 0 files unresolved + $ hg rebase + rebasing 5:d832ddc604ec "zephir" + $ hg log -G + @ 8 (monkey) zephir + | + | o 7 (elephant) babar + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + +Rebase on other topic heads if any + + $ hg up 'desc(c_delta)' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ echo "General Huc" >> monkeyville + $ hg add monkeyville + $ hg ci -t monkey -m Huc + $ hg log -G + @ 9 (monkey) Huc + | + | o 8 (monkey) zephir + | | + | | o 7 (elephant) babar + | |/ + | o 6 () c_epsilon + |/ + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + $ hg rebase + rebasing 9:d79a104e2902 "Huc" (tip) + $ hg log -G + @ 10 (monkey) Huc + | + o 8 (monkey) zephir + | + | o 7 (elephant) babar + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + +merge destination +================= + + $ hg up 'ngtip(default)' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg up default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo zeta >> zeta + $ hg add zeta + $ hg ci -m "c_zeta" + $ hg log -G + @ 11 () c_zeta + | + | o 10 (monkey) Huc + | | + | o 8 (monkey) zephir + |/ + | o 7 (elephant) babar + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + $ hg up elephant + switching to topic elephant + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg rebase -d 'desc(c_zeta)' # make sure tip is elsewhere + rebasing 7:8d0b77140b05 "babar" + $ hg up monkey + switching to topic monkey + 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg merge + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg topic + elephant + * monkey + $ hg ci -m 'merge with default' + $ hg topic + elephant + * monkey + $ hg log -G + @ 13 (monkey) merge with default + |\ + | | o 12 (elephant) babar + | |/ + | o 11 () c_zeta + | | + o | 10 (monkey) Huc + | | + o | 8 (monkey) zephir + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + + +Check pull --rebase +------------------- + +(we broke it a some point) + + $ cd .. + $ hg clone jungle other --rev '2' + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 3 files + updating to branch default + 3 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd other + $ echo other > other + $ hg add other + $ hg ci -m 'c_other' + $ hg pull -r default --rebase + pulling from $TESTTMP/jungle (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 3 files (+1 heads) + rebasing 3:dbc48dd9e743 "c_other" + $ hg log -G + @ 7 () c_other + | + o 6 () c_zeta + | + o 5 () c_epsilon + | + o 4 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + $ cd ../jungle + + +Default destination for update +=============================== + +initial setup + + $ hg up elephant + switching to topic elephant + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo arthur >> jungle + $ hg ci -m arthur + $ echo pompadour >> jungle + $ hg ci -m pompadour + $ hg up 'roots(all())' + 0 files updated, 0 files merged, 6 files removed, 0 files unresolved + $ hg log -G + o 15 (elephant) pompadour + | + o 14 (elephant) arthur + | + | o 13 (monkey) merge with default + | |\ + o---+ 12 (elephant) babar + / / + | o 11 () c_zeta + | | + o | 10 (monkey) Huc + | | + o | 8 (monkey) zephir + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + @ 0 () c_alpha + + +testing default destination on a branch + + $ hg up + 5 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg log -G + o 15 (elephant) pompadour + | + o 14 (elephant) arthur + | + | o 13 (monkey) merge with default + | |\ + o---+ 12 (elephant) babar + / / + | @ 11 () c_zeta + | | + o | 10 (monkey) Huc + | | + o | 8 (monkey) zephir + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + +extra setup for topic +(making sure tip is not the topic) + + $ hg up 'desc(c_zeta)' + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 'eta' >> 'eta' + $ hg add 'eta' + $ hg commit -m 'c_eta' + $ hg log -G + @ 16 () c_eta + | + | o 15 (elephant) pompadour + | | + | o 14 (elephant) arthur + | | + +---o 13 (monkey) merge with default + | | | + | o | 12 (elephant) babar + |/ / + o | 11 () c_zeta + | | + | o 10 (monkey) Huc + | | + | o 8 (monkey) zephir + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + +Testing default destination for topic + + $ hg up 'roots(topic(elephant))' + switching to topic elephant + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg up + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg log -G + o 16 () c_eta + | + | @ 15 (elephant) pompadour + | | + | o 14 (elephant) arthur + | | + +---o 13 (monkey) merge with default + | | | + | o | 12 (elephant) babar + |/ / + o | 11 () c_zeta + | | + | o 10 (monkey) Huc + | | + | o 8 (monkey) zephir + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + +Testing default destination for topic + + $ hg up 'p1(roots(topic(elephant)))' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic elephant + $ hg up + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg log -G + o 16 () c_eta + | + | @ 15 (elephant) pompadour + | | + | o 14 (elephant) arthur + | | + +---o 13 (monkey) merge with default + | | | + | o | 12 (elephant) babar + |/ / + o | 11 () c_zeta + | | + | o 10 (monkey) Huc + | | + | o 8 (monkey) zephir + |/ + o 6 () c_epsilon + | + o 3 () c_delta + | + o 2 () c_gamma + | + o 1 () c_beta + | + o 0 () c_alpha + + +Default destination for histedit +================================ + +By default histedit should edit with the current topic only +(even when based on other draft + + $ hg phase 'desc(c_zeta)' + 11: draft + $ HGEDITOR=cat hg histedit | grep pick + pick e44744d9ad73 12 babar + pick 38eea8439aee 14 arthur + pick 411315c48bdc 15 pompadour + # p, pick = use commit diff -r efda653c96a7 -r 94432e742a02 tests/test-topic-push.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-topic-push.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,414 @@ + $ . "$TESTDIR/testlib/topic_setup.sh" + + $ cat << EOF >> $HGRCPATH + > [ui] + > logtemplate = {rev} {branch} {get(namespaces, "topics")} {phase} {desc|firstline}\n + > [ui] + > ssh =python "$RUNTESTDIR/dummyssh" + > EOF + + $ hg init main + $ hg init draft + $ cat << EOF >> draft/.hg/hgrc + > [phases] + > publish=False + > EOF + $ hg clone main client + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat << EOF >> client/.hg/hgrc + > [paths] + > draft=../draft + > EOF + + +Testing core behavior to make sure we did not break anything +============================================================ + +Pushing a first changeset + + $ cd client + $ echo aaa > aaa + $ hg add aaa + $ hg commit -m 'CA' + $ hg outgoing -G + comparing with $TESTTMP/main (glob) + searching for changes + @ 0 default draft CA + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + +Pushing two heads + + $ echo aaa > bbb + $ hg add bbb + $ hg commit -m 'CB' + $ echo aaa > ccc + $ hg up 'desc(CA)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg add ccc + $ hg commit -m 'CC' + created new head + $ hg outgoing -G + comparing with $TESTTMP/main (glob) + searching for changes + @ 2 default draft CC + + o 1 default draft CB + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + abort: push creates new remote head 9fe81b7f425d! + (merge or see 'hg help push' for details about pushing new heads) + [255] + $ hg outgoing -r 'desc(CB)' -G + comparing with $TESTTMP/main (glob) + searching for changes + o 1 default draft CB + + $ hg push -r 'desc(CB)' + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + +Pushing a new branch + + $ hg branch mountain + marked working directory as branch mountain + (branches are permanent and global, did you want a bookmark?) + $ hg commit --amend + $ hg outgoing -G + comparing with $TESTTMP/main (glob) + searching for changes + @ 4 mountain draft CC + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + abort: push creates new remote branches: mountain! + (use 'hg push --new-branch' to create new remote branches) + [255] + $ hg push --new-branch + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + 2 new obsolescence markers + +Including on non-publishing + + $ hg push --new-branch draft + pushing to $TESTTMP/draft (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 3 files (+1 heads) + 2 new obsolescence markers + +Testing topic behavior +====================== + +Local peer tests +---------------- + + $ hg up -r 'desc(CA)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic babar + $ echo aaa > ddd + $ hg add ddd + $ hg commit -m 'CD' + $ hg log -G # keep track of phase because I saw some strange bug during developement + @ 5 default babar draft CD + | + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a non publishing server should not be seen as a new head + + $ hg push draft + pushing to $TESTTMP/draft (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + $ hg log -G + @ 5 default babar draft CD + | + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a publishing server should be seen as a new head + + $ hg push + pushing to $TESTTMP/main (glob) + searching for changes + abort: push creates new remote head 67f579af159d! + (merge or see 'hg help push' for details about pushing new heads) + [255] + $ hg log -G + @ 5 default babar draft CD + | + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +wireprotocol tests +------------------ + + $ hg up -r 'desc(CA)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic celeste + $ echo aaa > eee + $ hg add eee + $ hg commit -m 'CE' + $ hg log -G # keep track of phase because I saw some strange bug during developement + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a non publishing server without topic -> new head + + $ cat << EOF >> ../draft/.hg/hgrc + > [extensions] + > topic=! + > EOF + $ hg push ssh://user@dummy/draft + pushing to ssh://user@dummy/draft + searching for changes + abort: push creates new remote head 84eaf32db6c3! + (merge or see 'hg help push' for details about pushing new heads) + [255] + $ hg log -G + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a non publishing server should not be seen as a new head + + $ printf "topic=" >> ../draft/.hg/hgrc + $ hg config extensions.topic >> ../draft/.hg/hgrc + $ hg push ssh://user@dummy/draft + pushing to ssh://user@dummy/draft + searching for changes + remote: adding changesets + remote: adding manifests + remote: adding file changes + remote: added 1 changesets with 1 changes to 1 files (+1 heads) + $ hg log -G + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Pushing a new topic to a publishing server should be seen as a new head + + $ hg push ssh://user@dummy/main + pushing to ssh://user@dummy/main + searching for changes + abort: push creates new remote head 67f579af159d! + (merge or see 'hg help push' for details about pushing new heads) + [255] + $ hg log -G + @ 6 default celeste draft CE + | + | o 5 default babar draft CD + |/ + | o 4 mountain public CC + |/ + | o 1 default public CB + |/ + o 0 default public CA + + +Check that we reject multiple head on the same topic +---------------------------------------------------- + + $ hg up 'desc(CB)' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic babar + $ echo aaa > fff + $ hg add fff + $ hg commit -m 'CF' + $ hg log -G + @ 7 default babar draft CF + | + | o 6 default celeste draft CE + | | + | | o 5 default babar draft CD + | |/ + | | o 4 mountain public CC + | |/ + o | 1 default public CB + |/ + o 0 default public CA + + + $ hg push draft + pushing to $TESTTMP/draft (glob) + searching for changes + abort: push creates new remote head f0bc62a661be on branch 'default:babar'! + (merge or see 'hg help push' for details about pushing new heads) + [255] + +Multiple head on a branch merged in a topic changesets +------------------------------------------------------------------------ + + + $ hg up 'desc(CA)' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ echo aaa > ggg + $ hg add ggg + $ hg commit -m 'CG' + created new head + $ hg up 'desc(CF)' + switching to topic babar + 2 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg merge 'desc(CG)' + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg commit -m 'CM' + $ hg log -G + @ 9 default babar draft CM + |\ + | o 8 default draft CG + | | + o | 7 default babar draft CF + | | + | | o 6 default celeste draft CE + | |/ + | | o 5 default babar draft CD + | |/ + | | o 4 mountain public CC + | |/ + o | 1 default public CB + |/ + o 0 default public CA + + +Reject when pushing to draft + + $ hg push draft -r . + pushing to $TESTTMP/draft (glob) + searching for changes + abort: push creates new remote head 4937c4cad39e! + (merge or see 'hg help push' for details about pushing new heads) + [255] + + +Reject when pushing to publishing + + $ hg push -r . + pushing to $TESTTMP/main (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 2 changes to 2 files + + $ cd .. + +Test phase move +================================== + +setup, two repo knowns about two small topic branch + + $ hg init repoA + $ hg clone repoA repoB + updating to branch default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat << EOF >> repoA/.hg/hgrc + > [phases] + > publish=False + > EOF + $ cat << EOF >> repoB/.hg/hgrc + > [phases] + > publish=False + > EOF + $ cd repoA + $ echo aaa > base + $ hg add base + $ hg commit -m 'CBASE' + $ echo aaa > aaa + $ hg add aaa + $ hg topic topicA + $ hg commit -m 'CA' + $ hg up 'desc(CBASE)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo aaa > bbb + $ hg add bbb + $ hg topic topicB + $ hg commit -m 'CB' + $ cd .. + $ hg push -R repoA repoB + pushing to repoB + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 3 files (+1 heads) + $ hg log -G -R repoA + @ 2 default topicB draft CB + | + | o 1 default topicA draft CA + |/ + o 0 default draft CBASE + + +We turn different topic to public on each side, + + $ hg -R repoA phase --public topicA + $ hg -R repoB phase --public topicB + +Pushing should complain because it create to heads on default + + $ hg push -R repoA repoB + pushing to repoB + searching for changes + no changes found + abort: push create a new head on branch "default" + [255] diff -r efda653c96a7 -r 94432e742a02 tests/test-topic-stack-data.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-topic-stack-data.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,270 @@ +Setup +===== + + $ . "$TESTDIR/testlib/topic_setup.sh" + + $ hg init test-list + $ cd test-list + $ cat <> .hg/hgrc + > [phases] + > publish=false + > EOF + $ cat <> $HGRCPATH + > [experimental] + > # disable the new graph style until we drop 3.7 support + > graphstyle.missing = | + > # turn evolution on + > evolution=all + > EOF + + + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + +Build some basic graph +---------------------- + + $ for x in base_a base_b base_c base_d base_e ; do + > mkcommit $x + > done + +Add another branch with two heads + + $ hg up 'desc(base_a)' + 0 files updated, 0 files merged, 4 files removed, 0 files unresolved + $ hg branch lake + marked working directory as branch lake + (branches are permanent and global, did you want a bookmark?) + $ mkcommit lake_a + $ mkcommit lake_b + $ hg up 'desc(lake_a)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit lake_c + created new head + + +Add some topics +--------------- + +A simple topic that need rebasing + + $ hg up 'desc(base_c)' + 2 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg topic baz + $ mkcommit baz_a + $ mkcommit baz_b + +A simple topic with unstability + + $ hg up 'desc(base_d)' + 1 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg topic fuz + $ mkcommit fuz_a + $ mkcommit fuz_b + $ mkcommit fuz_c + $ hg up 'desc(fuz_a)' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg commit --amend --message 'fuz1_a' + +A topic with multiple heads + + $ hg up 'desc(base_e)' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg topic bar + $ mkcommit bar_a + $ mkcommit bar_b + $ mkcommit bar_c + $ hg up 'desc(bar_b)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit bar_d + $ mkcommit bar_e + $ hg up 'desc(bar_d)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg commit --amend --message 'bar1_d' + +topic 'foo' on the multi headed branch + + $ hg up 'desc(lake_a)' + 1 files updated, 0 files merged, 7 files removed, 0 files unresolved + $ hg topic foo + $ mkcommit foo_a + $ mkcommit foo_b + +Summary +------- + + $ hg summary + parent: 21:3e54b49a3113 tip + add foo_b + branch: lake + commit: (clean) + update: 2 new changesets (update) + phases: 22 draft + unstable: 3 changesets + topic: foo + $ hg log --graph -T '{desc} ({branch}) [{topic}]' + @ add foo_b (lake) [] + | + o add foo_a (lake) [] + | + | o bar1_d (default) [] + | | + | | o add bar_e (default) [] + | | | + | | x add bar_d (default) [] + | |/ + | | o add bar_c (default) [] + | |/ + | o add bar_b (default) [] + | | + | o add bar_a (default) [] + | | + | | o fuz1_a (default) [] + | | | + | | | o add fuz_c (default) [] + | | | | + | | | o add fuz_b (default) [] + | | | | + | | | x add fuz_a (default) [] + | | |/ + | | | o add baz_b (default) [] + | | | | + | | | o add baz_a (default) [] + | | | | + +-------o add lake_c (lake) [] + | | | | + +-------o add lake_b (lake) [] + | | | | + o | | | add lake_a (lake) [] + | | | | + | o | | add base_e (default) [] + | |/ / + | o / add base_d (default) [] + | |/ + | o add base_c (default) [] + | | + | o add base_b (default) [] + |/ + o add base_a (default) [] + + +Actual Testing +============== + +basic output + + $ hg topic + bar + baz + * foo + fuz + +quiet version + + $ hg topic --quiet + bar + baz + foo + fuz + +verbose + + $ hg topic --verbose + bar (on branch: default, 5 changesets, 1 troubled, 2 heads) + baz (on branch: default, 2 changesets, 2 behind) + * foo (on branch: lake, 2 changesets, ambiguous destination) + fuz (on branch: default, 3 changesets, 2 troubled, 1 behind) + +json + + $ hg topic -T json + [ + { + "active": false, + "topic": "bar" + }, + { + "active": false, + "topic": "baz" + }, + { + "active": true, + "topic": "foo" + }, + { + "active": false, + "topic": "fuz" + } + ] + +json --verbose + + $ hg topic -T json --verbose + [ + { + "active": false, + "branches+": "default", + "changesetcount": 5, + "headcount": 2, + "topic": "bar", + "troubledcount": 1 + }, + { + "active": false, + "behindcount": 2, + "branches+": "default", + "changesetcount": 2, + "topic": "baz" + }, + { + "active": true, + "behinderror": "ambiguous destination", + "branches+": "lake", + "changesetcount": 2, + "topic": "foo" + }, + { + "active": false, + "behindcount": 1, + "branches+": "default", + "changesetcount": 3, + "topic": "fuz", + "troubledcount": 2 + } + ] + +Also test this situation with 'hg stack' +======================================= + + $ hg stack bar + ### topic: bar (2 heads) + ### branch: default + t5: add bar_c + t2^ add bar_b (base) + t4$ add bar_e (unstable) + t3: bar1_d + t2: add bar_b + t1: add bar_a + ^ add base_e + $ hg stack baz + ### topic: baz + ### branch: default, 2 behind + t2: add baz_b + t1: add baz_a + ^ add base_c + $ hg stack foo + ### topic: foo + ### branch: lake, ambigious rebase destination + t2@ add foo_b (current) + t1: add foo_a + ^ add lake_a + $ hg stack fuz + ### topic: fuz + ### branch: default, 1 behind + t3$ add fuz_c (unstable) + t2$ add fuz_b (unstable) + t1: fuz1_a + ^ add base_d diff -r efda653c96a7 -r 94432e742a02 tests/test-topic-stack.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-topic-stack.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,254 @@ + $ . "$TESTDIR/testlib/topic_setup.sh" + +Initial setup + + + $ cat << EOF >> $HGRCPATH + > [ui] + > logtemplate = {rev} {branch} \{{get(namespaces, "topics")}} {phase} {desc|firstline}\n + > [experimental] + > evolution=createmarkers,exchange,allowunstable + > EOF + + $ hg init main + $ cd main + $ hg topic other + $ echo aaa > aaa + $ hg add aaa + $ hg commit -m c_a + $ echo aaa > bbb + $ hg add bbb + $ hg commit -m c_b + $ hg topic foo + $ echo aaa > ccc + $ hg add ccc + $ hg commit -m c_c + $ echo aaa > ddd + $ hg add ddd + $ hg commit -m c_d + $ echo aaa > eee + $ hg add eee + $ hg commit -m c_e + $ echo aaa > fff + $ hg add fff + $ hg commit -m c_f + $ hg log -G + @ 5 default {foo} draft c_f + | + o 4 default {foo} draft c_e + | + o 3 default {foo} draft c_d + | + o 2 default {foo} draft c_c + | + o 1 default {other} draft c_b + | + o 0 default {other} draft c_a + + +Check that topic without any parent does not crash --list +--------------------------------------------------------- + + $ hg up other + switching to topic other + 0 files updated, 0 files merged, 4 files removed, 0 files unresolved + $ hg topic --list + ### topic: other + ### branch: default + t2@ c_b (current) + t1: c_a + $ hg phase --public 'topic("other")' + $ hg up foo + switching to topic foo + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Simple test +----------- + +'hg stack' list all changeset in the topic + + $ hg topic + * foo + $ hg stack + ### topic: foo + ### branch: default + t4@ c_f (current) + t3: c_e + t2: c_d + t1: c_c + ^ c_b + +error case, nothing to list + + $ hg topic --clear + $ hg stack + abort: no active topic to list + [255] + +Test "t#" reference +------------------- + + + $ hg up t2 + abort: cannot resolve "t2": no active topic + [255] + $ hg topic foo + $ hg up t42 + abort: cannot resolve "t42": topic "foo" has only 4 changesets + [255] + $ hg up t2 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg summary + parent: 3:e629654d7050 + c_d + branch: default + commit: (clean) + update: (current) + phases: 4 draft + topic: foo + +Case with some of the topic unstable +------------------------------------ + + $ echo bbb > ddd + $ hg commit --amend + $ hg log -G + @ 7 default {foo} draft c_d + | + | o 5 default {foo} draft c_f + | | + | o 4 default {foo} draft c_e + | | + | x 3 default {foo} draft c_d + |/ + o 2 default {foo} draft c_c + | + o 1 default {} public c_b + | + o 0 default {} public c_a + + $ hg topic --list + ### topic: foo + ### branch: default + t4$ c_f (unstable) + t3$ c_e (unstable) + t2@ c_d (current) + t1: c_c + ^ c_b + +Also test the revset: + + $ hg log -r 'stack()' + 2 default {foo} draft c_c + 7 default {foo} draft c_d + 4 default {foo} draft c_e + 5 default {foo} draft c_f + +Case with multiple heads on the topic +------------------------------------- + +Make things linear again + + $ hg rebase -s 'desc(c_e)' -d 'desc(c_d) - obsolete()' + rebasing 4:0f9ac936c87d "c_e" + rebasing 5:6559e6d93aea "c_f" + $ hg log -G + o 9 default {foo} draft c_f + | + o 8 default {foo} draft c_e + | + @ 7 default {foo} draft c_d + | + o 2 default {foo} draft c_c + | + o 1 default {} public c_b + | + o 0 default {} public c_a + + + +Create the second branch + + $ hg up 'desc(c_d)' + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo aaa > ggg + $ hg add ggg + $ hg commit -m c_g + $ echo aaa > hhh + $ hg add hhh + $ hg commit -m c_h + $ hg log -G + @ 11 default {foo} draft c_h + | + o 10 default {foo} draft c_g + | + | o 9 default {foo} draft c_f + | | + | o 8 default {foo} draft c_e + |/ + o 7 default {foo} draft c_d + | + o 2 default {foo} draft c_c + | + o 1 default {} public c_b + | + o 0 default {} public c_a + + +Test output + + $ hg top -l + ### topic: foo (2 heads) + ### branch: default + t6: c_f + t5: c_e + t2^ c_d (base) + t4@ c_h (current) + t3: c_g + t2: c_d + t1: c_c + ^ c_b + +Case with multiple heads on the topic with unstability involved +--------------------------------------------------------------- + +We amend the message to make sure the display base pick the right changeset + + $ hg up 'desc(c_d)' + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ echo ccc > ddd + $ hg commit --amend -m 'c_D' + $ hg rebase -d . -s 'desc(c_g)' + rebasing 10:81264ae8a36a "c_g" + rebasing 11:fde5f5941642 "c_h" + $ hg log -G + o 15 default {foo} draft c_h + | + o 14 default {foo} draft c_g + | + @ 13 default {foo} draft c_D + | + | o 9 default {foo} draft c_f + | | + | o 8 default {foo} draft c_e + | | + | x 7 default {foo} draft c_d + |/ + o 2 default {foo} draft c_c + | + o 1 default {} public c_b + | + o 0 default {} public c_a + + + $ hg topic --list + ### topic: foo (2 heads) + ### branch: default + t6$ c_f (unstable) + t5$ c_e (unstable) + t2^ c_D (base) + t4: c_h + t3: c_g + t2@ c_D (current) + t1: c_c + ^ c_b diff -r efda653c96a7 -r 94432e742a02 tests/test-topic-tutorial.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-topic-tutorial.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,468 @@ +============== +Topic Tutorial +============== + +.. This test file is also supposed to be able to compile as a rest file. + + +.. Some Setup:: + + $ . "$TESTDIR/testlib/topic_setup.sh" + $ hg init server + $ cd server + $ cat >> .hg/hgrc << EOF + > [ui] + > user= Shopping Master + > EOF + $ cat >> shopping << EOF + > Spam + > Whizzo butter + > Albatross + > Rat (rather a lot) + > Jugged fish + > Blancmange + > Salmon mousse + > EOF + $ hg commit -A -m "Shopping list" + adding shopping + $ cd .. + $ hg clone server client + updating to branch default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd client + $ cat >> .hg/hgrc << EOF + > [ui] + > user= Tutorial User + > EOF + +Topic branches are lightweight branches which disappear when changes are +finalized (move to the public phase). They can help users to organise and share +their unfinished work. + +Topic Basics +============ + +Let's says we use Mercurial to manage our shopping list:: + + $ hg log --graph + @ changeset: 0:38da43f0a2ea + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Shopping list + + +We are about to do some edition to this list and would like to do them within +a topic. Creating a new topic is done using the ``topic`` command:: + + $ hg topic food + +As for named branch, our topic is active but it does not contains any changesets yet:: + + $ hg topic + * food + $ hg summary + parent: 0:38da43f0a2ea tip + Shopping list + branch: default + commit: (clean) + update: (current) + topic: food + $ hg log --graph + @ changeset: 0:38da43f0a2ea + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Shopping list + + +Our next commit will be part of the active topic:: + + $ cat >> shopping << EOF + > Egg + > Suggar + > Vinegar + > Oil + > EOF + $ hg commit -m "adding condiments" + $ hg log --graph --rev 'topic("food")' + @ changeset: 1:13900241408b + | tag: tip + ~ topic: food + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: adding condiments + + +And future commit will be part of that topic too:: + + $ cat >> shopping << EOF + > Bananas + > Pear + > Apple + > EOF + $ hg commit -m "adding fruits" + $ hg log --graph --rev 'topic("food")' + @ changeset: 2:287de11b401f + | tag: tip + | topic: food + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding fruits + | + o changeset: 1:13900241408b + | topic: food + ~ user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: adding condiments + + +We can get a compact view of the content of our topic using the ``stack`` command:: + + $ hg stack + ### topic: food + ### branch: default + t2@ adding fruits (current) + t1: adding condiments + ^ Shopping list + +The topic desactivate when we update away from it:: + + $ hg up default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg topic + food + +Note that ``default`` (name of the branch) now refers to the tipmost changeset of default without a topic:: + + $ hg log --graph + o changeset: 2:287de11b401f + | tag: tip + | topic: food + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding fruits + | + o changeset: 1:13900241408b + | topic: food + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding condiments + | + @ changeset: 0:38da43f0a2ea + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Shopping list + + +And updating back to the topic reactivate it:: + + $ hg up food + switching to topic food + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg topic + * food + +The name used for updating does not affect the activation of the topic, updating to a revision part of a topic will activate it in all cases:: + + $ hg up default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg up --rev 'desc("condiments")' + switching to topic food + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg topic + * food + +.. server side activity:: + + $ cd ../server/ + $ cat > shopping << EOF + > T-Shirt + > Trousers + > Spam + > Whizzo butter + > Albatross + > Rat (rather a lot) + > Jugged fish + > Blancmange + > Salmon mousse + > EOF + $ hg commit -A -m "Adding clothes" + $ cd ../client + +Topic will also affect rebase and merge destination. Let's pull the latest update from the main server:: + + $ hg pull + pulling from $TESTTMP/server (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + (run 'hg heads' to see heads) + $ hg log -G + o changeset: 3:6104862e8b84 + | tag: tip + | parent: 0:38da43f0a2ea + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Adding clothes + | + | o changeset: 2:287de11b401f + | | topic: food + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: adding fruits + | | + | @ changeset: 1:13900241408b + |/ topic: food + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding condiments + | + o changeset: 0:38da43f0a2ea + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Shopping list + + +The topic head will not be considered when merge from the new head of the branch:: + + $ hg up default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg merge + abort: branch 'default' has one head - please merge with an explicit rev + (run 'hg heads' to see all heads) + [255] + +But the topic will see that branch head as a valid destination:: + + $ hg up food + switching to topic food + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg rebase + rebasing 1:13900241408b "adding condiments" + merging shopping + rebasing 2:287de11b401f "adding fruits" + merging shopping + $ hg log --graph + @ changeset: 5:2d50db8b5b4c + | tag: tip + | topic: food + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding fruits + | + o changeset: 4:4011b46eeb33 + | topic: food + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding condiments + | + o changeset: 3:6104862e8b84 + | parent: 0:38da43f0a2ea + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Adding clothes + | + o changeset: 0:38da43f0a2ea + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Shopping list + + +The topic information will fade out when we publish the changesets:: + + $ hg topic + food + $ hg push + pushing to $TESTTMP/server (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 1 files + 2 new obsolescence markers + $ hg topic + $ hg log --graph + @ changeset: 5:2d50db8b5b4c + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding fruits + | + o changeset: 4:4011b46eeb33 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: adding condiments + | + o changeset: 3:6104862e8b84 + | parent: 0:38da43f0a2ea + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Adding clothes + | + o changeset: 0:38da43f0a2ea + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Shopping list + + $ hg up default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Working with Multiple Topics +============================ + +In the above example, topic are not bring much benefit since you only have one +line of developement. Topic start to be more useful when you have to work on +multiple features are the same time. + +We might go shopping in a hardware store in the same go, so let's add some +tools to the shopping list withing a new topic:: + + $ hg topic tools + $ echo hammer >> shopping + $ hg ci -m 'Adding hammer' + $ echo saw >> shopping + $ hg ci -m 'Adding saw' + $ echo drill >> shopping + $ hg ci -m 'Adding drill' + +But are not sure to actually go in the hardward store, so in the meantime, we +want to extend the list with drinks. We go back to the official default branch +and start a new topic:: + + $ hg up default + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg topic drinks + $ echo 'apple juice' >> shopping + $ hg ci -m 'Adding apple juice' + $ echo 'orange juice' >> shopping + $ hg ci -m 'Adding orange juice' + +We now have two topics:: + + $ hg topic + * drinks + tools + +The information ``hg stack`` command adapt to the active topic:: + + $ hg stack + ### topic: drinks + ### branch: default + t2@ Adding orange juice (current) + t1: Adding apple juice + ^ adding fruits + $ hg up tools + switching to topic tools + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg stack + ### topic: tools + ### branch: default + t3@ Adding drill (current) + t2: Adding saw + t1: Adding hammer + ^ adding fruits + +They are seen as independant branch by Mercurial. No rebase or merge betwen them will be attempted by default:: + + $ hg rebase + nothing to rebase + [1] + +.. server activity:: + + $ cd ../server + $ hg up + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ mv shopping foo + $ echo 'Coat' > shopping + $ cat foo >> shopping + $ hg ci -m 'add a coat' + $ echo 'Coat' > shopping + $ echo 'Shoes' >> shopping + $ cat foo >> shopping + $ hg rm foo + not removing foo: file is untracked + [1] + $ hg ci -m 'add a pair of shoes' + $ cd ../client + +Lets see what other people did in the mean time:: + + $ hg pull + pulling from $TESTTMP/server (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 1 files (+1 heads) + (run 'hg heads' to see heads) + +There is new changes! We can simply use ``hg rebase`` to update our changeset on top of the latest:: + + $ hg rebase + rebasing 6:183984ef46d1 "Adding hammer" + merging shopping + rebasing 7:cffff85af537 "Adding saw" + merging shopping + rebasing 8:34255b455dac "Adding drill" + merging shopping + +But what about the other topic? You can use 'hg topic --verbose' to see information about them:: + + $ hg topic --verbose + drinks (on branch: default, 2 changesets, 2 behind) + tools (on branch: default, 3 changesets) + +The "2 behind" is telling you that there is 2 new changesets on the named branch of the topic. You need to merge or rebase to incorporate them. + +Pushing that topic would create a new heads will be prevented:: + + $ hg push --rev drinks + pushing to $TESTTMP/server (glob) + searching for changes + abort: push creates new remote head 70dfa201ed73! + (merge or see 'hg help push' for details about pushing new heads) + [255] + + +Even after a rebase Pushing all active topics at the same time will complains about the multiple heads it would create on that branch:: + + $ hg rebase -b drinks + rebasing 9:8dfa45bd5e0c "Adding apple juice" + merging shopping + rebasing 10:70dfa201ed73 "Adding orange juice" + merging shopping + switching to topic tools + $ hg push + pushing to $TESTTMP/server (glob) + searching for changes + abort: push creates new remote head 4cd7c1591a67! + (merge or see 'hg help push' for details about pushing new heads) + [255] + +Publishing only one of them is allowed (as long as it does not create a new branch head has we just saw in the previous case):: + + $ hg push -r drinks + pushing to $TESTTMP/server (glob) + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 1 files + 2 new obsolescence markers + +The publishing topic has now vanished, and the one still draft is now marked as "behind":: + + $ hg topic --verbose + * tools (on branch: default, 3 changesets, 2 behind) + $ hg stack + ### topic: tools + ### branch: default, 2 behind + t3@ Adding drill (current) + t2: Adding saw + t1: Adding hammer + ^ add a pair of shoes + diff -r efda653c96a7 -r 94432e742a02 tests/test-topic.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-topic.t Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,656 @@ + $ . "$TESTDIR/testlib/topic_setup.sh" + + $ hg init pinky + $ cd pinky + $ cat <> .hg/hgrc + > [phases] + > publish=false + > EOF + $ cat <> $HGRCPATH + > [experimental] + > # disable the new graph style until we drop 3.7 support + > graphstyle.missing = | + > EOF + + $ hg help topics + hg topics [TOPIC] + + View current topic, set current topic, or see all topics. + + The --verbose version of this command display various information on the + state of each topic. + + options: + + --clear clear active topic if any + --change VALUE revset of existing revisions to change topic + -l --list show the stack of changeset in the topic + + (some details hidden, use --verbose to show complete help) + $ hg topics + +Test topics interaction with evolution: + + $ hg topics --config experimental.evolution= + $ hg topics --config experimental.evolution= --change . bob + abort: must have obsolete enabled to use --change + [255] + +Create some changes: + + $ for x in alpha beta gamma delta ; do + > echo file $x >> $x + > hg addremove + > hg ci -m "Add file $x" + > done + adding alpha + adding beta + adding gamma + adding delta + +Still no topics + $ hg topics + +Test commit flag and help text + + $ echo stuff >> alpha + $ HGEDITOR=cat hg ci -t topicflag + + + HG: Enter commit message. Lines beginning with 'HG:' are removed. + HG: Leave message empty to abort commit. + HG: -- + HG: user: test + HG: topic 'topicflag' + HG: branch 'default' + HG: changed alpha + abort: empty commit message + [255] + $ hg revert alpha + $ hg topic + * topicflag + +Make a topic + $ hg topic narf + $ hg topics + * narf + $ echo topic work >> alpha + $ hg ci -m 'start on narf' + $ hg co .^ + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg topic fran + $ hg topics + * fran + narf + $ echo >> fran work >> beta + $ hg ci -m 'start on fran' + $ hg co narf + switching to topic narf + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg topic + fran + * narf + $ hg log -r . -T '{topics}\n' + narf + $ echo 'narf!!!' >> alpha + $ hg ci -m 'narf!' + $ hg log -G + @ changeset: 6:7c34953036d6 + | tag: tip + | topic: narf + | parent: 4:fb147b0b417c + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: narf! + | + | o changeset: 5:0469d521db49 + | | topic: fran + | | parent: 3:a53952faf762 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: start on fran + | | + o | changeset: 4:fb147b0b417c + |/ topic: narf + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on narf + | + o changeset: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file delta + | + o changeset: 2:15d1eb11d2fa + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file gamma + | + o changeset: 1:c692ea2c9224 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file beta + | + o changeset: 0:c2b7d2f7d14b + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Add file alpha + + +Exchanging of topics: + $ cd .. + $ hg init brain + $ hg -R pinky push -r 4 brain + pushing to brain + searching for changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 5 changes to 4 files + +Export + + $ hg -R pinky export + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 7c34953036d6a36eae468c550d0592b89ee8bffc + # Parent fb147b0b417c25ca15547cd945acf51cf8dcaf02 + # EXP-Topic narf + narf! + + diff -r fb147b0b417c -r 7c34953036d6 alpha + --- a/alpha Thu Jan 01 00:00:00 1970 +0000 + +++ b/alpha Thu Jan 01 00:00:00 1970 +0000 + @@ -1,2 +1,3 @@ + file alpha + topic work + +narf!!! + +Import + + $ hg -R pinky export > narf.diff + $ hg -R pinky --config extensions.strip= strip . + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/pinky/.hg/strip-backup/7c34953036d6-1ff3bae2-backup.hg (glob) + $ hg -R pinky import narf.diff + applying narf.diff + $ hg -R pinky log -r . + changeset: 6:7c34953036d6 + tag: tip + topic: narf + parent: 4:fb147b0b417c + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: narf! + +Now that we've pushed to brain, the work done on narf is no longer a +draft, so we won't see that topic name anymore: + + $ hg log -R pinky -G + @ changeset: 6:7c34953036d6 + | tag: tip + | topic: narf + | parent: 4:fb147b0b417c + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: narf! + | + | o changeset: 5:0469d521db49 + | | topic: fran + | | parent: 3:a53952faf762 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: start on fran + | | + o | changeset: 4:fb147b0b417c + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on narf + | + o changeset: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file delta + | + o changeset: 2:15d1eb11d2fa + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file gamma + | + o changeset: 1:c692ea2c9224 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file beta + | + o changeset: 0:c2b7d2f7d14b + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Add file alpha + + $ cd brain + $ hg co tip + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Because the change is public, we won't inherit the topic from narf. + + $ hg topic + $ echo what >> alpha + $ hg topic query + $ hg ci -m 'what is narf, pinky?' + $ hg log -Gl2 + @ changeset: 5:c01515cfc331 + | tag: tip + | topic: query + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: what is narf, pinky? + | + o changeset: 4:fb147b0b417c + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on narf + | + $ hg push -f ../pinky -r query + pushing to ../pinky + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + $ hg -R ../pinky log -Gl 4 + o changeset: 7:c01515cfc331 + | tag: tip + | topic: query + | parent: 4:fb147b0b417c + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: what is narf, pinky? + | + | @ changeset: 6:7c34953036d6 + |/ topic: narf + | parent: 4:fb147b0b417c + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: narf! + | + | o changeset: 5:0469d521db49 + | | topic: fran + | | parent: 3:a53952faf762 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: start on fran + | | + o | changeset: 4:fb147b0b417c + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on narf + | + $ hg topics + * query + $ cd ../pinky + $ hg co query + switching to topic query + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo answer >> alpha + $ hg ci -m 'Narf is like `zort` or `poit`!' + $ hg merge narf + merging alpha + warning: conflicts while merging alpha! (edit, then use 'hg resolve --mark') + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon + [1] + $ hg revert -r narf alpha + $ hg resolve -m alpha + (no more unresolved files) + $ hg topic narf + $ hg ci -m 'Finish narf' + $ hg topics + fran + * narf + query + $ hg debugnamecomplete # branch:topic here is a buggy side effect + default + default:fran + default:narf + default:query + fran + narf + query + tip + $ hg phase --public narf + +POSSIBLE BUG: narf topic stays alive even though we just made all +narf commits public: + + $ hg topics + fran + * narf + $ hg log -Gl 6 + @ changeset: 9:ae074045b7a7 + |\ tag: tip + | | parent: 8:54c943c1c167 + | | parent: 6:7c34953036d6 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: Finish narf + | | + | o changeset: 8:54c943c1c167 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: Narf is like `zort` or `poit`! + | | + | o changeset: 7:c01515cfc331 + | | parent: 4:fb147b0b417c + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: what is narf, pinky? + | | + o | changeset: 6:7c34953036d6 + |/ parent: 4:fb147b0b417c + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: narf! + | + | o changeset: 5:0469d521db49 + | | topic: fran + | | parent: 3:a53952faf762 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: start on fran + | | + o | changeset: 4:fb147b0b417c + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on narf + | + $ cd ../brain + $ hg topics + * query + $ hg pull ../pinky -r narf + pulling from ../pinky + abort: unknown revision 'narf'! + [255] + $ hg pull ../pinky -r default + pulling from ../pinky + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 1 files + (run 'hg update' to get a working copy) + $ hg topics + * query + +We can pull in the draft-phase change and we get the new topic + + $ hg pull ../pinky + pulling from ../pinky + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + (run 'hg heads' to see heads) + $ hg topics + fran + * query + $ hg log -Gr 'draft()' + o changeset: 9:0469d521db49 + | tag: tip + | topic: fran + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + +query is not an open topic, so when we clear the current topic it'll +disappear: + + $ hg topics --clear + $ hg topics + fran + +--clear when we don't have an active topic isn't an error: + + $ hg topics --clear + +Topic revset + $ hg log -r 'topic()' -G + o changeset: 9:0469d521db49 + | tag: tip + | topic: fran + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + $ hg log -r 'not topic()' -G + o changeset: 8:ae074045b7a7 + |\ parent: 7:54c943c1c167 + | | parent: 6:7c34953036d6 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: Finish narf + | | + | o changeset: 7:54c943c1c167 + | | parent: 5:c01515cfc331 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: Narf is like `zort` or `poit`! + | | + o | changeset: 6:7c34953036d6 + | | parent: 4:fb147b0b417c + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: narf! + | | + | @ changeset: 5:c01515cfc331 + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: what is narf, pinky? + | + o changeset: 4:fb147b0b417c + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on narf + | + o changeset: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file delta + | + o changeset: 2:15d1eb11d2fa + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file gamma + | + o changeset: 1:c692ea2c9224 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: Add file beta + | + o changeset: 0:c2b7d2f7d14b + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: Add file alpha + +No matches because narf is already closed: + $ hg log -r 'topic(narf)' -G +This regexp should match the topic `fran`: + $ hg log -r 'topic("re:.ra.")' -G + o changeset: 9:0469d521db49 + | tag: tip + | topic: fran + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | +Exact match on fran: + $ hg log -r 'topic(fran)' -G + o changeset: 9:0469d521db49 + | tag: tip + | topic: fran + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + +Match current topic: + $ hg topic + fran + $ hg log -r 'topic(.)' +(no output is expected) + $ hg co fran + switching to topic fran + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg log -r 'topic(.)' + changeset: 9:0469d521db49 + tag: tip + topic: fran + parent: 3:a53952faf762 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: start on fran + + +Deactivate the topic. + $ hg topics + * fran + $ hg topics --clear + $ echo fran? >> beta + $ hg ci -m 'fran?' + created new head + $ hg log -Gr 'draft()' + @ changeset: 10:4073470c35e1 + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: fran? + | + o changeset: 9:0469d521db49 + | topic: fran + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + $ hg topics + fran +Changing topic fails if we don't give a topic + $ hg topic --change 9 + abort: changing topic requires a topic name or --clear + [255] + +Can't change topic of a public change + $ hg topic --change 1:: --clear + abort: can't change topic of a public change + [255] + +Can clear topics + $ hg topic --change 9 --clear + changed topic on 1 changes + please run hg evolve --rev "not topic()" now + $ hg log -Gr 'draft() and not obsolete()' + o changeset: 11:783930e1d79e + | tag: tip + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + | @ changeset: 10:4073470c35e1 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | trouble: unstable + | | summary: fran? + | | + +Normally you'd do this with evolve, but we'll use rebase to avoid +bonus deps in the testsuite. + + $ hg rebase -d tip -s . + rebasing 10:4073470c35e1 "fran?" + +Can add a topic to an existing change + $ hg topic --change 11 wat + changed topic on 1 changes + please run hg evolve --rev "topic(wat)" now + $ hg log -Gr 'draft() and not obsolete()' + o changeset: 13:d91cd8fd490e + | tag: tip + | topic: wat + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + | @ changeset: 12:d9e32f4c4806 + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | trouble: unstable + | | summary: fran? + | | + +Normally you'd do this with evolve, but we'll use rebase to avoid +bonus deps in the testsuite. + + $ hg rebase -d tip -s . + rebasing 12:d9e32f4c4806 "fran?" + + $ hg log -Gr 'draft()' + @ changeset: 14:cf24ad8bbef5 + | tag: tip + | topic: wat + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: fran? + | + o changeset: 13:d91cd8fd490e + | topic: wat + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + +Amend a topic + + $ hg topic watwat + $ hg ci --amend + $ hg log -Gr 'draft()' + @ changeset: 16:893ffcf66c1f + | tag: tip + | topic: watwat + | parent: 13:d91cd8fd490e + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: fran? + | + o changeset: 13:d91cd8fd490e + | topic: wat + | parent: 3:a53952faf762 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: start on fran + | + +Clear and amend: + + $ hg topic --clear + $ hg ci --amend + $ hg log -r . + changeset: 18:a13639e22b65 + tag: tip + parent: 13:d91cd8fd490e + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: fran? + +Readding the same topic with topic --change should work: + $ hg topic --change . watwat + changed topic on 1 changes diff -r efda653c96a7 -r 94432e742a02 tests/test-touch.t --- a/tests/test-touch.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-touch.t Fri Mar 31 15:33:59 2017 +0200 @@ -7,7 +7,7 @@ > [extensions] > hgext.rebase= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ hg init repo $ cd repo diff -r efda653c96a7 -r 94432e742a02 tests/test-tutorial.t --- a/tests/test-tutorial.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-tutorial.t Fri Mar 31 15:33:59 2017 +0200 @@ -59,7 +59,7 @@ $ cat >> $HGRCPATH < [extensions] - > evolve = $TESTDIR/../hgext/evolve.py + > evolve = $TESTDIR/../hgext3rd/evolve/ > # enabling rebase is also needed for now > rebase = > EOF diff -r efda653c96a7 -r 94432e742a02 tests/test-uncommit.t --- a/tests/test-uncommit.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-uncommit.t Fri Mar 31 15:33:59 2017 +0200 @@ -2,7 +2,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ glog() { > hg glog --template '{rev}:{node|short}@{branch}({separate("/", obsolete, phase)}) {desc|firstline}\n' "$@" diff -r efda653c96a7 -r 94432e742a02 tests/test-unstable.t --- a/tests/test-unstable.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-unstable.t Fri Mar 31 15:33:59 2017 +0200 @@ -15,7 +15,7 @@ > [extensions] > hgext.graphlog= > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" > hg add "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-userguide.t --- a/tests/test-userguide.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-userguide.t Fri Mar 31 15:33:59 2017 +0200 @@ -32,7 +32,7 @@ > [extensions] > rebase = > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH example 3: safe amend with "hg commit --amend" (figure 2) $ echo 'tweak feature Y' >> file1.c diff -r efda653c96a7 -r 94432e742a02 tests/test-wireproto-bundle1.t --- a/tests/test-wireproto-bundle1.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-wireproto-bundle1.t Fri Mar 31 15:33:59 2017 +0200 @@ -8,7 +8,7 @@ > publish = False > [extensions] > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" diff -r efda653c96a7 -r 94432e742a02 tests/test-wireproto.t --- a/tests/test-wireproto.t Tue Feb 28 17:22:21 2017 +0100 +++ b/tests/test-wireproto.t Fri Mar 31 15:33:59 2017 +0200 @@ -11,7 +11,7 @@ > publish = False > [extensions] > EOF - $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext3rd/evolve/" >> $HGRCPATH $ mkcommit() { > echo "$1" > "$1" @@ -30,7 +30,7 @@ $ cp -r client other Smoke testing -=============== +===============.t $ cd client $ mkcommit 0 diff -r efda653c96a7 -r 94432e742a02 tests/testlib/checkheads-util.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/testlib/checkheads-util.sh Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,26 @@ +# common setup for head checking code + +. $TESTDIR/testlib/common.sh + +cat >> $HGRCPATH < "$1" + hg add "$1" + hg ci -m "$1" +} + +getid() { + hg log --hidden --template '{node}\n' --rev "$1" +} + +cat >> $HGRCPATH <> $HGRCPATH <> $HGRCPATH + +mkcommit() { + echo "$1" > "$1" + hg add "$1" + hg ci -m "$1" +} +getid() { + hg log --hidden --template '{node}\n' --rev "$1" +} + +setuprepos() { + echo creating test repo for test case $1 + mkdir $1 + cd $1 + echo - pulldest + hg init pushdest + cd pushdest + mkcommit O + hg phase --public . + cd .. + echo - main + hg clone -q pushdest main + echo - pushdest + hg clone -q main pulldest + echo 'cd into `main` and proceed with env setup' +} + +dotest() { +# dotest TESTNAME [TARGETNODE] + + testcase=$1 + shift + target="$1" + if [ $# -gt 0 ]; then + shift + fi + targetnode="" + desccall="" + cd $testcase + echo "## Running testcase $testcase" + if [ -n "$target" ]; then + desccall="desc("\'"$target"\'")" + targetnode="`hg -R main id -qr \"$desccall\"`" + echo "# testing echange of \"$target\" ($targetnode)" + fi + echo "## initial state" + echo "# obstore: main" + hg -R main debugobsolete | sort + echo "# obstore: pushdest" + hg -R pushdest debugobsolete | sort + echo "# obstore: pulldest" + hg -R pulldest debugobsolete | sort + + if [ -n "$target" ]; then + echo "## pushing \"$target\"" from main to pushdest + hg -R main push -r "$desccall" $@ pushdest + else + echo "## pushing from main to pushdest" + hg -R main push pushdest $@ + fi + echo "## post push state" + echo "# obstore: main" + hg -R main debugobsolete | sort + echo "# obstore: pushdest" + hg -R pushdest debugobsolete | sort + echo "# obstore: pulldest" + hg -R pulldest debugobsolete | sort + if [ -n "$target" ]; then + echo "## pulling \"$targetnode\"" from main into pulldest + hg -R pulldest pull -r $targetnode $@ main + else + echo "## pulling from main into pulldest" + hg -R pulldest pull main $@ + fi + echo "## post pull state" + echo "# obstore: main" + hg -R main debugobsolete | sort + echo "# obstore: pushdest" + hg -R pushdest debugobsolete | sort + echo "# obstore: pulldest" + hg -R pulldest debugobsolete | sort + + cd .. + +} diff -r efda653c96a7 -r 94432e742a02 tests/testlib/pythonpath.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/testlib/pythonpath.sh Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,9 @@ +# utility to setup pythonpath to point into the tested repository + +export SRCDIR=`dirname $TESTDIR` +if [ -n "$PYTHONPATH" ]; then + export HGTEST_ORIG_PYTHONPATH=$PYTHONPATH + export PYTHONPATH=$SRCDIR:$PYTHONPATH +else + export PYTHONPATH=$SRCDIR +fi diff -r efda653c96a7 -r 94432e742a02 tests/testlib/topic_setup.sh --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/testlib/topic_setup.sh Fri Mar 31 15:33:59 2017 +0200 @@ -0,0 +1,15 @@ +#!/bin/sh + +# This file holds logic that is used in many tests. +# It can be called in a test like this: +# $ . "$TESTDIR/testlib/topic_setup.sh" + +# Enable obsolete markers and enable extensions +cat >> $HGRCPATH << EOF +[experimental] +evolution=createmarkers,exchange + +[extensions] +rebase= +EOF +echo "topic=$(echo $(dirname $TESTDIR))/hgext3rd/topic" >> $HGRCPATH