# HG changeset patch # User Pierre-Yves David # Date 1392929817 28800 # Node ID f49d4774b999465125a718f2bc5cd50b876c7977 # Parent 5af3098650405e474ea4d0a7853bb476c2de87b0# Parent ad2060da7ffa7a6320df30c716f4c44d1250641a importing fastop extension in this repo This repo is dedicated to experimentation on mutable history topic. Fastop is appropriate there. diff -r ad2060da7ffa -r f49d4774b999 .hgignore --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,16 @@ +syntax: re +/figures/[^/]+\.png$ +^docs/build/ +^docs/html/ +^html/ +\.pyc$ +~$ +\.swp$ +\.orig$ +\.rej$ +\.err$ +^tests/easy_run.sh$ +^build/ +^MANIFEST$ +^docs/tutorials/.*\.rst$ +\.ico$ diff -r ad2060da7ffa -r f49d4774b999 .hgtags --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgtags Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,21 @@ +6c6bb7a23bb5125bf06da73265f039dd3447dafa 0.1.0 +d3f20770b86a31dba56ae7b252089e12b34702da 0.2.0 +c046b083a5e0b21af69027f31cee141800cf894b 0.3.0 +9bbcd274689829d9239978236e16610688978233 0.4.0 +4ecbaec1d664b1e6f8ebc78292e1ced77a8e69c0 0.4.1 +7ef8ab8c6feadb8a9d9e13af144a17cb23e9a38d 0.5 +4a5488c01a66be4729252175cea0ea739a88a540 0.6 +18a0d96ed559089edf90206c469f3f8c26681c64 0.7 +18a0d96ed559089edf90206c469f3f8c26681c64 0.7 +1b2757c1bd918509184f6c1d06b2329a847e31b0 0.7 +b18b000363550f02f413aed008f8e306318c608c 1.0.0 +ca5bb72d14aeb6e6053e3a53c064a2b7dc8010e5 1.0.1 +b1bdcb4506defef0e857e2710633f7686d8034a5 1.0.2 +5559e5a4b656978c592d364f242edc62369d7e84 1.0.2 +c062edbcaf13135d0312fd2039deca74573ff4f0 1.1.0 +22cacfce2a65ab965c6179ae862b148f4abc7d8a 1.1.0 +d43e80504e55db9ad4826e860e50530103a27b0f 2.0.0 +f9d305deeff3dba782e65faf4ef3fd1569995859 2.1.0 +862b6b71a35836e81f090ba7229c2888e8ed2f9f 3.0.0 +cdb52bbbe5b8770d5e68943b7e73bee4ba136ecc 3.1.0 +c3ba8a965a7a173e388d84819e936ea9bae9797f 3.2.0 diff -r ad2060da7ffa -r f49d4774b999 COPYING --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/COPYING Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff -r ad2060da7ffa -r f49d4774b999 MANIFEST.in --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MANIFEST.in Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,17 @@ +recursive-include docs/figures *.svg +include docs/figures/hgview-example.png +include docs/*.rst +include docs/*.py +include docs/tutorials/*.t +include docs/makefile +include docs/static/*.svg +include hgext/__init__.py +include hgext/evolve.py +include hgext/pushexperiment.py +include setup.py +include README +include COPYING +include tests/*.t +include tests/*.py +exclude tests/test-oldconvert.t +exclude tests/test-qsync.t diff -r ad2060da7ffa -r f49d4774b999 Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Makefile Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,38 @@ +PYTHON=python +HG=`which hg` +VERSION=$(shell python setup.py --version) + + +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 + +deb-prepare: + python setup.py sdist --dist-dir .. + mv -f ../hg-evolve-$(VERSION).tar.gz ../mercurial-evolve_$(VERSION).orig.tar.gz + tar xf ../mercurial-evolve_$(VERSION).orig.tar.gz + rm -rf ../mercurial-evolve_$(VERSION).orig + mv hg-evolve-$(VERSION) ../mercurial-evolve_$(VERSION).orig + cp -r debian/ ../mercurial-evolve_$(VERSION).orig/ + @cd ../mercurial-evolve_$(VERSION).orig && echo 'debian build directory ready at' `pwd` + +.PHONY: tests all-version-tests diff -r ad2060da7ffa -r f49d4774b999 README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/README Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,197 @@ +============================= +Mutable History For Mercurial +============================= + +This package supplies the ``evolve`` extension for Mercurial, which +provides several commands to mutate history and deal with the +resulting issues. + +It also: + + - enables the "changeset obsolescence" feature of Mercurial + - issues several warning messages when trouble appears in your repository + +**This extension is experimental and not yet meant for production.** + +You can enable it by adding the line below to the ``extensions`` +section of your hgrc:: + + evolve = PATH/TO/mutable-history/hgext/evolve.py + +We recommend reading the documentation first. An online version is +available here: + + http://hg-lab.logilab.org/doc/mutable-history/html/ + +Or see the ``doc/`` directory for a local copy. + +Contribute +========== + +The simplest way to contribute is to issue a pull request on Bitbucket +(https://bitbucket.org/marmoute/mutable-history). + +However, some cutting-edge changes may be found in a mutable repository hosted +by logilab before they are published. + + http://hg-lab.logilab.org/wip/mutable-history/ + +Be sure to check latest draft changeset before submitting new changesets. + + +Changelog +========= + +3.3.0 -- + +- added Augie Facklers `fastop` extension (usage not recommended yet) +- add verbose hint about how to handle corner case by hand. + This should help people until evolve is able to to it itself. +- removed the qsync extension. The only user I knew about (logilab) is not + using it anymore. It not compatible with coming Mercurial version 2.9. +- add progress indicator for long evolve command +- report troubles creation from `hg import` + +3.2.0 -- 2013-11-15 + +- conform to the Mercurial custom of lowercase messages +- added a small extension to experiment with obsolescence marker push +- amend: drop the deprecated note option +- amend: use core mechanism for amend (fix multiple bugs) +- parents command: add "working directory parent is obsolete" message +- evolve command: allow updating to the successor if the parent is + obsolete +- gdown and gup commands: add next and previous alias, respectively +- make grab aliases compatible with Mercurial 2.8 +- Tested with 2.6, 2.7 and 2.8 + +3.1.0 -- 2013-02-11 + +- amend: drop deprecated --change option for amend +- alias: add a grab aliast to be used instead of graft -O +- touch: add a --duplicate option to *not* obsolete the old version +- touch: fix touching multiple revision at the same time +- evolve: add a --all option +- prune: various minor improvements +- prune: add option to prune a specific bookmark +- prune: add -u and -d option to control metadata + +3.0.0 -- 2013-02-02 + +- compatibility with 2.5 + +2.2.0 -- + +- make evolve smarter at picking next troubled to solved without --any + +2.1.0 -- 2012-12-03 + +- qsync fixes +- have qfold ask for commit message + +2.0.0 -- 2012-10-26 + +- compat with mercurial 2.4 + +1.1.0 -- 2012-10-26 + +- fix troubles creation reporting from rebase +- rename latecomer to bumped +- renamed conflicting to divergent +- smarter divergent handling + +1.0.2 -- 2012-09-19 + +- fix hg fold bug +- fix hg pull --rebase +- fix detection of conflict with external tools +- adapt to core movement (caches and --amend) + +1.0.1 -- 2012-08-31 + +- documentation improvement +- fix a performance bug with hgweb + +1.0 -- 2012-08-29 + +- Align with Mercurial version 2.3 (drop 2.2 support). +- stabilize handle killed parent +- stabilize handle late comer +- stabilize handle conflicting +- stabilize get a --continue switch +- merge and update ignore extinct changeset in most case. +- new "troubled()" revset +- summary now reports troubles changesets +- new touch command +- new fold command +- new basic olog alias + +- rebase refuse to work on public changeset again +- rebase explicitly state that there is nothing to rebase because everything is + extinct() when that happen. +- amend now cleanly abort when --change switch is misused + + +0.7 -- 2012-08-06 + +- hook: work around insanely huge value in obsolete pushkey call +- pushkey: properly handle abort during obsolete markers push +- amend: wrap the whole process in a single transaction. +- evolve: tweak and add EOL to kill warning +- obsolete: fix doc, rebase no longer aborts with --keep +- obsolete/evolve: fix grammar in prerequisite messages +- evolve: avoid duplication in graft wrapper +- evolve: graft --continue is optional, test + +0.6 -- 2012-07-31 + +- obsolete: change warning output to match mercurial core on +- qsync: ignore unexistent nodes +- make compat server both compatible with "dump" and "dump%i" version + +0.5 -- 2012-07-16 + +- obsolete: Detect conflicting changeset! +- obsolete: adapt to core: marker are written in transaction now +- evolve: add the solve alias to obsolete +- doc: big update of terms and summary of the concept +- evolve: switch the official name for "kill" to prune + + +0.4.1 -- 2012-07-10 + +- [convert] properly exclude null successors from convertion +- Ignore buggy marker in newerversion + + +0.4.0 -- 2012-07-06 + +- obsolete: public changeset are no longer latecomer. +- obsolete: move to official binary format +- adapt for new mercurial +- obsolete: we are not compatible with 2.1 any more + +0.3.0 -- 2012-06-27 + +- +- obsolete: Add "latecomer" error detection (stabilize does not handle resolution yet) +- evolve: Introduce a new `uncommit` command to remove change from a changeset +- 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` + + diff -r ad2060da7ffa -r f49d4774b999 contrib/nopushpublish.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/contrib/nopushpublish.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,36 @@ +# Extension which prevent changeset to be turn public by push operation +# +# Copyright 2011 Logilab SA +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + + +from mercurial import extensions, util +from mercurial import discovery + +def checkpublish(orig, repo, remote, outgoing, *args): + + # is remote publishing? + publish = True + if 'phases' in remote.listkeys('namespaces'): + remotephases = remote.listkeys('phases') + publish = remotephases.get('publishing', False) + + npublish = 0 + if publish: + for rev in outgoing.missing: + if repo[rev].phase(): + npublish += 1 + if npublish: + repo.ui.warn("Push would publish %s changesets" % npublish) + + ret = orig(repo, remote, outgoing, *args) + if npublish: + raise util.Abort("Publishing push forbiden", + hint="Use `hg phase -p ` to manually publish them") + + return ret + +def uisetup(ui): + extensions.wrapfunction(discovery, 'checkheads', checkpublish) diff -r ad2060da7ffa -r f49d4774b999 debian/changelog --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/changelog Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,42 @@ +mercurial-evolve (3.2.0-1) UNRELEASED; urgency=low + + * New upstream release. + + -- Julien Cristau Tue, 04 Jun 2013 17:28:02 +0200 + +mercurial-evolve (3.1.0-1) UNRELEASED; urgency=low + + * New upstream release. + + -- Pierre-Yves David Mon, 04 Mar 2013 18:02:15 +0100 + +mercurial-evolve (2.1.0-1) UNRELEASED; urgency=low + + * New upstream release + + -- Pierre-Yves David Mon, 03 Dec 2012 15:19:19 +0100 + +mercurial-evolve (1.1.0-1) UNRELEASED; urgency=low + + * New upstream release. + + -- Pierre-Yves David Tue, 20 Nov 2012 16:28:12 +0100 + +mercurial-evolve (1.0.2-1) UNRELEASED; urgency=low + + * New upstream Release + + -- Pierre-Yves David Wed, 19 Sep 2012 17:38:47 +0200 + +mercurial-evolve (1.0.1-1) UNRELEASED; urgency=low + + * New bug fix release + * remove conflicting __init__.py + + -- Pierre-Yves David Fri, 31 Aug 2012 11:31:03 +0200 + +mercurial-evolve (1.0.0-1) UNRELEASED; urgency=low + + * Initial release. + + -- Julien Cristau Fri, 24 Aug 2012 16:46:30 +0200 diff -r ad2060da7ffa -r f49d4774b999 debian/compat --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/compat Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1 @@ +8 diff -r ad2060da7ffa -r f49d4774b999 debian/control --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/control Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,39 @@ +Source: mercurial-evolve +Section: vcs +Priority: optional +Maintainer: Logilab +Uploaders: + Julien Cristau , + Pierre-Yves David , +Standards-Version: 3.9.3 +Build-Depends: + mercurial (>=2.6~), + python, + debhelper (>= 8), + python-sphinx (>= 1.0.8), + imagemagick, + librsvg2-bin, +Python-Version: >= 2.6 +Homepage: https://bitbucket.org/marmoute/mutable-history + +Package: mercurial-evolve +Architecture: all +Depends: + ${python:Depends}, + ${misc:Depends}, + mercurial (>= 2.6), +Description: evolve extension for Mercurial + This package provides the experimental "evolve" extension for the Mercurial + DVCS. + . + This extension provides several commands to mutate history and deal with issues + it may raise. + . + It also: + - enables the "Changeset Obsolescence" feature of mercurial, + - alters core command and extension that rewrite history to use this feature, + - improves some aspects of the early implementation in Mercurial 2.3. + . + **These extensions are experimental and are not meant for production.** + + diff -r ad2060da7ffa -r f49d4774b999 debian/copyright --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/copyright Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,15 @@ +This software was downloaded from +https://bitbucket.org/marmoute/mutable-history + +Copyright 2011 Peter Arrenbrecht + Logilab SA + Pierre-Yves David + Patrick Mezard + + +This software may be used and distributed according to the terms of the GNU +General Public License version 2 or any later version. + +On Debian systems, the complete text of the GNU General Public License version +2 can be found in `/usr/share/common-licenses/GPL-2'. + diff -r ad2060da7ffa -r f49d4774b999 debian/docs --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/docs Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1 @@ +html diff -r ad2060da7ffa -r f49d4774b999 debian/rules --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/rules Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,27 @@ +#!/usr/bin/make -f + +clean %: + dh $@ --with python2 --buildsystem=python_distutils + +build: + dh build --with python2 --buildsystem=python_distutils + $(MAKE) -C docs + +ifeq (,$(filter nocheck, $(DEB_BUILD_OPTIONS))) +override_dh_auto_test: + cd tests && python run-tests.py --with-hg=`which hg` +endif + +override_dh_python2: + # avoid conflict with mercurial's own hgext/__init__.py + find debian -name __init__.py -delete + dh_python2 + +clean: clean-docs + +clean-docs: + rm -rf html + rm -f docs/static/logo-evolve.ico + rm -f docs/tutorials/tutorial.rst + +.PHONY: build clean clean-docs diff -r ad2060da7ffa -r f49d4774b999 debian/source/format --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/debian/source/format Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1 @@ +3.0 (quilt) diff -r ad2060da7ffa -r f49d4774b999 docs/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/README Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ +doc generated with sphinx. tutorial exported using sphinxedhg + +http://hg.piranha.org.ua/sphinxedhg/ diff -r ad2060da7ffa -r f49d4774b999 docs/conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/conf.py Thu Feb 20 12:56:57 2014 -0800 @@ -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 +# " v 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 = 'logo-evolve.svg' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = 'logo-evolve.ico' + +# 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/. +#html_copy_source = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# 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 = '' diff -r ad2060da7ffa -r f49d4774b999 docs/evolve-collaboration.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/evolve-collaboration.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,135 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +------------------------------------------------ +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 her 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 evolve + +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, provides evolution +semantics by only adding new information to the repository (but more +on that later). + +She pushes again. + diff -r ad2060da7ffa -r f49d4774b999 docs/evolve-faq.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/evolve-faq.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,215 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +--------------------------------------------------------------------- +Evolve How To +--------------------------------------------------------------------- + + + +Add a changeset: ``commit`` +------------------------------------------------------------ + +Just use commit as usual. New changesets will be in the `draft` phase. + +Rewrite a changeset: ``commit --amend`` +------------------------------------------------------------ + +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 like + pdiff=diff --rev .^ + + # status what amend will look like + pstatus=status --rev .^ + +This command can even be invoked on changesets with children, provided +none are public. + +.. [#] (defined by the evolve extension for you) + + + +Move a changeset: ``grab`` +------------------------------------------------------------ + +You can use ``hg grab `` to move a rev at your current location, making the +old version obsolete. + +.. note:: grab is an alias for ``hg rebase --dest . --rev $@; hg up `` + + +Delete a changeset: ``prune`` +------------------------------------------------------------ + +A new ``prune`` command allows removing a changeset. + +Just use ``hg prune ``. + + +Moving within the history: ``gdown`` and ``gup`` +------------------------------------------------------------ + +While working on mutable part of the history you often need to move between +mutable commits. + +You just need to use standard update to work with evolve. For convenience, you +can use ``hg gup`` to move to the child commit or ``hg gdown`` to move to the parent commit. + +Those command have ``previous`` and ``next`` alias. + +.. note:: Those commands only exist for the convenience of getting qpush and qpop + feeling back. + +Collapse changesets: ``fold`` +------------------------------------------------------------ + +You can use ``hg fold`` to collapse multiple changesets in a single one. + +It takes two forms: + +``hg fold `` folds everything from you current changeset to `` + +``hg fold -r `` fold everything changeset matching the revset together. + +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 a 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 + +For more complexe scenario we recommend the use of the histedit_ extension. + +.. _histedit: http://mercurial.selenic.com/wiki/HisteditExtension + + +Update my current work in progress 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 changesets 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 changesets already obsolete will likely result in + divergent versions of the changesets. + +Resolve history troubles: ``evolve`` +------------------------------------------------------------ + +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. + +You can also use evolve to solve `bumped` and `divergent` changeset/ + + +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 --succ `` to add obsolete +marker. + +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 currently supports this feature. You +need version 1.6.2 + +.. _hgview: http://www.logilab.org/project/hgview/ + +.. image:: figures/hgview-example.png + :scale: 50% + + +You can also use a debug command + + $ hg debugobsolete + 5eb72dbe0cb4 e8db4aa611f6 + c4cbebac3751 4f1c269eab68 + + + +Important Note +===================================================================== + +View change to your file +------------------------------------------------------------ + +Extinct changesets are hidden using the *hidden* feature of mercurial. + +Only ``hg log`` and ``hgview`` support it, other +graphical viewer do not. + +You can use ``hg log --graph --hidden`` from the command line + + + + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/evolve-good-practice.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/evolve-good-practice.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,47 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +----------------------------------------- +Good practice for (early) users 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 constrained to linear history. + +Making a branch per independent branch will help you avoid unstability +and conflict. + +Rewrite your changes 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 branches will help you to achieve this goal. + +Prefer pushing unstability to touching other people changesets +-------------------------------------------------------------- + + +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. diff -r ad2060da7ffa -r f49d4774b999 docs/figures/edit-is-rewrite-step1.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/edit-is-rewrite-step1.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ";Alice;Babar"19b08111 + + + + "19b08111;Alice;Babar,Celeste"925d8319 + + + + "6fcdd7fe;Bob;Babar"⟶ebc2b5a1 + + + + "925d8319;Alice;Babar,Celeste,flore"6fcdd7fe + + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/edit-is-rewrite-step2.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/edit-is-rewrite-step2.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,832 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + ";Alice;Babar"19b08111 + + + + "19b08111;Alice;Babar,Celeste"925d8319 + + + + "6fcdd7fe;Bob;Babar"⟶ebc2b5a1 + + + + "925d8319;Alice;Babar,Celeste,flore"6fcdd7fe + + + + + + + "19b08111;Bob;Babar,Celeste"1a25964c + + + + "6d9e1549;Bob;Babar"dda72e36 + + + + "1a25964c;Alice;Babar,Celeste,Flore"6d9e1549 + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/error-conflicting.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-conflicting.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-18 23:47ZCanvas 1Layer 1A’AA’’Conflicting diff -r ad2060da7ffa -r f49d4774b999 docs/figures/error-extinct.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-extinct.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-18 23:47ZCanvas 1Layer 1ABBAA'CC'CObsoleteUnstableextinctsuspended diff -r ad2060da7ffa -r f49d4774b999 docs/figures/error-obsolete.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-obsolete.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-18 23:47ZCanvas 1Layer 1ACBB’C’Obsolete diff -r ad2060da7ffa -r f49d4774b999 docs/figures/error-unstable.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/error-unstable.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-18 23:47ZCanvas 1Layer 1ABAA'ObsoleteUnstableB diff -r ad2060da7ffa -r f49d4774b999 docs/figures/example-1-update.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/example-1-update.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-21 08:32ZCanvas 1Layer 1AA’A diff -r ad2060da7ffa -r f49d4774b999 docs/figures/example-2-split.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/example-2-split.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-21 08:32ZCanvas 1Layer 1AA1A2A diff -r ad2060da7ffa -r f49d4774b999 docs/figures/explain-troubles-concurrent-10-solution.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/explain-troubles-concurrent-10-solution.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,929 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + B'' + + + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + B'' + + + + + + + + + + + + B + + + + B' + + + + B'' + + + + B* + + + + + + + + B* + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/explain-troubles-concurrent-10-sumup.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/explain-troubles-concurrent-10-sumup.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Local + Distant + final = divergent + + + + + initial + + + + B + + + + A + + + + Z + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + + + + + B + + + + A + + + + Z + + + + + + B'' + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + B'' + + + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/explain-troubles-latecomer-10-sumup.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/explain-troubles-latecomer-10-sumup.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1039 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + Local + Distant + final = tardif + + + + + + + + A + + + + Z + + + + + + + A + + + + Z + + + + + A' + + + + + initial + + + + v4.2 + + + + A + + + + Z + + + + + + A' + + + + + + + + v4.2 + + + + A + + + + Z + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/explain-troubles-latecomer-15-solution.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/explain-troubles-latecomer-15-solution.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,994 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + v4.2 + + + + A + + + + Z + + + + + + A' + + + + + + + + + v4.2 + + + + A + + + + Z + + + + + + A' + + Aᵟ + + + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/explain-troubles-unstable-10-sumup.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/explain-troubles-unstable-10-sumup.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + A' + + + + + + + Local + + + + + + B + + + + A + + + + Z + + + + + + C + + + + Distant + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + A' + + + + + + + + C + + + + final = instable + + + + + + B + + + + A + + + + Z + + + + + initial + + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/explain-troubles-unstable-15-solution.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/explain-troubles-unstable-15-solution.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1006 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + A' + + + + + + + + C + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + A' + + + + + + + + C + + + + + C' + + + + + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/git.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/git.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,863 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + A' + + + + + + + + + + + + B + + + + A + + + + Z + + + + + + B' + + + + A' + + + + + + C + + + + + feature-babar + other/feature-babar + feature-babar + + diff -r ad2060da7ffa -r f49d4774b999 docs/figures/hgview-example.png Binary file docs/figures/hgview-example.png has changed diff -r ad2060da7ffa -r f49d4774b999 docs/figures/simple-3-merge.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/simple-3-merge.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-21 08:32ZCanvas 1Layer 1AABBC diff -r ad2060da7ffa -r f49d4774b999 docs/figures/simple-4-reorder.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/simple-4-reorder.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-21 08:32ZCanvas 1Layer 1AABBA'B' diff -r ad2060da7ffa -r f49d4774b999 docs/figures/simple-5-delete.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/figures/simple-5-delete.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,3 @@ + + +2012-03-21 08:32ZCanvas 1Layer 1AA diff -r ad2060da7ffa -r f49d4774b999 docs/from-mq.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/from-mq.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,172 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +------------------------------------------- +From MQ To Evolve, The Refugee Book +------------------------------------------- + +Cheat sheet +------------- + +============================== ============================================ +mq command new equivalent +============================== ============================================ +qseries ``log`` +qnew ``commit`` +qrefresh ``amend`` +qrefresh --exclude ``uncommit`` +qpop ``update`` or ``gdown`` +qpush ``update`` or ``gup`` sometimes ``evolve`` +qrm ``prune`` +qfold ``fold`` +qdiff ``odiff`` +qrecord ``record`` + +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:: + + [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 parent's 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. + + +.. -c is very confusig +.. +.. The amend command also has a -c switch which allows you to make an +.. explicit amending commit before rewriting a changeset.:: +.. +.. $ hg record -m 'feature A' +.. # oups, I forgot 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 --exclude +````````````````````` + +To remove changes from your current commit use:: + + $ hg uncommit not-ready.txt + + +hg qpop +````````` + +The following command emulates 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 +"unstable". You need to rewrite them on top of the new version of their +ancestor. + +The evolution extension adds a command to rewrite "unstable" +changesets::: + + $ hg evolve + +You can also decide to do it manually using:: + + $ hg graft -O + +or:: + + $ hg rebase -r -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 + +hg qfold +````````` + + +:: + + $ hg fold first::last + +hg qdiff +````````` + +``pdiff`` is an alias for `hg diff -r .^` It works like qdiff, but outside MQ. + + + +hg qfinish and hg qimport +```````````````````````````` + +These are not necessary anymore. If you want to control the +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. diff -r ad2060da7ffa -r f49d4774b999 docs/index.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/index.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,166 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +======================================== +Changeset Evolution Experimentation +======================================== + + +This is the online documentation of the `evolve extension`_. An experimental +extension that drive the implementation of the `changeset evolution concept`_ for +Mercurial. + +.. _`evolve extension`: http://mercurial.selenic.com/wiki/EvolveExtension +.. _`changeset evolution concept`: http://mercurial.selenic.com/wiki/ChangesetEvolution + +Here are various materials on planned improvement to Mercurial regarding +rewriting history. + +First, read about what challenges 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 **obsolescence marker** concept aims to provide an alternative to ``strip`` + to get rid of changesets. This concept has been partially implemented since + Mercurial 2.3. + + * The **evolve** Mercurial extension rewrites history using obsolete + *marker* under the hood. + +The first and most important step is by far the **obsolescence 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 by MQ and pbranch but has multiple advantages over +them: + +* Focus on your current work. + + You can focus your work on a single changeset and take care of adapting + descendent changesets later. + +* Handle **non-linear history with branches and merges** + +* Rely internally on Mercurial's **robust merge** mechanism. + + Simple conflicts are handled by real merge tools using the appropriate ancestor. + Conflicts 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 changesets** and forget about patches. Evolve 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 duplicates + (thanks to obsolete marker). + +* Cover all MQ usage but guard. + +.. warning:: The evolve extension and obsolete markers are at an experimental + stage. While using obsolete you willl likely be exposed to complex + implications of the **obsolete marker** concept. I do not recommend + non-power users to test this at this stage. + + While numbered 1.0.0, the command line API of this version should + **not** be regarded as *stable*: command behavior, name and + options may change in future releases or once integrated into + Mercurial. It is still an immature extension; a lot of + features are still missing but there is low risk of + repository corruption. + + Production-ready version should hide such details from normal users. + +The evolve extension requires Mercurial 2.5 (older versions supports down to 2.2) + +To enable the evolve extension use:: + + $ hg clone https://bitbucket.org/marmoute/mutable-history -u stable + $ echo '[extensions]\nevolve=$PWD/mutable-history/hgext/evolve.py' >> ~/.hgrc + +You will probably want to use hgview_ to visualize obsolescence. Version 1.7.1 +or later is required. + +.. _hgview: http://www.logilab.org/project/hgview/ + + + --- + +For more information see the documents below: + +.. toctree:: + :maxdepth: 1 + + tutorials/tutorial + evolve-good-practice + evolve-faq + from-mq + evolve-collaboration + qsync + +Smart changeset deletion: Obsolete Marker +========================================== + +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 that 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 + +- 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 the documents below + +.. toctree:: + :maxdepth: 1 + + obs-concept + obs-terms + obs-implementation + + +Known limitations and bugs +================================= + +Here is a list of known issues that will be fixed later: + + +* You need to provide to `graft --continue -O` if you started you + graft using `-O`. + + you to manually specify target all the time. + +* Trying to exchange obsolete marker with a static http repo will crash. + +* 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. Neither ``hg heads`` nor other visual viewers do. + +* hg heads shows extinct changesets. diff -r ad2060da7ffa -r f49d4774b999 docs/instability.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/instability.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,222 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +----------------------------------- +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 which leads from the old content to the new content. + +All three elements are used to compute a *unique* hash that identifies the changeset +(with various other metadata). This identification is a key part of DVCS design. + +This is a very useful property because changing B's parent means +changing B's content too. This requires the creation of **another** +changeset, which is semantically good. + +.. figure:: ./figures/edit-is-rewrite-step2.svg + + +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 the +children's parents! And because children of the rewritten changeset +still **depend** on the older "dead" version of the changeset, we +cannot 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 from getting rid of it. + +This instability is an **unavoidable consequence** of the strict dependency of +changesets. Rewriting history always needs to take it into account and +provide a way to rewrite the descendants of the new changeset to avoid +coexistence of the old and new versions of a rewritten changeset. + + +Everybody is working around the issue +------------------------------------------------ + +I'm not claiming that rewriting history is impossible. People have been successfully +doing it for years. However they all need to work around *instability*. Several +workaround strategies exist. + + +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 changesets with descendants. The git +branch design enforces this approach in git too. + + +However, DVCS are **distributed**. This means that you do not control what +happens outside your repository. Once a changeset has 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 changesets +``````````````````````````````````` + +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 developer 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. + + +Lose 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 uses plain patch to store changeset contents, which has + problems 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 trouble. + +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 allows overwriting 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 options +you deny. And even the most restrictive workflow 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 +am developing a new feature for Mercurial called "Obsolete markers". +Obsolete markers have two key properties: + + +* Any "old" changeset we want to get rid of is **explicitly** marked + as "obsolete" by history rewriting operations. + + 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 edit conflicts [#]_ . + +.. [#] Edit conflicts is another major obstable to collaboration. See the + section dedicated to obsolete marker for details. + +Improved robustness == improved simplicity +```````````````````````````````````````````````` + +This proposal should **first** be seen as a safety measure. + +It allows detecting instability as soon as possible. + +:: + + $ hg pull + added 3 changeset + +2 unstable changeset + (do you want "hg evolve" ?) + working directory parent is obsolete! + $ hg push + outgoing unstable changesets + (use "hg evolve" or force the push) + +And it 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. + + diff -r ad2060da7ffa -r f49d4774b999 docs/makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/makefile Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,11 @@ + +all: tutorials/tutorial.rst static/logo-evolve.ico + sphinx-build . ../html/ + +tutorials/tutorial.rst: tutorials/tutorial.t + python test2rst.py tutorials/ + +static/logo-evolve.ico: static/logo-evolve.svg + convert -resize 36x36 static/logo-evolve.svg static/logo-evolve.ico + + diff -r ad2060da7ffa -r f49d4774b999 docs/obs-concept.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/obs-concept.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,390 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +----------------------------------------------------------- +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*. + + +.. figure:: ./figures/git.* + + +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'` + +.. figure:: ./figures/explain-troubles-concurrent-10-solution.* + +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 **bumped** and + highlighted as an error case. + +.. figure:: ./figures/explain-troubles-concurrent-10-sumup.* + +Solving such an error is easy. Because we know what changeset a +*bumped* tries to rewrite, we can easily compute a smaller +changeset containing only the change from the old *public* to the new +*bumped*. + +.. figure:: ./figures/explain-troubles-concurrent-15-solution.* + + +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. diff -r ad2060da7ffa -r f49d4774b999 docs/obs-implementation.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/obs-implementation.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,106 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +----------------------------------------------------- +Implementation of Obsolete Marker +----------------------------------------------------- +.. warning:: This document is still in heavy work in progress + +Main questions about Obsolete Marker Implementation +----------------------------------------------------- + + + + +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 +----------------------------------------------------- + +Obsolete marker are partialy in core. + +2.3: + +- storage over obsolete marker +- exchange suing pushkey +- extinct changeset are properly hidden +- extinct changeset are excluded from exchange diff -r ad2060da7ffa -r f49d4774b999 docs/obs-terms.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/obs-terms.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,237 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +----------------------------------------------------------- +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. | +| | | | +| +--------------------------+-----------------------------+ +| | | | +| | **troubled** | **unstable** | +| | | | +| | *troubled* 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 *troubled* base to | +| | column. It is possible | solve the problem. | +| | for *troubled* | | +| | changeset to combine | (possible alternative name: | +| | multiple issue at once. | precarious) | +| | (a.k.a. divergent and | | +| | unstable) +-----------------------------+ +| | | | +| | (possible alternative | **bumped** | +| | names: unsettled, | | +| | troublesome | *bumped* is a changeset | +| | | that tries to be successor | +| | | of public changesets. | +| | | | +| | | Public changeset can't | +| | | be deleted and replace | +| | | *bumped* | +| | | need to be converted into | +| | | an overlay to this public | +| | | changeset. | +| | | | +| | | (possible alternative names:| +| | | mislead, naive, unaware, | +| | | mindless, disenchanting) | +| | | | +| | +-----------------------------+ +| | | **divergent** | +| | | | +| | | *divergent* is changeset | +| | | that appears when multiple | +| | | changesets are successors | +| | | of the same precursor. | +| | | | +| | | *divergent* are solved | +| | | through a three ways merge | +| | | between the two | +| | | *divergent* , | +| | | using the last "obsolete- | +| | | -common-ancestor" as the | +| | | base. | +| | | | +| | | (*splitting* is | +| | | properly not detected as a | +| | | conflict) | +| | | | +| | | (possible alternative names:| +| | | clashing, rival, concurent, | +| | | conflicting) | +| | | | +| +--------------------------+-----------------------------+ +| | | +| | Mutable changesets which are neither *obsolete* or | +| | *troubled* 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.| +| | ++------------------------------------------------------------------------------+ + + + +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. + +evolve +``````````````` + +Automatically resolve *troublesome* changesets +(*unstable*, *bumped* and *divergent*) + +This is an important name as hg pull/push will suggest it the same way it +suggest merging when you add heads. + +alternative names: + +- solve (too generic ?) +- stabilize diff -r ad2060da7ffa -r f49d4774b999 docs/qsync.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/qsync.rst Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,19 @@ +.. Copyright 2011 Pierre-Yves David +.. Logilab SA + +--------------------------------------------------------------------- +Qsync: Mercurial to MQ exporter +--------------------------------------------------------------------- + + +People may have tools or co-workers that expect 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 diff -r ad2060da7ffa -r f49d4774b999 docs/static/logo-evolve.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/static/logo-evolve.svg Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,622 @@ + + + +image/svg+xmlCali Mastny and Matt MackallFeb 12 2008 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +evolve + \ No newline at end of file diff -r ad2060da7ffa -r f49d4774b999 docs/test2rst.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/test2rst.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,62 @@ +#!/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: + 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 + 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) + + 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]) diff -r ad2060da7ffa -r f49d4774b999 docs/tutorials/tutorial.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/tutorials/tutorial.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1 @@ +../../tests/test-tutorial.t \ No newline at end of file diff -r ad2060da7ffa -r f49d4774b999 hgext/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/__init__.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1 @@ +# Copyright 2011 Logilab SA diff -r ad2060da7ffa -r f49d4774b999 hgext/drophack.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/drophack.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,163 @@ +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +'''This extension add a hacky command to drop changeset during review + +This extension is intended as a temporary hack to allow Matt Mackall to use +evolve in the Mercurial review it self. You should probably not use it if your +name is not Matt Mackall. +''' + +import os +import time +import contextlib + +from mercurial.i18n import _ +from mercurial import cmdutil +from mercurial import repair +from mercurial import scmutil +from mercurial import lock as lockmod +from mercurial import util +from mercurial import commands + +cmdtable = {} +command = cmdutil.command(cmdtable) + + +@contextlib.contextmanager +def timed(ui, caption): + ostart = os.times() + cstart = time.time() + yield + cstop = time.time() + ostop = os.times() + wall = cstop - cstart + user = ostop[0] - ostart[0] + sys = ostop[1] - ostart[1] + comb = user + sys + ui.write("%s: wall %f comb %f user %f sys %f\n" + % (caption, wall, comb, user, sys)) + +def obsmarkerchainfrom(obsstore, nodes): + """return all marker chain starting from node + + Starting from mean "use as successors".""" + # XXX need something smarter for descendant of bumped changeset + seennodes = set(nodes) + seenmarkers = set() + pendingnodes = set(nodes) + precursorsmarkers = obsstore.precursors + while pendingnodes: + current = pendingnodes.pop() + new = set() + for precmark in precursorsmarkers.get(current, ()): + if precmark in seenmarkers: + continue + seenmarkers.add(precmark) + new.add(precmark[0]) + yield precmark + new -= seennodes + pendingnodes |= new + +def stripmarker(ui, repo, markers): + """remove from the repo obsstore + + The old obsstore content is saved in a `obsstore.prestrip` file + """ + repo = repo.unfiltered() + repo.destroying() + oldmarkers = list(repo.obsstore._all) + util.rename(repo.sjoin('obsstore'), + repo.join('obsstore.prestrip')) + del repo.obsstore # drop the cache + newstore = repo.obsstore + assert not newstore # should be empty after rename + tr = repo.transaction('drophack') + try: + for m in oldmarkers: + if m not in markers: + newstore.add(tr, [m]) + tr.close() + finally: + tr.release() + repo.destroyed() + + +@command('drop', [('r', 'rev', [], 'revision to update')], _('[-r] revs')) +def cmddrop(ui, repo, *revs, **opts): + """I'm hacky do not use me! + + This command strip a changeset, its precursors and all obsolescence marker + associated to its chain. + + There is no way to limit the extend of the purge yet. You may have to + repull from other source to get some changeset and obsolescence marker + back. + + This intended for Matt Mackall usage only. do not use me. + """ + revs = list(revs) + revs.extend(opts['rev']) + if not revs: + revs = ['.'] + # get the changeset + revs = scmutil.revrange(repo, revs) + if not revs: + ui.write_err('no revision to drop\n') + return 1 + # lock from the beginning to prevent race + wlock = lock = None + try: + lock = repo.wlock() + lock = repo.lock() + # check they have no children + if repo.revs('%ld and public()', revs): + ui.write_err('cannot drop public revision') + return 1 + if repo.revs('children(%ld) - %ld', revs, revs): + ui.write_err('cannot drop revision with children') + return 1 + if repo.revs('. and %ld', revs): + newrevs = repo.revs('max(::. - %ld)', revs) + if newrevs: + assert len(newrevs) == 1 + newrev = newrevs[0] + else: + newrev = -1 + commands.update(ui, repo, newrev) + ui.status(_('working directory now at %s\n') % repo[newrev]) + # get all markers and successors up to root + nodes = [repo[r].node() for r in revs] + with timed(ui, 'search obsmarker'): + markers = set(obsmarkerchainfrom(repo.obsstore, nodes)) + ui.write('%i obsmarkers found\n' % len(markers)) + cl = repo.unfiltered().changelog + with timed(ui, 'search nodes'): + allnodes = set(nodes) + allnodes.update(m[0] for m in markers if cl.hasnode(m[0])) + ui.write('%i nodes found\n' % len(allnodes)) + cl = repo.changelog + visiblenodes = set(n for n in allnodes if cl.hasnode(n)) + # check constraint again + if repo.revs('%ln and public()', visiblenodes): + ui.write_err('cannot drop public revision') + return 1 + if repo.revs('children(%ln) - %ln', visiblenodes, visiblenodes): + ui.write_err('cannot drop revision with children') + return 1 + + if markers: + # strip them + with timed(ui, 'strip obsmarker'): + stripmarker(ui, repo, markers) + # strip the changeset + with timed(ui, 'strip nodes'): + repair.strip(ui, repo, allnodes, backup="all", topic='drophack') + + finally: + lockmod.release(lock, wlock) + + # rewrite the whole file. + # print data. + # - time to compute the chain + # - time to strip the changeset + # - time to strip the obs marker. diff -r ad2060da7ffa -r f49d4774b999 hgext/evolve.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/evolve.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1852 @@ +# Copyright 2011 Peter Arrenbrecht +# Logilab SA +# Pierre-Yves David +# Patrick Mezard +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +'''Extends Mercurial feature related to Changeset Evolution + +This extension provides several commands to mutate history and deal with +issues it may raise. + +It also: + + - enables the "Changeset Obsolescence" feature of mercurial, + - alters core commands and extensions that rewrite history to use + this feature, + - improves some aspect of the early implementation in 2.3 +''' + +testedwith = '2.7 2.7.1 2.7.2 2.8 2.8.1' +buglink = 'https://bitbucket.org/marmoute/mutable-history/issues' + +import sys +import random + +import mercurial +from mercurial import util + +try: + from mercurial import obsolete + if not obsolete._enabled: + obsolete._enabled = True + from mercurial import bookmarks + bookmarks.bmstore +except (ImportError, AttributeError): + raise util.Abort('Your Mercurial is too old for this version of Evolve', + hint='requires version >> 2.4.x') + + + +from mercurial import bookmarks +from mercurial import cmdutil +from mercurial import commands +from mercurial import context +from mercurial import copies +from mercurial import error +from mercurial import extensions +from mercurial import hg +from mercurial import lock as lockmod +from mercurial import merge +from mercurial import node +from mercurial import phases +from mercurial import revset +from mercurial import scmutil +from mercurial import templatekw +from mercurial.i18n import _ +from mercurial.commands import walkopts, commitopts, commitopts2 +from mercurial.node import nullid + + + +# This extension contains the following code +# +# - Extension Helper code +# - Obsolescence cache +# - ... +# - Older format compat + + + +##################################################################### +### Extension helper ### +##################################################################### + +class exthelper(object): + """Helper for modular extension setup + + A single helper should be instanciated for each extension. Helper + methods are then used as decorator for various purpose. + + All decorators return the original function and may be chained. + """ + + def __init__(self): + self._uicallables = [] + self._extcallables = [] + self._repocallables = [] + self._revsetsymbols = [] + self._templatekws = [] + self._commandwrappers = [] + self._extcommandwrappers = [] + self._functionwrappers = [] + self._duckpunchers = [] + + def final_uisetup(self, ui): + """Method to be used as the extension uisetup + + The following operations belong here: + + - Changes to ui.__class__ . The ui object that will be used to run the + command has not yet been created. Changes made here will affect ui + objects created after this, and in particular the ui that will be + passed to runcommand + - Command wraps (extensions.wrapcommand) + - Changes that need to be visible to other extensions: because + initialization occurs in phases (all extensions run uisetup, then all + run extsetup), a change made here will be visible to other extensions + during extsetup + - Monkeypatch or wrap function (extensions.wrapfunction) of dispatch + module members + - Setup of pre-* and post-* hooks + - pushkey setup + """ + for cont, funcname, func in self._duckpunchers: + setattr(cont, funcname, func) + for command, wrapper in self._commandwrappers: + extensions.wrapcommand(commands.table, command, wrapper) + for cont, funcname, wrapper in self._functionwrappers: + extensions.wrapfunction(cont, funcname, wrapper) + for c in self._uicallables: + c(ui) + + def final_extsetup(self, ui): + """Method to be used as a the extension extsetup + + The following operations belong here: + + - Changes depending on the status of other extensions. (if + extensions.find('mq')) + - Add a global option to all commands + - Register revset functions + """ + knownexts = {} + for name, symbol in self._revsetsymbols: + revset.symbols[name] = symbol + for name, kw in self._templatekws: + templatekw.keywords[name] = kw + for ext, command, wrapper in self._extcommandwrappers: + if ext not in knownexts: + e = extensions.find(ext) + if e is None: + raise util.Abort('extension %s not found' % ext) + knownexts[ext] = e.cmdtable + extensions.wrapcommand(knownexts[ext], commands, wrapper) + for c in self._extcallables: + c(ui) + + def final_reposetup(self, ui, repo): + """Method to be used as a the extension reposetup + + The following operations belong here: + + - All hooks but pre-* and post-* + - Modify configuration variables + - Changes to repo.__class__, repo.dirstate.__class__ + """ + for c in self._repocallables: + c(ui, repo) + + def uisetup(self, call): + """Decorated function will be executed during uisetup + + example:: + + @eh.uisetup + def setupbabar(ui): + print 'this is uisetup!' + """ + self._uicallables.append(call) + return call + + def extsetup(self, call): + """Decorated function will be executed during extsetup + + example:: + + @eh.extsetup + def setupcelestine(ui): + print 'this is extsetup!' + """ + self._extcallables.append(call) + return call + + def reposetup(self, call): + """Decorated function will be executed during reposetup + + example:: + + @eh.reposetup + def setupzephir(ui, repo): + print 'this is reposetup!' + """ + self._repocallables.append(call) + return call + + def revset(self, symbolname): + """Decorated function is a revset symbol + + The name of the symbol must be given as the decorator argument. + The symbol is added during `extsetup`. + + example:: + + @eh.revset('hidden') + def revsetbabar(repo, subset, x): + args = revset.getargs(x, 0, 0, 'babar accept no argument') + return [r for r in subset if 'babar' in repo[r].description()] + """ + def dec(symbol): + self._revsetsymbols.append((symbolname, symbol)) + return symbol + return dec + + + def templatekw(self, keywordname): + """Decorated function is a revset keyword + + The name of the keyword must be given as the decorator argument. + The symbol is added during `extsetup`. + + example:: + + @eh.templatekw('babar') + def kwbabar(ctx): + return 'babar' + """ + def dec(keyword): + self._templatekws.append((keywordname, keyword)) + return keyword + return dec + + def wrapcommand(self, command, extension=None): + """Decorated function is a command wrapper + + The name of the command must be given as the decorator argument. + The wrapping is installed during `uisetup`. + + If the second option `extension` argument is provided, the wrapping + will be applied in the extension commandtable. This argument must be a + string that will be searched using `extension.find` if not found and + Abort error is raised. If the wrapping applies to an extension, it is + installed during `extsetup` + + example:: + + @eh.wrapcommand('summary') + def wrapsummary(orig, ui, repo, *args, **kwargs): + ui.note('Barry!') + return orig(ui, repo, *args, **kwargs) + + """ + def dec(wrapper): + if extension is None: + self._commandwrappers.append((command, wrapper)) + else: + self._extcommandwrappers.append((extension, command, wrapper)) + return wrapper + return dec + + def wrapfunction(self, container, funcname): + """Decorated function is a function wrapper + + This function takes two arguments, the container and the name of the + function to wrap. The wrapping is performed during `uisetup`. + (there is no extension support) + + example:: + + @eh.function(discovery, 'checkheads') + def wrapfunction(orig, *args, **kwargs): + ui.note('His head smashed in and his heart cut out') + return orig(*args, **kwargs) + """ + def dec(wrapper): + self._functionwrappers.append((container, funcname, wrapper)) + return wrapper + return dec + + def addattr(self, container, funcname): + """Decorated function is to be added to the container + + This function takes two arguments, the container and the name of the + function to wrap. The wrapping is performed during `uisetup`. + + example:: + + @eh.function(context.changectx, 'babar') + def babar(ctx): + return 'babar' in ctx.description + """ + def dec(func): + self._duckpunchers.append((container, funcname, func)) + return func + return dec + +eh = exthelper() +uisetup = eh.final_uisetup +extsetup = eh.final_extsetup +reposetup = eh.final_reposetup + +##################################################################### +### Critical fix ### +##################################################################### + +@eh.wrapfunction(mercurial.obsolete, '_readmarkers') +def safereadmarkers(orig, data): + """safe maker wrapper to remove nullid succesors + + Nullid successors was created by older version of evolve. + """ + nb = 0 + for marker in orig(data): + if nullid in marker[1]: + marker = (marker[0], + tuple(s for s in marker[1] if s != nullid), + marker[2], + marker[3]) + nb += 1 + yield marker + if nb: + e = sys.stderr + print >> e, 'repo contains %i invalid obsolescence markers' % nb + +getrevs = obsolete.getrevs + +##################################################################### +### Complete troubles computation logic ### +##################################################################### + + +### Cache computation +latediff = 1 # flag to prevent taking late comer fix into account + +### changectx method + +@eh.addattr(context.changectx, 'latecomer') +def latecomer(ctx): + """is the changeset bumped (Try to succeed to public change)""" + return ctx.bumped() + +@eh.addattr(context.changectx, 'conflicting') +def conflicting(ctx): + """is the changeset divergent (Try to succeed to public change)""" + return ctx.divergent() + +### revset symbol + +eh.revset('latecomer')(revset.symbols['bumped']) +eh.revset('conflicting')(revset.symbols['divergent']) + + + + +##################################################################### +### Additional Utilities ### +##################################################################### + +# This section contains a lot of small utility function and method + +# - Function to create markers +# - useful alias pstatus and pdiff (should probably go in evolve) +# - "troubles" method on changectx +# - function to travel throught the obsolescence graph +# - function to find useful changeset to stabilize + +createmarkers = obsolete.createmarkers + + +### Useful alias + +@eh.uisetup +def _installalias(ui): + if ui.config('alias', 'pstatus', None) is None: + ui.setconfig('alias', 'pstatus', 'status --rev .^') + if ui.config('alias', 'pdiff', None) is None: + ui.setconfig('alias', 'pdiff', 'diff --rev .^') + if ui.config('alias', 'olog', None) is None: + ui.setconfig('alias', 'olog', "log -r 'precursors(.)' --hidden") + if ui.config('alias', 'odiff', None) is None: + ui.setconfig('alias', 'odiff', "diff --hidden --rev 'limit(precursors(.),1)' --rev .") + if ui.config('alias', 'grab', None) is None: + ui.setconfig('alias', 'grab', "! $HG rebase --dest . --rev $@ && $HG up tip") + + +### Troubled revset symbol + +@eh.revset('troubled') +def revsettroubled(repo, subset, x): + """``troubled()`` + Changesets with troubles. + """ + _ = revset.getargs(x, 0, 0, 'troubled takes no arguments') + return repo.revs('%ld and (unstable() + bumped() + divergent())', + subset) + + +### Obsolescence graph + +# XXX SOME MAJOR CLEAN UP TO DO HERE XXX + +def _precursors(repo, s): + """Precursor of a changeset""" + cs = set() + nm = repo.changelog.nodemap + markerbysubj = repo.obsstore.precursors + 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 _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.precursors + while toproceed: + nc = toproceed.pop() + for mark in allsubjects.get(nc, ()): + np = mark[0] + if np not in seen: + seen.add(np) + toproceed.append(np) + nm = repo.changelog.nodemap + cs = set() + for p in seen: + pr = nm.get(p) + if pr is not None: + cs.add(pr) + return cs + +def _successors(repo, s): + """Successors of a changeset""" + cs = set() + nm = repo.changelog.nodemap + markerbyobj = repo.obsstore.successors + 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 _allsuccessors(repo, s, haltonflags=0): # XXX we need a better naming + """transitive successors of a subset + + haltonflags allows to provide flags which prevent the evaluation of a + marker. """ + toproceed = [repo[r].node() for r in s] + seen = set() + allobjects = repo.obsstore.successors + while toproceed: + nc = toproceed.pop() + for mark in allobjects.get(nc, ()): + if mark[2] & haltonflags: + continue + for sub in mark[1]: + if sub == nullid: + continue # should not be here! + if sub not in seen: + seen.add(sub) + toproceed.append(sub) + nm = repo.changelog.nodemap + cs = set() + for s in seen: + sr = nm.get(s) + if sr is not None: + cs.add(sr) + return cs + + + + +##################################################################### +### Extending revset and template ### +##################################################################### + +# this section add several useful revset symbol not yet in core. +# they are subject to changes + + +### XXX I'm not sure this revset is useful +@eh.revset('suspended') +def revsetsuspended(repo, subset, x): + """``suspended()`` + Obsolete changesets with non-obsolete descendants. + """ + args = revset.getargs(x, 0, 0, 'suspended takes no arguments') + suspended = getrevs(repo, 'suspended') + return [r for r in subset if r in suspended] + + +@eh.revset('precursors') +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] + + +@eh.revset('allprecursors') +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] + + +@eh.revset('successors') +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] + +@eh.revset('allsuccessors') +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 +# XXX it does not handle troubles well :-/ + +@eh.templatekw('obsolete') +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 ctx.obsolete(): + if ctx.extinct(): + return 'extinct' + else: + return 'suspended' + elif ctx.unstable(): + return 'unstable' + return 'stable' + +##################################################################### +### Various trouble warning ### +##################################################################### + +# This section take care of issue warning to the user when troubles appear + +@eh.wrapcommand("update") +@eh.wrapcommand("parents") +@eh.wrapcommand("pull") +def wrapmayobsoletewc(origfn, ui, repo, *args, **opts): + """Warn that the working directory parent is an obsolete changeset""" + res = origfn(ui, repo, *args, **opts) + if repo['.'].obsolete(): + ui.warn(_('working directory parent is obsolete!\n')) + return res + +# XXX this could wrap transaction code +# XXX (but this is a bit a layer violation) +@eh.wrapcommand("commit") +@eh.wrapcommand("import") +@eh.wrapcommand("push") +@eh.wrapcommand("pull") +@eh.wrapcommand("graft") +@eh.wrapcommand("phase") +@eh.wrapcommand("unbundle") +def warnobserrors(orig, ui, repo, *args, **kwargs): + """display warning is the command resulted in more instable changeset""" + # part of the troubled stuff may be filtered (stash ?) + # This needs a better implementation but will probably wait for core. + filtered = repo.changelog.filteredrevs + priorunstables = len(set(getrevs(repo, 'unstable')) - filtered) + priorbumpeds = len(set(getrevs(repo, 'bumped')) - filtered) + priordivergents = len(set(getrevs(repo, 'divergent')) - filtered) + ret = orig(ui, repo, *args, **kwargs) + # workaround phase stupidity + #phases._filterunknown(ui, repo.changelog, repo._phasecache.phaseroots) + filtered = repo.changelog.filteredrevs + newunstables = len(set(getrevs(repo, 'unstable')) - filtered) - priorunstables + newbumpeds = len(set(getrevs(repo, 'bumped')) - filtered) - priorbumpeds + newdivergents = len(set(getrevs(repo, 'divergent')) - filtered) - priordivergents + if newunstables > 0: + ui.warn(_('%i new unstable changesets\n') % newunstables) + if newbumpeds > 0: + ui.warn(_('%i new bumped changesets\n') % newbumpeds) + if newdivergents > 0: + ui.warn(_('%i new divergent changesets\n') % newdivergents) + return ret + +@eh.reposetup +def _repostabilizesetup(ui, repo): + """Add a hint for "hg evolve" when troubles make push fails + """ + if not repo.local(): + return + + class evolvingrepo(repo.__class__): + def push(self, remote, *args, **opts): + """wrapper around pull that pull obsolete relation""" + try: + result = super(evolvingrepo, self).push(remote, *args, **opts) + except util.Abort, ex: + hint = _("use 'hg evolve' to get a stable history " + "or --force to ignore warnings") + if (len(ex.args) >= 1 + and ex.args[0].startswith('push includes ') + and ex.hint is None): + ex.hint = hint + raise + return result + repo.__class__ = evolvingrepo + +def summaryhook(ui, repo): + def write(fmt, count): + s = fmt % count + if count: + ui.write(s) + else: + ui.note(s) + + nbunstable = len(getrevs(repo, 'unstable')) + nbbumped = len(getrevs(repo, 'bumped')) + nbdivergent = len(getrevs(repo, 'divergent')) + write('unstable: %i changesets\n', nbunstable) + write('bumped: %i changesets\n', nbbumped) + write('divergent: %i changesets\n', nbdivergent) + +@eh.extsetup +def obssummarysetup(ui): + cmdutil.summaryhooks.add('evolve', summaryhook) + + +##################################################################### +### Core Other extension compat ### +##################################################################### + + +@eh.extsetup +def _rebasewrapping(ui): + # warning about more obsolete + try: + rebase = extensions.find('rebase') + if rebase: + extensions.wrapcommand(rebase.cmdtable, 'rebase', warnobserrors) + except KeyError: + pass # rebase not found + try: + histedit = extensions.find('histedit') + if histedit: + extensions.wrapcommand(histedit.cmdtable, 'histedit', warnobserrors) + except KeyError: + pass # rebase not found + + +##################################################################### +### Old Evolve extension content ### +##################################################################### + +# XXX need clean up and proper sorting in other section + +### util function +############################# + +### changeset rewriting logic +############################# + +def rewrite(repo, old, updates, head, newbases, commitopts): + """Return (nodeid, created) where nodeid is the identifier of the + changeset generated by the rewrite process, and created is True if + nodeid was actually created. If created is False, nodeid + references a changeset existing before the rewrite call. + """ + 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 + updatebookmarks(newid) + finally: + wlock.release() + + return newid, created + +class MergeFailure(util.Abort): + pass + +def relocate(repo, orig, dest): + """rewrite on dest""" + try: + rebase = extensions.find('rebase') + # dummy state to trick rebase node + if not orig.p2().rev() == node.nullrev: + raise util.Abort( + 'no support for evolution merge changesets yet', + hint="Redo the merge a use `hg prune` to obsolete the old one") + destbookmarks = repo.nodebookmarks(dest.node()) + cmdutil.duplicatecopies(repo, orig.node(), dest.node()) + nodesrc = orig.node() + destphase = repo[nodesrc].phase() + try: + r = rebase.rebasenode(repo, orig.node(), dest.node(), + {node.nullrev: node.nullrev}, False) + if r[-1]: #some conflict + raise util.Abort( + 'unresolved merge conflicts (see hg help resolve)') + nodenew = rebase.concludenode(repo, orig.node(), dest.node(), + node.nullid) + except util.Abort, exc: + class LocalMergeFailure(MergeFailure, exc.__class__): + pass + exc.__class__ = LocalMergeFailure + raise + oldbookmarks = repo.nodebookmarks(nodesrc) + if nodenew is not None: + phases.retractboundary(repo, destphase, [nodenew]) + createmarkers(repo, [(repo[nodesrc], (repo[nodenew],))]) + for book in oldbookmarks: + repo._bookmarks[book] = nodenew + else: + createmarkers(repo, [(repo[nodesrc], ())]) + # Behave like rebase, move bookmarks to dest + for book in oldbookmarks: + repo._bookmarks[book] = dest.node() + for book in destbookmarks: # restore bookmark that rebase move + repo._bookmarks[book] = dest.node() + if oldbookmarks or destbookmarks: + repo._bookmarks.write() + return nodenew + except util.Abort: + # Invalidate the previous setparents + repo.dirstate.invalidate() + raise + +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: + repo._bookmarks.write() + return updatebookmarks + +### new command +############################# +cmdtable = {} +command = cmdutil.command(cmdtable) +metadataopts = [ + ('d', 'date', '', + _('record the specified date in metadata'), _('DATE')), + ('u', 'user', '', + _('record the specified user in metadata'), _('USER')), +] + + +@command('^evolve|stabilize|solve', + [('n', 'dry-run', False, 'do not perform actions, print what to be done'), + ('A', 'any', False, 'evolve any troubled changeset'), + ('a', 'all', False, 'evolve all troubled changesets'), + ('c', 'continue', False, 'continue an interrupted evolution'), ], + _('[OPTIONS]...')) +def evolve(ui, repo, **opts): + """Solve trouble in your repository + + - rebase unstable changeset to make it stable again, + - create proper diff from bumped changeset, + - merge divergent changesets. + - update to a successor if the working directory parent is + obsolete + + By default, take the first trouble changeset that looks relevant. + + (The logic is still a bit fuzzy) + + - For unstable, this means taking the first which could be rebased as a + child of the working directory parent revision or one of its descendants + and rebasing it. + + - For divergent, this means taking "." if applicable. + + With --any, evolve picks any troubled changeset to solve. + + The working directory is updated to the newly created revision. + """ + + contopt = opts['continue'] + anyopt = opts['any'] + allopt = opts['all'] + dryrunopt = opts['dry_run'] + + if contopt: + if anyopt: + raise util.Abort('can not specify both "--any" and "--continue"') + if allopt: + raise util.Abort('can not specify both "--all" and "--continue"') + graftcmd = commands.table['graft'][0] + return graftcmd(ui, repo, old_obsolete=True, **{'continue': True}) + + tr = _picknexttroubled(ui, repo, anyopt or allopt) + if tr is None: + if repo['.'].obsolete(): + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + successors = set() + + for successorsset in obsolete.successorssets(repo, repo['.'].node()): + for nodeid in successorsset: + successors.add(repo[nodeid]) + + if not successors: + ui.warn(_('parent is obsolete without successors; ' + + 'likely killed\n')) + return 2 + + elif len(successors) > 1: + ui.warn(_('parent is obsolete with multiple successors:\n')) + + for ctx in sorted(successors, key=lambda ctx: ctx.rev()): + displayer.show(ctx) + + return 2 + + else: + ctx = successors.pop() + + ui.status(_('update:')) + if not ui.quiet: + displayer.show(ctx) + + if dryrunopt: + print 'hg update %s' % ctx.rev() + return 0 + else: + return hg.update(repo, ctx.rev()) + + troubled = repo.revs('troubled()') + if troubled: + ui.write_err(_('nothing to evolve here\n')) + ui.status(_('(%i troubled changesets, do you want --any ?)\n') + % len(troubled)) + return 2 + else: + ui.write_err(_('no troubled changesets\n')) + return 1 + + def progresscb(): + if allopt: + ui.progress('evolve', seen, unit='changesets', total=count) + seen = 1 + count = allopt and _counttroubled(ui, repo) or 1 + + while tr is not None: + progresscb() + result = _evolveany(ui, repo, tr, dryrunopt, progresscb=progresscb) + progresscb() + seen += 1 + if not allopt: + return result + progresscb() + tr = _picknexttroubled(ui, repo, anyopt or allopt) + + if allopt: + ui.progress('evolve', None) + + +def _evolveany(ui, repo, tr, dryrunopt, progresscb): + repo = repo.unfiltered() + tr = repo[tr.rev()] + cmdutil.bailifchanged(repo) + troubles = tr.troubles() + if 'unstable' in troubles: + return _solveunstable(ui, repo, tr, dryrunopt, progresscb) + elif 'bumped' in troubles: + return _solvebumped(ui, repo, tr, dryrunopt, progresscb) + elif 'divergent' in troubles: + repo = repo.unfiltered() + tr = repo[tr.rev()] + return _solvedivergent(ui, repo, tr, dryrunopt, progresscb) + else: + assert False # WHAT? unknown troubles + +def _counttroubled(ui, repo): + """Count the amount of troubled changesets""" + troubled = set() + troubled.update(getrevs(repo, 'unstable')) + troubled.update(getrevs(repo, 'bumped')) + troubled.update(getrevs(repo, 'divergent')) + return len(troubled) + +def _picknexttroubled(ui, repo, pickany=False, progresscb=None): + """Pick a the next trouble changeset to solve""" + if progresscb: progresscb() + tr = _stabilizableunstable(repo, repo['.']) + if tr is None: + wdp = repo['.'] + if 'divergent' in wdp.troubles(): + tr = wdp + if tr is None and pickany: + troubled = list(repo.set('unstable()')) + if not troubled: + troubled = list(repo.set('bumped()')) + if not troubled: + troubled = list(repo.set('divergent()')) + if troubled: + tr = troubled[0] + + return tr + +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 prec in repo.set('allprecursors(%d)', pctx): + yield prec + for ctx in pctx.descendants(): + yield ctx + for prec in repo.set('allprecursors(%d)', ctx): + yield prec + + # 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): + for child in ctx.children(): + if child.unstable(): + return child + return None + +def _solveunstable(ui, repo, orig, dryrun=False, progresscb=None): + """Stabilize a unstable changeset""" + obs = orig.parents()[0] + if not obs.obsolete(): + print obs.rev(), orig.parents() + print orig.rev() + obs = orig.parents()[1] + assert obs.obsolete() + newer = obsolete.successorssets(repo, obs.node()) + # search of a parent which is not killed + while not newer or newer == [()]: + ui.debug("stabilize target %s is plain dead," + " trying to stabilize on its parent") + obs = obs.parents()[0] + newer = obsolete.successorssets(repo, obs.node()) + if len(newer) > 1: + raise util.Abort(_("conflict rewriting. can't choose destination\n")) + targets = newer[0] + assert targets + if len(targets) > 1: + raise util.Abort(_("does not handle split parents 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(orig) + repo.ui.status(_('atop:')) + if not ui.quiet: + displayer.show(target) + if progresscb: progresscb() + todo = 'hg rebase -r %s -d %s\n' % (orig, target) + if dryrun: + repo.ui.write(todo) + else: + repo.ui.note(todo) + if progresscb: progresscb() + lock = repo.lock() + try: + relocate(repo, orig, target) + except MergeFailure: + repo.opener.write('graftstate', orig.hex() + '\n') + repo.ui.write_err(_('evolve failed!\n')) + repo.ui.write_err(_('fix conflict and run "hg evolve --continue"\n')) + raise + finally: + lock.release() + +def _solvebumped(ui, repo, bumped, dryrun=False, progresscb=None): + """Stabilize a bumped changeset""" + # For now we deny bumped merge + if len(bumped.parents()) > 1: + raise util.Abort('late comer stabilization is confused by bumped' + ' %s being a merge' % bumped) + prec = repo.set('last(allprecursors(%d) and public())', bumped).next() + # For now we deny target merge + if len(prec.parents()) > 1: + raise util.Abort('late comer evolution is confused by precursors' + ' %s being a merge' % prec) + + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + repo.ui.status(_('recreate:')) + if not ui.quiet: + displayer.show(bumped) + repo.ui.status(_('atop:')) + if not ui.quiet: + displayer.show(prec) + if dryrun: + todo = 'hg rebase --rev %s --dest %s;\n' % (bumped, prec.p1()) + repo.ui.write(todo) + repo.ui.write('hg update %s;\n' % prec) + repo.ui.write('hg revert --all --rev %s;\n' % bumped) + repo.ui.write('hg commit --msg "bumped update to %s"') + return 0 + if progresscb: progresscb() + wlock = repo.wlock() + try: + newid = tmpctx = None + tmpctx = bumped + lock = repo.lock() + try: + bmupdate = _bookmarksupdater(repo, bumped.node()) + # Basic check for common parent. Far too complicated and fragile + tr = repo.transaction('bumped-stabilize') + try: + if not list(repo.set('parents(%d) and parents(%d)', bumped, prec)): + # Need to rebase the changeset at the right place + repo.ui.status(_('rebasing to destination parent: %s\n') % prec.p1()) + try: + tmpid = relocate(repo, bumped, prec.p1()) + if tmpid is not None: + tmpctx = repo[tmpid] + createmarkers(repo, [(bumped, (tmpctx,))]) + except MergeFailure: + repo.opener.write('graftstate', bumped.hex() + '\n') + repo.ui.write_err(_('evolution failed!\n')) + repo.ui.write_err(_('fix conflict and run "hg evolve --continue"\n')) + raise + # Create the new commit context + repo.ui.status(_('computing new diff\n')) + files = set() + copied = copies.pathcopies(prec, bumped) + precmanifest = prec.manifest() + for key, val in bumped.manifest().iteritems(): + if precmanifest.pop(key, None) != val: + files.add(key) + files.update(precmanifest) # add missing files + # commit it + if files: # something to commit! + def filectxfn(repo, ctx, path): + if path in bumped: + fctx = bumped[path] + flags = fctx.flags() + mctx = context.memfilectx(fctx.path(), fctx.data(), + islink='l' in flags, + isexec='x' in flags, + copied=copied.get(path)) + return mctx + raise IOError() + text = 'bumped update to %s:\n\n' % prec + text += bumped.description() + + new = context.memctx(repo, + parents=[prec.node(), node.nullid], + text=text, + files=files, + filectxfn=filectxfn, + user=bumped.user(), + date=bumped.date(), + extra=bumped.extra()) + + newid = repo.commitctx(new) + if newid is None: + createmarkers(repo, [(tmpctx, ())]) + newid = prec.node() + else: + phases.retractboundary(repo, bumped.phase(), [newid]) + createmarkers(repo, [(tmpctx, (repo[newid],))], + flag=latediff) + bmupdate(newid) + tr.close() + repo.ui.status(_('commited as %s\n') % node.short(newid)) + finally: + tr.release() + finally: + lock.release() + # reroute the working copy parent to the new changeset + repo.dirstate.setparents(newid, node.nullid) + finally: + wlock.release() + +def _solvedivergent(ui, repo, divergent, dryrun=False, progresscb=None): + base, others = divergentdata(divergent) + if len(others) > 1: + othersstr = "[%s]" % (','.join([str(i) for i in others])) + hint = ("changeset %d is divergent with a changeset that got splitted " + "| into multiple ones:\n[%s]\n" + "| This is not handled by automatic evolution yet\n" + "| You have to fallback to manual handling with commands as:\n" + "| - hg touch -D\n" + "| - hg prune\n" + "| \n" + "| You should contact your local evolution Guru for help.\n" + % (divergent, othersstr)) + raise util.Abort("We do not handle divergence with split yet", + hint='') + other = others[0] + if divergent.phase() <= phases.public: + raise util.Abort("We can't resolve this conflict from the public side", + hint="%s is public, try from %s" % (divergent, other)) + if len(other.parents()) > 1: + raise util.Abort("divergent changeset can't be a merge (yet)", + hint="You have to fallback to solving this by hand...\n" + "| This probably mean to redo the merge and use " + "| `hg prune` to kill older version.") + if other.p1() not in divergent.parents(): + raise util.Abort("parents are not common (not handled yet)", + hint="| %(d)s, %(o)s are not based on the same changeset." + "| With the current state of its implementation, " + "| evolve does not work in that case.\n" + "| rebase one of them next to the other and run " + "| this command again.\n" + "| - either: hg rebase -dest 'p1(%(d)s)' -r %(o)s" + "| - or: hg rebase -dest 'p1(%(d)s)' -r %(o)s" + % {'d': divergent, 'o': other}) + + displayer = cmdutil.show_changeset(ui, repo, {'template': shorttemplate}) + ui.status(_('merge:')) + if not ui.quiet: + displayer.show(divergent) + ui.status(_('with: ')) + if not ui.quiet: + displayer.show(other) + ui.status(_('base: ')) + if not ui.quiet: + displayer.show(base) + if dryrun: + ui.write('hg update -c %s &&\n' % divergent) + ui.write('hg merge %s &&\n' % other) + ui.write('hg commit -m "auto merge resolving conflict between ' + '%s and %s"&&\n' % (divergent, other)) + ui.write('hg up -C %s &&\n' % base) + ui.write('hg revert --all --rev tip &&\n') + ui.write('hg commit -m "`hg log -r %s --template={desc}`";\n' + % divergent) + return + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + if divergent not in repo[None].parents(): + repo.ui.status(_('updating to "local" conflict\n')) + hg.update(repo, divergent.rev()) + repo.ui.note(_('merging divergent changeset\n')) + if progresscb: progresscb() + stats = merge.update(repo, + other.node(), + branchmerge=True, + force=False, + partial=None, + ancestor=base.node(), + mergeancestor=True) + hg._showstats(repo, stats) + if stats[3]: + repo.ui.status(_("use 'hg resolve' to retry unresolved file merges " + "or 'hg update -C .' to abandon\n")) + if stats[3] > 0: + raise util.Abort('Merge conflict between several amendments, and this is not yet automated', + hint="""/!\ You can try: +/!\ * manual merge + resolve => new cset X +/!\ * hg up to the parent of the amended changeset (which are named W and Z) +/!\ * hg revert --all -r X +/!\ * hg ci -m "same message as the amended changeset" => new cset Y +/!\ * hg kill -n Y W Z +""") + if progresscb: progresscb() + tr = repo.transaction('stabilize-divergent') + try: + repo.dirstate.setparents(divergent.node(), node.nullid) + oldlen = len(repo) + amend(ui, repo, message='', logfile='') + if oldlen == len(repo): + new = divergent + # no changes + else: + new = repo['.'] + createmarkers(repo, [(other, (new,))]) + phases.retractboundary(repo, other.phase(), [new.node()]) + tr.close() + finally: + tr.release() + finally: + lockmod.release(lock, wlock) + + +def divergentdata(ctx): + """return base, other part of a conflict + + This only return the first one. + + XXX this woobly function won't survive XXX + """ + for base in ctx._repo.set('reverse(precursors(%d))', ctx): + newer = obsolete.successorssets(ctx._repo, base.node()) + # drop filter and solution including the original ctx + newer = [n for n in newer if n and ctx.node() not in n] + if newer: + return base, tuple(ctx._repo[o] for o in newer[0]) + raise util.Abort('base of divergent changeset not found', + hint='this case is not yet handled') + + + +shorttemplate = '[{rev}] {desc|firstline}\n' + +@command('^gdown|previous', + [], + '') +def cmdprevious(ui, repo): + """update to parent and 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|next', + [], + '') +def cmdnext(ui, repo): + """update to child and 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 + +def _reachablefrombookmark(repo, revs, mark): + """filter revisions and bookmarks reachable from the given bookmark + yoinked from mq.py + """ + marks = repo._bookmarks + if mark not in marks: + raise util.Abort(_("bookmark '%s' not found") % mark) + + # If the requested bookmark is not the only one pointing to a + # a revision we have to only delete the bookmark and not strip + # anything. revsets cannot detect that case. + uniquebm = True + for m, n in marks.iteritems(): + if m != mark and n == repo[mark].node(): + uniquebm = False + break + if uniquebm: + rsrevs = repo.revs("ancestors(bookmark(%s)) - " + "ancestors(head() and not bookmark(%s)) - " + "ancestors(bookmark() and not bookmark(%s)) - " + "obsolete()", + mark, mark, mark) + revs.update(set(rsrevs)) + return marks,revs + +def _deletebookmark(ui, marks, mark): + del marks[mark] + marks.write() + ui.write(_("bookmark '%s' deleted\n") % mark) + + + +def _getmetadata(**opts): + metadata = {} + date = opts.get('date') + user = opts.get('user') + if date and '0 0' != date: + metadata['date'] = '%i %i' % util.parsedate(date) + if user: + metadata['user'] = user + return metadata + + +@command('^prune|obsolete|kill', + [('n', 'new', [], _("successor changeset (DEPRECATED)")), + ('s', 'succ', [], _("successor changeset")), + ('r', 'rev', [], _("revisions to prune")), + ('', 'biject', False, _("do a 1-1 map between rev and successor ranges")), + ('B', 'bookmark', '', _("remove revs only reachable from given" + " bookmark"))] + metadataopts, + _('[OPTION] [-r] REV...')) + # -U --noupdate option to prevent wc update and or bookmarks update ? +def cmdprune(ui, repo, *revs, **opts): + """get rid of changesets by marking them obsolete + + Obsolete changesets becomes invisible to all commands. + + Non-pruned descendant of pruned changesets becomes "unstable". Use the + :hg:`evolve` to handle such situation. + + When the working directory parent is pruned the repository is updated to a + non obsolete parents. + + You can use the ``--succ`` option to informs mercurial that a newer version + of the pruned changeset exists. + + You can use the ``--biject`` option to specify a 1-1 (bijection) between + revisions to prune and successor changesets. This option may be removed in + a future release (with the functionality absored automatically). + + """ + revs = set(scmutil.revrange(repo, list(revs) + opts.get('rev'))) + succs = opts['new'] + opts['succ'] + bookmark = opts.get('bookmark') + metadata = _getmetadata(**opts) + biject = opts.get('biject') + + if bookmark: + marks,revs = _reachablefrombookmark(repo, revs, bookmark) + if not revs: + # no revisions to prune - delete bookmark immediately + _deletebookmark(ui, marks, bookmark) + + if not revs: + raise util.Abort(_('nothing to prune')) + + wlock = lock = None + wlock = repo.wlock() + sortedrevs = lambda specs: sorted(set(scmutil.revrange(repo, specs))) + try: + lock = repo.lock() + # defines pruned changesets + precs = [] + for p in sortedrevs(revs): + cp = repo[p] + if not cp.mutable(): + # note: create marker would had raise something anyway + raise util.Abort('cannot prune immutable changeset: %s' % cp, + hint='see "hg help phases" for details') + precs.append(cp) + if not precs: + raise util.Abort('nothing to prune') + + # defines successors changesets + sucs = tuple(repo[n] for n in sortedrevs(succs)) + if not biject and len(sucs) > 1 and len(precs) > 1: + msg = "Can't use multiple successors for multiple precursors" + raise util.Abort(msg) + + if biject and len(sucs) != len(precs): + msg = "Can't use %d successors for %d precursors" % (len(sucs), len(precs)) + raise util.Abort(msg) + + relations = [(p, sucs) for p in precs] + if biject: + relations = [(p, (s,)) for p, s in zip(precs, sucs)] + + # create markers + createmarkers(repo, relations, metadata=metadata) + + # informs that changeset have been pruned + ui.status(_('%i changesets pruned\n') % len(precs)) + # 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) + # update bookmarks + if bookmark: + _deletebookmark(ui, marks, bookmark) + for ctx in repo.unfiltered().set('bookmark() and %ld', precs): + ldest = list(repo.set('max((::%d) - obsolete())', ctx)) + if ldest: + dest = ldest[0] + updatebookmarks = _bookmarksupdater(repo, ctx.node()) + updatebookmarks(dest.node()) + finally: + lockmod.release(lock, wlock) + +@command('amend|refresh', + [('A', 'addremove', None, + _('mark new/missing files as added/removed before committing')), + ('e', 'edit', False, _('invoke editor on commit messages')), + ('', 'close-branch', None, + _('mark a branch as closed, hiding it from the branch list')), + ('s', 'secret', None, _('use the secret phase for committing')), + ] + walkopts + commitopts + commitopts2, + _('[OPTION]... [FILE]...')) +def amend(ui, repo, *pats, **opts): + """combine a changeset with updates and replace it with a new one + + Commits a new changeset incorporating both the changes to the given files + and all the changes from the current parent changeset into the repository. + + See :hg:`commit` for details about committing changes. + + If you don't specify -m, the parent's message will be reused. + + Behind the scenes, Mercurial first commits the update as a regular child + of the current parent. Then it creates a new commit on the parent's parents + with the updated contents. Then it changes the working copy parent to this + new combined changeset. Finally, the old changeset and its update are hidden + from :hg:`log` (unless you use --hidden with log). + + Returns 0 on success, 1 if nothing changed. + """ + opts = opts.copy() + edit = opts.pop('edit', False) + opts['amend'] = True + if not (edit or opts['message']): + opts['message'] = repo['.'].description() + _alias, commitcmd = cmdutil.findcmd('commit', commands.table) + return commitcmd[0](ui, repo, *pats, **opts) + +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 + createmarkers(repo, [(old, (repo[newid],))]) + 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() + +@eh.wrapcommand('commit') +def commitwrapper(orig, ui, repo, *arg, **kwargs): + if kwargs.get('amend', False): + lock = None + else: + 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 = [] + markers = [] + for old in obsoleted: + oldbookmarks.extend(repo.nodebookmarks(old.node())) + markers.append((old, (new,))) + if markers: + createmarkers(repo, markers) + for book in oldbookmarks: + repo._bookmarks[book] = new.node() + if oldbookmarks: + repo._bookmarks.write() + return result + finally: + if lock is not None: + lock.release() + +@command('^touch', + [('r', 'rev', [], 'revision to update'), + ('D', 'duplicate', False, + 'do not mark the new revision as successor of the old one')], + # allow to choose the seed ? + _('[-r] revs')) +def touch(ui, repo, *revs, **opts): + """Create successors with exact same property but hash + + This is used to "resurrect" changesets + """ + duplicate = opts['duplicate'] + revs = list(revs) + revs.extend(opts['rev']) + if not revs: + revs = ['.'] + revs = scmutil.revrange(repo, revs) + if not revs: + ui.write_err('no revision to touch\n') + return 1 + if not duplicate and repo.revs('public() and %ld', revs): + raise util.Abort("can't touch public revision") + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction('touch') + revs.sort() # ensure parent are run first + newmapping = {} + try: + for r in revs: + ctx = repo[r] + extra = ctx.extra().copy() + extra['__touch-noise__'] = random.randint(0, 0xffffffff) + # search for touched parent + p1 = ctx.p1().node() + p2 = ctx.p2().node() + p1 = newmapping.get(p1, p1) + p2 = newmapping.get(p2, p2) + new, _ = rewrite(repo, ctx, [], ctx, + [p1, p2], + commitopts={'extra': extra}) + # store touched version to help potential children + newmapping[ctx.node()] = new + if not duplicate: + createmarkers(repo, [(ctx, (repo[new],))]) + phases.retractboundary(repo, ctx.phase(), [new]) + if ctx in repo[None].parents(): + repo.dirstate.setparents(new, node.nullid) + tr.close() + finally: + tr.release() + finally: + lockmod.release(lock, wlock) + +@command('^fold', + [('r', 'rev', [], _("explicitly specify the full set of revision to fold")), + ], + # allow to choose the seed ? + _('rev')) +def fold(ui, repo, *revs, **opts): + """Fold multiple revisions into a single one + + Revision from your current working directory to the specified one are fold + as a new one replacing the other + + you can alternatively use --rev to explicitly specify revision to be fold + ignoring the current working directory parent. + """ + revs = list(revs) + if revs: + if opts.get('rev', ()): + raise util.Abort("cannot specify both --rev and a target revision") + targets = scmutil.revrange(repo, revs) + revs = repo.revs('(%ld::.) or (.::%ld)', targets, targets) + elif 'rev' in opts: + revs = scmutil.revrange(repo, opts['rev']) + else: + revs = () + if not revs: + ui.write_err('no revision to fold\n') + return 1 + roots = repo.revs('roots(%ld)', revs) + if len(roots) > 1: + raise util.Abort("set has multiple roots") + root = repo[roots[0]] + if root.phase() <= phases.public: + raise util.Abort("can't fold public revisions") + heads = repo.revs('heads(%ld)', revs) + if len(heads) > 1: + raise util.Abort("set has multiple heads") + head = repo[heads[0]] + wlock = lock = None + try: + wlock = repo.wlock() + lock = repo.lock() + tr = repo.transaction('touch') + try: + allctx = [repo[r] for r in revs] + targetphase = max(c.phase() for c in allctx) + msgs = ["HG: This is a fold of %d changesets." % len(allctx)] + msgs += ["HG: Commit message of changeset %s.\n\n%s\n" % + (c.rev(), c.description()) for c in allctx] + commitopts = {'message': "\n".join(msgs)} + commitopts['edit'] = True + newid, _ = rewrite(repo, root, allctx, head, + [root.p1().node(), root.p2().node()], + commitopts=commitopts) + phases.retractboundary(repo, targetphase, [newid]) + createmarkers(repo, [(ctx, (repo[newid],)) + for ctx in allctx]) + tr.close() + finally: + tr.release() + ui.status('%i changesets folded\n' % len(revs)) + if repo['.'].rev() in revs: + hg.update(repo, newid) + finally: + lockmod.release(lock, wlock) + + + +@eh.wrapcommand('graft') +def graftwrapper(orig, ui, repo, *revs, **kwargs): + kwargs = dict(kwargs) + revs = list(revs) + kwargs.get('rev', []) + kwargs['rev'] = [] + obsoleted = kwargs.setdefault('obsolete', []) + + lock = repo.lock() + try: + if kwargs.get('old_obsolete'): + if kwargs.get('continue'): + obsoleted.extend(repo.opener.read('graftstate').splitlines()) + else: + obsoleted.extend(revs) + # convert obsolete target into revs to avoid alias joke + obsoleted[:] = [str(i) for i in repo.revs('%lr', obsoleted)] + if obsoleted and len(revs) > 1: + + raise error.Abort(_('cannot graft multiple revisions while ' + 'obsoleting (for now).')) + + return commitwrapper(orig, ui, repo,*revs, **kwargs) + finally: + lock.release() + +@eh.extsetup +def oldevolveextsetup(ui): + try: + rebase = extensions.find('rebase') + except KeyError: + raise error.Abort(_('evolution extension requires rebase extension.')) + + for cmd in ['kill', 'uncommit', 'touch', 'fold']: + entry = extensions.wrapcommand(cmdtable, cmd, + warnobserrors) + + entry = cmdutil.findcmd('commit', commands.table)[1] + entry[1].append(('o', 'obsolete', [], + _("make commit obsolete this revision"))) + entry = cmdutil.findcmd('graft', commands.table)[1] + entry[1].append(('o', 'obsolete', [], + _("make graft obsoletes this revision"))) + entry[1].append(('O', 'old-obsolete', False, + _("make graft obsoletes its source"))) + diff -r ad2060da7ffa -r f49d4774b999 hgext/obsolete.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/obsolete.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,157 @@ +# Copyright 2011 Pierre-Yves David +# Logilab SA +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. +"""Deprecated extension that formely introduces "Changeset Obsolescence". + +This concept is now partially in Mercurial core (starting with mercurial 2.3). The remaining logic have been grouped with the evolve extension. + +Some code cemains in this extensions to detect and convert prehistoric format of obsolete marker than early user may have create. Keep it enabled if you were such user. +""" + +from mercurial import util + +try: + from mercurial import obsolete + if not obsolete._enabled: + obsolete._enabled = True +except ImportError: + raise util.Abort('Obsolete extension requires Mercurial 2.3 (or later)') + +import sys +import json + +from mercurial import cmdutil +from mercurial import error +from mercurial.node import bin, nullid + + +##################################################################### +### Older format management ### +##################################################################### + +# Code related to detection and management of older legacy format never +# handled by core + + +def reposetup(ui, repo): + """Detect that a repo still contains some old obsolete format + """ + if not repo.local(): + return + 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.') + +def _obsdeserialise(flike): + """read a file like object serialised with _obsserialise + + this desierialize into a {subject -> objects} mapping + + this was the very first format ever.""" + rels = {} + for line in flike: + subhex, objhex = line.split() + subnode = bin(subhex) + if subnode == nullid: + subnode = None + rels.setdefault( subnode, set()).add(bin(objhex)) + return rels + +cmdtable = {} +command = cmdutil.command(cmdtable) +@command('debugconvertobsolete', [], '') +def cmddebugconvertobsolete(ui, repo): + """import markers from an .hg/obsolete-relations file""" + cnt = 0 + err = 0 + l = repo.lock() + some = False + try: + unlink = [] + tr = repo.transaction('convert-obsolete') + try: + repo._importoldobsolete = True + store = repo.obsstore + ### very first format + try: + f = repo.opener('obsolete-relations') + try: + some = True + for line in f: + subhex, objhex = line.split() + suc = bin(subhex) + prec = bin(objhex) + sucs = (suc==nullid) and [] or [suc] + meta = { + 'date': '%i %i' % util.makedate(), + 'user': ui.username(), + } + try: + store.create(tr, prec, sucs, 0, 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) diff -r ad2060da7ffa -r f49d4774b999 hgext/pushexperiment.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/pushexperiment.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,112 @@ +"""Small extension altering some push behavior + +- Add a new wire protocol command to exchange obsolescence markers. Sending the + raw file as a binary instead of using pushkey hack. +- Add a "push done" notification +- Push obsolescence marker before anything else (This works around the lack of global transaction) + +""" + +import errno +from StringIO import StringIO + +from mercurial.i18n import _ +from mercurial import extensions +from mercurial import wireproto +from mercurial import obsolete +from mercurial import localrepo + + +def client_pushobsmarkers(self, obsfile): + """wireprotocol peer method""" + self.requirecap('_push_experiment_pushobsmarkers_0', + _('push obsolete markers faster')) + ret, output = self._callpush('push_experiment_pushobsmarkers_0', obsfile) + for l in output.splitlines(True): + self.ui.status(_('remote: '), l) + return ret + + +def srv_pushobsmarkers(repo, proto): + """wireprotocol command""" + fp = StringIO() + proto.redirect() + proto.getfile(fp) + data = fp.getvalue() + fp.close() + lock = repo.lock() + try: + tr = repo.transaction('pushkey: obsolete markers') + try: + repo.obsstore.mergemarkers(tr, data) + tr.close() + finally: + tr.release() + finally: + lock.release() + return wireproto.pushres(0) + + +def syncpush(orig, repo, remote): + """wraper for obsolete.syncpush to use the fast way if possible""" + if not (obsolete._enabled and repo.obsstore): + return + if remote.capable('_push_experiment_pushobsmarkers_0'): + return # already pushed before changeset + remote.push_experiment_pushobsmarkers_0(obsfp) + return + return orig(repo, remote) + + +def client_notifypushend(self): + """wire peer command to notify a push is done""" + self.requirecap('_push_experiment_notifypushend_0', _('hook once push is all done')) + return self._call('push_experiment_notifypushend_0') + + +def srv_notifypushend(repo, proto): + """wire protocol command to notify a push is done""" + proto.redirect() + repo.hook('notifypushend') + return wireproto.pushres(0) + + +def augmented_push(orig, repo, remote, *args, **kwargs): + """push wrapped that call the wire protocol command""" + if not remote.canpush(): + raise util.Abort(_("destination does not support push")) + if (obsolete._enabled and repo.obsstore + and remote.capable('_push_experiment_pushobsmarkers_0')): + # push marker early to limit damage of pushing too early. + try: + obsfp = repo.sopener('obsstore') + except IOError as e: + if e.errno != errno.ENOENT: + raise + else: + remote.push_experiment_pushobsmarkers_0(obsfp) + ret = orig(repo, remote, *args, **kwargs) + if remote.capable('_push_experiment_notifypushend_0'): + remote.push_experiment_notifypushend_0() + return ret + + +def capabilities(orig, repo, proto): + """wrapper to advertise new capability""" + caps = orig(repo, proto) + if obsolete._enabled: + caps += ' _push_experiment_pushobsmarkers_0' + caps += ' _push_experiment_notifypushend_0' + return caps + + +def extsetup(ui): + wireproto.wirepeer.push_experiment_pushobsmarkers_0 = client_pushobsmarkers + wireproto.wirepeer.push_experiment_notifypushend_0 = client_notifypushend + wireproto.commands['push_experiment_pushobsmarkers_0'] = (srv_pushobsmarkers, '') + wireproto.commands['push_experiment_notifypushend_0'] = (srv_notifypushend, '') + extensions.wrapfunction(wireproto, 'capabilities', capabilities) + extensions.wrapfunction(obsolete, 'syncpush', syncpush) + extensions.wrapfunction(localrepo.localrepository, 'push', augmented_push) + + diff -r ad2060da7ffa -r f49d4774b999 hgext/simple4server.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/simple4server.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,12 @@ +'''enable experimental obsolescence feature of Mercurial + +OBSOLESCENCE IS AN EXPERIMENTAL FEATURE MAKE SURE YOU UNDERSTOOD THE INVOLVED +CONCEPT BEFORE USING IT. + +/!\ THIS EXTENSION IS INTENDED FOR SERVER SIDE ONLY USAGE /!\ + +For client side usages it is recommended to use the evolve extension for +improved user interface.''' + +import mercurial.obsolete +mercurial.obsolete._enabled = True diff -r ad2060da7ffa -r f49d4774b999 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,18 @@ +# Copied from histedit setup.py +# Credit to Augie Fackler + +from distutils.core import setup + +setup( + name='hg-evolve', + version='3.2.0', + author='Pierre-Yves David', + maintainer='Pierre-Yves David', + maintainer_email='pierre-yves.david@logilab.fr', + url='https://bitbucket.org/marmoute/mutable-history', + description='Flexible evolution of Mercurial history.', + long_description=open('README').read(), + keywords='hg mercurial', + license='GPLv2+', + py_modules=['hgext.evolve', 'hgext.pushexperiment'], +) diff -r ad2060da7ffa -r f49d4774b999 tests/killdaemons.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/killdaemons.py Thu Feb 20 12:56:57 2014 -0800 @@ -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 diff -r ad2060da7ffa -r f49d4774b999 tests/run-tests.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/run-tests.py Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,1128 @@ +#!/usr/bin/env python +# +# run-tests.py - Run a set of tests on Mercurial +# +# Copyright 2006 Matt Mackall +# +# This software may be used and distributed according to the terms of the +# GNU General Public License version 2 or any later version. + +# Modifying this script is tricky because it has many modes: +# - serial (default) vs parallel (-jN, N > 1) +# - no coverage (default) vs coverage (-c, -C, -s) +# - temp install (default) vs specific hg script (--with-hg, --local) +# - tests are a mix of shell scripts and Python scripts +# +# If you change this script, it is recommended that you ensure you +# haven't broken it by running it in various modes with a representative +# sample of test scripts. For example: +# +# 1) serial, no coverage, temp install: +# ./run-tests.py test-s* +# 2) serial, no coverage, local hg: +# ./run-tests.py --local test-s* +# 3) serial, coverage, temp install: +# ./run-tests.py -c test-s* +# 4) serial, coverage, local hg: +# ./run-tests.py -c --local test-s* # unsupported +# 5) parallel, no coverage, temp install: +# ./run-tests.py -j2 test-s* +# 6) parallel, no coverage, local hg: +# ./run-tests.py -j2 --local test-s* +# 7) parallel, coverage, temp install: +# ./run-tests.py -j2 -c test-s* # currently broken +# 8) parallel, coverage, local install: +# ./run-tests.py -j2 -c --local test-s* # unsupported (and broken) +# 9) parallel, custom tmp dir: +# ./run-tests.py -j2 --tmpdir /tmp/myhgtests +# +# (You could use any subset of the tests: test-s* happens to match +# enough that it's worth doing parallel runs, few enough that it +# completes fairly quickly, includes both shell and Python scripts, and +# includes some scripts that run daemon processes.) + +from distutils import version +import difflib +import errno +import optparse +import os +import shutil +import subprocess +import signal +import sys +import tempfile +import time +import re + +closefds = os.name == 'posix' +def Popen4(cmd, bufsize=-1): + p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, + close_fds=closefds, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + p.fromchild = p.stdout + p.tochild = p.stdin + p.childerr = p.stderr + return p + +# reserved exit code to skip test (used by hghave) +SKIPPED_STATUS = 80 +SKIPPED_PREFIX = 'skipped: ' +FAILED_PREFIX = 'hghave check failed: ' +PYTHON = sys.executable +IMPL_PATH = 'PYTHONPATH' +if 'java' in sys.platform: + IMPL_PATH = 'JYTHONPATH' + +requiredtools = ["python", "diff", "grep", "sed"] + +defaults = { + 'jobs': ('HGTEST_JOBS', 1), + 'timeout': ('HGTEST_TIMEOUT', 180), + 'port': ('HGTEST_PORT', 20059), +} + +def parseargs(): + parser = optparse.OptionParser("%prog [options] [tests]") + + # keep these sorted + parser.add_option("--blacklist", action="append", + help="skip tests listed in the specified blacklist file") + parser.add_option("-C", "--annotate", action="store_true", + help="output files annotated with coverage") + parser.add_option("--child", type="int", + help="run as child process, summary to given fd") + parser.add_option("-c", "--cover", action="store_true", + help="print a test coverage report") + parser.add_option("-d", "--debug", action="store_true", + help="debug mode: write output of test scripts to console" + " rather than capturing and diff'ing it (disables timeout)") + parser.add_option("-f", "--first", action="store_true", + help="exit on the first test failure") + parser.add_option("--inotify", action="store_true", + help="enable inotify extension when running tests") + parser.add_option("-i", "--interactive", action="store_true", + help="prompt to accept changed output") + parser.add_option("-j", "--jobs", type="int", + help="number of jobs to run in parallel" + " (default: $%s or %d)" % defaults['jobs']) + parser.add_option("--keep-tmpdir", action="store_true", + help="keep temporary directory after running tests") + parser.add_option("-k", "--keywords", + help="run tests matching keywords") + parser.add_option("-l", "--local", action="store_true", + help="shortcut for --with-hg=/../hg") + parser.add_option("-n", "--nodiff", action="store_true", + help="skip showing test changes") + parser.add_option("-p", "--port", type="int", + help="port on which servers should listen" + " (default: $%s or %d)" % defaults['port']) + parser.add_option("--pure", action="store_true", + help="use pure Python code instead of C extensions") + parser.add_option("-R", "--restart", action="store_true", + help="restart at last error") + parser.add_option("-r", "--retest", action="store_true", + help="retest failed tests") + parser.add_option("-S", "--noskips", action="store_true", + help="don't report skip tests verbosely") + parser.add_option("-t", "--timeout", type="int", + help="kill errant tests after TIMEOUT seconds" + " (default: $%s or %d)" % defaults['timeout']) + parser.add_option("--tmpdir", type="string", + help="run tests in the given temporary directory" + " (implies --keep-tmpdir)") + parser.add_option("-v", "--verbose", action="store_true", + help="output verbose messages") + parser.add_option("--view", type="string", + help="external diff viewer") + parser.add_option("--with-hg", type="string", + metavar="HG", + help="test using specified hg script rather than a " + "temporary installation") + parser.add_option("-3", "--py3k-warnings", action="store_true", + help="enable Py3k warnings on Python 2.6+") + + for option, default in defaults.items(): + defaults[option] = int(os.environ.get(*default)) + parser.set_defaults(**defaults) + (options, args) = parser.parse_args() + + # jython is always pure + if 'java' in sys.platform or '__pypy__' in sys.modules: + options.pure = True + + if options.with_hg: + if not (os.path.isfile(options.with_hg) and + os.access(options.with_hg, os.X_OK)): + parser.error('--with-hg must specify an executable hg script') + if not os.path.basename(options.with_hg) == 'hg': + sys.stderr.write('warning: --with-hg should specify an hg script') + if options.local: + testdir = os.path.dirname(os.path.realpath(sys.argv[0])) + hgbin = os.path.join(os.path.dirname(testdir), 'hg') + if not os.access(hgbin, os.X_OK): + parser.error('--local specified, but %r not found or not executable' + % hgbin) + options.with_hg = hgbin + + options.anycoverage = options.cover or options.annotate + if options.anycoverage: + try: + import coverage + covver = version.StrictVersion(coverage.__version__).version + if covver < (3, 3): + parser.error('coverage options require coverage 3.3 or later') + except ImportError: + parser.error('coverage options now require the coverage package') + + if options.anycoverage and options.local: + # this needs some path mangling somewhere, I guess + parser.error("sorry, coverage options do not work when --local " + "is specified") + + global vlog + if options.verbose: + if options.jobs > 1 or options.child is not None: + pid = "[%d]" % os.getpid() + else: + pid = None + def vlog(*msg): + if pid: + print pid, + for m in msg: + print m, + print + sys.stdout.flush() + else: + vlog = lambda *msg: None + + if options.tmpdir: + options.tmpdir = os.path.expanduser(options.tmpdir) + + if options.jobs < 1: + parser.error('--jobs must be positive') + if options.interactive and options.jobs > 1: + print '(--interactive overrides --jobs)' + options.jobs = 1 + if options.interactive and options.debug: + parser.error("-i/--interactive and -d/--debug are incompatible") + if options.debug: + if options.timeout != defaults['timeout']: + sys.stderr.write( + 'warning: --timeout option ignored with --debug\n') + options.timeout = 0 + if options.py3k_warnings: + if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0): + parser.error('--py3k-warnings can only be used on Python 2.6+') + if options.blacklist: + blacklist = dict() + for filename in options.blacklist: + try: + path = os.path.expanduser(os.path.expandvars(filename)) + f = open(path, "r") + except IOError, err: + if err.errno != errno.ENOENT: + raise + print "warning: no such blacklist file: %s" % filename + continue + + for line in f.readlines(): + line = line.strip() + if line and not line.startswith('#'): + blacklist[line] = filename + + f.close() + + options.blacklist = blacklist + + return (options, args) + +def rename(src, dst): + """Like os.rename(), trade atomicity and opened files friendliness + for existing destination support. + """ + shutil.copy(src, dst) + os.remove(src) + +def splitnewlines(text): + '''like str.splitlines, but only split on newlines. + keep line endings.''' + i = 0 + lines = [] + while True: + n = text.find('\n', i) + if n == -1: + last = text[i:] + if last: + lines.append(last) + return lines + lines.append(text[i:n + 1]) + i = n + 1 + +def parsehghaveoutput(lines): + '''Parse hghave log lines. + Return tuple of lists (missing, failed): + * the missing/unknown features + * the features for which existence check failed''' + missing = [] + failed = [] + for line in lines: + if line.startswith(SKIPPED_PREFIX): + line = line.splitlines()[0] + missing.append(line[len(SKIPPED_PREFIX):]) + elif line.startswith(FAILED_PREFIX): + line = line.splitlines()[0] + failed.append(line[len(FAILED_PREFIX):]) + + return missing, failed + +def showdiff(expected, output, ref, err): + try: + for line in difflib.unified_diff(expected, output, ref, err): + sys.stdout.write(line) + except IOError, ex: + print >>sys.stderr, 'BORKEN PIPE', ex.errno + pass + +def findprogram(program): + """Search PATH for a executable program""" + for p in os.environ.get('PATH', os.defpath).split(os.pathsep): + name = os.path.join(p, program) + if os.access(name, os.X_OK): + return name + return None + +def checktools(): + # Before we go any further, check for pre-requisite tools + # stuff from coreutils (cat, rm, etc) are not tested + for p in requiredtools: + if os.name == 'nt': + p += '.exe' + found = findprogram(p) + if found: + vlog("# Found prerequisite", p, "at", found) + else: + print "WARNING: Did not find prerequisite tool: "+p + +def killdaemons(): + # Kill off any leftover daemon processes + try: + fp = open(DAEMON_PIDS) + for line in fp: + try: + pid = int(line) + except ValueError: + continue + try: + os.kill(pid, 0) + vlog('# Killing daemon process %d' % pid) + os.kill(pid, signal.SIGTERM) + time.sleep(0.25) + os.kill(pid, 0) + vlog('# Daemon process %d is stuck - really killing it' % pid) + os.kill(pid, signal.SIGKILL) + except OSError, err: + if err.errno != errno.ESRCH: + raise + fp.close() + os.unlink(DAEMON_PIDS) + except IOError: + pass + +def cleanup(options): + if not options.keep_tmpdir: + vlog("# Cleaning up HGTMP", HGTMP) + shutil.rmtree(HGTMP, True) + +def usecorrectpython(): + # some tests run python interpreter. they must use same + # interpreter we use or bad things will happen. + exedir, exename = os.path.split(sys.executable) + if exename == 'python': + path = findprogram('python') + if os.path.dirname(path) == exedir: + return + vlog('# Making python executable in test path use correct Python') + mypython = os.path.join(BINDIR, 'python') + try: + os.symlink(sys.executable, mypython) + except AttributeError: + # windows fallback + shutil.copyfile(sys.executable, mypython) + shutil.copymode(sys.executable, mypython) + +def installhg(options): + vlog("# Performing temporary installation of HG") + installerrs = os.path.join("tests", "install.err") + pure = options.pure and "--pure" or "" + + # Run installer in hg root + script = os.path.realpath(sys.argv[0]) + hgroot = os.path.dirname(os.path.dirname(script)) + os.chdir(hgroot) + nohome = '--home=""' + if os.name == 'nt': + # The --home="" trick works only on OS where os.sep == '/' + # because of a distutils convert_path() fast-path. Avoid it at + # least on Windows for now, deal with .pydistutils.cfg bugs + # when they happen. + nohome = '' + cmd = ('%s setup.py %s clean --all' + ' build --build-base="%s"' + ' install --force --prefix="%s" --install-lib="%s"' + ' --install-scripts="%s" %s >%s 2>&1' + % (sys.executable, pure, os.path.join(HGTMP, "build"), + INST, PYTHONDIR, BINDIR, nohome, installerrs)) + vlog("# Running", cmd) + if os.system(cmd) == 0: + if not options.verbose: + os.remove(installerrs) + else: + f = open(installerrs) + for line in f: + print line, + f.close() + sys.exit(1) + os.chdir(TESTDIR) + + usecorrectpython() + + vlog("# Installing dummy diffstat") + f = open(os.path.join(BINDIR, 'diffstat'), 'w') + f.write('#!' + sys.executable + '\n' + 'import sys\n' + 'files = 0\n' + 'for line in sys.stdin:\n' + ' if line.startswith("diff "):\n' + ' files += 1\n' + 'sys.stdout.write("files patched: %d\\n" % files)\n') + f.close() + os.chmod(os.path.join(BINDIR, 'diffstat'), 0700) + + if options.py3k_warnings and not options.anycoverage: + vlog("# Updating hg command to enable Py3k Warnings switch") + f = open(os.path.join(BINDIR, 'hg'), 'r') + lines = [line.rstrip() for line in f] + lines[0] += ' -3' + f.close() + f = open(os.path.join(BINDIR, 'hg'), 'w') + for line in lines: + f.write(line + '\n') + f.close() + + if options.anycoverage: + custom = os.path.join(TESTDIR, 'sitecustomize.py') + target = os.path.join(PYTHONDIR, 'sitecustomize.py') + vlog('# Installing coverage trigger to %s' % target) + shutil.copyfile(custom, target) + rc = os.path.join(TESTDIR, '.coveragerc') + vlog('# Installing coverage rc to %s' % rc) + os.environ['COVERAGE_PROCESS_START'] = rc + fn = os.path.join(INST, '..', '.coverage') + os.environ['COVERAGE_FILE'] = fn + +def outputcoverage(options): + + vlog('# Producing coverage report') + os.chdir(PYTHONDIR) + + def covrun(*args): + cmd = 'coverage %s' % ' '.join(args) + vlog('# Running: %s' % cmd) + os.system(cmd) + + if options.child: + return + + covrun('-c') + omit = ','.join([BINDIR, TESTDIR]) + covrun('-i', '-r', '"--omit=%s"' % omit) # report + if options.annotate: + adir = os.path.join(TESTDIR, 'annotated') + if not os.path.isdir(adir): + os.mkdir(adir) + covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit) + +class Timeout(Exception): + pass + +def alarmed(signum, frame): + raise Timeout + +def pytest(test, options, replacements): + py3kswitch = options.py3k_warnings and ' -3' or '' + cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test) + vlog("# Running", cmd) + return run(cmd, options, replacements) + +def shtest(test, options, replacements): + cmd = '"%s"' % test + vlog("# Running", cmd) + return run(cmd, options, replacements) + +needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search +escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub +escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256)) +escapemap.update({'\\': '\\\\', '\r': r'\r'}) +def escapef(m): + return escapemap[m.group(0)] +def stringescape(s): + return escapesub(escapef, s) + +def tsttest(test, options, replacements): + t = open(test) + out = [] + script = [] + salt = "SALT" + str(time.time()) + + pos = prepos = -1 + after = {} + expected = {} + for n, l in enumerate(t): + if not l.endswith('\n'): + l += '\n' + if l.startswith(' $ '): # commands + after.setdefault(pos, []).append(l) + prepos = pos + pos = n + script.append('echo %s %s $?\n' % (salt, n)) + script.append(l[4:]) + elif l.startswith(' > '): # continuations + after.setdefault(prepos, []).append(l) + script.append(l[4:]) + elif l.startswith(' '): # results + # queue up a list of expected results + expected.setdefault(pos, []).append(l[2:]) + else: + # non-command/result - queue up for merged output + after.setdefault(pos, []).append(l) + + t.close() + + script.append('echo %s %s $?\n' % (salt, n + 1)) + + fd, name = tempfile.mkstemp(suffix='hg-tst') + + try: + for l in script: + os.write(fd, l) + os.close(fd) + + cmd = '/bin/sh "%s"' % name + vlog("# Running", cmd) + exitcode, output = run(cmd, options, replacements) + # do not merge output if skipped, return hghave message instead + # similarly, with --debug, output is None + if exitcode == SKIPPED_STATUS or output is None: + return exitcode, output + finally: + os.remove(name) + + def rematch(el, l): + try: + # ensure that the regex matches to the end of the string + return re.match(el + r'\Z', l) + except re.error: + # el is an invalid regex + return False + + def globmatch(el, l): + # The only supported special characters are * and ?. Escaping is + # supported. + i, n = 0, len(el) + res = '' + while i < n: + c = el[i] + i += 1 + if c == '\\' and el[i] in '*?\\': + res += el[i - 1:i + 1] + i += 1 + elif c == '*': + res += '.*' + elif c == '?': + res += '.' + else: + res += re.escape(c) + return rematch(res, l) + + pos = -1 + postout = [] + ret = 0 + for n, l in enumerate(output): + lout, lcmd = l, None + if salt in l: + lout, lcmd = l.split(salt, 1) + + if lout: + if lcmd: + lout += ' (no-eol)\n' + + el = None + if pos in expected and expected[pos]: + el = expected[pos].pop(0) + + if el == lout: # perfect match (fast) + postout.append(" " + lout) + elif (el and + (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or + el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout) + or el.endswith(" (esc)\n") and + el.decode('string-escape') == l)): + postout.append(" " + el) # fallback regex/glob/esc match + else: + if needescape(lout): + lout = stringescape(lout.rstrip('\n')) + " (esc)\n" + postout.append(" " + lout) # let diff deal with it + + if lcmd: + # add on last return code + ret = int(lcmd.split()[1]) + if ret != 0: + postout.append(" [%s]\n" % ret) + if pos in after: + postout += after.pop(pos) + pos = int(lcmd.split()[0]) + + if pos in after: + postout += after.pop(pos) + + return exitcode, postout + +wifexited = getattr(os, "WIFEXITED", lambda x: False) +def run(cmd, options, replacements): + """Run command in a sub-process, capturing the output (stdout and stderr). + Return a tuple (exitcode, output). output is None in debug mode.""" + # TODO: Use subprocess.Popen if we're running on Python 2.4 + if options.debug: + proc = subprocess.Popen(cmd, shell=True) + ret = proc.wait() + return (ret, None) + + if os.name == 'nt' or sys.platform.startswith('java'): + tochild, fromchild = os.popen4(cmd) + tochild.close() + output = fromchild.read() + ret = fromchild.close() + if ret is None: + ret = 0 + else: + proc = Popen4(cmd) + def cleanup(): + os.kill(proc.pid, signal.SIGTERM) + ret = proc.wait() + if ret == 0: + ret = signal.SIGTERM << 8 + killdaemons() + return ret + + try: + output = '' + proc.tochild.close() + output = proc.fromchild.read() + ret = proc.wait() + if wifexited(ret): + ret = os.WEXITSTATUS(ret) + except Timeout: + vlog('# Process %d timed out - killing it' % proc.pid) + ret = cleanup() + output += ("\n### Abort: timeout after %d seconds.\n" + % options.timeout) + except KeyboardInterrupt: + vlog('# Handling keyboard interrupt') + cleanup() + raise + + for s, r in replacements: + output = re.sub(s, r, output) + return ret, splitnewlines(output) + +def runone(options, test, skips, fails): + '''tristate output: + None -> skipped + True -> passed + False -> failed''' + + def skip(msg): + if not options.verbose: + skips.append((test, msg)) + else: + print "\nSkipping %s: %s" % (testpath, msg) + return None + + def fail(msg): + fails.append((test, msg)) + if not options.nodiff: + print "\nERROR: %s %s" % (testpath, msg) + return None + + vlog("# Test", test) + + # create a fresh hgrc + hgrc = open(HGRCPATH, 'w+') + hgrc.write('[ui]\n') + hgrc.write('slash = True\n') + hgrc.write('[defaults]\n') + hgrc.write('backout = -d "0 0"\n') + hgrc.write('commit = -d "0 0"\n') + hgrc.write('tag = -d "0 0"\n') + if options.inotify: + hgrc.write('[extensions]\n') + hgrc.write('inotify=\n') + hgrc.write('[inotify]\n') + hgrc.write('pidfile=%s\n' % DAEMON_PIDS) + hgrc.write('appendpid=True\n') + hgrc.close() + + testpath = os.path.join(TESTDIR, test) + ref = os.path.join(TESTDIR, test+".out") + err = os.path.join(TESTDIR, test+".err") + if os.path.exists(err): + os.remove(err) # Remove any previous output files + try: + tf = open(testpath) + firstline = tf.readline().rstrip() + tf.close() + except: + firstline = '' + lctest = test.lower() + + if lctest.endswith('.py') or firstline == '#!/usr/bin/env python': + runner = pytest + elif lctest.endswith('.t'): + runner = tsttest + ref = testpath + else: + # do not try to run non-executable programs + if not os.access(testpath, os.X_OK): + return skip("not executable") + runner = shtest + + # Make a tmp subdirectory to work in + testtmp = os.environ["TESTTMP"] = os.path.join(HGTMP, test) + os.mkdir(testtmp) + os.chdir(testtmp) + + if options.timeout > 0: + signal.alarm(options.timeout) + + ret, out = runner(testpath, options, [ + (re.escape(testtmp), '$TESTTMP'), + (r':%s\b' % options.port, ':$HGPORT'), + (r':%s\b' % (options.port + 1), ':$HGPORT1'), + (r':%s\b' % (options.port + 2), ':$HGPORT2'), + ]) + vlog("# Ret was:", ret) + + if options.timeout > 0: + signal.alarm(0) + + mark = '.' + + skipped = (ret == SKIPPED_STATUS) + + # If we're not in --debug mode and reference output file exists, + # check test output against it. + if options.debug: + refout = None # to match "out is None" + elif os.path.exists(ref): + f = open(ref, "r") + refout = splitnewlines(f.read()) + f.close() + else: + refout = [] + + if (ret != 0 or out != refout) and not skipped and not options.debug: + # Save errors to a file for diagnosis + f = open(err, "wb") + for line in out: + f.write(line) + f.close() + + if skipped: + mark = 's' + if out is None: # debug mode: nothing to parse + missing = ['unknown'] + failed = None + else: + missing, failed = parsehghaveoutput(out) + if not missing: + missing = ['irrelevant'] + if failed: + fail("hghave failed checking for %s" % failed[-1]) + skipped = False + else: + skip(missing[-1]) + elif out != refout: + mark = '!' + if ret: + fail("output changed and returned error code %d" % ret) + else: + fail("output changed") + if not options.nodiff: + if options.view: + os.system("%s %s %s" % (options.view, ref, err)) + else: + showdiff(refout, out, ref, err) + ret = 1 + elif ret: + mark = '!' + fail("returned error code %d" % ret) + + if not options.verbose: + try: + sys.stdout.write(mark) + sys.stdout.flush() + except IOError, ex: + print >>sys.stderr, 'BORKEN PIPE', ex.errno + pass + + killdaemons() + + os.chdir(TESTDIR) + if not options.keep_tmpdir: + shutil.rmtree(testtmp, True) + if skipped: + return None + return ret == 0 + +_hgpath = None + +def _gethgpath(): + """Return the path to the mercurial package that is actually found by + the current Python interpreter.""" + global _hgpath + if _hgpath is not None: + return _hgpath + + cmd = '%s -c "import mercurial; print mercurial.__path__[0]"' + pipe = os.popen(cmd % PYTHON) + try: + _hgpath = pipe.read().strip() + finally: + pipe.close() + return _hgpath + +def _checkhglib(verb): + """Ensure that the 'mercurial' package imported by python is + the one we expect it to be. If not, print a warning to stderr.""" + expecthg = os.path.join(PYTHONDIR, 'mercurial') + actualhg = _gethgpath() + if actualhg != expecthg: + sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n' + ' (expected %s)\n' + % (verb, actualhg, expecthg)) + +def runchildren(options, tests): + if INST: + installhg(options) + _checkhglib("Testing") + + optcopy = dict(options.__dict__) + optcopy['jobs'] = 1 + del optcopy['blacklist'] + if optcopy['with_hg'] is None: + optcopy['with_hg'] = os.path.join(BINDIR, "hg") + optcopy.pop('anycoverage', None) + + opts = [] + for opt, value in optcopy.iteritems(): + name = '--' + opt.replace('_', '-') + if value is True: + opts.append(name) + elif value is not None: + opts.append(name + '=' + str(value)) + + tests.reverse() + jobs = [[] for j in xrange(options.jobs)] + while tests: + for job in jobs: + if not tests: + break + job.append(tests.pop()) + fps = {} + + for j, job in enumerate(jobs): + if not job: + continue + rfd, wfd = os.pipe() + childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)] + childtmp = os.path.join(HGTMP, 'child%d' % j) + childopts += ['--tmpdir', childtmp] + cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job + vlog(' '.join(cmdline)) + fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r') + os.close(wfd) + signal.signal(signal.SIGINT, signal.SIG_IGN) + failures = 0 + tested, skipped, failed = 0, 0, 0 + skips = [] + fails = [] + while fps: + pid, status = os.wait() + fp = fps.pop(pid) + l = fp.read().splitlines() + try: + test, skip, fail = map(int, l[:3]) + except ValueError: + test, skip, fail = 0, 0, 0 + split = -fail or len(l) + for s in l[3:split]: + skips.append(s.split(" ", 1)) + for s in l[split:]: + fails.append(s.split(" ", 1)) + tested += test + skipped += skip + failed += fail + vlog('pid %d exited, status %d' % (pid, status)) + failures |= status + print + if not options.noskips: + for s in skips: + print "Skipped %s: %s" % (s[0], s[1]) + for s in fails: + print "Failed %s: %s" % (s[0], s[1]) + + _checkhglib("Tested") + print "# Ran %d tests, %d skipped, %d failed." % ( + tested, skipped, failed) + + if options.anycoverage: + outputcoverage(options) + sys.exit(failures != 0) + +def runtests(options, tests): + global DAEMON_PIDS, HGRCPATH + DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids') + HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc') + + try: + if INST: + installhg(options) + _checkhglib("Testing") + + if options.timeout > 0: + try: + signal.signal(signal.SIGALRM, alarmed) + vlog('# Running each test with %d second timeout' % + options.timeout) + except AttributeError: + print 'WARNING: cannot run tests with timeouts' + options.timeout = 0 + + tested = 0 + failed = 0 + skipped = 0 + + if options.restart: + orig = list(tests) + while tests: + if os.path.exists(tests[0] + ".err"): + break + tests.pop(0) + if not tests: + print "running all tests" + tests = orig + + skips = [] + fails = [] + + for test in tests: + if options.blacklist: + filename = options.blacklist.get(test) + if filename is not None: + skips.append((test, "blacklisted (%s)" % filename)) + skipped += 1 + continue + + if options.retest and not os.path.exists(test + ".err"): + skipped += 1 + continue + + if options.keywords: + fp = open(test) + t = fp.read().lower() + test.lower() + fp.close() + for k in options.keywords.lower().split(): + if k in t: + break + else: + skipped += 1 + continue + + ret = runone(options, test, skips, fails) + if ret is None: + skipped += 1 + elif not ret: + if options.interactive: + print "Accept this change? [n] ", + answer = sys.stdin.readline().strip() + if answer.lower() in "y yes".split(): + if test.endswith(".t"): + rename(test + ".err", test) + else: + rename(test + ".err", test + ".out") + tested += 1 + fails.pop() + continue + failed += 1 + if options.first: + break + tested += 1 + + if options.child: + fp = os.fdopen(options.child, 'w') + fp.write('%d\n%d\n%d\n' % (tested, skipped, failed)) + for s in skips: + fp.write("%s %s\n" % s) + for s in fails: + fp.write("%s %s\n" % s) + fp.close() + else: + print + for s in skips: + print "Skipped %s: %s" % s + for s in fails: + print "Failed %s: %s" % s + _checkhglib("Tested") + print "# Ran %d tests, %d skipped, %d failed." % ( + tested, skipped, failed) + + if options.anycoverage: + outputcoverage(options) + except KeyboardInterrupt: + failed = True + print "\ninterrupted!" + + if failed: + sys.exit(1) + +def main(): + (options, args) = parseargs() + if not options.child: + os.umask(022) + + checktools() + + if len(args) == 0: + args = os.listdir(".") + args.sort() + + tests = [] + skipped = [] + for test in args: + if (test.startswith("test-") and '~' not in test and + ('.' not in test or test.endswith('.py') or + test.endswith('.bat') or test.endswith('.t'))): + if not os.path.exists(test): + skipped.append(test) + else: + tests.append(test) + if not tests: + for test in skipped: + print 'Skipped %s: does not exist' % test + print "# Ran 0 tests, %d skipped, 0 failed." % len(skipped) + return + tests = tests + skipped + + # Reset some environment variables to well-known values so that + # the tests produce repeatable output. + os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C' + os.environ['TZ'] = 'GMT' + os.environ["EMAIL"] = "Foo Bar " + os.environ['CDPATH'] = '' + os.environ['COLUMNS'] = '80' + os.environ['GREP_OPTIONS'] = '' + os.environ['http_proxy'] = '' + + # unset env related to hooks + for k in os.environ.keys(): + if k.startswith('HG_'): + # can't remove on solaris + os.environ[k] = '' + del os.environ[k] + + global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE + TESTDIR = os.environ["TESTDIR"] = os.getcwd() + if options.tmpdir: + options.keep_tmpdir = True + tmpdir = options.tmpdir + if os.path.exists(tmpdir): + # Meaning of tmpdir has changed since 1.3: we used to create + # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if + # tmpdir already exists. + sys.exit("error: temp dir %r already exists" % tmpdir) + + # Automatically removing tmpdir sounds convenient, but could + # really annoy anyone in the habit of using "--tmpdir=/tmp" + # or "--tmpdir=$HOME". + #vlog("# Removing temp dir", tmpdir) + #shutil.rmtree(tmpdir) + os.makedirs(tmpdir) + else: + tmpdir = tempfile.mkdtemp('', 'hgtests.') + HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir) + DAEMON_PIDS = None + HGRCPATH = None + + os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"' + os.environ["HGMERGE"] = "internal:merge" + os.environ["HGUSER"] = "test" + os.environ["HGENCODING"] = "ascii" + os.environ["HGENCODINGMODE"] = "strict" + os.environ["HGPORT"] = str(options.port) + os.environ["HGPORT1"] = str(options.port + 1) + os.environ["HGPORT2"] = str(options.port + 2) + + if options.with_hg: + INST = None + BINDIR = os.path.dirname(os.path.realpath(options.with_hg)) + + # This looks redundant with how Python initializes sys.path from + # the location of the script being executed. Needed because the + # "hg" specified by --with-hg is not the only Python script + # executed in the test suite that needs to import 'mercurial' + # ... which means it's not really redundant at all. + PYTHONDIR = BINDIR + else: + INST = os.path.join(HGTMP, "install") + BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin") + PYTHONDIR = os.path.join(INST, "lib", "python") + + os.environ["BINDIR"] = BINDIR + os.environ["PYTHON"] = PYTHON + + if not options.child: + path = [BINDIR] + os.environ["PATH"].split(os.pathsep) + os.environ["PATH"] = os.pathsep.join(path) + + # Include TESTDIR in PYTHONPATH so that out-of-tree extensions + # can run .../tests/run-tests.py test-foo where test-foo + # adds an extension to HGRC + pypath = [PYTHONDIR, TESTDIR] + # We have to augment PYTHONPATH, rather than simply replacing + # it, in case external libraries are only available via current + # PYTHONPATH. (In particular, the Subversion bindings on OS X + # are in /opt/subversion.) + oldpypath = os.environ.get(IMPL_PATH) + if oldpypath: + pypath.append(oldpypath) + os.environ[IMPL_PATH] = os.pathsep.join(pypath) + + COVERAGE_FILE = os.path.join(TESTDIR, ".coverage") + + vlog("# Using TESTDIR", TESTDIR) + vlog("# Using HGTMP", HGTMP) + vlog("# Using PATH", os.environ["PATH"]) + vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH]) + + try: + if len(tests) > 1 and options.jobs > 1: + runchildren(options, tests) + else: + runtests(options, tests) + finally: + time.sleep(1) + cleanup(options) + +if __name__ == '__main__': + main() diff -r ad2060da7ffa -r f49d4774b999 tests/test-amend.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-amend.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,83 @@ + $ cat >> $HGRCPATH < [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ glog() { + > hg glog --template '{rev}@{branch}({phase}) {desc|firstline}\n' "$@" + > } + + $ hg init repo --traceback + $ 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 debugobsolete + 07f4944404050f47db2e5c5071e0e84e7a27bba9 6a022cbb61d5ba0f03f98ff2d36319dfea1034ae 0 {'date': '* *', 'user': 'test'} (glob) + b2e32ffb533cbe1d5759638c0cd4e8abc43b2738 0 {'date': '* *', 'user': 'test'} (glob) + $ hg branch + foo + $ hg branches + foo 2:6a022cbb61d5 + $ glog + @ 2@foo(draft) adda + +Test no-op + + $ hg amend + nothing changed + [1] + $ glog + @ 2@foo(draft) adda + + +Test forcing the message to the same value, no intermediate revision. + + $ hg amend -m 'adda' + nothing changed + [1] + $ 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 status + M a + $ hg pstatus + $ hg diff + diff -r f7a50201fe3a a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a * +0000 (glob) + @@ -1,2 +1,1 @@ + a + -a + $ hg pdiff + $ hg ci -m reseta + $ hg debugobsolete + 07f4944404050f47db2e5c5071e0e84e7a27bba9 6a022cbb61d5ba0f03f98ff2d36319dfea1034ae 0 {'date': '* *', 'user': 'test'} (glob) + b2e32ffb533cbe1d5759638c0cd4e8abc43b2738 0 {'date': '* *', 'user': 'test'} (glob) + $ hg phase 2 + 2: draft + $ glog + @ 4@foo(draft) reseta + | + o 3@foo(draft) changea + | + o 2@foo(draft) adda + + diff -r ad2060da7ffa -r f49d4774b999 tests/test-corrupt.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-corrupt.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,122 @@ + + $ cat >> $HGRCPATH < [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 "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 + 2 changesets pruned + $ 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 + + + diff -r ad2060da7ffa -r f49d4774b999 tests/test-drop.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-drop.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,267 @@ + + $ cat >> $HGRCPATH < [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "drophack=$(echo $(dirname $TESTDIR))/hgext/drophack.py" >> $HGRCPATH + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + $ summary() { + > echo ============ graph ============== + > hg log -G + > echo ============ hidden ============= + > hg log --hidden -G + > echo ============ obsmark ============ + > hg debugobsolete + > } + + + $ hg init repo + $ cd repo + $ mkcommit base + +drop a single changeset without any rewrite +================================================ + + + $ mkcommit simple-single + $ summary + ============ graph ============== + @ changeset: 1:d4e7845543ff + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add simple-single + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ hidden ============= + @ changeset: 1:d4e7845543ff + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add simple-single + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ obsmark ============ + $ hg drop . + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory now at b4952fcf48cf + search obsmarker: wall * comb * user * sys * (glob) + 0 obsmarkers found + search nodes: wall * comb * user * sys * (glob) + 1 nodes found + saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d4e7845543ff-drophack.hg + strip nodes: wall * comb * user * sys * (glob) + $ summary + ============ graph ============== + @ changeset: 0:b4952fcf48cf + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ hidden ============= + @ changeset: 0:b4952fcf48cf + tag: tip + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ obsmark ============ + +Try to drop a changeset with children +================================================ + + $ mkcommit parent + $ mkcommit child + $ summary + ============ graph ============== + @ changeset: 2:34b6c051bf1f + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add child + | + o changeset: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ hidden ============= + @ changeset: 2:34b6c051bf1f + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add child + | + o changeset: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ obsmark ============ + $ hg drop 1 + cannot drop revision with children (no-eol) + [1] + $ summary + ============ graph ============== + @ changeset: 2:34b6c051bf1f + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add child + | + o changeset: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ hidden ============= + @ changeset: 2:34b6c051bf1f + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add child + | + o changeset: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ obsmark ============ + +Try to drop a public changeset +================================================ + + $ hg phase --public 2 + $ hg drop 2 + cannot drop public revision (no-eol) + [1] + + +Try to drop a changeset with rewrite +================================================ + + $ hg phase --force --draft 2 + $ echo babar >> child + $ hg commit --amend + $ summary + ============ graph ============== + @ changeset: 4:a2c06c884bfe + | tag: tip + | parent: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add child + | + o changeset: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ hidden ============= + @ changeset: 4:a2c06c884bfe + | tag: tip + | parent: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add child + | + | x changeset: 3:87ea30a976fd + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: temporary amend commit for 34b6c051bf1f + | | + | x changeset: 2:34b6c051bf1f + |/ user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add child + | + o changeset: 1:19509a42b0d0 + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ obsmark ============ + 34b6c051bf1f78db6aef400776de5cb964470207 a2c06c884bfe53d3840026248bd8a7eafa152df8 0 {'date': '* *', 'user': 'test'} (glob) + 87ea30a976fdf235bf096f04899cb02a903873e2 0 {'date': '* *', 'user': 'test'} (glob) + $ hg drop . + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory now at 19509a42b0d0 + search obsmarker: wall * comb * user * sys * (glob) + 1 obsmarkers found + search nodes: wall * comb * user * sys * (glob) + 2 nodes found + strip obsmarker: wall * comb * user * sys * (glob) + saved backup bundle to $TESTTMP/repo/.hg/strip-backup/a2c06c884bfe-drophack.hg (glob) + strip nodes: wall * comb * user * sys * (glob) + $ summary + ============ graph ============== + @ changeset: 1:19509a42b0d0 + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ hidden ============= + @ changeset: 1:19509a42b0d0 + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: add parent + | + o changeset: 0:b4952fcf48cf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add base + + ============ obsmark ============ + 87ea30a976fdf235bf096f04899cb02a903873e2 0 {'date': '* *', 'user': 'test'} (glob) diff -r ad2060da7ffa -r f49d4774b999 tests/test-evolve.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-evolve.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,647 @@ + $ cat >> $HGRCPATH < [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 "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 kill and immutable changeset + + $ hg log -r 1 --template '{rev} {phase} {obsolete}\n' + 1 public stable + $ hg kill 1 + abort: cannot prune immutable changeset: 7c3bad9141dc + (see "hg help phases" for details) + [255] + $ hg log -r 1 --template '{rev} {phase} {obsolete}\n' + 1 public stable + +test simple kill + + $ hg id -n + 5 + $ hg kill . + 1 changesets pruned + 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 -r 3 + 2 changesets pruned + 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 . + 1 changesets pruned + 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 + +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 + +(amend of public changeset denied) + + $ hg phase --public 0 -v + phase changed for 1 changesets + + +(amend of on ancestors) + + $ hg amend + 1 new unstable 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:ba0ec09b1bab@default(draft) a nifty feature + | + | x 5:c296b79833d1@default(draft) temporary amend commit for 568a468b60fc + | | + | | o 4:207cbc4ea7fe@default(draft) another feature + | |/ + | | x 3:5bb880fc0f12@default(draft) temporary amend commit for 7b36850622b2 + | | | + | | x 2:7b36850622b2@default(draft) another feature + | |/ + | x 1:568a468b60fc@default(draft) a nifty feature + |/ + @ 0:e55e0562ee93@default(public) base + + $ hg debugobsolete + 7b36850622b2fd159fa30a4fb2a1edd2043b4a14 207cbc4ea7fee30d18b3a25f534fe5db22c6071b 0 {'date': '* *', 'user': 'test'} (glob) + 5bb880fc0f12dd61eee6de36f62b93fdbc3684b0 0 {'date': '* *', 'user': 'test'} (glob) + 568a468b60fc99a42d5d4ddbe181caff1eef308d ba0ec09b1babf3489b567853807f452edd46704f 0 {'date': '* *', 'user': 'test'} (glob) + c296b79833d1d497f33144786174bf35e04e44a3 0 {'date': '* *', 'user': 'test'} (glob) + $ hg evolve + 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 bumped warning + + $ hg phase --hidden --public 7 + 1 new bumped changesets + +all solving bumped troubled + + $ hg glog + @ 8 feature-B: another feature that rox - test + | + | o 7 : another feature - test + |/ + o 6 feature-A: a nifty feature - test + | + o 0 : base - test + + $ hg evolve --any --traceback + recreate:[8] another feature that rox + atop:[7] another feature + computing new diff + commited as ca3b75e3e59b + $ hg glog + @ 9 feature-B: bumped update to abe98aeaaa35: - test + | + o 7 : another feature - test + | + o 6 feature-A: a nifty feature - test + | + o 0 : base - test + + $ hg diff --hidden -r 9 -r 8 + $ hg diff -r 9^ -r 9 + diff --git a/main-file-1 b/main-file-1 + --- a/main-file-1 + +++ b/main-file-1 + @@ -3,1 +3,1 @@ + -Zwei + +deux + $ hg log -r 'bumped()' # no more bumped + +test evolve --all + $ sed -i'' -e s/deux/to/ main-file-1 + $ hg commit -m 'dansk 2!' + $ sed -i'' -e s/Three/tre/ main-file-1 + $ hg commit -m 'dansk 3!' + $ hg update 9 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ sed -i'' -e s/Un/Én/ main-file-1 + $ hg commit --amend -m 'dansk!' + 2 new unstable changesets + + $ hg evolve --all --traceback + move:[10] dansk 2! + atop:[13] dansk! + merging main-file-1 + move:[11] dansk 3! + atop:[14] dansk 2! + merging main-file-1 + $ hg glog + @ 15 : dansk 3! - test + | + o 14 : dansk 2! - test + | + o 13 feature-B: dansk! - test + | + o 7 : another feature - test + | + o 6 feature-A: a nifty feature - test + | + o 0 : base - test + + + $ 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()' + abort: empty revision set + [255] + $ hg --config extensions.hgext.mq= strip --hidden '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 + + $ cd .. + +Test graft --obsolete/--old-obsolete + + $ hg init test-graft + $ cd test-graft + $ mkcommit 0 + $ mkcommit 1 + $ mkcommit 2 + $ mkcommit 3 + $ hg up -qC 0 + $ mkcommit 4 + created new head + $ glog --hidden + @ 4:ce341209337f@default(draft) add 4 + | + | o 3:0e84df4912da@default(draft) add 3 + | | + | o 2:db038628b9e5@default(draft) add 2 + | | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg graft -r3 -O + grafting revision 3 + $ hg graft -r1 -o 2 + grafting revision 1 + $ glog --hidden + @ 6:acb28cd497b7@default(draft) add 1 + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | x 3:0e84df4912da@default(draft) add 3 + | | + | x 2:db038628b9e5@default(draft) add 2 + | | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg debugobsolete + 0e84df4912da4c7cad22a3b4fcfd58ddfb7c8ae9 0b9e50c35132ff548ec0065caea6a87e1ebcef32 0 {'date': '* *', 'user': 'test'} (glob) + db038628b9e56f51a454c0da0c508df247b41748 acb28cd497b7f8767e01ef70f68697a959573c2d 0 {'date': '* *', 'user': 'test'} (glob) + +Test graft --continue + + $ hg up -qC 0 + $ echo 2 > 1 + $ hg ci -Am conflict 1 + created new head + $ hg up -qC 6 + $ hg graft -O 7 + grafting revision 7 + merging 1 + warning: conflicts during merge. + merging 1 incomplete! (edit conflicts, then use 'hg resolve --mark') + abort: unresolved conflicts, can't continue + (use hg resolve and hg graft --continue) + [255] + $ hg log -r7 --template '{rev}:{node|short} {obsolete}\n' + 7:a5bfd90a2f29 stable + $ echo 3 > 1 + $ hg resolve -m 1 + $ hg graft --continue -O + grafting revision 7 + $ glog --hidden + @ 8:920e58bb443b@default(draft) conflict + | + | x 7:a5bfd90a2f29@default(draft) conflict + | | + o | 6:acb28cd497b7@default(draft) add 1 + | | + o | 5:0b9e50c35132@default(draft) add 3 + | | + o | 4:ce341209337f@default(draft) add 4 + |/ + | x 3:0e84df4912da@default(draft) add 3 + | | + | x 2:db038628b9e5@default(draft) add 2 + | | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg debugobsolete + 0e84df4912da4c7cad22a3b4fcfd58ddfb7c8ae9 0b9e50c35132ff548ec0065caea6a87e1ebcef32 0 {'date': '* *', 'user': 'test'} (glob) + db038628b9e56f51a454c0da0c508df247b41748 acb28cd497b7f8767e01ef70f68697a959573c2d 0 {'date': '* *', 'user': 'test'} (glob) + a5bfd90a2f29c7ccb8f917ff4e5013a9053d0a04 920e58bb443b73eea9d6d65570b4241051ea3229 0 {'date': '* *', 'user': 'test'} (glob) + +Test touch + + $ glog + @ 8:920e58bb443b@default(draft) conflict + | + o 6:acb28cd497b7@default(draft) add 1 + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg touch + $ glog + @ 9:*@default(draft) conflict (glob) + | + o 6:acb28cd497b7@default(draft) add 1 + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg touch . + $ glog + @ 10:*@default(draft) conflict (glob) + | + o 6:acb28cd497b7@default(draft) add 1 + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + +Test fold + + $ rm *.orig + $ hg fold + no revision to fold + [1] + $ hg fold 6 --rev 10 + abort: cannot specify both --rev and a target revision + [255] + $ hg fold 6 # want to run hg fold 6 + 2 changesets folded + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ glog + @ 11:*@default(draft) add 1 (glob) + | + o 5:0b9e50c35132@default(draft) add 3 + | + o 4:ce341209337f@default(draft) add 4 + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg log -r 11 --template '{desc}\n' + add 1 + + + conflict + $ hg debugrebuildstate + $ hg st + +Test fold with wc parent is not the head of the folded revision + + $ hg up 4 + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + $ hg fold --rev 4::11 + 3 changesets folded + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ glog + @ 12:*@default(draft) add 4 (glob) + | + | o 1:73d38bb17fd7@default(draft) add 1 + |/ + o 0:8685c6d34325@default(draft) add 0 + + $ hg log -r 12 --template '{desc}\n' + add 4 + + + add 3 + + + add 1 + + + conflict + $ hg debugrebuildstate + $ hg st + +Test olog + + $ hg olog + 4 : add 4 - test + 5 : add 3 - test + 11 : add 1 - test diff -r ad2060da7ffa -r f49d4774b999 tests/test-obsolete-push.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-obsolete-push.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,47 @@ + $ cat >> $HGRCPATH < [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ 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 + 2 changesets pruned + 1 new unstable 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 < [phases] + > publish = false + > EOF + $ hg outgoing ../clone --template "$template" + comparing with ../clone + searching for changes + 0:1994f17a630e@default(suspended/draft) A diff -r ad2060da7ffa -r f49d4774b999 tests/test-obsolete.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-obsolete.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,689 @@ + $ cat >> $HGRCPATH < [web] + > push_ssl = false + > allow_push = * + > [phases] + > publish=False + > [extensions] + > hgext.rebase= + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + $ getid() { + > hg id --hidden --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'} (glob) + + +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 --hidden 3 -q + working directory parent is obsolete! +(reported by parents too) + $ hg parents + changeset: 3:0d3f46688ccc + parent: 1:7c3bad9141dc + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add obsol_c + + working directory parent is obsolete! + $ mkcommit d # 5 (on 3) + 1 new unstable 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 log -G --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 unstable changeset: a7a6f2b5d8a5! + (use 'hg evolve' 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 --hidden 3 -q + working directory parent is obsolete! + $ mkcommit obsol_d # 6 + created new head + 1 new unstable 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 unstable changeset: 95de7fc6918d! + (use 'hg evolve' 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 --hidden -q .^ # 3 + working directory parent is obsolete! + $ mkcommit "obsol_d'" # 7 + created new head + 1 new unstable 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 [12] files \(\+1 heads\) (re) + \(run 'hg heads( \.)?' to see heads, 'hg merge' to merge\) (re) + $ qlog -R ../other-new + 6 + - 909a0fb57e5d + 3 + - 725c380fe99b + 2 + - 0d3f46688ccc + 1 + - 7c3bad9141dc + 0 + - 1f0dee641bb7 + +pushing to stuff that doesn't support obsolete + +DISABLED. the _enable switch it global :-/ + +.. $ 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 evolve' 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-ol +.. 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 --hidden .^ -q # 3 + working directory parent is obsolete! + $ mkcommit "obsol_d''" + created new head + 1 new unstable 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 [12] files \(\+1 heads\) (re) + \(run 'hg heads( \.)?' to see heads, 'hg merge' to merge\) (re) + + $ 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: 2 new changesets, 2 branch heads (merge) + unstable: 1 changesets + $ 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 bumped 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 that auto update ignore hidden changeset + $ hg up 0 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg up + 3 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg id -n + 8 + +Check that named update do too + + $ hg update default + 0 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg id -n + 8 + + $ hg up null -q # to be not based on 9 anymore + +check rebase compat + + $ hg log -G --template='{rev} - {node|short} {desc}\n' + o 8 - 159dfc9fa5d3 add obsol_d'' + | + | o 4 - 725c380fe99b add obsol_c' + | | + x | 3 - 0d3f46688ccc add obsol_c + |/ + o 1 - 7c3bad9141dc add b + | + o 0 - 1f0dee641bb7 add a + + + $ hg log -G --template='{rev} - {node|short}\n' --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 + +#excluded 'whole rebase set is extinct and ignored.' message not in core + $ hg rebase -b '3' -d 4 --traceback + 2 new divergent changesets + $ hg up tip + ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) + $ hg log -G --template='{rev} - {node|short} {desc}\n' + @ 11 - 9468a5f5d8b2 add obsol_d'' + | + o 10 - 2033b4e49474 add obsol_c + | + o 4 - 725c380fe99b add obsol_c' + | + o 1 - 7c3bad9141dc add b + | + o 0 - 1f0dee641bb7 add a + + +Does not complain about new head if you obsolete the old one +(re necesarry when we start runnind discovery on unfiltered repo in core) + + $ 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 [12] files (re) + $ 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 bumped detection +(make an obsolete changeset public) + + $ cd local + $ hg phase --hidden --public 11 + 1 new bumped changesets + $ hg log -G --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 'bumped()' + 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 bumped changeset: 6db5e282cb91! + (use 'hg evolve' 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 --quiet + $ hg log -G + @ changeset: 15:705ab2a6b72e + | 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 debugobsolete | grep -v 33d458d86621f3186c40bfccd77652f4a122743e + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0d3f46688ccc6e756c7e96cf64c391c411309597 0 {'date': '', 'user': 'test'} (glob) + 0d3f46688ccc6e756c7e96cf64c391c411309597 725c380fe99b5e76613493f0903e8d11ddc70d54 0 {'date': '', 'user': 'test'} (glob) + a7a6f2b5d8a54b81bc7aa2fba2934ad6d700a79e 95de7fc6918dea4c9c8d5382f50649794b474c4a 0 {'date': '', 'user': 'test'} (glob) + 95de7fc6918dea4c9c8d5382f50649794b474c4a 909a0fb57e5d909f353d89e394ffd7e0890fec88 0 {'date': '', 'user': 'test'} (glob) + 909a0fb57e5d909f353d89e394ffd7e0890fec88 159dfc9fa5d334d7e03a0aecfc7f7ab4c3431fea 0 {'date': '', 'user': 'test'} (glob) + 1f0dee641bb7258c56bd60e93edfa2405381c41e 83b5778897adafb967ef2f75be3aaa4fce49a4cc 0 {'date': '', 'user': 'test'} (glob) + 83b5778897adafb967ef2f75be3aaa4fce49a4cc 0 {'date': '', 'user': 'test'} (glob) + 0d3f46688ccc6e756c7e96cf64c391c411309597 2033b4e494742365851fac84d276640cbf52833e 0 {'date': '* *', 'user': 'test'} (glob) + 159dfc9fa5d334d7e03a0aecfc7f7ab4c3431fea 9468a5f5d8b2c5d91e17474e95ae4791e9718fdf 0 {'date': '* *', 'user': 'test'} (glob) + 9468a5f5d8b2c5d91e17474e95ae4791e9718fdf 6db5e282cb91df5c43ff1f1287c119ff83230d42 0 {'date': '', 'user': 'test'} (glob) + 0b1b6dd009c037985363e2290a0b579819f659db 705ab2a6b72e2cd86edb799ebe15f2695f86143e 0 {'date': '* *', 'user': 'test'} (glob) +#no produced by 2.3 +33d458d86621f3186c40bfccd77652f4a122743e 3734a65252e69ddcced85901647a4f335d40de1e 0 {'date': '* *', 'user': 'test'} (glob) + +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: 1[46]:50f11e5e3a63 tip (re) + add obsolet_conflicting_d + branch: default + commit: (clean) + update: (2|9|11) new changesets, (3|9|10) branch heads \(merge\) (re) + bumped: 1 changesets + $ hg debugobsolete `getid a7a6f2b5d8a5` `getid 50f11e5e3a63` + $ hg log -r 'conflicting()' + changeset: 12:6db5e282cb91 + parent: 10:2033b4e49474 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add obsol_d''' + + changeset: 16:50f11e5e3a63 + tag: tip + parent: 11:9468a5f5d8b2 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add obsolet_conflicting_d + + + $ hg up --hidden 3 -q + working directory parent is obsolete! + $ hg evolve + parent is obsolete with multiple successors: + [4] add obsol_c' + [10] add obsol_c + [2] + $ hg olog + changeset: 2:4538525df7e2 + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: add c + + +Check import reports new unstable changeset: + + $ hg up --hidden 2 + 1 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory parent is obsolete! + $ hg export 9468a5f5d8b2 | hg import - + applying patch from stdin + 1 new unstable changesets diff -r ad2060da7ffa -r f49d4774b999 tests/test-oldconvert.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-oldconvert.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,114 @@ + $ cat >> $HGRCPATH < [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 debugobsolete + 7c3bad9141dcb46ff89abf5f61856facd56e476c d67cd0334eeecfded222fed9009f0db4beb57585 0 {'date': '* *', 'user': 'test'} (glob) + $ 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 ", + > "date": [ + > 1336503323.9768269, + > -7200 + > ], + > "object": "3e03d82708d4da97a92158558dd13386d8f09ad5", + > "id": "4743f676eaf3923cb98c921ee06b2e91052c365b" + > }, + > { + > "reason": "import from older format.", + > "user": "Pierre-Yves David ", + > "date": [ + > 1336557472.7875929, + > -7200 + > ], + > "object": "5c722672795c3a2cb94d0cc9a821c394c1475f87", + > "id": "1fd90a84b7225d2e3062b7e1b3100aa2e060fc72" + > }, + > { + > "reason": "import from older format.", + > "subjects": [ + > "0000000000000000000000000000000000000000" + > ], + > "user": "Pierre-Yves David ", + > "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 debugobsolete + 7c3bad9141dcb46ff89abf5f61856facd56e476c d67cd0334eeecfded222fed9009f0db4beb57585 0 {'date': '* *', 'user': 'test'} (glob) + 3e03d82708d4da97a92158558dd13386d8f09ad5 3218406b50ed13480765e7c260669620f37fba6e 0 {'date': '* *', 'user': 'Pierre-Yves David '} (glob) + 5c722672795c3a2cb94d0cc9a821c394c1475f87 0 {'date': '* *', 'user': 'Pierre-Yves David '} (glob) + 2c3784e102bb34ccc93862af5bd6d609ee30c577 0 {'date': '* *', 'user': 'Pierre-Yves David '} (glob) diff -r ad2060da7ffa -r f49d4774b999 tests/test-prune.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-prune.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,258 @@ + $ cat >> $HGRCPATH < [ui] + > logtemplate={rev}:{node|short}[{bookmarks}] ({obsolete}/{phase}) {desc|firstline}\n + > [extensions] + > hgext.rebase= + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ mkcommit() { + > echo "$1" > "$1" + > hg add "$1" + > hg ci -m "add $1" + > } + + $ hg init repo + $ cd repo + $ mkcommit a + $ hg phase --public . + $ mkcommit b + $ mkcommit c + $ mkcommit d + $ mkcommit e + $ hg bookmarks BABAR + $ hg log -G + @ 4:9d206ffc875e[BABAR] (stable/draft) add e + | + o 3:47d2a3944de8[] (stable/draft) add d + | + o 2:4538525df7e2[] (stable/draft) add c + | + o 1:7c3bad9141dc[] (stable/draft) add b + | + o 0:1f0dee641bb7[] (stable/public) add a + + +Check simple case +---------------------------- + +prune current and tip changeset + + $ hg prune --user blah --date '1979-12-15' . + 1 changesets pruned + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory now at 47d2a3944de8 + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + +prune leaving unstability behind + + $ hg prune 1 + 1 changesets pruned + 2 new unstable changesets + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + +pruning multiple changeset at once + + $ hg prune 2: + 2 changesets pruned + 0 files updated, 0 files merged, 3 files removed, 0 files unresolved + working directory now at 1f0dee641bb7 + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0 {'date': '*', 'user': 'test'} (glob) + 47d2a3944de8b013de3be9578e8e344ea2e6c097 0 {'date': '*', 'user': 'test'} (glob) + +cannot prune public changesets + + $ hg prune 0 + abort: cannot prune immutable changeset: 1f0dee641bb7 + (see "hg help phases" for details) + [255] + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0 {'date': '*', 'user': 'test'} (glob) + 47d2a3944de8b013de3be9578e8e344ea2e6c097 0 {'date': '*', 'user': 'test'} (glob) + +Check successors addition +---------------------------- + + $ mkcommit bb + $ mkcommit cc + $ mkcommit dd + $ mkcommit ee + $ hg up 0 + 0 files updated, 0 files merged, 4 files removed, 0 files unresolved + $ mkcommit nB + created new head + $ mkcommit nC + $ mkcommit nD + $ mkcommit nE + + $ hg log -G + @ 12:6e8148413dd5[] (stable/draft) add nE + | + o 11:8ee176ff1d4b[] (stable/draft) add nD + | + o 10:aa96dc3f04c2[] (stable/draft) add nC + | + o 9:6f6f25e4f748[] (stable/draft) add nB + | + | o 8:bb5e90a7ea1f[] (stable/draft) add ee + | | + | o 7:00ded550b1e2[] (stable/draft) add dd + | | + | o 6:354011cd103f[] (stable/draft) add cc + | | + | o 5:814c38b95e72[] (stable/draft) add bb + |/ + o 0:1f0dee641bb7[BABAR] (stable/public) add a + + +one old, one new + + $ hg prune 'desc("add ee")' -s 'desc("add nE")' + 1 changesets pruned + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0 {'date': '*', 'user': 'test'} (glob) + 47d2a3944de8b013de3be9578e8e344ea2e6c097 0 {'date': '*', 'user': 'test'} (glob) + bb5e90a7ea1f3b4b38b23150a4a597b6146d70ef 6e8148413dd541855b72a920a90c06fca127c7e7 0 {'date': '*', 'user': 'test'} (glob) + $ hg log -G + @ 12:6e8148413dd5[] (stable/draft) add nE + | + o 11:8ee176ff1d4b[] (stable/draft) add nD + | + o 10:aa96dc3f04c2[] (stable/draft) add nC + | + o 9:6f6f25e4f748[] (stable/draft) add nB + | + | o 7:00ded550b1e2[] (stable/draft) add dd + | | + | o 6:354011cd103f[] (stable/draft) add cc + | | + | o 5:814c38b95e72[] (stable/draft) add bb + |/ + o 0:1f0dee641bb7[BABAR] (stable/public) add a + + +one old, two new + + $ hg prune 'desc("add dd")' -s 'desc("add nD")' -s 'desc("add nC")' + 1 changesets pruned + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0 {'date': '*', 'user': 'test'} (glob) + 47d2a3944de8b013de3be9578e8e344ea2e6c097 0 {'date': '*', 'user': 'test'} (glob) + bb5e90a7ea1f3b4b38b23150a4a597b6146d70ef 6e8148413dd541855b72a920a90c06fca127c7e7 0 {'date': '*', 'user': 'test'} (glob) + 00ded550b1e28bba454bd34cec1269d22cf3ef25 aa96dc3f04c2c2341fe6880aeb6dc9fbffff9ef9 8ee176ff1d4b2034ce51e3efc579c2de346b631d 0 {'date': '**', 'user': 'test'} (glob) + $ hg log -G + @ 12:6e8148413dd5[] (stable/draft) add nE + | + o 11:8ee176ff1d4b[] (stable/draft) add nD + | + o 10:aa96dc3f04c2[] (stable/draft) add nC + | + o 9:6f6f25e4f748[] (stable/draft) add nB + | + | o 6:354011cd103f[] (stable/draft) add cc + | | + | o 5:814c38b95e72[] (stable/draft) add bb + |/ + o 0:1f0dee641bb7[BABAR] (stable/public) add a + + +two old, two new (should be denied) + + $ hg prune 'desc("add cc")' 'desc("add bb")' -s 'desc("add nD")' -s 'desc("add nC")' + abort: Can't use multiple successors for multiple precursors + [255] + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0 {'date': '*', 'user': 'test'} (glob) + 47d2a3944de8b013de3be9578e8e344ea2e6c097 0 {'date': '*', 'user': 'test'} (glob) + bb5e90a7ea1f3b4b38b23150a4a597b6146d70ef 6e8148413dd541855b72a920a90c06fca127c7e7 0 {'date': '*', 'user': 'test'} (glob) + 00ded550b1e28bba454bd34cec1269d22cf3ef25 aa96dc3f04c2c2341fe6880aeb6dc9fbffff9ef9 8ee176ff1d4b2034ce51e3efc579c2de346b631d 0 {'date': '**', 'user': 'test'} (glob) + +two old, one new: + + $ hg prune 'desc("add cc")' 'desc("add bb")' -s 'desc("add nB")' + 2 changesets pruned + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0 {'date': '*', 'user': 'test'} (glob) + 47d2a3944de8b013de3be9578e8e344ea2e6c097 0 {'date': '*', 'user': 'test'} (glob) + bb5e90a7ea1f3b4b38b23150a4a597b6146d70ef 6e8148413dd541855b72a920a90c06fca127c7e7 0 {'date': '*', 'user': 'test'} (glob) + 00ded550b1e28bba454bd34cec1269d22cf3ef25 aa96dc3f04c2c2341fe6880aeb6dc9fbffff9ef9 8ee176ff1d4b2034ce51e3efc579c2de346b631d 0 {'date': '**', 'user': 'test'} (glob) + 814c38b95e72dfe2cbf675b1649ea9d780c89a80 6f6f25e4f748d8f7571777e6e168aedf50350ce8 0 {'date': '*', 'user': 'test'} (glob) + 354011cd103f58bbbd9091a3cee6d6a6bd0dddf7 6f6f25e4f748d8f7571777e6e168aedf50350ce8 0 {'date': '*', 'user': 'test'} (glob) + +two old, two new with --biject + + $ hg up 0 + 0 files updated, 0 files merged, 4 files removed, 0 files unresolved + $ mkcommit n1 + created new head + $ mkcommit n2 + + $ hg prune 'desc("add n1")::desc("add n2")' -s 'desc("add nD")::desc("add nE")' --biject + 2 changesets pruned + 0 files updated, 0 files merged, 2 files removed, 0 files unresolved + working directory now at 1f0dee641bb7 + $ hg debugobsolete + 9d206ffc875e1bc304590549be293be36821e66c 0 {'date': '314064000 0', 'user': 'blah'} (glob) + 7c3bad9141dcb46ff89abf5f61856facd56e476c 0 {'date': '*', 'user': 'test'} (glob) + 4538525df7e2b9f09423636c61ef63a4cb872a2d 0 {'date': '*', 'user': 'test'} (glob) + 47d2a3944de8b013de3be9578e8e344ea2e6c097 0 {'date': '*', 'user': 'test'} (glob) + bb5e90a7ea1f3b4b38b23150a4a597b6146d70ef 6e8148413dd541855b72a920a90c06fca127c7e7 0 {'date': '*', 'user': 'test'} (glob) + 00ded550b1e28bba454bd34cec1269d22cf3ef25 aa96dc3f04c2c2341fe6880aeb6dc9fbffff9ef9 8ee176ff1d4b2034ce51e3efc579c2de346b631d 0 {'date': '**', 'user': 'test'} (glob) + 814c38b95e72dfe2cbf675b1649ea9d780c89a80 6f6f25e4f748d8f7571777e6e168aedf50350ce8 0 {'date': '* *', 'user': 'test'} (glob) + 354011cd103f58bbbd9091a3cee6d6a6bd0dddf7 6f6f25e4f748d8f7571777e6e168aedf50350ce8 0 {'date': '* *', 'user': 'test'} (glob) + cb7f8f706a6532967b98cf8583a81baab79a0fa7 8ee176ff1d4b2034ce51e3efc579c2de346b631d 0 {'date': '* *', 'user': 'test'} (glob) + 21b6f2f1cece8c10326e575dd38239189d467190 6e8148413dd541855b72a920a90c06fca127c7e7 0 {'date': '* *', 'user': 'test'} (glob) + +test hg prune -B bookmark +yoinked from test-mq-strip.t + + $ cd .. + $ hg init bookmarks + $ cd bookmarks + $ hg debugbuilddag '..<2.*1/2:m<2+3:c> $HGRCPATH < [ui] + > interactive=false + > merge=internal:merge + > [defaults] + > amend=-d "0 0" + > [merge-tools] + > touch.checkchanged=true + > touch.gui=true + > touch.args=babar + > [extensions] + > hgext.rebase= + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ safesed() { + > sed "$1" "$2" > `pwd`/sed.temp + > mv `pwd`/sed.temp "$2" + > } + +create a simple repo + + $ hg init repo + $ cd repo + $ cat << EOF > babar + > un + > deux + > trois + > quatre + > cinq + > EOF + $ hg add babar + $ hg commit -m "babar count up to five" + $ cat << EOF >> babar + > six + > sept + > huit + > neuf + > dix + > EOF + $ hg commit -m "babar count up to ten" + $ cat << EOF >> babar + > onze + > douze + > treize + > quatorze + > quinze + > EOF + $ hg commit -m "babar count up to fifteen" + + +proper behavior without conflict +---------------------------------- + + $ hg gdown + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + [1] babar count up to ten + $ safesed 's/huit/eight/' babar + $ hg diff + diff -r 9d5daf8bd956 babar + --- a/babar Thu Jan 01 00:00:00 1970 +0000 + +++ b/babar * (glob) + @@ -5,6 +5,6 @@ + cinq + six + sept + -huit + +eight + neuf + dix + $ hg amend + 1 new unstable changesets + $ hg evolve + move:[2] babar count up to fifteen + atop:[4] babar count up to ten + merging babar + $ hg resolve -l + $ hg log -G + @ changeset: 5:71c18f70c34f + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to fifteen + | + o changeset: 4:5977072d13c5 + | parent: 0:29ec1554cfaf + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to ten + | + o changeset: 0:29ec1554cfaf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: babar count up to five + + + +proper behavior with conflict using internal:merge +-------------------------------------------------- + + $ hg gdown + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + [4] babar count up to ten + $ safesed 's/dix/ten/' babar + $ hg diff + diff -r 5977072d13c5 babar + --- a/babar Thu Jan 01 00:00:00 1970 +0000 + +++ b/babar * (glob) + @@ -7,4 +7,4 @@ + sept + eight + neuf + -dix + +ten + $ hg amend + 1 new unstable changesets + $ hg evolve + move:[5] babar count up to fifteen + atop:[7] babar count up to ten + merging babar + warning: conflicts during merge. + merging babar incomplete! (edit conflicts, then use 'hg resolve --mark') + evolve failed! + fix conflict and run "hg evolve --continue" + abort: unresolved merge conflicts (see hg help resolve) + [255] + $ hg resolve -l + U babar + $ hg log -G + @ changeset: 7:e04690b09bc6 + | tag: tip + | parent: 0:29ec1554cfaf + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to ten + | + | @ changeset: 5:71c18f70c34f + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: babar count up to fifteen + | | + | x changeset: 4:5977072d13c5 + |/ parent: 0:29ec1554cfaf + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to ten + | + o changeset: 0:29ec1554cfaf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: babar count up to five + +(fix the conflict and continue) + + $ hg revert -r 5 --all + reverting babar + $ safesed 's/dix/ten/' babar + $ hg resolve --all -m + $ hg evolve --continue + grafting revision 5 + $ hg resolve -l + $ hg log -G + @ changeset: 8:1836b91c6c1d + | tag: tip + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to fifteen + | + o changeset: 7:e04690b09bc6 + | parent: 0:29ec1554cfaf + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to ten + | + o changeset: 0:29ec1554cfaf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: babar count up to five + +proper behavior with conflict using an external merge tools +----------------------------------------------------------- + + $ safesed 's/merge=.*/merge=touch/' $HGRCPATH + $ safesed 's/touch.gui=.*/touch.gui=false/' $HGRCPATH + $ hg gdown + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + [7] babar count up to ten + $ safesed 's/ten/zehn/' babar + $ hg diff + diff -r e04690b09bc6 babar + --- a/babar Thu Jan 01 00:00:00 1970 +0000 + +++ b/babar * (glob) + @@ -7,4 +7,4 @@ + sept + eight + neuf + -ten + +zehn + $ hg amend + 1 new unstable changesets + $ safesed 's/interactive=.*/interactive=true/' $HGRCPATH + $ HGMERGE=touch hg evolve < n + > EOF + move:[8] babar count up to fifteen + atop:[10] babar count up to ten + merging babar + output file babar appears unchanged + was merge successful (yn)? merging babar failed! + evolve failed! + fix conflict and run "hg evolve --continue" + abort: unresolved merge conflicts (see hg help resolve) + [255] + $ hg resolve -l + U babar + $ hg log -G + @ changeset: 10:b20d08eea373 + | tag: tip + | parent: 0:29ec1554cfaf + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to ten + | + | @ changeset: 8:1836b91c6c1d + | | user: test + | | date: Thu Jan 01 00:00:00 1970 +0000 + | | summary: babar count up to fifteen + | | + | x changeset: 7:e04690b09bc6 + |/ parent: 0:29ec1554cfaf + | user: test + | date: Thu Jan 01 00:00:00 1970 +0000 + | summary: babar count up to ten + | + o changeset: 0:29ec1554cfaf + user: test + date: Thu Jan 01 00:00:00 1970 +0000 + summary: babar count up to five + + $ cat babar + un + deux + trois + quatre + cinq + six + sept + eight + neuf + zehn diff -r ad2060da7ffa -r f49d4774b999 tests/test-stabilize-order.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-stabilize-order.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,171 @@ + $ cat >> $HGRCPATH < [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ 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 unstable changesets + $ hg gdown + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [1] adda + $ echo a >> a + $ hg amend + 1 new unstable changesets + $ glog + @ 7:005fe5914f78@default(draft) adda + | + | o 5:22619daeed78@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 evolve -v + move:[5] addb + atop:[7] adda + hg rebase -r 22619daeed78 -d 005fe5914f78 + resolving manifests + getting b + b + $ glog + @ 8:bede829dd2d3@default(draft) addb + | + o 7:005fe5914f78@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 debugobsolete > successors.old + $ hg evolve -v + move:[3] addc + atop:[8] addb + hg rebase -r 7a7552255fb5 -d bede829dd2d3 + resolving manifests + getting b + resolving manifests + getting c + c + $ hg debugobsolete > successors.new + $ diff -u successors.old successors.new + --- successors.old* (glob) + +++ successors.new* (glob) + @@ -3,3 +3,4 @@ + 93418d2c0979643ad446f621195e78720edb05b4 005fe5914f78e8bc64c7eba28117b0b1fa210d0d 0 {'date': '* *', 'user': 'test'} (glob) + 7a7d76dc97c57751de9e80f61ed2a639bd03cd24 0 {'date': '* *', 'user': 'test'} (glob) + 22619daeed78036f80fbd326b6852519c4f0c25e bede829dd2d3b2ae9bf198c23432b250dc964458 0 {'date': '* *', 'user': 'test'} (glob) + +7a7552255fb5f8bd745e46fba6f0ca633a4dd716 65095d7d0dd5e4f15503bb7b1f433a5fe9bac052 0 {'date': '* *', 'user': 'test'} (glob) + [1] + + + + $ glog + @ 9:65095d7d0dd5@default(draft) addc + | + o 8:bede829dd2d3@default(draft) addb + | + o 7:005fe5914f78@default(draft) adda + | + o 0:c471ef929e6a@default(draft) addroot + + $ hg evolve -v + no troubled changesets + [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 unstable changesets + $ glog + @ 11:036cf654e942@default(draft) addb + | + | o 9:65095d7d0dd5@default(draft) addc + | | + | x 8:bede829dd2d3@default(draft) addb + |/ + o 7:005fe5914f78@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 evolve -v + nothing to evolve here + (1 troubled changesets, do you want --any ?) + [2] + $ hg evolve --any -v + move:[9] addc + atop:[11] addb + hg rebase -r 65095d7d0dd5 -d 036cf654e942 + resolving manifests + removing c + getting b + resolving manifests + getting c + c + $ glog + @ 12:e99ecf51c867@default(draft) addc + | + o 11:036cf654e942@default(draft) addb + | + o 7:005fe5914f78@default(draft) adda + | + o 0:c471ef929e6a@default(draft) addroot + + $ hg evolve --any -v + no troubled changesets + [1] diff -r ad2060da7ffa -r f49d4774b999 tests/test-stabilize-result.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-stabilize-result.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,330 @@ + $ cat >> $HGRCPATH < [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ glog() { + > hg glog --template \ + > '{rev}:{node|short}@{branch}({phase}) bk:[{bookmarks}] {desc|firstline}\n' "$@" + > } + +Test evolve removing the changeset being evolved + + $ 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 unstable changesets + $ hg evolve -v + move:[2] changea + atop:[4] changea + hg rebase -r cce2c55b8965 -d fb9d051ec0a4 + resolving manifests + $ glog --hidden + @ 4:fb9d051ec0a4@default(draft) bk:[changea] changea + | + | x 3:c5727dbded3c@default(draft) bk:[] temporary amend commit for 102a90ea7b4a + | | + | | x 2:cce2c55b8965@default(draft) bk:[] changea + | |/ + | x 1:102a90ea7b4a@default(draft) bk:[] addb + |/ + o 0:07f494440405@default(draft) bk:[] adda + + $ hg debugobsolete + 102a90ea7b4a3361e4082ed620918c261189a36a fb9d051ec0a450a4aa2ffc8c324979832ef88065 0 {'date': '* *', 'user': 'test'} (glob) + c5727dbded3c3a6877cf60d6bb552a76812cb844 0 {'date': '* *', 'user': 'test'} (glob) + cce2c55b896511e0b6e04173c9450ba822ebc740 0 {'date': '* *', 'user': 'test'} (glob) + +Test evolve with conflict + + $ ls + a + b + $ hg pdiff a + diff -r 07f494440405 a + --- a/a * (glob) + +++ b/a * (glob) + @@ -1,1 +1,2 @@ + a + +a + $ echo 'newer a' >> a + $ hg ci -m 'newer a' + $ hg gdown + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + [4] changea + $ echo 'a' > a + $ hg amend + 1 new unstable changesets + $ hg evolve + move:[5] newer a + atop:[7] changea + merging a + warning: conflicts during merge. + merging a incomplete! (edit conflicts, then use 'hg resolve --mark') + evolve failed! + fix conflict and run "hg evolve --continue" + abort: unresolved merge conflicts (see hg help resolve) + [255] + $ hg revert -r 'unstable()' a + $ hg diff + diff -r 66719795a494 a + --- a/a * (glob) + +++ b/a * (glob) + @@ -1,1 +1,3 @@ + a + +a + +newer a + $ hg evolve --continue + grafting revision 5 + abort: unresolved merge conflicts (see hg help resolve) + [255] + $ hg resolve -m a + $ hg evolve --continue + grafting revision 5 + +Stabilize of late comer with different parent +================================================== + +(the same parent case is handled in test-evolve.t) + + $ glog + @ 8:1cf0aacfd363@default(draft) bk:[] newer a + | + o 7:66719795a494@default(draft) bk:[changea] changea + | + o 0:07f494440405@default(draft) bk:[] adda + +Add another commit + + $ hg gdown + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + [7] changea + $ echo 'c' > c + $ hg add c + $ hg commit -m 'add c' + created new head + +Get a successors of 8 on it + + $ hg grab 8 + ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) + +Add real change to the successors + + $ echo 'babar' >> a + $ hg amend + +Make precursors public + + $ hg phase --hidden --public 8 + 1 new bumped changesets + $ glog + @ 12:(73b15c7566e9|d5c7ef82d003)@default\(draft\) bk:\[\] newer a (re) + | + o 9:7bc2f5967f5e@default(draft) bk:[] add c + | + | o 8:1cf0aacfd363@default(public) bk:[] newer a + |/ + o 7:66719795a494@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + +Stabilize ! + + $ hg evolve --any --dry-run + recreate:[12] newer a + atop:[8] newer a + hg rebase --rev (73b15c7566e9|d5c7ef82d003) --dest 66719795a494; (re) + hg update 1cf0aacfd363; + hg revert --all --rev (73b15c7566e9|d5c7ef82d003); (re) + hg commit --msg "bumped update to %s" (no-eol) + $ hg evolve --any + recreate:[12] newer a + atop:[8] newer a + rebasing to destination parent: 66719795a494 + computing new diff + commited as (a7cabd7bd9c2|671b9d7eeaec) (re) + $ glog + @ 14:(a7cabd7bd9c2|671b9d7eeaec)@default\(draft\) bk:\[\] bumped update to 1cf0aacfd363: (re) + | + | o 9:7bc2f5967f5e@default(draft) bk:[] add c + | | + o | 8:1cf0aacfd363@default(public) bk:[] newer a + |/ + o 7:66719795a494@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + +Stabilize conflicting changesets with same parent +================================================= + + $ rm a.orig + $ hg up 9 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cat << EOF >> a + > flore + > arthur + > zephir + > some + > less + > conflict + > EOF + $ hg ci -m 'More addition' + $ glog + @ 15:3932c176bbaa@default(draft) bk:[] More addition + | + | o 14:(a7cabd7bd9c2|671b9d7eeaec)@default\(draft\) bk:\[\] bumped update to 1cf0aacfd363: (re) + | | + o | 9:7bc2f5967f5e@default(draft) bk:[] add c + | | + | o 8:1cf0aacfd363@default(public) bk:[] newer a + |/ + o 7:66719795a494@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + $ echo 'babar' >> a + $ hg amend + $ hg up --hidden 15 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory parent is obsolete! + $ mv a a.old + $ echo 'jungle' > a + $ cat a.old >> a + $ rm a.old + $ hg amend + 2 new divergent changesets + $ glog + @ 19:eacc9c8240fe@default(draft) bk:[] More addition + | + | o 17:d2f173e25686@default(draft) bk:[] More addition + |/ + | o 14:(a7cabd7bd9c2|671b9d7eeaec)@default\(draft\) bk:\[\] bumped update to 1cf0aacfd363: (re) + | | + o | 9:7bc2f5967f5e@default(draft) bk:[] add c + | | + | o 8:1cf0aacfd363@default(public) bk:[] newer a + |/ + o 7:66719795a494@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + +Stabilize It + + $ hg evolve -qn --traceback + hg update -c eacc9c8240fe && + hg merge d2f173e25686 && + hg commit -m "auto merge resolving conflict between eacc9c8240fe and d2f173e25686"&& + hg up -C 3932c176bbaa && + hg revert --all --rev tip && + hg commit -m "`hg log -r eacc9c8240fe --template={desc}`"; + $ hg evolve -v + merge:[19] More addition + with: [17] More addition + base: [15] More addition + merging divergent changeset + resolving manifests + merging a + 0 files updated, 1 files merged, 0 files removed, 0 files unresolved + amending changeset eacc9c8240fe + a + copying changeset 283ccd10e2b8 to 7bc2f5967f5e + a + committed changeset 21:f344982e63c4 + $ hg st + $ glog + @ 21:f344982e63c4@default(draft) bk:[] More addition + | + | o 14:(a7cabd7bd9c2|671b9d7eeaec)@default\(draft\) bk:\[\] bumped update to 1cf0aacfd363: (re) + | | + o | 9:7bc2f5967f5e@default(draft) bk:[] add c + | | + | o 8:1cf0aacfd363@default(public) bk:[] newer a + |/ + o 7:66719795a494@default(public) bk:[changea] changea + | + o 0:07f494440405@default(public) bk:[] adda + + $ hg summary + parent: 21:f344982e63c4 tip + More addition + branch: default + commit: (clean) + update: 2 new changesets, 2 branch heads (merge) + $ hg export . + # HG changeset patch + # User test + # Date 0 0 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID f344982e63c462b1e44c0371c804685389e673a9 + # Parent 7bc2f5967f5e4ed277f60a89b7b04cc5d6407ced + More addition + + diff -r 7bc2f5967f5e -r f344982e63c4 a + --- a/a Thu Jan 01 00:00:00 1970 +0000 + +++ b/a Thu Jan 01 00:00:00 1970 +0000 + @@ -1,1 +1,9 @@ + +jungle + a + +flore + +arthur + +zephir + +some + +less + +conflict + +babar + +Check conflicting during conflicting resolution +------------------------------------------------- + + $ hg up --hidden 15 + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory parent is obsolete! + $ echo 'gotta break' >> a + $ hg amend + 2 new divergent changesets + $ hg phase 'divergent()' + 21: draft + 23: draft + $ hg evolve -qn + hg update -c 36e188246d67 && + hg merge f344982e63c4 && + hg commit -m "auto merge resolving conflict between 36e188246d67 and f344982e63c4"&& + hg up -C 3932c176bbaa && + hg revert --all --rev tip && + hg commit -m "`hg log -r 36e188246d67 --template={desc}`"; + $ hg evolve + merge:[23] More addition + with: [21] More addition + base: [15] More addition + merging a + warning: conflicts during merge. + merging a incomplete! (edit conflicts, then use 'hg resolve --mark') + 0 files updated, 0 files merged, 0 files removed, 1 files unresolved + use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon + abort: Merge conflict between several amendments, and this is not yet automated + (/!\ You can try: + /!\ * manual merge + resolve => new cset X + /!\ * hg up to the parent of the amended changeset (which are named W and Z) + /!\ * hg revert --all -r X + /!\ * hg ci -m "same message as the amended changeset" => new cset Y + /!\ * hg kill -n Y W Z + ) + [255] diff -r ad2060da7ffa -r f49d4774b999 tests/test-touch.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-touch.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,88 @@ + + $ cat >> $HGRCPATH < [ui] + > logtemplate={rev}:{node|short} {desc}\n + > [defaults] + > amend=-d "0 0" + > [extensions] + > hgext.rebase= + > EOF + $ echo "evolve=$(echo $(dirname $TESTDIR))/hgext/evolve.py" >> $HGRCPATH + + $ hg init repo + $ cd repo + $ echo A > a + $ hg add a + $ hg commit -m a + +Basic usage + + $ hg log -G + @ 0:e93df3427f45 a + + $ hg touch . + $ hg log -G + @ 1:[0-9a-f]{12} a (re) + + + +Revive usage + + $ echo A > b + $ hg add b + $ hg commit -m ab --amend + $ hg up --hidden 1 + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + working directory parent is obsolete! + $ hg log -G + o 3:[0-9a-f]{12} ab (re) + + @ 1:[0-9a-f]{12} a (re) + + $ hg touch . + 2 new divergent changesets + $ hg log -G + @ 4:[0-9a-f]{12} a (re) + + o 3:[0-9a-f]{12} ab (re) + + $ hg prune 3 + 1 changesets pruned + +Duplicate + + $ hg touch --duplicate . + $ hg log -G + @ 5:[0-9a-f]{12} a (re) + + o 4:[0-9a-f]{12} a (re) + + +Multiple touch + + $ echo C > c + $ hg add c + $ hg commit -m c + $ echo D > d + $ hg add d + $ hg commit -m d + $ hg log -G + @ 7:[0-9a-f]{12} d (re) + | + o 6:[0-9a-f]{12} c (re) + | + o 5:[0-9a-f]{12} a (re) + + o 4:[0-9a-f]{12} a (re) + + $ hg touch 6:7 + $ hg log -G + @ 9:[0-9a-f]{12} d (re) + | + o 8:[0-9a-f]{12} c (re) + | + o 5:[0-9a-f]{12} a (re) + + o 4:[0-9a-f]{12} a (re) + + diff -r ad2060da7ffa -r f49d4774b999 tests/test-tutorial.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-tutorial.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,833 @@ + +Initial setup +------------- + +This Mercurial configuration example is used for testing. +.. Various setup + + $ cat >> $HGRCPATH << EOF + > [ui] + > # This is change the default output of log for clear tutorial + > logtemplate ="{node|short} ({phase}): {desc}\n" + > [diff] + > # use "git" diff format, clearer and smarter format + > 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 + > # (this tutorial is automatically tested.) + > amend = amend -d '0 0' + > 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 uses the following configuration for Mercurial: + +A compact log template with phase data: + + $ hg showconfig ui | grep log + ui.logtemplate="{node|short} ({phase}): {desc}\n" + +Improved git format diff: + + $ hg showconfig diff + diff.git=1 + +And of course, we enable the experimental extensions for mutable history: + + $ cat >> $HGRCPATH < [extensions] + > evolve = $TESTDIR/../hgext/evolve.py + > # enabling rebase is also needed for now + > rebase = + > EOF + +----------------------- +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 log -G + @ 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 + # Thu Jan 01 00:00:00 1970 +0000 + # 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 it has not been exchanged with +the outside. The first one has been exchanged and is "public" (immutable). + + $ hg log -G + @ d85de4546133 (draft): adding fruit + | + o 4d5dc8187023 (draft): adding condiment + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +hopefully. I can use `hg commit --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 commit --amend + +A new changeset with the right diff replace the wrong one. + + $ hg log -G + @ 9d0363b81950 (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 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID 9d0363b81950646bc6ad1ec5de8b8197ea586541 + # 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 rid of branchy history +---------------------------------- + +While I was working on my list. someone 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 + | + | @ 9d0363b81950 (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 --dest 9ca060c80d74 --source 4d5dc8187023 + merging shopping + merging shopping + + +My local work is now rebased on the remote one. + + $ hg log -G + @ 41aff6a42b75 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Removing changesets +------------------------ + +I add new item to my list + + $ cat >> shopping << EOF + > car + > bus + > plane + > boat + > EOF + $ hg ci -m 'transport' + $ hg log -G + @ 1125e39fbf21 (draft): transport + | + o 41aff6a42b75 (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 changesets pruned + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory now at 41aff6a42b75 + +The silly changeset is gone. + + $ hg log -G + @ 41aff6a42b75 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +Reordering changesets +------------------------ + + +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 + @ fac207dec9f5 (draft): SPAM SPAM + | + o 10b8aeaa8cc8 (draft): bathroom stuff + | + o 41aff6a42b75 (draft): adding fruit + | + o dfd3a2d7691e (draft): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +.. note:: We can't amend changeset 7e82d3f3c2cb or 9ca060c80d74 as they are immutable. + + I now want to push to remote all my changes except the bathroom one, which I'm + not totally happy with yet. To be able to push "SPAM SPAM" I need a version of + "SPAM SPAM" which is not a child of "bathroom stuff" + +You can use the 'grab' alias for that. + +.. note: grab is an alias for `hg rebase --dest . --rev ; hg up ` + + $ hg up 'p1(10b8aeaa8cc8)' # going on "bathroom stuff" parent + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ hg grab fac207dec9f5 # moving "SPAM SPAM" to the working directory parent + merging shopping + ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) + $ hg log -G + @ a224f2a4fb9f (draft): SPAM SPAM + | + | o 10b8aeaa8cc8 (draft): bathroom stuff + |/ + o 41aff6a42b75 (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 # enough 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 + # Thu Jan 01 00:00:00 1970 +0000 + # Node ID a224f2a4fb9f9f828f608959912229d7b38b26de + # Parent 41aff6a42b7578ec7ec3cb2041633f1ca43cca96 + 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 10b8aeaa8cc8 + +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 sake we get the bathroom change in line again + + $ hg grab 10b8aeaa8cc8 + merging shopping + ? files updated, 0 files merged, 0 files removed, 0 files unresolved (glob) + $ hg phase --draft . + $ hg log -G + @ 75954b8cd933 (draft): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + + +Splitting change +------------------ + +This part is not written yet, but you can use either the `histedit` extension +of the `uncommit` command to splitting a change. + + $ hg help uncommit + hg uncommit [OPTION]... [NAME] + + 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. + + options: + + -a --all uncommit all changes when no arguments given + -I --include PATTERN [+] include names matching the given patterns + -X --exclude PATTERN [+] exclude names matching the given patterns + + [+] marked option can be specified multiple times + + use "hg -v help uncommit" to show the global options + + +The edit command of histedit can be used to split changeset: + + +Collapsing change +------------------ + +The tutorial part is not written yet but can use `hg fold`: + + $ hg help fold + hg fold rev + + Fold multiple revisions into a single one + + Revision from your current working directory to the specified one are fold + as a new one replacing the other + + you can alternatively use --rev to explicitly specify revision to be fold + ignoring the current working directory parent. + + options: + + -r --rev VALUE [+] explicitly specify the full set of revision to fold + + [+] marked option can be specified multiple times + + use "hg -v help fold" to show the global options + + +----------------------- +Collaboration +----------------------- + + +sharing mutable changesets +---------------------------- + +To share mutable changesets with others, 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 75954b8cd933 (public): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (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. + +.. Warning: Rollback is actually a dangerous kind of internal command that is deprecated and should not be exposed to user. Please forget you read about it until someone fix this tutorial. + + $ hg rollback + repository tip rolled back to revision 4 (undo pull) + $ hg log -G + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (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 75954b8cd933 (draft): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (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 75954b8cd933 -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 75954b8cd933 -q + $ sed -i'' -e 's/... More bathroom stuff to come/Bath Robe/' shopping + $ hg commit --amend + $ hg log -G + @ a44c85f957d3 (draft): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (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 unstable changesets + + +The new changeset "animal" is based on an old changeset of "bathroom". You can +see both version showing up in the log. + + $ hg log -G + o bf1b0d202029 (draft): animals + | + | @ a44c85f957d3 (draft): bathroom stuff + | | + x | 75954b8cd933 (draft): bathroom stuff + |/ + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + +The older version 75954b8cd933 never ceased to exist in the local repo. It was +just hidden and excluded from pull and push. + +.. note:: In hgview there is a nice dotted relation highlighting a44c85f957d3 as a new version of 75954b8cd933. this is not yet ported to ``hg log -G``. + +There is now an **unstable** changeset in this history. Mercurial will refuse to +share it with the outside: + + $ hg push other + pushing to $TESTTMP/other + searching for changes + abort: push includes unstable changeset: bf1b0d202029! + (use 'hg evolve' to get a stable history or --force to ignore warnings) + [255] + + + + +To resolve this unstable state, you need to rebase bf1b0d202029 onto +a44c85f957d3. The `hg evolve` command will do this for you. + +It has a --dry-run option to only suggest the next move. + + $ hg evolve --dry-run + move:[15] animals + atop:[14] bathroom stuff + hg rebase -r bf1b0d202029 -d a44c85f957d3 + +Let's do it + + $ hg evolve + move:[15] animals + atop:[14] bathroom stuff + merging shopping + +The old version of bathroom is hidden again. + + $ hg log -G + @ ee942144f952 (draft): animals + | + o a44c85f957d3 (draft): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (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! + +now let's see where we are, and update to the successor + + $ hg parents + bf1b0d202029 (draft): animals + working directory parent is obsolete! + $ hg evolve + update:[8] animals + 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 99f039c5ec9e (draft): SPAM SPAM SPAM + | + @ ee942144f952 (draft): animals + | + o a44c85f957d3 (draft): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (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 ee942144f952 + 1 changesets pruned + 1 files updated, 0 files merged, 0 files removed, 0 files unresolved + working directory now at a44c85f957d3 + 1 new unstable 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 99f039c5ec9e (draft): SPAM SPAM SPAM + | + x ee942144f952 (draft): animals + | + @ a44c85f957d3 (draft): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + $ hg log -r 'unstable()' + 99f039c5ec9e (draft): SPAM SPAM SPAM + + $ hg evolve + move:[17] SPAM SPAM SPAM + atop:[14] bathroom stuff + merging shopping + + $ hg log -G + @ 40aa40daeefb (draft): SPAM SPAM SPAM + | + o a44c85f957d3 (draft): bathroom stuff + | + o a224f2a4fb9f (public): SPAM SPAM + | + o 41aff6a42b75 (public): adding fruit + | + o dfd3a2d7691e (public): adding condiment + | + o 9ca060c80d74 (public): SPAM + | + o 7e82d3f3c2cb (public): Monthy Python Shopping list + + + +Handling Divergent amend +---------------------------------------------- + +We can detect that multiple diverging/conflicting amendments have been made. +The `evolve` command can solve this situation. But all corner case are not +handled now. + +This section needs to be written. diff -r ad2060da7ffa -r f49d4774b999 tests/test-uncommit.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test-uncommit.t Thu Feb 20 12:56:57 2014 -0800 @@ -0,0 +1,335 @@ + $ cat >> $HGRCPATH < [extensions] + > hgext.rebase= + > hgext.graphlog= + > EOF + $ 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 debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) + +Test phase is preserved, no local changes + + $ hg up -C 3 --hidden + 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 divergent 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 debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob) + +Test --all + + $ hg up -C 3 --hidden + 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 divergent 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 debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c4cbebac3751269bdf12d1466deabcc78521d272 0 {'date': '* *', 'user': 'test'} (glob) + +Display a warning if nothing left + + $ hg uncommit e + new changeset is empty + (use "hg kill ." to remove it) + $ hg debugobsolete + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 e8db4aa611f6d5706374288e6898e498f5c44098 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c706fe2c12f83ba5010cb60ea6af3bd1f0c2d6d3 0 {'date': '* *', 'user': 'test'} (glob) + 5eb72dbe0cb409d094e3b4ae8eaa30071c1b8730 c4cbebac3751269bdf12d1466deabcc78521d272 0 {'date': '* *', 'user': 'test'} (glob) + c4cbebac3751269bdf12d1466deabcc78521d272 4f1c269eab68720f54e88ce3c1dc02b2858b6b89 0 {'date': '* *', 'user': 'test'} (glob) + +Test instability warning + + $ hg ci -m touncommit + $ echo unrelated > unrelated + $ hg ci -Am addunrelated unrelated + $ hg previous + 0 files updated, 0 files merged, 1 files removed, 0 files unresolved + [8] touncommit + $ hg uncommit aa + 1 new unstable changesets