Mercurial > evolve
changeset 403:c2ef47fce3f9
add nopushpublish extension.
author | Pierre-Yves David <pierre-yves.david@logilab.fr> |
---|---|
date | Mon, 30 Jul 2012 14:45:42 +0200 |
parents | 4d63f8a00787 (diff) 8ef1096ee5ab (current diff) |
children | b5b1bf5166a2 |
files | |
diffstat | 47 files changed, 8342 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,11 @@ +syntax: re +/figures/[^/]+\.png$ +^docs/build/ +^docs/html/ +^html/ +\.pyc$ +~$ +\.swp$ +\.orig$ +\.rej$ +\.err$
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgtags Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,6 @@ +6c6bb7a23bb5125bf06da73265f039dd3447dafa 0.1.0 +d3f20770b86a31dba56ae7b252089e12b34702da 0.2.0 +c046b083a5e0b21af69027f31cee141800cf894b 0.3.0 +9bbcd274689829d9239978236e16610688978233 0.4.0 +4ecbaec1d664b1e6f8ebc78292e1ced77a8e69c0 0.4.1 +7ef8ab8c6feadb8a9d9e13af144a17cb23e9a38d 0.5
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,27 @@ +PYTHON=python +HG=`which hg` + +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 + +tests: + cd tests && $(PYTHON) run-tests.py --with-hg=$(HG) $(TESTFLAGS) + +test-%: + cd tests && $(PYTHON) run-tests.py --with-hg=$(HG) $(TESTFLAGS) $@ + +tests-%: + @echo "Path to crew repo is $(CREW) - set this with CREW= if needed." + hg -R $(CREW) checkout $$(echo $@ | sed s/tests-//) && \ + (cd $(CREW) ; $(MAKE) clean ) && \ + cd tests && $(PYTHON) $(CREW)/tests/run-tests.py $(TESTFLAGS) + +all-version-tests: tests-1.3.1 tests-1.4.3 tests-1.5.4 \ + tests-1.6.4 tests-1.7.5 tests-1.8 tests-tip + +.PHONY: tests all-version-tests
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,68 @@ +============================= +Mutable History For Mercurial +============================= + +:obsolete: + + Introduce an ``obsolete`` concept that tracks new versions of rewritten + changesets. + +:evolve: + + A collection of commands to rewrite the mutable part of the history. + + + +**These extensions are experimental and are not meant for production.** + +You can quicky enable them using:: + + ./enable.sh >> ~/.hgrc + +But it's recommended to look at the doc in the first place. + +See doc/ directory for details. + +Online version of the doc is available: + + http://hg-lab.logilab.org/doc/mutable-history/html/ + +Contribute +================== + +The simplest way to contribute is to issue a pull request on bitbucket. + +However, some cutting edge change may be found in a mutable repository hosted +by logilab before they are published. + + http://hg-lab.logilab.org/wip/mutable-history/ + +Make sure to check lastest draft changeset before submitting new changeset. + + +Changelog +================== + +0.3.0 -- + +- obsolete: Add "latecomer" error detection (stabilize does not handle resolution yet) +- evolve: Introduce a new `uncommit` command to remove change from a changeset +- rebase: allow the use of --keep again +- commit: --amend option create obsolete marker (but still strip) +- obsolete: fewer marker are created when collapsing revision. +- revset: add, successors(), allsuccessors(), precursors(), allprecursors(), + latecomer() and hidden() +- evolve: add `prune` alias to `kill`. +- stabilize: clearly state that stabilize does nto handle conflict +- 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 +- rebase: handle removing empty changesets +- rebase: handle --collapse +- evolve: add `obsolete` alias to `kill` +- evolve: add `evolve` alias to `stabilize` + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/README Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +doc generated with sphinx. tutorial exported using sphinxedhg + +http://hg.piranha.org.ua/sphinxedhg/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/conf.py Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,124 @@ +# 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' +# Add any paths that contain templates here, relative to this directory. +#templates_path = [] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General substitutions. +project = 'Obsolete experimentation' +copyright = '2010-2011, pierre-yves.david@logilab.fr' + +# The default replacements for |version| and |release|, also used in various +# other places throughout the built documents. +# +# The short X.Y version. +version = '0.0' +# The full version, including alpha/beta/rc tags. +release = '0.0' + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +unused_docs = [] + +# List of directories, relative to source directories, that shouldn't be searched +# for source files. +#exclude_dirs = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#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 + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# 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' + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +html_title = project +html_theme = 'haiku' +html_theme_path = ['.'] + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (within the static path) to place at the top of +# the sidebar. +#html_logo = None + +# 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 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['.static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +html_use_modindex = False + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, the reST sources are included in the HTML build as _sources/<name>. +#html_copy_source = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '.html' + +# Output file base name for HTML help builder. +#htmlhelp_basename = ''
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/evolve-collaboration.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,133 @@ + +------------------------------------------------ +Collaboration Using Evolve: A user story +------------------------------------------------ + + +After having written some code for ticket #42, Alice starts a patch +(this will be kind of like a 'work-in-progress' checkpoint +initially):: + + $ hg ci -m '[entities] remove magic' + +Instant patch ! Note how the default phase of this changeset is (still) +in "draft" state. + +This is easily checkable:: + + $ hg phase tip + 827: draft + +See? Until the day it becomes a "public" changeset, this can be +altered to no end. How? It happens with an explicit:: + + $ hg phase --public + +In practice, pushing to a "publishing" repository can also turn draft +changesets into public ones. Older Mercurial releases are automatically +"publishing" since they do not have the notion of non-public changesets +(or mutable history). + +During the transition from older mercurial servers to new ones, this will +happen often, so be careful. + +Now let's come back to our patch. Next hour sees good progress and Alice +wants to complete the patch with the recent stuff (all that's shown by +an "hg diff") to share with a co-worker, Bob:: + + $ hg amend -m '[entities] fix frobulator (closes #42)' + +Note that we also fix the commit message. (For recovering mq users: this +is just like "hg qrefresh -m"). + +Before leaving, let's push to the central shared repository. That will +give Bob the signal that something is ripe for review / further amendments:: + + $ hg push # was done with a modern mercurial, draft phase is preserved + +The next day, Bob, who arrives very early, can immediately work out +some glitches in the patch. + +He then starts two others, for ticket #43 and #44 and finally commits them. +Then, as original worker arrives, he pushes his stuff. + +Alice, now equipped with enough properly sugared coffee to survive the +next two hours:: + + $ hg pull + +Then:: + + $ hg up "tip ~ 2" + +brings her to yesterday's patch. Indeed the patch serial number has +increased (827 still exists but has been obsoleted). + +She understands that his original patch has been altered. But how did it +evolve? + +The enhanced hgview shows the two patches. By default only the most +recent version of a patch is shown. + +Now, when Alice installed the mutable-history extensions, she got an alias +that allows her to see the diff between two amendments, defined like this:: + + odiff=diff --rev 'limit(obsparents(.),1)' --rev . + +She can see exactly how Bob amended her work. + +* odiff + + +Amend ... Stabilize +-------------------- + +Almost perfect ! Alice just needs to fix a half dozen grammar oddities in +the new docstrings and it will be publishable. + +Then, another round of: + + $ hg amend + +and a quick look at hgview ... shows something strange (at first). + +Ticket #42 yesterday's version is still showing up, with two descendant lineages: + +* the next version, containing grammar fixes, + +* the two stacked changesets for tickets #43 .. 44 committed by Bob. + +Indeed, since this changeset still has non-obsolete descendant +changesets it cannot be hidden. This branch (old version of #42 and +the two descendants by C.W.) is said to be _unstable_. + +Why would one want such a state? Why not auto-stabilize each time "hg +amend" is typed out? + +Alice for one, wouldn't want to merge each time she amends something that +might conflict with the descendant changesets. Remember she is +currently updating the very middle of an history! + +Being now done with grammar and typo fixes, Alice decides it is time to +stabilize again the tree. She does:: + + $ hg stabilize + +two times, one for each unstable descendant. The last time, hgview +shows her a straight line again. Wow! that feels a bit like a +well-planned surgical operation. At the end, the patient tree has +been properly trimmed and any conflict properly handled. + +Of course nothing fancy really happened: each "stablilize" can be +understood in terms of a rebase of the next unstable descendant to the +newest version of its parent (including the possible manual conflict +resolution intermission ...). + +Except that rebase is a destructive (it removes information from the +repository), unrecoverable operation, and the "evolve + obsolete" +combo, using changeset copy and obsolescence marker, provide evolution +semantics by only adding new information to the repository (but more +on that later). + +She pushes again. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/evolve-faq.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,236 @@ + +--------------------------------------------------------------------- +Evolve How To +--------------------------------------------------------------------- + + + +Add a changeset: ``commit`` +------------------------------------------------------------ + +Just use commit as usual. New changesets will be in the `draft` phase. + +Rewrite a changeset: ``amend`` +------------------------------------------------------------ + +A new command ``hg amend`` is added by the extension. It writes a new +changeset combining working-directory parent changes and parent. It +will work on any `draft` or `secret` changeset. It will not work on +`public` changesets. + +To understand what the result of amend will be I use the two following +aliases [#]_:: + + # diff what amend will look likes + pdiff=diff --rev .^ + + # status what amend will look likes + pstatus=status --rev .^ + +It takes various options to pick an author, a date and the branch of the +result... (see ``hg help amend`` for details). + +This command can even be invoked on changesets with children (provided +none is public) ! + +.. note:: the amend command is very similar to mq's ``qrefresh``, a ``refresh`` + alias for amend is also available. But note that contrary to + ``qrefresh``, ``amend`` does not exclude changes on file not specified + on the command line. + + XXX add idank example + + +.. [#] (added by enable.sh) + + + +Move a changeset: ``graft`` +------------------------------------------------------------ + +The graft command introduced in 2.0 allows to "copy changes from other +branches onto the current branch". + +The graft command has been altered to allow the creation of an +obsolete marker indicating both the result cset and its source +(actually recording changeset movements). + +This is achieved using a new flag `-O` (or `old-obsolete`) [#]_. + + +XXX example + +.. warning:: when using graft --continue after conflict resolution you **MUST** + pass `-O` or `-o` flag again because they are not saved for now + + +.. [#] add this `-O` to graft instead of a dedicated command is probably + abusive. But this was very convenient for experimental purposes. + This will likely change in non experimental release. + +Delete a changeset: ``prune`` +------------------------------------------------------------ + +A new ``prune`` command allows removing a changeset. + +Just use ``hg prune <some-rev>``. + +Moving within the history: ``up`` ``gdown`` and ``gup`` +------------------------------------------------------------ + +While working on mutable part of the history you often need to move between +mutable commit. + +You just need to use standard update to work with evolve. For convenience, you +can use ``hg gup`` to move to children commit or ``hg gdown`` to move to working +directory parent commit. + +.. note:: those command only exist for the convenience of getting qpush and qpop + feeling back. + +Collapse changesets: ``amend`` +------------------------------------------------------------ + +you can use amend -c to collapse multiple changeset in a single one. + +Getting changes out of a commit +------------------------------------------------------------ + +The ``hg uncommit`` command lets you rewrite the parent commit without +selected changed files. Target files content is not altered and +appears again as "modified":: + + $ hg st + M babar + M celestine + $ hg commit babar celestine + $ hg st + $ hg uncommit celestine + $ hg status + M celestine + +Split a changeset +----------------------- + +To split on file boundaries, just use ``uncommit`` command. + +If you need fine-grained split, there is no official command for that yet. +However, it is easily achieved by manual operation:: + + ### you want to split changeset A: 42 + # update to A parent + $ hg up 42^ + # restore content from A + $ hg revert -r 42 --all + # partially commit the first part + $ hg record + # commit the second part + $ hg commit + # informs mercurial of what appened + # current changeset (.) and previous one (.^) replace A (42) + $ hg prune --new . --new .^ 42 + + +Update my current work in progess after a pull +---------------------------------------------- + +Whenever you are working on some changesets, it is more likely that a pull +will, eventually, import new changesets in your tree. + +And it is likely that you will want your work in progress changsets to be +rebased on the top of this newly imported subtree. + +Doing so is only a matter of rebasing. + + + +Move multiple changesets: ``rebase`` +------------------------------------------------------------ + +You can still use rebase to move a whole segment of the changeset graph together. + +.. warning:: Beware that rebasing obsolete changesets will result in + conflicting versions of the changesets. + +Stabilize history: ``stabilize`` +------------------------------------------------------------ + +When you rewrite (amend) a changeset with children without rewriting +those children you create *unstable* changesets and *suspended +obsolete* changesets. + +When you are finished amending a given changeset, you will want to +declare it stable, in other words rebase its former descendants on its +newest version. This is not done automatically to avoid the +proliferation of useless hidden changesets. + +.. warning:: ``hg stabilize`` have no --continue to use after conflict + resolution + +.. warning:: stabilization does not handle deletion yet. + +.. warning:: obsolete currently relies on changesets in secret phase + to avoid exchanging obsolete and unstable changesets. + + XXX details issue here + + +Fix my history afterward: ``prune -n`` +------------------------------------------------------------ + +Sometimes you need to create an obsolete marker by hand. This may happen when +upstream has applied some of your patches for example. + +you can use ``hg prune --new <new-changeset> <old-changeset>`` to add obsolete +marker. + +Export to mq: ``synchronize`` +------------------------------------------------------------ + +Another extension allows to export your changes to mq. + +View diff from the last amend +------------------------------------------------------------ + +An ``odiff`` alias have been added by ``enable.sh`` + +:: + [alias] + odiff = diff --rev 'limit(precursors(.),1)' --rev . + +View obsolete markers +------------------------------------------------------------ + +hgview is the only viewer that support this feature. You need an experimental +version available here: + + $ hg clone http://hg-dev.octopoid.net/hgwebdir.cgi/hgview/ + +You can also use a debug command + + $ hg debugsuccessors + 5eb72dbe0cb4 e8db4aa611f6 + c4cbebac3751 4f1c269eab68 + + + +Important Note +===================================================================== + +View change to your file +------------------------------------------------------------ + +Extinct changesets are hidden using the *hidden* feature of mercurial. + +Only ``hg log``, ``hg glog`` and ``hgview`` support it, other +graphical viewer do not. + + + + + + + + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/evolve-good-practice.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,44 @@ +----------------------------------------- +Good pratice for (early) user of evolve +----------------------------------------- + +Avoid unstability +-------------------------------- + +The less unstability you have the less you need to resolve. + +Evolve is not yet able to detect and solve every situation. And your mind is +not ready neither. + +Branch as much as possible +-------------------------------- + +This is not MQ, you are not constrainted to linear history. + +Making a branch per independent branch will help you avoid unstability +and conflict. + +Rewrite you change only +------------------------------------------------ + +There is no descent conflict detection and handling right now. +Rewriting other people's changesets guarantees that you will get +conflicts. Communicate with your fellow developers before trying to +touch other people's work (which is a good pratice in any case). + +Using multiple branch will help you to achieve this goal. + +Prefer pushing unstability than touching other people changeset +------------------------------------------------------------------ + + +If you have children changesets from other people that you don't really care +about, prefer not altering them to risking a conflict by stabilizing them. + + +Do not get too confident +--------------------------- + +This is an experimental extension and a complex concept. This is beautiful, +powerful and robust on paper, but the tool and your mind may not be prepared for +all situations yet.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-conflicting.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="342 1890 349 427" width="349pt" height="427pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-18 23:47Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -3 6 6" markerWidth="6" markerHeight="6" color="blue"><g><path d="M 3.7333333 0 L 1.8666667 -1.4 L 0 0 L 1.8666667 1.4 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="17" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id465_Graphic" filter="url(#Shadow)"/><use xl:href="#id466_Graphic" filter="url(#Shadow)"/><use xl:href="#id472_Graphic" filter="url(#Shadow)"/><use xl:href="#id471_Graphic" filter="url(#Shadow)"/><use xl:href="#id475_Graphic" filter="url(#Shadow)"/><use xl:href="#id718_Graphic" filter="url(#Shadow)"/><use xl:href="#id722_Graphic" filter="url(#Shadow)"/></g><g id="id465_Graphic"><rect x="362.1148" y="1906.3591" width="308.04614" height="386.30615" fill="white" fill-opacity=".5"/><rect x="362.1148" y="1906.3591" width="308.04614" height="386.30615" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id466_Graphic"><path d="M 460.24097 2058.0142 C 471.311 2069.0842 471.311 2087.032 460.24097 2098.102 C 449.171 2109.1721 431.22305 2109.1721 420.15308 2098.102 C 409.08304 2087.032 409.08304 2069.0842 420.15308 2058.0142 C 431.22305 2046.9441 449.171 2046.9441 460.24097 2058.0142" fill="yellow"/><path d="M 460.24097 2058.0142 C 471.311 2069.0842 471.311 2087.032 460.24097 2098.102 C 449.171 2109.1721 431.22305 2109.1721 420.15308 2098.102 C 409.08304 2087.032 409.08304 2069.0842 420.15308 2058.0142 C 431.22305 2046.9441 449.171 2046.9441 460.24097 2058.0142" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><text transform="translate(422.51984 2062.5581)" fill="maroon"><tspan font-family="Helvetica" font-size="26" font-weight="500" fill="maroon" x="6.1180782" y="25" textLength="23.118164">A’</tspan></text></g><line x1="508.74466" y1="2219.2893" x2="453.23264" y2="2104.9158" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id472_Graphic"><path d="M 525.1753 2222.4712 C 530.71033 2228.0061 530.71033 2236.9802 525.1753 2242.5151 C 519.64032 2248.0503 510.66632 2248.0503 505.13132 2242.5151 C 499.5963 2236.9802 499.5963 2228.0061 505.13132 2222.4712 C 510.66632 2216.936 519.64032 2216.936 525.1753 2222.4712" fill="black"/><path d="M 525.1753 2222.4712 C 530.71033 2228.0061 530.71033 2236.9802 525.1753 2242.5151 C 519.64032 2248.0503 510.66632 2248.0503 505.13132 2242.5151 C 499.5963 2236.9802 499.5963 2228.0061 505.13132 2222.4712 C 510.66632 2216.936 519.64032 2216.936 525.1753 2222.4712" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id471_Graphic"><path d="M 525.1753 2268.4714 C 530.71033 2274.0063 530.71033 2282.9805 525.1753 2288.5154 C 519.64032 2294.0505 510.66632 2294.0505 505.13132 2288.5154 C 499.5963 2282.9805 499.5963 2274.0063 505.13132 2268.4714 C 510.66632 2262.9363 519.64032 2262.9363 525.1753 2268.4714" fill="black"/><path d="M 525.1753 2268.4714 C 530.71033 2274.0063 530.71033 2282.9805 525.1753 2288.5154 C 519.64032 2294.0505 510.66632 2294.0505 505.13132 2288.5154 C 499.5963 2282.9805 499.5963 2274.0063 505.13132 2268.4714 C 510.66632 2262.9363 519.64032 2262.9363 525.1753 2268.4714" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="515.15332" y1="2247.1665" x2="515.15332" y2="2263.8201" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="515.15332" y1="2217.8198" x2="515.15332" y2="2168.1504" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id475_Graphic"><path d="M 535.19727 2119.26 C 546.26727 2130.3301 546.26727 2148.2778 535.19727 2159.3479 C 524.12732 2170.418 506.17935 2170.418 495.10938 2159.3479 C 484.03934 2148.2778 484.03934 2130.3301 495.10938 2119.26 C 506.17935 2108.19 524.12732 2108.19 535.19727 2119.26" fill="yellow" fill-opacity=".5"/><path d="M 535.19727 2119.26 C 546.26727 2130.3301 546.26727 2148.2778 535.19727 2159.3479 C 524.12732 2170.418 506.17935 2170.418 495.10938 2159.3479 C 484.03934 2148.2778 484.03934 2130.3301 495.10938 2119.26 C 506.17935 2108.19 524.12732 2108.19 535.19727 2119.26" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(497.47614 2123.804)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="463.31088" y1="2096.9441" x2="482.01135" y2="2112.2239" marker-end="url(#FilledDiamond_Marker)" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/><g id="id718_Graphic"><path d="M 618.86163 2058.0142 C 629.93164 2069.0842 629.93164 2087.032 618.86163 2098.102 C 607.7917 2109.1721 589.8437 2109.1721 578.77374 2098.102 C 567.70374 2087.032 567.70374 2069.0842 578.77374 2058.0142 C 589.8437 2046.9441 607.7917 2046.9441 618.86163 2058.0142" fill="#ff6"/><path d="M 618.86163 2058.0142 C 629.93164 2069.0842 629.93164 2087.032 618.86163 2098.102 C 607.7917 2109.1721 589.8437 2109.1721 578.77374 2098.102 C 567.70374 2087.032 567.70374 2069.0842 578.77374 2058.0142 C 589.8437 2046.9441 607.7917 2046.9441 618.86163 2058.0142" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><text transform="translate(581.14056 2062.5581)" fill="maroon"><tspan font-family="Helvetica" font-size="26" font-weight="500" fill="maroon" x="3.2298946" y="25" textLength="28.894531">A’’</tspan></text></g><line x1="522.14453" y1="2219.5881" x2="584.5973" y2="2104.3074" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="574.73132" y1="2095.6902" x2="549.68903" y2="2114.0222" marker-end="url(#FilledDiamond_Marker)" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/><line x1="470.04346" y1="2078.1025" x2="568.97186" y2="2078.2505" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="7"/><g id="id722_Graphic"><path d="M 419.06274 2052.1411 C 459.63675 2034.4656 555.9978 2028.5737 616.85608 2041.7183 C 677.71497 2054.8606 641.8429 2119.8875 608.87634 2121.7026 C 575.90967 2123.5149 553.70416 2096.4143 553.70416 2096.4148 L 498.5315 2097.185 C 498.5315 2097.185 500.21146 2125.1155 452.02893 2119.2227 C 403.84808 2113.3301 378.49112 2069.8181 419.06274 2052.1411 Z" fill="red" fill-opacity=".15000001"/><path d="M 419.06274 2052.1411 C 459.63675 2034.4656 555.9978 2028.5737 616.85608 2041.7183 C 677.71497 2054.8606 641.8429 2119.8875 608.87634 2121.7026 C 575.90967 2123.5149 553.70416 2096.4143 553.70416 2096.4148 L 498.5315 2097.185 C 498.5315 2097.185 500.21146 2125.1155 452.02893 2119.2227 C 403.84808 2113.3301 378.49112 2069.8181 419.06274 2052.1411 Z" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="4,9,1,5"/></g><text transform="translate(481.40805 1999.724)" fill="red"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="red" x=".11987305" y="16" textLength="43.429688">Confl</tspan><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="red" x="43.54956" y="16" textLength="45.330566">icting</tspan></text></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-extinct.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="286 1306 644 435" width="644pt" height="435pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-18 23:47Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -2 4 4" markerWidth="4" markerHeight="4" color="blue"><g><path d="M 1.6 0 L .8 -.60000002 L 0 0 L .8 .60000002 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="17" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id581_Graphic" filter="url(#Shadow)"/><use xl:href="#id423_Graphic" filter="url(#Shadow)"/><use xl:href="#id422_Graphic" filter="url(#Shadow)"/><use xl:href="#id415_Graphic" filter="url(#Shadow)"/><use xl:href="#id567_Graphic" filter="url(#Shadow)"/><use xl:href="#id588_Graphic" filter="url(#Shadow)"/><use xl:href="#id565_Graphic" filter="url(#Shadow)"/><use xl:href="#id592_Graphic" filter="url(#Shadow)"/><use xl:href="#id595_Graphic" filter="url(#Shadow)"/><use xl:href="#id597_Graphic" filter="url(#Shadow)"/><use xl:href="#id599_Graphic" filter="url(#Shadow)"/><use xl:href="#id604_Graphic" filter="url(#Shadow)"/></g><g id="id581_Graphic"><rect x="538.9198" y="1322.60815" width="371" height="394" fill="white" fill-opacity=".5"/><rect x="538.9198" y="1322.60815" width="371" height="394" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id423_Graphic"><rect x="306.04608" y="1379.2305" width="160" height="280.84644" fill="white" fill-opacity=".5"/><rect x="306.04608" y="1379.2305" width="160" height="280.84644" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id422_Graphic"><circle cx="384.04633" cy="1522.2869" r="28.346533" fill="red"/><circle cx="384.04633" cy="1522.2869" r="28.346533" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(366.36917 1506.7869)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="371.27045" y1="1496.4152" x2="353.8865" y2="1461.212" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><path d="M 394.06836 1589.8818 C 399.60336 1595.4169 399.60336 1604.3907 394.06836 1609.9258 C 388.53336 1615.4608 379.55936 1615.4608 374.02435 1609.9258 C 368.48935 1604.3907 368.48935 1595.4169 374.02435 1589.8818 C 379.55936 1584.3468 388.53336 1584.3468 394.06836 1589.8818" fill="black"/><path d="M 394.06836 1589.8818 C 399.60336 1595.4169 399.60336 1604.3907 394.06836 1609.9258 C 388.53336 1615.4608 379.55936 1615.4608 374.02435 1609.9258 C 368.48935 1604.3907 368.48935 1595.4169 374.02435 1589.8818 C 379.55936 1584.3468 388.53336 1584.3468 394.06836 1589.8818" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 394.06836 1635.8817 C 399.60336 1641.4167 399.60336 1650.3906 394.06836 1655.9257 C 388.53336 1661.4607 379.55936 1661.4607 374.02435 1655.9257 C 368.48935 1650.3906 368.48935 1641.4167 374.02435 1635.8817 C 379.55936 1630.3467 388.53336 1630.3467 394.06836 1635.8817" fill="black"/><path d="M 394.06836 1635.8817 C 399.60336 1641.4167 399.60336 1650.3906 394.06836 1655.9257 C 388.53336 1661.4607 379.55936 1661.4607 374.02435 1655.9257 C 368.48935 1650.3906 368.48935 1641.4167 374.02435 1635.8817 C 379.55936 1630.3467 388.53336 1630.3467 394.06836 1635.8817" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="384.05234" y1="1614.577" x2="384.05911" y2="1631.2305" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id415_Graphic"><circle cx="341.11066" cy="1435.3403" r="28.346533" fill="yellow"/><circle cx="341.11066" cy="1435.3403" r="28.346533" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(323.43347 1419.8403)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g><line x1="384.04633" y1="1551.1333" x2="384.04636" y2="1585.2306" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id567_Graphic"><path d="M 649.09058 1405.5331 C 665.0907 1366.53296 703.09058 1353.5331 727.09058 1382.5331 C 751.0899 1411.5319 743.091 1543.5331 730.0907 1547.5327 C 717.09076 1551.5322 681.09106 1566.5331 662.09082 1553.533 C 643.0905 1540.5328 633.091 1444.5325 649.09058 1405.5331 Z" fill="blue" fill-opacity=".15000001"/><path d="M 649.09058 1405.5331 C 665.0907 1366.53296 703.09058 1353.5331 727.09058 1382.5331 C 751.0899 1411.5319 743.091 1543.5331 730.0907 1547.5327 C 717.09076 1551.5322 681.09106 1566.5331 662.09082 1553.533 C 643.0905 1540.5328 633.091 1444.5325 649.09058 1405.5331 Z" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="4,9,1,5"/></g><line x1="670.96875" y1="1499.7141" x2="608.7143" y2="1440.1655" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><path d="M 763.18304 1609.1852 C 768.7181 1614.7202 768.7181 1623.6941 763.18304 1629.2291 C 757.64807 1634.7642 748.67407 1634.7642 743.1391 1629.2291 C 737.60406 1623.6941 737.60406 1614.7202 743.1391 1609.1852 C 748.67407 1603.6501 757.64807 1603.6501 763.18304 1609.1852" fill="black"/><path d="M 763.18304 1609.1852 C 768.7181 1614.7202 768.7181 1623.6941 763.18304 1629.2291 C 757.64807 1634.7642 748.67407 1634.7642 743.1391 1629.2291 C 737.60406 1623.6941 737.60406 1614.7202 743.1391 1609.1852 C 748.67407 1603.6501 757.64807 1603.6501 763.18304 1609.1852" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 763.18304 1655.1852 C 768.7181 1660.7202 768.7181 1669.6941 763.18304 1675.2291 C 757.64807 1680.7642 748.67407 1680.7642 743.1391 1675.2291 C 737.60406 1669.6941 737.60406 1660.7202 743.1391 1655.1852 C 748.67407 1649.6501 757.64807 1649.6501 763.18304 1655.1852" fill="black"/><path d="M 763.18304 1655.1852 C 768.7181 1660.7202 768.7181 1669.6941 763.18304 1675.2291 C 757.64807 1680.7642 748.67407 1680.7642 743.1391 1675.2291 C 737.60406 1669.6941 737.60406 1660.7202 743.1391 1655.1852 C 748.67407 1649.6501 757.64807 1649.6501 763.18304 1655.1852" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="753.16107" y1="1633.8804" x2="753.16107" y2="1650.5339" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id588_Graphic"><circle cx="587.5074" cy="1419.88025" r="28.346518" fill="yellow"/><path d="M 607.55133 1399.8363 C 618.62134 1410.90625 618.62134 1428.85425 607.55133 1439.9242 C 596.4814 1450.9943 578.5334 1450.9943 567.46344 1439.9242 C 556.39343 1428.85425 556.39343 1410.90625 567.46344 1399.8363 C 578.5334 1388.7662 596.4814 1388.7662 607.55133 1399.8363" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke-dasharray="16,9"/><text transform="translate(569.83026 1404.38025)" fill="red"><tspan font-family="Helvetica" font-size="26" font-weight="500" fill="red" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g><g id="id565_Graphic"><circle cx="691.8144" cy="1519.6537" r="28.346518" fill="red" fill-opacity=".5"/><path d="M 711.85834 1499.6097 C 722.92834 1510.6797 722.92834 1528.6277 711.85834 1539.6976 C 700.7884 1550.7677 682.8404 1550.7677 671.77045 1539.6976 C 660.70044 1528.6277 660.70044 1510.6797 671.77045 1499.6097 C 682.8404 1488.5397 700.7884 1488.5397 711.85834 1499.6097" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(674.13727 1504.1537)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="706.95093" y1="1544.2173" x2="745.4616" y2="1606.7125" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="784.66077" y1="1519.52405" x2="730.5608" y2="1519.5996" marker-end="url(#FilledDiamond_Marker)" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="1,3"/><g id="id592_Graphic"><circle cx="813.5072" cy="1519.4838" r="28.346518" fill="red"/><circle cx="813.5072" cy="1519.4838" r="28.346518" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(795.8301 1503.9838)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="6.5243282" y="25" textLength="22.305664">A'</tspan></text></g><line x1="798.56934" y1="1544.1689" x2="760.75946" y2="1606.6506" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id595_Graphic"><circle cx="426.39252" cy="1435.3403" r="28.346533" fill="#ff8000"/><circle cx="426.39252" cy="1435.3403" r="28.346533" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(408.71533 1419.8403)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="8.2889767" y="25" textLength="18.776367">C</tspan></text></g><line x1="396.6806" y1="1496.3458" x2="413.75824" y2="1461.2814" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id597_Graphic"><circle cx="817.5072" cy="1419.88025" r="28.346518" fill="#ff8000"/><circle cx="817.5072" cy="1419.88025" r="28.346518" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(799.8301 1404.38025)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="5.807043" y="25" textLength="23.740234">C'</tspan></text></g><line x1="816.34967" y1="1448.7037" x2="814.66473" y2="1490.6603" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id599_Graphic"><circle cx="691.8144" cy="1419.88025" r="28.346518" fill="#ff8000" fill-opacity=".5"/><path d="M 711.85834 1399.8363 C 722.92834 1410.90625 722.92834 1428.85425 711.85834 1439.9242 C 700.7884 1450.9943 682.8404 1450.9943 671.77045 1439.9242 C 660.70044 1428.85425 660.70044 1410.90625 671.77045 1399.8363 C 682.8404 1388.7662 700.7884 1388.7662 711.85834 1399.8363" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(674.13727 1404.38025)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="8.2889767" y="25" textLength="18.776367">C</tspan></text></g><line x1="691.8196" y1="1448.7267" x2="691.82715" y2="1490.80725" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="788.66077" y1="1419.88025" x2="730.56085" y2="1419.88025" marker-end="url(#FilledDiamond_Marker)" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="1,3"/><text transform="translate(557.16095 1491.30725)" fill="blue"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="blue" x=".13012695" y="16" textLength="72.739746">Obsolete</tspan></text><g id="id604_Graphic"><path d="M 548.05884 1393.9442 C 562.8297 1376.26746 597.9101 1370.3754 620.06622 1383.5195 C 642.22168 1396.6632 634.83734 1456.4924 622.8358 1458.3053 C 610.83466 1460.118 577.60083 1466.9171 560.06024 1461.0248 C 542.51978 1455.1326 533.28845 1411.6206 548.05884 1393.9442 Z" fill="#ff8000" fill-opacity=".15000001"/><path d="M 548.05884 1393.9442 C 562.8297 1376.26746 597.9101 1370.3754 620.06622 1383.5195 C 642.22168 1396.6632 634.83734 1456.4924 622.8358 1458.3053 C 610.83466 1460.118 577.60083 1466.9171 560.06024 1461.0248 C 542.51978 1455.1326 533.28845 1411.6206 548.05884 1393.9442 Z" stroke="#ff8000" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="4,9,1,5"/></g><text transform="translate(551.5074 1339.08044)" fill="#ff8000"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="#ff8000" x=".103271484" y="16" textLength="71.793457">Unstable</tspan></text><line x1="631.31445" y1="1469.267" x2="752.31433" y2="1469.267" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="4,3,1,3"/><text transform="translate(712.3343 1373.30725)" fill="maroon"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="maroon" x=".103271484" y="16" textLength="54.793457">extinct</tspan></text><text transform="translate(580.1609 1539.234)" fill="#008040"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="#008040" x=".13012695" y="16" textLength="89.739746">suspended</tspan></text></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-obsolete.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="125 130 278 413" width="278pt" height="413pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-18 23:47Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -2 4 4" markerWidth="4" markerHeight="4" color="blue"><g><path d="M 1.6 0 L .8 -.60000002 L 0 0 L .8 .60000002 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="17" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id411_Graphic" filter="url(#Shadow)"/><use xl:href="#id410_Graphic" filter="url(#Shadow)"/><use xl:href="#id408_Graphic" filter="url(#Shadow)"/><use xl:href="#id407_Graphic" filter="url(#Shadow)"/><use xl:href="#id395_Graphic" filter="url(#Shadow)"/><use xl:href="#id394_Graphic" filter="url(#Shadow)"/><use xl:href="#id391_Graphic" filter="url(#Shadow)"/><use xl:href="#id390_Graphic" filter="url(#Shadow)"/><use xl:href="#id724_Graphic" filter="url(#Shadow)"/></g><g id="id411_Graphic"><rect x="145" y="146" width="238" height="373" fill="white" fill-opacity=".5"/><rect x="145" y="146" width="238" height="373" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id410_Graphic"><circle cx="290.34644" cy="351.50146" r="28.34651" fill="red"/><circle cx="290.34644" cy="351.50146" r="28.34651" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(272.66928 336.0015)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="216.72533" y1="434.68497" x2="271.22772" y2="373.10342" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id408_Graphic"><circle cx="207.00024" cy="445.67322" r="14.173263" fill="black"/><circle cx="207.00024" cy="445.67322" r="14.173263" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id407_Graphic"><circle cx="206.30316" cy="488.67325" r="14.173263" fill="black"/><circle cx="206.30316" cy="488.67325" r="14.173263" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="206.7624" y1="460.34457" x2="206.541" y2="474.0019" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id395_Graphic"><circle cx="206.30315" cy="266.11322" r="24.803194" fill="#ff8000" fill-opacity=".5"/><path d="M 223.8416 248.57477 C 233.52788 258.26099 233.52788 273.96545 223.8416 283.65167 C 214.15538 293.33795 198.45091 293.33795 188.76469 283.65167 C 179.07841 273.96545 179.07841 258.26099 188.76469 248.57477 C 198.45091 238.88849 214.15538 238.88849 223.8416 248.57477" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(191.46063 250.61322)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="5.4543362" y="25" textLength="18.776367">C</tspan></text></g><g id="id394_Graphic"><circle cx="206.30315" cy="351.50146" r="24.803194" fill="yellow" fill-opacity=".5"/><path d="M 223.8416 333.96301 C 233.52788 343.64923 233.52788 359.3537 223.8416 369.03992 C 214.15538 378.7262 198.45091 378.7262 188.76469 369.03992 C 179.07841 359.3537 179.07841 343.64923 188.76469 333.96301 C 198.45091 324.27673 214.15538 324.27673 223.8416 333.96301" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(191.46063 336.00146)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="6.1716213" y="25" textLength="17.341797">B</tspan></text></g><line x1="206.89162" y1="431.00034" x2="206.49045" y2="376.80396" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="206.30315" y1="326.1983" x2="206.30315" y2="291.41638" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id391_Graphic"><circle cx="290.34644" cy="269.6565" r="28.346504" fill="yellow"/><circle cx="290.34644" cy="269.6565" r="28.346504" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(272.66928 254.15652)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="6.1180782" y="25" textLength="23.118164">B’</tspan></text></g><g id="id390_Graphic"><circle cx="290.34644" cy="187.80643" r="28.3465" fill="#ff8000"/><circle cx="290.34644" cy="187.80643" r="28.3465" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(272.66928 172.30643)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="5.400793" y="25" textLength="24.552734">C’</tspan></text></g><line x1="290.34644" y1="322.655" x2="290.34644" y2="298.50296" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="290.3464" y1="240.81003" x2="290.3464" y2="216.65289" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="269.24112" y1="207.47118" x2="232.0592" y2="242.11519" marker-end="url(#FilledDiamond_Marker)" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="1,3"/><line x1="269.68039" y1="289.78198" x2="231.52318" y2="326.9411" marker-end="url(#FilledDiamond_Marker)" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="1,3"/><text transform="translate(154.02574 177.80643)" fill="blue"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="blue" x=".13012695" y="16" textLength="72.739746">Obsolete</tspan></text><g id="id724_Graphic"><path d="M 167.93994 250.66768 C 182.56549 215.87003 217.30066 204.27097 239.23875 230.14594 C 261.17621 256.02005 253.8645 373.79715 241.98106 377.36597 C 230.09801 380.93442 197.19122 394.31873 179.82327 382.71951 C 162.45541 371.1203 153.31494 285.46457 167.93994 250.66768 Z" fill="blue" fill-opacity=".15000001"/><path d="M 167.93994 250.66768 C 182.56549 215.87003 217.30066 204.27097 239.23875 230.14594 C 261.17621 256.02005 253.8645 373.79715 241.98106 377.36597 C 230.09801 380.93442 197.19122 394.31873 179.82327 382.71951 C 162.45541 371.1203 153.31494 285.46457 167.93994 250.66768 Z" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="4,9,1,5"/></g></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-unstable.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="305 746 658 435" width="658pt" height="435pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-18 23:47Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -2 4 4" markerWidth="4" markerHeight="4" color="blue"><g><path d="M 1.6 0 L .8 -.60000002 L 0 0 L .8 .60000002 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="17" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="532.22656" cap-height="719.72656" ascent="770.01953" descent="-229.98047" font-weight="bold"><font-face-src><font-face-name name="Helvetica-Bold"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id672_Graphic" filter="url(#Shadow)"/><use xl:href="#id673_Graphic" filter="url(#Shadow)"/><use xl:href="#id674_Graphic" filter="url(#Shadow)"/><use xl:href="#id680_Graphic" filter="url(#Shadow)"/><use xl:href="#id689_Graphic" filter="url(#Shadow)"/><use xl:href="#id692_Graphic" filter="url(#Shadow)"/><use xl:href="#id702_Graphic" filter="url(#Shadow)"/><use xl:href="#id714_Graphic" filter="url(#Shadow)"/><use xl:href="#id688_Graphic" filter="url(#Shadow)"/></g><g id="id672_Graphic"><rect x="571.2267" y="762.23743" width="371" height="394" fill="white" fill-opacity=".5"/><rect x="571.2267" y="762.23743" width="371" height="394" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id673_Graphic"><rect x="325.28735" y="817.17517" width="160" height="280.84595" fill="white" fill-opacity=".5"/><rect x="325.28735" y="817.17517" width="160" height="280.84595" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id674_Graphic"><circle cx="403.28784" cy="960.2316" r="28.346495" fill="red"/><circle cx="403.28784" cy="960.2316" r="28.346495" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(385.61066 944.73157)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="390.51233" y1="934.3598" x2="373.12732" y2="899.1533" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><path d="M 413.30957 1027.8263 C 418.84457 1033.3613 418.84457 1042.3352 413.30957 1047.87024 C 407.77457 1053.4053 398.80057 1053.4053 393.26556 1047.87024 C 387.73056 1042.3352 387.73056 1033.3613 393.26556 1027.8263 C 398.80057 1022.29126 407.77457 1022.29126 413.30957 1027.8263" fill="black"/><path d="M 413.30957 1027.8263 C 418.84457 1033.3613 418.84457 1042.3352 413.30957 1047.87024 C 407.77457 1053.4053 398.80057 1053.4053 393.26556 1047.87024 C 387.73056 1042.3352 387.73056 1033.3613 393.26556 1027.8263 C 398.80057 1022.29126 407.77457 1022.29126 413.30957 1027.8263" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 413.30957 1073.8263 C 418.84457 1079.3613 418.84457 1088.3352 413.30957 1093.87024 C 407.77457 1099.4053 398.80057 1099.4053 393.26556 1093.87024 C 387.73056 1088.3352 387.73056 1079.3613 393.26556 1073.8263 C 398.80057 1068.29126 407.77457 1068.29126 413.30957 1073.8263" fill="black"/><path d="M 413.30957 1073.8263 C 418.84457 1079.3613 418.84457 1088.3352 413.30957 1093.87024 C 407.77457 1099.4053 398.80057 1099.4053 393.26556 1093.87024 C 387.73056 1088.3352 387.73056 1079.3613 393.26556 1073.8263 C 398.80057 1068.29126 407.77457 1068.29126 413.30957 1073.8263" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="403.1737" y1="1052.5211" x2="403.04446" y2="1069.1771" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id680_Graphic"><circle cx="360.3518" cy="873.2816" r="28.346495" fill="yellow"/><circle cx="360.3518" cy="873.2816" r="28.346495" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(342.67462 857.7816)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g><line x1="403.28772" y1="989.078" x2="403.2876" y2="1023.17505" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="718.2204" y1="930.3955" x2="718.53784" y2="874.86414" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><path d="M 789.4242 1048.76624 C 794.95923 1054.3013 794.95923 1063.27515 789.4242 1068.8102 C 783.8892 1074.3452 774.9152 1074.3452 769.38025 1068.8102 C 763.8452 1063.27515 763.8452 1054.3013 769.38025 1048.76624 C 774.9152 1043.2312 783.8892 1043.2312 789.4242 1048.76624" fill="black"/><path d="M 789.4242 1048.76624 C 794.95923 1054.3013 794.95923 1063.27515 789.4242 1068.8102 C 783.8892 1074.3452 774.9152 1074.3452 769.38025 1068.8102 C 763.8452 1063.27515 763.8452 1054.3013 769.38025 1048.76624 C 774.9152 1043.2312 783.8892 1043.2312 789.4242 1048.76624" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><path d="M 789.4242 1094.7661 C 794.95923 1100.30115 794.95923 1109.275 789.4242 1114.81006 C 783.8892 1120.3451 774.9152 1120.3451 769.38025 1114.81006 C 763.8452 1109.275 763.8452 1100.30115 769.38025 1094.7661 C 774.9152 1089.2311 783.8892 1089.2311 789.4242 1094.7661" fill="black"/><path d="M 789.4242 1094.7661 C 794.95923 1100.30115 794.95923 1109.275 789.4242 1114.81006 C 783.8892 1120.3451 774.9152 1120.3451 769.38025 1114.81006 C 763.8452 1109.275 763.8452 1100.30115 769.38025 1094.7661 C 774.9152 1089.2311 783.8892 1089.2311 789.4242 1094.7661" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="779.4223" y1="1073.4614" x2="779.44507" y2="1090.1149" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id689_Graphic"><circle cx="718.0555" cy="959.24146" r="28.34648" fill="red" fill-opacity=".5"/><path d="M 738.09943 939.19757 C 749.16943 950.2675 749.16943 968.2155 738.09943 979.28546 C 727.0295 990.35547 709.0815 990.35547 698.01154 979.28546 C 686.94153 968.2155 686.94153 950.2675 698.01154 939.19757 C 709.0815 928.12756 727.0295 928.12756 738.09943 939.19757" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(700.37836 943.7415)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="733.1928" y1="983.8047" x2="771.7024" y2="1046.2938" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="800.166" y1="959.2338" x2="756.80194" y2="959.2374" marker-end="url(#FilledDiamond_Marker)" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="1,3"/><g id="id692_Graphic"><circle cx="829.01245" cy="959.23145" r="28.34648" fill="red"/><circle cx="829.01245" cy="959.23145" r="28.34648" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(811.33533 943.73145)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="6.5243282" y="25" textLength="22.305664">A'</tspan></text></g><line x1="816.1477" y1="985.059" x2="785.96265" y2="1045.65894" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><text transform="translate(687.9198 889.0216)" fill="blue"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="blue" x=".13012695" y="16" textLength="72.739746">Obsolete</tspan></text><g id="id702_Graphic"><path d="M 678.60693 819.58557 C 693.37787 801.90887 728.45825 796.0167 750.6144 809.16083 C 772.7699 822.3045 765.3855 882.13373 753.38397 883.94666 C 741.3828 885.75934 708.149 892.5584 690.6084 886.66614 C 673.06793 880.7739 663.8366 837.26196 678.60693 819.58557 Z" fill="#ff8000" fill-opacity=".15000001"/><path d="M 678.60693 819.58557 C 693.37787 801.90887 728.45825 796.0167 750.6144 809.16083 C 772.7699 822.3045 765.3855 882.13373 753.38397 883.94666 C 741.3828 885.75934 708.149 892.5584 690.6084 886.66614 C 673.06793 880.7739 663.8366 837.26196 678.60693 819.58557 Z" stroke="#ff8000" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="4,9,1,5"/></g><text transform="translate(674.40198 774.87)" fill="#ff8000"><tspan font-family="Helvetica" font-size="17" font-weight="bold" fill="#ff8000" x=".103271484" y="16" textLength="71.793457">Unstable</tspan></text><g id="id714_Graphic"><path d="M 679.4558 933.30554 C 694.08136 915.6288 728.81653 909.73657 750.75464 922.88074 C 772.6921 936.0244 765.38037 995.85364 753.49695 997.66656 C 741.6139 999.47925 708.7071 1006.2783 691.33917 1000.3861 C 673.9713 994.49384 664.8308 950.9819 679.4558 933.30554 Z" fill="blue" fill-opacity=".15000001"/><path d="M 679.4558 933.30554 C 694.08136 915.6288 728.81653 909.73657 750.75464 922.88074 C 772.6921 936.0244 765.38037 995.85364 753.49695 997.66656 C 741.6139 999.47925 708.7071 1006.2783 691.33917 1000.3861 C 673.9713 994.49384 664.8308 950.9819 679.4558 933.30554 Z" stroke="blue" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" stroke-dasharray="4,9,1,5"/></g><g id="id688_Graphic"><circle cx="718.0555" cy="845.52167" r="28.34648" fill="yellow"/><path d="M 738.09943 825.47766 C 749.16943 836.5476 749.16943 854.4956 738.09943 865.56555 C 727.0295 876.63556 709.0815 876.63556 698.01154 865.56555 C 686.94153 854.4956 686.94153 836.5476 698.01154 825.47766 C 709.0815 814.40765 727.0295 814.40765 738.09943 825.47766" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" stroke-dasharray="16,9"/><text transform="translate(700.37836 830.0216)" fill="red"><tspan font-family="Helvetica" font-size="26" font-weight="500" fill="red" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/example-1-update.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="47 621 426 249" width="426pt" height="249pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-21 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -3 6 6" markerWidth="6" markerHeight="6" color="red"><g><path d="M 3.7333333 0 L 1.8666667 -1.4 L 0 0 L 1.8666667 1.4 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id464_Graphic" filter="url(#Shadow)"/><use xl:href="#id463_Graphic" filter="url(#Shadow)"/><use xl:href="#id465_Graphic" filter="url(#Shadow)"/><use xl:href="#id466_Graphic" filter="url(#Shadow)"/><use xl:href="#id472_Graphic" filter="url(#Shadow)"/><use xl:href="#id471_Graphic" filter="url(#Shadow)"/><use xl:href="#id475_Graphic" filter="url(#Shadow)"/></g><g id="id464_Graphic"><rect x="67" y="637.9972" width="141" height="207.84595" fill="white" fill-opacity=".5"/><rect x="67" y="637.9972" width="141" height="207.84595" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id463_Graphic"><circle cx="128.71246" cy="718.34644" r="28.346489" fill="red"/><circle cx="128.71246" cy="718.34644" r="28.346489" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(111.035286 702.84644)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="128.93794" y1="770.9975" x2="128.83687" y2="747.19263" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><circle cx="129.000244" cy="785.6706" r="14.173275" fill="black"/><circle cx="129.000244" cy="785.6706" r="14.173275" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><circle cx="129.000244" cy="831.6706" r="14.173275" fill="black"/><circle cx="129.000244" cy="831.6706" r="14.173275" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="128.96199" y1="800.3438" x2="128.91855" y2="816.99756" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id465_Graphic"><rect x="277" y="637.99725" width="176" height="207.84595" fill="white" fill-opacity=".5"/><rect x="277" y="637.99725" width="176" height="207.84595" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id466_Graphic"><circle cx="407.34644" cy="703.95215" r="28.346495" fill="red"/><circle cx="407.34644" cy="703.95215" r="28.346495" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(389.66928 688.45215)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="6.1180782" y="25" textLength="23.118164">A’</tspan></text></g><line x1="378.9428" y1="772.1349" x2="396.34451" y2="730.6261" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id472_Graphic"><circle cx="373.26825" cy="785.67053" r="14.173267" fill="black"/><circle cx="373.26825" cy="785.67053" r="14.173267" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id471_Graphic"><circle cx="373.26825" cy="831.67053" r="14.173267" fill="black"/><circle cx="373.26825" cy="831.67053" r="14.173267" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="373.26837" y1="800.34375" x2="373.26852" y2="816.9973" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="365.1879" y1="773.41925" x2="335.23688" y2="728.00824" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id475_Graphic"><circle cx="319.30762" cy="703.95215" r="28.346495" fill="red" fill-opacity=".5"/><path d="M 339.35156 683.9082 C 350.4216 694.97815 350.4216 712.92615 339.35156 723.9961 C 328.28159 735.0661 310.33365 735.0661 299.26367 723.9961 C 288.19363 712.92615 288.19363 694.97815 299.26367 683.9082 C 310.33365 672.8382 328.28159 672.8382 339.35156 683.9082" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(301.63046 688.45215)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="378.49997" y1="703.95215" x2="362.1041" y2="703.95215" marker-end="url(#FilledDiamond_Marker)" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/example-2-split.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="27 1004 488 344" width="488pt" height="344pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-21 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><font-face font-family="Helvetica" font-size="15" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.316406" slope="0" x-height="522.94922" cap-height="717.28516" ascent="770.01953" descent="-229.98045" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -3 6 6" markerWidth="6" markerHeight="6" color="red"><g><path d="M 3.7333333 0 L 1.8666667 -1.4 L 0 0 L 1.8666667 1.4 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id478_Graphic" filter="url(#Shadow)"/><use xl:href="#id479_Graphic" filter="url(#Shadow)"/><use xl:href="#id481_Graphic" filter="url(#Shadow)"/><use xl:href="#id482_Graphic" filter="url(#Shadow)"/><use xl:href="#id485_Graphic" filter="url(#Shadow)"/><use xl:href="#id488_Graphic" filter="url(#Shadow)"/><use xl:href="#id489_Graphic" filter="url(#Shadow)"/><use xl:href="#id491_Graphic" filter="url(#Shadow)"/><use xl:href="#id492_Graphic" filter="url(#Shadow)"/><use xl:href="#id495_Graphic" filter="url(#Shadow)"/></g><g id="id478_Graphic"><rect x="47" y="1087.16406" width="143" height="202.84595" fill="white" fill-opacity=".5"/><rect x="47" y="1087.16406" width="143" height="202.84595" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id479_Graphic"><circle cx="118.50046" cy="1155.5105" r="28.346527" fill="#ff8000"/><circle cx="118.50046" cy="1155.5105" r="28.346527" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(100.823296 1140.0105)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="118.50029" y1="1215.66406" x2="118.50038" y2="1184.3569" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id481_Graphic"><path d="M 128.52223 1220.3153 C 134.05725 1225.85034 134.05725 1234.8242 128.52223 1240.35925 C 122.98724 1245.8943 114.013245 1245.8943 108.478256 1240.35925 C 102.94323 1234.8242 102.94323 1225.85034 108.478256 1220.3153 C 114.013245 1214.7803 122.98724 1214.7803 128.52223 1220.3153" fill="black"/><path d="M 128.52223 1220.3153 C 134.05725 1225.85034 134.05725 1234.8242 128.52223 1240.35925 C 122.98724 1245.8943 114.013245 1245.8943 108.478256 1240.35925 C 102.94323 1234.8242 102.94323 1225.85034 108.478256 1220.3153 C 114.013245 1214.7803 122.98724 1214.7803 128.52223 1220.3153" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id482_Graphic"><path d="M 128.52223 1265.8153 C 134.05725 1271.35034 134.05725 1280.3242 128.52223 1285.85925 C 122.98724 1291.3943 114.013245 1291.3943 108.478256 1285.85925 C 102.94323 1280.3242 102.94323 1271.35034 108.478256 1265.8153 C 114.013245 1260.2803 122.98724 1260.2803 128.52223 1265.8153" fill="black"/><path d="M 128.52223 1265.8153 C 134.05725 1271.35034 134.05725 1280.3242 128.52223 1285.85925 C 122.98724 1291.3943 114.013245 1291.3943 108.478256 1285.85925 C 102.94323 1280.3242 102.94323 1271.35034 108.478256 1265.8153 C 114.013245 1260.2803 122.98724 1260.2803 128.52223 1265.8153" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="118.500244" y1="1245.0105" x2="118.500244" y2="1261.1641" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id485_Graphic"><rect x="277" y="1020" width="218" height="303.08691" fill="white" fill-opacity=".5"/><rect x="277" y="1020" width="218" height="303.08691" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="384.58325" y1="1251.5149" x2="356.10718" y2="1212.0704" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id488_Graphic"><path d="M 403.19525 1253.3916 C 408.73026 1258.9266 408.73026 1267.9005 403.19525 1273.43555 C 397.66025 1278.9706 388.68625 1278.9706 383.15125 1273.43555 C 377.61624 1267.9005 377.61624 1258.9266 383.15125 1253.3916 C 388.68625 1247.8566 397.66025 1247.8566 403.19525 1253.3916" fill="black"/><path d="M 403.19525 1253.3916 C 408.73026 1258.9266 408.73026 1267.9005 403.19525 1273.43555 C 397.66025 1278.9706 388.68625 1278.9706 383.15125 1273.43555 C 377.61624 1267.9005 377.61624 1258.9266 383.15125 1253.3916 C 388.68625 1247.8566 397.66025 1247.8566 403.19525 1253.3916" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id489_Graphic"><path d="M 403.19525 1298.8916 C 408.73026 1304.4266 408.73026 1313.4005 403.19525 1318.93555 C 397.66025 1324.4706 388.68625 1324.4706 383.15125 1318.93555 C 377.61624 1313.4005 377.61624 1304.4266 383.15125 1298.8916 C 388.68625 1293.3566 397.66025 1293.3566 403.19525 1298.8916" fill="black"/><path d="M 403.19525 1298.8916 C 408.73026 1304.4266 408.73026 1313.4005 403.19525 1318.93555 C 397.66025 1324.4706 388.68625 1324.4706 383.15125 1318.93555 C 377.61624 1313.4005 377.61624 1304.4266 383.15125 1298.8916 C 388.68625 1293.3566 397.66025 1293.3566 403.19525 1298.8916" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="393.17325" y1="1278.0868" x2="393.17325" y2="1294.24036" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id491_Graphic"><circle cx="437.34644" cy="1188.5874" r="28.346533" fill="red"/><circle cx="437.34644" cy="1188.5874" r="28.346533" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(419.66928 1171.0874)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="4.8351192" y="25" textLength="17.341797">A</tspan><tspan font-family="Helvetica" font-size="15" font-weight="500" x="22.176916" y="32" textLength="8.342285">1</tspan></text></g><g id="id492_Graphic"><circle cx="437.34644" cy="1105.34644" r="28.346533" fill="yellow"/><circle cx="437.34644" cy="1105.34644" r="28.346533" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(419.66928 1087.84644)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="4.8351192" y="25" textLength="17.341797">A</tspan><tspan font-family="Helvetica" font-size="15" font-weight="500" x="22.176916" y="32" textLength="8.342285">2</tspan></text></g><line x1="400.63443" y1="1250.7749" x2="422.67834" y2="1213.4341" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="437.3464" y1="1159.741" x2="437.3464" y2="1134.1929" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id495_Graphic"><circle cx="339.34644" cy="1188.5874" r="28.346533" fill="#ff8000"/><path d="M 359.39038 1168.54346 C 370.46042 1179.6134 370.46042 1197.5614 359.39038 1208.63135 C 348.3204 1219.7014 330.37247 1219.7014 319.30249 1208.63135 C 308.23245 1197.5614 308.23245 1179.6134 319.30249 1168.54346 C 330.37247 1157.4734 348.3204 1157.4734 359.39038 1168.54346" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(321.66928 1173.0874)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="393.50046" y1="1152" x2="374.81244" y2="1164.626" marker-end="url(#FilledDiamond_Marker)" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/><line x1="417.8144" y1="1126.5746" x2="393.50046" y2="1153" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/><line x1="416.74606" y1="1168.3948" x2="399" y2="1151" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/simple-3-merge.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="27 1677 497 345" width="497pt" height="345pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-21 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -3 6 6" markerWidth="6" markerHeight="6" color="red"><g><path d="M 3.7333333 0 L 1.8666667 -1.4 L 0 0 L 1.8666667 1.4 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id500_Graphic" filter="url(#Shadow)"/><use xl:href="#id501_Graphic" filter="url(#Shadow)"/><use xl:href="#id503_Graphic" filter="url(#Shadow)"/><use xl:href="#id504_Graphic" filter="url(#Shadow)"/><use xl:href="#id507_Graphic" filter="url(#Shadow)"/><use xl:href="#id509_Graphic" filter="url(#Shadow)"/><use xl:href="#id510_Graphic" filter="url(#Shadow)"/><use xl:href="#id516_Graphic" filter="url(#Shadow)"/><use xl:href="#id284_Graphic" filter="url(#Shadow)"/><use xl:href="#id520_Graphic" filter="url(#Shadow)"/><use xl:href="#id523_Graphic" filter="url(#Shadow)"/></g><g id="id500_Graphic"><rect x="47" y="1726.5562" width="143" height="260.96387" fill="white" fill-opacity=".5"/><rect x="47" y="1726.5562" width="143" height="260.96387" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id501_Graphic"><circle cx="118.50044" cy="1853.0206" r="28.346527" fill="red"/><circle cx="118.50044" cy="1853.0206" r="28.346527" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(100.82328 1837.5206)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="118.50028" y1="1913.1742" x2="118.500366" y2="1881.8672" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id503_Graphic"><path d="M 128.52223 1917.8254 C 134.05725 1923.3605 134.05725 1932.3344 128.52223 1937.8694 C 122.98724 1943.4044 114.013245 1943.4044 108.478256 1937.8694 C 102.94323 1932.3344 102.94323 1923.3605 108.478256 1917.8254 C 114.013245 1912.2904 122.98724 1912.2904 128.52223 1917.8254" fill="black"/><path d="M 128.52223 1917.8254 C 134.05725 1923.3605 134.05725 1932.3344 128.52223 1937.8694 C 122.98724 1943.4044 114.013245 1943.4044 108.478256 1937.8694 C 102.94323 1932.3344 102.94323 1923.3605 108.478256 1917.8254 C 114.013245 1912.2904 122.98724 1912.2904 128.52223 1917.8254" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id504_Graphic"><path d="M 128.52223 1963.3254 C 134.05725 1968.8605 134.05725 1977.8344 128.52223 1983.3694 C 122.98724 1988.9044 114.013245 1988.9044 108.478256 1983.3694 C 102.94323 1977.8344 102.94323 1968.8605 108.478256 1963.3254 C 114.013245 1957.7904 122.98724 1957.7904 128.52223 1963.3254" fill="black"/><path d="M 128.52223 1963.3254 C 134.05725 1968.8605 134.05725 1977.8344 128.52223 1983.3694 C 122.98724 1988.9044 114.013245 1988.9044 108.478256 1983.3694 C 102.94323 1977.8344 102.94323 1968.8605 108.478256 1963.3254 C 114.013245 1957.7904 122.98724 1957.7904 128.52223 1963.3254" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="118.500244" y1="1942.5206" x2="118.500244" y2="1958.6742" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id507_Graphic"><rect x="285.5" y="1693.958" width="218" height="303.08691" fill="white" fill-opacity=".5"/><rect x="285.5" y="1693.958" width="218" height="303.08691" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="393.10345" y1="1925.458" x2="364.69406" y2="1885.965" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id509_Graphic"><path d="M 411.69525 1927.3492 C 417.23026 1932.8843 417.23026 1941.8582 411.69525 1947.3932 C 406.16025 1952.9282 397.18625 1952.9282 391.65125 1947.3932 C 386.11624 1941.8582 386.11624 1932.8843 391.65125 1927.3492 C 397.18625 1921.8142 406.16025 1921.8142 411.69525 1927.3492" fill="black"/><path d="M 411.69525 1927.3492 C 417.23026 1932.8843 417.23026 1941.8582 411.69525 1947.3932 C 406.16025 1952.9282 397.18625 1952.9282 391.65125 1947.3932 C 386.11624 1941.8582 386.11624 1932.8843 391.65125 1927.3492 C 397.18625 1921.8142 406.16025 1921.8142 411.69525 1927.3492" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id510_Graphic"><path d="M 411.69525 1972.8492 C 417.23026 1978.3843 417.23026 1987.3582 411.69525 1992.8932 C 406.16025 1998.4282 397.18625 1998.4282 391.65125 1992.8932 C 386.11624 1987.3582 386.11624 1978.3843 391.65125 1972.8492 C 397.18625 1967.3142 406.16025 1967.3142 411.69525 1972.8492" fill="black"/><path d="M 411.69525 1972.8492 C 417.23026 1978.3843 417.23026 1987.3582 411.69525 1992.8932 C 406.16025 1998.4282 397.18625 1998.4282 391.65125 1992.8932 C 386.11624 1987.3582 386.11624 1978.3843 391.65125 1972.8492 C 397.18625 1967.3142 406.16025 1967.3142 411.69525 1972.8492" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="401.67789" y1="1952.0444" x2="401.68301" y2="1968.198" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id516_Graphic"><circle cx="347.84644" cy="1862.5444" r="28.346533" fill="red" fill-opacity=".5"/><path d="M 367.89038 1842.5005 C 378.96042 1853.5704 378.96042 1871.5184 367.89038 1882.5884 C 356.8204 1893.6584 338.87247 1893.6584 327.8025 1882.5884 C 316.73245 1871.5184 316.73245 1853.5704 327.8025 1842.5005 C 338.87247 1831.4304 356.8204 1831.4304 367.89038 1842.5005" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(330.16928 1847.0444)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="427.0911" y1="1818.7612" x2="385.31204" y2="1841.8445" marker-end="url(#FilledDiamond_Marker)" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/><line x1="118.50028" y1="1824.1741" x2="118.50015" y2="1800.5271" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id284_Graphic"><circle cx="118.5" cy="1771.6807" r="28.346527" fill="yellow"/><circle cx="118.5" cy="1771.6807" r="28.346527" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(100.82284 1756.1807)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g><g id="id520_Graphic"><circle cx="347.84644" cy="1754.9026" r="28.346533" fill="yellow" fill-opacity=".5"/><path d="M 367.89038 1734.8586 C 378.96042 1745.9286 378.96042 1763.8766 367.89038 1774.9465 C 356.8204 1786.0166 338.87247 1786.0166 327.8025 1774.9465 C 316.73245 1763.8766 316.73245 1745.9286 327.8025 1734.8586 C 338.87247 1723.7886 356.8204 1723.7886 367.89038 1734.8586" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(330.16928 1739.4026)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g><line x1="347.84897" y1="1833.698" x2="347.85342" y2="1783.749" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="406.91367" y1="1923.662" x2="442.04407" y2="1831.7589" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id523_Graphic"><circle cx="452.34644" cy="1804.8074" r="28.346533" fill="#ff8000"/><circle cx="452.34644" cy="1804.8074" r="28.346533" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(434.66928 1789.3074)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="8.2889767" y="25" textLength="18.776367">C</tspan></text></g><line x1="426.309" y1="1792.373" x2="386.4721" y2="1773.3486" marker-end="url(#FilledDiamond_Marker)" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/simple-4-reorder.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="27 2166 497 345" width="497pt" height="345pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-21 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -3 6 6" markerWidth="6" markerHeight="6" color="red"><g><path d="M 3.7333333 0 L 1.8666667 -1.4 L 0 0 L 1.8666667 1.4 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id532_Graphic" filter="url(#Shadow)"/><use xl:href="#id525_Graphic" filter="url(#Shadow)"/><use xl:href="#id526_Graphic" filter="url(#Shadow)"/><use xl:href="#id528_Graphic" filter="url(#Shadow)"/><use xl:href="#id529_Graphic" filter="url(#Shadow)"/><use xl:href="#id534_Graphic" filter="url(#Shadow)"/><use xl:href="#id535_Graphic" filter="url(#Shadow)"/><use xl:href="#id537_Graphic" filter="url(#Shadow)"/><use xl:href="#id540_Graphic" filter="url(#Shadow)"/><use xl:href="#id541_Graphic" filter="url(#Shadow)"/><use xl:href="#id546_Graphic" filter="url(#Shadow)"/><use xl:href="#id549_Graphic" filter="url(#Shadow)"/></g><g id="id532_Graphic"><rect x="285.5" y="2182.9602" width="218" height="303.08691" fill="white" fill-opacity=".5"/><rect x="285.5" y="2182.9602" width="218" height="303.08691" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="427.7653" y1="2330.3635" x2="376.8968" y2="2275.3335" marker-end="url(#FilledDiamond_Marker)" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/><g id="id525_Graphic"><rect x="47" y="2215.56" width="143" height="260.96387" fill="white" fill-opacity=".5"/><rect x="47" y="2215.56" width="143" height="260.96387" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id526_Graphic"><path d="M 138.5444 2321.9729 C 149.61443 2333.043 149.61443 2350.9907 138.5444 2362.0608 C 127.47443 2373.1309 109.526474 2373.1309 98.456505 2362.0608 C 87.386475 2350.9907 87.386475 2333.043 98.456505 2321.9729 C 109.526474 2310.9028 127.47443 2310.9028 138.5444 2321.9729" fill="red"/><path d="M 138.5444 2321.9729 C 149.61443 2333.043 149.61443 2350.9907 138.5444 2362.0608 C 127.47443 2373.1309 109.526474 2373.1309 98.456505 2362.0608 C 87.386475 2350.9907 87.386475 2333.043 98.456505 2321.9729 C 109.526474 2310.9028 127.47443 2310.9028 138.5444 2321.9729" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(100.82329 2326.5168)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="118.50029" y1="2402.1704" x2="118.500374" y2="2370.8633" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id528_Graphic"><path d="M 128.52225 2406.8218 C 134.05727 2412.3567 134.05727 2421.3308 128.52225 2426.8657 C 122.98724 2432.4009 114.01326 2432.4009 108.478264 2426.8657 C 102.94324 2421.3308 102.94324 2412.3567 108.478264 2406.8218 C 114.01326 2401.2866 122.98724 2401.2866 128.52225 2406.8218" fill="black"/><path d="M 128.52225 2406.8218 C 134.05727 2412.3567 134.05727 2421.3308 128.52225 2426.8657 C 122.98724 2432.4009 114.01326 2432.4009 108.478264 2426.8657 C 102.94324 2421.3308 102.94324 2412.3567 108.478264 2406.8218 C 114.01326 2401.2866 122.98724 2401.2866 128.52225 2406.8218" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id529_Graphic"><path d="M 128.52225 2452.3218 C 134.05727 2457.8567 134.05727 2466.8308 128.52225 2472.3657 C 122.98724 2477.9009 114.01326 2477.9009 108.478264 2472.3657 C 102.94324 2466.8308 102.94324 2457.8567 108.478264 2452.3218 C 114.01326 2446.7866 122.98724 2446.7866 128.52225 2452.3218" fill="black"/><path d="M 128.52225 2452.3218 C 134.05727 2457.8567 134.05727 2466.8308 128.52225 2472.3657 C 122.98724 2477.9009 114.01326 2477.9009 108.478264 2472.3657 C 102.94324 2466.8308 102.94324 2457.8567 108.478264 2452.3218 C 114.01326 2446.7866 122.98724 2446.7866 128.52225 2452.3218" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="118.50025" y1="2431.5171" x2="118.50025" y2="2447.6704" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="393.10349" y1="2414.4607" x2="364.69394" y2="2374.9673" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id534_Graphic"><path d="M 411.69525 2416.352 C 417.23026 2421.887 417.23026 2430.8611 411.69525 2436.396 C 406.16025 2441.9312 397.18625 2441.9312 391.65125 2436.396 C 386.11624 2430.8611 386.11624 2421.887 391.65125 2416.352 C 397.18625 2410.8169 406.16025 2410.8169 411.69525 2416.352" fill="black"/><path d="M 411.69525 2416.352 C 417.23026 2421.887 417.23026 2430.8611 411.69525 2436.396 C 406.16025 2441.9312 397.18625 2441.9312 391.65125 2436.396 C 386.11624 2430.8611 386.11624 2421.887 391.65125 2416.352 C 397.18625 2410.8169 406.16025 2410.8169 411.69525 2416.352" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id535_Graphic"><path d="M 411.69525 2461.852 C 417.23026 2467.387 417.23026 2476.3611 411.69525 2481.896 C 406.16025 2487.4312 397.18625 2487.4312 391.65125 2481.896 C 386.11624 2476.3611 386.11624 2467.387 391.65125 2461.852 C 397.18625 2456.3169 406.16025 2456.3169 411.69525 2461.852" fill="black"/><path d="M 411.69525 2461.852 C 417.23026 2467.387 417.23026 2476.3611 411.69525 2481.896 C 406.16025 2487.4312 397.18625 2487.4312 391.65125 2481.896 C 386.11624 2476.3611 386.11624 2467.387 391.65125 2461.852 C 397.18625 2456.3169 406.16025 2456.3169 411.69525 2461.852" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="401.67325" y1="2441.0474" x2="401.67325" y2="2457.2007" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id537_Graphic"><path d="M 367.89038 2331.5027 C 378.96042 2342.5728 378.96042 2360.5205 367.89038 2371.5906 C 356.8204 2382.6606 338.87247 2382.6606 327.8025 2371.5906 C 316.73245 2360.5205 316.73245 2342.5728 327.8025 2331.5027 C 338.87247 2320.4326 356.8204 2320.4326 367.89038 2331.5027" fill="red" fill-opacity=".5"/><path d="M 367.89038 2331.5027 C 378.96042 2342.5728 378.96042 2360.5205 367.89038 2371.5906 C 356.8204 2382.6606 338.87247 2382.6606 327.8025 2371.5906 C 316.73245 2360.5205 316.73245 2342.5728 327.8025 2331.5027 C 338.87247 2320.4326 356.8204 2320.4326 367.89038 2331.5027" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(330.16928 2336.0466)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="118.500275" y1="2313.1704" x2="118.50013" y2="2289.523" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id540_Graphic"><path d="M 138.5439 2240.6326 C 149.61392 2251.7026 149.61392 2269.6504 138.5439 2280.7205 C 127.47393 2291.7905 109.52598 2291.7905 98.45601 2280.7205 C 87.38598 2269.6504 87.38598 2251.7026 98.45601 2240.6326 C 109.52598 2229.5625 127.47393 2229.5625 138.5439 2240.6326" fill="yellow"/><path d="M 138.5439 2240.6326 C 149.61392 2251.7026 149.61392 2269.6504 138.5439 2280.7205 C 127.47393 2291.7905 109.52598 2291.7905 98.45601 2280.7205 C 87.38598 2269.6504 87.38598 2251.7026 98.45601 2240.6326 C 109.52598 2229.5625 127.47393 2229.5625 138.5439 2240.6326" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(100.82279 2245.1765)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g><g id="id541_Graphic"><path d="M 367.89038 2223.8625 C 378.96042 2234.9326 378.96042 2252.8804 367.89038 2263.9504 C 356.8204 2275.0205 338.87247 2275.0205 327.8025 2263.9504 C 316.73245 2252.8804 316.73245 2234.9326 327.8025 2223.8625 C 338.87247 2212.7925 356.8204 2212.7925 367.89038 2223.8625" fill="yellow" fill-opacity=".5"/><path d="M 367.89038 2223.8625 C 378.96042 2234.9326 378.96042 2252.8804 367.89038 2263.9504 C 356.8204 2275.0205 338.87247 2275.0205 327.8025 2263.9504 C 316.73245 2252.8804 316.73245 2234.9326 327.8025 2223.8625 C 338.87247 2212.7925 356.8204 2212.7925 367.89038 2223.8625" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(330.16928 2228.4065)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">B</tspan></text></g><line x1="347.84644" y1="2322.7002" x2="347.84644" y2="2272.7529" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id546_Graphic"><path d="M 467.39038 2218.6619 C 478.46042 2229.732 478.46042 2247.6797 467.39038 2258.7498 C 456.3204 2269.8198 438.37247 2269.8198 427.3025 2258.7498 C 416.23245 2247.6797 416.23245 2229.732 427.3025 2218.6619 C 438.37247 2207.5918 456.3204 2207.5918 467.39038 2218.6619" fill="red"/><path d="M 467.39038 2218.6619 C 478.46042 2229.732 478.46042 2247.6797 467.39038 2258.7498 C 456.3204 2269.8198 438.37247 2269.8198 427.3025 2258.7498 C 416.23245 2247.6797 416.23245 2229.732 427.3025 2218.6619 C 438.37247 2207.5918 456.3204 2207.5918 467.39038 2218.6619" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(429.66928 2223.2058)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="6.5243282" y="25" textLength="22.305664">A'</tspan></text></g><line x1="409.35617" y1="2413.8691" x2="432.46094" y2="2376.2634" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><line x1="447.3466" y1="2267.5522" x2="447.34692" y2="2322.7002" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id549_Graphic"><path d="M 467.39038 2331.5027 C 478.46042 2342.5728 478.46042 2360.5205 467.39038 2371.5906 C 456.3204 2382.6606 438.37247 2382.6606 427.3025 2371.5906 C 416.23245 2360.5205 416.23245 2342.5728 427.3025 2331.5027 C 438.37247 2320.4326 456.3204 2320.4326 467.39038 2331.5027" fill="yellow"/><path d="M 467.39038 2331.5027 C 478.46042 2342.5728 478.46042 2360.5205 467.39038 2371.5906 C 456.3204 2382.6606 438.37247 2382.6606 427.3025 2371.5906 C 416.23245 2360.5205 416.23245 2342.5728 427.3025 2331.5027 C 438.37247 2320.4326 456.3204 2320.4326 467.39038 2331.5027" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(429.66928 2336.0466)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="6.5243282" y="25" textLength="22.305664">B'</tspan></text></g><line x1="428.2673" y1="2260.343" x2="376.15155" y2="2319.4465" marker-end="url(#FilledDiamond_Marker)" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/simple-5-delete.svg Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,3 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="66 2640 420 262" width="35pc" height="262pt"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:date>2012-03-21 08:32Z</dc:date><!-- Produced by OmniGraffle Professional 5.3.6 --></metadata><defs><filter id="Shadow" filterUnits="userSpaceOnUse"><feGaussianBlur in="SourceAlpha" result="blur" stdDeviation="3.488"/><feOffset in="blur" result="offset" dx="0" dy="4"/><feFlood flood-color="black" flood-opacity=".75" result="flood"/><feComposite in="flood" in2="offset" operator="in"/></filter><font-face font-family="Helvetica" font-size="26" units-per-em="1000" underline-position="-75.683594" underline-thickness="49.31641" slope="0" x-height="522.94922" cap-height="717.2852" ascent="770.0196" descent="-229.98048" font-weight="500"><font-face-src><font-face-name name="Helvetica"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="FilledDiamond_Marker" viewBox="-1 -3 6 6" markerWidth="6" markerHeight="6" color="red"><g><path d="M 3.7333333 0 L 1.8666667 -1.4 L 0 0 L 1.8666667 1.4 Z" fill="currentColor" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="Ball_Marker" viewBox="-4 -3 5 6" markerWidth="5" markerHeight="6" color="red"><g><circle cx="-1.3999994" cy="0" r="1.3999988" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><g><use xl:href="#id575_Graphic" filter="url(#Shadow)"/><use xl:href="#id554_Graphic" filter="url(#Shadow)"/><use xl:href="#id555_Graphic" filter="url(#Shadow)"/><use xl:href="#id557_Graphic" filter="url(#Shadow)"/><use xl:href="#id558_Graphic" filter="url(#Shadow)"/><use xl:href="#id562_Graphic" filter="url(#Shadow)"/><use xl:href="#id563_Graphic" filter="url(#Shadow)"/><use xl:href="#id565_Graphic" filter="url(#Shadow)"/></g><g id="id575_Graphic"><rect x="322.92328" y="2656" width="143" height="221.08398" fill="white" fill-opacity=".5"/><rect x="322.92328" y="2656" width="143" height="221.08398" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id554_Graphic"><rect x="86" y="2670.28" width="143" height="192.52393" fill="white" fill-opacity=".5"/><rect x="86" y="2670.28" width="143" height="192.52393" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id555_Graphic"><path d="M 177.54439 2708.2524 C 188.61441 2719.3225 188.61441 2737.2703 177.54439 2748.3403 C 166.47443 2759.4104 148.52646 2759.4104 137.4565 2748.3403 C 126.38647 2737.2703 126.38647 2719.3225 137.4565 2708.2524 C 148.52646 2697.1824 166.47443 2697.1824 177.54439 2708.2524" fill="red"/><path d="M 177.54439 2708.2524 C 188.61441 2719.3225 188.61441 2737.2703 177.54439 2748.3403 C 166.47443 2759.4104 148.52646 2759.4104 137.4565 2748.3403 C 126.38647 2737.2703 126.38647 2719.3225 137.4565 2708.2524 C 148.52646 2697.1824 166.47443 2697.1824 177.54439 2708.2524" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><text transform="translate(139.82329 2712.7964)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="157.50027" y1="2788.45" x2="157.50037" y2="2757.1428" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id557_Graphic"><path d="M 167.52223 2793.1013 C 173.05725 2798.6362 173.05725 2807.6104 167.52223 2813.1453 C 161.98724 2818.6804 153.013245 2818.6804 147.47826 2813.1453 C 141.94324 2807.6104 141.94324 2798.6362 147.47826 2793.1013 C 153.013245 2787.5662 161.98724 2787.5662 167.52223 2793.1013" fill="black"/><path d="M 167.52223 2793.1013 C 173.05725 2798.6362 173.05725 2807.6104 167.52223 2813.1453 C 161.98724 2818.6804 153.013245 2818.6804 147.47826 2813.1453 C 141.94324 2807.6104 141.94324 2798.6362 147.47826 2793.1013 C 153.013245 2787.5662 161.98724 2787.5662 167.52223 2793.1013" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id558_Graphic"><path d="M 167.52223 2838.6013 C 173.05725 2844.1362 173.05725 2853.1104 167.52223 2858.6453 C 161.98724 2864.1804 153.013245 2864.1804 147.47826 2858.6453 C 141.94324 2853.1104 141.94324 2844.1362 147.47826 2838.6013 C 153.013245 2833.0662 161.98724 2833.0662 167.52223 2838.6013" fill="black"/><path d="M 167.52223 2838.6013 C 173.05725 2844.1362 173.05725 2853.1104 167.52223 2858.6453 C 161.98724 2864.1804 153.013245 2864.1804 147.47826 2858.6453 C 141.94324 2853.1104 141.94324 2844.1362 147.47826 2838.6013 C 153.013245 2833.0662 161.98724 2833.0662 167.52223 2838.6013" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="157.50024" y1="2817.7966" x2="157.50024" y2="2833.95" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><line x1="394.42325" y1="2798.7" x2="394.42328" y2="2764.2998" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="3"/><g id="id562_Graphic"><path d="M 404.44525 2803.3513 C 409.98026 2808.8862 409.98026 2817.8604 404.44525 2823.3953 C 398.91025 2828.9304 389.93625 2828.9304 384.40125 2823.3953 C 378.86624 2817.8604 378.86624 2808.8862 384.40125 2803.3513 C 389.93625 2797.8162 398.91025 2797.8162 404.44525 2803.3513" fill="black"/><path d="M 404.44525 2803.3513 C 409.98026 2808.8862 409.98026 2817.8604 404.44525 2823.3953 C 398.91025 2828.9304 389.93625 2828.9304 384.40125 2823.3953 C 378.86624 2817.8604 378.86624 2808.8862 384.40125 2803.3513 C 389.93625 2797.8162 398.91025 2797.8162 404.44525 2803.3513" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><g id="id563_Graphic"><path d="M 404.44525 2848.8513 C 409.98026 2854.3862 409.98026 2863.3604 404.44525 2868.8953 C 398.91025 2874.4304 389.93625 2874.4304 384.40125 2868.8953 C 378.86624 2863.3604 378.86624 2854.3862 384.40125 2848.8513 C 389.93625 2843.3162 398.91025 2843.3162 404.44525 2848.8513" fill="black"/><path d="M 404.44525 2848.8513 C 409.98026 2854.3862 409.98026 2863.3604 404.44525 2868.8953 C 398.91025 2874.4304 389.93625 2874.4304 384.40125 2868.8953 C 378.86624 2863.3604 378.86624 2854.3862 384.40125 2848.8513 C 389.93625 2843.3162 398.91025 2843.3162 404.44525 2848.8513" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/></g><line x1="394.42325" y1="2828.0466" x2="394.42325" y2="2844.2" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1"/><g id="id565_Graphic"><path d="M 414.46722 2715.4094 C 425.53726 2726.4795 425.53726 2744.4272 414.46722 2755.4973 C 403.39725 2766.5674 385.4493 2766.5674 374.37933 2755.4973 C 363.3093 2744.4272 363.3093 2726.4795 374.37933 2715.4094 C 385.4493 2704.3394 403.39725 2704.3394 414.46722 2715.4094" fill="red" fill-opacity=".5"/><path d="M 414.46722 2715.4094 C 425.53726 2726.4795 425.53726 2744.4272 414.46722 2755.4973 C 403.39725 2766.5674 385.4493 2766.5674 374.37933 2755.4973 C 363.3093 2744.4272 363.3093 2726.4795 374.37933 2715.4094 C 385.4493 2704.3394 403.39725 2704.3394 414.46722 2715.4094" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1" stroke-dasharray="8,5"/><text transform="translate(376.7461 2719.9534)" fill="black"><tspan font-family="Helvetica" font-size="26" font-weight="500" x="9.006262" y="25" textLength="17.341797">A</tspan></text></g><line x1="442.8926" y1="2681.3723" x2="422.98685" y2="2703.5828" marker-end="url(#FilledDiamond_Marker)" marker-start="url(#Ball_Marker)" stroke="red" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" stroke-dasharray="1,4"/></g></g></svg>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/from-mq.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,172 @@ +------------------------------------------- +From MQ To Evolve, The Refugee Book +------------------------------------------- + +Cheat sheet +------------- + +============================== ============================================ +mq command new equivalent +============================== ============================================ +qseries ``log`` +qnew ``commit`` +qrefresh ``amend`` +qpop ``update`` or ``qdown`` +qpush ``update`` or ``gup`` sometimes ``stabilize`` +qrm ``prune`` +qfold ``amend -c`` (for now, ``collapse`` soon) +qdiff ``odiff`` + +qfinish -- +qimport -- +============================== ============================================ + + +Replacement details +--------------------- + +hg qseries +``````````` + +All your work in progress is now in real changesets all the time. + +You can use the standard log command to display them. You can use the +phase revset to display unfinished work only, and use templates to have +the same kind of compact that the output of qseries has. + +This will result in something like that:: + + [alias] + wip = log -r 'not public()' --template='{rev}:{node|short} {desc|firstline}\n' + +hg qnew +```````` + +With evolve you handle standard changesets without an additional overlay. + +Standard changeset are created using hg commit as usual.:: + + $ hg commit + +If you want to keep the "WIP is not pushed" behavior, you want to +set your changeset in the secret phase using the phase command. + +Note that you only need it for the first commit you want to be secret. Later +commits will inherit their parents phase. + +If you always want your new commit to be in the secret phase, your should +consider updating your configuration: + + [phases] + new-commit=secret + +hg qref +```````` + +A new command from evolution will allow you to rewrite the changeset you are +currently on. Just call: + + $ hg amend + +This command takes the same options as commit, plus the switch '-e' (--edit) +to edit the commit message in an editor. + +The amend command also has a -c switch which allow you to make an +explicit amending commit before rewriting a changeset.:: + + $ hg record -m 'feature A' + # oups, I forget some stuff + $ hg record babar.py + $ hg amend -c .^ # .^ refer to "working directoy parent, here 'feature A' + +note: refresh is an alias for amend + +hg qref -X +```````````` + +To remove changes from you current commit use:: + + $ hg uncommit not-ready.txt + + +hg qpop +````````` + +The following command emulate the behavior of hg qpop: + + $ hg gdown + +If you need to go back to an arbitrary commit you can use: + + $ hg update + +.. note:: gdown and update allow movement with working directory changes applied + and gracefully merge them. + +hg qpush +```````` + +When you rewrite changesets, descendants of rewritten changesets are marked as +"out of sync". You need to rewrite them on top of the new version of their +ancestor. + +The evolution extension adds a command to rewrite the "out of sync" +changesets::: + + $ hg stabilize + +You can also decide to do it manually using:: + + $ hg graft -O <old-version> + +or:: + + $ hg rebase -r <revset for old version> -d . + +note: using graft allows you to pick the changeset you want next as the --move +option of qpush do. + + +hg qrm +``````` + +evolution introduce a new command to mark a changeset as "not wanted anymore".:: + + $ hg prune <revset> + +hg qfold +````````` + + +:: + + $ hg up <top changeset> + $ amend --edit -c <bottom changeset> + + +or later:: + + $ hg collapse # XXX not implemented + $ hg rebase --collapse # XXX not tested + + +hg qdiff +````````` + +``odiff`` is an alias for `hg diff -r .^` it works as qdiff, but outside mq. + + + +hg qfinish and hg qimport +```````````````````````````` + +These are not necessary anymore. If you want to control exchange and +mutability of changesets, see the phase feature + + + +hg qcommit +``````````````` + +If you really need to send patches through versioned mq patches, you should +look at the qsync extension.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/index.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,166 @@ +======================================== +Safe Mutable History +======================================== + + +Here are various materials on planned improvement to Mercurial regarding +rewriting history. + +First read about what challenge arise while rewriting history and how we plan to +solve them once and for all. + +.. toctree:: + :maxdepth: 2 + + instability + +The effort is split in two parts: + + * The **obsolete marker** concept aims to provide an alternative to ``strip`` + to get rid of changesets. + + * The **evolve** mercurial extension rewrites history using obsolete + *marker* under the hood. + +The first and most important step is by far the **obsolete marker**. However +most users will never be directly exposed to the concept. For this reason +this manual starts with changeset evolution. + +Evolve: A robust alternative to MQ +==================================== + +Evolve is an experimental history rewriting extension that uses obsolete +markers. It is inspired from MQ and pbranch but have multiple advantages over +them: + +* Focus on your current work. + + You can focus your work on a single changeset and take care of adapting + descendent changeset later. + +* Handle **non-linear history with branches and merges** + +* Rely internally on **robust merge** mechanism of mercurial. + + Simple conflict are handled by real merge tools using appropriate ancestor. + Conflict are much rarer and much more user friendly. + +* Mutable history **fully available all the time**. + + always use 'hg update' and forget about (un)applying patches to access the + mutable part of your history. + + +* Use only **plain changeset** and forget about patches. Evole will create and + exchange real changesets. Mutable history can be used in all usual operations: + pull, push, log, diff, etc. + +* Allow **sharing and collaboration** mutable history without fear of duplicate + (thanks to obsolete marker). + +* Cover all mq usage but guard. + +.. warning:: The evolve extention and the obsolete marker are at an experimental + stage. While using obsolete you'll likely be exposed to complex + implication of the **obsolete marker** concept. I do not recommend + non-power user to test this at this stage. + + XXX make sure to read the XXX section before using it. + + Production ready version should hide such details to normal user. + +To enable the evolve extension use:: + + $ hg clone https://bitbucket.org/marmoute/mutable-history -u stable + $ mutable-history/enable.sh >> ~/.hgrc + +You will probably want to use the associated version of hgview (qt viewer +recommended). :: + + $ hg clone http://hg-lab.logilab.org/wip/hgview/ -u obsolete + $ cd hgview + $ python setup.py install --user + +Works with mercurial 2.2 + + --- + +For more information see documents below: + +.. toctree:: + :maxdepth: 1 + + tutorials/tutorial + evolve-good-practice + evolve-faq + from-mq + evolve-collaboration + qsync + +Smart changeset deletion: Obsolete Marker +========================================== + +Obsolete marker is a powerful concept that allow mercurial to safely handle +history rewriting operations. It is a new type of relation between Mercurial +changesets that track the result of history rewriting operations. + +This concept is simple to define and provides a very solid base to: + +- Very fast history rewriting operations, + +- auditable and reversible history rewriting process, + +- clean final history, + +- share and collaborate on mutable parts of the history, + +- gracefully handle history rewriting conflicts, + +- allow various history rewriting UI to collaborate with a underlying common API. + + --- + +For more information see documents below + +.. toctree:: + :maxdepth: 1 + + obs-concept + obs-terms + obs-implementation + obs-road-map + + +Known limitation and bug +================================= + +Here is a list of know issue that will be fixed later: + + +* ``hg stabilize`` does not handle merge conflict. + + You must fallback to graft or rebase when that happen. + +* rewriting conflict are not detected yet``hg stabilize`` does not + handle them. + +* ``hg update`` can move an obsolete parent + +* you need to provide to `graft --continue -O` if you started you + graft using `-O`. + +* ``hg merge`` considers an extinct head to be a valid target, hence requiring + you to manually specify target all the time. + +* trying to exchange obsolete marker with a static http repo will crash. + +* trying to exchange a lot of obsolete markers through http crash. + +* Extinct changesets are turned secret by various commands. + +* Extinct changesets are hidden using the *hidden* feature of mercurial only + supported by a few commands. + + Only ``hg log``, ``hgview`` and `hg glog` support it. ``hg head`` or other visual viewer don't. + +* hg heads show extinct changeset
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/instability.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,221 @@ + +----------------------------------- +The instability Principle +----------------------------------- + + + +An intrinsic contradiction +----------------------------------- + +XXX starts by talking about getting ride of changeset. + +DVCSes bring two new major concepts to the Version Control Scene: + + * History is organized as a robust DAG, + * History can be rewritten. + +However, the two concepts are in contradiction: + +To achieve a robust history, three key elements are gathered in *changesets*: + + * Full snapshot of the versioned content, + * Reference to the previous full snapshot used to build the new one, + * A description of the change who lead from the old content to the new old. + +All three elements are to compute a *unique* hash that identify the changeset +(with various other metadata). This identification is a key part of DVCS design. + +This is a very useful property because Changing B parent means +changing B content too. This requires the creation of **another** +changeset, which is semantically good. + +:: + + Schema base, A, B and B' + +To avoid duplication, the older changeset is usually discarded from accessible +history. I'm calling them *obsolete* changesets. + + +But rewriting a changeset with children does not change these +children's parent! And because children of the rewritten changeset +still **depend** on the older "dead" version of the changeset with +can not get rid of this dead version. + +:: + + Schema base, A and A' and B. + +I'm calling these children **unstable** because they are based on a +dead changeset and prevent people to get rid of it. + +This instability is an **unavoidable consequence** of the strict dependency of +changeset. History Rewriting history alway need to take it in account and +provide a way to rewrite the descendant on the new changeset to avoid +coexistence of the old and new version of a rewritten changeset. + + +Everybody is working around the issue +------------------------------------------------ + +I'm not claiming that rewriting history is impossible. People are successfully +doing for years. However they all need to work around *instability*. Several +work around strategy exists. + + +Rewriting all at once +`````````````````````````` + +The simplest way to avoid instability is to ensure rewriting +operations always end in a stable situation. This is achieved by +rewriting all affected changesets at the same time. + +Rewriting all descendants at the same time when rewriting a changeset. + +:: + + Schema! + +Several Mercurial commands apply it: rebase, collapse, histedit. +Mercurial also refuses to amend changeset with descendant. The git +branch design enforces such approach in git too. + + +However, DVCS are **Distributed**. This means that you do not control what +happen outside your repository. Once a changeset have been exchanged *outside*, +there is no way to be sure it does not have descendants somewhere else. +Therefore **if you rewrite changeset that exists elsewhere, you can't eradicate +the risk of instability.** + +Do not rewrite exchanged changeset +``````````````````````````````````` + +To work around the issue above, Mercurial introduced phases, which +prevent you from rewriting shared changesets and ensure others can't +pull certain changesets from you. But this is a very frustrating +limitation that prevents you to efficiently sharing, reviewing and +collaborating on mutable changesets. + +In the Git world, they use another approach to prevent instability. By +convention only a single developper works on a changeset contained in +a named branch. But once again this is a huge blocker for +collaborating. Moreover clueless people **will** mess up social +convention soon or later. + + +Loose the DAG robustness +```````````````````````````` + +The other approach in Mercurial is to keep the mutable part of the +history outside the DVCS constraint. This is the MQ approach of +sticking a quilt queue over Mercurial. + +This allow much more flexible workflow but two major feature are lost in the +process: + +:Graceful merge: MQ use plain-patch to store changeset content and patch have + trouble to apply in changing context. Applying your queue + becomes very painful when context changes. + +:easy branching: A quilt queue is by definition a linear queue. Increasing risk + of conflict + +It is possible to collaborate over versioned mq! But you are going to +have a lot of troubles. + +Ignore conflicts +``````````````````````````````````` + +Another ignored issue is a conflicting rewrite of the same changeset. +If a changeset is rewritten two times we have two newer versions, +and duplicated history is complicated to merge. + +Mercurial work around by + +The "One set of mutable changset == One developer" mantra is also a way to work +around conflicting rewriting of changeset. If two different people are able to + +The git branch model allow to overwrite changeset version by another +one, but it does not care about divergent version. It is the equivalent +of "common ftp" source management for changesets. + +Facing The Danger Once And For All +------------------------------------------------ + +Above we saw that, the more effort you put to avoid instability, the more option +you deny. And even most restrictive work flow can't guarantee that instability +will never show up! + +Obsolete marker can handle the job +``````````````````````````````````` + +It is time to provide a full featured solution to deal with +instability and to stop working around the issue! This is why I +developing a new feature for mercurial called "Obsolete markers". +Obsolete markers have two key properties: + + +* Any "old" changeset we want to get ride of is **explicitly** marked + as "obsolete" by history rewriting operation. + + By explicitly marking the obsolete part of the history, we will be able to + easily detect instability situation. + +* Relations between old and new version of changesets are tracked by obsolete + markers. + + By Storing a meta-history of changeset evolution we are able to easily resolve + instability and edition conflict [#]_ . + +.. [#] edition conflict is another major obstable to collaboration. See the + section dedicated to obsolete marker for details. + +Improves robustness == improves simplicity +```````````````````````````````````````````````` + +This proposal should **first** be seen as a safety measure. + +It allow to detect instability as soon as possible + +:: + + $ hg pull + added 3 changeset + +2 unstable changeset + (do you want "hg stabilize" ?) + working directory parent is obsolete! + $ hg push + outgoing unstable changesets + (use "hg stabilize" or force the push) + +And should not not encourage people to create instability + +:: + + $ hg up 42 + $ hg commit --amend + changeset have descendant. + $ hg commit --amend -f + +5 unstable changeset + + $ hg rebase -D --rev 40::44 + rebasing already obsolete changeset 42:AAA will conflict with newer version 48:BBB + +While allowing powerful feature +```````````````````````````````````````````````` + + +* Help to automatically solve instability. + +* "prune" changeset remotely. + +* track resulting changeset when submitting patch//pull request. + +* Focus on what you do: + + I do not like the "all at once" model of history rewriting. I'm comfortable + with instability and obsolete marker offer all the tool to safely create and + handle instability locally. + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/makefile Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,6 @@ + +all: tutorial + sphinx-build . ../html/ + +tutorial: + python test2rst.py tutorials/
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/obs-concept.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,393 @@ +----------------------------------------------------------- +Why Do We Need a New Concept +----------------------------------------------------------- + +Current DVCSes are great tools for forging a series of flawless +changesets on your own. But they perform poorly when it comes to +**sharing** some work in progress and **collaborating** on such work +in progress. + +When people forge a new version of a changeset they actually create a +new changeset and get rid of the original changeset. Difficulties to +collaborate mostly came from the way old content is *removed* from +a repository. + +Mercurial Approach: Strip +----------------------------------------------------- + +With the current version of mercurial, every changeset that exists in +your repository is *visible* and *meaningful*. To delete old +(rewritten) changesets, mercurial removes them from the repository +storage with an operation called *strip*. After the *stripping*, the +repository looks as if the changeset never existed. + +This approach is simple and effective except for one big +drawback: you can remove changesets from **your repository only**. If +a stripped changeset exists in another repository it touches, it will +show up again. This is because a shared changeset becomes +part of a shared global history. Stripping a changeset from all +repositories is at best impractical and in most case impossible. + +As consequence, **you can not rewrite something once you exchange it with +others**. The old version will still exist along side the new one [#]_. + +Moreover stripping changesets creates backup bundles. This allows +restoration of the deleted changesets, but the process is painful. + +Finally, as the repository format is not optimized for deletion. stripping a +changeset may be slow in some situations. + +To sum up, the strip approach is very simple but does not handle +interaction with the outer world, which is very unfortunate for a +*Distributed* VCS. + +.. [#] various work around exists but they require their own workflows + which are distinct from the very elegant basic workflow of + Mercurial. + +Git Approach: Overwrite Reference +----------------------------------------------------- + +The Git approach to repository structure is a bit more complex: there +can be any amount of unrelated changesets in a repository, and **only +changesets referenced by a git branch** are *visible* and +*meaningful*. + + +.. warning:: add a schema:: + + C + | B---<foo> + |/ + | + A + + Only B and A are visible. + +This simplifies the process of getting rid of old changesets. You can +just leave them in place and move the reference on the new one. You +can then propagate this change by moving the git-branch on remote host +with the newer version of the marker overwriting the older one. + +This approach goes a bit further but still has a major drawback: + +Because you **overwrite** the git-branch, you have no conflict +resolution. The last to act wins. This makes collaboration on multiple +changesets difficult because you can't merge concurrent updates on a +changeset. + +Every overwrite is a forced operation where the operator says, "yes I +want this to replace that". In highly distributed environments, a user +may end up with conflicting references and no proper way to choose. + +Because of this way to visualize a repository, git-branches are a core +part of git, which makes the user interface more complicated and +constrains moving through history. + +Finally, even if all older changesets still exist in the repository, +accesing them is still painful. + + +----------------------------------------------------- +The Obsolete Marker Concept +----------------------------------------------------- + + +As none of the concepts was powerful enough to fulfill the need of +safely rewriting history, including easy sharing and collaboration on +mutable history, we needed another one. + +Basic concept +----------------------------------------------------- + + +Every history rewriting operation stores the information that old rewritten +changeset is replaced by newer version in a given set of changesets. + +All basic history rewriting operation can create an appropriate obsolete marker. + + +.. figure:: ./figures/example-1-update.* + + *Updating* a changeset + + Create one obsolete marker: ``([A'] obsolete A)`` + + + +.. figure:: ./figures/example-2-split.* + + *Splitting* a changeset in multiple one + + Create one obsolete marker ``([B1, B2] obsolete B)]`` + + +.. figure:: ./figures/simple-3-merge.* + + *Merging* multiple changeset in a single one + + Create two obsolete markers ``([C] obsolete A), ([C] obsolete B)`` + +.. figure:: ./figures/simple-4-reorder.* + + *Moving* changeset around + + Reordering those two changesets need two obsolete markers: + ``([A'] obsolete A), ([B'] obsolete B)`` + + + +.. figure:: ./figures/simple-5-delete.* + + *Removing* a changeset: + + One obselete marker ``([] obsolete B)`` + + +To conclude, a single obsolete marker express a relation from **0..n** new +changesets to **1** old changeset. + +Basic Usage +----------------------------------------------------- + +Obsolete markers create a perpendicular history: **a versioned +changeset graph**. This means that offers the same features we have +for versioned files but applied to changeset: + +First: we can display a **coherent view** of the history graph in which only a +single version of your changesets is displayed by the UI. + +Second, because obsolete changeset content is still **available**. You can +you can + + * **browse** the content of your obsolete commits, + + * **compare** newer and older versions of a changeset, + + * **restore** content of previously obsolete changesets. + +Finally, the obsolete marker can be **exchanged between +repositories**. You are able to share the result on your history +rewriting operations with other prople and **collaborate on the +mutable part of the history**. + +Conflicting history rewriting operation can be detected and +**resolved** as easily as conflicting changes on a file. + + +Detecting and solving tricky situations +----------------------------------------------------- + +History rewriting can lead to complex situations. The obsolete marker +introduces a simple representation for this complex reality. But +people using complex workflows will one day or another have to face +the intrinsic complexity of some real-world situation. + +This section describes possible situations, defines precise sets of +changesets involved in such situations and explains how the error +cases can be resolved automatically using the available information. + + +Obsolete changesets +```````````````````` + +Old changesets left behind by obsolete operation are called **obsolete**. + +With the current version of mercurial, this *obsolete* part is stripped from the +repository before the end of every rewriting operation. + +.. figure:: ./figures/error-obsolete.* + + Rebasing `B` and `C` on `A` (as `B'`, `C'`) + + This rebase operation added two obsolete markers from new + changesets to old changesets. These two old changesets are now + part of the *obsolete* part of the history. + +In most cases, the obsolete set will be fully hidden to both the UI and +discovery, hence users do not have to care about them unless they want to +audit history rewriting operations. + +Unstable changesets +``````````````````` + +While exploring the possibilities of the obsolete marker a bit +further, you may end up with *obsolete* changesets which have +*non-obsolete* children. There is two common ways to achieve this: + +* Pull a changeset based of an old version of a changeset [#]_. + +* Use a partial rewriting operation. For example amend on a changeset with + children. + +*Non-obsolete* changeset based on *obsolete* one are called **unstable** + +.. figure:: ./figures/error-unstable.* + + Amend `A` into `A'` leaving `B` behind. + + In this situation we cannot consider `B` as *obsolete*. But we + have all the necessary data to detect `B` as an *unstable* branch + of the history because its parent `A` is *obsolete*. In addition, + we have enough data to automatically resolve this instability: we + know that the new version of `B` parent (`A`) is `A'`. We can + deduce that we should rebase `B` on `A'` to get a stable history + again. + +Proper warnings should be issued when part of the history becomes +unstable. The UI will be able to use the obsolete marker to +automatically suggest a resolution to the user of even carry them out +for them. + + +XXX details on automatic resolution for + +* movement + +* handling deletion + +* handling split on multiple head + + +.. [#] For this to happen one needs to explicitly enable exchange of draft + changesets. See phase help for details. + +The two parts of the obsolete set +`````````````````````````````````````` + +The previous section shows that there could be two kinds of *obsolete* +changesets: + +* an *obsolete* changeset with no or *obsolete* only descendants is called **extinct**. + +* an *obsolete* changeset with *unstable* descendants is called **suspended**. + + +.. figure:: ./figures/error-extinct.* + + Amend `A` and `C` leaving `B` behind. + + In this example we have two *obsolete* changesets: `C` with no *unstable* + children is *extinct*. `A` with *unstable* descendant (`B`) is *suspended*. + `B` is *unstable* as before. + + +Because nothing outside the obsolete set default on *extinct* +changesets, they can be safely hidden in the UI and even garbage +collected. *Suspended* changesets have to stay visible and available +until their unstable descendant are rewritten into stable version. + + +Conflicting rewrites +```````````````````` + +If people start to concurrently edit the same part of the history they will +likely meet conflicting situations when a changeset has been rewritten in two +different ways. + + +.. figure:: ./figures/error-conflicting.* + + Conflicting rewrite of `A` into `A'` and `A''` + +This kind of conflict is easy to detect with an obsolete marker +because an obsolete changeset can have more than one new version. It +may be seen as the multiple heads case. Mercurial warns you about this +on pull. It is resolved the same way by a merge of A' and A'' that +will keep the same parent than `A'` and `A''` with two obsolete +markers pointing to both `A` and `A'` + +.. warning:: TODO: Add a schema of the resolution. (merge A' and A'' with A as + ancestor and graft the result of A^) + +Allowing multiple new changesets to obsolete a single one allows to +distinguish a split changeset from a history rewriting conflict. + +Reliable history +`````````````````````` + +Obsolete markers help to smooth rewriting operation process. However +they do not change the fact that **you should only rewrite the mutable +part of the history**. The phase concept enforces this rule by +explicitly defining a public immutable set of changesets. Rewriting +operations refuse to work on public changesets, but there are still +some corner cases where previously rewritten changesets are made +public. + +Special rules apply for obsolete markers pointing to public changesets: + +* Public changesets are excluded from the obsolete set (public + changesets are never hidden or candidate to garbage collection) + +* *newer* version of a public changeset are called **latecomer** and + highlighted as an error case. + +Solving such an error is easy. Because we know what changeset a +*latecomer* tries to rewrite, we can easily compute a smaller +changeset containing only the change from the old *public* to the new +*latecomer*. + +.. warning:: add a schema + + +Conclusion +---------------- + +The obsolete marker is a powerful concept that allows mercurial to safely handle +history rewriting operations. It is a new type of relation between Mercurial +changesets which tracks the result of history rewriting operations. + +This concept is simple to define and provides a very solid base for: + + +- Very fast history rewriting operations, + +- auditable and reversible history rewriting process, + +- clean final history, + +- sharing and collaborating on the mutable part of the history, + +- gracefully handling history rewriting conflicts, + +- various history rewriting UI's collaborating with an underlying common API. + +.. list-table:: Comparison on solution [#]_ + :header-rows: 1 + + * - Solution + - Remove changeset locally + - Works on any point of your history + - Propagation + - Collaboration + - Speed + - Access to older version + + * - Strip + - `+` + - `+` + - \ + - \ + - \ + - `- -` + + * - Reference + - `+` + - \ + - `+` + - \ + - `+` + - `-` + + * - Obsolete + - `+` + - `+` + - `++` + - `++` + - `+` + - `+` + + + +.. [#] To preserve good tradition in comparison table, an overwhelming advantage + goes to the defended solution.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/obs-implementation.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,213 @@ + +----------------------------------------------------- +Implementation of Obsolete Marker +----------------------------------------------------- +.. warning:: This document is still in heavy work in progress + +Main questions about Obsolete Marker Implementation +----------------------------------------------------- + + +What data should be contained in a Marker ? +```````````````````````````````````````````````````` + +There are two critical pieces of information that **must** be stored +in an obsolete Marker. + +:object: + the old obsoleted changeset + +:replacements: + list of new changeset. list size can be anything, including 0 (0..N) + +Everybody agreed on this point. + + --- + +This is probably a good idea to have an unique Identifier, for UI, transfer and +access. + + :id: same as changeset but for marker. + +The field below will depend on the way we exchange obsolete marker between +changesets. + + --- + +Having audit data will be very useful. When it gets messy you need all the +information available to understand the situation. + +I have the feeling that we are versioning history. Therefor we will probably +need the same kind of information than when versioning Files. + +:date: date of the marker creation + +:user: ui.username + +To go further: + +:description: "Optional reason for the rewrite (generated by the user)" + +:tool: the automated tool that made this + +:operation: Kind of rewritting operation that created the marker (delete, + update, split, fold, reordering), to help conflict resolution. + +Matt said this is "too complicated". I'll wait for him to meet a very hairy +situation to agree that they are needed. + +Leaving the door open to any addition data is an option too. + +How shall we store Marker on disk +````````````````````````````````````````````````````````` + +Requirement +............. + +We need to quickly load the 'object' to know the "obsolete" set. +We need quick access by object and replacements to travels along the graph. + +Common Part +............. + +The file is store in `.hg/store/obsmarkers`. It is a binary files: + +The files starts with a Format Version string + + +Minimalistic proposal +......................... + +The core of a Marker will we stored as: + +* number of replacement (8-Bytes integer) +* node id of the obsolete changeset (20-Bytes hash) +* node id of replacement changeset (20-Bytes hash x number of remplacement) + +Version with ID +......................... + +This version add a node id computed from the marker content. It will be present +*before* other data: + +* node id of the maker (20-Bytes hash) + + +Version with Metadata proposal +............................... + +An extra files is used to old metadata (date, user, etc) `.hg/store/obs-extra`:. + +The format of this field is undefined yet. This will add the following +field at the end of a marker + +* offset of the metadata in obs-extra (8-Bytes integer) + + +How shall we exchange Marker over the Wire ? +````````````````````````````````````````````````````````` + +We can have a lot of markers. We do not want to exchange data for the one we +already know. Listkey() is not very appropriate there as you get everything. + +Moreover, we might want to only hear about Marker that impact changeset we are +pulling. + +pushkey is not batchable yet (could be fixed) + +A dedicated discovery and exchange protocol seems mandatory here. + + +Various technical details +----------------------------------------------------- + +Some stuff that worse to note. some may deserve their own section later. + +storing old changeset +`````````````````````` + +The new general delta format allows a very efficient storage of two very similar +changesets. Storing obsolete children using general delta takes no more place +than storing the obsolete diff. Reverted file will even we reused. The whole +operation will take much less space the strip backup. + + +Abstraction from history rewriting UI +``````````````````````````````````````````` + +How Mercurial handles obsolete marker is independent from what decides +to create them and what actual operation solves the error case. Any of +the existing history rewriting UI (rebase, mq, histedit) can lay +obsolete markers and resolve situation created by others. To go +further, a hook system of obsolete marker creation would allow each +mechanism to collaborate with other though a standard and central +mechanism. + + +Obsolete marker storage +``````````````````````````` + +The Obsolete marker will most likely be stored outside standard +history. They are multiple reasons for this: + +First, obsolete markers are really perpendicular to standard history +there is no strong reason to include it here other than convenience. + +Second, storing obsolete marker inside standard history means: + +* A changeset must be created every time an obsolete relation is added. Very + inconvenient for delete operation. + +* Obsolete marker must be forged at the creation of the new changeset. This + is very inconvenient for split operation. And in general it becomes + complicated to fix history afterward in particular when working with older + clients. + +Storing obsolete marker outside history have several pros: + +* It eases Exchange of obsolete markers without unnecessary obsolete + changeset contents. + +* It allows tuning the actual storage and protocol exchange while maintaining + compatibility with older clients through the wire (as we do the repository + format). + +* It eases the exchange of obsolete related information during + discovery to exchange obsolete changeset relevant to conflict + resolution. Exchanging such information deserves a dedicated + protocol. + +Persistent +``````````````````````` + +*Extinct* changeset and obsolete marker will most likely be garbage collected as +some point. However, archive server may decide to keep them forever in order to +keep a fully auditable history in its finest conception. + + +Current status +----------------------------------------------------- + +An experimental implementatione exists. What have been done so far. + + +* 1-1 obsolete marker stored outside history, + +* compute obsolete-tip + +* obsolete marker exchange through pushkey, + +* compute obsolete, unstable, extinct and suspended set. + +* hidden extinct changesets for UI. + +* Use secret phase to remove from discovery obsolete and unstable changesets (to + be improved soon) + +* alter rebase to use obsolete markers instead of stripping. + +* Have an experimental mq-like extension to rewrite history (more on that later) + +* Have an extension to update and mq repository according evolution of + standard (more on that later) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/obs-terms.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,239 @@ +----------------------------------------------------------- +Terminology of the obsolete concept +----------------------------------------------------------- + +Obsolete markers +--------------------------------- + +The mutable concept is based on **obsolete markers**. Creating an obsolete +marker registers a relation between an old obsoleted changeset and its newer +version. + +Old changesets are called **precursors** while their new versions are called +**successors**. A marker always registers a single *precursor* and: + +- no *successor*: the *precursor* is just discarded. +- one *successor*: the *precursor* has been rewritten +- multiple *successors*: the *precursor* were splits in multiple + changesets. + +.. The *precursors* and *successors* terms can be used on changeset directy: + +.. :precursors: of a changeset `A` are changesets used as *precursors* by +.. obsolete marker using changeset `A` as *successors* + +.. :successors: of a changeset `B` are changesets used as *successors* by +.. obsolete marker using changeset `B` as *precursors* + +Chaining obsolete markers is allowed to rewrite a changeset that is already a +*successor*. This is a kind of *second order version control*. +To clarify ambiguous situations one can use **direct precursors** or +**direct successors** to name changesets that are directly related. + +The set of all *obsolete markers* forms a direct acyclic graph the same way +standard *parents*/*children* relation does. In this graph we have: + +:any precursors: are transitive precursors of a changeset: *direct precursors* + and *precursors* of *precursors*. + +:any successors: are transitive successors of a changeset: *direct successors* + and *successors* of *successors*) + +Obsolete markers may refer changesets that are not known locally. +So, *direct precursors* of a changeset may be unknown locally. +This is why we usually focus on the **first known precursors** of the rewritten +changeset. The same apply for *successors*. + +Changeset in *any successors* which are not **obsolete** are called +**newest successors**.. + +.. note:: I'm not very happy with this naming scheme and I'm looking for a + better distinction between *direct successors* and **any successors*. + +Possible changesets "type" +--------------------------------- + +The following table describes names and behaviors of changesets affected by +obsolete markers. The left column describes generic categories and the right +columns are about sub-categories. + + ++---------------------+--------------------------+-----------------------------+ +| **mutable** | **obsolete** | **extinct** | +| | | | +| Changeset in either | Obsolete changeset is | *extinct* changeset is | +| *draft* or *secret* | *mutable* used as a | *obsolete* which has only | +| phase. | *precursor*. | *obsolete* descendants. | +| | | | +| | A changeset is used as | They can safely be: | +| | a *precursor* when at | | +| | least one obsolete | - hidden in the UI, | +| | marker refers to it | - silently excluded from | +| | as precursors. | pull and push operations | +| | | - mostly ignored | +| | | - garbage collected | +| | | | +| | +-----------------------------+ +| | | | +| | | **suspended** | +| | | | +| | | *suspended* changeset is | +| | | *obsolete* with at least | +| | | one non-obsolete descendant | +| | | | +| | | Thoses descendants prevent | +| | | properties of extincts | +| | | changesets to apply. But | +| | | they will refuse to be | +| | | pushed without --force. | +| | | | +| +--------------------------+-----------------------------+ +| | | | +| | **troublesome** | **unstable** | +| | | | +| | *troublesome* has | *unstable* is a changeset | +| | unresolved issue caused | with obsolete ancestors. | +| | by *obsolete* relations. | | +| | | | +| | Possible issues are | It must be rebased on a | +| | listed in the next | non *troublesome* base to | +| | column. It is possible | solve the problem. | +| | for *troublesome* | | +| | changeset to combine | (possible alternative name: | +| | multiple issue at once. | precarious) | +| | (a.k.a. conflicting and | | +| | unstable) +-----------------------------+ +| | | | +| | (possible alternative | **latecomer** | +| | names: unsettled, | | +| | troubled) | *latecomer* is a changeset | +| | | that tries to be successor | +| | | of public changesets. | +| | | | +| | | Public changeset can't | +| | | be deleted and replace | +| | | *latecomer* | +| | | need to be converted into | +| | | an overlay to this public | +| | | changeset. | +| | | | +| | | (possible alternative names:| +| | | mislead, naive, unaware, | +| | | mindless, disenchanting) | +| | | | +| | +-----------------------------+ +| | | **conflicting** | +| | | | +| | | *conflicting* is changeset | +| | | that appears when multiple | +| | | changesets are successors | +| | | of the same precursor. | +| | | | +| | | *conflicting* are solved | +| | | through a three ways merge | +| | | between the two | +| | | *conflictings*, | +| | | using the last "obsolete- | +| | | -common-ancestor" as the | +| | | base. | +| | | | +| | | (*splitting* is | +| | | properly not detected as a | +| | | conflict) | +| | | | +| +--------------------------+-----------------------------+ +| | | +| | Mutable changesets which are neither *obsolete* or | +| | *troublesome* are *"ok"*. | +| | | +| | Do we really need a name for it ? *"ok"* is a pretty | +| | crappy name :-/ other possibilities are: | +| | | +| | - stable (confusing with stable branch) | +| | - sane | +| | - healthy | +| | | ++---------------------+--------------------------------------------------------+ +| | +| **immutable** | +| | +| Changesets in the *public* phases. | +| | +| Rewriting operation refuse to work on immutable changeset. | +| | +| Obsolete markers that refer an immutable changeset as precursors have | +| no effect on the precussors but may have effect on the successors. | +| | +| When a *mutable* changeset becomes *immutable* (changing its phase from draft| +| to public) it is just *immutable* and loose any property of it's former | +| state. | +| | +| The phase properties says that public changesets stay as *immutable* forever.| +| | ++------------------------------------------------------------------------------+ + +.. note:: I'm not very happy with the naming of: + + - "ok" changeset + - latecomer + - troublesome + + Any better idea are welcome. + + +Command and operation name +--------------------------------- + + +Existing terms +`````````````` + +Mercurial core already uses the following terms: + +:amend: to rewrite a changeset +:graft: to copy a changeset +:rebase: to move a changeset + + +Uncommit +````````````` + +Remove files from a commit (and leave them as dirty in the working directory) + +The *evolve* extension have an `uncommit` command that aims to replace most +`rollback` usage. + +Fold +`````````` + +Collapse multiple changesets into a unique one. + +The *evolve* extension will have a `fold` command. + +Prune +`````````` + +Make a changeset obsolete without successors. + +This an important operation as it should mostly replace *strip*. + +Alternative names: + +- kill: shall has funny effects when you forget "hg" in front of ``hg kill``. +- obsolete: too vague, too long and too generic. + +Stabilize +``````````````` + +Automatically resolve *troublesome* changesets +(*unstable*, *latecomer* and *conflicting*) + +This is an important name as hg pull/push will suggest it the same way it +suggest merging when you add heads. + +I do not like stabilize much. + +alternative names: + +- solve (too generic ?) +- evolve (too vague)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/qsync.rst Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,16 @@ +--------------------------------------------------------------------- +Qsync: Mercurial to MQ exporter +--------------------------------------------------------------------- + + +People may have tools or co-workers that export to receive mutable history using +a versioned mq repository. + +For this purpose you can use the ``qsync`` extension: + + +To enable the evolve extension use:: + + $ hg clone http://hg-dev.octopoid.net/hgwebdir.cgi/mutable-history/ + $ mutable-history/iqsync-enable.sh >> ~/.hgrc + $ hg help qsync
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/test2rst.py Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,64 @@ +#!/usr/bin/env python + +import os, os.path as op, re, 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 +# on a next line (indented text) +CODEBLOCK = re.compile(r'()\n(([^ \n][^\n]|[^\n][^ \n])[^\n]*)\n+ ') + +INDEX = ''' +Mercurial tests +=============== + +.. toctree:: + :maxdepth: 1 +''' + + +def rstify(orig, name): + header = '%s\n%s\n\n' % (name, '=' * len(name)) + content = header + orig + content = CODEBLOCK.sub(r'\n\1\n\n::\n\n ', content) + return content + + +def main(base): + if os.path.isdir(base): + one_dir(base) + else: + print one_file(base) + + +def one_dir(base): + index = INDEX + #doc = lambda x: op.join(op.dirname(__file__), 'docs', x) + + for fn in sorted(os.listdir(base)): + if not fn.endswith('.t'): + continue + print fn + 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: + f.write(content) + print f + + index += '\n ' + name + + #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) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print 'Please supply a path to tests dir as parameter' + sys.exit() + main(sys.argv[1])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/tutorials/tutorial.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,775 @@ + +Initial setup +------------- + +This Mercurial configuration example is used for testing. +.. Various setup + + $ cat >> $HGRCPATH << EOF + > [ui] + > logtemplate ="{node|short} ({phase}): {desc}\n" + > [diff] + > git = 1 + > [alias] + > # "-d '0 0'" means that the new commit will be at January 1st 1970. + > # This is used for stable hash during test + > amend = amend -d '0 0' + > [extensions] + > hgext.graphlog= + > EOF + + $ hg init local + $ cat >> local/.hg/hgrc << EOF + > [paths] + > remote = ../remote + > other = ../other + > [ui] + > user = Babar the King + > EOF + + $ hg init remote + $ cat >> remote/.hg/hgrc << EOF + > [paths] + > local = ../local + > [ui] + > user = Celestine the Queen + > EOF + + $ hg init other + $ cat >> other/.hg/hgrc << EOF + > [ui] + > user = Princess Flore + > EOF + + +This tutorial use the following configuration for Mercurial: + +A compact log template with phase data: + + $ hg showconfig ui + ui.slash=True + ui.logtemplate="{node|short} ({phase}): {desc}\n" + +Improved git format diff: + + $ hg showconfig diff + diff.git=1 + +And the graphlog extension + $ hg showconfig extensions + extensions.hgext.graphlog= + +And of course, we anabled the experimental extensions for mutable history: + + $ $(dirname $TESTDIR)/enable.sh >> $HGRCPATH 2> /dev/null + + +----------------------- +Single Developer Usage +----------------------- + +This tutorial shows how to use evolution to rewrite history locally. + + +Fixing mistake with `hg amend` +-------------------------------- + +We are versionning a shopping list + + $ cd local + $ cat >> shopping << EOF + > Spam + > Whizzo butter + > Albatross + > Rat (rather a lot) + > Jugged fish + > Blancmange + > Salmon mousse + > EOF + $ hg commit -A -m "Monthy Python Shopping list" + adding shopping + +Its first version is shared with the outside. + + $ hg push remote + pushing to $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + +Later I add additional item to my list + + $ cat >> shopping << EOF + > Egg + > Suggar + > Vinegar + > Oil + > EOF + $ hg commit -m "adding condiment" + $ cat >> shopping << EOF + > Bananos + > Pear + > Apple + > EOF + $ hg commit -m "adding fruit" + +This history is very linear + + $ hg glog + @ d85de4546133 (draft): adding fruit + | + o 4d5dc8187023 (draft): adding condiment + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +But a typo was made in Babanas! + + $ hg export tip + # HG changeset patch + # User test + # Date 0 0 + # Node ID d85de4546133030c82d257bbcdd9b1b416d0c31c + # Parent 4d5dc81870237d492284826e21840b2ca00e26d1 + adding fruit + + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -9,3 +9,6 @@ + Suggar + Vinegar + Oil + +Bananos + +Pear + +Apple + +The faulty changeset is in the "draft" phase because he was not exchanged with +the outside. The first one have been exchanged and is an immutable public +changeset. + + $ hg glog + @ d85de4546133 (draft): adding fruit + | + o 4d5dc8187023 (draft): adding condiment + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +hopefully. I can use hg amend to rewrite my faulty changeset! + + $ sed -i'' -e s/Bananos/Banana/ shopping + $ hg diff + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -9,6 +9,6 @@ + Suggar + Vinegar + Oil + -Bananos + +Banana + Pear + Apple + $ hg amend + +A new changeset with the right diff replace the wrong one. + + $ hg glog + @ 0cacb48f4482 (draft): adding fruit + | + o 4d5dc8187023 (draft): adding condiment + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + $ hg export tip + # HG changeset patch + # User test + # Date 0 0 + # Node ID 0cacb48f44828d2fd31c4e45e18fde32a5b2f07b + # Parent 4d5dc81870237d492284826e21840b2ca00e26d1 + adding fruit + + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -9,3 +9,6 @@ + Suggar + Vinegar + Oil + +Banana + +Pear + +Apple + +Getting Ride of branchy history +---------------------------------- + +While I was working on my list. someone help made a change remotly. + + $ cd ../remote + $ hg up -q + $ sed -i'' -e 's/Spam/Spam Spam Spam/' shopping + $ hg ci -m 'SPAM' + $ cd ../local + +I'll get this remote changeset when pulling + + $ hg pull remote + pulling from $TESTTMP/remote + 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 merge' to merge) + +I now have a new heads. Note that this remote head is immutable + + $ hg log -G + o 9ca060c80d74 (public): SPAM + | + | @ 0cacb48f4482 (draft): adding fruit + | | + | o 4d5dc8187023 (draft): adding condiment + |/ + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +instead of merging my head with the new one. I'm going to rebase my work + + $ hg diff + $ hg rebase -d 9ca060c80d74 -s 4d5dc8187023 + merging shopping + merging shopping + + +My local work is now rebased on the remote one. + + $ hg log -G + @ 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Removing changeset +------------------------ + +I add new item to my list + + $ cat >> shopping << EOF + > car + > bus + > plane + > boat + > EOF + $ hg ci -m 'transport' + $ hg log -G + @ d58c77aa15d7 (draft): transport + | + o 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +I have a new commit but I realize that don't want it. (transport shop list does +not fit well in my standard shopping list) + + $ hg prune . # . is for working directory parent + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory now at 387187ad9bd9 + +The silly changeset is gone. + + $ hg log -G + @ 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Reordering changeset +------------------------ + + +We create two changesets. + + + $ cat >> shopping << EOF + > Shampoo + > Toothbrush + > ... More bathroom stuff to come + > Towel + > Soap + > EOF + $ hg ci -m 'bathroom stuff' -q # XXX remove the -q + + $ sed -i'' -e 's/Spam/Spam Spam Spam/g' shopping + $ hg ci -m 'SPAM SPAM' + $ hg log -G + @ c48f32fb1787 (draft): SPAM SPAM + | + o 8d39a843582d (draft): bathroom stuff + | + o 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +.. note: don't amend changeset 7e82d3f3c2cb or 9ca060c80d74 as they are immutable. + +I now want to push to remote all my change but the bathroom one that i'm not +totally happy with yet. To be able to push "SPAM SPAM" I need a version of "SPAM SPAM" not children of +"bathroom stuff" + +You can use 'rebase -r' or 'graft -O' for that: + + $ hg up 'p1(8d39a843582d)' # going on "bathroom stuff" parent + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg graft -O c48f32fb1787 # moving "SPAM SPAM" to the working directory parent + grafting revision 10 + merging shopping + $ hg log -G + @ a2fccc2e7b08 (draft): SPAM SPAM + | + | o 8d39a843582d (draft): bathroom stuff + |/ + o 387187ad9bd9 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +We have a new SPAM SPAM version without the bathroom stuff + + $ grep Spam shopping # enouth spam + Spam Spam Spam Spam Spam Spam Spam Spam Spam + $ grep Toothbrush shopping # no Toothbrush + [1] + $ hg export . + # HG changeset patch + # User test + # Date 0 0 + # Node ID a2fccc2e7b08bbce6af7255b989453f7089e4cf0 + # Parent 387187ad9bd9d8f9a00a9fa804a26231db547429 + SPAM SPAM + + diff --git a/shopping b/shopping + --- a/shopping + +++ b/shopping + @@ -1,4 +1,4 @@ + -Spam Spam Spam + +Spam Spam Spam Spam Spam Spam Spam Spam Spam + Whizzo butter + Albatross + Rat (rather a lot) + +To make sure I do not push unready changeset by mistake I set the "bathroom +stuff" changeset in the secret phase. + + $ hg phase --force --secret 8d39a843582d + +we can now push our change: + + $ hg push remote + pushing to $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 3 changes to 1 files + +for simplicity shake we get the bathroom change in line again + + $ hg rebase -Dr 8d39a843582d -d a2fccc2e7b08 + merging shopping + $ hg phase --draft . + $ hg log -G + @ 8a79ae8b029e (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + + +Splitting change +------------------ + +To be done (currently achieve with "two commit + debugobsolete") + +Collapsing change +------------------ + +To be done (currently achieve with "revert + debugobsolete" or "rebase --collapse") + + + + + + +----------------------- +Collaboration +----------------------- + + +sharing mutable changeset +---------------------------- + +To share mutable changeset with other just check that the repo you interact +with is "not publishing". Otherwise you will get the previously observe +behavior where exchanged changeset are automatically published. + + $ cd ../remote + $ hg -R ../local/ showconfig phases + +the localrepo does not have any specific configuration for `phases.publish`. It +is ``true`` by default. + + $ hg pull local + pulling from $TESTTMP/local + 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 log -G + o 8a79ae8b029e (public): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + @ 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + + +We do not want to publish the "bathroom changeset". Let's rollback the last transaction + + $ hg rollback + repository tip rolled back to revision 4 (undo pull) + $ hg log -G + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + @ 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Let's make the local repo "non publishing" + + $ echo '[phases]' >> ../local/.hg/hgrc + $ echo 'publish=false' >> ../local/.hg/hgrc + $ echo '[phases]' >> .hg/hgrc + $ echo 'publish=false' >> .hg/hgrc + $ hg showconfig phases + phases.publish=false + $ hg -R ../local/ showconfig phases + phases.publish=false + + +I can now exchange mutable changeset between "remote" and "local" repository. + + $ hg pull local + pulling from $TESTTMP/local + 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 log -G + o 8a79ae8b029e (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + @ 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Rebasing unstable change after pull +---------------------------------------------- + +Remotely someone add a new changeset on top of the mutable "bathroom" on. + + $ hg up 8a79ae8b029e -q + $ cat >> shopping << EOF + > Giraffe + > Rhino + > Lion + > Bear + > EOF + $ hg ci -m 'animals' + +But at the same time, locally, this same "bathroom changeset" was updated. + + $ cd ../local + $ hg up 8a79ae8b029e -q + $ sed -i'' -e 's/... More bathroom stuff to come/Bath Robe/' shopping + $ hg amend + $ hg log -G + @ ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + +When we pull from remote again we get an unstable state! + + + $ hg pull remote + pulling from $TESTTMP/remote + 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 merge' to merge) + 1 new unstables changesets + + +The new changeset "animal" is based one an old changeset of "bathroom". You can +see both version showing up in the log. + + $ hg log -G + o 9ac5d0e790a2 (draft): animals + | + | @ ffa278c50818 (draft): bathroom stuff + | | + x | 8a79ae8b029e (draft): bathroom stuff + |/ + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +The older version 8a79ae8b029e never ceased to exist in the local repo. It was +jsut hidden and excluded from pull and push. + +.. note:: In hgview there is a nice doted relation highlighting ffa278c50818 as a new version of 8a79ae8b029e. this is not yet ported to graphlog. + +Their is **unstable** changeset in this history now. Mercurial will refuse to +share it with the outside: + + $ hg push other + pushing to $TESTTMP/other + searching for changes + abort: push includes an unstable changeset: 9ac5d0e790a2! + (use 'hg stabilize' to get a stable history or --force to ignore warnings) + [255] + + + + +To resolve this unstable state, you need to rebase 9ac5d0e790a2 onto +ffa278c50818 the "hg stabilize" command will make this for you. + +It has a --dry-run option to only suggest the next move. + + $ hg stabilize --dry-run + move:[15] animals + atop:[14] bathroom stuff + hg rebase -Dr 9ac5d0e790a2 -d ffa278c50818 + +Let's do it + + $ hg rebase -Dr 9ac5d0e790a2 -d ffa278c50818 + merging shopping + +The old version of bathroom is hidden again. + + $ hg log -G + @ 437efbcaf700 (draft): animals + | + o ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + +We can push this evolution to remote + + $ hg push remote + pushing to $TESTTMP/remote + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 2 changes to 1 files (+1 heads) + +remote get a warning that current working directory is based on an obsolete changeset + + $ cd ../remote + $ hg pull local # we up again to trigger the warning. it was displayed during the push + pulling from $TESTTMP/local + searching for changes + no changes found + Working directory parent is obsolete + + $ hg up 437efbcaf700 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + +Relocating unstable change after prune +---------------------------------------------- + +The remote guy keep working + + $ sed -i'' -e 's/Spam/Spam Spam Spam Spam/g' shopping + $ hg commit -m "SPAM SPAM SPAM" + +I'm pulling its work locally. + + $ cd ../local + $ hg pull remote + pulling from $TESTTMP/remote + 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 log -G + o ae45c0c3092a (draft): SPAM SPAM SPAM + | + @ 437efbcaf700 (draft): animals + | + o ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +In the mean time I noticed you can't buy animals in a super market and I prune the animal changeset: + + $ hg prune 437efbcaf700 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory now at ffa278c50818 + 1 new unstables changesets + + +The animals changeset is still displayed because the "SPAM SPAM SPAM" changeset +is neither dead or obsolete. My repository is in an unstable state again. + + $ hg log -G + o ae45c0c3092a (draft): SPAM SPAM SPAM + | + x 437efbcaf700 (draft): animals + | + @ ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + $ hg log -r 'unstable()' + ae45c0c3092a (draft): SPAM SPAM SPAM + +# XXX make prune stabilization works +# $ hg stabilize --any +# merging shopping + + $ hg graft -O ae45c0c3092a + grafting revision 17 + merging shopping + + $ hg log -G + @ 20de1fb1cec5 (draft): SPAM SPAM SPAM + | + o ffa278c50818 (draft): bathroom stuff + | + o a2fccc2e7b08 (public): SPAM SPAM + | + o 387187ad9bd9 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + +Handling Conflicting amend +---------------------------------------------- + +We can detect that multiple diverging//conflicting amend have been made. There +will be a "evol-merge" command to merge conflicting amend + +This command is not ready yet.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/enable.sh Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,52 @@ +#!/bin/sh + +here=`python -c "import os; print os.path.realpath('$0')"` +repo_root=`dirname "$here"` + +if !( hg --version -q | grep -qe 'version 2\.[2-9]' ); then + echo 'You need mercurial 2.2 or later' >&2 + exit 2 +fi + + + +cat << EOF >&2 +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +XXX Add lines below to the [extensions] section of you hgrc XXX +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + + +EOF + +cat << EOF | sed -e "s#XXXREPOPATHXXX#${repo_root}#" +[extensions] +### experimental extensions for history rewriting + +# obsolete relation support (will move in core) +obsolete=XXXREPOPATHXXX/hgext/obsolete.py + +# history rewriting UI +# needed by evolve +hgext.rebase= +evolve=XXXREPOPATHXXX/hgext/evolve.py + + +[alias] +### useful alias to check future amend result +# equivalent to the qdiff command for mq + +# diff +pdiff=diff --rev .^ + +# status +pstatus=status --rev .^ + +# diff with the previous amend +odiff=diff --rev 'limit(precursors(.),1)' --rev . +EOF + +cat << EOF >&2 + + +### check qsync-enable.sh if your need mq export too. +EOF
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/evolve.py Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,674 @@ +# states.py - introduce the state concept for mercurial changeset +# +# Copyright 2011 Peter Arrenbrecht <peter.arrenbrecht@gmail.com> +# Logilab SA <contact@logilab.fr> +# Pierre-Yves David <pierre-yves.david@ens-lyon.org> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''a set of commands to handle changeset mutation''' + +from mercurial import cmdutil +from mercurial import scmutil +from mercurial import node +from mercurial import error +from mercurial import extensions +from mercurial import commands +from mercurial import bookmarks +from mercurial import phases +from mercurial import commands +from mercurial import context +from mercurial import copies +from mercurial import util +from mercurial.i18n import _ +from mercurial.commands import walkopts, commitopts, commitopts2, logopts +from mercurial import hg + +### util function +############################# + +def noderange(repo, revsets): + """The same as revrange but return node""" + return map(repo.changelog.node, + scmutil.revrange(repo, revsets)) + +### 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. + """ + if len(old.parents()) > 1: #XXX remove this unecessary limitation. + raise error.Abort(_('cannot amend merge changesets')) + base = old.p1() + updatebookmarks = _bookmarksupdater(repo, old.node()) + + wlock = repo.wlock() + try: + + # 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(fctx.path(), fctx.data(), + islink='l' in flags, + isexec='x' in flags, + copied=copied.get(path)) + return mctx + raise IOError() + if commitopts.get('message') and commitopts.get('logfile'): + raise util.Abort(_('options --message and --logfile are mutually' + ' exclusive')) + if commitopts.get('logfile'): + message= open(commitopts['logfile']).read() + elif commitopts.get('message'): + message = commitopts['message'] + else: + message = old.description() + + user = commitopts.get('user') or old.user() + date = commitopts.get('date') or None # old.date() + extra = dict(commitopts.get('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 + if created: + updatebookmarks(newid) + # add evolution metadata + collapsed = set([u.node() for u in updates] + [old.node()]) + repo.addcollapsedobsolete(collapsed, new.node()) + else: + # newid is an existing revision. It could make sense to + # replace revisions with existing ones but probably not by + # default. + pass + finally: + wlock.release() + + return newid, created + +def relocate(repo, orig, dest): + """rewrite <rev> on dest""" + try: + rebase = extensions.find('rebase') + # dummy state to trick rebase node + assert orig.p2().rev() == node.nullrev, 'no support yet' + destbookmarks = repo.nodebookmarks(dest.node()) + cmdutil.duplicatecopies(repo, orig.node(), dest.node()) + nodesrc = orig.node() + destphase = repo[nodesrc].phase() + if rebase.rebasenode.func_code.co_argcount == 5: + # rebasenode collapse argument was introduced by + # d1afbf03e69a (2.3) + rebase.rebasenode(repo, orig.node(), dest.node(), + {node.nullrev: node.nullrev}, False) + else: + rebase.rebasenode(repo, orig.node(), dest.node(), + {node.nullrev: node.nullrev}) + try: + nodenew = rebase.concludenode(repo, orig.node(), dest.node(), + node.nullid) + except util.Abort: + repo.ui.write_err(_('/!\\ stabilize failed /!\\\n')) + repo.ui.write_err(_('/!\\ Their is no "hg stabilize --continue" /!\\\n')) + repo.ui.write_err(_('/!\\ use "hg up -C . ; hg stabilize --dry-run" /!\\\n')) + raise + oldbookmarks = repo.nodebookmarks(nodesrc) + if nodenew is not None: + phases.retractboundary(repo, destphase, [nodenew]) + repo.addobsolete(nodenew, nodesrc) + for book in oldbookmarks: + repo._bookmarks[book] = nodenew + else: + repo.addobsolete(node.nullid, 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: + bookmarks.write(repo) + except util.Abort: + # Invalidate the previous setparents + repo.dirstate.invalidate() + raise + +def stabilizableunstable(repo, pctx): + """Return a changectx for an unstable changeset which can be + stabilized on top of pctx or one of its descendants. None if none + can be found. + """ + def selfanddescendants(repo, pctx): + yield pctx + for ctx in pctx.descendants(): + yield ctx + + # Look for an unstable which can be stabilized as a child of + # node. The unstable must be a child of one of node predecessors. + for ctx in selfanddescendants(repo, pctx): + unstables = list(repo.set('unstable() and children(obsancestors(%d))', + ctx.rev())) + if unstables: + return unstables[0] + return None + +def _bookmarksupdater(repo, oldid): + """Return a callable update(newid) updating the current bookmark + and bookmarks bound to oldid to newid. + """ + bm = bookmarks.readcurrent(repo) + def updatebookmarks(newid): + dirty = False + if bm: + repo._bookmarks[bm] = newid + dirty = True + oldbookmarks = repo.nodebookmarks(oldid) + if oldbookmarks: + for b in oldbookmarks: + repo._bookmarks[b] = newid + dirty = True + if dirty: + bookmarks.write(repo) + return updatebookmarks + +### new command +############################# +cmdtable = {} +command = cmdutil.command(cmdtable) + +@command('^stabilize|evolve|solve', + [('n', 'dry-run', False, 'do not perform actions, print what to be done'), + ('A', 'any', False, 'stabilize any unstable changeset'),], + _('[OPTIONS]...')) +def stabilize(ui, repo, **opts): + """rebase an unstable changeset to make it stable again + + By default, take the first unstable changeset which could be + rebased as child of the working directory parent revision or one + of its descendants and rebase it. + + With --any, stabilize any unstable changeset. + + The working directory is updated to the rebased revision. + """ + + obsolete = extensions.find('obsolete') + + node = None + if not opts['any']: + node = stabilizableunstable(repo, repo['.']) + if node is None: + unstables = list(repo.set('unstable()')) + if unstables and not opts['any']: + ui.write_err(_('nothing to stabilize here\n')) + ui.status(_('(%i unstable changesets, do you want --any ?)\n') + % len(unstables)) + return 2 + elif not unstables: + ui.write_err(_('no unstable changeset\n')) + return 1 + node = unstables[0] + + obs = node.parents()[0] + if not obs.obsolete(): + obs = node.parents()[1] + assert obs.obsolete() + newer = obsolete.newerversion(repo, obs.node()) + if len(newer) > 1: + ui.write_err(_("conflict rewriting. can't choose destination\n")) + return 2 + targets = newer[0] + if not targets: + ui.write_err(_("does not handle kill parent yet\n")) + return 2 + if len(targets) > 1: + ui.write_err(_("does not handle splitted parent yet\n")) + return 2 + target = targets[0] + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + target = repo[target] + repo.ui.status(_('move:')) + if not ui.quiet: + displayer.show(node) + repo.ui.status(_('atop:')) + if not ui.quiet: + displayer.show(target) + todo= 'hg rebase -Dr %s -d %s\n' % (node, target) + if opts['dry_run']: + repo.ui.write(todo) + else: + repo.ui.note(todo) + lock = repo.lock() + try: + relocate(repo, node, target) + finally: + lock.release() + +shorttemplate = '[{rev}] {desc|firstline}\n' + +@command('^gdown', + [], + '') +def cmdgdown(ui, repo): + """update to parent an display summary lines""" + wkctx = repo[None] + wparents = wkctx.parents() + if len(wparents) != 1: + raise util.Abort('merge in progress') + + parents = wparents[0].parents() + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if len(parents) == 1: + p = parents[0] + hg.update(repo, p.rev()) + displayer.show(p) + return 0 + else: + for p in parents: + displayer.show(p) + ui.warn(_('multiple parents, explicitly update to one\n')) + return 1 + +@command('^gup', + [], + '') +def cmdup(ui, repo): + """update to child an display summary lines""" + wkctx = repo[None] + wparents = wkctx.parents() + if len(wparents) != 1: + raise util.Abort('merge in progress') + + children = [ctx for ctx in wparents[0].children() if not ctx.obsolete()] + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + if not children: + ui.warn(_('No non-obsolete children\n')) + return 1 + if len(children) == 1: + c = children[0] + hg.update(repo, c.rev()) + displayer.show(c) + return 0 + else: + for c in children: + displayer.show(c) + ui.warn(_('Multiple non-obsolete children, explicitly update to one\n')) + return 1 + +@command('^prune|obsolete|kill', + [('n', 'new', [], _("successor changeset"))], + _('[OPTION] REV...')) +def kill(ui, repo, *revs, **opts): + """mark a changeset as obsolete + + This update the parent directory to a not-killed parent if the current + working directory parent are killed. + + XXX bookmark support + XXX handle merge + XXX check immutable first + """ + wlock = repo.wlock() + try: + new = set(noderange(repo, opts['new'])) + targetnodes = set(noderange(repo, revs)) + if not new: + new = [node.nullid] + for n in targetnodes: + if not repo[n].mutable(): + ui.warn(_("Can't kill immutable changeset %s") % repo[n]) + else: + for ne in new: + repo.addobsolete(ne, n) + # update to an unkilled parent + wdp = repo['.'] + newnode = wdp + while newnode.obsolete(): + newnode = newnode.parents()[0] + if newnode.node() != wdp.node(): + commands.update(ui, repo, newnode.rev()) + ui.status(_('working directory now at %s\n') % newnode) + + finally: + wlock.release() + +@command('^amend|refresh', + [('A', 'addremove', None, + _('mark new/missing files as added/removed before committing')), + ('n', 'note', '', _('use text as commit message for this update')), + ('c', 'change', '', _('specifies the changesets to amend'), _('REV')), + ('e', 'edit', False, _('invoke editor on commit messages')), + ] + walkopts + commitopts + commitopts2, + _('[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. + + If you specify --change, amend additionally considers all + changesets between the indicated changeset and the working copy + parent as updates to be subsumed. + + 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. + """ + + # determine updates to subsume + old = scmutil.revsingle(repo, opts.get('change') or '.') + + lock = repo.lock() + try: + wlock = repo.wlock() + try: + if old.phase() == phases.public: + raise util.Abort(_("can not rewrite immutable changeset %s") + % old) + oldphase = old.phase() + # commit current changes as update + # code copied from commands.commit to avoid noisy messages + ciopts = dict(opts) + ciopts.pop('message', None) + ciopts.pop('logfile', None) + ciopts['message'] = opts.get('note') or ('amends %s' % old.hex()) + e = cmdutil.commiteditor + def commitfunc(ui, repo, message, match, opts): + return repo.commit(message, opts.get('user'), opts.get('date'), + match, editor=e) + revcount = len(repo) + tempid = cmdutil.commit(ui, repo, commitfunc, pats, ciopts) + if len(repo) == revcount: + # No revision created + tempid = None + + # find all changesets to be considered updates + head = repo['.'] + updatenodes = set(repo.changelog.nodesbetween( + roots=[old.node()], heads=[head.node()])[0]) + updatenodes.remove(old.node()) + okoptions = ['message', 'logfile', 'edit', 'user'] + if not updatenodes: + for o in okoptions: + if opts.get(o): + break + else: + raise error.Abort(_('no updates found')) + updates = [repo[n] for n in updatenodes] + + # perform amend + if opts.get('edit'): + opts['force_editor'] = True + newid, created = rewrite(repo, old, updates, head, + [old.p1().node(), old.p2().node()], opts) + if created: + # reroute the working copy parent to the new changeset + phases.retractboundary(repo, oldphase, [newid]) + repo.dirstate.setparents(newid, node.nullid) + else: + # rewrite() recreated an existing revision, discard + # the intermediate revision if any. No need to update + # phases or parents. + if tempid is not None: + repo.addobsolete(node.nullid, tempid) + # XXX: need another message in collapse case. + raise error.Abort(_('no updates found')) + finally: + wlock.release() + finally: + lock.release() + +def _commitfiltered(repo, ctx, match): + """Recommit ctx with changed files not in match. Return the new + node identifier, or None if nothing changed. + """ + base = ctx.p1() + m, a, r = repo.status(base, ctx)[:3] + allfiles = set(m + a + r) + files = set(f for f in allfiles if not match(f)) + if files == allfiles: + return None + + # Filter copies + copied = copies.pathcopies(base, ctx) + copied = dict((src, dst) for src, dst in copied.iteritems() + if dst in files) + def filectxfn(repo, memctx, path): + if path not in ctx: + raise IOError() + fctx = ctx[path] + flags = fctx.flags() + mctx = context.memfilectx(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')), + ] + commands.walkopts, + _('[OPTION]... [NAME]')) +def uncommit(ui, repo, *pats, **opts): + """move changes from parent revision to working directory + + Changes to selected files in parent revision appear again as + uncommitted changed in the working directory. A new revision + without selected changes is created, becomes the new parent and + obsoletes the previous one. + + The --include option specify pattern to uncommit + The --exclude option specify pattern to keep in the commit + + Return 0 if changed files are uncommitted. + """ + lock = repo.lock() + try: + wlock = repo.wlock() + try: + wctx = repo[None] + if len(wctx.parents()) <= 0: + raise util.Abort(_("cannot uncommit null changeset")) + if len(wctx.parents()) > 1: + raise util.Abort(_("cannot uncommit while merging")) + old = repo['.'] + if old.phase() == phases.public: + raise util.Abort(_("cannot rewrite immutable changeset")) + if len(old.parents()) > 1: + raise util.Abort(_("cannot uncommit merge changeset")) + oldphase = old.phase() + updatebookmarks = _bookmarksupdater(repo, old.node()) + # Recommit the filtered changeset + newid = None + if (pats or opts.get('include') or opts.get('exclude') + or opts.get('all')): + match = scmutil.match(old, pats, opts) + newid = _commitfiltered(repo, old, match) + if newid is None: + raise util.Abort(_('nothing to uncommit')) + # Move local changes on filtered changeset + repo.addobsolete(newid, old.node()) + phases.retractboundary(repo, oldphase, [newid]) + repo.dirstate.setparents(newid, node.nullid) + _uncommitdirstate(repo, old, match) + updatebookmarks(newid) + if not repo[newid].files(): + ui.warn(_("new changeset is empty\n")) + ui.status(_('(use "hg kill ." to remove it)\n')) + finally: + wlock.release() + finally: + lock.release() + +def commitwrapper(orig, ui, repo, *arg, **kwargs): + 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 successed + new = repo['-1'] + oldbookmarks = [] + for old in obsoleted: + oldbookmarks.extend(repo.nodebookmarks(old.node())) + repo.addobsolete(new.node(), old.node()) + for book in oldbookmarks: + repo._bookmarks[book] = new.node() + if oldbookmarks: + bookmarks.write(repo) + return result + finally: + lock.release() + +def graftwrapper(orig, ui, repo, *revs, **kwargs): + lock = repo.lock() + try: + if kwargs.get('old_obsolete'): + obsoleted = kwargs.setdefault('obsolete', []) + if kwargs['continue']: + obsoleted.extend(repo.opener.read('graftstate').splitlines()) + else: + obsoleted.extend(revs) + # convert obsolete target into revs to avoid alias joke + obsoleted = kwargs.setdefault('obsolete', []) + obsoleted[:] = [str(i) for i in repo.revs('%lr', obsoleted)] + if obsoleted and len(revs) > 1: + + raise error.Abort(_('Can not graft multiple revision while ' + 'obsoleting (for now).')) + + return commitwrapper(orig, ui, repo,*revs, **kwargs) + finally: + lock.release() + +def extsetup(ui): + try: + obsolete = extensions.find('obsolete') + except KeyError: + raise error.Abort(_('evolution extension require obsolete extension.')) + try: + rebase = extensions.find('rebase') + except KeyError: + rebase = None + raise error.Abort(_('evolution extension require rebase extension.')) + + for cmd in ['amend', 'kill', 'uncommit']: + entry = extensions.wrapcommand(cmdtable, cmd, + obsolete.warnobserrors) + + entry = extensions.wrapcommand(commands.table, 'commit', commitwrapper) + entry[1].append(('o', 'obsolete', [], + _("make commit obsolete this revision"))) + entry = extensions.wrapcommand(commands.table, 'graft', graftwrapper) + entry[1].append(('o', 'obsolete', [], + _("make graft obsoletes this revision"))) + entry[1].append(('O', 'old-obsolete', False, + _("make graft obsoletes its source"))) +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/obsolete.py Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,849 @@ +# obsolete.py - introduce the obsolete concept in mercurial. +# +# Copyright 2011 Pierre-Yves David <pierre-yves.david@ens-lyon.org> +# Logilab SA <contact@logilab.fr> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +"""Introduce the Obsolete concept to mercurial + +General concept +=============== + +This extension introduces the *obsolete* concept. It adds a new *obsolete* +relation between two changesets. A relation ``<changeset B> obsolete <changeset +A>`` is set to denote that ``<changeset B>`` is new version of ``<changeset +A>``. + +The *obsolete* relation act as a **perpendicular history** to the standard +changeset history. Standard changeset history versions files. The *obsolete* +relation versions changesets. + +:obsolete: a changeset that has been replaced by another one. +:unstable: a changeset that is not obsolete but has an obsolete ancestor. +:suspended: an obsolete changeset with unstable descendant. +:extinct: an obsolete changeset without unstable descendant. + (subject to garbage collection) + +Another name for unstable could be out of sync. + + +Usage and Feature +================= + + +New commands +------------ + +Note that rebased changesets are not marked obsolete rather than being stripped +In this experimental extensions, this is done forcing the --keep option. Trying +to use the --keep option of rebase with this extensionn this experimental +extension will cause such a call to abort. Until better releasen please use +graft command to rebase and copy changesets. + +""" + +import os, sys +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from mercurial.i18n import _ + +import json + +import struct +from mercurial import util, base85 + +_pack = struct.pack +_unpack = struct.unpack + +from mercurial import util +from mercurial import context +from mercurial import revset +from mercurial import scmutil +from mercurial import extensions +from mercurial import pushkey +from mercurial import discovery +from mercurial import error +from mercurial import commands +from mercurial import changelog +from mercurial import phases +from mercurial.node import hex, bin, short, nullid +from mercurial.lock import release +from mercurial import localrepo +from mercurial import cmdutil +from mercurial import templatekw + +try: + from mercurial.localrepo import storecache + storecache('babar') # to trigger import +except (TypeError, ImportError): + def storecache(*args): + return scmutil.filecache(*args, instore=True) + + +### Patch changectx +############################# + +def unstable(ctx): + """is the changeset unstable (have obsolete ancestor)""" + if ctx.node() is None: + return False + return ctx.rev() in ctx._repo._unstableset + +context.changectx.unstable = unstable + +def extinct(ctx): + """is the changeset extinct by other""" + if ctx.node() is None: + return False + return ctx.rev() in ctx._repo._extinctset + +context.changectx.extinct = extinct + +def latecomer(ctx): + """is the changeset latecomer (Try to succeed to public change)""" + if ctx.node() is None: + return False + return ctx.rev() in ctx._repo._latecomerset + +context.changectx.latecomer = latecomer + +def conflicting(ctx): + """is the changeset conflicting (Try to succeed to public change)""" + if ctx.node() is None: + return False + return ctx.rev() in ctx._repo._conflictingset + +context.changectx.conflicting = conflicting + + +### revset +############################# + +def revsethidden(repo, subset, x): + """``hidden()`` + Changeset is hidden. + """ + args = revset.getargs(x, 0, 0, 'hidden takes no argument') + return [r for r in subset if r in repo.hiddenrevs] + +def revsetobsolete(repo, subset, x): + """``obsolete()`` + Changeset is obsolete. + """ + args = revset.getargs(x, 0, 0, 'obsolete takes no argument') + return [r for r in subset if r in repo._obsoleteset and repo._phasecache.phase(repo, r) > 0] + +# XXX Backward compatibility, to be removed once stabilized +if '_phasecache' not in vars(localrepo.localrepository): # new api + def revsetobsolete(repo, subset, x): + """``obsolete()`` + Changeset is obsolete. + """ + args = revset.getargs(x, 0, 0, 'obsolete takes no argument') + return [r for r in subset if r in repo._obsoleteset and repo._phaserev[r] > 0] + +def revsetunstable(repo, subset, x): + """``unstable()`` + Unstable changesets are non-obsolete with obsolete ancestors. + """ + args = revset.getargs(x, 0, 0, 'unstable takes no arguments') + return [r for r in subset if r in repo._unstableset] + +def revsetsuspended(repo, subset, x): + """``suspended()`` + Obsolete changesets with non-obsolete descendants. + """ + args = revset.getargs(x, 0, 0, 'suspended takes no arguments') + return [r for r in subset if r in repo._suspendedset] + +def revsetextinct(repo, subset, x): + """``extinct()`` + Obsolete changesets with obsolete descendants only. + """ + args = revset.getargs(x, 0, 0, 'extinct takes no arguments') + return [r for r in subset if r in repo._extinctset] + +def revsetlatecomer(repo, subset, x): + """``latecomer()`` + Changesets marked as successors of public changesets. + """ + args = revset.getargs(x, 0, 0, 'latecomer takes no arguments') + return [r for r in subset if r in repo._latecomerset] + +def revsetconflicting(repo, subset, x): + """``conflicting()`` + Changesets marked as successors of a same changeset. + """ + args = revset.getargs(x, 0, 0, 'conflicting takes no arguments') + return [r for r in subset if r in repo._conflictingset] + +def _precursors(repo, s): + """Precursor of a changeset""" + cs = set() + nm = repo.changelog.nodemap + markerbysubj = repo.obsstore.successors + for r in s: + for p in markerbysubj.get(repo[r].node(), ()): + pr = nm.get(p[0]) + if pr is not None: + cs.add(pr) + return cs + +def revsetprecursors(repo, subset, x): + """``precursors(set)`` + Immediate precursors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _precursors(repo, s) + return [r for r in subset if r in cs] + +def _allprecursors(repo, s): # XXX we need a better naming + """transitive precursors of a subset""" + toproceed = [repo[r].node() for r in s] + seen = set() + allsubjects = repo.obsstore.successors + 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) + return cs + +def revsetallprecursors(repo, subset, x): + """``allprecursors(set)`` + Transitive precursors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _allprecursors(repo, s) + return [r for r in subset if r in cs] + +def _successors(repo, s): + """Successors of a changeset""" + cs = set() + nm = repo.changelog.nodemap + markerbyobj = repo.obsstore.precursors + for r in s: + for p in markerbyobj.get(repo[r].node(), ()): + for sub in p[1]: + sr = nm.get(sub) + if sr is not None: + cs.add(sr) + return cs + +def revsetsuccessors(repo, subset, x): + """``successors(set)`` + Immediate successors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _successors(repo, s) + return [r for r in subset if r in cs] + +def _allsuccessors(repo, s): # XXX we need a better naming + """transitive successors of a subset""" + toproceed = [repo[r].node() for r in s] + seen = set() + allobjects = repo.obsstore.precursors + while toproceed: + nc = toproceed.pop() + for mark in allobjects.get(nc, ()): + for sub in mark[1]: + 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) + return cs + +def revsetallsuccessors(repo, subset, x): + """``allsuccessors(set)`` + Transitive successors of changesets in set. + """ + s = revset.getset(repo, range(len(repo)), x) + cs = _allsuccessors(repo, s) + return [r for r in subset if r in cs] + + +### template keywords +##################### + +def obsoletekw(repo, ctx, templ, **args): + """:obsolete: String. The obsolescence level of the node, could be + ``stable``, ``unstable``, ``suspended`` or ``extinct``. + """ + rev = ctx.rev() + if rev in repo._extinctset: + return 'extinct' + if rev in repo._suspendedset: + return 'suspended' + if rev in repo._unstableset: + return 'unstable' + return 'stable' + +### Other Extension compat +############################ + + +def buildstate(orig, repo, dest, rebaseset, *ags, **kws): + """wrapper for rebase 's buildstate that exclude obsolete changeset""" + rebaseset = repo.revs('%ld - extinct()', rebaseset) + return orig(repo, dest, rebaseset, *ags, **kws) + +def defineparents(orig, repo, rev, target, state, *args, **kwargs): + rebasestate = getattr(repo, '_rebasestate', None) + if rebasestate is not None: + repo._rebasestate = dict(state) + repo._rebasetarget = target + return orig(repo, rev, target, state, *args, **kwargs) + +def concludenode(orig, repo, rev, p1, *args, **kwargs): + """wrapper for rebase 's concludenode that set obsolete relation""" + newrev = orig(repo, rev, p1, *args, **kwargs) + rebasestate = getattr(repo, '_rebasestate', None) + if rebasestate is not None: + if newrev is not None: + nrev = repo[newrev].rev() + else: + nrev = p1 + repo._rebasestate[rev] = nrev + return newrev + +def cmdrebase(orig, ui, repo, *args, **kwargs): + + reallykeep = kwargs.get('keep', False) + kwargs = dict(kwargs) + kwargs['keep'] = True + + # We want to mark rebased revision as obsolete and set their + # replacements if any. Doing it in concludenode() prevents + # aborting the rebase, and is not called with all relevant + # revisions in --collapse case. Instead, we try to track the + # rebase state structure by sampling/updating it in + # defineparents() and concludenode(). The obsolete markers are + # added from this state after a successful call. + repo._rebasestate = {} + repo._rebasetarget = None + try: + res = orig(ui, repo, *args, **kwargs) + if not reallykeep: + # Filter nullmerge or unrebased entries + repo._rebasestate = dict(p for p in repo._rebasestate.iteritems() + if p[1] >= 0) + if not res and not kwargs.get('abort') and repo._rebasestate: + # Rebased revisions are assumed to be descendants of + # targetrev. If a source revision is mapped to targetrev + # or to another rebased revision, it must have been + # removed. + targetrev = repo[repo._rebasetarget].rev() + newrevs = set([targetrev]) + replacements = {} + for rev, newrev in sorted(repo._rebasestate.items()): + oldnode = repo[rev].node() + if newrev not in newrevs: + newnode = repo[newrev].node() + newrevs.add(newrev) + else: + newnode = nullid + replacements[oldnode] = newnode + + if kwargs.get('collapse'): + newnodes = set(n for n in replacements.values() if n != nullid) + if newnodes: + # Collapsing into more than one revision? + assert len(newnodes) == 1, newnodes + newnode = newnodes.pop() + else: + newnode = nullid + repo.addcollapsedobsolete(replacements, newnode) + else: + for oldnode, newnode in replacements.iteritems(): + repo.addobsolete(newnode, oldnode) + return res + finally: + delattr(repo, '_rebasestate') + delattr(repo, '_rebasetarget') + + +def extsetup(ui): + + revset.symbols["hidden"] = revsethidden + revset.symbols["obsolete"] = revsetobsolete + revset.symbols["unstable"] = revsetunstable + revset.symbols["suspended"] = revsetsuspended + revset.symbols["extinct"] = revsetextinct + revset.symbols["latecomer"] = revsetlatecomer + revset.symbols["conflicting"] = revsetconflicting + revset.symbols["obsparents"] = revsetprecursors # DEPR + revset.symbols["precursors"] = revsetprecursors + revset.symbols["obsancestors"] = revsetallprecursors # DEPR + revset.symbols["allprecursors"] = revsetallprecursors # bad name + revset.symbols["successors"] = revsetsuccessors + revset.symbols["allsuccessors"] = revsetallsuccessors # bad name + + templatekw.keywords['obsolete'] = obsoletekw + + # warning about more obsolete + for cmd in ['commit', 'push', 'pull', 'graft', 'phase', 'unbundle']: + entry = extensions.wrapcommand(commands.table, cmd, warnobserrors) + try: + rebase = extensions.find('rebase') + if rebase: + entry = extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) + extensions.wrapfunction(rebase, 'buildstate', buildstate) + extensions.wrapfunction(rebase, 'defineparents', defineparents) + extensions.wrapfunction(rebase, 'concludenode', concludenode) + extensions.wrapcommand(rebase.cmdtable, "rebase", cmdrebase) + except KeyError: + pass # rebase not found + +### Discovery wrapping +############################# + + +def wrapcheckheads(orig, repo, remote, outgoing, *args, **kwargs): + """wrap mercurial.discovery.checkheads + + * prevent unstability to be pushed + * patch remote to ignore obsolete heads on remote + """ + # do not push instability + for h in outgoing.missingheads: + # Checking heads is enough, obsolete descendants are either + # obsolete or unstable. + ctx = repo[h] + if ctx.latecomer(): + raise util.Abort(_("push includes a latecomer changeset: %s!") + % ctx) + if ctx.conflicting(): + raise util.Abort(_("push includes a conflicting changeset: %s!") + % ctx) + return orig(repo, remote, outgoing, *args, **kwargs) + +def wrapclearcache(orig, repo, *args, **kwargs): + try: + return orig(repo, *args, **kwargs) + finally: + repo._clearobsoletecache() + + +### New commands +############################# + +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, 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.sopener.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, 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) + +@command('debugsuccessors', [], '') +def cmddebugsuccessors(ui, repo): + """dump obsolete changesets and their successors + + Each line matches an existing marker, the first identifier is the + obsolete changeset identifier, followed by it successors. + """ + lock = repo.lock() + try: + allsuccessors = repo.obsstore.precursors + for old in sorted(allsuccessors): + successors = [sorted(m[1]) for m in allsuccessors[old]] + for i, group in enumerate(sorted(successors)): + ui.write('%s' % short(old)) + for new in group: + ui.write(' %s' % short(new)) + ui.write('\n') + finally: + lock.release() + +### Altering existing command +############################# + +def wrapmayobsoletewc(origfn, ui, repo, *args, **opts): + res = origfn(ui, repo, *args, **opts) + if repo['.'].obsolete(): + ui.warn(_('Working directory parent is obsolete\n')) + return res + +def warnobserrors(orig, ui, repo, *args, **kwargs): + """display warning is the command resulted in more instable changeset""" + priorunstables = len(repo.revs('unstable()')) + priorlatecomers = len(repo.revs('latecomer()')) + priorconflictings = len(repo.revs('conflicting()')) + #print orig, priorunstables + #print len(repo.revs('secret() - obsolete()')) + try: + return orig(ui, repo, *args, **kwargs) + finally: + newunstables = len(repo.revs('unstable()')) - priorunstables + newlatecomers = len(repo.revs('latecomer()')) - priorlatecomers + newconflictings = len(repo.revs('conflicting()')) - priorconflictings + #print orig, newunstables + #print len(repo.revs('secret() - obsolete()')) + if newunstables > 0: + ui.warn(_('%i new unstables changesets\n') % newunstables) + if newlatecomers > 0: + ui.warn(_('%i new latecomers changesets\n') % newlatecomers) + if newconflictings > 0: + ui.warn(_('%i new conflictings changesets\n') % newconflictings) + +def wrapcmdutilamend(orig, ui, repo, commitfunc, old, *args, **kwargs): + oldnode = old.node() + new = orig(ui, repo, commitfunc, old, *args, **kwargs) + if new != oldnode: + lock = repo.lock() + try: + tr = repo.transaction('post-amend-obst') + try: + meta = { + 'date': '%i %i' % util.makedate(), + 'user': ui.username(), + } + repo.obsstore.create(tr, oldnode, [new], 0, meta) + tr.close() + repo._clearobsoletecache() + finally: + tr.release() + finally: + lock.release() + return new + +def uisetup(ui): + extensions.wrapcommand(commands.table, "update", wrapmayobsoletewc) + extensions.wrapcommand(commands.table, "pull", wrapmayobsoletewc) + if util.safehasattr(cmdutil, 'amend'): + extensions.wrapfunction(cmdutil, 'amend', wrapcmdutilamend) + extensions.wrapfunction(discovery, 'checkheads', wrapcheckheads) + extensions.wrapfunction(phases, 'advanceboundary', wrapclearcache) + +### serialisation +############################# + +def _obsserialise(obssubrels, flike): + """serialise an obsolete relation mapping in a plain text one + + this is for subject -> [objects] mapping + + format is:: + + <subject-full-hex> <object-full-hex>\n""" + for sub, objs in obssubrels.iteritems(): + for obj in objs: + if sub is None: + sub = nullid + flike.write('%s %s\n' % (hex(sub), hex(obj))) + +def _obsdeserialise(flike): + """read a file like object serialised with _obsserialise + + this desierialize into a {subject -> objects} mapping""" + 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 + +### diagnostique tools +############################# + +def unstables(repo): + """Return all unstable changeset""" + return scmutil.revrange(repo, ['obsolete():: and (not obsolete())']) + +def newerversion(repo, obs): + """Return the newer version of an obsolete changeset""" + toproceed = set([(obs,)]) + # XXX known optimization available + newer = set() + objectrels = repo.obsstore.precursors + while toproceed: + current = toproceed.pop() + assert len(current) <= 1, 'splitting not handled yet. %r' % current + current = [n for n in current if n != nullid] + if current: + n, = current + if n in objectrels: + markers = objectrels[n] + for mark in markers: + toproceed.add(tuple(mark[1])) + else: + newer.add(tuple(current)) + else: + newer.add(()) + return sorted(newer) + +### repo subclassing +############################# + +def reposetup(ui, repo): + if not repo.local(): + return + + if not util.safehasattr(repo.opener, 'tryread'): + raise util.Abort('Obsolete extension require Mercurial 2.2 (or later)') + opush = repo.push + o_updatebranchcache = repo.updatebranchcache + + # /!\ api change in Hg 2.2 (97efd26eb9576f39590812ea9) /!\ + if util.safehasattr(repo, '_journalfiles'): # Hg 2.2 + o_journalfiles = repo._journalfiles + o_writejournal = repo._writejournal + + + class obsoletingrepo(repo.__class__): + + ### Public method + def obsoletedby(self, node): + """return the set of node that make <node> obsolete (obj)""" + others = set() + for marker in self.obsstore.precursors.get(node, []): + others.update(marker[1]) + return others + + def obsolete(self, node): + """return the set of node that <node> make obsolete (sub)""" + return set(marker[0] for marker in self.obsstore.successors.get(node, [])) + + @util.propertycache + def _obsoleteset(self): + """the set of obsolete revision""" + obs = set() + nm = self.changelog.nodemap + for prec in self.obsstore.precursors: + rev = nm.get(prec) + if rev is not None: + obs.add(rev) + return obs + + @util.propertycache + def _unstableset(self): + """the set of non obsolete revision with obsolete parent""" + return set(self.revs('(obsolete()::) - obsolete()')) + + @util.propertycache + def _suspendedset(self): + """the set of obsolete parent with non obsolete descendant""" + return set(self.revs('obsolete() and obsolete()::unstable()')) + + @util.propertycache + def _extinctset(self): + """the set of obsolete parent without non obsolete descendant""" + return set(self.revs('obsolete() - obsolete()::unstable()')) + + @util.propertycache + def _latecomerset(self): + """the set of rev trying to obsolete public revision""" + query = 'allsuccessors(public()) - obsolete() - public()' + return set(self.revs(query)) + + @util.propertycache + def _conflictingset(self): + """the set of rev trying to obsolete public revision""" + conflicting = set() + obsstore = self.obsstore + newermap = {} + for ctx in self.set('(not public()) - obsolete()'): + prec = obsstore.successors.get(ctx.node(), ()) + toprocess = set(prec) + while toprocess: + prec = toprocess.pop()[0] + if prec not in newermap: + newermap[prec] = newerversion(self, prec) + newer = [n for n in newermap[prec] if n] # filter kill + if len(newer) > 1: + conflicting.add(ctx.rev()) + break + toprocess.update(obsstore.successors.get(prec, ())) + return conflicting + + def _clearobsoletecache(self): + if '_obsoleteset' in vars(self): + del self._obsoleteset + self._clearunstablecache() + + def updatebranchcache(self): + o_updatebranchcache() + self._clearunstablecache() + + def _clearunstablecache(self): + if '_unstableset' in vars(self): + del self._unstableset + if '_suspendedset' in vars(self): + del self._suspendedset + if '_extinctset' in vars(self): + del self._extinctset + if '_latecomerset' in vars(self): + del self._latecomerset + if '_conflictingset' in vars(self): + del self._conflictingset + + def addobsolete(self, sub, obj): + """Add a relation marking that node <sub> is a new version of <obj>""" + assert sub != obj + if not repo[obj].phase(): + if sub is None: + self.ui.warn( + _("trying to kill immutable changeset %(obj)s\n") + % {'obj': short(obj)}) + if sub is not None: + self.ui.warn( + _("%(sub)s try to obsolete immutable changeset %(obj)s\n") + % {'sub': short(sub), 'obj': short(obj)}) + lock = self.lock() + try: + tr = self.transaction('add-obsolete') + try: + meta = { + 'date': '%i %i' % util.makedate(), + 'user': ui.username(), + } + subs = (sub == nullid) and [] or [sub] + mid = self.obsstore.create(tr, obj, subs, 0, meta) + tr.close() + self._clearobsoletecache() + return mid + finally: + tr.release() + finally: + lock.release() + + def addcollapsedobsolete(self, oldnodes, newnode): + """Mark oldnodes as collapsed into newnode.""" + # Assume oldnodes are all descendants of a single rev + rootrevs = self.revs('roots(%ln)', oldnodes) + assert len(rootrevs) == 1, rootrevs + #rootnode = self[rootrevs[0]].node() + for n in oldnodes: + self.addobsolete(newnode, n) + + ### pull // push support + + def push(self, remote, *args, **opts): + """wrapper around pull that pull obsolete relation""" + try: + result = opush(remote, *args, **opts) + except util.Abort, ex: + hint = _("use 'hg stabilize' 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 + return result + + + repo.__class__ = obsoletingrepo + for arg in sys.argv: + if 'debugc' in arg: + break + else: + data = repo.opener.tryread('obsolete-relations') + if not data: + data = repo.sopener.tryread('obsoletemarkers') + if data: + raise util.Abort('old format of obsolete marker detected!\n' + 'run `hg debugconvertobsolete` once.')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/qsync.py Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,261 @@ +"""synchronize patches queues and evolving changesets""" + +import re +from cStringIO import StringIO +import json + +from mercurial.i18n import _ +from mercurial import commands +from mercurial import patch +from mercurial import util +from mercurial.node import nullid, hex, short, bin +from mercurial import cmdutil +from mercurial import hg +from mercurial import scmutil +from mercurial import error +from mercurial import extensions +from mercurial import phases + +### old compat code +############################# + +BRANCHNAME="qsubmit2" + +### new command +############################# +cmdtable = {} +command = cmdutil.command(cmdtable) + +@command('^qsync|sync', + [ + ('a', 'review-all', False, _('mark all touched patches ready for review (no editor)')), + ], + '') +def cmdsync(ui, repo, **opts): + '''Export draft changeset as mq patch in a mq patches repository commit. + + This command get all changesets in draft phase and create an mq changeset: + + * on a "qsubmit2" branch (based on the last changeset) + + * one patch per draft changeset + + * a series files listing all generated patch + + * qsubmitdata holding useful information + + It does use obsolete relation to update patches that already existing in the qsubmit2 branch. + + Already existing patch which became public, draft or got killed are remove from the mq repo. + + Patch name are generated using the summary line for changeset description. + + .. warning:: Series files is ordered topologically. So two series with + interleaved changeset will appear interleaved. + ''' + + review = 'edit' + if opts['review_all']: + review = 'all' + mqrepo = repo.mq.qrepo() + if mqrepo is None: + raise util.Abort('No patches repository') + + try: + parent = mqrepo[BRANCHNAME] + except error.RepoLookupError: + parent = initqsubmit(mqrepo) + store, data, touched = fillstore(repo, parent) + if not touched: + raise util.Abort('Nothing changed') + files = ['qsubmitdata', 'series'] + touched + # mark some as ready for review + message = 'qsubmit commit\n\n' + review_list = [] + applied_list = [] + if review: + olddata = get_old_data(parent) + oldfiles = dict([(name, bin(ctxhex)) for ctxhex, name in olddata]) + + for patch_name in touched: + try: + store.getfile(patch_name) + review_list.append(patch_name) + except IOError: + oldnode = oldfiles[patch_name] + obsolete = extensions.find('obsolete') + newnodes = obsolete.newerversion(repo, oldnode) + if newnodes: + newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing + if not newnodes: + # changeset has been killed (eg. reject) + pass + else: + assert len(newnodes) == 1 # conflict!!! + newnode = newnodes[0] + assert len(newnode) == 1 # split unsupported for now + newnode = list(newnode)[0] + # XXX unmanaged case where a cs is obsoleted by an unavailable one + #if newnode.node() not in repo.changelog.nodemap: + # raise util.Abort('%s is obsoleted by an unknown node %s'% (oldnode, newnode)) + ctx = repo[newnode] + if ctx.phase() == phases.public: + # applied + applied_list.append(patch_name) + elif ctx.phase() == phases.secret: + # already exported changeset is now secret + repo.ui.warn("An already exported changeset is now secret!!!") + else: + # draft + assert False, "Should be exported" + + if review: + if applied_list: + message += '\n'.join('* applied %s' % x for x in applied_list) + '\n' + if review_list: + message += '\n'.join('* %s ready for review' % x for x in review_list) + '\n' + memctx = patch.makememctx(mqrepo, (parent.node(), nullid), + message, + None, + None, + parent.branch(), files, store, + editor=None) + if review == 'edit': + memctx._text = cmdutil.commitforceeditor(mqrepo, memctx, []) + mqrepo.savecommitmessage(memctx.description()) + n = memctx.commit() + return 0 + + +def makename(ctx): + """create a patch name form a changeset""" + descsummary = ctx.description().splitlines()[0] + descsummary = re.sub(r'\s+', '_', descsummary) + descsummary = re.sub(r'\W+', '', descsummary) + if len(descsummary) > 45: + descsummary = descsummary[:42] + '.' + return '%s-%s.diff' % (ctx.branch().upper(), descsummary) + + +def get_old_data(mqctx): + """read qsubmit data to fetch previous export data + + get old data from the content of an mq commit""" + try: + old_data = mqctx['qsubmitdata'] + return json.loads(old_data.data()) + except error.LookupError: + return [] + +def get_current_data(repo): + """Return what would be exported if no previous data exists""" + data = [] + for ctx in repo.set('draft() - (obsolete() + merge())'): + name = makename(ctx) + data.append([ctx.hex(), makename(ctx)]) + merges = repo.revs('draft() and merge()') + if merges: + repo.ui.warn('ignoring %i merge\n' % len(merges)) + return data + + +def patchmq(repo, store, olddata, newdata): + """export the mq patches and return all useful data to be exported""" + finaldata = [] + touched = set() + currentdrafts = set(d[0] for d in newdata) + usednew = set() + usedold = set() + obsolete = extensions.find('obsolete') + for oldhex, oldname in olddata: + if oldhex in usedold: + continue # no duplicate + usedold.add(oldhex) + oldname = str(oldname) + oldnode = bin(oldhex) + newnodes = obsolete.newerversion(repo, oldnode) + if newnodes: + newnodes = [n for n in newnodes if n and n[0] in repo] # remove killing + if len(newnodes) > 1: + newnodes = [short(nodes[0]) for nodes in newnodes] + raise util.Abort('%s have more than one newer version: %s'% (oldname, newnodes)) + if newnodes: + # else, changeset have been killed + newnode = list(newnodes)[0][0] + ctx = repo[newnode] + if ctx.hex() != oldhex and ctx.phase(): + fp = StringIO() + cmdutil.export(repo, [ctx.rev()], fp=fp) + data = fp.getvalue() + store.setfile(oldname, data, (None, None)) + finaldata.append([ctx.hex(), oldname]) + usednew.add(ctx.hex()) + touched.add(oldname) + continue + if oldhex in currentdrafts: + # else changeset is now public or secret + finaldata.append([oldhex, oldname]) + usednew.add(ctx.hex()) + continue + touched.add(oldname) + + for newhex, newname in newdata: + if newhex in usednew: + continue + newnode = bin(newhex) + ctx = repo[newnode] + fp = StringIO() + cmdutil.export(repo, [ctx.rev()], fp=fp) + data = fp.getvalue() + store.setfile(newname, data, (None, None)) + finaldata.append([ctx.hex(), newname]) + touched.add(newname) + # sort by branchrev number + finaldata.sort(key=lambda x: sort_key(repo[x[0]])) + # sort touched too (ease review list) + stouched = [f[1] for f in finaldata if f[1] in touched] + stouched += [x for x in touched if x not in stouched] + return finaldata, stouched + +def sort_key(ctx): + """ctx sort key: (branch, rev)""" + return (ctx.branch(), ctx.rev()) + + +def fillstore(repo, basemqctx): + """fill store with patch data""" + olddata = get_old_data(basemqctx) + newdata = get_current_data(repo) + store = patch.filestore() + try: + data, touched = patchmq(repo, store, olddata, newdata) + # put all name in the series + series ='\n'.join(d[1] for d in data) + '\n' + store.setfile('series', series, (False, False)) + + # export data to ease futur work + store.setfile('qsubmitdata', json.dumps(data, indent=True), + (False, False)) + finally: + store.close() + return store, data, touched + + +def initqsubmit(mqrepo): + """create initial qsubmit branch""" + store = patch.filestore() + try: + files = set() + store.setfile('DO-NOT-EDIT-THIS-WORKING-COPY-BY-HAND', 'WE WARNED YOU!', (False, False)) + store.setfile('.hgignore', '^status$\n', (False, False)) + memctx = patch.makememctx(mqrepo, (nullid, nullid), + 'qsubmit init', + None, + None, + BRANCHNAME, ('.hgignore',), store, + editor=None) + mqrepo.savecommitmessage(memctx.description()) + n = memctx.commit() + finally: + store.close() + return mqrepo[n]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qsync-enable.sh Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,20 @@ +#!/bin/sh + +here=`readlink -f "$0"` +repo_root=`dirname "$here"` + + + +cat << EOF >&2 +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +XXX Add lines below to the [extensions] section of you hgrc XXX +XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + + +EOF + +cat << EOF | sed -e "s#XXXREPOPATHXXX#${repo_root}#" +[extensions] +# experimental extensions for mq export +qsync=XXXREPOPATHXXX/hgext/qsync.py +EOF
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/killdaemons.py Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,25 @@ +#!/usr/bin/env python + +import os, time, errno, signal + +# Kill off any leftover daemon processes +try: + fp = open(os.environ['DAEMON_PIDS']) + for line in fp: + try: + pid = int(line) + except ValueError: + continue + try: + os.kill(pid, 0) + os.kill(pid, signal.SIGTERM) + for i in range(10): + time.sleep(0.05) + os.kill(pid, 0) + os.kill(pid, signal.SIGKILL) + except OSError, err: + if err.errno != errno.ESRCH: + raise + fp.close() +except IOError: + pass
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/run-tests.py Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,1128 @@ +#!/usr/bin/env python +# +# run-tests.py - Run a set of tests on Mercurial +# +# Copyright 2006 Matt Mackall <mpm@selenic.com> +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +# Modifying this script is tricky because it has many modes: +# - serial (default) vs parallel (-jN, N > 1) +# - no coverage (default) vs coverage (-c, -C, -s) +# - temp install (default) vs specific hg script (--with-hg, --local) +# - tests are a mix of shell scripts and Python scripts +# +# If you change this script, it is recommended that you ensure you +# haven't broken it by running it in various modes with a representative +# sample of test scripts. For example: +# +# 1) serial, no coverage, temp install: +# ./run-tests.py test-s* +# 2) serial, no coverage, local hg: +# ./run-tests.py --local test-s* +# 3) serial, coverage, temp install: +# ./run-tests.py -c test-s* +# 4) serial, coverage, local hg: +# ./run-tests.py -c --local test-s* # unsupported +# 5) parallel, no coverage, temp install: +# ./run-tests.py -j2 test-s* +# 6) parallel, no coverage, local hg: +# ./run-tests.py -j2 --local test-s* +# 7) parallel, coverage, temp install: +# ./run-tests.py -j2 -c test-s* # currently broken +# 8) parallel, coverage, local install: +# ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) +# 9) parallel, custom tmp dir: +# ./run-tests.py -j2 --tmpdir /tmp/myhgtests +# +# (You could use any subset of the tests: test-s* happens to match +# enough that it's worth doing parallel runs, few enough that it +# completes fairly quickly, includes both shell and Python scripts, and +# includes some scripts that run daemon processes.) + +from distutils import version +import difflib +import errno +import optparse +import os +import shutil +import subprocess +import signal +import sys +import tempfile +import time +import re + +closefds = os.name == 'posix' +def Popen4(cmd, bufsize=-1): + p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, + close_fds=closefds, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + p.fromchild = p.stdout + p.tochild = p.stdin + p.childerr = p.stderr + return p + +# reserved exit code to skip test (used by hghave) +SKIPPED_STATUS = 80 +SKIPPED_PREFIX = 'skipped: ' +FAILED_PREFIX = 'hghave check failed: ' +PYTHON = sys.executable +IMPL_PATH = 'PYTHONPATH' +if 'java' in sys.platform: + IMPL_PATH = 'JYTHONPATH' + +requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"] + +defaults = { + 'jobs': ('HGTEST_JOBS', 1), + 'timeout': ('HGTEST_TIMEOUT', 180), + 'port': ('HGTEST_PORT', 20059), +} + +def parseargs(): + parser = optparse.OptionParser("%prog [options] [tests]") + + # keep these sorted + parser.add_option("--blacklist", action="append", + help="skip tests listed in the specified blacklist file") + parser.add_option("-C", "--annotate", action="store_true", + help="output files annotated with coverage") + parser.add_option("--child", type="int", + help="run as child process, summary to given fd") + parser.add_option("-c", "--cover", action="store_true", + help="print a test coverage report") + parser.add_option("-d", "--debug", action="store_true", + help="debug mode: write output of test scripts to console" + " rather than capturing and diff'ing it (disables timeout)") + parser.add_option("-f", "--first", action="store_true", + help="exit on the first test failure") + parser.add_option("--inotify", action="store_true", + help="enable inotify extension when running tests") + parser.add_option("-i", "--interactive", action="store_true", + help="prompt to accept changed output") + parser.add_option("-j", "--jobs", type="int", + help="number of jobs to run in parallel" + " (default: $%s or %d)" % defaults['jobs']) + parser.add_option("--keep-tmpdir", action="store_true", + help="keep temporary directory after running tests") + parser.add_option("-k", "--keywords", + help="run tests matching keywords") + parser.add_option("-l", "--local", action="store_true", + help="shortcut for --with-hg=<testdir>/../hg") + parser.add_option("-n", "--nodiff", action="store_true", + help="skip showing test changes") + parser.add_option("-p", "--port", type="int", + help="port on which servers should listen" + " (default: $%s or %d)" % defaults['port']) + parser.add_option("--pure", action="store_true", + help="use pure Python code instead of C extensions") + parser.add_option("-R", "--restart", action="store_true", + help="restart at last error") + parser.add_option("-r", "--retest", action="store_true", + help="retest failed tests") + parser.add_option("-S", "--noskips", action="store_true", + help="don't report skip tests verbosely") + parser.add_option("-t", "--timeout", type="int", + help="kill errant tests after TIMEOUT seconds" + " (default: $%s or %d)" % defaults['timeout']) + parser.add_option("--tmpdir", type="string", + help="run tests in the given temporary directory" + " (implies --keep-tmpdir)") + parser.add_option("-v", "--verbose", action="store_true", + help="output verbose messages") + parser.add_option("--view", type="string", + help="external diff viewer") + parser.add_option("--with-hg", type="string", + metavar="HG", + help="test using specified hg script rather than a " + "temporary installation") + parser.add_option("-3", "--py3k-warnings", action="store_true", + help="enable Py3k warnings on Python 2.6+") + + for option, default in defaults.items(): + defaults[option] = int(os.environ.get(*default)) + parser.set_defaults(**defaults) + (options, args) = parser.parse_args() + + # jython is always pure + if 'java' in sys.platform or '__pypy__' in sys.modules: + options.pure = True + + if options.with_hg: + if not (os.path.isfile(options.with_hg) and + os.access(options.with_hg, os.X_OK)): + parser.error('--with-hg must specify an executable hg script') + if not os.path.basename(options.with_hg) == 'hg': + sys.stderr.write('warning: --with-hg should specify an hg script') + if options.local: + testdir = os.path.dirname(os.path.realpath(sys.argv[0])) + hgbin = os.path.join(os.path.dirname(testdir), 'hg') + if not os.access(hgbin, os.X_OK): + parser.error('--local specified, but %r not found or not executable' + % hgbin) + options.with_hg = hgbin + + options.anycoverage = options.cover or options.annotate + if options.anycoverage: + try: + import coverage + covver = version.StrictVersion(coverage.__version__).version + if covver < (3, 3): + parser.error('coverage options require coverage 3.3 or later') + except ImportError: + parser.error('coverage options now require the coverage package') + + if options.anycoverage and options.local: + # this needs some path mangling somewhere, I guess + parser.error("sorry, coverage options do not work when --local " + "is specified") + + global vlog + if options.verbose: + if options.jobs > 1 or options.child is not None: + pid = "[%d]" % os.getpid() + else: + pid = None + def vlog(*msg): + if pid: + print pid, + for m in msg: + print m, + print + sys.stdout.flush() + else: + vlog = lambda *msg: None + + if options.tmpdir: + options.tmpdir = os.path.expanduser(options.tmpdir) + + if options.jobs < 1: + parser.error('--jobs must be positive') + if options.interactive and options.jobs > 1: + print '(--interactive overrides --jobs)' + options.jobs = 1 + if options.interactive and options.debug: + parser.error("-i/--interactive and -d/--debug are incompatible") + if options.debug: + if options.timeout != defaults['timeout']: + sys.stderr.write( + 'warning: --timeout option ignored with --debug\n') + options.timeout = 0 + if options.py3k_warnings: + if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): + parser.error('--py3k-warnings can only be used on Python 2.6+') + if options.blacklist: + blacklist = dict() + for filename in options.blacklist: + try: + path = os.path.expanduser(os.path.expandvars(filename)) + f = open(path, "r") + except IOError, err: + if err.errno != errno.ENOENT: + raise + print "warning: no such blacklist file: %s" % filename + continue + + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + blacklist[line] = filename + + f.close() + + options.blacklist = blacklist + + return (options, args) + +def rename(src, dst): + """Like os.rename(), trade atomicity and opened files friendliness + for existing destination support. + """ + shutil.copy(src, dst) + os.remove(src) + +def splitnewlines(text): + '''like str.splitlines, but only split on newlines. + keep line endings.''' + i = 0 + lines = [] + while True: + n = text.find('\n', i) + if n == -1: + last = text[i:] + if last: + lines.append(last) + return lines + lines.append(text[i:n + 1]) + i = n + 1 + +def parsehghaveoutput(lines): + '''Parse hghave log lines. + Return tuple of lists (missing, failed): + * the missing/unknown features + * the features for which existence check failed''' + missing = [] + failed = [] + for line in lines: + if line.startswith(SKIPPED_PREFIX): + line = line.splitlines()[0] + missing.append(line[len(SKIPPED_PREFIX):]) + elif line.startswith(FAILED_PREFIX): + line = line.splitlines()[0] + failed.append(line[len(FAILED_PREFIX):]) + + return missing, failed + +def showdiff(expected, output, ref, err): + try: + for line in difflib.unified_diff(expected, output, ref, err): + sys.stdout.write(line) + except IOError, ex: + print >>sys.stderr, 'BORKEN PIPE', ex.errno + pass + +def findprogram(program): + """Search PATH for a executable program""" + for p in os.environ.get('PATH', os.defpath).split(os.pathsep): + name = os.path.join(p, program) + if os.access(name, os.X_OK): + return name + return None + +def checktools(): + # Before we go any further, check for pre-requisite tools + # stuff from coreutils (cat, rm, etc) are not tested + for p in requiredtools: + if os.name == 'nt': + p += '.exe' + found = findprogram(p) + if found: + vlog("# Found prerequisite", p, "at", found) + else: + print "WARNING: Did not find prerequisite tool: "+p + +def killdaemons(): + # Kill off any leftover daemon processes + try: + fp = open(DAEMON_PIDS) + for line in fp: + try: + pid = int(line) + except ValueError: + continue + try: + os.kill(pid, 0) + vlog('# Killing daemon process %d' % pid) + os.kill(pid, signal.SIGTERM) + time.sleep(0.25) + os.kill(pid, 0) + vlog('# Daemon process %d is stuck - really killing it' % pid) + os.kill(pid, signal.SIGKILL) + except OSError, err: + if err.errno != errno.ESRCH: + raise + fp.close() + os.unlink(DAEMON_PIDS) + except IOError: + pass + +def cleanup(options): + if not options.keep_tmpdir: + vlog("# Cleaning up HGTMP", HGTMP) + shutil.rmtree(HGTMP, True) + +def usecorrectpython(): + # some tests run python interpreter. they must use same + # interpreter we use or bad things will happen. + exedir, exename = os.path.split(sys.executable) + if exename == 'python': + path = findprogram('python') + if os.path.dirname(path) == exedir: + return + vlog('# Making python executable in test path use correct Python') + mypython = os.path.join(BINDIR, 'python') + try: + os.symlink(sys.executable, mypython) + except AttributeError: + # windows fallback + shutil.copyfile(sys.executable, mypython) + shutil.copymode(sys.executable, mypython) + +def installhg(options): + vlog("# Performing temporary installation of HG") + installerrs = os.path.join("tests", "install.err") + pure = options.pure and "--pure" or "" + + # Run installer in hg root + script = os.path.realpath(sys.argv[0]) + hgroot = os.path.dirname(os.path.dirname(script)) + os.chdir(hgroot) + nohome = '--home=""' + if os.name == 'nt': + # The --home="" trick works only on OS where os.sep == '/' + # because of a distutils convert_path() fast-path. Avoid it at + # least on Windows for now, deal with .pydistutils.cfg bugs + # when they happen. + nohome = '' + cmd = ('%s setup.py %s clean --all' + ' build --build-base="%s"' + ' install --force --prefix="%s" --install-lib="%s"' + ' --install-scripts="%s" %s >%s 2>&1' + % (sys.executable, pure, os.path.join(HGTMP, "build"), + INST, PYTHONDIR, BINDIR, nohome, installerrs)) + vlog("# Running", cmd) + if os.system(cmd) == 0: + if not options.verbose: + os.remove(installerrs) + else: + f = open(installerrs) + for line in f: + print line, + f.close() + sys.exit(1) + os.chdir(TESTDIR) + + usecorrectpython() + + vlog("# Installing dummy diffstat") + f = open(os.path.join(BINDIR, 'diffstat'), 'w') + f.write('#!' + sys.executable + '\n' + 'import sys\n' + 'files = 0\n' + 'for line in sys.stdin:\n' + ' if line.startswith("diff "):\n' + ' files += 1\n' + 'sys.stdout.write("files patched: %d\\n" % files)\n') + f.close() + os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) + + if options.py3k_warnings and not options.anycoverage: + vlog("# Updating hg command to enable Py3k Warnings switch") + f = open(os.path.join(BINDIR, 'hg'), 'r') + lines = [line.rstrip() for line in f] + lines[0] += ' -3' + f.close() + f = open(os.path.join(BINDIR, 'hg'), 'w') + for line in lines: + f.write(line + '\n') + f.close() + + if options.anycoverage: + custom = os.path.join(TESTDIR, 'sitecustomize.py') + target = os.path.join(PYTHONDIR, 'sitecustomize.py') + vlog('# Installing coverage trigger to %s' % target) + shutil.copyfile(custom, target) + rc = os.path.join(TESTDIR, '.coveragerc') + vlog('# Installing coverage rc to %s' % rc) + os.environ['COVERAGE_PROCESS_START'] = rc + fn = os.path.join(INST, '..', '.coverage') + os.environ['COVERAGE_FILE'] = fn + +def outputcoverage(options): + + vlog('# Producing coverage report') + os.chdir(PYTHONDIR) + + def covrun(*args): + cmd = 'coverage %s' % ' '.join(args) + vlog('# Running: %s' % cmd) + os.system(cmd) + + if options.child: + return + + covrun('-c') + omit = ','.join([BINDIR, TESTDIR]) + covrun('-i', '-r', '"--omit=%s"' % omit) # report + if options.annotate: + adir = os.path.join(TESTDIR, 'annotated') + if not os.path.isdir(adir): + os.mkdir(adir) + covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit) + +class Timeout(Exception): + pass + +def alarmed(signum, frame): + raise Timeout + +def pytest(test, options, replacements): + py3kswitch = options.py3k_warnings and ' -3' or '' + cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test) + vlog("# Running", cmd) + return run(cmd, options, replacements) + +def shtest(test, options, replacements): + cmd = '"%s"' % test + vlog("# Running", cmd) + return run(cmd, options, replacements) + +needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search +escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub +escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256)) +escapemap.update({'\\': '\\\\', '\r': r'\r'}) +def escapef(m): + return escapemap[m.group(0)] +def stringescape(s): + return escapesub(escapef, s) + +def tsttest(test, options, replacements): + t = open(test) + out = [] + script = [] + salt = "SALT" + str(time.time()) + + pos = prepos = -1 + after = {} + expected = {} + for n, l in enumerate(t): + if not l.endswith('\n'): + l += '\n' + if l.startswith(' $ '): # commands + after.setdefault(pos, []).append(l) + prepos = pos + pos = n + script.append('echo %s %s $?\n' % (salt, n)) + script.append(l[4:]) + elif l.startswith(' > '): # continuations + after.setdefault(prepos, []).append(l) + script.append(l[4:]) + elif l.startswith(' '): # results + # queue up a list of expected results + expected.setdefault(pos, []).append(l[2:]) + else: + # non-command/result - queue up for merged output + after.setdefault(pos, []).append(l) + + t.close() + + script.append('echo %s %s $?\n' % (salt, n + 1)) + + fd, name = tempfile.mkstemp(suffix='hg-tst') + + try: + for l in script: + os.write(fd, l) + os.close(fd) + + cmd = '/bin/sh "%s"' % name + vlog("# Running", cmd) + exitcode, output = run(cmd, options, replacements) + # do not merge output if skipped, return hghave message instead + # similarly, with --debug, output is None + if exitcode == SKIPPED_STATUS or output is None: + return exitcode, output + finally: + os.remove(name) + + def rematch(el, l): + try: + # ensure that the regex matches to the end of the string + return re.match(el + r'\Z', l) + except re.error: + # el is an invalid regex + return False + + def globmatch(el, l): + # The only supported special characters are * and ?. Escaping is + # supported. + i, n = 0, len(el) + res = '' + while i < n: + c = el[i] + i += 1 + if c == '\\' and el[i] in '*?\\': + res += el[i - 1:i + 1] + i += 1 + elif c == '*': + res += '.*' + elif c == '?': + res += '.' + else: + res += re.escape(c) + return rematch(res, l) + + pos = -1 + postout = [] + ret = 0 + for n, l in enumerate(output): + lout, lcmd = l, None + if salt in l: + lout, lcmd = l.split(salt, 1) + + if lout: + if lcmd: + lout += ' (no-eol)\n' + + el = None + if pos in expected and expected[pos]: + el = expected[pos].pop(0) + + if el == lout: # perfect match (fast) + postout.append(" " + lout) + elif (el and + (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or + el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout) + or el.endswith(" (esc)\n") and + el.decode('string-escape') == l)): + postout.append(" " + el) # fallback regex/glob/esc match + else: + if needescape(lout): + lout = stringescape(lout.rstrip('\n')) + " (esc)\n" + postout.append(" " + lout) # let diff deal with it + + if lcmd: + # add on last return code + ret = int(lcmd.split()[1]) + if ret != 0: + postout.append(" [%s]\n" % ret) + if pos in after: + postout += after.pop(pos) + pos = int(lcmd.split()[0]) + + if pos in after: + postout += after.pop(pos) + + return exitcode, postout + +wifexited = getattr(os, "WIFEXITED", lambda x: False) +def run(cmd, options, replacements): + """Run command in a sub-process, capturing the output (stdout and stderr). + Return a tuple (exitcode, output). output is None in debug mode.""" + # TODO: Use subprocess.Popen if we're running on Python 2.4 + if options.debug: + proc = subprocess.Popen(cmd, shell=True) + ret = proc.wait() + return (ret, None) + + if os.name == 'nt' or sys.platform.startswith('java'): + tochild, fromchild = os.popen4(cmd) + tochild.close() + output = fromchild.read() + ret = fromchild.close() + if ret is None: + ret = 0 + else: + proc = Popen4(cmd) + def cleanup(): + os.kill(proc.pid, signal.SIGTERM) + ret = proc.wait() + if ret == 0: + ret = signal.SIGTERM << 8 + killdaemons() + return ret + + try: + output = '' + proc.tochild.close() + output = proc.fromchild.read() + ret = proc.wait() + if wifexited(ret): + ret = os.WEXITSTATUS(ret) + except Timeout: + vlog('# Process %d timed out - killing it' % proc.pid) + ret = cleanup() + output += ("\n### Abort: timeout after %d seconds.\n" + % options.timeout) + except KeyboardInterrupt: + vlog('# Handling keyboard interrupt') + cleanup() + raise + + for s, r in replacements: + output = re.sub(s, r, output) + return ret, splitnewlines(output) + +def runone(options, test, skips, fails): + '''tristate output: + None -> skipped + True -> passed + False -> failed''' + + def skip(msg): + if not options.verbose: + skips.append((test, msg)) + else: + print "\nSkipping %s: %s" % (testpath, msg) + return None + + def fail(msg): + fails.append((test, msg)) + if not options.nodiff: + print "\nERROR: %s %s" % (testpath, msg) + return None + + vlog("# Test", test) + + # create a fresh hgrc + hgrc = open(HGRCPATH, 'w+') + hgrc.write('[ui]\n') + hgrc.write('slash = True\n') + hgrc.write('[defaults]\n') + hgrc.write('backout = -d "0 0"\n') + hgrc.write('commit = -d "0 0"\n') + hgrc.write('tag = -d "0 0"\n') + if options.inotify: + hgrc.write('[extensions]\n') + hgrc.write('inotify=\n') + hgrc.write('[inotify]\n') + hgrc.write('pidfile=%s\n' % DAEMON_PIDS) + hgrc.write('appendpid=True\n') + hgrc.close() + + testpath = os.path.join(TESTDIR, test) + ref = os.path.join(TESTDIR, test+".out") + err = os.path.join(TESTDIR, test+".err") + if os.path.exists(err): + os.remove(err) # Remove any previous output files + try: + tf = open(testpath) + firstline = tf.readline().rstrip() + tf.close() + except: + firstline = '' + lctest = test.lower() + + if lctest.endswith('.py') or firstline == '#!/usr/bin/env python': + runner = pytest + elif lctest.endswith('.t'): + runner = tsttest + ref = testpath + else: + # do not try to run non-executable programs + if not os.access(testpath, os.X_OK): + return skip("not executable") + runner = shtest + + # Make a tmp subdirectory to work in + testtmp = os.environ["TESTTMP"] = os.path.join(HGTMP, test) + os.mkdir(testtmp) + os.chdir(testtmp) + + if options.timeout > 0: + signal.alarm(options.timeout) + + ret, out = runner(testpath, options, [ + (re.escape(testtmp), '$TESTTMP'), + (r':%s\b' % options.port, ':$HGPORT'), + (r':%s\b' % (options.port + 1), ':$HGPORT1'), + (r':%s\b' % (options.port + 2), ':$HGPORT2'), + ]) + vlog("# Ret was:", ret) + + if options.timeout > 0: + signal.alarm(0) + + mark = '.' + + skipped = (ret == SKIPPED_STATUS) + + # If we're not in --debug mode and reference output file exists, + # check test output against it. + if options.debug: + refout = None # to match "out is None" + elif os.path.exists(ref): + f = open(ref, "r") + refout = splitnewlines(f.read()) + f.close() + else: + refout = [] + + if (ret != 0 or out != refout) and not skipped and not options.debug: + # Save errors to a file for diagnosis + f = open(err, "wb") + for line in out: + f.write(line) + f.close() + + if skipped: + mark = 's' + if out is None: # debug mode: nothing to parse + missing = ['unknown'] + failed = None + else: + missing, failed = parsehghaveoutput(out) + if not missing: + missing = ['irrelevant'] + if failed: + fail("hghave failed checking for %s" % failed[-1]) + skipped = False + else: + skip(missing[-1]) + elif out != refout: + mark = '!' + if ret: + fail("output changed and returned error code %d" % ret) + else: + fail("output changed") + if not options.nodiff: + if options.view: + os.system("%s %s %s" % (options.view, ref, err)) + else: + showdiff(refout, out, ref, err) + ret = 1 + elif ret: + mark = '!' + fail("returned error code %d" % ret) + + if not options.verbose: + try: + sys.stdout.write(mark) + sys.stdout.flush() + except IOError, ex: + print >>sys.stderr, 'BORKEN PIPE', ex.errno + pass + + killdaemons() + + os.chdir(TESTDIR) + if not options.keep_tmpdir: + shutil.rmtree(testtmp, True) + if skipped: + return None + return ret == 0 + +_hgpath = None + +def _gethgpath(): + """Return the path to the mercurial package that is actually found by + the current Python interpreter.""" + global _hgpath + if _hgpath is not None: + return _hgpath + + cmd = '%s -c "import mercurial; print mercurial.__path__[0]"' + pipe = os.popen(cmd % PYTHON) + try: + _hgpath = pipe.read().strip() + finally: + pipe.close() + return _hgpath + +def _checkhglib(verb): + """Ensure that the 'mercurial' package imported by python is + the one we expect it to be. If not, print a warning to stderr.""" + expecthg = os.path.join(PYTHONDIR, 'mercurial') + actualhg = _gethgpath() + if actualhg != expecthg: + sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n' + ' (expected %s)\n' + % (verb, actualhg, expecthg)) + +def runchildren(options, tests): + if INST: + installhg(options) + _checkhglib("Testing") + + optcopy = dict(options.__dict__) + optcopy['jobs'] = 1 + del optcopy['blacklist'] + if optcopy['with_hg'] is None: + optcopy['with_hg'] = os.path.join(BINDIR, "hg") + optcopy.pop('anycoverage', None) + + opts = [] + for opt, value in optcopy.iteritems(): + name = '--' + opt.replace('_', '-') + if value is True: + opts.append(name) + elif value is not None: + opts.append(name + '=' + str(value)) + + tests.reverse() + jobs = [[] for j in xrange(options.jobs)] + while tests: + for job in jobs: + if not tests: + break + job.append(tests.pop()) + fps = {} + + for j, job in enumerate(jobs): + if not job: + continue + rfd, wfd = os.pipe() + childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] + childtmp = os.path.join(HGTMP, 'child%d' % j) + childopts += ['--tmpdir', childtmp] + cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job + vlog(' '.join(cmdline)) + fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') + os.close(wfd) + signal.signal(signal.SIGINT, signal.SIG_IGN) + failures = 0 + tested, skipped, failed = 0, 0, 0 + skips = [] + fails = [] + while fps: + pid, status = os.wait() + fp = fps.pop(pid) + l = fp.read().splitlines() + try: + test, skip, fail = map(int, l[:3]) + except ValueError: + test, skip, fail = 0, 0, 0 + split = -fail or len(l) + for s in l[3:split]: + skips.append(s.split(" ", 1)) + for s in l[split:]: + fails.append(s.split(" ", 1)) + tested += test + skipped += skip + failed += fail + vlog('pid %d exited, status %d' % (pid, status)) + failures |= status + print + if not options.noskips: + for s in skips: + print "Skipped %s: %s" % (s[0], s[1]) + for s in fails: + print "Failed %s: %s" % (s[0], s[1]) + + _checkhglib("Tested") + print "# Ran %d tests, %d skipped, %d failed." % ( + tested, skipped, failed) + + if options.anycoverage: + outputcoverage(options) + sys.exit(failures != 0) + +def runtests(options, tests): + global DAEMON_PIDS, HGRCPATH + DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') + HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') + + try: + if INST: + installhg(options) + _checkhglib("Testing") + + if options.timeout > 0: + try: + signal.signal(signal.SIGALRM, alarmed) + vlog('# Running each test with %d second timeout' % + options.timeout) + except AttributeError: + print 'WARNING: cannot run tests with timeouts' + options.timeout = 0 + + tested = 0 + failed = 0 + skipped = 0 + + if options.restart: + orig = list(tests) + while tests: + if os.path.exists(tests[0] + ".err"): + break + tests.pop(0) + if not tests: + print "running all tests" + tests = orig + + skips = [] + fails = [] + + for test in tests: + if options.blacklist: + filename = options.blacklist.get(test) + if filename is not None: + skips.append((test, "blacklisted (%s)" % filename)) + skipped += 1 + continue + + if options.retest and not os.path.exists(test + ".err"): + skipped += 1 + continue + + if options.keywords: + fp = open(test) + t = fp.read().lower() + test.lower() + fp.close() + for k in options.keywords.lower().split(): + if k in t: + break + else: + skipped += 1 + continue + + ret = runone(options, test, skips, fails) + if ret is None: + skipped += 1 + elif not ret: + if options.interactive: + print "Accept this change? [n] ", + answer = sys.stdin.readline().strip() + if answer.lower() in "y yes".split(): + if test.endswith(".t"): + rename(test + ".err", test) + else: + rename(test + ".err", test + ".out") + tested += 1 + fails.pop() + continue + failed += 1 + if options.first: + break + tested += 1 + + if options.child: + fp = os.fdopen(options.child, 'w') + fp.write('%d\n%d\n%d\n' % (tested, skipped, failed)) + for s in skips: + fp.write("%s %s\n" % s) + for s in fails: + fp.write("%s %s\n" % s) + fp.close() + else: + print + for s in skips: + print "Skipped %s: %s" % s + for s in fails: + print "Failed %s: %s" % s + _checkhglib("Tested") + print "# Ran %d tests, %d skipped, %d failed." % ( + tested, skipped, failed) + + if options.anycoverage: + outputcoverage(options) + except KeyboardInterrupt: + failed = True + print "\ninterrupted!" + + if failed: + sys.exit(1) + +def main(): + (options, args) = parseargs() + if not options.child: + os.umask(022) + + checktools() + + if len(args) == 0: + args = os.listdir(".") + args.sort() + + tests = [] + skipped = [] + for test in args: + if (test.startswith("test-") and '~' not in test and + ('.' not in test or test.endswith('.py') or + test.endswith('.bat') or test.endswith('.t'))): + if not os.path.exists(test): + skipped.append(test) + else: + tests.append(test) + if not tests: + for test in skipped: + print 'Skipped %s: does not exist' % test + print "# Ran 0 tests, %d skipped, 0 failed." % len(skipped) + return + tests = tests + skipped + + # Reset some environment variables to well-known values so that + # the tests produce repeatable output. + os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C' + os.environ['TZ'] = 'GMT' + os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>" + os.environ['CDPATH'] = '' + os.environ['COLUMNS'] = '80' + os.environ['GREP_OPTIONS'] = '' + os.environ['http_proxy'] = '' + + # unset env related to hooks + for k in os.environ.keys(): + if k.startswith('HG_'): + # can't remove on solaris + os.environ[k] = '' + del os.environ[k] + + global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE + TESTDIR = os.environ["TESTDIR"] = os.getcwd() + if options.tmpdir: + options.keep_tmpdir = True + tmpdir = options.tmpdir + if os.path.exists(tmpdir): + # Meaning of tmpdir has changed since 1.3: we used to create + # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if + # tmpdir already exists. + sys.exit("error: temp dir %r already exists" % tmpdir) + + # Automatically removing tmpdir sounds convenient, but could + # really annoy anyone in the habit of using "--tmpdir=/tmp" + # or "--tmpdir=$HOME". + #vlog("# Removing temp dir", tmpdir) + #shutil.rmtree(tmpdir) + os.makedirs(tmpdir) + else: + tmpdir = tempfile.mkdtemp('', 'hgtests.') + HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir) + DAEMON_PIDS = None + HGRCPATH = None + + os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' + os.environ["HGMERGE"] = "internal:merge" + os.environ["HGUSER"] = "test" + os.environ["HGENCODING"] = "ascii" + os.environ["HGENCODINGMODE"] = "strict" + os.environ["HGPORT"] = str(options.port) + os.environ["HGPORT1"] = str(options.port + 1) + os.environ["HGPORT2"] = str(options.port + 2) + + if options.with_hg: + INST = None + BINDIR = os.path.dirname(os.path.realpath(options.with_hg)) + + # This looks redundant with how Python initializes sys.path from + # the location of the script being executed. Needed because the + # "hg" specified by --with-hg is not the only Python script + # executed in the test suite that needs to import 'mercurial' + # ... which means it's not really redundant at all. + PYTHONDIR = BINDIR + else: + INST = os.path.join(HGTMP, "install") + BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin") + PYTHONDIR = os.path.join(INST, "lib", "python") + + os.environ["BINDIR"] = BINDIR + os.environ["PYTHON"] = PYTHON + + if not options.child: + path = [BINDIR] + os.environ["PATH"].split(os.pathsep) + os.environ["PATH"] = os.pathsep.join(path) + + # Include TESTDIR in PYTHONPATH so that out-of-tree extensions + # can run .../tests/run-tests.py test-foo where test-foo + # adds an extension to HGRC + pypath = [PYTHONDIR, TESTDIR] + # We have to augment PYTHONPATH, rather than simply replacing + # it, in case external libraries are only available via current + # PYTHONPATH. (In particular, the Subversion bindings on OS X + # are in /opt/subversion.) + oldpypath = os.environ.get(IMPL_PATH) + if oldpypath: + pypath.append(oldpypath) + os.environ[IMPL_PATH] = os.pathsep.join(pypath) + + COVERAGE_FILE = os.path.join(TESTDIR, ".coverage") + + vlog("# Using TESTDIR", TESTDIR) + vlog("# Using HGTMP", HGTMP) + vlog("# Using PATH", os.environ["PATH"]) + vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH]) + + try: + if len(tests) > 1 and options.jobs > 1: + runchildren(options, tests) + else: + runtests(options, tests) + finally: + time.sleep(1) + cleanup(options) + +if __name__ == '__main__': + main()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-amend.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,105 @@ + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ glog() { + > hg glog --template '{rev}@{branch}({phase}) {desc|firstline}\n' "$@" + > } + + $ hg init repo + $ cd repo + $ echo a > a + $ hg ci -Am adda + adding a + +Test amend captures branches + + $ hg branch foo + marked working directory as branch foo + (branches are permanent and global, did you want a bookmark?) + $ hg amend + $ hg debugsuccessors + 07f494440405 a34b93d251e4 + bd19cbe78fbf a34b93d251e4 + $ hg branch + foo + $ hg branches + foo 2:a34b93d251e4 + default 0:07f494440405 (inactive) + $ glog + @ 2@foo(draft) adda + +Test no-op + + $ hg amend + abort: no updates found + [255] + $ glog + @ 2@foo(draft) adda + + +Test forcing the message to the same value, no intermediate revision. + + $ hg amend -m 'adda' + abort: no updates found + [255] + $ glog + @ 2@foo(draft) adda + + +Test collapsing into an existing revision, no intermediate revision. + + $ echo a >> a + $ hg ci -m changea + $ echo a > a + $ hg ci -m reseta + $ hg amend --change 2 + abort: no updates found + [255] + $ hg debugsuccessors + 07f494440405 a34b93d251e4 + bd19cbe78fbf a34b93d251e4 + $ hg phase 2 + 2: draft + $ glog + @ 4@foo(draft) reseta + | + o 3@foo(draft) changea + | + o 2@foo(draft) adda + + +Test collapsing into an existing rev, with an intermediate revision. + + $ hg branch --force default + marked working directory as branch default + (branches are permanent and global, did you want a bookmark?) + $ hg ci -m resetbranch + created new head + $ hg branch --force foo + marked working directory as branch foo + (branches are permanent and global, did you want a bookmark?) + $ hg amend --change 2 + abort: no updates found + [255] + $ hg debugsuccessors + 07f494440405 a34b93d251e4 + 7384bbcba36f 000000000000 + bd19cbe78fbf a34b93d251e4 + $ glog + @ 6@foo(draft) amends a34b93d251e49c93d5685ebacad785c73a7e8605 + | + o 5@default(draft) resetbranch + | + o 4@foo(draft) reseta + | + o 3@foo(draft) changea + | + o 2@foo(draft) adda +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-corrupt.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,122 @@ + + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish = False + > [alias] + > qlog = log --template='{rev} - {node|short} {desc} ({phase})\n' + > [diff] + > git = 1 + > unified = 0 + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ mkcommit() { + > echo "$1" >> "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + + $ hg init local + $ hg init other + $ cd local + $ touch 1 2 3 4 5 6 7 8 9 0 + $ hg add 1 2 3 4 5 6 7 8 9 0 + $ mkcommit A + $ mkcommit B + $ mkcommit C + $ hg glog + @ changeset: 2:829b19580856 + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add C + | + o changeset: 1:97b8f02ab29e + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add B + | + o changeset: 0:5d8dabd3961b + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add A + + $ hg push ../other + pushing to ../other + searching for changes + adding changesets + adding manifests + adding file changes + added 3 changesets with 13 changes to 13 files + + + $ hg -R ../other verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 13 files, 3 changesets, 13 total revisions + $ mkcommit D + $ mkcommit E + $ hg up -q .^^ + $ hg revert -r tip -a -q + $ hg ci -m 'coin' -q + $ hg glog + @ changeset: 5:8313a6afebbb + | tag: tip + | parent: 2:829b19580856 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: coin + | + | o changeset: 4:076ec8ade1ac + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: add E + | | + | o changeset: 3:824d9bb109f6 + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add D + | + o changeset: 2:829b19580856 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add C + | + o changeset: 1:97b8f02ab29e + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add B + | + o changeset: 0:5d8dabd3961b + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add A + + + $ hg kill -n -1 -- -2 -3 + $ hg push ../other + pushing to ../other + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 2 changes to 2 files + $ hg -R ../other verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 15 files, 4 changesets, 15 total revisions + + +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-evolve.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,358 @@ + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish = False + > [alias] + > qlog = log --template='{rev} - {node|short} {desc} ({phase})\n' + > [diff] + > git = 1 + > unified = 0 + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + + $ glog() { + > hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n' "$@" + > } + +various init + + $ hg init local + $ cd local + $ mkcommit a + $ mkcommit b + $ cat >> .hg/hgrc << EOF + > [phases] + > publish = True + > EOF + $ hg pull -q . # make 1 public + $ rm .hg/hgrc + $ mkcommit c + $ mkcommit d + $ hg up 1 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ mkcommit e -q + created new head + $ mkcommit f + $ hg qlog + 5 - e44648563c73 add f (draft) + 4 - fbb94e3a0ecf add e (draft) + 3 - 47d2a3944de8 add d (draft) + 2 - 4538525df7e2 add c (draft) + 1 - 7c3bad9141dc add b (public) + 0 - 1f0dee641bb7 add a (public) + +test simple kill + + $ hg id -n + 5 + $ hg kill . + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory now at fbb94e3a0ecf + $ hg qlog + 4 - fbb94e3a0ecf add e (draft) + 3 - 47d2a3944de8 add d (draft) + 2 - 4538525df7e2 add c (draft) + 1 - 7c3bad9141dc add b (public) + 0 - 1f0dee641bb7 add a (public) + +test multiple kill + + $ hg kill 4 3 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory now at 7c3bad9141dc + $ hg qlog + 2 - 4538525df7e2 add c (draft) + 1 - 7c3bad9141dc add b (public) + 0 - 1f0dee641bb7 add a (public) + +test kill with dirty changes + + $ hg up 2 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo 4 > g + $ hg add g + $ hg kill . + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory now at 7c3bad9141dc + $ hg st + A g + $ cd .. + +########################## +importing Parren test +########################## + + $ cat << EOF >> $HGRCPATH + > [ui] + > logtemplate = "{rev}\t{bookmarks}: {desc|firstline} - {author|user}\n" + > EOF + +Creating And Updating Changeset +=============================== + +Setup the Base Repo +------------------- + +We start with a plain base repo:: + + $ hg init main; cd main + $ cat >main-file-1 <<-EOF + > One + > + > Two + > + > Three + > EOF + $ echo Two >main-file-2 + $ hg add + adding main-file-1 + adding main-file-2 + $ hg commit --message base + $ cd .. + +and clone this into a new repo where we do our work:: + + $ hg clone main work + updating to branch default + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd work + + +Create First Patch +------------------ + +To begin with, we just do the changes that will be the initial version of the changeset:: + + $ echo One >file-from-A + $ sed -i'' -e s/One/Eins/ main-file-1 + $ hg add file-from-A + +So this is what we would like our changeset to be:: + + $ hg diff + diff --git a/file-from-A b/file-from-A + new file mode 100644 + --- /dev/null + +++ b/file-from-A + @@ -0,0 +1,1 @@ + +One + diff --git a/main-file-1 b/main-file-1 + --- a/main-file-1 + +++ b/main-file-1 + @@ -1,1 +1,1 @@ + -One + +Eins + +To commit it we just - commit it:: + + $ hg commit --message "a nifty feature" + +and place a bookmark so we can easily refer to it again (which we could have done before the commit):: + + $ hg book feature-A + + +Create Second Patch +------------------- + +Let's do this again for the second changeset:: + + $ echo Two >file-from-B + $ sed -i'' -e s/Two/Zwie/ main-file-1 + $ hg add file-from-B + +Before committing, however, we need to switch to a new bookmark for the second +changeset. Otherwise we would inadvertently move the bookmark for our first changeset. +It is therefore advisable to always set the bookmark before committing:: + + $ hg book feature-B + $ hg commit --message "another feature" + +So here we are:: + + $ hg book + feature-A 1:568a468b60fc + * feature-B 2:7b36850622b2 + + +Fix The Second Patch +-------------------- + +There's a typo in feature-B. We spelled *Zwie* instead of *Zwei*:: + + $ hg diff --change tip | grep -F Zwie + +Zwie + +Fixing this is very easy. Just change:: + + $ sed -i'' -e s/Zwie/Zwei/ main-file-1 + +and **amend**:: + + $ hg amend --note "fix spelling of Zwei" + +The `--note` is our commit message for the *update* only. So its only purpose +is to document the evolution of the changeset. If we use `--message` with +`amend`, it replaces the commit message of the changeset itself. + +This results in a new single changeset for our amended changeset, and the old +changeset plus the updating changeset are hidden from view by default:: + + $ hg log + 4 feature-B: another feature - test + 1 feature-A: a nifty feature - test + 0 : base - test + + $ hg up feature-A -q + $ hg bookmark -i feature-A + $ sed -i'' -e s/Eins/Un/ main-file-1 + + $ hg amend --note 'french looks better' + 1 new unstables changesets + $ hg log + 6 feature-A: a nifty feature - test + 4 feature-B: another feature - test + 1 : a nifty feature - test + 0 : base - test + $ hg up -q 0 + $ glog --hidden + o 6:23409eba69a0@default(draft) a nifty feature + | + | x 5:e416e48b2742@default(draft) french looks better + | | + | | o 4:f8111a076f09@default(draft) another feature + | |/ + | | x 3:524e478d4811@default(draft) fix spelling of Zwei + | | | + | | x 2:7b36850622b2@default(draft) another feature + | |/ + | x 1:568a468b60fc@default(draft) a nifty feature + |/ + @ 0:e55e0562ee93@default(draft) base + + $ hg debugsuccessors + 524e478d4811 f8111a076f09 + 568a468b60fc 23409eba69a0 + 7b36850622b2 f8111a076f09 + e416e48b2742 23409eba69a0 + $ hg stabilize + move:[4] another feature + atop:[6] a nifty feature + merging main-file-1 + $ hg log + 7 feature-B: another feature - test + 6 feature-A: a nifty feature - test + 0 : base - test + +Test commit -o options + + $ hg up 6 + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg revert -r 7 --all + adding file-from-B + reverting main-file-1 + $ sed -i'' -e s/Zwei/deux/ main-file-1 + $ hg commit -m 'another feature that rox' -o 7 + created new head + $ hg log + 8 feature-B: another feature that rox - test + 6 feature-A: a nifty feature - test + 0 : base - test + +phase change turning obsolete changeset public issue a latecomer warning + + $ hg phase --public 7 + 1 new latecomers changesets + + $ cd .. + +enable general delta + + $ cat << EOF >> $HGRCPATH + > [format] + > generaldelta=1 + > EOF + + + + $ hg init alpha + $ cd alpha + $ echo 'base' > firstfile + $ hg add firstfile + $ hg ci -m 'base' + + $ cd .. + $ hg clone -Ur 0 alpha beta + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files + $ cd alpha + + $ cat << EOF > A + > We + > need + > some + > kind + > of + > file + > big + > enough + > to + > prevent + > snapshot + > . + > yes + > new + > lines + > are + > useless + > . + > EOF + $ hg add A + $ hg commit -m 'adding A' + $ hg mv A B + $ echo '.' >> B + $ hg amend -m 'add B' + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 3 files, 4 changesets, 4 total revisions + $ hg --config extensions.hgext.mq= strip 'extinct()' + saved backup bundle to $TESTTMP/alpha/.hg/strip-backup/e87767087a57-backup.hg + $ hg verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 2 files, 2 changesets, 2 total revisions + $ cd .. + +Clone just this branch + + $ cd beta + $ hg pull -r tip ../alpha + pulling from ../alpha + 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 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-obsolete-push.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,47 @@ + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ template='{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' + $ glog() { + > hg glog --template "$template" "$@" + > } + +Test outgoing, common A is suspended, B unstable and C secret, remote +has A and B, neither A or C should be in outgoing. + + $ hg init source + $ cd source + $ echo a > a + $ hg ci -qAm A a + $ echo b > b + $ hg ci -qAm B b + $ hg up 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo c > c + $ hg ci -qAm C c + $ hg phase --secret --force . + $ hg kill 0 1 + 1 new unstables changesets + $ glog --hidden + @ 2:244232c2222a@default(unstable/secret) C + | + | x 1:6c81ed0049f8@default(extinct/draft) B + |/ + x 0:1994f17a630e@default(suspended/draft) A + + $ hg init ../clone + $ cat > ../clone/.hg/hgrc <<EOF + > [phases] + > publish = false + > EOF + $ hg outgoing ../clone --template "$template" + comparing with ../clone + searching for changes + 0:1994f17a630e@default(suspended/draft) A
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-obsolete-rebase.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,218 @@ + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + + $ glog() { + > hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n'\ + > "$@" + > } + + $ hg init repo + $ cd repo + $ echo a > a + $ hg ci -Am adda + adding a + $ echo a >> a + $ hg ci -m changea + +Test regular rebase + + $ hg up 0 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo b > b + $ hg ci -Am addb + adding b + created new head + $ echo e > e + $ hg ci -Am adde e + $ hg rebase -d 1 -r 3 --detach --keep + $ glog + @ 4:9c5494949763@default(draft) adde + | + | o 3:98e4a024635e@default(draft) adde + | | + | o 2:102a90ea7b4a@default(draft) addb + | | + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ glog --hidden + @ 4:9c5494949763@default(draft) adde + | + | o 3:98e4a024635e@default(draft) adde + | | + | o 2:102a90ea7b4a@default(draft) addb + | | + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ hg debugsuccessors + $ hg --config extensions.hgext.mq= strip tip + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9c5494949763-backup.hg + $ hg rebase -d 1 -r 3 --detach + $ glog + @ 4:9c5494949763@default(draft) adde + | + | o 2:102a90ea7b4a@default(draft) addb + | | + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ glog --hidden + @ 4:9c5494949763@default(draft) adde + | + | x 3:98e4a024635e@default(draft) adde + | | + | o 2:102a90ea7b4a@default(draft) addb + | | + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ hg debugsuccessors + 98e4a024635e 9c5494949763 + +Test rebase with deleted empty revision + + $ hg up 0 + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg branch foo + marked working directory as branch foo + (branches are permanent and global, did you want a bookmark?) + $ echo a >> a + $ hg ci -m changea + $ hg rebase -d 1 + $ glog --hidden + x 5:4e322f7ce8e3@foo(draft) changea + | + | o 4:9c5494949763@default(draft) adde + | | + | | x 3:98e4a024635e@default(draft) adde + | | | + +---o 2:102a90ea7b4a@default(draft) addb + | | + | @ 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ hg debugsuccessors + 4e322f7ce8e3 000000000000 + 98e4a024635e 9c5494949763 + +Test rebase --collapse + + $ hg up 0 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo c > c + $ hg ci -Am addc + adding c + created new head + $ echo c >> c + $ hg ci -m changec + $ hg rebase --collapse -d 1 + merging c + $ glog --hidden + @ 8:a7773ffa7edc@default(draft) Collapsed revision + | + | x 7:03f31481307a@default(draft) changec + | | + | x 6:076e9b2ffbe1@default(draft) addc + | | + | | x 5:4e322f7ce8e3@foo(draft) changea + | |/ + +---o 4:9c5494949763@default(draft) adde + | | + | | x 3:98e4a024635e@default(draft) adde + | | | + | | o 2:102a90ea7b4a@default(draft) addb + | |/ + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ hg debugsuccessors + 03f31481307a a7773ffa7edc + 076e9b2ffbe1 a7773ffa7edc + 4e322f7ce8e3 000000000000 + 98e4a024635e 9c5494949763 + +Test rebase --abort + + $ hg debugsuccessors > ../successors.old + $ hg up 0 + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo d > d + $ hg ci -Am addd d + created new head + $ echo b >> a + $ hg ci -m appendab + $ hg rebase -d 1 + merging a + warning: conflicts during merge. + merging a incomplete! (edit conflicts, then use 'hg resolve --mark') + abort: unresolved conflicts (see hg resolve, then hg rebase --continue) + [255] + $ hg rebase --abort + saved backup bundle to $TESTTMP/repo/.hg/strip-backup/03f165c84ea8-backup.hg + rebase aborted + $ hg debugsuccessors > ../successors.new + $ diff -u ../successors.old ../successors.new + +Test rebase --continue + + $ hg rebase -d 1 + merging a + warning: conflicts during merge. + merging a incomplete! (edit conflicts, then use 'hg resolve --mark') + abort: unresolved conflicts (see hg resolve, then hg rebase --continue) + [255] + $ hg resolve --tool internal:other a + $ hg rebase --continue + $ glog --hidden + @ 12:1951ead97108@default(draft) appendab + | + o 11:03f165c84ea8@default(draft) addd + | + | x 10:4b9d80f48523@default(draft) appendab + | | + | x 9:a31943eabc43@default(draft) addd + | | + +---o 8:a7773ffa7edc@default(draft) Collapsed revision + | | + | | x 7:03f31481307a@default(draft) changec + | | | + | | x 6:076e9b2ffbe1@default(draft) addc + | |/ + | | x 5:4e322f7ce8e3@foo(draft) changea + | |/ + +---o 4:9c5494949763@default(draft) adde + | | + | | x 3:98e4a024635e@default(draft) adde + | | | + | | o 2:102a90ea7b4a@default(draft) addb + | |/ + o | 1:540395c44225@default(draft) changea + |/ + o 0:07f494440405@default(draft) adda + + $ hg debugsuccessors > ../successors.new + $ diff -u ../successors.old ../successors.new + --- ../successors.old* (glob) + +++ ../successors.new* (glob) + @@ -1,4 +1,6 @@ + 03f31481307a a7773ffa7edc + 076e9b2ffbe1 a7773ffa7edc + +4b9d80f48523 1951ead97108 + 4e322f7ce8e3 000000000000 + 98e4a024635e 9c5494949763 + +a31943eabc43 03f165c84ea8 + [1]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-obsolete.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,626 @@ + $ cat >> $HGRCPATH <<EOF + > [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish=False + > [alias] + > odiff=diff --rev 'limit(precursors(.),1)' --rev . + > [extensions] + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + $ getid() { + > hg id --debug -ir "$1" + > } + + $ alias qlog="hg log --template='{rev}\n- {node|short}\n'" + $ hg init local + $ cd local + $ mkcommit a # 0 + $ hg phase -p . + $ mkcommit b # 1 + $ mkcommit c # 2 + $ hg up 1 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit obsol_c # 3 + created new head + $ getid 2 + 4538525df7e2b9f09423636c61ef63a4cb872a2d + $ getid 3 + 0d3f46688ccc6e756c7e96cf64c391c411309597 + $ hg debugobsolete 4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 + $ hg debugobsolete + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 0 {'date': '', 'user': 'test'} + + +Test hidden() revset + + $ qlog -r 'hidden()' --hidden + 2 + - 4538525df7e2 + +Test that obsolete changeset are hidden + + $ qlog + 3 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ qlog --hidden + 3 + - 0d3f46688ccc + 2 + - 4538525df7e2 + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ qlog -r 'obsolete()' --hidden + 2 + - 4538525df7e2 + +Test that obsolete parent a properly computed + + $ qlog -r 'precursors(.)' --hidden + 2 + - 4538525df7e2 + $ qlog -r . + 3 + - 0d3f46688ccc + $ hg odiff + diff -r 4538525df7e2 -r 0d3f46688ccc c + --- a/c Thu Jan 01 00:00:00 1970 +0000 + +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 + @@ -1,1 +0,0 @@ + -c + diff -r 4538525df7e2 -r 0d3f46688ccc obsol_c + --- /dev/null Thu Jan 01 00:00:00 1970 +0000 + +++ b/obsol_c Thu Jan 01 00:00:00 1970 +0000 + @@ -0,0 +1,1 @@ + +obsol_c + +Test that obsolete successors a properly computed + + $ qlog -r 'successors(2)' --hidden + 3 + - 0d3f46688ccc + +test obsolete changeset with no-obsolete descendant + $ hg up 1 -q + $ mkcommit "obsol_c'" # 4 (on 1) + created new head + $ hg debugobsolete `getid 3` `getid 4` + $ qlog + 4 + - 725c380fe99b + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ qlog -r 'obsolete()' --hidden + 2 + - 4538525df7e2 + 3 + - 0d3f46688ccc + $ qlog -r 'allprecursors(4)' --hidden + 2 + - 4538525df7e2 + 3 + - 0d3f46688ccc + $ qlog -r 'allsuccessors(2)' --hidden + 3 + - 0d3f46688ccc + 4 + - 725c380fe99b + $ hg up 3 -q + Working directory parent is obsolete + $ mkcommit d # 5 (on 3) + 1 new unstables changesets + $ qlog -r 'obsolete()' + 3 + - 0d3f46688ccc + + $ qlog -r 'extinct()' --hidden + 2 + - 4538525df7e2 + $ qlog -r 'suspended()' + 3 + - 0d3f46688ccc + $ qlog -r 'unstable()' + 5 + - a7a6f2b5d8a5 + +Test obsolete keyword + + $ hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' \ + > --hidden + @ 5:a7a6f2b5d8a5@default(unstable/draft) add d + | + | o 4:725c380fe99b@default(stable/draft) add obsol_c' + | | + x | 3:0d3f46688ccc@default(suspended/draft) add obsol_c + |/ + | x 2:4538525df7e2@default(extinct/draft) add c + |/ + o 1:7c3bad9141dc@default(stable/draft) add b + | + o 0:1f0dee641bb7@default(stable/public) add a + + +Test communication of obsolete relation with a compatible client + + $ hg init ../other-new + $ hg phase --draft 'secret() - extinct()' # until we fix exclusion + abort: empty revision set + [255] + $ hg push ../other-new + pushing to ../other-new + searching for changes + abort: push includes an unstable changeset: a7a6f2b5d8a5! + (use 'hg stabilize' to get a stable history or --force to ignore warnings) + [255] + $ hg push -f ../other-new + pushing to ../other-new + searching for changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 5 changes to 5 files (+1 heads) + $ hg -R ../other-new verify + checking changesets + checking manifests + crosschecking files in changesets and manifests + checking files + 5 files, 5 changesets, 5 total revisions + $ qlog -R ../other-new -r 'obsolete()' + 2 + - 0d3f46688ccc + $ qlog -R ../other-new + 4 + - a7a6f2b5d8a5 + 3 + - 725c380fe99b + 2 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ hg up 3 -q + Working directory parent is obsolete + $ mkcommit obsol_d # 6 + created new head + 1 new unstables changesets + $ hg debugobsolete `getid 5` `getid 6` + $ qlog + 6 + - 95de7fc6918d + 4 + - 725c380fe99b + 3 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ qlog -r 'obsolete()' + 3 + - 0d3f46688ccc + $ hg push ../other-new + pushing to ../other-new + searching for changes + abort: push includes an unstable changeset: 95de7fc6918d! + (use 'hg stabilize' to get a stable history or --force to ignore warnings) + [255] + $ hg push ../other-new -f # use f because there is unstability + pushing to ../other-new + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + $ qlog -R ../other-new + 5 + - 95de7fc6918d + 3 + - 725c380fe99b + 2 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ qlog -R ../other-new -r 'obsolete()' + 2 + - 0d3f46688ccc + +Pushing again does not advertise extinct changeset + + $ hg push ../other-new + pushing to ../other-new + searching for changes + no changes found + [1] + + $ hg up -q .^ # 3 + Working directory parent is obsolete + $ mkcommit "obsol_d'" # 7 + created new head + 1 new unstables changesets + $ hg debugobsolete `getid 6` `getid 7` + $ hg pull -R ../other-new . + pulling from . + 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 merge' to merge) + $ qlog -R ../other-new + 6 + - 909a0fb57e5d + 3 + - 725c380fe99b + 2 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + +pushing to stuff that doesn't support obsolete + + $ hg init ../other-old + > # XXX I don't like this but changeset get published otherwise + > # remove it when we will get a --keep-state flag for push + $ echo '[extensions]' > ../other-old/.hg/hgrc + $ echo "obsolete=!$(echo $(dirname $TESTDIR))/obsolete.py" >> ../other-old/.hg/hgrc + $ hg push ../other-old + pushing to ../other-old + searching for changes + abort: push includes an unstable changeset: 909a0fb57e5d! + (use 'hg stabilize' to get a stable history or --force to ignore warnings) + [255] + $ hg push -f ../other-old + pushing to ../other-old + searching for changes + adding changesets + adding manifests + adding file changes + added 5 changesets with 5 changes to 5 files (+1 heads) + $ qlog -R ../other-old + 4 + - 909a0fb57e5d + 3 + - 725c380fe99b + 2 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + +clone support + + $ hg clone . ../cloned + > # The warning should go away once we have default value to set ready before we pull + updating to branch default + 4 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ qlog -R ../cloned --hidden + 7 + - 909a0fb57e5d + 6 + - 95de7fc6918d + 5 + - a7a6f2b5d8a5 + 4 + - 725c380fe99b + 3 + - 0d3f46688ccc + 2 + - 4538525df7e2 + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + +Test rollback support + + $ hg up .^ -q # 3 + Working directory parent is obsolete + $ mkcommit "obsol_d''" + created new head + 1 new unstables changesets + $ hg debugobsolete `getid 7` `getid 8` + $ cd ../other-new + $ hg up -q 3 + $ hg pull ../local/ + pulling from ../local/ + 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 merge' to merge) + + $ hg up -q 7 # to check rollback update behavior + $ qlog + 7 + - 159dfc9fa5d3 + 3 + - 725c380fe99b + 2 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ hg rollback + repository tip rolled back to revision 6 (undo pull) + working directory now based on revision 3 + $ hg summary + parent: 3:725c380fe99b + add obsol_c' + branch: default + commit: 1 deleted, 2 unknown (clean) + update: 4 new changesets, 4 branch heads (merge) + $ qlog + 6 + - 909a0fb57e5d + 3 + - 725c380fe99b + 2 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + $ cd ../local + +obsolete public changeset + +# move draft boundary from 0 to 1 + $ sed -e 's/1f0dee641bb7258c56bd60e93edfa2405381c41e/7c3bad9141dcb46ff89abf5f61856facd56e476c/' -i'.back' .hg/store/phaseroots + + $ hg up null + 0 files updated, 0 files merged, 4 files removed, 0 files unresolved + $ mkcommit toto # 9 + created new head + $ hg id -n + 9 + $ hg debugobsolete `getid 0` `getid 9` +83b5778897ad try to obsolete immutable changeset 1f0dee641bb7 +# at core level the warning is not issued +# this is now a big issue now that we have latecomer warning + $ qlog -r 'obsolete()' + 3 + - 0d3f46688ccc +allow to just kill changeset + + $ qlog + 9 + - 83b5778897ad + 8 + - 159dfc9fa5d3 + 4 + - 725c380fe99b + 3 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + + $ hg debugobsolete `getid 9` #kill + $ hg up null -q # to be not based on 9 anymore + $ qlog + 8 + - 159dfc9fa5d3 + 4 + - 725c380fe99b + 3 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + +check rebase compat + + $ hg glog -r 'not extinct()' --template='{rev} - {node|short}\n' + o 8 - 159dfc9fa5d3 + | + | o 4 - 725c380fe99b + | | + x | 3 - 0d3f46688ccc + |/ + o 1 - 7c3bad9141dc + | + o 0 - 1f0dee641bb7 + + + $ hg glog --template='{rev} - {node|short}\n' `(hg --version | grep -q 'version 2.1') || echo '--hidden'` + x 9 - 83b5778897ad + + o 8 - 159dfc9fa5d3 + | + | x 7 - 909a0fb57e5d + |/ + | x 6 - 95de7fc6918d + |/ + | x 5 - a7a6f2b5d8a5 + |/ + | o 4 - 725c380fe99b + | | + x | 3 - 0d3f46688ccc + |/ + | x 2 - 4538525df7e2 + |/ + o 1 - 7c3bad9141dc + | + o 0 - 1f0dee641bb7 + + +should not rebase extinct changeset + + $ hg --config extensions.hgext.rebase= rebase -b 3 -d 4 --traceback + $ hg --config extensions.graphlog= glog -r 'not extinct()' --template='{rev} - {node|short}\n' + @ 11 - 9468a5f5d8b2 + | + o 10 - 2033b4e49474 + | + o 4 - 725c380fe99b + | + o 1 - 7c3bad9141dc + | + o 0 - 1f0dee641bb7 + + +Does not complain about new head if you obsolete the old one + + $ hg push ../other-new --traceback + pushing to ../other-new + searching for changes + adding changesets + adding manifests + adding file changes + added 2 changesets with 1 changes to 1 files + $ hg up -q 10 + $ mkcommit "obsol_d'''" + created new head + $ hg debugobsolete `getid 11` `getid 12` + $ hg push ../other-new --traceback + pushing to ../other-new + searching for changes + adding changesets + adding manifests + adding file changes + added 1 changesets with 1 changes to 1 files (+1 heads) + $ cd .. + +check latecomer detection +(make an obsolete changeset public) + + $ cd local + $ hg phase --public 11 + 1 new latecomers changesets + $ hg --config extensions.graphlog=glog glog --template='{rev} - ({phase}) {node|short} {desc}\n' + @ 12 - (draft) 6db5e282cb91 add obsol_d''' + | + | o 11 - (public) 9468a5f5d8b2 add obsol_d'' + |/ + o 10 - (public) 2033b4e49474 add obsol_c + | + o 4 - (public) 725c380fe99b add obsol_c' + | + o 1 - (public) 7c3bad9141dc add b + | + o 0 - (public) 1f0dee641bb7 add a + + $ hg log -r 'latecomer()' + changeset: 12:6db5e282cb91 + tag: tip + parent: 10:2033b4e49474 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add obsol_d''' + + $ hg push ../other-new/ + pushing to ../other-new/ + searching for changes + abort: push includes a latecomer changeset: 6db5e282cb91! + (use 'hg stabilize' to get a stable history or --force to ignore warnings) + [255] + +Check hg commit --amend compat + + $ hg up 'desc(obsol_c)' + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit f + created new head + $ echo 42 >> f + $ hg commit --amend --traceback + saved backup bundle to $TESTTMP/local/.hg/strip-backup/0b1b6dd009c0-amend-backup.hg + $ hg glog + @ changeset: 13:3734a65252e6 + | tag: tip + | parent: 10:2033b4e49474 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add f + | + | o changeset: 12:6db5e282cb91 + |/ parent: 10:2033b4e49474 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_d''' + | + | o changeset: 11:9468a5f5d8b2 + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_d'' + | + o changeset: 10:2033b4e49474 + | parent: 4:725c380fe99b + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_c + | + o changeset: 4:725c380fe99b + | parent: 1:7c3bad9141dc + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add obsol_c' + | + o changeset: 1:7c3bad9141dc + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add b + | + o changeset: 0:1f0dee641bb7 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add a + + $ hg debugsuccessors + 0b1b6dd009c0 3734a65252e6 + 0d3f46688ccc 2033b4e49474 + 0d3f46688ccc 725c380fe99b + 159dfc9fa5d3 9468a5f5d8b2 + 1f0dee641bb7 83b5778897ad + 4538525df7e2 0d3f46688ccc + 83b5778897ad + 909a0fb57e5d 159dfc9fa5d3 + 9468a5f5d8b2 6db5e282cb91 + 95de7fc6918d 909a0fb57e5d + a7a6f2b5d8a5 95de7fc6918d + +Check conflict detection + + $ hg up 9468a5f5d8b2 # add obsol_d'' + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ mkcommit "obsolet_conflicting_d" + $ hg summary + parent: 14:50f11e5e3a63 tip + add obsolet_conflicting_d + branch: default + commit: (clean) + update: 9 new changesets, 9 branch heads (merge) + $ hg debugobsolete `getid a7a6f2b5d8a5` `getid 50f11e5e3a63` + $ hg log -r 'conflicting()' + changeset: 14:50f11e5e3a63 + tag: tip + parent: 11:9468a5f5d8b2 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add obsolet_conflicting_d +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-oldconvert.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,114 @@ + $ cat >> $HGRCPATH <<EOF + > [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish=False + > [alias] + > odiff=diff --rev 'limit(obsparents(.),1)' --rev . + > [extensions] + > hgext.graphlog= + > EOF + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + +create commit + + $ hg init repo + $ cd repo + $ mkcommit a + $ mkcommit b + $ hg up -q 0 + $ mkcommit c + created new head + +forge old style relation files + + $ hg log -r 2 --template='{node} ' > .hg/obsolete-relations + $ hg log -r 1 --template='{node}' >> .hg/obsolete-relations + +enable the extensions + + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + + $ hg glog + abort: old format of obsolete marker detected! + run `hg debugconvertobsolete` once. + [255] + $ hg debugconvertobsolete --traceback + 1 obsolete marker converted + $ hg glog + @ changeset: 2:d67cd0334eee + | tag: tip + | parent: 0:1f0dee641bb7 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add c + | + o changeset: 0:1f0dee641bb7 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add a + + $ hg debugsuccessors + 7c3bad9141dc d67cd0334eee + $ hg debugconvertobsolete + nothing to do + 0 obsolete marker converted + +Convert json + + $ cat > .hg/store/obsoletemarkers << EOF + > [ + > { + > "reason": "import from older format.", + > "subjects": [ + > "3218406b50ed13480765e7c260669620f37fba6e" + > ], + > "user": "Pierre-Yves David <pierre-yves.david@ens-lyon.org>", + > "date": [ + > 1336503323.9768269, + > -7200 + > ], + > "object": "3e03d82708d4da97a92158558dd13386d8f09ad5", + > "id": "4743f676eaf3923cb98c921ee06b2e91052c365b" + > }, + > { + > "reason": "import from older format.", + > "user": "Pierre-Yves David <pierre-yves.david@logilab.fr>", + > "date": [ + > 1336557472.7875929, + > -7200 + > ], + > "object": "5c722672795c3a2cb94d0cc9a821c394c1475f87", + > "id": "1fd90a84b7225d2e3062b7e1b3100aa2e060fc72" + > }, + > { + > "reason": "import from older format.", + > "subjects": [ + > "0000000000000000000000000000000000000000" + > ], + > "user": "Pierre-Yves David <pierre-yves.david@logilab.fr>", + > "date": [ + > 1336557472.784307, + > -7200 + > ], + > "object": "2c3784e102bb34ccc93862af5bd6d609ee30c577", + > "id": "7d940c5ee1f886c8a6c0d805b43e522cb3ef7a15" + > } + > ] + > EOF + $ hg glog + abort: old format of obsolete marker detected! + run `hg debugconvertobsolete` once. + [255] + $ hg debugconvertobsolete --traceback + 3 obsolete marker converted + $ hg debugsuccessors + 2c3784e102bb + 3e03d82708d4 3218406b50ed + 5c722672795c + 7c3bad9141dc d67cd0334eee
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-qsync.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,240 @@ + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish = False + > [alias] + > qlog = log --template='{rev} - {node|short} {desc} ({phase})\n' + > mqlog = log --mq --template='{rev} - {desc}\n' + > [diff] + > git = 1 + > unified = 0 + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > hgext.mq= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ echo "qsync=$(echo $(dirname $TESTDIR))/hgext/qsync.py" >> $HGRCPATH + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + +basic sync + + $ hg init local + $ cd local + $ hg qinit -c + $ hg qci -m "initial commit" + $ mkcommit a + $ mkcommit b + $ hg qlog + 1 - 7c3bad9141dc add b (draft) + 0 - 1f0dee641bb7 add a (draft) + $ hg qsync -a + $ hg mqlog + 2 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 1 - qsubmit init + 0 - initial commit + +basic sync II + + $ hg init local + $ cd local + $ hg qinit -c + $ hg qci -m "initial commit" + $ mkcommit a + $ mkcommit b + $ hg qlog + 1 - 7c3bad9141dc add b (draft) + 0 - 1f0dee641bb7 add a (draft) + $ hg qsync -a + $ hg mqlog + 2 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 1 - qsubmit init + 0 - initial commit + + $ echo "b" >> b + $ hg amend + $ hg qsync -a + $ hg mqlog + 3 - qsubmit commit + + * DEFAULT-add_b.diff ready for review + 2 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 1 - qsubmit init + 0 - initial commit + + $ hg up -r 0 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo "a" >> a + $ hg amend + 1 new unstables changesets + $ hg graft -O 3 + grafting revision 3 + $ hg qsync -a + $ hg mqlog + 4 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 3 - qsubmit commit + + * DEFAULT-add_b.diff ready for review + 2 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 1 - qsubmit init + 0 - initial commit + +sync with published changeset + + $ hg init local + $ cd local + $ hg qinit -c + $ hg qci -m "initial commit" + $ mkcommit a + $ mkcommit b + $ hg qlog + 1 - 7c3bad9141dc add b (draft) + 0 - 1f0dee641bb7 add a (draft) + $ hg qsync -a + $ hg mqlog + 2 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 1 - qsubmit init + 0 - initial commit + + $ hg phase -p 0 + $ hg qsync -a + $ hg mqlog + 3 - qsubmit commit + + * applied DEFAULT-add_a.diff + 2 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 1 - qsubmit init + 0 - initial commit + + $ mkcommit c + $ mkcommit d + $ hg qsync -a + $ hg mqlog + 4 - qsubmit commit + + * DEFAULT-add_c.diff ready for review + * DEFAULT-add_d.diff ready for review + 3 - qsubmit commit + + * applied DEFAULT-add_a.diff + 2 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 1 - qsubmit init + 0 - initial commit + + $ cd .. + $ hg qclone -U local local2 + $ cd local2 + $ hg qlog + 3 - 47d2a3944de8 add d (draft) + 2 - 4538525df7e2 add c (draft) + 1 - 7c3bad9141dc add b (draft) + 0 - 1f0dee641bb7 add a (public) + $ hg strip -n 1 --no-backup + $ hg up + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg up --mq 4 + 6 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg qseries + DEFAULT-add_b.diff + DEFAULT-add_c.diff + DEFAULT-add_d.diff + $ hg qpush + applying DEFAULT-add_b.diff + now at: DEFAULT-add_b.diff + $ hg qfinish -a + $ hg phase -p . + $ hg qci -m "applied DEFAULT-add_b.diff" + $ cd ../local + $ hg pull ../local2 + pulling from ../local2 + searching for changes + no changes found + $ hg pull --mq ../local2/.hg/patches + pulling from ../local2/.hg/patches + 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 qlog + 3 - 47d2a3944de8 add d (draft) + 2 - 4538525df7e2 add c (draft) + 1 - 7c3bad9141dc add b (public) + 0 - 1f0dee641bb7 add a (public) + $ hg mqlog -l 1 + 5 - applied DEFAULT-add_b.diff + $ hg status --mq --rev tip:-2 + M series + A DEFAULT-add_b.diff + $ hg qsync -a + $ hg status --mq --rev tip:-2 + M qsubmitdata + $ hg mqlog -l 1 + 6 - qsubmit commit + + * applied DEFAULT-add_b.diff + $ hg qsync -a + abort: Nothing changed + [255] + +mixed sync + + $ hg init local + $ cd local + $ hg qinit -c + $ mkcommit a + $ mkcommit b + $ hg qlog + 1 - 7c3bad9141dc add b (draft) + 0 - 1f0dee641bb7 add a (draft) + $ hg qsync -a + $ hg mqlog + 1 - qsubmit commit + + * DEFAULT-add_a.diff ready for review + * DEFAULT-add_b.diff ready for review + 0 - qsubmit init + $ hg phase -p 0 + $ echo "b" >> b + $ hg amend + $ hg qsync -a + $ hg mqlog -l 1 + 2 - qsubmit commit + + * applied DEFAULT-add_a.diff + * DEFAULT-add_b.diff ready for review +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-stabilize-order.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,171 @@ + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ glog() { + > hg glog --template '{rev}:{node|short}@{branch}({phase}) {desc|firstline}\n' "$@" + > } + + $ hg init repo + $ cd repo + $ echo root > root + $ hg ci -Am addroot + adding root + $ echo a > a + $ hg ci -Am adda + adding a + $ echo b > b + $ hg ci -Am addb + adding b + $ echo c > c + $ hg ci -Am addc + adding c + $ glog + @ 3:7a7552255fb5@default(draft) addc + | + o 2:ef23d6ef94d6@default(draft) addb + | + o 1:93418d2c0979@default(draft) adda + | + o 0:c471ef929e6a@default(draft) addroot + + $ hg gdown + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [2] addb + $ echo b >> b + $ hg amend + 1 new unstables changesets + $ hg gdown + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [1] adda + $ echo a >> a + $ hg amend + 1 new unstables changesets + $ glog + @ 7:f5ff10856e5a@default(draft) adda + | + | o 5:ab8cbb6d87ff@default(draft) addb + | | + | | o 3:7a7552255fb5@default(draft) addc + | | | + | | x 2:ef23d6ef94d6@default(draft) addb + | |/ + | x 1:93418d2c0979@default(draft) adda + |/ + o 0:c471ef929e6a@default(draft) addroot + + +Test stabilizing a predecessor child + + $ hg stabilize -v + move:[5] addb + atop:[7] adda + hg rebase -Dr ab8cbb6d87ff -d f5ff10856e5a + resolving manifests + getting b + b + $ glog + @ 8:6bf44048e43f@default(draft) addb + | + o 7:f5ff10856e5a@default(draft) adda + | + | o 3:7a7552255fb5@default(draft) addc + | | + | x 2:ef23d6ef94d6@default(draft) addb + | | + | x 1:93418d2c0979@default(draft) adda + |/ + o 0:c471ef929e6a@default(draft) addroot + + +Test stabilizing a descendant predecessors child + + $ hg up 7 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ hg debugsuccessors > successors.old + $ hg stabilize -v + move:[3] addc + atop:[8] addb + hg rebase -Dr 7a7552255fb5 -d 6bf44048e43f + resolving manifests + getting b + resolving manifests + getting c + c + $ hg debugsuccessors > successors.new + $ diff -u successors.old successors.new + --- successors.old* (glob) + +++ successors.new* (glob) + @@ -1,5 +1,6 @@ + 3a4a591493f8 f5ff10856e5a + 3ca0ded0dc50 ab8cbb6d87ff + +7a7552255fb5 5e819fbb0d27 + 93418d2c0979 f5ff10856e5a + ab8cbb6d87ff 6bf44048e43f + ef23d6ef94d6 ab8cbb6d87ff + [1] + $ glog + @ 9:5e819fbb0d27@default(draft) addc + | + o 8:6bf44048e43f@default(draft) addb + | + o 7:f5ff10856e5a@default(draft) adda + | + o 0:c471ef929e6a@default(draft) addroot + + $ hg stabilize -v + no unstable changeset + [1] + +Test behaviour with --any + + $ hg up 8 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + $ echo b >> b + $ hg amend + 1 new unstables changesets + $ glog + @ 11:4e7cec6b4afe@default(draft) addb + | + | o 9:5e819fbb0d27@default(draft) addc + | | + | x 8:6bf44048e43f@default(draft) addb + |/ + o 7:f5ff10856e5a@default(draft) adda + | + o 0:c471ef929e6a@default(draft) addroot + + $ hg up 9 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg stabilize -v + nothing to stabilize here + (1 unstable changesets, do you want --any ?) + [2] + $ hg stabilize --any -v + move:[9] addc + atop:[11] addb + hg rebase -Dr 5e819fbb0d27 -d 4e7cec6b4afe + resolving manifests + removing c + getting b + resolving manifests + getting c + c + $ glog + @ 12:24f95816bb21@default(draft) addc + | + o 11:4e7cec6b4afe@default(draft) addb + | + o 7:f5ff10856e5a@default(draft) adda + | + o 0:c471ef929e6a@default(draft) addroot + + $ hg stabilize --any -v + no unstable changeset + [1]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-stabilize-result.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,51 @@ + $ cat >> $HGRCPATH <<EOF + > [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ glog() { + > hg glog --template \ + > '{rev}:{node|short}@{branch}({phase}) bk:[{bookmarks}] {desc|firstline}\n' "$@" + > } + +Test stabilize removing the changeset being stabilized + + $ hg init empty + $ cd empty + $ echo a > a + $ hg ci -Am adda a + $ echo b > b + $ hg ci -Am addb b + $ echo a >> a + $ hg ci -m changea + $ hg bookmark changea + $ hg up 1 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ echo a >> a + $ hg amend -m changea + 1 new unstables changesets + $ hg stabilize -v + move:[2] changea + atop:[4] changea + hg rebase -Dr cce2c55b8965 -d 1447e1c4828d + resolving manifests + $ glog --hidden + @ 4:1447e1c4828d@default(draft) bk:[changea] changea + | + | x 3:41ad4fe8c795@default(draft) bk:[] amends 102a90ea7b4a3361e4082ed620918c261189a36a + | | + | | x 2:cce2c55b8965@default(draft) bk:[] changea + | |/ + | x 1:102a90ea7b4a@default(draft) bk:[] addb + |/ + o 0:07f494440405@default(draft) bk:[] adda + + $ hg debugsuccessors + 102a90ea7b4a 1447e1c4828d + 41ad4fe8c795 1447e1c4828d + cce2c55b8965 000000000000
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-tutorial.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,1 @@ +../docs/tutorials/tutorial.t \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-uncommit.t Mon Jul 30 14:45:42 2012 +0200 @@ -0,0 +1,336 @@ + $ cat >> $HGRCPATH <<EOF + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "obsolete=$(echo $(dirname $TESTDIR))/hgext/obsolete.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ glog() { + > hg glog --template '{rev}:{node|short}@{branch}({obsolete}/{phase}) {desc|firstline}\n' "$@" + > } + + $ hg init repo + $ cd repo + +Cannot uncommit null changeset + + $ hg uncommit + abort: cannot rewrite immutable changeset + [255] + +Cannot uncommit public changeset + + $ echo a > a + $ hg ci -Am adda a + $ hg phase --public . + $ hg uncommit + abort: cannot rewrite immutable changeset + [255] + $ hg phase --force --draft . + +Cannot uncommit merge + + $ hg up -q null + $ echo b > b + $ echo c > c + $ echo d > d + $ echo f > f + $ echo g > g + $ echo j > j + $ echo m > m + $ echo n > n + $ echo o > o + $ hg ci -Am addmore + adding b + adding c + adding d + adding f + adding g + adding j + adding m + adding n + adding o + created new head + $ hg merge + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + (branch merge, don't forget to commit) + $ hg uncommit + abort: cannot uncommit while merging + [255] + $ hg ci -m merge + $ hg uncommit + abort: cannot uncommit merge changeset + [255] + +Prepare complicated changeset + + $ hg branch bar + marked working directory as branch bar + (branches are permanent and global, did you want a bookmark?) + $ hg cp a aa + $ echo b >> b + $ hg rm c + $ echo d >> d + $ echo e > e + $ hg mv f ff + $ hg mv g h + $ echo j >> j + $ echo k > k + $ echo l > l + $ hg rm m + $ hg rm n + $ echo o >> o + $ hg ci -Am touncommit + adding e + adding k + adding l + $ hg st --copies --change . + M b + M d + M j + M o + A aa + a + A e + A ff + f + A h + g + A k + A l + R c + R f + R g + R m + R n + $ hg man -r . + a + aa + b + d + e + ff + h + j + k + l + o + +Add a couple of bookmarks + + $ glog --hidden + @ 3:5eb72dbe0cb4@bar(stable/draft) touncommit + | + o 2:f63b90038565@default(stable/draft) merge + |\ + | o 1:f15c744d48e8@default(stable/draft) addmore + | + o 0:07f494440405@default(stable/draft) adda + + $ hg bookmark -r 2 unrelated + $ hg bookmark touncommit-bm + $ hg bookmark --inactive touncommit-bm-inactive + $ hg bookmarks + * touncommit-bm 3:5eb72dbe0cb4 + touncommit-bm-inactive 3:5eb72dbe0cb4 + unrelated 2:f63b90038565 + +Prepare complicated working directory + + $ hg branch foo + marked working directory as branch foo + (branches are permanent and global, did you want a bookmark?) + $ hg mv ff f + $ hg mv h i + $ hg rm j + $ hg rm k + $ echo l >> l + $ echo m > m + $ echo o > o + +Test uncommit without argument, should be a no-op + + $ hg uncommit + abort: nothing to uncommit + [255] + $ hg bookmarks + * touncommit-bm 3:5eb72dbe0cb4 + touncommit-bm-inactive 3:5eb72dbe0cb4 + unrelated 2:f63b90038565 + +Test no matches + + $ hg uncommit --include nothere + abort: nothing to uncommit + [255] + +Enjoy uncommit + + $ hg uncommit aa b c f ff g h j k l m o + $ hg branch + foo + $ hg st --copies + M b + A aa + a + A i + g + A l + R c + R g + R j + R m + $ cat aa + a + $ cat b + b + b + $ cat l + l + l + $ cat m + m + $ test -f c && echo 'error: c was removed!' + [1] + $ test -f j && echo 'error: j was removed!' + [1] + $ test -f k && echo 'error: k was removed!' + [1] + $ hg st --copies --change . + M d + A e + R n + $ hg man -r . + a + b + c + d + e + f + g + j + m + o + $ hg cat -r . d + d + d + $ hg cat -r . e + e + $ glog --hidden + @ 4:e8db4aa611f6@bar(stable/draft) touncommit + | + | x 3:5eb72dbe0cb4@bar(extinct/draft) touncommit + |/ + o 2:f63b90038565@default(stable/draft) merge + |\ + | o 1:f15c744d48e8@default(stable/draft) addmore + | + o 0:07f494440405@default(stable/draft) adda + + $ hg bookmarks + * touncommit-bm 4:e8db4aa611f6 + touncommit-bm-inactive 4:e8db4aa611f6 + unrelated 2:f63b90038565 + $ hg debugsuccessors + 5eb72dbe0cb4 e8db4aa611f6 + +Test phase is preserved, no local changes + + $ hg up -C 3 + 8 files updated, 0 files merged, 1 files removed, 0 files unresolved + Working directory parent is obsolete + $ hg --config extensions.purge= purge + $ hg uncommit -I 'set:added() and e' + 2 new conflictings changesets + $ hg st --copies + A e + $ hg st --copies --change . + M b + M d + M j + M o + A aa + A ff + f + A h + g + A k + A l + R c + R f + R g + R m + R n + $ glog --hidden + @ 5:c706fe2c12f8@bar(stable/draft) touncommit + | + | o 4:e8db4aa611f6@bar(stable/draft) touncommit + |/ + | x 3:5eb72dbe0cb4@bar(extinct/draft) touncommit + |/ + o 2:f63b90038565@default(stable/draft) merge + |\ + | o 1:f15c744d48e8@default(stable/draft) addmore + | + o 0:07f494440405@default(stable/draft) adda + + $ hg debugsuccessors + 5eb72dbe0cb4 c706fe2c12f8 + 5eb72dbe0cb4 e8db4aa611f6 + +Test --all + + $ hg up -C 3 + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + Working directory parent is obsolete + $ hg --config extensions.purge= purge + $ hg uncommit --all -X e + 1 new conflictings changesets + $ hg st --copies + M b + M d + M j + M o + A aa + a + A ff + f + A h + g + A k + A l + R c + R f + R g + R m + R n + $ hg st --copies --change . + A e + + $ hg debugsuccessors + 5eb72dbe0cb4 c4cbebac3751 + 5eb72dbe0cb4 c706fe2c12f8 + 5eb72dbe0cb4 e8db4aa611f6 + +Display a warning if nothing left + + $ hg uncommit e + new changeset is empty + (use "hg kill ." to remove it) + $ hg debugsuccessors + 5eb72dbe0cb4 c4cbebac3751 + 5eb72dbe0cb4 c706fe2c12f8 + 5eb72dbe0cb4 e8db4aa611f6 + c4cbebac3751 4f1c269eab68 + +Test instability warning + + $ hg ci -m touncommit + $ echo unrelated > unrelated + $ hg ci -Am addunrelated unrelated + $ hg gdown + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [8] touncommit + $ hg uncommit aa + 1 new unstables changesets