bzr-loom-2.2.0/.testr.conf0000644000000000000000000000022511722461011013462 0ustar 00000000000000[DEFAULT] test_command=bzr selftest --subunit $IDOPTION $LISTOPT bzrlib.plugins.loom Loom test_id_option=--load-list $IDFILE test_list_option=--list bzr-loom-2.2.0/CONTRIBUTORS0000644000000000000000000000033711722461011013260 0ustar 00000000000000Robert Collins Scott James Remnant Aaron Bentley Rob Weir Jonathan Lange Gary Wilson Jr. bzr-loom-2.2.0/COPYING0000644000000000000000000004310511722461011012433 0ustar 00000000000000 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. bzr-loom-2.2.0/HOWTO0000644000000000000000000002262311722461011012225 0ustar 00000000000000Loom, a plugin for bzr to assist in developing focused patches. Copyright (C) 2006 Canonical Limited. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. 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 A quick guide to using looms ++++++++++++++++++++++++++++ Overview ======== Loom is a Bazaar plugin to assist in developing focused patches. It adds a 'loom' to a bzr branch. A loom allows the development of multiple patches at once, while still giving each patch a branch of its own. Installation ============ Loom is most easily installed by symlinking its source directory to ``~/.bazaar/plugins/loom``. Alternatively, you can install it for the entire machine using ``python setup.py --install``. You can verify that loom is installed by running ``bzr plugins`` and looking for loom in the output. Getting Started =============== 1. First, convert an upstream version branch into a loom. In this example, I call the upstream branch ``upstream``. In your upstream branch, run:: % bzr nick upstream % bzr loomify This will convert your branch to a loom - from this point forward the loom plugin is required to work on this branch. It will also create a thread called ``upstream``, which will act as a pristine branch to track the upstream code. The ``bzr nick`` is used to index into the threads in the loom. The current thread is the bzr nick for the branch and is recorded in commits as such. You can use ``bzr nick newname`` to change the name of a thread. If you ``bzr push`` to a new branch now, it will make the remote branch a loom, as well. However, if you push to an existing, normal bzr branch, then the current thread of your loom is what will be pushed. You can use this to publish individual threads for people that do not have the loom plugin. 2. Now you can create another thread, which will represent the first patch you are building on top of the upstream code:: % bzr create-thread "demo-patch" This creates a new thread, named ``demo-patch``, above your current thread and switches you onto the new thread. You can see which thread you are currently on using the ``show-loom`` command:: % bzr show-loom =>demo-patch upstream The output represents a stack of threads, with the ``=>`` marker indicating the current thread. At the top of the thread stack is the thread you are working towards, below that are the threads that it depends upon, and at the bottom is the thread that represents the upstream code being built upon. Working with the loom ===================== You can now work with the thread you are in just like a normal bzr branch - you can pull work from a normal branch into it, perform commits, uncommits, look at bzr log, etc. There are, however, additional commands that the loom plugin provides - they are covered in the following sections. For more information on them, use ``bzr help command-name``. Starting a new patch -------------------- When you make a new patch, you need to decide where in the loom it should go. Best practice is to put the thread for this new patch as close to the upstream as makes sense. For instance, if you are a Debian maintainer with the following loom:: =>debian configure-update compilation-fix upstream ...and you have received a bug report and patch that corrects a typographical error in the manual, you could put this in one of the existing threads - but none fit that well. You could put it above the ``debian`` thread, but it does not depend on the ``debian`` thread; however, the ``debian`` thread *does* depend on it, because the ``debian`` thread represents what you will be uploading to the distro. You could put it above ``configure-update``, above ``compilation-fix``, or right above ``upstream``. So, where should you put this new thread? If none of the threads are in the process of being merged into upstream, and this is something you plan to send upstream immediately, then the best place for this new thread is probably directly above ``upstream``. On the other hand, if the ``compilation-fix`` and ``configure-update`` threads are already in the process of being merged into upstream, then the best place for the new thread is directly above the ``configure-update`` thread. To create a new thread above the ``configure-update`` thread, use the ``down-thread`` command once to switch to the ``configure-update`` thread, and then invoke ``create-thread``:: % bzr down-thread % bzr create-thread documentation-fixes Now you can apply the patch and commit it:: % patch -0 < doco-fix.patch % bzr commit Updating to a new upstream version ---------------------------------- When upstream makes a new release, you need to bring their changes into your baseline thread - the bottom thread, and then merge the changes up through your loom to the top thread. 1. First, use ``down-thread`` repeatedly to move down to the bottom thread of the loom (this will be made easier in the future with an automatic mode of operation):: % bzr down-thread % bzr down-thread % bzr down-thread % bzr show-loom debian configure-update compilation-fix =>upstream 2. Next, pull the new upstream release. You can use ``bzr pull`` or ``bzr merge`` for this, depending on whether you want the ``upstream`` branch to be a clone of the upstream branch you are tracking, or to reflect the times that you have updated to upstream:: % bzr pull UPSTREAM-URL 3. Now, integrate the changes from upstream into your loom by moving up through your threads using the ``up-thread`` command. ``up-thread`` will automatically perform a merge and commit for each thread in your loom, moving up one thread at a time and stopping after it has committed to the top thread:: % bzr up-thread All changes applied successfully. Moved to thread 'compilation-fix'. Committing to: /demo Committed revision 140. All changes applied successfully. Moved to thread 'configure-update'. Committing to: /demo Committed revision 141. All changes applied successfully. Moved to thread 'debian'. Committing to: /demo Committed revision 142. If you would prefer to commit the change to each thread yourself, instead of letting ``up-thread`` perform this automatically, just specify the ``--manual`` flag. In this mode of operation, the merge will still happen automatically, but you'll have the opportunity to inspect the changes before committing them yourself. Continue using ``up-thread`` and committing until you've reached the top, or drop the ``--manual`` flag to automatically perform the merge and commit on the remaining threads:: % bzr up-thread --manual All changes applied successfully. Moved to thread 'compilation-fix'. % bzr diff % bzr st % bzr commit -m 'New upstream release.' Committing to: /demo Committed revision 140. % bzr up-thread All changes applied successfully. Moved to thread 'configure-update'. Committing to: /demo Committed revision 141. All changes applied successfully. Moved to thread 'debian'. Committing to: /demo Committed revision 142. 4. Once at the top, you are fully updated to upstream and you've adjusted every thread to the new release. This is a good time to record your loom, so that you can push a new release:: % bzr record "Update to 1.2." After upstream has merged your patch ------------------------------------ When you are performing an update to upstream and they have merged your patch, your thread will suddenly lose all its changes. Lets say in the example above that upstream has merged your changes in the ``configure-update`` thread. When you update to that thread, a call to ``diff -r below:`` will show no changes:: % bzr up-thread All changes applied successfully. Moved to thread 'configure-update'. % bzr diff -r thread: Because there are no changes to the thread below, this thread has been fully merged. Unless you are planning further configure changes, you don't need it in your stack anymore. You remove a thread using the ``combine-thread`` command. ``combine-thread`` is the reverse of ``create-thread`` - where ``create-thread`` makes a new thread above the current one, ``combine-thread`` combines the current thread into the one below it:: % bzr show-loom debian =>configure-update compilation-fix upstream % bzr combine-thread % bzr show-loom debian =>compilation-fix upstream Showing a single patch ---------------------- You can show a single patch without knowing the names of other threads by using the ``below:`` and ``thread:`` revision specifiers:: % bzr show-loom debian =>configure-update upstream % bzr diff -r below:configure-update..thread:configure-update bzr-loom-2.2.0/NEWS0000644000000000000000000001016611722461011012100 0ustar 00000000000000---------------------- bzr-loom Release Notes ---------------------- .. contents:: 2.2 === NOTES WHEN UPGRADING -------------------- * bzr-loom requires bzr 2.4.0 due to API changes in bzr. On older versions of bzr bzr-loom will still work for most operations but will fail when making new branches as part of a push or branch operation. (Robert Collins, #201613) CHANGES ------- * --auto is now the default on up-thread. You can supply a thread name to stop at a given thread, or --manual to go up a single thread. (Aaron Bentley) * ``bzr combine-thread`` now accepts a ``--force`` option. FEATURES -------- * A new revision specifier ``below:`` has been added. (Robert Collins, #195282) IMPROVEMENTS ------------ * bzr-loom is now compatible with bzr 2.3b5 and newer. There were some API additions bzr-loom needed to support. Compatibility with earlier versions is unaffected. (Andrew Bennetts) * Loom now takes advantage of lazy loading of bzr objects (though not to a complete degree), reducing the overhead of having it installed. (Robert Collins) * Loom now registers a ``bzr status`` hook rather than overriding the ``bzr status`` command. (Jelmer Vernooij) * Loom now checks that a compatible version of bzr is being used. (Jelmer Vernooij, #338214) BUGFIXES -------- * ``bzr combine-thread`` will no longer combine threads without ``--force`` when the thread being removed has work not merged into either the thread above or below. (Robert Collins, #506235) * ``bzr loomify`` explicitly checks that branches being converted are not Looms already. This should not have been needed, but apparently it was. (Robert Collins, #600452) * ``bzr nick`` will now rename a thread rather than setting the current thread pointer to an invalid value. (Robert Collins, #203203, #260947, #304608) * ``bzr nick`` will now rename the branch too. (Vincent Ladeuil, #606174) * ``switch`` now accepts the ``--directory`` option. (Vincent Ladeuil, #595563) * The ``thread:`` revision specifier will no longer throw an attribute error when used on a normal branch. (Robert Collins, #231283) * The ``bzr status`` hook for looms will no longer crash on non-workingtree trees. (Jelmer Vernooij, #904095) API BREAKS ---------- TESTING ------- INTERNALS --------- 2.1 === NOTES WHEN UPGRADING: CHANGES: FEATURES: IMPROVEMENTS: BUGFIXES: * Stop using APIs deprecated for 2.1.0 (child progress bars for merge and trace.info). (Vincent Ladeuil, #528472) * Work with changes to bzr trunk - colocated branches and switch -r. API BREAKS: TESTING: INTERNALS: * .testr.conf added to help use with testr - still need to specify what tests to run. (Robert Collins) 2.0 === NOTES WHEN UPGRADING: CHANGES: FEATURES: IMPROVEMENTS: * ``bzr status`` now shows the current thread of the loom. (Jonathan Lange, #232465) * ``bzr switch`` now accepts ``top:`` and ``bottom:`` to jump to the top and bottom thread respectively. (Jonathan Lange) * ``bzr switch -b newthread`` now works. (Robert Collins, #433811) * ``bzr push`` now pushes the last-loom rather than creating an empty loom. (Robert Collins, #201613) * ``up`` and ``down`` are now aliases for ``up-thread`` and ``down-thread`` respectively. * ``up-thread`` now notifies when a thread becomes empty. This is a step towards removing it automatically/prompting to do so. (James Westby, #195133) BUGFIXES: * ``pull`` expects the keywork local. (Mark Lee, #379347) * ``setup.py`` doesn't actually install. (Mark Lee, #379069) * module has no attribute ``PushResult``. (Robert Collins) API BREAKS: TESTING: INTERNALS: 1.3 === IMPROVEMENTS: * New command ``export-loom`` allows exporting the loom threads to individual branches. (Aaron Bentley) * Running loom command on non-loom branches will now give a clean error rather than a traceback. (Rob Weir) * ``up-thread`` is now significantly faster by using more modern bzrlib methods. (Aaron Bentley) * ``up-thread`` now accepts merge type parameters such as ``--lca``. (Aaron Bentley) bzr-loom-2.2.0/README0000644000000000000000000001001111722461011012246 0ustar 00000000000000Loom, a plugin for bzr to assist in developing focused patches. Copyright (C) 2006, 2007, 2008 Canonical Limited. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. 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 The bzr loom plugin +++++++++++++++++++ Introduction ============ Loom is a Bazaar plugin that helps you to manage and develop patches. It is similar to tools such as Quilt, Stacked Git and Mercurial Queues. However, it also gives you the full power of version control when managing your stack of patches. This makes collaboration just as easy as if you were working with a normal Bazaar branch. Using Loom, you can roll back, annotate, push and pull the stack you're building. The name "Loom" is appropriate because the tool works in two dimensions: * Up and down: similar to Quilt, Stacked Git and Mercurial Queues, allowing you to manage a stack of patches. * Time: Loom records the changes to your stack of patches over time, just like the version history of a standard Bazaar branch. This lets you push, pull and merge your stack with other developers. That version history of each patch is a thread running through your loom. Commands ======== * loomify: prepares a standard Bazaar branch for use as a loom. It converts the branch's format so you must have the Loom plugin to work on the branch. * create-thread: adds a new thread, with the supplied name, to the loom and makes that the current thread. * record: similar to a standard bzr commit, this records the current stack of patches into the loom's version history, allowing it to be pushed, pulled and merged. * revert-loom: revert all changes in the current stack of patches to the last recorded version. * show-loom: list the threads in the loom. In future, this will also show the number of commits in each thread. * down-thread: start working on the next thread down so that commits and merges affect the newly selected thread. * up-thread: start working on the next thread up. Any changes in the current thread will be merged, ready to commit. * combine-thread: combine the current thread with the thread below it. If the current thread is the last one, this will produce an error message. In future, it will turn the loom into a normal branch. The existing Bazaar command 'push' and 'branch' will create a new loom branch from an existing loom. Pushing a loom into a standard branch will not convert it. Instead, it will push the current thread into the standard branch. Similarly, pulling from a standard branch will pull into the current thread of a loom. This assymetry makes it easy to work with developers who are not using looms. Loom also adds new revision specifiers 'thread:' and 'below:'. You can use these to diff against threads in the current Loom. For instance, 'bzr diff -r thread:' will show you the different between the thread below yours, and your thread. See ``bzr help revisionspec`` for the detailed help on these two revision specifiers. Documentation ============= The HOWTO provides a small but growing manual for Loom. Internals ========= XXX: In development Why we dont use the revision_id to index into the loom: # XXX: possibly we should use revision_id to determine what the # loom looked like when it was committed, rather than taking a # revision id in the loom branch. This suggests recording the # loom revision when writing a commit to a warp, which would mean # that commit() could not do record() - we would have to record # a loom revision that was not yet created. bzr-loom-2.2.0/TODO0000644000000000000000000001605511722461011012074 0ustar 00000000000000loom format2 - based on branch6 commands to make: - l-commit: 'record' has been written as a primitive to record a new patch. ensures nick in loom, record tree into loom branch for nick, commit into the branch of the nick in the loom and pull that onto the tree, with --overwrite. - push - pushes loom, checks for loom tip correctness. - eject - remove a entry from the loom, and merge it out of the next one up. - unloomify - remove the loom branch reference. - revert loom-merge - diff - status teach launchpad about looms. this fixes the first-push-problem of new branches with bzr for package management because it means one loom per package, not one branch per change. Create a 'bzr help loom' command to give a good overview. 'bzr check' on a loom branch should check the loom is consistent with the branch. rename threads to 'warps' - the threads held under tension by the loom. Modelled on a ground loom with the supports to the left and right - old and new - of the weaver. possibly: override the nick property on the loom to do a push/pop etc as needed. teach up-thread to use '<<< THREADNAME' rather than '<<< MERGE-SOURCE' cut-warp ? eject - whatever - should refuse to eject the last one, or perhaps should unloom at that point. push/branch should not set an explicit nick if there are no threads in the loom normal branch.pull (loom) gets current warp. (default contract ensures this) loom.pull(normal branch) pulls into current warp. (default contract ensures this) loom.pull(loom) starts at the lowest warp and pulls matching warps until a conflict occurs. Optimised by pulling the union of: * current rh-tip and thread rev ids. LoomTreeDecorator has too much UI code - it raises the wrong exceptions and does note calls. - make merge do something sane... XXX: Currently we do not handle any new warps in the source: they are skipped over deleted warps in this loom: they are treated as new warps in the source renamed warps: threated as new warps in the source. deleted warps in the source: they remain in this loom, though the content removals in the remote loom which have been propogated are propogated here. - do merge of a loom logic. - teach uncommit to uncommit the loom *IF appropriate*. - make pull and push print something sensible rather than revisions pushed. - perhaps a 'change descrption' object that the pull method can return. - make pull BRANCH#warp work to grab one element of a warp. In a warp this should attempt to pull the lower warp elements first?, then just pull the warp into this warp. A reason to grab the lower warp elements is to preserve the right delta between the lower warp elements. A reason not to, is that the 3-way merge will do the right thing anyway, and there is no need to require my warp to have all the subcomponents of yours when I just want the resulting patch as a component of my warp. If your baseline warp is ahead of mine, I'll just get no-op updates. - record loom with merges to record a merge - record loom with conflicts to refuce to commit - pull into a 'new loom' without error or warning from an existing loom. - ' merge into a loom to create combined loom.' - include basis and parent revids in current-loom for each warp. - revert loom falls back to the next lower thread. - factor our LoomParsing etc into a helper class. - test for revert -thread when the thread stays present. - revert thread puts you on the next up remaining thread when the current thread goes awol if there are up-threads to go to. - want to be able to say 'here is a partial loom, now change it' - want to be able to say 'here is a loom stream, read it' - want to be able to say 'here are some threads, write a loom' - want to be able to say 'here are some parents, write a current-loom' - want to be able to say 'here is a current-loom, want to update it.' - cache loom state objects in the transaction entity cache [probably a bad idea RBC 20080120] - TODO: a merge of a no-change commit should be allowed? - disallow pull with a modified current loom, or do a merge during pull. - raise clear errors on corrupt loom-state. - lazy use of LoomStateReader in LoomState? - LoomState to enforce valid revisions in set_parents etc - no \n or whitespace. - bug: up-thread incorrectly sets pending merge when the thread is already merged. - UI question - show a marker on all 'applied' threads. i.e. a 'empire state besides the threads list'. - revert-loom thread did not reset branch status correctly in the tree. - bug: up-thread with an out of date tree fails with BzrCommandError not bound. - bzr loomify && bzr revert-loom should preserve revision history. - nice errors for loom branch commands on non-loom branches. - better offset management for adjust_current_index. - nice 'split diff into branches' tool - diff -r thread: to diff against lower thread (using -ancestry logic) (-r thread: seems best placed to report on the threads actual revision id; this TODO either means changing 'diff's defaults, adding a flag to diff, or using a different revspec prefix; or something like that.) - export-patch to export the diff from this thread to the lower thread (using -ancestry logic) to a file named as the warp is named. (what about / ?) - during up-thread, if we could pull or if there is no diff, then the thread has been merged, offer to remove it. (Currently suggests to remove it). - loom to have the same 'tree root id' as its branches, to allow nested looms by reference. EEK!. - show-loom to allow -r -1. - combine-thread to warn if the thread being combined has changes not present in the one below it. I.e. by ancestry, or by doing a merge and recording differences. For bonus points, do the merge, but record the lower thread as the last-revision in the tree still, and set no pending-merges. This preserves the difference whilst still combining the threads. - revert-thread on a combined or ejected thread should do something reasonable. - branch.remove_thread needs testing for cases: no such thread, thread is the current thread. - record_thread should allow a record to happen even if the thread is not changed IF the parents list is different, not counting None entries. - pull should not throwaway local commits - they should get preserved in some manner. i.e. if the revision id in last-loom != that in the basis loom, it should become a parent in last-loom. This implies a variable length list of parents for every line - which is catered for. - remove thunks to NULL_REVISION to be EMPTY_REVISION once bzrlib supports that. - test revision spec with non loom branches. - print a message 'pushing thread to branch' when doing a push/pull from a normal branch. - fetch should examine *every* fetched loom revision and fetch the contents therein as well, to support uncommit between record calls. - combine-thread should de-dup penmding merges (use case: up-thread finds a fully merged thread so there are pending merges but no diff between threads; this is when combine-thread is often called). - support tags on push/pull in looms - perhaps bzr send should send the whole loom ? (e.g. as a patch bomb - a series of patches?) bzr-loom-2.2.0/__init__.py0000644000000000000000000001103111722461011013502 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # from __future__ import absolute_import """Loom is a bzr plugin which adds new commands to manage a loom of patches. Loom adds the following new commands: * loomify: This converts a branch into a loom enabled branch. As a result of this, the branch format is converted and you need to have the loom plugin installed to use it after that. The current branch nickname becomes the base thread in the loom. * create-thread: This adds a new thread to the loom with the supplied name and positions the branch on the new thread. * record: Perform a commit of the loom - record the current stack of patches into history, allowing it to be pushed, pulled and merged. * revert-loom: Revert all change in the current stack of patches to the last recorded one. * show-loom: Shows the threads in the loom. It currently does not show the # of commits in each thread, but it is planned to do that in the future. * down-thread: Move the branch down a thread. After doing this commits and merges in this branch will affect the newly selected thread. * up-thread: Move the branch up a thread. This will merge in all the changes from the current thread that are not yet integrated into the new thread into it and leave you ready to commit them. * combine-thread: Combine the current thread with the thread below it. If It is the last thread, this will currently refuse to operate, but in the future will just turn the loom into a normal branch again. Use this command to remove a thread which has been merged into upstream. Loom also adds new revision specifiers 'thread:' and 'below:'. You can use these to diff against threads in the current Loom. For instance, 'bzr diff -r thread:' will show you the different between the thread below yours, and your thread. See ``bzr help revisionspec`` for the detailed help on these two revision specifiers. """ from bzrlib.plugins.loom.version import ( bzr_plugin_version as version_info, bzr_minimum_version, ) import bzrlib import bzrlib.api bzrlib.api.require_api(bzrlib, bzr_minimum_version) import bzrlib.builtins import bzrlib.commands from bzrlib.plugins.loom import ( commands, formats, ) for command in [ 'combine_thread', 'create_thread', 'down_thread', 'export_loom', 'loomify', 'record', 'revert_loom', 'show_loom', 'up_thread', ]: bzrlib.commands.plugin_cmds.register_lazy('cmd_' + command, [], 'bzrlib.plugins.loom.commands') # XXX: bzr fix needed: for switch, we have to register directly, not # lazily, because register_lazy does not stack in the same way register_command # does. if not hasattr(bzrlib.builtins, "cmd_switch"): # provide a switch command (allows bzrlib.commands.register_command(getattr(commands, 'cmd_switch')) else: commands.cmd_switch._original_command = bzrlib.commands.register_command( getattr(commands, 'cmd_switch'), True) from bzrlib.hooks import install_lazy_named_hook def show_loom_summary(params): branch = getattr(params.new_tree, "branch", None) if branch is None: # Not a working tree, ignore return try: formats.require_loom_branch(branch) except formats.NotALoom: return params.to_file.write('Current thread: %s\n' % branch.nick) install_lazy_named_hook('bzrlib.status', 'hooks', 'post_status', show_loom_summary, 'loom status') from bzrlib.revisionspec import revspec_registry revspec_registry.register_lazy('thread:', 'bzrlib.plugins.loom.revspec', 'RevisionSpecThread') revspec_registry.register_lazy('below:', 'bzrlib.plugins.loom.revspec', 'RevisionSpecBelow') #register loom formats formats.register_formats() def test_suite(): import bzrlib.plugins.loom.tests return bzrlib.plugins.loom.tests.test_suite() bzr-loom-2.2.0/branch.py0000644000000000000000000011664011722461011013214 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 - 2008 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """The Loom Branch format. A Loom branch extends the behaviour of various methods to manage and propogate the Loom specific data. In the future it would be nice to have this data registered with a normal bzr branch. That said, the branch format should still be specific to loom, to ensure people have the loom plugin when working on a loom branch. """ from __future__ import absolute_import from StringIO import StringIO import bzrlib.branch from bzrlib import ( bzrdir, errors, fetch as _mod_fetch, remote, symbol_versioning, trace, tree as _mod_tree, ui, urlutils, ) from bzrlib.decorators import needs_read_lock, needs_write_lock from bzrlib.revision import is_null, NULL_REVISION from bzrlib.plugins.loom import ( formats, loom_io, loom_state, ) EMPTY_REVISION = 'empty:' def create_thread(loom, thread_name): """Create a thread in the branch loom called thread.""" require_loom_branch(loom) loom.lock_write() try: loom.new_thread(thread_name, loom.nick) loom._set_nick(thread_name) finally: loom.unlock() class AlreadyLoom(errors.BzrError): _fmt = """Loom %(loom)s is already a loom.""" def __init__(self, loom): errors.BzrError.__init__(self) self.loom = loom def loomify(branch): """Convert branch to a loom. If branch is a BzrBranch5 branch, it will become a LoomBranch. """ try: branch.lock_write() try: require_loom_branch(branch) except NotALoom: pass else: raise AlreadyLoom(branch) try: format = { bzrlib.branch.BzrBranchFormat5: BzrBranchLoomFormat1, bzrlib.branch.BzrBranchFormat6: BzrBranchLoomFormat6, bzrlib.branch.BzrBranchFormat7: BzrBranchLoomFormat7, }[branch._format.__class__]() except KeyError: raise UnsupportedBranchFormat(branch._format) format.take_over(branch) finally: branch.unlock() require_loom_branch = formats.require_loom_branch NotALoom = formats.NotALoom class LoomThreadError(errors.BzrError): """Base class for Loom-Thread errors.""" def __init__(self, branch, thread): errors.BzrError.__init__(self) self.branch = branch self.thread = thread class UnrecordedRevision(errors.BzrError): _fmt = """The revision %(revision_id)s is not recorded in the loom %(branch)s.""" def __init__(self, branch, revision_id): errors.BzrError.__init__(self) self.branch = branch self.revision_id = revision_id class UnsupportedBranchFormat(errors.BzrError): _fmt = """The branch format %(format)s is not supported by loomify.""" def __init__(self, format): self.format = format class DuplicateThreadName(LoomThreadError): _fmt = """The thread %(thread)s already exists in branch %(branch)s.""" class UnchangedThreadRevision(LoomThreadError): _fmt = """No new commits to record on thread %(thread)s.""" class NoSuchThread(LoomThreadError): _fmt = """No such thread '%(thread)s'.""" class NoLowerThread(errors.BzrError): _fmt = """No lower thread exists.""" class CannotCombineOnLastThread(NoLowerThread): _fmt = """Cannot combine threads on the bottom thread.""" class LoomMetaTree(_mod_tree.InventoryTree): """A 'tree' object that is used to commit the loom meta branch.""" def __init__(self, loom_meta_ie, loom_stream, loom_sha1): """Create a Loom Meta Tree. :param loom_content_lines: the unicode content to be used for the loom. """ self._inventory = bzrlib.inventory.Inventory() self.inventory.add(loom_meta_ie) self._loom_stream = loom_stream self._loom_sha1 = loom_sha1 def get_file(self, file_id, path): """Get the content of file_id from this tree. As usual this must be for the single existing file 'loom'. """ return self._loom_stream def get_file_with_stat(self, file_id, path=None): return (self.get_file(file_id, path), None) def get_file_sha1(self, file_id, path): """Get the sha1 for a file. This tree only has one file, so it MUST be present! """ assert path == 'loom' assert file_id == 'loom_meta_tree' return self._loom_sha1 def is_executable(self, file_id, path): """get the executable status for file_id. Nothing in a LoomMetaTree is executable. """ return False class LoomSupport(object): """Loom specific logic called into from Branch.""" def _adjust_nick_after_changing_threads(self, threads, current_index): """Adjust the branch nick when we may have removed a current thread. :param threads: The current threads. :param position: The position in the old threads self.nick had. """ threads_dict = dict(thread[0:2] for thread in threads) if self.nick not in threads_dict: if not len(threads): # all threads gone # revert to being a normal branch: revert to an empty revision # history. self.generate_revision_history(bzrlib.revision.NULL_REVISION) return # TODO, calculate the offset of removed threads. # i.e. if there are ten threads removed, and current_index is 5, # if 4 of the ten removed were 2,3,4,5, then the new index should # be 2. if len(threads) <= current_index: # removed the end # take the new end thread self._set_nick(threads[-1][0]) new_rev = threads[-1][1] if new_rev == EMPTY_REVISION: new_rev = bzrlib.revision.NULL_REVISION self.generate_revision_history(new_rev) return # non-end thread removed. self._set_nick(threads[current_index][0]) new_rev = threads[current_index][1] if new_rev == EMPTY_REVISION: new_rev = bzrlib.revision.NULL_REVISION self.generate_revision_history(new_rev) elif self.last_revision() != threads_dict[self.nick]: new_rev = threads_dict[self.nick] if new_rev == EMPTY_REVISION: new_rev = bzrlib.revision.NULL_REVISION self.generate_revision_history(new_rev) def bind(self, other): """Bind the local branch the other branch. :param other: The branch to bind to :type other: Branch """ # Looms are not currently bindable. raise errors.UpgradeRequired(self.base) @needs_read_lock def clone(self, to_bzrdir, revision_id=None, repository_policy=None, name=None): """Clone the branch into to_bzrdir. This differs from the base clone by cloning the loom, setting the current nick to the top of the loom, not honouring any branch format selection on the target bzrdir, and ensuring that the format of the created branch is stacking compatible. """ # If the target is a stackable repository, force-upgrade the # output loom format if (isinstance(to_bzrdir, bzrdir.BzrDirMeta1) and to_bzrdir._format.repository_format.supports_external_lookups): format = BzrBranchLoomFormat7() else: format = self._format result = format.initialize(to_bzrdir, name=name) if repository_policy is not None: repository_policy.configure_branch(result) bzrlib.branch.InterBranch.get(self, result).copy_content_into( revision_id=revision_id) return result def _get_checkout_format(self, lightweight=False): """Checking out a Loom gets a regular branch for now. This is a short term measure to get to an all-tests passing status. """ format = self.repository.bzrdir.checkout_metadir() format.set_branch_format(bzrlib.branch.BzrBranchFormat6()) return format def get_loom_state(self): """Get the current loom state object.""" # TODO: cache the loom state during the transaction lifetime. current_content = self._transport.get('last-loom') reader = loom_io.LoomStateReader(current_content) state = loom_state.LoomState(reader) return state def get_old_bound_location(self): """Return the URL of the branch we used to be bound to.""" # No binding for looms yet. raise errors.UpgradeRequired(self.base) def get_threads(self, rev_id): """Return the threads from a loom revision. :param rev_id: A specific loom revision to retrieve. :return: a list of threads. e.g. [('threadname', 'last_revision')] """ if rev_id is None: symbol_versioning.warn('NULL_REVISION should be used for the null' ' revision instead of None, as of bzr 0.90.', DeprecationWarning, stacklevel=2) if is_null(rev_id): return [] content = self._loom_content(rev_id) return self._parse_loom(content) def export_threads(self, root_transport): """Export the threads in this loom as branches. :param root_transport: Transport for the directory to place branches under. Defaults to branch root transport. """ threads = self.get_loom_state().get_threads() for thread_name, thread_revision, _parents in threads: thread_transport = root_transport.clone(thread_name) user_location = urlutils.unescape_for_display( thread_transport.base, 'utf-8') try: control_dir = bzrdir.BzrDir.open(thread_transport.base, possible_transports=[thread_transport]) tree, branch = control_dir._get_tree_branch() except errors.NotBranchError: trace.note('Creating branch at %s' % user_location) branch = bzrdir.BzrDir.create_branch_convenience( thread_transport.base, possible_transports=[thread_transport]) tree, branch = branch.bzrdir.open_tree_or_branch( thread_transport.base) else: if thread_revision == branch.last_revision(): trace.note('Skipping up-to-date branch at %s' % user_location) continue else: trace.note('Updating branch at %s' % user_location) if tree is not None: tree.pull(self, stop_revision=thread_revision) else: branch.pull(self, stop_revision=thread_revision) def _loom_content(self, rev_id): """Return the raw formatted content of a loom as a series of lines. :param rev_id: A specific loom revision to retrieve. Currently the disk format is: ---- Loom meta 1 revisionid threadname_in_utf8 ---- if revisionid is empty:, this is a new, empty branch. """ tree = self.repository.revision_tree(rev_id) lines = tree.get_file('loom_meta_tree').read().split('\n') assert lines[0] == 'Loom meta 1' return lines[1:-1] def loom_parents(self): """Return the current parents to use in the next commit.""" return self.get_loom_state().get_parents() def new_thread(self, thread_name, after_thread=None): """Add a new thread to this branch called 'thread_name'.""" state = self.get_loom_state() threads = state.get_threads() if thread_name in state.get_threads_dict(): raise DuplicateThreadName(self, thread_name) assert after_thread is None or after_thread in state.get_threads_dict() if after_thread is None: insertion_point = len(threads) else: insertion_point = state.thread_index(after_thread) + 1 if insertion_point == 0: revision_for_thread = self.last_revision() else: revision_for_thread = threads[insertion_point - 1][1] if is_null(revision_for_thread): revision_for_thread = EMPTY_REVISION threads.insert( insertion_point, (thread_name, revision_for_thread, [None] * len(state.get_parents()) ) ) state.set_threads(threads) self._set_last_loom(state) def _parse_loom(self, content): """Parse the body of a loom file.""" result = [] for line in content: rev_id, name = line.split(' ', 1) result.append((name, rev_id)) return result def _loom_get_nick(self): return self._get_nick(local=True) def _rename_thread(self, nick): """Rename the current thread to nick.""" state = self.get_loom_state() threads = state.get_threads() if not len(threads): # No threads at all - probably a default initialised loom in the # test suite. return self._set_nick(nick) current_index = state.thread_index(self.nick) threads[current_index] = (nick,) + threads[current_index][1:] state.set_threads(threads) self._set_last_loom(state) # Preserve default behavior: set the branch nick self._set_nick(nick) nick = property(_loom_get_nick, _rename_thread) def heads_to_fetch(self): """See Branch.heads_to_fetch.""" # The base implementation returns ([tip], tags) must_fetch, should_fetch = super(LoomSupport, self).heads_to_fetch() # Add each thread's content to must_fetch must_fetch.update( thread_rev for thread_name, thread_rev, thread_parents in self.get_loom_state().get_threads()) must_fetch.discard(EMPTY_REVISION) must_fetch.discard(bzrlib.revision.NULL_REVISION) return must_fetch, should_fetch @needs_read_lock def push(self, target, overwrite=False, stop_revision=None, lossy=False, _override_hook_source_branch=None): # Not ideal, but see the issues raised on bazaar@lists.canonical.com # about the push api needing work. if not isinstance(target, LoomSupport): return super(LoomSupport, self).push(target, overwrite, stop_revision, lossy=lossy, _override_hook_source_branch=None) if lossy: raise errors.LossyPushToSameVCS(self, target) return _Pusher(self, target).transfer(overwrite, stop_revision, run_hooks=True) @needs_write_lock def record_loom(self, commit_message): """Perform a 'commit' to the loom branch. :param commit_message: The commit message to use when committing. """ state = self.get_loom_state() parents = state.get_parents() old_threads = self.get_threads(state.get_basis_revision_id()) threads = state.get_threads() # check the semantic value, not the serialised value for equality. if old_threads == threads: raise errors.PointlessCommit builder = self.get_commit_builder(parents) loom_ie = bzrlib.inventory.make_entry( 'file', 'loom', bzrlib.inventory.ROOT_ID, 'loom_meta_tree') writer = loom_io.LoomWriter() loom_stream = StringIO() new_threads = [thread[0:2] for thread in threads] loom_sha1 = writer.write_threads(new_threads, loom_stream) loom_stream.seek(0) loom_tree = LoomMetaTree(loom_ie, loom_stream, loom_sha1) if getattr(builder, 'record_root_entry', False): root_ie = bzrlib.inventory.make_entry( 'directory', '', None, bzrlib.inventory.ROOT_ID) builder.record_entry_contents(root_ie, [], '', loom_tree, ('directory', None, None, None)) builder.record_entry_contents( loom_ie, list(self.repository.iter_inventories(parents)), 'loom', loom_tree, # a fake contents so that the file is determined as changed. ('file', 0, False, None)) builder.finish_inventory() rev_id = builder.commit(commit_message) state.set_parents([rev_id]) state.set_threads((thread + ([thread[1]],) for thread in new_threads)) self._set_last_loom(state) return rev_id @needs_write_lock def record_thread(self, thread_name, revision_id): """Record an updated version of an existing thread. :param thread_name: the thread to record. :param revision_id: the revision it is now at. This should be a child of the next lower thread. """ state = self.get_loom_state() threads = state.get_threads() assert thread_name in state.get_threads_dict() if is_null(revision_id): revision_id = EMPTY_REVISION for position, (name, rev, parents) in enumerate(threads): if name == thread_name: if revision_id == rev: raise UnchangedThreadRevision(self, thread_name) threads[position] = (name, revision_id, parents) state.set_threads(threads) self._set_last_loom(state) @needs_write_lock def remove_thread(self, thread_name): """Remove thread from the current loom. :param thread_name: The thread to remove. """ state = self.get_loom_state() threads = state.get_threads() current_index = state.thread_index(thread_name) del threads[current_index] state.set_threads(threads) self._set_last_loom(state) @needs_write_lock def revert_loom(self): """Revert the loom to be the same as the basis loom.""" state = self.get_loom_state() # get the current position position = state.thread_index(self.nick) # reset the current threads basis_threads = self.get_threads(state.get_basis_revision_id()) state.set_threads( (thread + ([thread[1]],) for thread in basis_threads) ) basis_rev_id = state.get_basis_revision_id() # reset the parents list to just the basis. if basis_rev_id is not None: state.set_parents([basis_rev_id]) self._adjust_nick_after_changing_threads(state.get_threads(), position) self._set_last_loom(state) @needs_write_lock def revert_thread(self, thread): """Revert a single thread. :param thread: the thread to restore to its state in the basis. If it was not present in the basis it will be removed from the current loom. """ state = self.get_loom_state() parents = state.get_parents() threads = state.get_threads() position = state.thread_index(thread) basis_threads = self.get_threads(state.get_basis_revision_id()) if thread in dict(basis_threads): basis_rev = dict(basis_threads)[thread] threads[position] = (thread, basis_rev, threads[position][2]) else: del threads[position] state.set_threads(threads) self._set_last_loom(state) # adjust the nickname to be valid self._adjust_nick_after_changing_threads(threads, position) def _set_last_loom(self, state): """Record state to the last-loom control file.""" stream = StringIO() writer = loom_io.LoomStateWriter(state) writer.write(stream) stream.seek(0) self._transport.put_file('last-loom', stream) def unlock(self): """Unlock the loom after a lock. If at the end of the lock, the current revision in the branch is not recorded correctly in the loom, an automatic record is attempted. """ if (self.control_files._lock_count==1 and self.control_files._lock_mode=='w'): # about to release the lock state = self.get_loom_state() threads = state.get_threads() if len(threads): # looms are enabled: lastrev = self.last_revision() if is_null(lastrev): lastrev = EMPTY_REVISION if dict(state.get_threads_dict())[self.nick][0] != lastrev: self.record_thread(self.nick, lastrev) super(LoomSupport, self).unlock() class _Puller(object): # XXX: Move into InterLoomBranch. def __init__(self, source, target): self.target = target self.source = source # If _Puller has been created, we need real branch objects. self.real_target = self.unwrap_branch(target) self.real_source = self.unwrap_branch(source) def unwrap_branch(self, branch): if isinstance(branch, remote.RemoteBranch): branch._ensure_real() return branch._real_branch return branch def prepare_result(self, _override_hook_target): result = self.make_result() result.source_branch = self.source result.target_branch = _override_hook_target if result.target_branch is None: result.target_branch = self.target # cannot bind currently result.local_branch = None result.master_branch = self.target result.old_revno, result.old_revid = self.target.last_revision_info() return result def finish_result(self, result): result.new_revno, result.new_revid = self.target.last_revision_info() def do_hooks(self, result, run_hooks): self.finish_result(result) # get the final result object details if run_hooks: for hook in self.post_hooks(): hook(result) return result @staticmethod def make_result(): return bzrlib.branch.PullResult() @staticmethod def post_hooks(): return bzrlib.branch.Branch.hooks['post_pull'] def plain_transfer(self, result, run_hooks, stop_revision, overwrite): # no thread commits ever # just pull the main branch. new_rev = stop_revision if new_rev is None: new_rev = self.source.last_revision() if new_rev == EMPTY_REVISION: new_rev = bzrlib.revision.NULL_REVISION fetch_spec = self.build_fetch_spec(stop_revision) self.target.repository.fetch(self.source.repository, fetch_spec=fetch_spec) self.target.generate_revision_history(new_rev, self.target.last_revision(), self.source) tag_ret = self.source.tags.merge_to(self.target.tags) if isinstance(tag_ret, tuple): result.tag_updates, result.tag_conflicts = tag_ret else: result.tag_conflicts = tag_ret # get the final result object details self.do_hooks(result, run_hooks) return result def build_fetch_spec(self, stop_revision): factory = _mod_fetch.FetchSpecFactory() factory.source_branch = self.source factory.source_repo = self.source.repository factory.source_branch_stop_revision_id = stop_revision factory.target_repo = self.target.repository factory.target_repo_kind = _mod_fetch.TargetRepoKinds.PREEXISTING return factory.make_fetch_spec() def transfer(self, overwrite, stop_revision, run_hooks=True, possible_transports=None, _override_hook_target=None, local=False): """Implementation of push and pull""" if local: raise errors.LocalRequiresBoundBranch() # pull the loom, and position our pb = ui.ui_factory.nested_progress_bar() try: result = self.prepare_result(_override_hook_target) self.target.lock_write() self.source.lock_read() try: source_state = self.real_source.get_loom_state() source_parents = source_state.get_parents() if not source_parents: return self.plain_transfer(result, run_hooks, stop_revision, overwrite) # pulling a loom # the first parent is the 'tip' revision. my_state = self.target.get_loom_state() source_loom_rev = source_state.get_parents()[0] if not overwrite: # is the loom compatible? if len(my_state.get_parents()) > 0: graph = self.source.repository.get_graph() if not graph.is_ancestor(my_state.get_parents()[0], source_loom_rev): raise errors.DivergedBranches( self.target, self.source) # fetch the loom content self.target.repository.fetch(self.source.repository, revision_id=source_loom_rev) # get the threads for the new basis threads = self.target.get_threads( source_state.get_basis_revision_id()) # stopping at from our repository. revisions = [rev for name,rev in threads] # fetch content for all threads and tags. fetch_spec = self.build_fetch_spec(stop_revision) self.target.repository.fetch(self.source.repository, fetch_spec=fetch_spec) # set our work threads to match (this is where we lose data if # there are local mods) my_state.set_threads( (thread + ([thread[1]],) for thread in threads) ) # and the new parent data my_state.set_parents([source_loom_rev]) # and save the state. self.target._set_last_loom(my_state) # set the branch nick. self.target._set_nick(threads[-1][0]) # and position the branch on the top loom new_rev = threads[-1][1] if new_rev == EMPTY_REVISION: new_rev = bzrlib.revision.NULL_REVISION self.target.generate_revision_history(new_rev) # merge tags tag_ret = self.source.tags.merge_to(self.target.tags) if isinstance(tag_ret, tuple): result.tag_updates, tag_conflicts = tag_ret else: result.tag_conflicts = tag_ret self.do_hooks(result, run_hooks) return result finally: self.source.unlock() self.target.unlock() finally: pb.finished() class _Pusher(_Puller): @staticmethod def make_result(): return bzrlib.branch.BranchPushResult() @staticmethod def post_hooks(): return bzrlib.branch.Branch.hooks['post_push'] class LoomBranch(LoomSupport, bzrlib.branch.BzrBranch5): """The Loom branch. A mixin is used as the easiest migration path to support branch6. A delegated object may well be cleaner. """ class LoomBranch6(LoomSupport, bzrlib.branch.BzrBranch6): """Branch6 Loom branch. A mixin is used as the easiest migration path to support branch6. A delegated object may well be cleaner. """ class LoomBranch7(LoomSupport, bzrlib.branch.BzrBranch7): """Branch6 Loom branch. A mixin is used as the easiest migration path to support branch7. A rewrite would be preferable, but a stackable loom format is needed quickly. """ class LoomFormatMixin(object): """Support code for Loom formats.""" # A mixin is not ideal because it is tricky to test, but it seems to be the # best solution for now. def initialize(self, a_bzrdir, name=None, repository=None, append_revisions_only=None): """Create a branch of this format in a_bzrdir.""" super(LoomFormatMixin, self).initialize(a_bzrdir, name=name, repository=repository, append_revisions_only=append_revisions_only) branch_transport = a_bzrdir.get_branch_transport(self) files = [] state = loom_state.LoomState() writer = loom_io.LoomStateWriter(state) state_stream = StringIO() writer.write(state_stream) state_stream.seek(0) files.append(('last-loom', state_stream)) control_files = bzrlib.lockable_files.LockableFiles( branch_transport, 'lock', bzrlib.lockdir.LockDir) control_files.lock_write() try: for filename, stream in files: branch_transport.put_file(filename, stream) finally: control_files.unlock() return self.open(a_bzrdir, _found=True, name=name) def open(self, a_bzrdir, name=None, _found=False, ignore_fallbacks=False, found_repository=None, possible_transports=None): """Return the branch object for a_bzrdir _found is a private parameter, do not use it. It is used to indicate if format probing has already be done. :param name: The 'colocated branches' name for the branch to open. """ if name is None: name = a_bzrdir._get_selected_branch() if not _found: format = bzrlib.branch.BranchFormat.find_format(a_bzrdir, name=name) assert format.__class__ == self.__class__ transport = a_bzrdir.get_branch_transport(None, name=name) control_files = bzrlib.lockable_files.LockableFiles( transport, 'lock', bzrlib.lockdir.LockDir) if found_repository is None: found_repository = a_bzrdir.find_repository() return self._branch_class()(_format=self, _control_files=control_files, a_bzrdir=a_bzrdir, _repository=found_repository, ignore_fallbacks=ignore_fallbacks, name=name) def take_over(self, branch): """Take an existing bzrlib branch over into Loom format. This currently cannot convert branches to Loom format unless they are in Branch 5 format. The conversion takes effect when the branch is next opened. """ assert branch._format.__class__ is self._parent_classs branch._transport.put_bytes('format', self.get_format_string()) state = loom_state.LoomState() writer = loom_io.LoomStateWriter(state) state_stream = StringIO() writer.write(state_stream) state_stream.seek(0) branch._transport.put_file('last-loom', state_stream) class BzrBranchLoomFormat1(LoomFormatMixin, bzrlib.branch.BzrBranchFormat5): """Loom's first format. This format is an extension to BzrBranchFormat5 with the following changes: - a loom-revision file. The loom-revision file has a revision id in it which points into the loom data branch in the repository. This format is new in the loom plugin. """ def _branch_class(self): return LoomBranch _parent_classs = bzrlib.branch.BzrBranchFormat5 @classmethod def get_format_string(cls): """See BranchFormat.get_format_string().""" return "Bazaar-NG Loom branch format 1\n" def get_format_description(self): """See BranchFormat.get_format_description().""" return "Loom branch format 1" def __str__(self): return "Bazaar-NG Loom format 1" class BzrBranchLoomFormat6(LoomFormatMixin, bzrlib.branch.BzrBranchFormat6): """Loom's second edition - based on bzr's Branch6. This format is an extension to BzrBranchFormat6 with the following changes: - a last-loom file. The last-loom file has a revision id in it which points into the loom data branch in the repository. This format is new in the loom plugin. """ def _branch_class(self): return LoomBranch6 _parent_classs = bzrlib.branch.BzrBranchFormat6 @classmethod def get_format_string(cls): """See BranchFormat.get_format_string().""" return "Bazaar-NG Loom branch format 6\n" def get_format_description(self): """See BranchFormat.get_format_description().""" return "Loom branch format 6" def __str__(self): return "bzr loom format 6 (based on bzr branch format 6)\n" class BzrBranchLoomFormat7(LoomFormatMixin, bzrlib.branch.BzrBranchFormat7): """Loom's second edition - based on bzr's Branch7. This format is an extension to BzrBranchFormat7 with the following changes: - a last-loom file. The last-loom file has a revision id in it which points into the loom data branch in the repository. This format is new in the loom plugin. """ def _branch_class(self): return LoomBranch7 _parent_classs = bzrlib.branch.BzrBranchFormat7 @classmethod def get_format_string(cls): """See BranchFormat.get_format_string().""" return "Bazaar-NG Loom branch format 7\n" def get_format_description(self): """See BranchFormat.get_format_description().""" return "Loom branch format 7" def __str__(self): return "bzr loom format 7 (based on bzr branch format 7)\n" # Handle the smart server: class InterLoomBranch(bzrlib.branch.GenericInterBranch): @classmethod def _get_branch_formats_to_test(klass): default_format = bzrlib.branch.format_registry.get_default() return [ (default_format, BzrBranchLoomFormat7()), (BzrBranchLoomFormat7(), default_format), (BzrBranchLoomFormat7(), BzrBranchLoomFormat7()), ] def unwrap_branch(self, branch): if isinstance(branch, remote.RemoteBranch): branch._ensure_real() return branch._real_branch return branch @classmethod def is_compatible(klass, source, target): # 1st cut: special case and handle all *->Loom and Loom->* return klass.branch_is_loom(source) or klass.branch_is_loom(target) def get_loom_state(self, branch): branch = self.unwrap_branch(branch) return branch.get_loom_state() def get_threads(self, branch, revision_id): branch = self.unwrap_branch(branch) return branch.get_threads(revision_id) @classmethod def branch_is_loom(klass, branch): format = klass.unwrap_format(branch._format) return isinstance(format, LoomFormatMixin) @needs_write_lock def copy_content_into(self, revision_id=None): if not self.__class__.branch_is_loom(self.source): # target is loom, but the generic code path works Just Fine for # regular to loom copy_content_into. return super(InterLoomBranch, self).copy_content_into( revision_id=revision_id) # XXX: hint for bzrlib - break this into two routines, one for # copying the last-rev pointer, one for copying parent etc. source_nick = self.source.nick state = self.get_loom_state(self.source) parents = state.get_parents() if parents: loom_tip = parents[0] else: loom_tip = None threads = self.get_threads(self.source, state.get_basis_revision_id()) if revision_id not in (None, NULL_REVISION): if threads: # revision_id should be in the loom, or its an error found_threads = [thread for thread, rev in threads if rev == revision_id] if not found_threads: # the thread we have been asked to set in the remote # side has not been recorded yet, so its data is not # present at this point. raise UnrecordedRevision(self.source, revision_id) # pull in the warp, which was skipped during the initial pull # because the front end does not know what to pull. # nb: this is mega huge hacky. THINK. RBC 2006062 nested = ui.ui_factory.nested_progress_bar() try: if parents: self.target.repository.fetch(self.source.repository, revision_id=parents[0]) if threads: for thread, rev_id in reversed(threads): # fetch the loom content for this revision self.target.repository.fetch(self.source.repository, revision_id=rev_id) finally: nested.finished() state = loom_state.LoomState() try: require_loom_branch(self.target) if threads: last_rev = threads[-1][1] if last_rev == EMPTY_REVISION: last_rev = bzrlib.revision.NULL_REVISION self.target.generate_revision_history(last_rev) state.set_parents([loom_tip]) state.set_threads( (thread + ([thread[1]],) for thread in threads) ) else: # no threads yet, be a normal branch. self.source._synchronize_history(self.target, revision_id) target_loom = self.unwrap_branch(self.target) target_loom._set_last_loom(state) except NotALoom: self.source._synchronize_history(self.target, revision_id) try: parent = self.source.get_parent() except errors.InaccessibleParent, e: trace.mutter('parent was not accessible to copy: %s', e) else: if parent: self.target.set_parent(parent) if threads: self.target._set_nick(threads[-1][0]) if self.source._push_should_merge_tags(): self.source.tags.merge_to(self.target.tags) @needs_write_lock def pull(self, overwrite=False, stop_revision=None, run_hooks=True, possible_transports=None, _override_hook_target=None, local=False): """Perform a pull, reading from self.source and writing to self.target. If the source branch is a non-loom branch, the pull is done against the current warp. If it is a loom branch, then the pull is done against the entire loom and the current thread set to the top thread. """ # Special code only needed when both source and targets are looms: if (self.__class__.branch_is_loom(self.target) and self.__class__.branch_is_loom(self.source)): return _Puller(self.source, self.target).transfer(overwrite, stop_revision, run_hooks, possible_transports, _override_hook_target, local) return super(InterLoomBranch, self).pull( overwrite=overwrite, stop_revision=stop_revision, possible_transports=possible_transports, _override_hook_target=_override_hook_target, local=local, run_hooks=run_hooks) bzrlib.branch.InterBranch.register_optimiser(InterLoomBranch) bzr-loom-2.2.0/commands.py0000644000000000000000000003504611722461011013560 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006, 2008 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Loom commands.""" from __future__ import absolute_import from bzrlib import bzrdir, directory_service, workingtree import bzrlib.commands import bzrlib.branch from bzrlib import errors from bzrlib.lazy_import import lazy_import from bzrlib.option import Option import bzrlib.trace import bzrlib.transport from bzrlib.plugins.loom import formats lazy_import(globals(), """ from bzrlib.plugins.loom import branch from bzrlib.plugins.loom.tree import LoomTreeDecorator """) class cmd_loomify(bzrlib.commands.Command): """Add a loom to this branch. This creates a loom in your branch, which will alter the behaviour of bzr for a number of commands to manage a group of patches being evolved in parallel. You must have a branch nickname explicitly set to use this command, as the branch nickname becomes the 'base thread' of the loom. You can specify the branch nick with the --base option. """ takes_args = ['location?'] takes_options = [Option('base', type=str, help='The name to use for the base thread.')] def run(self, location='.', base=None): (target, path) = bzrlib.branch.Branch.open_containing(location) target.lock_write() try: if base is not None: target.nick = base elif not target.get_config().has_explicit_nickname(): raise errors.BzrCommandError( 'You must specify --base or have a branch nickname set to' ' loomify a branch') branch.loomify(target) loom = target.bzrdir.open_branch() finally: target.unlock() # requires a new lock as its a new instance, XXX: teach bzrdir about # format changes ? loom.new_thread(loom.nick) class cmd_combine_thread(bzrlib.commands.Command): __doc__ = """Combine the current thread with the thread below it. This will currently refuse to operate on the last thread, but in the future will just turn the loom into a normal branch again. Use combine-thread to remove a thread which has been merged into upstream. In precise terms this will: * Remove the entry from the loom for the current thread. * Change threads to the thread below. """ takes_options = [ Option('force', help='Combine even if work in the thread is not ' 'integrated up or down the loom.'), ] def run(self, force=False): (tree, path) = workingtree.WorkingTree.open_containing('.') branch.require_loom_branch(tree.branch) self.add_cleanup(tree.lock_write().unlock) current_thread = tree.branch.nick state = tree.branch.get_loom_state() if not force: # Check for unmerged work. # XXX: Layering issue whom should be caring for the check, not the # command thats for sure. threads = state.get_threads() current_index = state.thread_index(current_thread) rev_below = None rev_current = threads[current_index][1] rev_above = None if current_index: # There is a thread below rev_below = threads[current_index - 1][1] if current_index < len(threads) - 1: rev_above = threads[current_index + 1][1] graph = tree.branch.repository.get_graph() candidates = [rev for rev in (rev_below, rev_current, rev_above) if rev] heads = graph.heads(candidates) # If current is not a head, its trivially merged, or # if current is == rev_below, its also merged, or # if there is only one thread its merged (well its not unmerged). if (rev_current == rev_below or rev_current not in heads or (rev_below is None and rev_above is None)): merged = True else: merged = False if not merged: raise errors.BzrCommandError("Thread '%s' has unmerged work" ". Use --force to combine anyway." % current_thread) new_thread = state.get_new_thread_after_deleting(current_thread) if new_thread is None: raise branch.CannotCombineOnLastThread bzrlib.trace.note("Combining thread '%s' into '%s'", current_thread, new_thread) LoomTreeDecorator(tree).down_thread(new_thread) tree.branch.remove_thread(current_thread) class cmd_create_thread(bzrlib.commands.Command): """Add a thread to this loom. This creates a new thread in this loom and moves the branch onto that thread. The thread-name must be a valid branch 'nickname', and must not be the name of an existing thread in your loom. The new thread is created immediately after the current thread. """ takes_args = ['thread'] def run(self, thread): (loom, path) = bzrlib.branch.Branch.open_containing('.') branch.create_thread(loom, thread) class cmd_show_loom(bzrlib.commands.Command): """Show the threads in this loom. Output the threads in this loom with the newest thread at the top and the base thread at the bottom. A => marker indicates the thread that 'commit' will commit to. """ takes_args = ['location?'] def run(self, location='.'): (loom, path) = bzrlib.branch.Branch.open_containing(location) branch.require_loom_branch(loom) loom.lock_read() try: threads = loom.get_loom_state().get_threads() nick = loom.nick for thread, revid, parents in reversed(threads): if thread == nick: symbol = '=>' else: symbol = ' ' print "%s%s" % (symbol, thread) finally: loom.unlock() class cmd_switch(bzrlib.builtins.cmd_switch): """Set the branch of a checkout and update. For looms, this is equivalent to 'down-thread' when to_location is the name of a thread in the loom. For lightweight checkouts, this changes the branch being referenced. For heavyweight checkouts, this checks that there are no local commits versus the current bound branch, then it makes the local branch a mirror of the new location and binds to it. In both cases, the working tree is updated and uncommitted changes are merged. The user can commit or revert these as they desire. Pending merges need to be committed or reverted before using switch. """ _original_command = None def _get_thread_name(self, loom, to_location): """Return the name of the thread pointed to by 'to_location'. Most of the time this will be the name of the thread, but if 'to_location' is 'bottom:' it will be the name of the bottom thread. If 'to_location' is 'top:', then it'll be the name of the top thread. """ aliases = {'bottom:': 0, 'top:': -1} if to_location in aliases: threads = loom.get_loom_state().get_threads() thread = threads[aliases[to_location]] return thread[0] return to_location def run(self, to_location=None, force=False, create_branch=False, revision=None, directory=None): # The top of this is cribbed from bzr; because bzr isn't factored out # enough. if directory is None: directory = u'.' control_dir, path = bzrdir.BzrDir.open_containing(directory) if to_location is None: if revision is None: raise errors.BzrCommandError( 'You must supply either a revision or a location') to_location = '.' try: from_branch = control_dir.open_branch() except errors.NotBranchError: from_branch = None if create_branch: if from_branch is None: raise errors.BzrCommandError( 'cannot create branch without source branch') to_location = directory_service.directories.dereference( to_location) if from_branch is not None: # Note: reopens. (tree, path) = workingtree.WorkingTree.open_containing(directory) tree = LoomTreeDecorator(tree) try: if create_branch: return branch.create_thread(tree.branch, to_location) thread_name = self._get_thread_name(tree.branch, to_location) return tree.down_thread(thread_name) except (AttributeError, branch.NoSuchThread, branch.NotALoom): # When there is no thread its probably an external branch # that we have been given. raise errors.MustUseDecorated else: # switching to a relocated branch raise errors.MustUseDecorated def run_argv_aliases(self, argv, alias_argv=None): """Parse command line and run. If the command requests it, run the decorated version. """ try: super(cmd_switch, self).run_argv_aliases(list(argv), alias_argv) except (errors.MustUseDecorated, errors.BzrOptionError): if self._original_command is None: raise self._original_command().run_argv_aliases(argv, alias_argv) class cmd_record(bzrlib.commands.Command): """Record the current last-revision of this tree into the current thread.""" takes_args = ['message'] def run(self, message): (abranch, path) = bzrlib.branch.Branch.open_containing('.') branch.require_loom_branch(abranch) abranch.record_loom(message) print "Loom recorded." class cmd_revert_loom(bzrlib.commands.Command): """Revert part or all of a loom. This will update the current loom to be the same as the basis when --all is supplied. If no parameters or options are supplied then nothing will happen. If a thread is named, then only that thread is reverted to its state in the last committed loom. """ takes_args = ['thread?'] takes_options = [Option('all', help='Revert all threads.'), ] def run(self, thread=None, all=None): if thread is None and all is None: bzrlib.trace.note('Please see revert-loom -h.') return (tree, path) = workingtree.WorkingTree.open_containing('.') branch.require_loom_branch(tree.branch) tree = LoomTreeDecorator(tree) if all: tree.revert_loom() bzrlib.trace.note('All threads reverted.') else: tree.revert_loom(thread) bzrlib.trace.note("thread '%s' reverted.", thread) class cmd_down_thread(bzrlib.commands.Command): """Move the branch down a thread in the loom. This removes the changes introduced by the current thread from the branch and sets the branch to be the next thread down. Down-thread refuses to operate if there are uncommitted changes, since this is typically a mistake. Switch can be used for this purpose, instead. """ takes_args = ['thread?'] aliases = ['down'] _see_also = ['switch', 'up-thread'] def run(self, thread=None): (wt, path) = workingtree.WorkingTree.open_containing('.') branch.require_loom_branch(wt.branch) tree = LoomTreeDecorator(wt) tree.lock_write() try: basis = wt.basis_tree() basis.lock_read() try: for change in wt.iter_changes(basis): raise errors.BzrCommandError( 'Working tree has uncommitted changes.') finally: basis.unlock() return tree.down_thread(thread) finally: tree.unlock() class cmd_up_thread(bzrlib.commands.Command): """Move the branch up to the top thread in the loom. This merges the changes done in this thread but not incorporated into the next thread up into the next thread up and switches your tree to be that thread. Unless there are conflicts, or --manual is specified, it will then commit and repeat the process. """ takes_args = ['thread?'] takes_options = ['merge-type', Option('auto', help='Deprecated - now the default.'), Option('manual', help='Perform commit manually.'), ] _see_also = ['down-thread', 'switch'] def run(self, merge_type=None, manual=False, thread=None, auto=None): (tree, path) = workingtree.WorkingTree.open_containing('.') branch.require_loom_branch(tree.branch) tree = LoomTreeDecorator(tree) if manual: if thread is not None: raise errors.BzrCommandError('Specifying a thread does not' ' work with --manual.') return tree.up_thread(merge_type) else: return tree.up_many(merge_type, thread) class cmd_export_loom(bzrlib.commands.Command): """Export loom threads as a full-fledged branches. LOCATION specifies the location to export the threads under. If it does not exist, it will be created. In any of the standard config files, "export_loom_root" may be set to provide a default location that will be used if no location is supplied. """ takes_args = ['location?'] _see_also = ['configuration'] def run(self, location=None): root_transport = None loom = bzrlib.branch.Branch.open_containing('.')[0] if location is None: location = loom.get_config().get_user_option('export_loom_root') if location is None: raise errors.BzrCommandError('No export root known or specified.') root_transport = bzrlib.transport.get_transport(location, possible_transports=[loom.bzrdir.root_transport]) root_transport.ensure_base() loom.export_threads(root_transport) bzr-loom-2.2.0/formats.py0000644000000000000000000000434111722461011013424 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2010 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # from __future__ import absolute_import """Format information about formats for Loom. This is split out from the implementation of the formats to permit lazy loading without requiring the implementation code to be cryptic. """ __all__ = [ 'NotALoom', 'register_formats', 'require_loom_branch', ] from bzrlib.lazy_import import lazy_import import bzrlib.errors lazy_import(globals(), """ from bzrlib import branch as _mod_branch """) _LOOM_FORMATS = { "Bazaar-NG Loom branch format 1\n": "BzrBranchLoomFormat1", "Bazaar-NG Loom branch format 6\n": "BzrBranchLoomFormat6", "Bazaar-NG Loom branch format 7\n": "BzrBranchLoomFormat7", } def register_formats(): branch_formats = [_mod_branch.MetaDirBranchFormatFactory(format_string, "bzrlib.plugins.loom.branch", format_class) for (format_string, format_class) in _LOOM_FORMATS.iteritems()] format_registry = getattr(_mod_branch, 'format_registry') map(format_registry.register, branch_formats) def require_loom_branch(branch): """Return None if branch is already loomified, or raise NotALoom.""" if branch._format.network_name() not in _LOOM_FORMATS: raise NotALoom(branch) # TODO: define errors without importing all errors. class NotALoom(bzrlib.errors.BzrError): _fmt = ("The branch %(branch)s is not a loom. " "You can use 'bzr loomify' to make it into a loom.") def __init__(self, branch): bzrlib.errors.BzrError.__init__(self) self.branch = branch bzr-loom-2.2.0/loom_io.py0000644000000000000000000001267311722461011013415 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Routines for reading and writing Looms in streams.""" import bzrlib.osutils # The current format marker for serialised loom state. # This belongs in a format object at some point. _CURRENT_LOOM_FORMAT_STRING = "Loom current 1" # the current loom format : # first line is the format signature # second line is the list of parents # third line and beyond are the current threads. # each thread line has one field for current status # one field for each parent # one field for the current revision id # and then the rest of the line for the thread name. class LoomWriter(object): """LoomWriter objects are used to serialise looms.""" def write_threads(self, threads, stream): """Write threads to stream with a format header.""" thread_content = 'Loom meta 1\n' for thread, rev_id in threads: thread_content += '%s %s\n' % (rev_id, thread) thread_content = thread_content.encode('utf8') stream.write(thread_content) return bzrlib.osutils.sha_strings([thread_content]) class LoomStateWriter(object): """LoomStateWriter objects are used to write out LoomState objects.""" def __init__(self, state): """Initialise a LoomStateWriter with a state object. :param state: The LoomState object to be written out. """ self._state = state def write(self, stream): """Write the state object to stream.""" lines = [_CURRENT_LOOM_FORMAT_STRING + '\n'] lines.append(' '.join(self._state.get_parents()) + '\n') # Note that we could possibly optimise our unicode handling here. for thread, rev_id, parents in self._state.get_threads(): assert len(parents) == len(self._state.get_parents()) # leading space for conflict status line = " " for parent in parents: if parent is not None: line += "%s " % parent.decode('utf8') else: line += " " line += ": " lines.append('%s%s %s\n' % (line, rev_id.decode('utf8'), thread)) stream.write(''.join(lines).encode('utf8')) class LoomStateReader(object): """LoomStateReaders are used to pull LoomState objects into memory.""" def __init__(self, stream): """Initialise a LoomStateReader with a serialised loom-state stream. :param stream: The stream that contains a loom-state object. This should allow relative seeking. """ self._stream = stream self._content = None def _read(self): """Read the entire stream into memory. This is just a first approximation - eventually partial reads are desirable. """ if self._content is None: # Names are unicode,revids are utf8 - it's arguable whether decode # all and encode revids, or vice verca is better. self._content = self._stream.read().decode('utf8').split('\n') # this is where detection of different formats should go. # we probably want either a factory for readers, or a strategy # for the reader that is looked up on this format string. # either way, its in the future. assert self._content[0] == _CURRENT_LOOM_FORMAT_STRING def read_parents(self): """Read the parents field from the stream. :return: a list of parent revision ids. """ self._read() return self._content[1].encode('utf8').split() def read_thread_details(self): """Read the details for the threads. :return: a list of thread details. Each thread detail is a 3-tuple containing the thread name, the current thread revision, and a list of parent thread revisions, in the same order and length as the list returned by read_parents. In the parent thread revision list, None means 'no present in the parent', and 'null:' means 'present but had no commits'. """ result = [] parent_count = len(self.read_parents()) split_count = parent_count + 2 # skip the format and parent lines, and the trailing \n line. for line in self._content[2:-1]: conflict_status, line = line.split(' ', 1) parents = [] parent = "" while True: parent, line = line.split(' ', 1) if parent == ':': break elif parent == '': parents.append(None) else: parents.append(parent.encode('utf8')) rev_id, name = line.split(' ', 1) result.append((name, rev_id.encode('utf8'), parents)) return result bzr-loom-2.2.0/loom_state.py0000644000000000000000000000712511722461011014122 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006, 2008 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """The current-loom state object.""" from bzrlib.revision import NULL_REVISION class LoomState(object): """The LoomState represents the content of the current-loom branch file. It is planned to not need access to repository data - it will be driven by the LoomBranch and have data fed into it. """ def __init__(self, reader=None): """Create a loom state object. :param reader: If not None, this should be a LoomStateReader from which this LoomState is meant to retrieve its current data. """ self._parents = [] self._threads = [] if reader is not None: # perhaps this should be lazy evaluated at some point? self._parents = reader.read_parents() for thread in reader.read_thread_details(): self._threads.append(thread) def get_basis_revision_id(self): """Get the revision id for the basis revision. None is return if there is no basis revision. """ if not self._parents: return NULL_REVISION else: return self._parents[0] def get_parents(self): """Get the list of loom revisions that are parents to this state.""" return self._parents def get_threads(self): """Get the threads for the current state.""" return list(self._threads) def get_threads_dict(self): """Get the threads as a dict. This loses ordering, but is useful for quickly locating the details on a given thread. """ return dict((thread[0], thread[1:]) for thread in self._threads) def thread_index(self, thread): """Find the index of thread in threads.""" # Avoid circular import from bzrlib.plugins.loom.branch import NoSuchThread thread_names = [name for name, rev, parents in self._threads] try: return thread_names.index(thread) except ValueError: raise NoSuchThread(self, thread) def get_new_thread_after_deleting(self, current_thread): if len(self._threads) == 1: return None current_index = self.thread_index(current_thread) if current_index == 0: new_index = 1 else: new_index = current_index - 1 return self._threads[new_index][0] def set_parents(self, parent_list): """Set the parents of this state to parent_list. :param parent_list: A list of (parent_id, threads) tuples. """ self._parents = list(parent_list) def set_threads(self, threads): """Set the current threads to threads. :param threads: A list of (name, revid) pairs that make up the threads. If the list is altered after calling set_threads, there is no effect on the LoomState. """ self._threads = list(threads) bzr-loom-2.2.0/revspec.py0000644000000000000000000000630211722461011013417 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006, 2008 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Loom specific revision-specifiers.""" from bzrlib.plugins.loom.branch import NoLowerThread from bzrlib.plugins.loom.formats import require_loom_branch from bzrlib.revisionspec import RevisionSpec, RevisionInfo class LoomRevisionSpec(RevisionSpec): """A revision spec that needs a loom.""" def _match_on(self, branch, revs): return RevisionInfo(branch, None, self._as_revision_id(branch)) def _as_revision_id(self, branch): require_loom_branch(branch) branch.lock_read() try: state = branch.get_loom_state() threads = state.get_threads() return self._as_thread_revision_id(branch, state, threads) finally: branch.unlock() class RevisionSpecBelow(LoomRevisionSpec): """The below: revision specifier.""" help_txt = """Selects the tip of the thread below a thread from a loom. Selects the tip of the thread below a thread in a loom. Examples:: below: -> return the tip of the next lower thread. below:foo -> return the tip of the thread under the one named 'foo' see also: loom, the thread: revision specifier """ prefix = 'below:' def _as_thread_revision_id(self, branch, state, threads): # '' -> next lower # foo -> thread under foo if len(self.spec): index = state.thread_index(self.spec) else: current_thread = branch.nick index = state.thread_index(current_thread) if index < 1: raise NoLowerThread() return threads[index - 1][1] class RevisionSpecThread(LoomRevisionSpec): """The thread: revision specifier.""" help_txt = """Selects the tip of a thread from a loom. Selects the tip of a thread in a loom. Examples:: thread: -> return the tip of the next lower thread. thread:foo -> return the tip of the thread named 'foo' see also: loom, the below: revision specifier """ prefix = 'thread:' def _as_thread_revision_id(self, branch, state, threads): # '' -> next lower # foo -> named if len(self.spec): index = state.thread_index(self.spec) else: current_thread = branch.nick index = state.thread_index(current_thread) - 1 if index < 0: raise NoLowerThread() return threads[index][1] bzr-loom-2.2.0/setup.py0000755000000000000000000000163311722461011013115 0ustar 00000000000000#!/usr/bin/env python2.4 from distutils.core import setup bzr_plugin_name = 'loom' bzr_commands = [ 'combine-thread', 'create-thread', 'down-thread', 'loomify', 'record', 'revert-loom', 'show-loom', 'status', 'up-thread', ] # Disk formats bzr_branch_formats = { "Bazaar-NG Loom branch format 1\n":"Loom branch format 1", "Bazaar-NG Loom branch format 6\n":"Loom branch format 6", } from version import * if __name__ == '__main__': setup(name="Loom", version="2.2.1dev0", description="Loom plugin for bzr.", author="Canonical Ltd", author_email="bazaar@lists.canonical.com", license = "GNU GPL v2", url="https://launchpad.net/bzr-loom", packages=['bzrlib.plugins.loom', 'bzrlib.plugins.loom.tests', ], package_dir={'bzrlib.plugins.loom': '.'}) bzr-loom-2.2.0/tests/0000755000000000000000000000000011722461011012537 5ustar 00000000000000bzr-loom-2.2.0/tree.py0000644000000000000000000002536011722461011012714 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006, 2008 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """The Loom Tree support routines. LoomTreeDecorator decorates any tree which has a loomed branch to give it loom-aware functionality. """ __all__ = ['LoomTreeDecorator'] from bzrlib import ( trace, ui, ) from bzrlib.decorators import needs_write_lock import bzrlib.errors import bzrlib.merge import bzrlib.revision from branch import EMPTY_REVISION class LoomTreeDecorator(object): """Adapt any tree with a loomed branch to give it loom-aware methods. Currently this does not implemeny the Tree protocol itself. The decorated tree is available for use via the decorator. Useful attributes: tree: The decorated tree. branch: The branch of the decorated tree. """ def __init__(self, a_tree): """Decorate a_tree with loom aware methods.""" self.tree = a_tree self.branch = self.tree.branch def _check_switch(self): if self.tree.last_revision() != self.tree.branch.last_revision(): raise bzrlib.errors.BzrCommandError('Cannot switch threads with an' ' out-of-date tree. Please run bzr update.') @needs_write_lock def up_thread(self, merge_type=None): """Move one thread up in the loom.""" self._check_switch() # set it up: current_revision = self.tree.last_revision() threadname = self.tree.branch.nick threads = self.tree.branch.get_loom_state().get_threads() old_thread_rev = None new_thread_name = None new_thread_rev = None # TODO: Factor this out into a search routine. for thread, rev, parents in reversed(threads): if thread == threadname: # found the current thread. old_thread_rev = rev break new_thread_name = thread new_thread_rev = rev if new_thread_rev is None: raise bzrlib.errors.BzrCommandError( 'Cannot move up from the highest thread.') graph = self.tree.branch.repository.get_graph() # special case no-change condition. if new_thread_rev == old_thread_rev: self.tree.branch._set_nick(new_thread_name) return 0 if new_thread_rev == EMPTY_REVISION: new_thread_rev = bzrlib.revision.NULL_REVISION if old_thread_rev == EMPTY_REVISION: old_thread_rev = bzrlib.revision.NULL_REVISION # merge the tree up into the new patch: if merge_type is None: merge_type = bzrlib.merge.Merge3Merger try: merge_controller = bzrlib.merge.Merger.from_revision_ids( None, self.tree, new_thread_rev, revision_graph=graph) except bzrlib.errors.UnrelatedBranches: raise bzrlib.errors.BzrCommandError('corrupt loom: thread %s' ' has no common ancestor with thread %s' % (new_thread_name, threadname)) merge_controller.merge_type = merge_type result = merge_controller.do_merge() # change the tree to the revision of the new thread. parent_trees = [] if new_thread_rev != bzrlib.revision.NULL_REVISION: parent_trees.append((new_thread_rev, merge_controller.other_tree)) # record the merge if: # the old thread != new thread (we have something to record) # and the new thread is not a descendant of old thread if (old_thread_rev != new_thread_rev and not graph.is_ancestor(old_thread_rev, new_thread_rev)): basis_tree = self.tree.basis_tree() basis_tree.lock_read() parent_trees.append((old_thread_rev, basis_tree)) else: basis_tree = None try: self.tree.set_parent_trees(parent_trees) finally: if basis_tree is not None: basis_tree.unlock() if len(parent_trees) == 0: new_thread_rev = bzrlib.revision.NULL_REVISION else: new_thread_rev = parent_trees[0][0] # change the branch self.tree.branch.generate_revision_history(new_thread_rev) # update the branch nick. self.tree.branch._set_nick(new_thread_name) trace.note("Moved to thread '%s'." % new_thread_name) if (basis_tree is not None and not result and not self.tree.changes_from(basis_tree).has_changed()): trace.note("This thread is now empty, you may wish to " 'run "bzr combine-thread" to remove it.') if result != 0: return 1 else: return 0 def up_many(self, merge_type=None, target_thread=None): loom_state = self.branch.get_loom_state() threads = loom_state.get_threads() if target_thread is None: target_thread = threads[-1][0] if self.branch.nick == target_thread: raise bzrlib.errors.BzrCommandError( 'Cannot move up from the highest thread.') else: upper_thread_i = loom_state.thread_index(target_thread) lower_thread_i = loom_state.thread_index(self.branch.nick) if lower_thread_i > upper_thread_i: raise bzrlib.errors.BzrCommandError( "Cannot up-thread to lower thread.") while self.branch.nick != target_thread: old_nick = self.branch.nick result = self.up_thread(merge_type) if result != 0: return result if len(self.tree.get_parent_ids()) > 1: self.tree.commit('Merge %s into %s' % (old_nick, self.branch.nick)) @needs_write_lock def down_thread(self, name=None): """Move to a thread down in the loom. :param name: If None, use the next lower thread; otherwise the nae of the thread to move to. """ self._check_switch() threadname = self.tree.branch.nick state = self.tree.branch.get_loom_state() threads = state.get_threads() old_thread_index = state.thread_index(threadname) old_thread_rev = threads[old_thread_index][1] if name is None: if old_thread_index == 0: raise bzrlib.errors.BzrCommandError( 'Cannot move down from the lowest thread.') new_thread_name, new_thread_rev, _ = threads[old_thread_index - 1] else: new_thread_name = name index = state.thread_index(name) new_thread_rev = threads[index][1] assert new_thread_rev is not None self.tree.branch._set_nick(new_thread_name) if new_thread_rev == old_thread_rev: # fast path no-op changes bzrlib.trace.note("Moved to thread '%s'." % new_thread_name) return 0 if new_thread_rev == EMPTY_REVISION: new_thread_rev = bzrlib.revision.NULL_REVISION if old_thread_rev == EMPTY_REVISION: old_thread_rev = bzrlib.revision.NULL_REVISION repository = self.tree.branch.repository try: basis_tree = self.tree.revision_tree(old_thread_rev) except bzrlib.errors.NoSuchRevisionInTree: basis_tree = repository.revision_tree(old_thread_rev) to_tree = repository.revision_tree(new_thread_rev) result = bzrlib.merge.merge_inner(self.tree.branch, to_tree, basis_tree, this_tree=self.tree) branch_revno, branch_revision = self.tree.branch.last_revision_info() graph = repository.get_graph() new_thread_revno = graph.find_distance_to_null(new_thread_rev, [(branch_revision, branch_revno)]) self.tree.branch.set_last_revision_info(new_thread_revno, new_thread_rev) if new_thread_rev == bzrlib.revision.NULL_REVISION: parent_list = [] else: parent_list = [(new_thread_rev, to_tree)] self.tree.set_parent_trees(parent_list) bzrlib.trace.note("Moved to thread '%s'." % new_thread_name) return result def lock_write(self): self.tree.lock_write() @needs_write_lock def revert_loom(self, thread=None): """Revert the loom. This function takes care of tree state for revert. If the current loom is not altered, the tree is not altered. If it is then the tree will be altered as 'most' appropriate. :param thread: Only revert a single thread. """ self._check_switch() current_thread = self.branch.nick last_rev = self.tree.last_revision() state = self.branch.get_loom_state() old_threads = state.get_threads() current_thread_rev = self.branch.last_revision() if thread is None: self.branch.revert_loom() else: self.branch.revert_thread(thread) state = self.branch.get_loom_state() threads = state.get_threads() threads_dict = state.get_threads_dict() # TODO find the next up thread if needed if not threads_dict: # last thread nuked to_rev = bzrlib.revision.NULL_REVISION elif current_thread != self.branch.nick: # thread change occured to_rev = threads_dict[self.branch.nick][0] else: # same thread tweaked if last_rev == threads_dict[current_thread][0]: return to_rev = threads_dict[current_thread] if current_thread_rev == EMPTY_REVISION: current_thread_rev = bzrlib.revision.NULL_REVISION if to_rev == EMPTY_REVISION: to_rev = bzrlib.revision.NULL_REVISION # the thread changed, do a merge to match. basis_tree = self.tree.branch.repository.revision_tree(current_thread_rev) to_tree = self.tree.branch.repository.revision_tree(to_rev) result = bzrlib.merge.merge_inner(self.tree.branch, to_tree, basis_tree, this_tree=self.tree) self.tree.set_last_revision(to_rev) def unlock(self): self.tree.unlock() bzr-loom-2.2.0/version.py0000644000000000000000000000174311722461011013441 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2010 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Versioning information for bzr-loom.""" __all__ = [ 'bzr_plugin_version', 'bzr_minimum_version', 'bzr_maximum_version', ] bzr_plugin_version = (2, 2, 0, 'final', 0) bzr_minimum_version = (2, 4, 0) bzr_maximum_version = None bzr-loom-2.2.0/tests/__init__.py0000644000000000000000000000341611722461011014654 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Tests for the loom plugin.""" import bzrlib.plugins.loom.branch from bzrlib.tests import TestCaseWithTransport from bzrlib.tests.TestUtil import TestLoader, TestSuite from bzrlib.workingtree import WorkingTree def test_suite(): module_names = [ 'bzrlib.plugins.loom.tests.test_branch', 'bzrlib.plugins.loom.tests.test_loom_io', 'bzrlib.plugins.loom.tests.test_loom_state', 'bzrlib.plugins.loom.tests.test_revspec', 'bzrlib.plugins.loom.tests.test_tree', 'bzrlib.plugins.loom.tests.blackbox', ] loader = TestLoader() return loader.loadTestsFromModuleNames(module_names) class TestCaseWithLoom(TestCaseWithTransport): def get_tree_with_loom(self, path="."): """Get a tree with no commits in loom format.""" # May open on Remote - we want the vfs backed version for loom tests. self.make_branch_and_tree(path) tree = WorkingTree.open(path) bzrlib.plugins.loom.branch.loomify(tree.branch) return tree.bzrdir.open_workingtree() bzr-loom-2.2.0/tests/blackbox.py0000644000000000000000000010224311722461011014700 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006, 2008 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """UI tests for loom.""" import os import bzrlib from bzrlib import branch as _mod_branch from bzrlib import workingtree from bzrlib.plugins.loom.branch import EMPTY_REVISION from bzrlib.plugins.loom.tree import LoomTreeDecorator from bzrlib.plugins.loom.tests import TestCaseWithLoom from bzrlib.revision import NULL_REVISION class TestsWithLooms(TestCaseWithLoom): """A base class with useful helpers for loom blackbox tests.""" def _add_patch(self, tree, name): """Add a patch to a new thread, returning the revid of te commit.""" tree.branch.new_thread(name) tree.branch._set_nick(name) self.build_tree([name]) tree.add(name) return tree.commit(name) def get_vendor_loom(self, path='.'): """Make a loom with a vendor thread. This returns a loom with a vendor thread, which has the current commit recorded in it, but nothing in the basis loom - its empty. """ tree = self.make_branch_and_tree(path) tree.branch.nick = 'vendor' tree.commit('first release') self.run_bzr(['loomify', path]) return tree.bzrdir.open_workingtree() def assert_exception_raised_on_non_loom_branch(self, args): """Helper to check UserError gets raised when commands are run in a non-loomed branch.""" tree = self.make_branch_and_tree('.') tree.branch.nick = 'somenick' out, err = self.run_bzr(args, retcode=3) self.assertEqual('', out) self.assertContainsRe(err, "is not a loom\\.") class TestLoomify(TestCaseWithLoom): def test_loomify_new_branch(self): b = self.make_branch('.') out, err = self.run_bzr(['loomify'], retcode=3) self.assertEqual('', out) self.assertEqual( 'bzr: ERROR: You must specify --base or have a branch nickname set' ' to loomify a branch\n', err) def test_loomify_new_branch_with_nick(self): b = self.make_branch('.') b.nick = 'base' out, err = self.run_bzr(['loomify']) # a loomed branch opens with a unique format b = bzrlib.branch.Branch.open('.') self.assertIsInstance(b, bzrlib.plugins.loom.branch.LoomSupport) threads = b.get_loom_state().get_threads() self.assertEqual( [('base', EMPTY_REVISION, [])], threads) def test_loomify_path(self): b = self.make_branch('foo') b.nick = 'base' out, err = self.run_bzr(['loomify', 'foo']) # a loomed branch opens with a unique format b = bzrlib.branch.Branch.open('foo') self.assertIsInstance(b, bzrlib.plugins.loom.branch.LoomSupport) threads = b.get_loom_state().get_threads() self.assertEqual( [('base', EMPTY_REVISION, [])], threads) def test_loomify_base_option(self): b = self.make_branch('foo') self.run_bzr(['loomify', 'foo', '--base', 'bar']) b = bzrlib.branch.Branch.open('foo') self.assertEqual('bar', b.nick) class TestCreate(TestsWithLooms): def test_create_no_changes(self): tree = self.get_vendor_loom() out, err = self.run_bzr(['create-thread', 'debian']) self.assertEqual('', out) self.assertEqual('', err) revid = tree.last_revision() self.assertEqual( [('vendor', revid, []), ('debian', revid, [])], tree.branch.get_loom_state().get_threads()) self.assertEqual('debian', tree.branch.nick) def test_create_not_end(self): tree = self.get_vendor_loom() tree.branch.new_thread('debian') # now we are at vendor, with debian after, so if we add # feature-foo we should get: # vendor - feature-foo - debian out, err = self.run_bzr(['create-thread', 'feature-foo']) self.assertEqual('', out) self.assertEqual('', err) revid = tree.last_revision() self.assertEqual( [('vendor', revid, []), ('feature-foo', revid, []), ('debian', revid, [])], tree.branch.get_loom_state().get_threads()) self.assertEqual('feature-foo', tree.branch.nick) def test_create_thread_on_non_loomed_branch(self): """We should raise a user-friendly exception if the branch isn't loomed yet.""" self.assert_exception_raised_on_non_loom_branch(['create-thread', 'some-thread']) class TestShow(TestsWithLooms): def test_show_loom(self): """Show the threads in the loom.""" tree = self.get_vendor_loom() self.assertShowLoom(['vendor'], 'vendor') tree.branch.new_thread('debian') self.assertShowLoom(['vendor', 'debian'], 'vendor') tree.branch._set_nick('debian') self.assertShowLoom(['vendor', 'debian'], 'debian') tree.branch.new_thread('patch A', 'vendor') self.assertShowLoom(['vendor', 'patch A', 'debian'], 'debian') tree.branch._set_nick('patch A') self.assertShowLoom(['vendor', 'patch A', 'debian'], 'patch A') def test_show_loom_with_location(self): """Should be able to provide an explicit location to show.""" tree = self.get_vendor_loom('subtree') self.assertShowLoom(['vendor'], 'vendor', 'subtree') def assertShowLoom(self, threads, selected_thread, location=None): """Check expected show-loom output.""" if location: out, err = self.run_bzr(['show-loom', location]) else: out, err = self.run_bzr(['show-loom']) # threads are in oldest-last order. expected_out = '' for thread in reversed(threads): if thread == selected_thread: expected_out += '=>' else: expected_out += ' ' expected_out += thread expected_out += '\n' self.assertEqual(expected_out, out) self.assertEqual('', err) def test_show_loom_on_non_loomed_branch(self): """We should raise a user-friendly exception if the branch isn't loomed yet.""" self.assert_exception_raised_on_non_loom_branch(['show-loom']) class TestStatus(TestsWithLooms): def setUp(self): super(TestStatus, self).setUp() # The test suite resets after each run, so manually register # the loom status hook. try: from bzrlib.hooks import install_lazy_named_hook except ImportError: pass else: from bzrlib.plugins.loom import show_loom_summary install_lazy_named_hook('bzrlib.status', 'hooks', 'post_status', show_loom_summary, 'loom status') def test_status_shows_current_thread(self): # 'bzr status' shows the current thread. tree = self.get_vendor_loom() self._add_patch(tree, 'thread1') out, err = self.run_bzr(['status'], retcode=0) self.assertEqual('', err) self.assertEqual('Current thread: thread1\n', out) def test_status_shows_current_thread_after_status(self): # 'bzr status' shows the current thread after the rest of the status # output. self.build_tree(['hello.c']) tree = self.get_vendor_loom() self._add_patch(tree, 'thread1') out, err = self.run_bzr(['status'], retcode=0) self.assertEqual('', err) self.assertEqual( 'unknown:\n hello.c\nCurrent thread: thread1\n', out) def test_status_on_non_loom_doesnt_error(self): # 'bzr status' on a non-loom doesn't error, despite the decoration # we've added. tree = self.make_branch_and_tree('.') out, err = self.run_bzr(['status'], retcode=0) self.assertEqual('', out) self.assertEqual('', err) def test_thread_in_status_is_up_to_date(self): # The current thread shown in 'bzr status' is updated when we change # threads. tree = self.get_vendor_loom() self._add_patch(tree, 'thread1') self._add_patch(tree, 'thread2') out, err = self.run_bzr(['status'], retcode=0) self.assertEqual('', err) self.assertEqual('Current thread: thread2\n', out) self.run_bzr(['switch', 'thread1'], retcode=0) out, err = self.run_bzr(['status'], retcode=0) self.assertEqual('', err) self.assertEqual('Current thread: thread1\n', out) class TestSwitch(TestsWithLooms): def test_switch_thread_up_does_not_merge(self): tree = self.get_vendor_loom() self._add_patch(tree, 'thread1') rev_id = self._add_patch(tree, 'thread2') loom_tree = LoomTreeDecorator(tree) loom_tree.down_thread('vendor') out, err = self.run_bzr(['switch', 'thread2'], retcode=0) self.assertEqual('', out) self.assertEqual( "All changes applied successfully.\nMoved to thread 'thread2'.\n", err) self.assertEqual([rev_id], tree.get_parent_ids()) def test_switch_bottom(self): # 'bzr switch bottom:' switches to the bottom thread. tree = self.get_vendor_loom() self._add_patch(tree, 'thread1') self._add_patch(tree, 'thread2') self.assertEqual(tree.branch.nick, 'thread2') out, err = self.run_bzr(['switch', 'bottom:'], retcode=0) self.assertEqual('', out) self.assertEqual( "All changes applied successfully.\nMoved to thread 'vendor'.\n", err) def test_switch_top(self): # 'bzr switch top:' switches to the top thread. tree = self.get_vendor_loom() self._add_patch(tree, 'thread1') self._add_patch(tree, 'thread2') LoomTreeDecorator(tree).down_thread('vendor') self.assertEqual(tree.branch.nick, 'vendor') out, err = self.run_bzr(['switch', 'top:'], retcode=0) self.assertEqual('', out) self.assertEqual( "All changes applied successfully.\nMoved to thread 'thread2'.\n", err) def test_switch_dash_b(self): # 'bzr switch -b new-thread' makes and switches to a new thread. tree = self.get_vendor_loom() self._add_patch(tree, 'thread2') LoomTreeDecorator(tree).down_thread('vendor') self.assertEqual(tree.branch.nick, 'vendor') out, err = self.run_bzr(['switch', '-b', 'thread1'], retcode=0) self.assertEqual(tree.branch.nick, 'thread1') self.assertEqual('', out) self.assertEqual('', err) class TestRecord(TestsWithLooms): def test_record_no_change(self): """If there are no changes record should error.""" tree = self.get_tree_with_loom() out, err = self.run_bzr(['record', 'Try to commit.'], retcode=3) self.assertEqual('', out) self.assertEqual( 'bzr: ERROR: No changes to commit\n', err) def test_record_new_thread(self): """Adding a new thread is enough to allow recording.""" tree = self.get_vendor_loom() tree.branch.new_thread('feature') tree.branch._set_nick('feature') out, err = self.run_bzr(['record', 'add feature branch.']) self.assertEqual('Loom recorded.\n', out) self.assertEqual('', err) def test_record_on_non_loomed_branch(self): """We should raise a user-friendly exception if the branch isn't loomed yet.""" self.assert_exception_raised_on_non_loom_branch(['record', 'some message']) class TestDown(TestsWithLooms): def test_down_thread_from_bottom(self): tree = self.get_vendor_loom() out, err = self.run_bzr(['down-thread'], retcode=3) self.assertEqual('', out) self.assertEqual('bzr: ERROR: Cannot move down from the lowest thread.\n', err) def test_down_thread_same_revision(self): """moving down when the revision is unchanged should work.""" tree = self.get_vendor_loom() tree.branch.new_thread('patch') tree.branch._set_nick('patch') rev = tree.last_revision() out, err = self.run_bzr(['down-thread']) self.assertEqual('', out) self.assertEqual("Moved to thread 'vendor'.\n", err) self.assertEqual('vendor', tree.branch.nick) self.assertEqual(rev, tree.last_revision()) def test_down_thread_removes_changes_between_threads(self): tree = self.get_vendor_loom() tree.branch.new_thread('patch') tree.branch._set_nick('patch') rev = tree.last_revision() self.build_tree(['afile']) tree.add('afile') tree.commit('add a file') out, err = self.run_bzr(['down-thread']) self.assertEqual('', out) self.assertEqual( "All changes applied successfully.\n" "Moved to thread 'vendor'.\n", err) self.assertEqual('vendor', tree.branch.nick) # the tree needs to be updated. self.assertEqual(rev, tree.last_revision()) # the branch needs to be updated. self.assertEqual(rev, tree.branch.last_revision()) self.assertFalse(tree.has_filename('afile')) def test_down_thread_switches_history_ok(self): """Do a down thread when the lower patch is not in the r-h of the old.""" tree = self.get_vendor_loom() tree.branch.new_thread('patch') tree.branch._set_nick('vendor') # do a null change in vendor - a new release. vendor_release = tree.commit('new vendor release.', allow_pointless=True) # pop up, then down self.run_bzr(['up-thread']) self.run_bzr(['revert']) out, err = self.run_bzr(['down-thread']) self.assertEqual('', out) self.assertEqual( 'All changes applied successfully.\n' "Moved to thread 'vendor'.\n", err) self.assertEqual('vendor', tree.branch.nick) # the tree needs to be updated. self.assertEqual(vendor_release, tree.last_revision()) # the branch needs to be updated. self.assertEqual(vendor_release, tree.branch.last_revision()) # diff should return 0 - no uncomitted changes. self.run_bzr(['diff']) self.assertEqual([vendor_release], tree.get_parent_ids()) def test_down_thread_works_with_named_thread(self): """Do a down thread when a thread name is given.""" tree = self.get_vendor_loom() rev = tree.last_revision() patch1_id = self._add_patch(tree, 'patch1') patch2_id = self._add_patch(tree, 'patch2') self.assertFalse(rev in [patch1_id, patch2_id]) out, err = self.run_bzr(['down-thread', 'vendor']) self.assertEqual('', out) self.assertEqual( "All changes applied successfully.\n" "Moved to thread 'vendor'.\n", err) self.assertEqual('vendor', tree.branch.nick) # the tree needs to be updated. self.assertEqual(rev, tree.last_revision()) # the branch needs to be updated. self.assertEqual(rev, tree.branch.last_revision()) # Neither of the patch files should have been preserved self.assertFalse(tree.has_filename('patch1')) self.assertFalse(tree.has_filename('patch2')) self.assertEqual(None, tree.path2id('patch1')) self.assertEqual(None, tree.path2id('patch2')) def test_down_thread_on_non_loomed_branch(self): """We should raise a user-friendly exception if the branch isn't loomed yet.""" self.assert_exception_raised_on_non_loom_branch(['down-thread']) def test_down_thread_with_changes(self): """Trying to down-thread with changes causes an error.""" tree = self.get_vendor_loom() tree.branch.new_thread('upper-thread') tree.branch._set_nick('upper-thread') self.build_tree(['new-file']) tree.add('new-file') out, err = self.run_bzr('down-thread', retcode=3) self.assertEqual('bzr: ERROR: Working tree has uncommitted changes.\n', err) class TestUp(TestsWithLooms): def test_up_thread_from_top(self): tree = self.get_vendor_loom() out, err = self.run_bzr(['up-thread'], retcode=3) self.assertEqual('', out) self.assertEqual( 'bzr: ERROR: Cannot move up from the highest thread.\n', err) def test_up_thread_same_revision(self): """moving up when the revision is unchanged should work.""" tree = self.get_vendor_loom() tree.branch.new_thread('patch') tree.branch._set_nick('vendor') rev = tree.last_revision() out, err = self.run_bzr(['up-thread']) self.assertEqual('', out) self.assertEqual('', err) self.assertEqual('patch', tree.branch.nick) self.assertEqual(rev, tree.last_revision()) def test_up_thread_manual_preserves_changes(self): tree = self.get_vendor_loom() tree.branch.new_thread('patch') tree.branch._set_nick('vendor') patch_rev = tree.last_revision() # add a change in vendor - a new release. self.build_tree(['afile']) tree.add('afile') vendor_release = tree.commit('new vendor release adds a file.') out, err = self.run_bzr(['up-thread', '--manual']) self.assertEqual('', out) self.assertEqual( "All changes applied successfully.\n" "Moved to thread 'patch'.\n" 'This thread is now empty, you may wish to run "bzr ' 'combine-thread" to remove it.\n', err) self.assertEqual('patch', tree.branch.nick) # the tree needs to be updated. self.assertEqual(patch_rev, tree.last_revision()) # the branch needs to be updated. self.assertEqual(patch_rev, tree.branch.last_revision()) self.assertTrue(tree.has_filename('afile')) # diff should return 1 now as we have uncommitted changes. self.run_bzr(['diff'], retcode=1) self.assertEqual([patch_rev, vendor_release], tree.get_parent_ids()) def test_up_thread_manual_rejects_specified_thread(self): tree = self.get_vendor_loom() tree.branch.new_thread('patch') out, err = self.run_bzr('up-thread --manual patch', retcode=3) self.assertContainsRe(err, 'Specifying a thread does not work with' ' --manual.') def test_up_thread_gets_conflicts(self): """Do a change in both the baseline and the next patch up.""" tree = self.get_vendor_loom() tree.branch.new_thread('patch') tree.branch._set_nick('patch') # add a change in patch - a new release. self.build_tree(['afile']) tree.add('afile') patch_rev = tree.commit('add afile as a patch') # add a change in vendor - a new release. self.run_bzr(['down-thread']) self.build_tree(['afile']) tree.add('afile') vendor_release = tree.commit('new vendor release adds a file.') # we want conflicts. out, err = self.run_bzr(['up-thread'], retcode=1) self.assertEqual('', out) self.assertEqual( 'Conflict adding file afile. Moved existing file to afile.moved.\n' '1 conflicts encountered.\n' "Moved to thread 'patch'.\n", err) self.assertEqual('patch', tree.branch.nick) # the tree needs to be updated. self.assertEqual(patch_rev, tree.last_revision()) # the branch needs to be updated. self.assertEqual(patch_rev, tree.branch.last_revision()) self.assertTrue(tree.has_filename('afile')) # diff should return 1 now as we have uncommitted changes. self.run_bzr(['diff'], retcode=1) self.assertEqual([patch_rev, vendor_release], tree.get_parent_ids()) def test_up_thread_on_non_loomed_branch(self): """We should raise a user-friendly exception if the branch isn't loomed yet.""" self.assert_exception_raised_on_non_loom_branch(['up-thread']) def test_up_thread_accepts_merge_type(self): tree = self.get_vendor_loom() self.run_bzr(['create-thread', 'top']) self.run_bzr(['down-thread']) self.run_bzr(['up-thread', '--lca']) def test_up_thread_no_manual(self): tree = self.get_vendor_loom() tree.branch.new_thread('middle') tree.branch.new_thread('top') self.run_bzr('up-thread') branch = _mod_branch.Branch.open('.') self.assertEqual('top', branch.nick) def test_up_with_clean_merge_leaving_thread_empty(self): """This tests what happens when a thread becomes empty. A thread becomes empty when all its changes are included in a lower thread, and so its diff to the thread below contains nothing. The user should be warned when this happens. """ tree = self.get_vendor_loom() self.build_tree(['afile']) tree.add('afile') patch_rev = tree.commit('add afile in base') tree.branch.new_thread('patch') tree.branch._set_nick('patch') # make a change to afile in patch. f = open('afile', 'wb') try: f.write('new contents of afile\n') finally: f.close() patch_rev = tree.commit('make a change to afile') # make the same change in vendor. self.run_bzr(['down-thread']) f = open('afile', 'wb') try: f.write('new contents of afile\n') finally: f.close() vendor_release = tree.commit('make the same change to afile') # check that the trees no longer differ after the up merge, # and that we are out, err = self.run_bzr(['up-thread', '--manual']) self.assertEqual('', out) self.assertStartsWith(err, "All changes applied successfully.\n" "Moved to thread 'patch'.\n" 'This thread is now empty, you may wish to run "bzr ' 'combine-thread" to remove it.\n') self.assertEqual('patch', tree.branch.nick) # the tree needs to be updated. self.assertEqual(patch_rev, tree.last_revision()) # the branch needs to be updated. self.assertEqual(patch_rev, tree.branch.last_revision()) self.assertTrue(tree.has_filename('afile')) # diff should return 0 now as we have no uncommitted changes. self.run_bzr(['diff']) self.assertEqual([patch_rev, vendor_release], tree.get_parent_ids()) def test_up_thread_accepts_thread(self): tree = self.get_vendor_loom() tree.branch.new_thread('lower-middle') tree.branch.new_thread('upper-middle') tree.branch.new_thread('top') self.run_bzr('up-thread upper-middle') branch = _mod_branch.Branch.open('.') self.assertEqual('upper-middle', branch.nick) class TestPush(TestsWithLooms): def test_push(self): """Integration smoke test for bzr push of a loom.""" tree = self.get_vendor_loom('source') tree.branch.record_loom('commit loom.') os.chdir('source') out, err = self.run_bzr(['push', '../target']) os.chdir('..') self.assertEqual('', out) self.assertEqual('Created new branch.\n', err) # lower level tests check behaviours, just check show-loom as a smoke # test. out, err = self.run_bzr(['show-loom', 'target']) self.assertEqual('=>vendor\n', out) self.assertEqual('', err) class TestBranch(TestsWithLooms): def test_branch(self): """Integration smoke test for bzr branch of a loom.""" tree = self.get_vendor_loom('source') tree.branch.record_loom('commit loom.') out, err = self.run_bzr(['branch', 'source', 'target']) self.assertEqual('', out) self.assertTrue( err == 'Branched 1 revision(s).\n' or err == 'Branched 1 revision.\n') # lower level tests check behaviours, just check show-loom as a smoke # test. out, err = self.run_bzr(['show-loom', 'target']) self.assertEqual('=>vendor\n', out) self.assertEqual('', err) class TestPull(TestsWithLooms): def test_pull(self): """Integration smoke test for bzr pull loom to loom.""" tree = self.get_vendor_loom('source') tree.branch.record_loom('commit loom.') tree.bzrdir.sprout('target') tree.commit('change the source', allow_pointless=True) tree.branch.new_thread('foo') LoomTreeDecorator(tree).up_thread() tree.branch.record_loom('commit loom again.') os.chdir('target') try: out, err = self.run_bzr(['pull']) finally: os.chdir('..') self.assertStartsWith(out, 'Using saved parent location:') self.assertEndsWith(out, 'Now on revision 2.\n') self.assertEqual( 'All changes applied successfully.\n', err) # lower level tests check behaviours, just check show-loom as a smoke # test. out, err = self.run_bzr(['show-loom', 'target']) self.assertEqual('=>foo\n vendor\n', out) self.assertEqual('', err) class TestRevert(TestsWithLooms): def test_revert_loom(self): """bzr revert-loom should give help.""" tree = self.get_vendor_loom() out, err = self.run_bzr(['revert-loom']) self.assertEqual('', out) self.assertEqual('Please see revert-loom -h.\n', err) def test_revert_loom_missing_thread(self): """bzr revert-loom missing-thread should give an error.""" tree = self.get_vendor_loom() out, err = self.run_bzr(['revert-loom', 'unknown-thread'], retcode=3) self.assertEqual('', out) self.assertEqual("bzr: ERROR: No such thread 'unknown-thread'.\n", err) def test_revert_loom_all(self): """bzr revert-loom --all should restore the state of a loom.""" tree = self.get_vendor_loom() tree.branch.new_thread('foo') last_rev = tree.last_revision() self.assertNotEqual(NULL_REVISION, last_rev) out, err = self.run_bzr(['revert-loom', '--all']) self.assertEqual('', out) self.assertEqual( 'All changes applied successfully.\n' 'All threads reverted.\n', err) self.assertNotEqual(last_rev, tree.last_revision()) self.assertEqual(NULL_REVISION, tree.last_revision()) self.assertEqual([], tree.branch.get_loom_state().get_threads()) def test_revert_thread(self): """bzr revert-loom threadname should restore the state of that thread.""" # we want a loom with > 1 threads, with a change made to a thread we are # not in, so we can revert that by name, tree = self.get_vendor_loom() tree.branch.new_thread('after-vendor') tree.branch._set_nick('after-vendor') tree.commit('after-vendor commit', allow_pointless=True) tree.branch.record_loom('save loom with vendor and after-vendor') old_threads = tree.branch.get_loom_state().get_threads() tree.commit('after-vendor commit 2', allow_pointless=True) LoomTreeDecorator(tree).down_thread() last_rev = tree.last_revision() self.assertNotEqual(NULL_REVISION, last_rev) out, err = self.run_bzr(['revert-loom', 'after-vendor']) self.assertEqual('', out) self.assertEqual("thread 'after-vendor' reverted.\n", err) self.assertEqual(last_rev, tree.last_revision()) self.assertEqual(old_threads, tree.branch.get_loom_state().get_threads()) def test_revert_loom_on_non_loomed_branch(self): """We should raise a user-friendly exception if the branch isn't loomed yet.""" self.assert_exception_raised_on_non_loom_branch(['revert-loom', 'foobar']) class TestCombineThread(TestsWithLooms): """Tests for combine-thread.""" def test_combine_last_thread(self): """Doing combine thread on the last thread is an error for now.""" tree = self.get_vendor_loom() out, err = self.run_bzr(['combine-thread'], retcode=3) self.assertEqual('', out) self.assertEqual('bzr: ERROR: Cannot combine threads on the bottom thread.\n', err) def get_two_thread_loom(self): tree = self.get_vendor_loom() tree.branch.new_thread('above-vendor') loom_tree = LoomTreeDecorator(tree) loom_tree.up_thread() self.build_tree(['file-a']) tree.add('file-a') tree.commit('change the tree', rev_id='above-vendor-1') loom_tree.down_thread() return tree, loom_tree def get_loom_with_unique_thread(self): """Return a loom with a unique thread. That is: vendor:[] unique-thread:[vendor] above-vendor:[vendor] - unique-thread has work not in vendor and not in above-vendor. The returned loom is on the vendor thread. """ tree, _ = self.get_two_thread_loom() tree.branch.new_thread('unique-thread', 'vendor') loom_tree = LoomTreeDecorator(tree) loom_tree.up_thread() self.build_tree(['file-b']) tree.add('file-b') tree.commit('a unique change', rev_id='uniquely-yrs-1') loom_tree.down_thread() return tree, loom_tree def test_combine_unmerged_thread_force(self): """Combining a thread with unique work works with --force.""" tree, loom_tree = self.get_loom_with_unique_thread() vendor_revid = tree.last_revision() loom_tree.up_thread() out, err = self.run_bzr(['combine-thread', '--force']) self.assertEqual('', out) self.assertEqual( "Combining thread 'unique-thread' into 'vendor'\n" 'All changes applied successfully.\n' "Moved to thread 'vendor'.\n", err) self.assertEqual(vendor_revid, tree.last_revision()) self.assertEqual('vendor', tree.branch.nick) def test_combine_unmerged_thread_errors(self): """Combining a thread with unique work errors without --force.""" tree, loom_tree = self.get_loom_with_unique_thread() loom_tree.up_thread() unique_revid = tree.last_revision() out, err = self.run_bzr(['combine-thread'], retcode=3) self.assertEqual('', out) self.assertEqual("bzr: ERROR: " "Thread 'unique-thread' has unmerged work. Use --force to combine anyway.\n", err) self.assertEqual(unique_revid, tree.last_revision()) self.assertEqual('unique-thread', tree.branch.nick) def test_combine_last_two_threads(self): """Doing a combine on two threads gives you just the bottom one.""" tree, loom_tree = self.get_two_thread_loom() # now we have a change between the threads, so merge this into the lower # thread to simulate real-world - different rev ids, and the lower # thread has merged the upper. # ugh, should make merge easier to use. self.run_bzr(['merge', '-r', 'thread:above-vendor', '.']) vendor_revid = tree.commit('merge in the above-vendor work.') loom_tree.up_thread() out, err = self.run_bzr(['combine-thread']) self.assertEqual('', out) self.assertEqual( "Combining thread 'above-vendor' into 'vendor'\n" 'All changes applied successfully.\n' "Moved to thread 'vendor'.\n", err) self.assertEqual(vendor_revid, tree.last_revision()) self.assertEqual('vendor', tree.branch.nick) def test_combine_lowest_thread(self): """Doing a combine on two threads gives you just the bottom one.""" tree, loom_tree = self.get_two_thread_loom() self.run_bzr('combine-thread') tree = workingtree.WorkingTree.open('.') self.assertEqual('above-vendor', tree.branch.nick) self.assertEqual('above-vendor-1', tree.last_revision()) def test_combine_thread_on_non_loomed_branch(self): """We should raise a user-friendly exception if the branch isn't loomed yet.""" self.assert_exception_raised_on_non_loom_branch(['combine-thread']) class TestExportLoom(TestsWithLooms): """Tests for export-loom.""" def test_export_loom_no_args(self): """Test exporting with no arguments""" tree = self.get_vendor_loom() err = self.run_bzr(['export-loom'], retcode=3)[1] self.assertContainsRe(err, 'bzr: ERROR: No export root known or specified.') def test_export_loom_config(self): tree = self.get_vendor_loom() tree.branch.get_config().set_user_option('export_loom_root', 'foo') err = self.run_bzr(['export-loom'])[1] self.assertContainsRe(err, 'Creating branch at .*/work/foo/vendor/\n') def test_export_loom_path(self): """Test exporting with specified path""" tree = self.get_vendor_loom() self.run_bzr(['export-loom', 'export-path']) branch = bzrlib.branch.Branch.open('export-path/vendor') bzr-loom-2.2.0/tests/test_branch.py0000644000000000000000000007107211722461011015414 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Tests of the loom Branch related routines.""" import bzrlib from bzrlib.branch import Branch import bzrlib.errors as errors from bzrlib.plugins.loom.branch import ( AlreadyLoom, EMPTY_REVISION, loomify, require_loom_branch, NotALoom, UnsupportedBranchFormat, ) from bzrlib.plugins.loom.tests import TestCaseWithLoom from bzrlib.plugins.loom.tree import LoomTreeDecorator import bzrlib.revision from bzrlib.revision import NULL_REVISION from bzrlib.tests import ( TestCaseWithTransport, test_server, ) from bzrlib.transport import get_transport from bzrlib.workingtree import WorkingTree class TestFormat(TestCaseWithTransport): def test_disk_format(self): bzrdir = self.make_bzrdir('.') bzrdir.create_repository() format = bzrlib.plugins.loom.branch.BzrBranchLoomFormat1() branch = format.initialize(bzrdir) self.assertFileEqual('Loom current 1\n\n', '.bzr/branch/last-loom') class StubFormat(object): def network_name(self): return "Nothing to see." class LockableStub(object): def __init__(self): self._calls = [] self._format = StubFormat() def lock_write(self): self._calls.append(("write",)) def unlock(self): self._calls.append(("unlock",)) class TestRequireLoomBranch(TestCaseWithTransport): def test_on_non_loom(self): branch = self.make_branch('.') self.assertRaises(NotALoom, require_loom_branch, branch) def works_on_format(self, format): branch = self.make_branch('.', format) loomify(branch) # reopen it branch = branch.bzrdir.open_branch() self.assertEqual(None, require_loom_branch(branch)) def test_works_on_loom1(self): self.works_on_format('knit') def test_works_on_loom6(self): self.works_on_format('pack-0.92') def test_works_on_loom7(self): self.works_on_format('1.6') def test_no_harm_to_looms(self): branch = self.make_branch('.') loomify(branch) branch = branch.bzrdir.open_branch() self.assertRaises(AlreadyLoom, loomify, branch) class TestLoomify(TestCaseWithTransport): def assertConvertedBranchFormat(self, branch, branch_class, format): """Assert that branch has been successfully converted to a loom.""" self.assertFalse(branch.is_locked()) # a loomed branch opens with a different format branch = bzrlib.branch.Branch.open('.') self.assertIsInstance(branch, branch_class) self.assertIsInstance(branch._format, format) # and it should have no recorded loom content so we can do self.assertFileEqual('Loom current 1\n\n', '.bzr/branch/last-loom') self.assertEqual([], branch.loom_parents()) def test_loomify_locks_branch(self): # loomify should take out a lock even on a bogus format as someone # might e.g. change the format if you don't hold the lock - its what we # are about to do! branch = LockableStub() self.assertRaises(UnsupportedBranchFormat, loomify, branch) self.assertEqual([("write",), ("unlock",)], branch._calls) def test_loomify_unknown_format(self): branch = self.make_branch('.', format='weave') self.assertRaises(UnsupportedBranchFormat, loomify, branch) self.assertFalse(branch.is_locked()) def test_loomify_branch_format_5(self): branch = self.make_branch('.', format='dirstate') loomify(branch) self.assertConvertedBranchFormat(branch, bzrlib.plugins.loom.branch.LoomBranch, bzrlib.plugins.loom.branch.BzrBranchLoomFormat1) def test_loomify_branch_format_6(self): branch = self.make_branch('.', format='dirstate-tags') loomify(branch) self.assertConvertedBranchFormat(branch, bzrlib.plugins.loom.branch.LoomBranch6, bzrlib.plugins.loom.branch.BzrBranchLoomFormat6) def test_loomify_branch_format_7(self): branch = self.make_branch('.', format='1.6') loomify(branch) self.assertConvertedBranchFormat(branch, bzrlib.plugins.loom.branch.LoomBranch7, bzrlib.plugins.loom.branch.BzrBranchLoomFormat7) class TestLoom(TestCaseWithLoom): def make_loom(self, path): bzrlib.plugins.loom.branch.loomify(self.make_branch(path)) return bzrlib.branch.Branch.open(path) def test_new_thread_empty_branch(self): branch = self.make_loom('.') branch.new_thread('foo') # assert that no loom data is committed, this change should # have been current-loom only self.assertEqual([], branch.loom_parents()) self.assertEqual( [('foo', EMPTY_REVISION, [])], branch.get_loom_state().get_threads()) branch.new_thread('bar') self.assertEqual([], branch.loom_parents()) self.assertEqual( [('foo', EMPTY_REVISION, []), ('bar', EMPTY_REVISION, [])], branch.get_loom_state().get_threads()) def test_new_thread_no_duplicate_names(self): branch = self.make_loom('.') branch.new_thread('foo') self.assertRaises(bzrlib.plugins.loom.branch.DuplicateThreadName, branch.new_thread, 'foo') self.assertEqual( [('foo', EMPTY_REVISION, [])], branch.get_loom_state().get_threads()) def get_tree_with_one_commit(self, path='.'): """Get a tree with a commit in loom format.""" tree = self.get_tree_with_loom(path=path) rev_id = tree.commit('first post') return tree def test_new_thread_with_commits(self): """Test converting a branch to a loom once it has commits.""" tree = self.get_tree_with_one_commit() tree.branch.new_thread('foo') self.assertEqual( [('foo', tree.last_revision(), [])], tree.branch.get_loom_state().get_threads()) def test_new_thread_after(self): """Test adding a thread at a nominated position.""" tree = self.get_tree_with_one_commit() rev_id = tree.last_revision() tree.branch.new_thread('baseline') tree.branch.new_thread('middlepoint') tree.branch.new_thread('endpoint') tree.branch._set_nick('middlepoint') rev_id2 = tree.commit('middle', allow_pointless=True) tree.branch._set_nick('endpoint') rev_id3 = tree.commit('end', allow_pointless=True) tree.branch.new_thread('afterbase', 'baseline') tree.branch.new_thread('aftermiddle', 'middlepoint') tree.branch.new_thread('atend', 'endpoint') self.assertEqual( [('baseline', rev_id, []), ('afterbase', rev_id, []), ('middlepoint', rev_id2, []), ('aftermiddle', rev_id2, []), ('endpoint', rev_id3, []), ('atend', rev_id3, []), ], tree.branch.get_loom_state().get_threads()) def test_record_loom_no_changes(self): tree = self.get_tree_with_loom() self.assertRaises(errors.PointlessCommit, tree.branch.record_loom, 'foo') def test_record_thread(self): tree = self.get_tree_with_one_commit() tree.branch.new_thread('baseline') tree.branch.new_thread('tail') tree.branch._set_nick('baseline') first_rev = tree.last_revision() # lock the tree to prevent unlock triggering implicit record tree.lock_write() try: tree.commit('change something', allow_pointless=True) self.assertEqual( [('baseline', first_rev, []), ('tail', first_rev, [])], tree.branch.get_loom_state().get_threads()) tree.branch.record_thread('baseline', tree.last_revision()) self.assertEqual( [('baseline', tree.last_revision(), []), ('tail', first_rev, [])], tree.branch.get_loom_state().get_threads()) self.assertEqual([], tree.branch.loom_parents()) finally: tree.unlock() def test_clone_empty_loom(self): source_tree = self.get_tree_with_loom('source') source_tree.branch._set_nick('source') target_tree = source_tree.bzrdir.clone('target').open_workingtree() self.assertLoomSproutedOk(source_tree, target_tree) def test_sprout_empty_loom(self): source_tree = self.get_tree_with_loom('source') target_tree = source_tree.bzrdir.sprout('target').open_workingtree() self.assertLoomSproutedOk(source_tree, target_tree) def test_clone_nonempty_loom_top(self): """Cloning a nonempty loom at the top should preserve the loom.""" source_tree = self.get_tree_with_one_commit('source') source_tree.branch.new_thread('bottom') source_tree.branch.new_thread('top') source_tree.branch._set_nick('top') source_tree.commit('phwoar', allow_pointless=True) source_tree.branch.record_loom('commit to loom') target_tree = source_tree.bzrdir.clone('target').open_workingtree() self.assertLoomSproutedOk(source_tree, target_tree) def test_clone_nonempty_loom_bottom(self): """Cloning loom should reset the current loom pointer.""" self.make_and_clone_simple_loom() def make_and_clone_simple_loom(self): source_tree = self.get_tree_with_one_commit('source') source_tree.branch.new_thread('bottom') source_tree.branch.new_thread('top') source_tree.branch._set_nick('top') source_tree.commit('phwoar', allow_pointless=True) source_tree.branch.record_loom('commit to loom') LoomTreeDecorator(source_tree).down_thread() # now clone from the 'default url' - transport_server rather than # vfs_server. source_branch = Branch.open(self.get_url('source')) target_tree = source_branch.bzrdir.sprout('target').open_workingtree() self.assertLoomSproutedOk(source_tree, target_tree) def test_sprout_remote_loom(self): # RemoteBranch should permit sprouting properly. self.transport_server = test_server.SmartTCPServer_for_testing self.make_and_clone_simple_loom() def test_sprout_nonempty_loom_bottom(self): """Sprouting always resets the loom to the top.""" source_tree = self.get_tree_with_one_commit('source') source_tree.branch.new_thread('bottom') source_tree.branch.new_thread('top') source_tree.branch._set_nick('top') source_tree.commit('phwoar', allow_pointless=True) source_tree.branch.record_loom('commit to loom') LoomTreeDecorator(source_tree).down_thread() # now sprout target_tree = source_tree.bzrdir.sprout('target').open_workingtree() self.assertLoomSproutedOk(source_tree, target_tree) def assertLoomSproutedOk(self, source_tree, target_tree): """A sprout resets the loom to the top to ensure up-thread works. Due to the calls made, this will ensure the loom content has been pulled, and that the tree state is correct. """ # the loom pointer has a parent of the source looms tip source_tree.lock_write() self.addCleanup(source_tree.unlock) source_parents = source_tree.branch.loom_parents() self.assertEqual( source_parents[:1], target_tree.branch.loom_parents()) # the branch nick is the top warp. source_threads = source_tree.branch.get_threads( source_tree.branch.get_loom_state().get_basis_revision_id()) if source_threads: self.assertEqual( source_threads[-1][0], target_tree.branch.nick) # no threads, nick is irrelevant # check that the working threads were created correctly: # the same revid for the parents as the created one. self.assertEqual( [thread + ([thread[1]],) for thread in source_threads], target_tree.branch.get_loom_state().get_threads()) # check content is mirrored for thread, rev_id in source_threads: self.assertTrue(target_tree.branch.repository.has_revision(rev_id)) # TODO: refactor generate_revision further into a revision # creation routine and a set call: until then change the source # to the right thread and compare if source_threads: source_tree.branch.generate_revision_history(source_threads[-1][1]) self.assertEqual( source_tree.branch.last_revision_info(), target_tree.branch.last_revision_info()) def test_pull_loom_at_bottom(self): """Pulling from a loom when in the bottom warp pulls all warps.""" source = self.get_tree_with_loom('source') source.branch.new_thread('bottom') source.branch.new_thread('top') source.branch._set_nick('bottom') source.branch.record_loom('commit to loom') target = source.bzrdir.sprout('target').open_branch() target._set_nick('top') # put a commit in the bottom and top of this loom bottom_rev1 = source.commit('commit my arse') source_loom_tree = LoomTreeDecorator(source) source_loom_tree.up_thread() top_rev1 = source.commit('integrate bottom changes.') source_loom_tree.down_thread() # and now another commit at the bottom bottom_rev2 = source.commit('bottom 2', allow_pointless=True) source.branch.record_loom('commit to loom again') # we now have two commits in the bottom warp, one in the top, and # all three should be pulled. We are pulling into a loom which has # a different current thread too, which should not affect us. target.pull(source.branch) for rev in (bottom_rev1, bottom_rev2, top_rev1): self.assertTrue(target.repository.has_revision(rev)) # check loom threads threads = target.get_loom_state().get_threads() self.assertEqual( [('bottom', bottom_rev2, [bottom_rev2]), ('top', top_rev1, [top_rev1])], threads) # check loom tip was pulled loom_rev_ids = source.branch.loom_parents() for rev_id in loom_rev_ids: self.assertTrue(target.repository.has_revision(rev_id)) self.assertEqual(source.branch.loom_parents(), target.loom_parents()) def test_pull_into_empty_loom(self): """Doing a pull into a loom with no loom revisions works.""" self.pull_into_empty_loom() def pull_into_empty_loom(self): source = self.get_tree_with_loom('source') target = source.bzrdir.sprout('target').open_branch() source.branch.new_thread('a thread') source.branch._set_nick('a thread') # put a commit in the thread for source. bottom_rev1 = source.commit('commit a thread') source.branch.record_loom('commit to loom') # now pull from the 'default url' - transport_server rather than # vfs_server - this may be a RemoteBranch. source_branch = Branch.open(self.get_url('source')) target.pull(source_branch) # check loom threads threads = target.get_loom_state().get_threads() self.assertEqual( [('a thread', bottom_rev1, [bottom_rev1])], threads) # check loom tip was pulled loom_rev_ids = source.branch.loom_parents() for rev_id in loom_rev_ids: self.assertTrue(target.repository.has_revision(rev_id)) self.assertEqual(source.branch.loom_parents(), target.loom_parents()) def test_pull_remote_loom(self): # RemoteBranch should permit sprouting properly. self.transport_server = test_server.SmartTCPServer_for_testing self.pull_into_empty_loom() def test_pull_thread_at_null(self): """Doing a pull when the source loom has a thread with no history.""" source = self.get_tree_with_loom('source') target = source.bzrdir.sprout('target').open_branch() source.branch.new_thread('a thread') source.branch._set_nick('a thread') source.branch.record_loom('commit to loom') target.pull(source.branch) # check loom threads threads = target.get_loom_state().get_threads() self.assertEqual( [('a thread', 'empty:', ['empty:'])], threads) # check loom tip was pulled loom_rev_ids = source.branch.loom_parents() for rev_id in loom_rev_ids: self.assertTrue(target.repository.has_revision(rev_id)) self.assertEqual(source.branch.loom_parents(), target.loom_parents()) def test_push_loom_loom(self): """Pushing a loom to a loom copies the current loom state.""" source = self.get_tree_with_loom('source') source.branch.new_thread('bottom') source.branch.new_thread('top') source.branch._set_nick('bottom') source.branch.record_loom('commit to loom') target = source.bzrdir.sprout('target').open_branch() target._set_nick('top') # put a commit in the bottom and top of this loom bottom_rev1 = source.commit('commit bottom') source_loom_tree = LoomTreeDecorator(source) source_loom_tree.up_thread() top_rev1 = source.commit('integrate bottom changes.') source_loom_tree.down_thread() # and now another commit at the bottom bottom_rev2 = source.commit('bottom 2', allow_pointless=True) source.branch.record_loom('commit to loom again') # we now have two commits in the bottom warp, one in the top, and # all three should be pulled. We are pushing into a loom which has # a different current thread too : that should not affect us. source.branch.push(target) for rev in (bottom_rev1, bottom_rev2, top_rev1): self.assertTrue(target.repository.has_revision(rev)) # check loom threads threads = target.get_loom_state().get_threads() self.assertEqual( [('bottom', bottom_rev2, [bottom_rev2]), ('top', top_rev1, [top_rev1])], threads) # check loom tip was pulled loom_rev_ids = source.branch.loom_parents() for rev_id in loom_rev_ids: self.assertTrue(target.repository.has_revision(rev_id)) self.assertEqual(source.branch.loom_parents(), target.loom_parents()) def test_implicit_record(self): tree = self.get_tree_with_loom('source') tree.branch.new_thread('bottom') tree.branch._set_nick('bottom') tree.lock_write() try: bottom_rev1 = tree.commit('commit my arse') # regular commands should not record self.assertEqual( [('bottom', EMPTY_REVISION, [])], tree.branch.get_loom_state().get_threads()) finally: tree.unlock() # unlocking should have detected the discrepancy and recorded. self.assertEqual( [('bottom', bottom_rev1, [])], tree.branch.get_loom_state().get_threads()) def test_trivial_record_loom(self): tree = self.get_tree_with_loom() # for this test, we want to ensure that we have an empty loom-branch. self.assertEqual([], tree.branch.loom_parents()) # add a thread and record it. tree.branch.new_thread('bottom') tree.branch._set_nick('bottom') rev_id = tree.branch.record_loom('Setup test loom.') # after recording, the parents list should have changed. self.assertEqual([rev_id], tree.branch.loom_parents()) def test_revert_loom(self): tree = self.get_tree_with_loom() # ensure we have some stuff to revert # new threads tree.branch.new_thread('foo') tree.branch.new_thread('bar') tree.branch._set_nick('bar') last_rev = tree.branch.last_revision() # and a change to the revision history of this thread tree.commit('change bar', allow_pointless=True) tree.branch.revert_loom() # the threads list should be restored self.assertEqual([], tree.branch.get_loom_state().get_threads()) self.assertEqual(last_rev, tree.branch.last_revision()) def test_revert_loom_changes_current_thread_history(self): tree = self.get_tree_with_loom() # new threads tree.branch.new_thread('foo') tree.branch.new_thread('bar') tree.branch._set_nick('bar') # and a change to the revision history of this thread tree.commit('change bar', allow_pointless=True) # now record tree.branch.record_loom('change bar') last_rev = tree.branch.last_revision() # and a change to the revision history of this thread to revert tree.commit('change bar', allow_pointless=True) tree.branch.revert_loom() # the threads list should be restored self.assertEqual( [(u'foo', 'empty:', [EMPTY_REVISION]), (u'bar', last_rev, [last_rev])], tree.branch.get_loom_state().get_threads()) self.assertEqual(last_rev, tree.branch.last_revision()) def test_revert_loom_remove_current_thread_mid_loom(self): # given the loom Base, => mid, top, with a basis of Base, top, revert # of the loom should end up with Base, =>top, including last-revision # changes tree = self.get_tree_with_loom() tree = LoomTreeDecorator(tree) # new threads tree.branch.new_thread('base') tree.branch.new_thread('top') tree.branch._set_nick('top') # and a change to the revision history of this thread tree.tree.commit('change top', allow_pointless=True) last_rev = tree.branch.last_revision() # now record tree.branch.record_loom('change top') tree.down_thread() tree.branch.new_thread('middle', 'base') tree.up_thread() self.assertEqual('middle', tree.branch.nick) tree.branch.revert_loom() # the threads list should be restored self.assertEqual( [('base', 'empty:', [EMPTY_REVISION]), ('top', last_rev, [last_rev])], tree.branch.get_loom_state().get_threads()) self.assertEqual(last_rev, tree.branch.last_revision()) def test_revert_thread_not_in_basis(self): tree = self.get_tree_with_loom() # ensure we have some stuff to revert tree.branch.new_thread('foo') tree.branch.new_thread('bar') # do a commit, so the last_revision should change. tree.branch._set_nick('bar') tree.commit('bar-ness', allow_pointless=True) tree.branch.revert_thread('bar') self.assertEqual( [('foo', EMPTY_REVISION, [])], tree.branch.get_loom_state().get_threads()) self.assertEqual(NULL_REVISION, tree.branch.last_revision()) def test_revert_thread_in_basis(self): tree = self.get_tree_with_loom() # ensure we have some stuff to revert tree.branch.new_thread('foo') tree.branch.new_thread('bar') tree.branch._set_nick('foo') # record the loom to put the threads in the basis tree.branch.record_loom('record it!') # do a commit, so the last_revision should change. tree.branch._set_nick('bar') tree.commit('bar-ness', allow_pointless=True) tree.branch.revert_thread('bar') self.assertEqual( [('foo', EMPTY_REVISION, [EMPTY_REVISION]), ('bar', EMPTY_REVISION, [EMPTY_REVISION]), ], tree.branch.get_loom_state().get_threads()) self.assertTrue(NULL_REVISION, tree.branch.last_revision()) def test_remove_thread(self): tree = self.get_tree_with_loom() tree.branch.new_thread('bar') tree.branch.new_thread('foo') tree.branch._set_nick('bar') tree.branch.remove_thread('foo') state = tree.branch.get_loom_state() self.assertEqual([('bar', 'empty:', [])], state.get_threads()) def test_get_threads_null(self): tree = self.get_tree_with_loom() # with no commmits in the loom: self.assertEqual([], tree.branch.get_threads(NULL_REVISION)) # and loom history should make no difference: tree.branch.new_thread('foo') tree.branch._set_nick('foo') tree.branch.record_loom('foo') self.assertEqual([], tree.branch.get_threads(NULL_REVISION)) def get_multi_threaded(self): tree = self.get_tree_with_loom() tree.branch.new_thread('thread1') tree.branch._set_nick('thread1') tree.commit('thread1', rev_id='thread1-id') tree.branch.new_thread('thread2', 'thread1') tree.branch._set_nick('thread2') tree.commit('thread2', rev_id='thread2-id') return tree def test_export_loom_initial(self): tree = self.get_multi_threaded() root_transport = tree.branch.bzrdir.root_transport tree.branch.export_threads(root_transport) thread1 = Branch.open_from_transport(root_transport.clone('thread1')) self.assertEqual('thread1-id', thread1.last_revision()) thread2 = Branch.open_from_transport(root_transport.clone('thread2')) self.assertEqual('thread2-id', thread2.last_revision()) def test_export_loom_update(self): tree = self.get_multi_threaded() root_transport = tree.branch.bzrdir.root_transport tree.branch.export_threads(root_transport) tree.commit('thread2-2', rev_id='thread2-2-id') tree.branch.export_threads(root_transport) thread1 = Branch.open_from_transport(root_transport.clone('thread1')) self.assertEqual('thread1-id', thread1.last_revision()) thread2 = Branch.open_from_transport(root_transport.clone('thread2')) self.assertEqual('thread2-2-id', thread2.last_revision()) def test_export_loom_root_transport(self): tree = self.get_multi_threaded() tree.branch.bzrdir.root_transport.mkdir('root') root_transport = tree.branch.bzrdir.root_transport.clone('root') tree.branch.export_threads(root_transport) thread1 = Branch.open_from_transport(root_transport.clone('thread1')) thread1 = Branch.open_from_transport(root_transport.clone('thread1')) self.assertEqual('thread1-id', thread1.last_revision()) thread2 = Branch.open_from_transport(root_transport.clone('thread2')) self.assertEqual('thread2-id', thread2.last_revision()) def test_export_loom_as_tree(self): tree = self.get_multi_threaded() tree.branch.bzrdir.root_transport.mkdir('root') root_transport = tree.branch.bzrdir.root_transport.clone('root') tree.branch.export_threads(root_transport) export_tree = WorkingTree.open(root_transport.local_abspath('thread1')) self.assertEqual('thread1-id', export_tree.last_revision()) def test_export_loom_as_branch(self): tree = self.get_multi_threaded() tree.branch.bzrdir.root_transport.mkdir('root') root_path = tree.branch.bzrdir.root_transport.local_abspath('root') repo = self.make_repository('root', shared=True) repo.set_make_working_trees(False) root_transport = get_transport('root') tree.branch.export_threads(root_transport) self.assertRaises(errors.NoWorkingTree, WorkingTree.open, root_transport.local_abspath('thread1')) export_branch = Branch.open_from_transport( root_transport.clone('thread1')) self.assertEqual('thread1-id', export_branch.last_revision()) def test_set_nick_renames_thread(self): tree = self.get_tree_with_loom() tree.branch.new_thread(tree.branch.nick) orig_threads = tree.branch.get_loom_state().get_threads() new_thread_name = 'new thread name' tree.branch.nick = new_thread_name new_threads = tree.branch.get_loom_state().get_threads() self.assertNotEqual(orig_threads, new_threads) self.assertEqual(new_thread_name, new_threads[0][0]) self.assertEqual(new_thread_name, tree.branch.nick) bzr-loom-2.2.0/tests/test_loom_io.py0000644000000000000000000001412411722461011015607 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Tests of the Loom parse and serialise routines.""" from cStringIO import StringIO import bzrlib import bzrlib.errors as errors import bzrlib.osutils from bzrlib.plugins.loom.branch import EMPTY_REVISION import bzrlib.plugins.loom.loom_io as loom_io import bzrlib.plugins.loom.loom_state as loom_state from bzrlib.plugins.loom.tree import LoomTreeDecorator import bzrlib.revision from bzrlib.tests import TestCase class TestLoomIO(TestCase): def test_writer_constructors(self): writer = loom_io.LoomWriter() state = loom_state.LoomState() writer = loom_io.LoomStateWriter(state) def assertWritesThreadsCorrectly(self, expected_stream, threads): """Write threads through a LoomWriter and check the output and sha1.""" writer = loom_io.LoomWriter() stream = StringIO() expected_sha1 = bzrlib.osutils.sha_strings([expected_stream]) self.assertEqual(expected_sha1, writer.write_threads(threads, stream)) self.assertEqual(expected_stream, stream.getvalue()) def test_write_empty_threads(self): self.assertWritesThreadsCorrectly('Loom meta 1\n', []) def test_write_threads(self): self.assertWritesThreadsCorrectly( 'Loom meta 1\n' 'empty: baseline\n' 'asdasdasdxxxrr not the baseline\n', [('baseline', EMPTY_REVISION), ('not the baseline', 'asdasdasdxxxrr')], ) def test_write_unicode_threads(self): self.assertWritesThreadsCorrectly( 'Loom meta 1\n' 'empty: base\xc3\x9eline\n' 'asd\xc3\xadasdasdxxxrr not the baseline\n', [(u'base\xdeline', EMPTY_REVISION), ('not the baseline', u'asd\xedasdasdxxxrr')], ) def assertWritesStateCorrectly(self, expected_stream, state): """Write state to a stream and check it against expected_stream.""" writer = loom_io.LoomStateWriter(state) stream = StringIO() writer.write(stream) self.assertEqual(expected_stream, stream.getvalue()) def test_write_empty_state(self): state = loom_state.LoomState() self.assertWritesStateCorrectly( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n\n', state) def test_write_state_with_parent(self): state = loom_state.LoomState() state.set_parents(['1']) self.assertWritesStateCorrectly( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n' '1\n', state) def test_write_state_with_parents(self): state = loom_state.LoomState() state.set_parents(['1', u'2\xeb']) self.assertWritesStateCorrectly( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n' '1 2\xc3\xab\n', state) def test_write_state_with_threads(self): state = loom_state.LoomState() state.set_threads( [('base ', 'baserev', []), (u'\xedtop', '\xc3\xa9toprev', []), ]) self.assertWritesStateCorrectly( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n' '\n' ' : baserev base \n' ' : \xc3\xa9toprev \xc3\xadtop\n', state) def test_write_state_with_threads_and_parents(self): state = loom_state.LoomState() state.set_threads( [('base ', 'baserev', [None, None]), (u'\xedtop', '\xc3\xa9toprev', [None, None]), ]) state.set_parents(['1', u'2\xeb']) self.assertWritesStateCorrectly( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n' '1 2\xc3\xab\n' ' : baserev base \n' ' : \xc3\xa9toprev \xc3\xadtop\n', state) def assertReadState(self, parents, threads, state_stream): """Check that the state in stream can be read correctly.""" state_reader = loom_io.LoomStateReader(state_stream) self.assertEqual(parents, state_reader.read_parents()) self.assertEqual(threads, state_reader.read_thread_details()) def test_read_state_empty(self): state_stream = StringIO(loom_io._CURRENT_LOOM_FORMAT_STRING + '\n\n') self.assertReadState([], [], state_stream) def test_read_state_no_parents_threads(self): state_stream = StringIO( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n' '\n' ' : baserev base \n' ' : \xc3\xa9toprev \xc3\xadtop\n') # yes this is utf8 self.assertReadState( [], [('base ', 'baserev', []), # name -> unicode, revid -> utf8 (u'\xedtop', '\xc3\xa9toprev', []), ], state_stream) def test_read_state_parents(self): state_stream = StringIO( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n' '1 2\xc3\xab\n') self.assertReadState( ['1', '2\xc3\xab'], [], state_stream) def test_read_state_parents_threads(self): state_stream = StringIO( loom_io._CURRENT_LOOM_FORMAT_STRING + '\n' '1 2\xc3\xab\n' ' : baserev base \n' ' : \xc3\xa9toprev \xc3\xadtop\n') # yes this is utf8 self.assertReadState( ['1', '2\xc3\xab'], [('base ', 'baserev', [None, None]), (u'\xedtop', '\xc3\xa9toprev', [None, None]), ], state_stream) bzr-loom-2.2.0/tests/test_loom_state.py0000644000000000000000000001245111722461011016321 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 - 2008 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Tests of the Loom current-state object.""" from cStringIO import StringIO import bzrlib import bzrlib.errors as errors import bzrlib.osutils import bzrlib.plugins.loom.loom_io as loom_io import bzrlib.plugins.loom.loom_state as loom_state from bzrlib.plugins.loom.tree import LoomTreeDecorator from bzrlib.revision import NULL_REVISION from bzrlib.tests import TestCase class TestLoomState(TestCase): def test_default_constructor(self): state = loom_state.LoomState() # the default object must have no parents and no threads. self.assertEqual([], state.get_parents()) self.assertEqual([], state.get_threads()) self.assertEqual(NULL_REVISION, state.get_basis_revision_id()) self.assertEqual({}, state.get_threads_dict()) def test_reader_constructor(self): # make a state state = loom_state.LoomState() state.set_threads([('name', 'rev', [None, None]), ('dangerous name', 'rev2', [None, None])]) state.set_parents(['bar', 'am']) stream = StringIO() writer = loom_io.LoomStateWriter(state) writer.write(stream) # creating state from a serialised loom stream.seek(0) reader = loom_io.LoomStateReader(stream) state = loom_state.LoomState(reader) self.assertEqual(['bar', 'am'], state.get_parents()) self.assertEqual( [('name', 'rev', [None, None]), ('dangerous name', 'rev2', [None, None])], state.get_threads()) self.assertEqual('bar', state.get_basis_revision_id()) def test_set_get_threads(self): state = loom_state.LoomState() sample_threads = [('foo', 'bar', []), (u'g\xbe', 'bar', [])] state.set_threads(sample_threads) self.assertEqual([], state.get_parents()) self.assertEqual(NULL_REVISION, state.get_basis_revision_id()) self.assertEqual(sample_threads, state.get_threads()) # alter the sample threads we just set, to see that the stored copy is # separate sample_threads.append('foo') self.assertNotEqual(sample_threads, state.get_threads()) # and check the returned copy is also independent. sample_threads = state.get_threads() sample_threads.append('foo') self.assertNotEqual(sample_threads, state.get_threads()) def test_set_get_parents(self): state = loom_state.LoomState() sample_threads = [('foo', 'bar', [])] state.set_threads(sample_threads) # can set parents to nothing with no side effects state.set_parents([]) self.assertEqual([], state.get_parents()) self.assertEqual(NULL_REVISION, state.get_basis_revision_id()) self.assertEqual(sample_threads, state.get_threads()) # can set a single parent with no threads state.set_parents(['foo']) self.assertEqual(['foo'], state.get_parents()) self.assertEqual('foo', state.get_basis_revision_id()) self.assertEqual(sample_threads, state.get_threads()) # can set a single parent with threads state.set_parents(['bar']) self.assertEqual(['bar'], state.get_parents()) self.assertEqual('bar', state.get_basis_revision_id()) self.assertEqual(sample_threads, state.get_threads()) # can set multiple parents state.set_parents(['bar', ' am']) self.assertEqual(['bar', ' am'], state.get_parents()) self.assertEqual('bar', state.get_basis_revision_id()) self.assertEqual(sample_threads, state.get_threads()) def get_sample_state(self): state = loom_state.LoomState() sample_threads = [('foo', 'bar', []), (u'g\xbe', 'bar', [])] state.set_threads(sample_threads) return state def test_get_threads_dict(self): state = self.get_sample_state() self.assertEqual( {'foo':('bar', []), u'g\xbe':('bar', []), }, state.get_threads_dict()) def test_thread_index(self): state = self.get_sample_state() self.assertEqual(0, state.thread_index('foo')) self.assertEqual(1, state.thread_index(u'g\xbe')) def test_new_thread_after_deleting(self): state = self.get_sample_state() self.assertEqual(u'g\xbe', state.get_new_thread_after_deleting('foo')) self.assertEqual('foo', state.get_new_thread_after_deleting(u'g\xbe')) def test_new_thread_after_deleting_one_thread(self): state = loom_state.LoomState() state.set_threads([('foo', 'bar', [])]) self.assertIs(None, state.get_new_thread_after_deleting('foo')) bzr-loom-2.2.0/tests/test_revspec.py0000644000000000000000000001111611722461011015617 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Tests of the loom revision-specifiers.""" import bzrlib.errors from bzrlib.plugins.loom.branch import NoLowerThread, NoSuchThread from bzrlib.plugins.loom.tests import TestCaseWithLoom import bzrlib.plugins.loom.tree from bzrlib.revisionspec import RevisionSpec class TestRevSpec(TestCaseWithLoom): def get_two_thread_loom(self): tree = self.get_tree_with_loom('source') tree.branch.new_thread('bottom') tree.branch.new_thread('top') tree.branch._set_nick('bottom') rev_id_bottom = tree.commit('change bottom') loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) loom_tree.up_thread() rev_id_top = tree.commit('change top') return tree, loom_tree, rev_id_bottom, rev_id_top class TestThreadRevSpec(TestRevSpec): """Tests of the ThreadRevisionSpecifier.""" def test_thread_colon_at_bottom_errors(self): tree, loom_tree, rev_id, _ = self.get_two_thread_loom() loom_tree.down_thread() spec = RevisionSpec.from_string('thread:') self.assertRaises(NoLowerThread, spec.in_branch, tree.branch) def test_thread_colon_gets_next_lower_thread(self): tree, loom_tree, rev_id, _ = self.get_two_thread_loom() spec = RevisionSpec.from_string('thread:') self.assertEqual(rev_id, spec.in_branch(tree.branch)[1]) def test_thread_colon_bad_name_errors(self): tree, loom_tree, _, _ = self.get_two_thread_loom() loom_tree.down_thread() spec = RevisionSpec.from_string('thread:foo') err = self.assertRaises(NoSuchThread, spec.in_branch, tree.branch) self.assertEqual('foo', err.thread) def test_thread_colon_name_gets_named_thread(self): tree, loom_tree, _, rev_id = self.get_two_thread_loom() loom_tree.down_thread() spec = RevisionSpec.from_string('thread:top') self.assertEqual(rev_id, spec.in_branch(tree.branch)[1]) def test_thread_colon_name_gets_named_thread_revision_id(self): tree, loom_tree, _, rev_id = self.get_two_thread_loom() loom_tree.down_thread() spec = RevisionSpec.from_string('thread:top') self.assertEqual(rev_id, spec.as_revision_id(tree.branch)) def test_thread_on_non_loom_gives_BzrError(self): tree = self.make_branch_and_tree('.') spec = RevisionSpec.from_string('thread:') err = self.assertRaises(bzrlib.errors.BzrError, spec.as_revision_id, tree.branch) self.assertFalse(err.internal_error) class TestBelowRevSpec(TestRevSpec): """Tests of the below: revision specifier.""" def test_below_gets_tip_of_thread_below(self): tree, loom_tree, _, rev_id = self.get_two_thread_loom() loom_tree.down_thread() expected_id = tree.branch.last_revision() loom_tree.up_thread() spec = RevisionSpec.from_string('below:') self.assertEqual(expected_id, spec.as_revision_id(tree.branch)) def test_below_on_bottom_thread_gives_BzrError(self): tree, loom_tree, _, rev_id = self.get_two_thread_loom() loom_tree.down_thread() spec = RevisionSpec.from_string('below:') err = self.assertRaises(bzrlib.errors.BzrError, spec.as_revision_id, tree.branch) self.assertFalse(err.internal_error) def test_below_named_thread(self): tree, loom_tree, _, rev_id = self.get_two_thread_loom() loom_tree.down_thread() expected_id = tree.branch.last_revision() spec = RevisionSpec.from_string('below:top') self.assertEqual(expected_id, spec.as_revision_id(tree.branch)) def test_below_on_non_loom_gives_BzrError(self): tree = self.make_branch_and_tree('.') spec = RevisionSpec.from_string('below:') err = self.assertRaises(bzrlib.errors.BzrError, spec.as_revision_id, tree.branch) self.assertFalse(err.internal_error) bzr-loom-2.2.0/tests/test_tree.py0000644000000000000000000002613211722461011015113 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 Canonical Limited. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as published # by the Free Software Foundation. # # 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 # """Tests of the loom Tree related routines.""" import bzrlib from bzrlib import ( errors, merge as _mod_merge, ) from bzrlib.plugins.loom.branch import EMPTY_REVISION from bzrlib.plugins.loom.tests import TestCaseWithLoom import bzrlib.plugins.loom.tree from bzrlib.revision import NULL_REVISION class TestTreeDecorator(TestCaseWithLoom): """Tests of the LoomTreeDecorator class.""" def get_loom_with_two_threads(self): tree = self.get_tree_with_loom('source') tree.branch.new_thread('bottom') tree.branch.new_thread('top') tree.branch._set_nick('bottom') return bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) def test_down_thread(self): tree = self.get_tree_with_loom('source') tree.branch.new_thread('bottom') tree.branch.new_thread('top') tree.branch._set_nick('top') loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) loom_tree.down_thread() self.assertEqual('bottom', tree.branch.nick) def _add_thread(self, tree, name): """Create a new thread with a commit and return the commit id.""" tree.branch.new_thread(name) tree.branch._set_nick(name) return tree.commit(name) def test_down_named_thread(self): tree = self.get_tree_with_loom('source') loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) bottom_id = self._add_thread(tree, 'bottom') self._add_thread(tree, 'middle') self._add_thread(tree, 'top') self.assertNotEqual(bottom_id, tree.last_revision()) loom_tree.down_thread('bottom') self.assertEqual('bottom', tree.branch.nick) self.assertEqual([bottom_id], tree.get_parent_ids()) def test_up_thread(self): loom_tree = self.get_loom_with_two_threads() tree = loom_tree.tree loom_tree.up_thread() self.assertEqual('top', tree.branch.nick) self.assertEqual([], tree.get_parent_ids()) def test_up_to_no_commits(self): tree = self.get_tree_with_loom('tree') tree.branch.new_thread('bottom') tree.branch.new_thread('top') tree.branch._set_nick('bottom') bottom_rev1 = tree.commit('bottom_commit') tree_loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) tree_loom_tree.up_thread() self.assertEqual('top', tree.branch.nick) self.assertEqual([bottom_rev1], tree.get_parent_ids()) def test_up_already_merged(self): """up-thread into a thread that already has this thread is a no-op.""" tree = self.get_tree_with_loom('tree') tree.branch.new_thread('bottom') tree.branch._set_nick('bottom') bottom_rev1 = tree.commit('bottom_commit') tree.branch.new_thread('top', 'bottom') tree.branch._set_nick('top') top_rev1 = tree.commit('top_commit', allow_pointless=True) tree_loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) tree_loom_tree.down_thread() # check the test will be valid tree.lock_read() try: graph = tree.branch.repository.get_graph() self.assertEqual([top_rev1, bottom_rev1, NULL_REVISION], [r for (r, ps) in graph.iter_ancestry([top_rev1])]) self.assertEqual([bottom_rev1], tree.get_parent_ids()) finally: tree.unlock() tree_loom_tree.up_thread() self.assertEqual('top', tree.branch.nick) self.assertEqual([top_rev1], tree.get_parent_ids()) def test_up_not_merged(self): """up-thread from a thread with new work.""" tree = self.get_tree_with_loom('tree') tree.branch.new_thread('bottom') tree.branch._set_nick('bottom') bottom_rev1 = tree.commit('bottom_commit') tree.branch.new_thread('top', 'bottom') tree.branch._set_nick('top') top_rev1 = tree.commit('top_commit', allow_pointless=True) tree_loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) tree_loom_tree.down_thread() # check the test will be valid tree.lock_read() try: graph = tree.branch.repository.get_graph() self.assertEqual([top_rev1, bottom_rev1, NULL_REVISION], [r for (r, ps) in graph.iter_ancestry([top_rev1])]) self.assertEqual([bottom_rev1], tree.get_parent_ids()) finally: tree.unlock() bottom_rev2 = tree.commit('bottom_two', allow_pointless=True) tree_loom_tree.up_thread() self.assertEqual('top', tree.branch.nick) self.assertEqual([top_rev1, bottom_rev2], tree.get_parent_ids()) def test_up_thread_at_top_with_lower_commit(self): loom_tree = self.get_loom_with_two_threads() self.build_tree_contents([('source/a', 'a')]) loom_tree.tree.commit('add a') loom_tree.up_thread() e = self.assertRaises(errors.BzrCommandError, loom_tree.up_thread) self.assertEqual('Cannot move up from the highest thread.', str(e)) def test_up_thread_merge_type(self): loom_tree = self.get_loom_with_two_threads() self.build_tree_contents([('source/a', 'a')]) loom_tree.tree.add('a') loom_tree.tree.commit('add a') loom_tree.up_thread() self.build_tree_contents([('source/a', 'b')]) loom_tree.tree.commit('content to b') loom_tree.down_thread() self.build_tree_contents([('source/a', 'c')]) loom_tree.tree.commit('content to c') loom_tree.up_thread(_mod_merge.WeaveMerger) # Disabled because WeaveMerger writes BASE files now. XXX: Figure out # how to test this actually worked, again. # self.failIfExists('source/a.BASE') def get_loom_with_three_threads(self): tree = self.get_tree_with_loom('source') tree.branch.new_thread('bottom') tree.branch.new_thread('middle') tree.branch.new_thread('top') tree.branch._set_nick('bottom') return bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) def test_up_many(self): loom_tree = self.get_loom_with_three_threads() loom_tree.up_many() self.assertEqual('top', loom_tree.tree.branch.nick) self.assertEqual([], loom_tree.tree.get_parent_ids()) def test_up_many_commits(self): loom_tree = self.get_loom_with_two_threads() loom_tree.tree.commit('bottom', rev_id='bottom-1') loom_tree.up_thread() loom_tree.tree.commit('top', rev_id='top-1') loom_tree.down_thread() loom_tree.tree.commit('bottom', rev_id='bottom-2') loom_tree.up_many() last_revision = loom_tree.tree.last_revision() self.assertNotEqual(last_revision, 'top-1') rev = loom_tree.tree.branch.repository.get_revision(last_revision) self.assertEqual(['top-1', 'bottom-2'], rev.parent_ids) self.assertEqual('Merge bottom into top', rev.message) def test_up_many_halts_on_conflicts(self): loom_tree = self.get_loom_with_three_threads() tree = loom_tree.tree self.build_tree_contents([('source/file', 'contents-a')]) tree.add('file') tree.commit('bottom', rev_id='bottom-1') loom_tree.up_thread() self.build_tree_contents([('source/file', 'contents-b')]) tree.commit('middle', rev_id='middle-1') loom_tree.down_thread() self.build_tree_contents([('source/file', 'contents-c')]) tree.commit('bottom', rev_id='bottom-2') loom_tree.up_many() self.assertEqual('middle', tree.branch.nick) self.assertEqual(['middle-1', 'bottom-2'], tree.get_parent_ids()) self.assertEqual(1, len(tree.conflicts())) def test_up_many_target_thread(self): loom_tree = self.get_loom_with_three_threads() tree = loom_tree.tree loom_tree.up_many(target_thread='middle') self.assertEqual('middle', tree.branch.nick) def test_up_many_target_thread_lower(self): loom_tree = self.get_loom_with_three_threads() tree = loom_tree.tree loom_tree.up_many(target_thread='top') e = self.assertRaises(errors.BzrCommandError, loom_tree.up_many, target_thread='middle') self.assertEqual('Cannot up-thread to lower thread.', str(e)) def test_revert_loom(self): tree = self.get_tree_with_loom(',') # ensure we have some stuff to revert tree.branch.new_thread('foo') tree.branch.new_thread('bar') tree.branch._set_nick('bar') tree.commit('change something', allow_pointless=True) loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) loom_tree.revert_loom() # the tree should be reverted self.assertEqual(NULL_REVISION, tree.last_revision()) # the current loom should be reverted # (we assume this means branch.revert_loom was called()) self.assertEqual([], tree.branch.get_loom_state().get_threads()) def test_revert_thread(self): tree = self.get_tree_with_loom(',') # ensure we have some stuff to revert tree.branch.new_thread('foo') tree.branch.new_thread('bar') tree.branch._set_nick('bar') tree.commit('change something', allow_pointless=True) loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) loom_tree.revert_loom(thread='bar') # the tree should be reverted self.assertEqual(NULL_REVISION, tree.last_revision()) # the current loom should be reverted # (we assume this means branch.revert_loom was called()) self.assertEqual( [('foo', EMPTY_REVISION, [])], tree.branch.get_loom_state().get_threads()) def test_revert_thread_different_thread(self): tree = self.get_tree_with_loom(',') # ensure we have some stuff to revert tree.branch.new_thread('foo') tree.branch.new_thread('bar') tree.branch._set_nick('bar') tree.commit('change something', allow_pointless=True) loom_tree = bzrlib.plugins.loom.tree.LoomTreeDecorator(tree) loom_tree.revert_loom(thread='foo') # the tree should not be reverted self.assertNotEqual(NULL_REVISION, tree.last_revision()) # the bottom thread should be reverted # (we assume this means branch.revert_thread was # called()) self.assertEqual([('bar', tree.last_revision(), [])], tree.branch.get_loom_state().get_threads())