bzr-pipeline/.the_kraken/0000755000000000000000000000000012271626614014004 5ustar 00000000000000bzr-pipeline/COPYING0000644000000000000000000004310312271626614012647 0ustar 00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, 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 Lesser 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 Street, 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 Lesser General Public License instead of this License. bzr-pipeline/NEWS0000644000000000000000000000314312271626614012313 0ustar 00000000000000###################### Pipeline Release Notes ###################### Release 1.5 "Funky Fixture" (2014-01-27) ######################################## Indicate compatibility with bzr 2.6 Release 1.4 "Energetic Elbow" (2012-01-19) ############################################# Fix store command to not prompt. Indicate compatibility with bzr 2.5 Add release-check script. Release 1.3 "Dolorous Drainpipe" (2011-09-26) ############################################# Fix compatibility with bzr 2.2. Update the-kraken settings. Release 1.2 "Circular Cistern" (2011-09-26) ########################################### Compatibility cleanups. Lazy Loading (Jelmer). Ignore build directory (Gary van der Mewe) Documentation updates Changes are unshelved when remove-pipe switches (bug #719178) reconfigure-pipeline creates a layout compatible with bzr-colo. Versioning now matches bzrtools-style versioning. Refactoring. Release 1.1 "Bloated Basin" (2011-01-20) ######################################## bzr pump supports --show-base, --reprocess and --merge-type. Pipes can no longer be accidentally named ":next", etc. Pipeline imposes smaller start-up cost when not used. Release 1.0.0 "Annoying Air-Lock" (2010-11-10) ############################################## Initial release, compatible with bzr 2.2.0 - 2.3.b4 New Features ************ ``pump --from-submit`` generalized to ``--from``. If used with pipes, this pumps from the specified pipe into the remaining pipes. If used with normal branches, this pumps the branch into the first pipe, and pumps the first pipe as normal. .. vim: tw=74 ft=rst ff=unix bzr-pipeline/__init__.py0000644000000000000000000000471212271626614013730 0ustar 00000000000000# Copyright (C) 2007, 2009-2011 Aaron Bentley # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA import bzrlib from bzrlib.trace import warning __version__ = '1.5' version_info = (1, 5, 0) minimum_bzrlib_version = (2, 2) maximum_bzrlib_version = (2, 6) TOO_OLD = 'too_old' COMPATIBLE = 'compatible' MAYBE_TOO_NEW = 'maybe_too_new' TOO_NEW = 'too_new' def check_version_compatibility(bzrlib_version, min_version, max_version): """Check whether a bzrlib version is compatible with desired version. If the bzrlib_version is not less than min_version and not greater than max_version, it is considered COMPATIBLE. If the version exceeds max_version by 1 and is not a 'candidate' or 'final' version, it is considered MAYBE_TOO_NEW. Other values greater than max_version are considered TOO_NEW, and values lower than min_version are considered TOO_OLD. """ bzrlib_version = bzrlib.version_info[:2] if bzrlib_version < min_version: return TOO_OLD if bzrlib_version <= max_version: return COMPATIBLE max_plus = (max_version[0], max_version[1] + 1) if bzrlib_version == max_plus: if bzrlib.version_info[3] not in ('final', 'candidate'): return COMPATIBLE return MAYBE_TOO_NEW return TOO_NEW compatibility = check_version_compatibility(bzrlib.version_info, minimum_bzrlib_version, maximum_bzrlib_version) if compatibility == TOO_OLD: warning('bzr %s is too old for pipeline %s', bzrlib.__version__, __version__) elif compatibility != COMPATIBLE: warning('bzr %s is too new for pipeline %s', bzrlib.__version__, __version__) if compatibility not in (TOO_OLD, TOO_NEW): from real_init import * __doc__ = real_init.__doc__ bzr-pipeline/check-release.py0000755000000000000000000000143312271626614014664 0ustar 00000000000000#!/usr/bin/env python import re from subprocess import call, PIPE import sys def minigrep(pattern, filename): setup = open(filename, 'rb') for line in setup: match = re.search(pattern, line) if match is not None: return match version = minigrep("__version__ = '([^']*)'", '__init__.py').group(1) print "Release: %s" % version newsmatch = minigrep('Release %s' % (version), 'NEWS') if newsmatch is None: print "NEWS entry missing" sys.exit(1) else: print "NEWS entry found" setupmatch = minigrep('version="%s"' % (version), 'setup.py') if setupmatch is None: print "setup.py is stale" sys.exit(1) else: print "setup.py is good" if call(['bzr', 'diff'], stdout=PIPE) != 0: print "Please commit before releasing" sys.exit(1) bzr-pipeline/commands.py0000644000000000000000000003761312271626614014000 0ustar 00000000000000# Copyright (C) 2009-2011 Aaron Bentley # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA from bzrlib import ( builtins, errors, merge, revision as _mod_revision, trace, ui, ) from bzrlib.branch import Branch from bzrlib.bzrdir import BzrDir from bzrlib.commands import Command from bzrlib.option import Option, RegistryOption from bzrlib.osutils import pathjoin from bzrlib.switch import switch from bzrlib.workingtree import WorkingTree from bzrlib.plugins.pipeline.pipeline import ( dwim_pipe, is_light_checkout, MergeConfig, NoSuchPipe, PipeManager, PipeStorage, tree_to_pipeline, ) def require_light_checkout(tree): if not is_light_checkout(tree): raise errors.BzrCommandError('Directory is not a lightweight' ' checkout.') class PipeCommand(Command): def _get_manager(self, location, after, before): tree, manager = self._get_checkout_manager(location, allow_tree=True) if after is not None: if before is not None: raise errors.BzrCommandError('Cannot specify --before and' ' --after.') manager = PipeManager(manager.storage.find_pipe(after), tree) if before is not None: manager = PipeManager(manager.storage.find_pipe(before), tree) return tree, manager def _get_location_manager(self, location='.'): branch = Branch.open_containing(location)[0] return PipeManager(branch, None) @staticmethod def _get_tree_and_branch(location, tree_optional): if tree_optional: tree, branch = BzrDir.open_containing_tree_or_branch(location)[:2] else: tree = WorkingTree.open_containing(location)[0] branch = tree.branch return tree, branch @classmethod def _get_checkout_manager(klass, location='.', checkout_optional=False, pipe=None, allow_tree=False): try: tree, branch = klass._get_tree_and_branch('.', checkout_optional) branch = PipeStorage(branch).find_pipe(location) except (errors.NotBranchError, NoSuchPipe): tree, branch = klass._get_tree_and_branch(location, checkout_optional) if tree is not None and not allow_tree: require_light_checkout(tree) manager = PipeManager(branch, tree) if pipe is not None: manager = PipeManager(manager.storage.find_pipe(pipe), tree) return tree, manager def _get_revision_id(self, branch, revision, manager, insert_before): if revision is None: if not insert_before: return None prev_revision_id = manager.get_prev_revision_id() if prev_revision_id is not None: return prev_revision_id else: return branch.last_revision() if len(revision) > 1: raise errors.BzrCommandError('Only one revision may be supplied.') return revision[0].as_revision_id(branch) class cmd_add_pipe(PipeCommand): """Add a pipe to the pipeline. By default, the pipe is added after the current active pipe, at the current revision. This command can be used to start a new pipeline. """ takes_args = ['pipe', 'neighbour?'] takes_options = [RegistryOption.from_kwargs('position', 'Position to insert the new pipe at.', after='Insert after the selected pipe.', before='Insert before the selected pipe.', value_switches=True, enum_switch=False), Option('interactive', short_name='i', help='Interactively decide which changes to place' ' in the new pipe.'), Option('no-switch', help="Do not switch to new pipe."), Option('directory', short_name='d', type=unicode, help='Directory of the pipe to add to, rather than' ' the one for the working directory.'), 'revision',] def run(self, pipe, neighbour=None, revision=None, interactive=False, no_switch=False, directory=None, position='after'): tree, manager = self._get_checkout_manager(directory, pipe=neighbour, allow_tree=True) if not is_light_checkout(tree): raise errors.BzrCommandError('add-pipe should be run in a ' 'lightweight checkout. See bzr help pipeline for details.') insert_before = (position == 'before') tree.branch.lock_read() try: revision_id = self._get_revision_id(tree.branch, revision, manager, insert_before) finally: tree.branch.unlock() try: new_br = Branch.open(pipe) except errors.NotBranchError: new_br = manager.storage.insert_pipe(pipe, revision_id, before=insert_before) else: manager.storage.insert_branch(new_br, insert_before) if revision_id is None and no_switch: PipeManager(new_br, tree).store_uncommitted(interactive) if no_switch: trace.note('Created pipe "%s".' % pipe) if not no_switch: switch(tree.bzrdir, new_br) trace.note('Created and switched to pipe "%s".' % pipe) class cmd_rename_pipe(PipeCommand): """Rename a pipe to a different name. This will rename the branch directory and update the pipeline metadata. It is not connected to the branch nick. """ takes_args = ['new_name'] def run(self, new_name): tree, manager = self._get_checkout_manager('.') manager.rename_pipe(new_name) class cmd_merge(builtins.cmd_merge): #Support merge --uncommitted PIPE __doc__ = builtins.cmd_merge.__doc__ def get_merger_from_uncommitted(self, tree, location, pb): try: pipe = PipeManager(dwim_pipe(PipeManager(tree.branch, tree), location), tree) except NoSuchPipe: return builtins.cmd_merge.get_merger_from_uncommitted( self, tree, location, pb) merger = pipe.make_uncommitted_merger(self) if merger is None: # Have to return a merger of some kind, so we don't try another # code path. merger = merge.Merger.from_revision_ids(None, tree, _mod_revision.NULL_REVISION, _mod_revision.NULL_REVISION) return merger class cmd_reconfigure_pipeline(PipeCommand): """Reconfigure a tree with branch into a lightweight checkout of a pipe. The pipeline will be stored in a new "branches" subdirectory of .bzr. This is compatible with the bzr-colo plugin. This is suitable if you have a standalone tree, but if you have a shared repository with its own organization scheme already, it's probably better to just create a lightweight checkout. """ def run(self): tree = WorkingTree.open_containing('.')[0] tree_to_pipeline(tree) class cmd_remove_pipe(PipeCommand): """Remove a pipe from the pipeline. By default, the current pipe is removed, but a pipe may be specified as the first parameter. By default, only the association of the pipe with its pipeline is removed, but if --branch is specified, the branch is also deleted. """ takes_args = ['pipe?'] takes_options = [Option('branch', help="Remove pipe's branch.")] def run(self, pipe=None, branch=False): tree, manager = self._get_checkout_manager(location=pipe, checkout_optional=True, allow_tree=True) target_branch = manager.get_nearest_pipe() if target_branch is None: raise errors.BzrCommandError('Branch is not connected to a' ' pipeline.') manager.remove_pipe(target_branch, delete_branch=branch) class cmd_switch_pipe(PipeCommand): """Switch from one pipe to another. Any uncommitted changes are stored. Any stored changes in the target pipe are restored. """ aliases = ['swp'] takes_args = ['pipe'] takes_options = [ Option('directory', type=unicode, short_name='d', help='Directory of the checkout to switch, rather than the' ' current directory.')] def run(self, pipe, directory=None): checkout, manager = self._get_checkout_manager(directory) old = checkout.branch.nick target = dwim_pipe(manager, pipe) manager.switch_to_pipe(target) trace.note('Switched from "%s" to "%s".' % (old, target.nick)) class cmd_store(PipeCommand): """Store uncommitted changes in the pipe.""" hidden = True def run(self): checkout, manager = self._get_checkout_manager('.') manager.store_uncommitted() class cmd_show_pipeline(PipeCommand): """Show the current pipeline. All pipes are listed with the beginning of the pipeline at the top and the end of the pipeline at the bottom. These indicators are used:: * - The current pipe. U - A pipe holding uncommitted changes. Uncommitted changes are automatically restored by the 'switch-pipe' command. """ takes_args = ['location?'] aliases = ['pipes'] _see_also = ['nick'] def run(self, location='.'): manager = self._get_location_manager(location) for pipe in manager.list_pipes(): if pipe is manager.storage.branch: selected = '*' else: selected = ' ' if PipeStorage(pipe).has_stored_changes(): uncommitted = 'U' else: uncommitted = ' ' self.outf.write('%s%s %s\n' % (selected, uncommitted, pipe.nick)) class cmd_pump(PipeCommand): """From this pipe onward, merge all pipes into their next pipe and commit. If the merge is successful, the changes are automatically committed, and the process repeats for the next pipe. Eventually, the last pipe will have all the changes from all of the affected pipes. On success, the checkout's initial state is restored. If the merge produces conflicts, the process aborts and no commit is performed. You should resolve the conflicts, commit, and re-run pump. --from may be a pipe, in which case the pumping starts at the specified pipe, or it may be a branch, in which case the branch will be pumped into the first pipe. """ takes_options = [ Option('directory', short_name='d', type=unicode, help='Directory in the pipeline to pump from.'), Option('from-submit', help="Start from the first pipe's submit" " branch."), Option('from', help="Pump start. May be a pipe or a non-pipe.", type=unicode), Option('show-base', help='Show base revision text in conflicts.'), 'reprocess', 'merge-type', ] def run(self, directory=None, from_submit=False, show_base=False, reprocess=False, merge_type=None, **kwargs): """Run the command. kwargs are needed to accept --from.""" tree, manager = self._get_checkout_manager(directory) unexpected = set(kwargs.keys()).difference(set(['from'])) if len(unexpected) != 0: raise TypeError("run() got an unexpected keyword argument '%s'" % list(unexpected)[0]) from_ = kwargs.get('from') from_branch = None if from_submit or from_: first_pipe = manager.get_first_pipe() manager = PipeManager(first_pipe, tree) if from_submit: from_ = first_pipe.get_submit_branch() if from_ is not None: try: from_pipe = dwim_pipe(manager, from_) manager = PipeManager(from_pipe, tree) except NoSuchPipe: from_branch = Branch.open(from_) else: from_branch = None merge_config = MergeConfig(merge_type, show_base, reprocess) if not manager.pipeline_merge(from_branch, merge_config): trace.note('Please resolve conflicts, commit, and re-run pump.') class cmd_pipe_patches(PipeCommand): """Export the pipeline as a collection of patches, one per pipe. The patch name begins with a sequence number, and ends with the pipe name. """ takes_args = ['patch_location?'] takes_options = [Option('directory', short_name='d', type=unicode, help='Directory of the pipeline.'), ] def run(self, patch_location='.', directory=None): checkout, manager = self._get_checkout_manager(directory, checkout_optional=True, allow_tree=True) for num, pipe in enumerate(manager.list_pipes()): pipe.lock_read() try: patch = PipeManager(pipe, checkout).get_patch() finally: pipe.unlock() if patch is None: continue filename = pathjoin(patch_location, '%.2d-%s.patch' % (num, pipe.nick)) my_file = open(filename, 'wb') try: my_file.write(patch) finally: my_file.close() class cmd_sync_pipeline(PipeCommand): """Synchronise the contents of this pipeline with another copy. The first argument is the location of one of the pipes in the remote pipeline. It defaults to the push location. If it does not exist, the whole remote pipeline will be created. If any remote pipes are missing, they will be created. The pipelines are then synchronized by pulling and pushing between pipes, depending on which is newer. If pipes have diverged, the process will abort. You should then merge the remote pipe into the local pipe and re-run sync-pipeline. """ takes_args = ['location?'] def run(self, location=None): checkout, manager = self._get_checkout_manager(checkout_optional=True, allow_tree=True) remote = None if location is None: branchless_location = None for pipe in manager.list_pipes(): location = pipe.get_push_location() if location is not None: try: remote = Branch.open(location) except errors.NotBranchError: if branchless_location is None: branchless_location = location continue manager = PipeManager(pipe, checkout) break else: if branchless_location is not None: location = branchless_location else: raise errors.BzrCommandError( 'No location specified and none remembered.') try: manager.sync_pipeline(location, remote) finally: ui.ui_factory.clear_term() bzr-pipeline/loom.py0000644000000000000000000000676512271626614013151 0ustar 00000000000000# Loom, a plugin for bzr to assist in developing focused patches. # Copyright (C) 2006 - 2008 Canonical Limited. # Copyright (C) 2009 Aaron Bentley # # 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 bzrlib import bzrdir from bzrlib.branch import Branch from bzrlib.commands import Command from bzrlib.option import RegistryOption from bzrlib.transport import get_transport import bzrlib.urlutils from bzrlib.plugins.pipeline.pipeline import PipeStorage class cmd_import_loom(Command): """Import a loom into a pipeline.""" hidden = True takes_args = ['pipeline', 'loom?'] takes_options = [ RegistryOption('format', help='Specify a format for this branch. ' 'See "help formats".', lazy_registry=('bzrlib.bzrdir', 'format_registry'), converter=lambda name: bzrdir.format_registry.make_bzrdir(name), value_switches=True, title="Branch Format", )] def run(self, pipeline, loom='.', format=None): root_transport = get_transport(pipeline) loom_branch = Branch.open(loom) import_threads(loom_branch, root_transport, format=format) def import_threads(loom_branch, root_transport, format=None): """Import the threads in this loom into pipes. :param loom_branch: A LoomBranch :param root_transport: Transport for the directory to place branches under. Defaults to branch root transport. """ threads = loom_branch.get_loom_state().get_threads() last_branch = None for thread_name, thread_revision, _parents in reversed(threads): thread_transport = root_transport.clone(thread_name) user_location = bzrlib.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 bzrlib.errors.NotBranchError: bzrlib.trace.note('Creating branch at %s' % user_location) branch = bzrdir.BzrDir.create_branch_convenience( thread_transport.base, force_new_tree=False, possible_transports=[thread_transport], format=format) tree, branch = branch.bzrdir.open_tree_or_branch( thread_transport.base) else: if thread_revision == branch.last_revision(): bzrlib.trace.note('Skipping up-to-date branch at %s' % user_location) continue else: bzrlib.trace.note('Updating branch at %s' % user_location) if tree is not None: tree.pull(loom_branch, stop_revision=thread_revision) else: branch.pull(loom_branch, stop_revision=thread_revision) if last_branch is not None: PipeStorage.connect(branch, last_branch) last_branch = branch bzr-pipeline/pipeline-and-loom.txt0000644000000000000000000000364612271626614015676 0ustar 00000000000000Pipelines and Looms are similar approaches to managing a series of related lines-of-development. But let's focus on the differences. Technically, looms and pipelines are not mutually exclusive. A loom could be part of a pipeline. But it's hard to imagine a reason for doing this. Pipelines use regular branches, while Looms use threads. Threads are a form of colocated branch, because they have multiple tips in a single branch. When Bazaar natively supports colocated branches, Pipelines will also be able to support colocation. For now, branches are more compatible with existing tools and web sites, such as Launchpad and PQM. While Loom threads cannot be viewed or manipulated without installing the plugin, pipes are normal branches that can be viewed and manipulated without installing the plugin. Pipes can be specified as URLs, and they can also be used via location aliases. Being branches means that pipes also support individual configuration. This means they can each have different push, pull and public locations. Because pipes are not colocated, switching between pipes requires the use of lightweight checkouts. But unlike looms, you can have multiple checkouts in the same pipeline. Looms store versioned information about the order of threads in a loom and their head revisions. Pipes don't version this information. This means that pipes cannot be as intelligent when deciding how to resolve the differences between a local and remote pipeline. I don't believe this there is a significant need for such intelligence. Most of the time, pipes are only added to a pipeline, not removed or renamed. On the other hand, it means that pipelines don't need a 'record' command. Pipes are also shelves-- i.e., they can store uncommitted changes. Looms don't attempt to provide this. This means that some operations with pipes require fewer steps. It also means that it is meaningful to merge uncommitted changes from a pipe. bzr-pipeline/pipeline.py0000644000000000000000000010463712271626614014005 0ustar 00000000000000# Copyright (C) 2009-2011 Aaron Bentley # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA from cStringIO import StringIO import difflib import itertools from bzrlib.branch import Branch, BranchReferenceFormat from bzrlib.transport import get_transport from bzrlib import ( diff, errors, merge, shelf, shelf_ui, switch, trace, transform as _mod_transform, urlutils, workingtree, ) from bzrlib.plugins.pipeline import is_pipe_alias class BranchInTheWay(errors.BzrCommandError): _fmt = 'Branch in the way at %(url)s.' def __init__(self, branch): errors.BzrCommandError.__init__(self, url=branch.base) class NoSuchPipe(errors.BzrCommandError): _fmt = """There is no pipe with nick "%(nick)s".""" def __init__(self, nick): errors.BzrCommandError.__init__(self, nick=nick) class ChangesAlreadyStored(errors.BzrCommandError): _fmt = ('Cannot store uncommitted changes because this pipe already stores' ' uncommitted changes.') class DuplicatePipe(errors.BzrCommandError): _fmt = 'There is already a pipe named "%(pipe_name)s."' def __init__(self, pipe_name): errors.BzrCommandError.__init__(self, pipe_name=pipe_name) class DuplicatePipeLocation(errors.BzrCommandError): _fmt = 'Branch already in pipeline: "%(pipe_name)s."' def __init__(self, pipe_name): errors.BzrCommandError.__init__(self, pipe_name=pipe_name) class DivergedPipes(errors.DivergedBranches): _fmt = 'Pipe "%(nick)s" has diverged.' def __init__(self, branch1, branch2): errors.DivergedBranches.__init__(self, branch1, branch2) self.nick = branch1.nick class UnknownRemotePipe(errors.BzrCommandError): _fmt = 'Pipeline has no pipe named "%(nick)s".' def __init__(self, nick): errors.BzrCommandError.__init__(self, nick=nick) class SwitchWithConflicts(errors.BzrCommandError): _fmt = 'Cannot switch while conflicts are present.' def is_light_checkout(tree): return (tree.branch.bzrdir.root_transport.base != tree.bzrdir.root_transport.base) class PipeStorage(object): """Store and retrieve pipe data.""" def __init__(self, branch): self.branch = branch def get_sibling(self, prev): option = 'prev_pipe' if prev else 'next_pipe' sibling = self.branch.get_config().get_user_option(option) if sibling == '': sibling = None return sibling def get_next(self): """Return the location of the next pipe.""" return self.get_sibling(False) def get_prev(self): """Return the location of the previous pipe.""" return self.get_sibling(True) def get_sibling_pipe(self, prev): url = self.get_sibling(prev) if url is None: return None transports = [self.branch.bzrdir.root_transport] url = urlutils.join(self.branch.base, url) return Branch.open(url, possible_transports=transports) def get_next_pipe(self): """Return the next pipe.""" return self.get_sibling_pipe(False) def get_prev_pipe(self): """Return the previous pipe.""" return self.get_sibling_pipe(True) def _pipe_url(self, url): """Determine the url to use for a pipe. Where possible, these urls are relative to the current pipe. If url is None, the result is ''. """ if url is None: return '' return urlutils.relative_url(self.branch.base, url) def _set_next(self, next): """Set the next pipe location. None may be supplied, to set the location to None. """ next = self._pipe_url(next) self.branch.get_config().set_user_option('next_pipe', next) def _set_prev(self, prev): """Set the previous pipe location. None may be supplied, to set the location to None. """ prev = self._pipe_url(prev) self.branch.get_config().set_user_option('prev_pipe', prev) def _get_sibling_transport(self, name): t = self.branch.bzrdir.root_transport.clone('..').clone(name) return get_transport(t.base) @staticmethod def connect(previous, next): """Connect two pipes together. :param previous: The previous pipe, as a branch. :param next: The next pipe, as a branch. """ if previous is None: prev_url = None else: prev_url = previous.base if next is None: next_url = None else: PipeStorage(next)._set_prev(prev_url) next_url = next.base if previous is not None: PipeStorage(previous)._set_next(next_url) def disconnect(self): """Remove this pipe from the pipeline.""" next_pipe = self.get_next_pipe() prev_pipe = self.get_prev_pipe() self.connect(prev_pipe, next_pipe) self._set_prev(None) self._set_next(None) def _open_sibling_branch(self, name): t = self._get_sibling_transport(name) return Branch.open_from_transport(t) def _create_sibling_branch(self, name, source, revision_id=None): t = self._get_sibling_transport(name) if revision_id is None: revision_id = source.last_revision() ctrl = source.bzrdir.clone_on_transport(t, revision_id) return ctrl.open_branch() def insert_pipe(self, name, revision_id=None, before=False): """Insert a pipe in this pipeline by nick. The pipe is inserted after this pipe by default. :param name: The name of the pipe to insert :param revision_id: The revision to insert the pipe at. :param before: If True, insert the pipe before this pipe. :return: The new pipe. """ try: self.find_pipe(name) except NoSuchPipe: pass else: raise DuplicatePipe(name) new_branch = self._create_sibling_branch(name, self.branch, revision_id) self.insert_branch(new_branch, before) return new_branch def insert_branch(self, pipe, before=False): urls = [b.base for b in self.list_pipes()] if pipe.base in urls: raise DuplicatePipeLocation(pipe.nick) if not before: cur_pipe = self.branch next_pipe = self.get_next_pipe() else: next_pipe = self.branch cur_pipe = self.get_prev_pipe() if cur_pipe is not None: self.connect(cur_pipe, pipe) if next_pipe is not None: self.connect(pipe, next_pipe) def iter_pipes(self, prev=True): """Iterate from this pipe through the list of pipes. :param prev: If True, iterate into the previous pipes. If false, iterate into the next pipes. """ storage = self while True: next_branch = storage.get_sibling_pipe(prev) if next_branch is None: return yield next_branch storage = PipeStorage(next_branch) def list_pipes(self): """Return a list of the Branches for this pipeline.""" pipes = list(self.iter_pipes(True)) pipes.reverse() pipes.append(self.branch) pipes.extend(self.iter_pipes(False)) return pipes def find_pipe(self, nick): """Find a pipe in the pipeline according to its nickname.""" if self.branch.nick == nick: return self.branch for pipe in itertools.chain(self.iter_pipes(True), self.iter_pipes(False)): if pipe.nick == nick: return pipe else: raise NoSuchPipe(nick) def _get_transform(self): """Retrieve a TreeTransform in serialized form. :return: a file-like object. """ try: return self.branch._transport.get('stored-transform') except errors.NoSuchFile: return None def _put_transform(self, input): """Store a TreeTransform in serialized form. :param input: a file-like object. """ if input is None: try: self.branch._transport.delete('stored-transform') except errors.NoSuchFile: pass else: self.branch._transport.put_file('stored-transform', input) def has_stored_changes(self): """If true, the pipe has stored, uncommitted changes in it.""" return self._get_transform() is not None class ShelfReporter(shelf_ui.ShelfReporter): def __init__(self, nick): self.nick = nick def no_changes(self): pass def shelved_id(self, shelved_id): trace.note('Uncommitted changes stored in pipe "%s".', self.nick) def selected_changes(self, transform): pass def guess_nick(location): return urlutils.unescape(urlutils.basename(location)) class PipeManager(object): def __init__(self, branch, checkout): self.storage = PipeStorage(branch) # A checkout connected to this pipeline. May be None. self.checkout = checkout @classmethod def from_checkout(klass, checkout): return klass(checkout.branch, checkout) def _open_disconnected_pipe(self, name, local_pipes): remote = self.storage._open_sibling_branch(name) remote.lock_read() try: local = dict(local_pipes)[name] local.lock_read() try: heads = self.get_heads(local, remote, local.last_revision(), remote.last_revision()) finally: local.unlock() finally: remote.unlock() if len(heads) > 1: raise BranchInTheWay(remote) remote_storage = PipeStorage(remote) if (remote_storage.get_next() is not None or remote_storage.get_prev() is not None): raise BranchInTheWay(remote) return remote def get_next_pipe(self): return self.storage.get_next_pipe() def get_prev_pipe(self): return self.storage.get_prev_pipe() def _get_terminal_pipe(self, first=True): pipe = self.storage.branch for pipe in self.storage.iter_pipes(prev=first): pass return pipe def get_first_pipe(self): return self._get_terminal_pipe() def get_last_pipe(self): return self._get_terminal_pipe(False) def get_nearest_pipe(self): nearest = self.get_next_pipe() if nearest is None: nearest = self.get_prev_pipe() return nearest def list_pipes(self): return self.storage.list_pipes() def rename_pipe(self, new_name): root_transport = self.storage.branch.bzrdir.root_transport containing_transport = root_transport.clone('..') old_name = urlutils.split(root_transport.base)[1] containing_transport.rename(old_name, new_name) new_transport = containing_transport.clone(new_name) self.storage.branch = Branch.open_from_transport(new_transport) prev_pipe = self.get_prev_pipe() if prev_pipe is not None: PipeStorage.connect(prev_pipe, self.storage.branch) next_pipe = self.get_next_pipe() if next_pipe is not None: PipeStorage.connect(self.storage.branch, next_pipe) switch._set_branch_location(self.checkout.bzrdir, self.storage.branch) def is_self_checkout(self): if self.checkout is None: return False if not is_light_checkout(self.checkout): return False return self.checkout.branch.base == self.storage.branch.base def remove_pipe(self, target_branch, delete_branch): """Remove this manager's pipe from the pipeline. If the checkout is a lightweight checkout of the pipe to remove, switch the checkout to target_branch first. If delete_branch is False, the branch is removed from the pipeline but remains on disk. If True, the branch is deleted. """ if self.is_self_checkout(): self.switch_to_pipe(target_branch) self.storage.disconnect() if delete_branch: self.storage.branch.bzrdir.root_transport.delete_tree('.') def sync_pipeline(self, location, remote_branch=None): """Synchronize this pipeline with another, mirror-style. Any missing remote pipes will be created, and the two pipelines will be updated to match each other by pushing or pulling pipes as appropriate. :location: The location of one of the remote pipes. """ possible_transports = [self.storage.branch.bzrdir.root_transport] local_pipes = [(p.nick, p) for p in self.list_pipes()] local_nicks = [n for n, p in local_pipes] try: if remote_branch is None: b = Branch.open(location, possible_transports=possible_transports) else: b = remote_branch except errors.NotBranchError: remote_nick = guess_nick(location) if remote_nick not in local_nicks: raise UnknownRemotePipe(remote_nick) trace.note('Creating new pipe at %s', location) t = get_transport(location, possible_transports=possible_transports) local = dict(local_pipes)[remote_nick] revision_id = local.last_revision() b = local.bzrdir.clone_on_transport(t, revision_id).open_branch() nick = b.nick remote_pipes = [(nick, b)] remote_manager = PipeManager(b, None) new_nicks = set([nick]) else: remote_manager = PipeManager(b, None) remote_pipes = [(b.nick, b) for b in remote_manager.list_pipes()] new_nicks = set() remote_nicks = [n for n, p in remote_pipes] remote_map = dict(remote_pipes) desired_remote = self._merge_nick_order(local_nicks, remote_nicks) for nick, pipe in reversed(local_pipes): if nick in remote_nicks: continue trace.note('Creating new pipe "%s"', nick) try: remote_map[nick] = remote_manager._open_disconnected_pipe( nick, local_pipes) except errors.NotBranchError: storage = remote_manager.storage remote_map[nick] = storage._create_sibling_branch(nick, pipe) pipe.set_push_location(remote_map[nick].base) new_nicks.add(nick) for prev, next in self._connections_to_create(desired_remote, remote_nicks): pipe = remote_map[nick] self.storage.connect(remote_map[prev], remote_map[next]) updatable = self._updatable_pipes(local_pipes, remote_map, new_nicks) self._push_pull(updatable) for nick, pipe in local_pipes: if pipe.get_push_location() is None: pipe.set_push_location(remote_map[nick].base) @staticmethod def list_connections(nicks): return zip(nicks[:-1], nicks[1:]) @classmethod def _connections_to_create(cls, desired, existing): existing_map = dict(cls.list_connections(existing)) connections = [] for prev, next in cls.list_connections(desired): if existing_map.get(prev) != next: connections.append((prev, next)) return connections @staticmethod def _updatable_pipes(local_pipes, remote_pipes, new_nicks): """Iterable of pairs of pipes that may need to be updated.""" for nick, pipe in local_pipes: if nick in new_nicks: continue yield nick, pipe, remote_pipes[nick] def _push_pull(self, updatable_pipes): """Push or pull branches to update them, depending on what's newer.""" for nick, pipe, remote_pipe in updatable_pipes: local_revision = pipe.last_revision() remote_revision = remote_pipe.last_revision() if remote_revision == local_revision: trace.note('%s is already up-to-date.', nick) continue pipe.lock_write() remote_pipe.lock_write() try: heads = self.get_heads(pipe, remote_pipe, local_revision, remote_revision) finally: remote_pipe.unlock() pipe.unlock() if len(heads) == 2: raise DivergedPipes(pipe, remote_pipe) head = heads.pop() if head == local_revision: trace.note('Pushing %s %s ', nick, remote_pipe.base) pipe.push(remote_pipe) else: trace.note('Pulling %s ', nick) if self.checkout.branch.base == pipe.base: self.checkout.pull(remote_pipe) else: pipe.pull(remote_pipe) @staticmethod def get_heads(pipe, remote_pipe, local_revision, remote_revision): graph = pipe.repository.get_graph(remote_pipe.repository) return graph.heads([local_revision, remote_revision]) @staticmethod def _merge_nick_order(local_nick, remote_nick): """Merge two orderings of nicknames into one containing both. Order of the source is preserved within matching sections and non-matching sections. Unmatched sequences are sorted according to their first element. """ matcher = difflib.SequenceMatcher(None, local_nick, remote_nick) prev_i = 0 prev_j = 0 result = [] for i, j, n in matcher.get_matching_blocks(): local_unique = (local_nick[pos] for pos in range(prev_i, i)) remote_unique = (remote_nick[pos] for pos in range(prev_j, j)) if (i != prev_i and j != prev_j and remote_nick[prev_j] < local_nick[prev_i]): result.extend(remote_unique) result.extend(local_unique) else: result.extend(local_unique) result.extend(remote_unique) for nick_pos in range(i, i+n): result.append(local_nick[nick_pos]) prev_i = i + n prev_j = j + n return result def shelve_changes(self, creator, message=None): # provided for interface-compatibility with Shelver if self.storage.has_stored_changes(): raise ChangesAlreadyStored transform = StringIO() creator.write_shelf(transform, message) try: transform.seek(0) self.storage._put_transform(transform) creator.transform() finally: creator.finalize() return 0 def has_stored_changes(self): return self.storage.has_stored_changes() def store_uncommitted(self, interactive=False): """Store uncommitted changes from this working tree in the pipe.""" if not interactive: return self.store_all() self.checkout.lock_write() try: target_tree = self.checkout.basis_tree() nick = self.storage.branch.nick shelver = shelf_ui.Shelver(self.checkout, target_tree, auto=not interactive, auto_apply=True, manager=self, reporter=ShelfReporter(nick)) shelver.run() finally: self.checkout.unlock() def store_all(self): self.checkout.lock_write() try: target_tree = self.checkout.basis_tree() shelf_creator = shelf.ShelfCreator(self.checkout, target_tree) try: change = None for change in shelf_creator.iter_shelvable(): shelf_creator.shelve_change(change) if change is None: return self.shelve_changes(shelf_creator) finally: shelf_creator.finalize() finally: self.checkout.unlock() trace.note('Uncommitted changes stored in pipe "%s".', self.storage.branch.nick) def make_uncommitted_merger(self, cleaner): metadata, base_tree, tt = self.get_transform_data() if tt is None: return None cleaner.add_cleanup(tt.finalize) return self.get_unshelve_merger(base_tree, tt, metadata) def get_transform_data(self): transform = self.storage._get_transform() if transform is None: return None, None, None records = shelf.Unshelver.iter_records(transform) metadata = shelf.Unshelver.parse_metadata(records) base_revision_id = metadata['revision_id'] try: base_tree = self.checkout.revision_tree(base_revision_id) except errors.NoSuchRevisionInTree: repo = self.storage.branch.repository base_tree = repo.revision_tree(base_revision_id) tt = _mod_transform.TransformPreview(base_tree) tt.deserialize(records) return metadata, base_tree, tt def get_unshelve_merger(self, base_tree, tt, metadata): unshelver = shelf.Unshelver(self.checkout, base_tree, tt, metadata.get('message')) merger = unshelver.make_merger() merger.ignore_zero = True return merger def restore_uncommitted(self, delete=True): """Restore uncommitted changes from this pipe into the working tree.""" self.checkout.lock_tree_write() try: metadata, base_tree, tt = self.get_transform_data() if tt is None: return None try: merger = self.get_unshelve_merger(base_tree, tt, metadata) merger.do_merge() finally: tt.finalize() finally: self.checkout.unlock() trace.note('Uncommitted changes restored from pipe "%s".', self.storage.branch.nick) if delete: self.storage._put_transform(None) def _switch_to_tree(self, to_branch, to_tree): merge.Merge3Merger(self.checkout, self.checkout, self.checkout, to_tree) self.checkout.set_last_revision(to_branch.last_revision()) switch._set_branch_location(self.checkout.bzrdir, to_branch) def switch_to_pipe(self, pipe): if len(self.checkout.conflicts()) > 0: raise SwitchWithConflicts pipe = PipeManager(pipe, self.checkout) self.store_uncommitted() self.checkout.lock_write() try: metadata, base_tree, tt = pipe.get_transform_data() try: direct = (tt is not None and base_tree.get_revision_id() == pipe.storage.branch.last_revision()) # do only one transform in the common case. if direct: preview = tt.get_preview_tree() switch_target = preview else: switch_target = pipe.storage.branch.basis_tree() self._switch_to_tree(pipe.storage.branch, switch_target) if not direct and tt is not None: merger = self.get_unshelve_merger(base_tree, tt, metadata) merger.do_merge() if tt is not None: pipe.storage._put_transform(None) finally: if tt is not None: tt.finalize() finally: self.checkout.unlock() return pipe def _make_old_merger(self, old_branch, tree, tree_branch, merge_config): old_revision_id = old_branch.last_revision() merger = merge.Merger.from_revision_ids(None, tree, old_revision_id, other_branch=old_branch, tree_branch=tree_branch) merger.merge_type = merge_config.merge_type merger.ignore_zero = True merger.show_base = merge_config.show_base merger.reprocess = merge_config.reprocess return merger def _merge_commit_branch(self, branch, old_branch, merge_config): branch.lock_write() old_branch.lock_read() try: graph = branch.repository.get_graph(old_branch.repository) old_last_revision = old_branch.last_revision() if is_lefthand_ancestor(graph, branch.last_revision(), old_last_revision): branch.pull(old_branch, stop_revision=old_last_revision) return True merge_controller = self._make_old_merger( old_branch, branch.basis_tree(), branch, merge_config) merger = merge_controller.make_merger() def null_warning(message): pass old_trace_warning = trace.warning trace.warning = null_warning try: tt = merger.make_preview_transform() finally: trace.warning = old_trace_warning try: if len(merger.cooked_conflicts) > 0: return False message = 'Merged %s into %s.' % (old_branch.nick, branch.nick) parents = [old_branch.last_revision()] tt.commit(branch, message, parents) return True finally: tt.finalize() finally: old_branch.unlock() branch.unlock() def _merge_tree(self, tree, old_branch, merge_config): """Merge the old_branch into the tree and commit the result.""" tree.lock_write() try: merger = self._make_old_merger(old_branch, tree, tree.branch, merge_config) conflicts = merger.do_merge() merger.set_pending() finally: tree.unlock() return conflicts @staticmethod def _skip_merge(old_branch, branch): old_branch.lock_read() branch.lock_read() try: old_revision_id = old_branch.last_revision() graph = old_branch.repository.get_graph(branch.repository) new_revision_id = branch.last_revision() return graph.is_ancestor(old_revision_id, new_revision_id) finally: branch.unlock() old_branch.unlock() def _mergeables(self, from_branch=None): """Iterable of new_branch, old_branch pairs to merge.""" if from_branch is not None: if not self._skip_merge(from_branch, self.storage.branch): yield self.storage.branch, from_branch old_branch = self.storage.branch for branch in self.storage.iter_pipes(False): if not self._skip_merge(old_branch, branch): yield branch, old_branch old_branch = branch @staticmethod def _refresh_tree(tree): return workingtree.WorkingTree.open(tree.bzrdir.root_transport.base) def _merge_commit(self, tree, new_branch, old_branch, merge_config): if self._merge_commit_branch(new_branch, old_branch, merge_config): return [] trace.note('Switching to %s for conflict resolution.' % new_branch.nick) self.store_uncommitted() self._switch_to_tree(new_branch, new_branch.basis_tree()) tree = self._refresh_tree(tree) return self._merge_tree(tree, old_branch, merge_config) def pipeline_merge(self, from_branch=None, merge_config=None): """Use the supplied tree to merge along the pipeline. Each pipe is merged into the next one, and committed, until conflicts are encountered or the end of the pipeline is reached. :param from_submit: If true, merge the pipe's submit branch into the pipe before merging the pipe into subsequent pipes. """ checkout_branch_changed = False if merge_config is None: merge_config = MergeConfig() for new_branch, old_branch in self._mergeables(from_branch): if self._merge_commit(self.checkout, new_branch, old_branch, merge_config) != []: return False if new_branch.base == self.checkout.branch.base: checkout_branch_changed = True else: if checkout_branch_changed: self.checkout.update() return True def get_merge_branch(self): previous_loc = self.storage.branch.get_submit_branch() if previous_loc is None: previous_loc = self.storage.branch.get_parent() if previous_loc is None: return None return Branch.open(previous_loc) def _patch_previous_branch(self): prev_pipe = self.get_prev_pipe() if prev_pipe is not None: return prev_pipe else: return self.get_merge_branch() def get_prev_revision_id(self): prev = self.get_prev_pipe() if prev is not None: return prev.last_revision() merge_branch = self.get_merge_branch() if merge_branch is None: return None branch = self.storage.branch merge_branch.lock_read() try: branch.lock_read() try: graph = branch.repository.get_graph(merge_branch.repository) return graph.find_unique_lca(merge_branch.last_revision(), branch.last_revision()) finally: branch.unlock() finally: merge_branch.unlock() def get_patch(self): """Get a patch representing this pipe. For the first pipe, the patch is against the submit branch, falling back to the parent branch. If there is neither a submit or parent branch, None is returned. For subsequent pipes, the patch is against the previous pipe. """ from_branch = self._patch_previous_branch() if from_branch is None: return None to_branch = self.storage.branch to_tree = to_branch.basis_tree() from_branch.lock_read() try: graph = from_branch.repository.get_graph(to_branch.repository) lca = graph.find_unique_lca(from_branch.last_revision(), to_branch.last_revision()) from_tree = from_branch.repository.revision_tree(lca) output = StringIO() diff.DiffTree(from_tree, to_tree, output).show_diff(None) return output.getvalue() finally: from_branch.unlock() def look_up(self, name, url): """Look up a branch location from a pipe location alias.""" manager = PipeManager(Branch.open_containing('.')[0], None) return look_up_pipe(manager, name).base def look_up_pipe(manager, name): if name.startswith('pipe:'): return manager.storage.find_pipe(name[len('pipe:'):]) elif name == 'first': result = manager.get_first_pipe() elif name == 'last': result = manager.get_last_pipe() elif name == 'next': result = manager.get_next_pipe() if result is None: raise errors.DirectoryLookupFailure('No next pipe.') else: result = manager.get_prev_pipe() if result is None: raise errors.DirectoryLookupFailure('No previous pipe.') return result def dwim_pipe(manager, pipe): """Convert a pipe name or alias into a branch.""" if pipe is not None and pipe[0] == ':' and is_pipe_alias(pipe[1:]): return look_up_pipe(manager, pipe[1:]) else: return manager.storage.find_pipe(pipe) def tree_to_pipeline(tree): """Convert a colocated tree and branch for use with pipelines. This moves the branch of a tree into a new "branches" subdirectory. It ensures that a shared repository is in use, creating one in "branches", if necessary. The previous location of the branch becomes a BranchReference to the new location. """ if (tree.bzrdir.root_transport.base != tree.branch.bzrdir.root_transport.base): raise errors.AlreadyLightweightCheckout(tree.bzrdir) branches_transport = tree.bzrdir.root_transport.clone('.bzr/branches') pipe_transport = branches_transport.clone(tree.branch.nick) pipe_transport.create_prefix() if not tree.branch.repository.is_shared(): format = tree.bzrdir.cloning_metadir() newdir = format.initialize_on_transport(branches_transport) newdir.create_repository(shared=True) new_branch = tree.bzrdir.sprout(pipe_transport.base, possible_transports=[pipe_transport], create_tree_if_local=False).open_branch() tree.bzrdir.destroy_branch() BranchReferenceFormat().initialize(tree.bzrdir, target_branch=new_branch) return workingtree.WorkingTree.open(tree.basedir) def is_lefthand_ancestor(graph, candidate_lefthand_ancestor, candidate_descendant): current = candidate_descendant searcher = graph._make_breadth_first_searcher( [candidate_lefthand_ancestor]) lefthand = set() while True: lefthand.add(current) if current == candidate_lefthand_ancestor: return True if len(lefthand.intersection(searcher.seen)) != 0: return False parents = graph.get_parent_map([current]) if len(parents) == 0: return False current_parents = parents[current] if len(current_parents) == 0: return False current = current_parents[0] searcher.step() class MergeConfig(object): """Configure merge behaviour.""" def __init__(self, merge_type=None, show_base=False, reprocess=False): if merge_type is None: merge_type = merge.Merge3Merger self.merge_type = merge_type self.show_base = show_base self.reprocess = reprocess bzr-pipeline/real_init.py0000644000000000000000000001570212271626614014140 0ustar 00000000000000# Copyright (C) 2009, 2011 Aaron Bentley # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA """Manage a series of branches as a pipeline. Here is a summary of the commands added. ==================== ======================================================= Command Description ==================== ======================================================= reconfigure-pipeline Reconfigure a tree with branch into a lightweight checkout of a pipe. add-pipe Add a pipe to the pipeline remove-pipe Remove a pipe from the pipeline switch-pipe Switch from one pipe to another show-pipeline Show the current pipeline pump From this pipe onward, merge all pipes into their next pipe and commit sync-pipeline Synchronise the contents of this pipeline with another copy pipe-patches Export the pipeline as a collection of patches, one per pipe ==================== ======================================================= It also extends `merge --uncommitted` to work with pipes. To get started, create a branch and lightweight checkout:: bzr init pipe1 bzr checkout --lightweight pipe1 tree cd tree commit -m "first commit" bzr add-pipe pipe2 bzr show-pipeline If you have an existing tree that you wish to start using as a pipeline, use `reconfigure-pipeline` to convert it. The switch-pipe command, in addition to the normal switch behavour, stores any uncommitted changes to the source pipe, and restores any uncommitted changes in the target pipe. This supports a common need to switch between several related tasks without committing. The pump command merges and commits changes along the pipeline, starting with the current pipe. For example:: bzr add-pipe next echo "hello" > new bzr add bzr commit -m "Added file new" bzr pump This will commit a revision to the pipe named "next" that adds the file "new". Each pipe is identified by its branch nick. The location of each pipe is provided as a location alias, which consists of ":pipe:" followed by its nick. So, to switch to pipe named "my-new" using the standard switch command:: bzr switch :pipe:my-new The aliases :prev and :next refer to the pipe before and after the current pipe, respectively. So to see the changes added in this pipe:: bzr diff -rancestor::prev These aliases work virtually everywhere you can specify a branch. Since pipes are branches, you can do anything with them that you could do with a branch. You might find the following command aliases useful:: # Abbreviate switching to next pipe next = switch-pipe :next # Abbreviate switching to previous pipe prev = switch-pipe :prev # Show diff of changes originated in this pipe pdiff = diff -r ancestor::prev # Show status for changes originated in this pipe pstatus = status --short -r branch::prev # Submit the changes originated in this pipe for merging psend = send -r ancestor::prev.. # Show commits which have not been pumped into the next pipe yet. unpumped = missing --mine :next For more information on bzr-pipeline, see the home page: http://bazaar-vcs.org/BzrPipeline. """ import bzrlib from bzrlib import errors from bzrlib.commands import plugin_cmds from bzrlib.directory_service import AliasDirectory, directories def register(name, aliases=None): if aliases is None: aliases = [] plugin_cmds.register_lazy(name, aliases, 'bzrlib.plugins.pipeline.commands') register('cmd_add_pipe') register('cmd_remove_pipe') register('cmd_rename_pipe') register('cmd_switch_pipe', ['swp']) register('cmd_store') register('cmd_show_pipeline', ['pipes']) register('cmd_pipe_patches') register('cmd_pump') register('cmd_merge') register('cmd_reconfigure_pipeline') register('cmd_sync_pipeline') plugin_cmds.register_lazy('cmd_import_loom', [], 'bzrlib.plugins.pipeline.loom') def is_pipe_alias(name): if name in ('first', 'last', 'next', 'prev'): return True if name.startswith('pipe:'): return True else: return False class PipeAliasDirectory(AliasDirectory): def look_up(self, name, url): if not is_pipe_alias(name): return AliasDirectory.look_up(self, name, url) from bzrlib.plugins.pipeline import pipeline return pipeline.look_up(self, name, url) directories.remove(':') directories.register(':', PipeAliasDirectory, 'Easy access to remembered branch locations') def get_prerequisite_from_pipe(hook_params): from bzrlib.plugins.pipeline.pipeline import PipeStorage from bzrlib.plugins.launchpad import lp_api source_branch = hook_params['source_branch'] launchpad = hook_params['launchpad'] storage = PipeStorage(source_branch.bzr) prev_pipe = storage.get_prev_pipe() if prev_pipe is not None: prerequisite_branch = lp_api.LaunchpadBranch.from_bzr(launchpad, prev_pipe) if (prerequisite_branch.lp.bzr_identity == hook_params['target_branch'].lp.bzr_identity): prerequisite_branch = None else: prerequisite_branch = None return prerequisite_branch try: from bzrlib.hooks import install_lazy_named_hook except ImportError: try: from bzrlib.plugins.launchpad import lp_propose except (ImportError, errors.DependencyNotPresent): pass else: # XXX: When we move the pipeline command, we should make sure it # constructs Submitter correctly. lp_propose.Proposer.hooks.install_named_hook( 'get_prerequisite', get_prerequisite_from_pipe, 'Get the prerequisite from the pipeline') else: install_lazy_named_hook( "bzrlib.plugins.launchpad.lp_propose", "Proposer.hooks", 'get_prerequisite', get_prerequisite_from_pipe, 'Get the prerequisite from the pipeline') def test_suite(): from bzrlib.tests.TestUtil import TestLoader, TestSuite loader = TestLoader() from bzrlib.plugins.pipeline.tests import test_pipeline, test_blackbox result = TestSuite() result.addTest(loader.loadTestsFromModule(test_pipeline)) result.addTest(loader.loadTestsFromModule(test_blackbox)) return result bzr-pipeline/setup.py0000755000000000000000000000073012271626614013330 0ustar 00000000000000#!/usr/bin/env python from distutils.core import setup setup(name="BzrPipeline", version="1.5", description="Bazaar plugin for working with a sequence of branches.", author="Aaron Bentley", author_email="aaron@aaronbentley.com", license = "GNU GPL v2", url="http://wiki.bazaar.canonical.com/BzrPipeline", packages=['bzrlib.plugins.pipeline', 'bzrlib.plugins.pipeline.tests'], package_dir={'bzrlib.plugins.pipeline': '.', }) bzr-pipeline/tests/0000755000000000000000000000000012271626614012755 5ustar 00000000000000bzr-pipeline/.the_kraken/config0000644000000000000000000000014112271626614015170 0ustar 00000000000000testing = false require_code_name = true edit_changelog = true check_script = ./check-release.py bzr-pipeline/tests/__init__.py0000644000000000000000000000434512271626614015074 0ustar 00000000000000from bzrlib import tests from bzrlib.plugins.pipeline.pipeline import ( PipeManager, PipeStorage, ) class TestCaseWithPipes(tests.TestCaseWithTransport): def make_branch_and_checkout(self, branch_path, checkout_path='checkout'): branch = self.make_branch(branch_path) return branch.create_checkout(checkout_path, lightweight=True) def connect_many(self, *branches): prev = None for branch in branches: if prev is not None: PipeStorage.connect(prev, branch) prev = branch def assertBaseEqual(self, left, right): self.assertEqual([l.base for l in left], [r.base for r in right]) def create_long_pipeline(self): foo = self.make_branch('foo') bar = self.make_branch('bar') baz = self.make_branch('baz') qux = self.make_branch('qux') quxx = self.make_branch('quxx') self.connect_many(foo, bar, baz, qux, quxx) return [foo, bar, baz, qux, quxx] def create_conflict_pipeline(self, base=None, other=None, this=None): if base is None: base = 'base text\n' if other is None: other = 'merge text\n' if this is None: this = 'tree text\n' foo = self.make_branch_and_checkout('foo') self.build_tree_contents([('checkout/foo', base)]) foo.add('foo') foo.commit('added foo') foo_manager = PipeManager(foo.branch, foo) last = PipeStorage(foo.branch).insert_pipe('last') first = foo.branch foo_manager.switch_to_pipe(last) foo = foo_manager._refresh_tree(foo) self.build_tree_contents([('checkout/foo', this)]) foo.commit('commiting this text') foo_manager.switch_to_pipe(first) foo = foo_manager._refresh_tree(foo) self.build_tree_contents([('checkout/foo', other)]) foo.commit('commiting other text') return foo # Compatibility with bzr 2.2 if getattr(TestCaseWithPipes, 'assertPathExists', None) is None: TestCaseWithPipes.assertPathExists = TestCaseWithPipes.failUnlessExists if getattr(TestCaseWithPipes, 'assertPathDoesNotExist', None) is None: TestCaseWithPipes.assertPathDoesNotExist = TestCaseWithPipes.failIfExists bzr-pipeline/tests/test_blackbox.py0000644000000000000000000005043712271626614016164 0ustar 00000000000000# Copyright (C) 2009-2011 Aaron Bentley # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA from cStringIO import StringIO import errno from textwrap import dedent from bzrlib import branch, osutils, transform, workingtree from bzrlib.plugins.pipeline.pipeline import PipeManager, PipeStorage from bzrlib.plugins.pipeline.tests import TestCaseWithPipes class TestBlackbox(TestCaseWithPipes): def test_add_pipe(self): self.make_branch_and_checkout('first') self.run_bzr('add-pipe last -d checkout') checkout = workingtree.WorkingTree.open('checkout') manager = PipeManager(checkout.branch, checkout) self.assertContainsRe(manager.storage.branch.base, 'last/$') self.assertContainsRe(manager.storage.get_prev(), 'first/$') top_storage = PipeStorage(branch.Branch.open('first')) self.assertContainsRe(top_storage.get_next(), 'last/$') def test_add_pipe_no_switch(self): self.make_branch_and_checkout('first') self.run_bzr('add-pipe last --no-switch', working_dir='checkout') storage = PipeStorage(branch.Branch.open('checkout')) self.assertContainsRe(storage.branch.base, 'first/$') self.assertContainsRe(storage.get_next(), 'last/$') top_storage = PipeStorage(branch.Branch.open('last')) self.assertContainsRe(top_storage.get_prev(), 'first/$') def test_add_pipe_after(self): tree = self.make_branch_and_checkout('first') branch2 = self.make_branch('middle') PipeStorage.connect(tree.branch, branch2) self.run_bzr('add-pipe last --after middle', working_dir='checkout') storage = PipeStorage(branch2) self.assertContainsRe(storage.get_next(), 'last/$') top_storage = PipeStorage(branch.Branch.open('last')) self.assertContainsRe(top_storage.get_prev(), 'middle/$') def test_add_pipe_before(self): tree = self.make_branch_and_checkout('first') tree.commit('first commit', rev_id='rev1') last = PipeStorage(tree.branch).insert_pipe('last') last.lock_write() try: tt = transform.TransformPreview(last.basis_tree()) tt.commit(last, 'empty commit') finally: last.unlock() self.run_bzr('add-pipe middle --before last', working_dir='checkout') storage = PipeStorage(last) self.assertContainsRe(storage.get_prev(), 'middle/$') middle_storage = PipeStorage(branch.Branch.open('middle')) self.assertContainsRe(middle_storage.get_next(), 'last/$') self.assertEqual('rev1', middle_storage.branch.last_revision()) def test_add_pipe_before_first(self): """Test the LCA codepath.""" submit = self.make_branch_and_tree('submit') submit.commit('rev1') tree = self.make_branch_and_checkout('first') tree.commit('foo') tree.branch.set_parent(submit.branch.base) self.run_bzr('add-pipe -d checkout new-first --before') def test_add_pipe_before_no_neighbour(self): tree = self.make_branch_and_checkout('last') branch2 = self.make_branch('middle') PipeStorage.connect(tree.branch, branch2) self.run_bzr('add-pipe first --before', working_dir='checkout') storage = PipeStorage(tree.branch) self.assertContainsRe(storage.get_prev(), 'first/$') first_storage = PipeStorage(branch.Branch.open('first')) self.assertContainsRe(first_storage.get_next(), 'last/$') def test_add_pipe_revision(self): tree = self.make_branch_and_checkout('first') rev1 = tree.commit('rev1') tree.commit('rev2') self.run_bzr('add-pipe -r 1 next', working_dir='checkout') self.assertEqual(rev1, branch.Branch.open('next').last_revision()) def test_add_pipe_two_revision(self): tree = self.make_branch_and_checkout('first') tree.commit('rev1') tree.commit('rev2') out, err = self.run_bzr('add-pipe -r 1..2 next', working_dir='checkout', retcode=3) self.assertContainsRe(err, 'Only one revision may be supplied.') def test_add_pipe_requires_lightweight_checkout(self): self.make_branch_and_tree('first') out, err = self.run_bzr('add-pipe new', working_dir='first', retcode=3) self.assertContainsRe(err, 'add-pipe should be run in a lightweight' ' checkout. See bzr help pipeline for details.') def do_add(self, no_switch=False): branch = self.make_branch('first') tree = branch.create_checkout('checkout', lightweight=True) self.build_tree(['checkout/file1']) tree.add('file1') tree.commit('rev1') self.build_tree(['checkout/file2']) tree.add('file2') cmdline = ['add-pipe', 'next'] if no_switch: cmdline.append('--no-switch') self.run_bzr(cmdline, working_dir='checkout') def test_add_pipe_no_switch_stores_uncommitted(self): self.do_add(no_switch=True) self.assertPathExists('checkout/file1') self.assertPathDoesNotExist('checkout/file2') def test_add_pipe_keeps_uncommitted(self): self.do_add(no_switch=False) self.assertPathExists('checkout/file1') self.assertPathExists('checkout/file2') def test_add_pipe_accepts_branch(self): self.make_branch_and_checkout('first') self.make_branch('second') self.run_bzr('add-pipe -d checkout second') def test_add_pipe_forbidden(self): self.make_branch_and_checkout('first') out, err = self.run_bzr('add-pipe -d checkout :next', retcode=3) self.assertContainsRe(err, 'No next pipe.') def test_remove_pipe(self): foo = PipeStorage(self.make_branch('foo')) bar = PipeStorage(foo.insert_pipe('bar')) self.run_bzr('remove-pipe foo') self.assertIs(None, foo.get_next_pipe()) self.assertIs(None, bar.get_prev_pipe()) def test_remove_switches_checkout_next(self): foo = PipeStorage(self.make_branch('foo')) bar = PipeStorage(foo.insert_pipe('bar')) foo.branch.create_checkout('checkout', lightweight=True) self.run_bzr('remove-pipe', working_dir='checkout') self.assertEqual(branch.Branch.open('checkout').base, bar.branch.base) def test_remove_switches_checkout_prev(self): foo = PipeStorage(self.make_branch('foo')) bar = foo.insert_pipe('bar') bar.create_checkout('checkout', lightweight=True) self.run_bzr('remove-pipe', working_dir='checkout') self.assertEqual(branch.Branch.open('checkout').base, foo.branch.base) def test_remove_ignores_non_checkout_tree(self): foo = self.make_branch_and_tree('foo') PipeStorage(foo.branch).insert_pipe('bar') self.run_bzr('remove-pipe', working_dir='foo') def test_remove_unconnected(self): foo = self.make_branch('foo') foo.create_checkout('checkout', lightweight=True) out, err = self.run_bzr('remove-pipe', working_dir='checkout', retcode=3) self.assertContainsRe(err, 'Branch is not connected to a pipeline.') def test_remove_specified_pipe(self): foo = self.make_branch('foo') storage = PipeStorage(foo) checkout = foo.create_checkout('checkout', lightweight=True) bar = PipeStorage(storage.insert_pipe('bar')) storage.insert_pipe('baz') self.run_bzr('remove-pipe bar', working_dir='checkout') self.assertIs(None, bar.get_next_pipe()) self.assertIs(None, bar.get_prev_pipe()) self.assertEqual('../baz/', storage.get_next()) checkout = workingtree.WorkingTree.open('checkout') self.assertEqual(foo.base, checkout.branch.base) def test_remove_pipe_keeps_branch(self): tree = self.make_branch_and_checkout('first') PipeStorage(tree.branch).insert_pipe('second') self.run_bzr('remove-pipe second', working_dir='checkout') self.assertPathExists('second') def test_remove_pipe_branch(self): tree = self.make_branch_and_checkout('first') PipeStorage(tree.branch).insert_pipe('second') self.run_bzr('remove-pipe second --branch', working_dir='checkout') self.assertPathDoesNotExist('second') def test_rename_pipe(self): tree = self.make_branch_and_checkout('first') PipeStorage(tree.branch).insert_pipe('old-name') self.run_bzr('rename-pipe new-name', working_dir='checkout') self.assertEndsWith(branch.Branch.open('checkout').base, 'new-name/') def test_switch_pipe(self): self.do_add(no_switch=True) self.build_tree(['checkout/file3']) workingtree.WorkingTree.open('checkout').add('file3') out, err = self.run_bzr('switch-pipe next -d checkout') self.assertPathExists('checkout/file1') self.assertPathExists('checkout/file2') self.assertPathDoesNotExist('checkout/file3') self.assertEqual('Uncommitted changes stored in pipe "first".\n' 'Switched from "first" to "next".\n', err) def test_switch_not_checkout_keeps_changes(self): foo = self.make_branch_and_tree('foo') PipeStorage(foo.branch).insert_pipe('bar') self.build_tree(['foo/file']) foo.add('file') out, err = self.run_bzr('switch-pipe bar', working_dir='foo', retcode=3) self.assertPathExists('foo/file') self.assertContainsRe(err, 'Directory is not a lightweight checkout.') def test_switch_pipe_accepts_aliases(self): checkout = self.make_branch_and_checkout('foo') checkout.commit('lala') PipeStorage(checkout.branch).insert_pipe('bar') self.run_bzr('switch-pipe -d checkout :next') checkout = workingtree.WorkingTree.open('checkout') def test_show_pipeline(self): first = self.make_branch('first') last = self.make_branch('last') PipeStorage.connect(first, last) PipeStorage(first)._put_transform(StringIO('lala')) last.create_checkout('checkout', lightweight=True) out, err = self.run_bzr('show-pipeline checkout') self.assertEqual(' U first\n* last\n', out) def test_show_pipeline_subdir(self): first = self.make_branch('first') last = self.make_branch('last') PipeStorage.connect(first, last) PipeStorage(first)._put_transform(StringIO('lala')) last.create_checkout('checkout', lightweight=True) self.build_tree(['checkout/subdir/']) out, err = self.run_bzr('show-pipeline', working_dir='checkout/subdir') self.assertEqual(' U first\n* last\n', out) def test_pump(self): first = self.make_branch('first') checkout = first.create_checkout('checkout', lightweight=True) checkout.commit('first 1', rev_id='rev1-id') first_manager = PipeManager(first, checkout) next = first_manager.storage.insert_pipe('next') checkout.commit('first 2', rev_id='rev2-id') first_manager.switch_to_pipe(next) checkout = workingtree.WorkingTree.open('checkout') checkout.commit('next', rev_id='rev3-id') PipeManager(next, checkout).switch_to_pipe(first) self.run_bzr('pump -d checkout') rev = next.repository.get_revision(next.last_revision()) self.assertEqual(['rev3-id', 'rev2-id'], rev.parent_ids) def test_pump_requires_light_checkout(self): self.make_branch_and_tree('foo') out, err = self.run_bzr('pump', working_dir='foo', retcode=3) self.assertContainsRe(err, 'Directory is not a lightweight checkout.') def test_pump_from_branch(self): submit = self.make_branch_and_tree('from') submit.commit('empty') one = submit.bzrdir.sprout('one').open_branch() one.set_submit_branch(submit.branch.base) self.build_tree(['from/from-file']) submit.add('from-file') submit.commit('added from-file') two = PipeStorage(one).insert_pipe('two') two.create_checkout('checkout', lightweight=True) self.run_bzr('pump -d checkout --from from') self.assertPathExists('checkout/from-file') def test_pump_from_pipe(self): checkout = self.make_branch_and_checkout('one') checkout.commit('empty commit') manager = PipeManager.from_checkout(checkout) two = manager.storage.insert_pipe('two') self.build_tree(['checkout/one-file']) checkout.add('one-file') checkout.commit('added one-file.') self.assertPathExists('checkout/one-file') manager.switch_to_pipe(two) self.run_bzr('pump -d checkout --from one') self.run_bzr('update checkout') self.assertPathExists('checkout/one-file') def test_pump_from_submit(self): submit = self.make_branch_and_tree('submit') submit.commit('empty') one = submit.bzrdir.sprout('one').open_branch() one.set_submit_branch(submit.branch.base) self.build_tree(['submit/submit-file']) submit.add('submit-file') submit.commit('added submit-file') two = PipeStorage(one).insert_pipe('two') two.create_checkout('checkout', lightweight=True) self.run_bzr('pump -d checkout --from-submit') self.assertPathExists('checkout/submit-file') def get_merge_text(self, foo, *options): out, err = self.run_bzr(['pump', '-d', 'checkout'] + list(options)) return foo.get_file_text(foo.path2id('foo')) def test_pump_show_base(self): foo = self.create_conflict_pipeline() text = self.get_merge_text(foo, '--show-base') self.assertEqual(dedent("""\ <<<<<<< TREE tree text ||||||| BASE-REVISION base text ======= merge text >>>>>>> MERGE-SOURCE """) , text) def test_pump_reprocess(self): foo = self.create_conflict_pipeline('base\n', 'common\none\n', 'common\ntwo\n') text = self.get_merge_text(foo, '--reprocess') self.assertEqual(dedent("""\ common <<<<<<< TREE two ======= one >>>>>>> MERGE-SOURCE """) , text) def test_pump_merge_type(self): foo = self.create_conflict_pipeline('base\n', 'ace\n', '') text = self.get_merge_text(foo, '--lca') self.assertEqual(dedent("""\ ace """) , text) def test_merge_uncommitted_from_pipe(self): foo = self.make_branch('foo') checkout = foo.create_checkout('checkout', lightweight=True) checkout.commit('Empty commit.') self.build_tree(['checkout/file']) checkout.add('file') bar = PipeManager(PipeStorage(foo).insert_pipe('bar'), checkout) bar.store_uncommitted() self.run_bzr('merge -d checkout --uncommitted bar') self.assertPathExists('checkout/file') self.assertTrue(bar.has_stored_changes()) def test_merge_uncommitted_from_pipe_alias(self): foo = self.make_branch('foo') checkout = foo.create_checkout('checkout', lightweight=True) checkout.commit('Empty commit.') self.build_tree(['checkout/file']) checkout.add('file') bar = PipeManager(PipeStorage(foo).insert_pipe('bar'), checkout) bar.store_uncommitted() self.run_bzr('merge --uncommitted :next', working_dir='checkout') self.assertPathExists('checkout/file') self.assertTrue(bar.has_stored_changes()) def test_merge_uncommitted_no_location_uses_submit_branch(self): foo = self.make_branch_and_checkout('foo') foo.commit('Empty commit.') bar = foo.bzrdir.sprout('bar').open_workingtree() self.build_tree(['bar/file']) bar.add('file') foo.branch.set_submit_branch(bar.branch.base) self.run_bzr('merge --uncommitted', working_dir='checkout') self.assertPathExists('checkout/file') def test_sync_pipeline(self): self.build_tree(['local/', 'remote/']) foo = PipeStorage(self.make_branch('local/foo')) foo.insert_pipe('bar') self.run_bzr('sync-pipeline ../../remote/foo', working_dir='local/foo') self.assertPathExists('remote/foo') self.assertPathExists('remote/bar') def test_sync_pipeline_default(self): self.build_tree(['local/', 'remote/']) foo = PipeStorage(self.make_branch('local/foo')) foo.insert_pipe('bar') foo.branch.set_push_location(osutils.abspath('remote/foo')) self.run_bzr('sync-pipeline', working_dir='local/foo') self.assertPathExists('remote/foo') self.assertPathExists('remote/bar') def test_sync_pipeline_errors_if_default_none(self): self.build_tree(['local/', 'remote/']) foo = PipeStorage(self.make_branch('local/foo')) foo.insert_pipe('bar') out, err = self.run_bzr('sync-pipeline', working_dir='local/foo', retcode=3) self.assertContainsRe(err, 'No location specified and none remembered.') def test_sync_pipeline_uses_first_pipe(self): self.build_tree(['local/', 'remote/']) foo = PipeStorage(self.make_branch('local/foo')) bar = foo.insert_pipe('bar') foo.branch.set_push_location(osutils.abspath('remote/foo')) bar.set_push_location(osutils.abspath('nowhere/bar')) self.run_bzr('sync-pipeline', working_dir='local/bar') self.assertPathExists('remote/foo') self.assertPathExists('remote/bar') def test_sync_pipeline_falls_back_to_other_pipes(self): self.build_tree(['local/', 'remote/']) foo = PipeStorage(self.make_branch('local/foo')) bar = foo.insert_pipe('bar') bar.set_push_location(osutils.abspath('remote/bar')) self.run_bzr('sync-pipeline', working_dir='local/foo') self.assertPathExists('remote/foo') self.assertPathExists('remote/bar') def test_sync_pipeline_previous_pipe_added_with_push_loc(self): self.build_tree(['local/', 'remote/']) foo = PipeStorage(self.make_branch('local/foo')) self.run_bzr('sync-pipeline ../../remote/foo', working_dir='local/foo') bar = foo.insert_pipe('bar', before=True) bar.set_push_location(osutils.abspath('remote/bar')) self.run_bzr('sync-pipeline', working_dir='local/bar') def test_reconfigure_pipeline(self): self.make_branch_and_tree('pipeline') self.run_bzr('reconfigure-pipeline', working_dir='pipeline') branch.Branch.open('pipeline/.bzr/branches/pipeline') def assertFileContainsRe(self, regex, path): try: my_file = open(path, 'rb') except IOError, e: if e.errno == errno.ENOENT: self.fail('%s does not exist' % path) else: raise try: self.assertContainsRe(my_file.read(), regex) finally: my_file.close() def test_pipe_patches(self): """Pipe-patches should export a series of patches.""" upstream = self.make_branch_and_tree('upstream') self.build_tree_contents([('upstream/file', 'A\n')]) upstream.add('file') upstream.commit('commit') b_pipe = upstream.bzrdir.sprout('b-pipe').open_workingtree() self.build_tree_contents([('b-pipe/file', 'B\n')]) b_pipe.commit('commit') c_pipe = PipeStorage(b_pipe.branch).insert_pipe('c-pipe') self.build_tree_contents([('c-pipe/file', 'C\n')]) c_pipe.bzrdir.open_workingtree().commit('commit') self.build_tree(['patches/']) self.run_bzr('pipe-patches -d b-pipe patches') self.assertFileContainsRe('-A\n\\+B', 'patches/00-b-pipe.patch') self.assertFileContainsRe('-B\n\\+C', 'patches/01-c-pipe.patch') bzr-pipeline/tests/test_pipeline.py0000644000000000000000000011323112271626614016174 0ustar 00000000000000# Copyright (C) 2009, 2010 Aaron Bentley # # 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 Street, Fifth Floor, Boston, MA 02110-1301 USA from cStringIO import StringIO import os from textwrap import dedent from bzrlib import ( branch, bzrdir, conflicts, directory_service, errors, merge, switch, workingtree, tests, transform, ) from bzrlib.plugins.pipeline.pipeline import ( BranchInTheWay, ChangesAlreadyStored, DuplicatePipe, DuplicatePipeLocation, dwim_pipe, MergeConfig, NoSuchPipe, PipeManager, PipeStorage, SwitchWithConflicts, tree_to_pipeline, UnknownRemotePipe, ) from bzrlib.plugins.pipeline.tests import TestCaseWithPipes class TestPipeStorage(TestCaseWithPipes): def test_connect(self): bar = self.make_branch('bar') baz = self.make_branch('baz') PipeStorage.connect(bar, baz) bar_manager = PipeStorage(bar) self.assertEqual('../baz/', bar_manager.get_next()) self.assertIs(None, bar_manager.get_prev()) baz_manager = PipeStorage(baz) self.assertEqual('../bar/', baz_manager.get_prev()) self.assertIs(None, baz_manager.get_next()) def test_connect_relative_url(self): foo = self.make_branch('foo') bar = self.make_branch('bar') PipeStorage.connect(foo, bar) self.assertEqual('../bar/', PipeStorage(foo).get_next()) self.assertEqual('../foo/', PipeStorage(bar).get_prev()) def test_disconnect_middle(self): foo = self.make_branch('foo') foo_manager = PipeStorage(foo) bar = foo_manager.insert_pipe('bar') bar_manager = PipeStorage(bar) baz = bar_manager.insert_pipe('baz') bar_manager.disconnect() self.assertEqual('../baz/', foo_manager.get_next()) self.assertEqual('../foo/', PipeStorage(baz).get_prev()) self.assertIs(None, bar_manager.get_next_pipe()) def test_disconnect_first(self): foo = self.make_branch('foo') foo_storage = PipeStorage(foo) bar = foo_storage.insert_pipe('bar') bar_storage = PipeStorage(bar) foo_storage.disconnect() self.assertIs(None, foo_storage.get_next_pipe()) self.assertIs(None, bar_storage.get_prev()) def test_disconnect_last(self): foo = self.make_branch('foo') foo_storage = PipeStorage(foo) bar = foo_storage.insert_pipe('bar') bar_storage = PipeStorage(bar) bar_storage.disconnect() self.assertIs(None, foo_storage.get_next_pipe()) self.assertIs(None, bar_storage.get_prev()) def test_insert_pipe(self): foo = self.make_branch('foo') baz = self.make_branch('baz') PipeStorage.connect(foo, baz) storage = PipeStorage(foo) bar = storage.insert_pipe('bar') self.assertBaseEqual([foo, bar, baz], storage.list_pipes()) def test_insert_pipe_before(self): foo = self.make_branch('foo') storage = PipeStorage(foo) bar = storage.insert_pipe('bar', before=True) self.assertBaseEqual([bar, foo], storage.list_pipes()) def test_insert_pipe_before_middle(self): foo = self.make_branch('foo') baz = self.make_branch('baz') PipeStorage.connect(foo, baz) storage = PipeStorage(baz) bar = storage.insert_pipe('bar', before=True) self.assertBaseEqual([foo, bar, baz], storage.list_pipes()) def test_insert_pipe_revision(self): foo = self.make_branch_and_tree('foo') rev1 = foo.commit('rev1') foo.commit('rev2') storage = PipeStorage(foo.branch) bar = storage.insert_pipe('bar', rev1) self.assertEqual(rev1, bar.last_revision()) def test_insert_pipe_checks_dupes(self): foo = self.make_branch_and_tree('foo') storage = PipeStorage(foo.branch) e = self.assertRaises(DuplicatePipe, storage.insert_pipe, 'foo') self.assertEqual('There is already a pipe named "foo."', str(e)) def test_insert_branch_checks_dupes(self): foo = self.make_branch('foo') manager = PipeStorage(foo) e = self.assertRaises(DuplicatePipeLocation, manager.insert_branch, foo) def test_get_put_transform(self): manager = PipeStorage(self.make_branch('branch')) self.assertIs(None, manager._get_transform()) manager._put_transform(StringIO('Hello')) self.assertEqual('Hello', manager._get_transform().read()) def test_put_transform_none(self): manager = PipeStorage(self.make_branch('branch')) manager._put_transform(StringIO('Hello')) manager._put_transform(None) self.assertIs(None, manager._get_transform()) manager._put_transform(None) def test_find_pipe(self): pipe_list = self.create_long_pipeline() storage = PipeStorage(pipe_list[2]) found = storage.find_pipe('foo') self.assertEqual(pipe_list[0].base, found.base) self.assertEqual(pipe_list[4].base, storage.find_pipe('quxx').base) def test_find_pipe_not_present(self): pipe_list = self.create_long_pipeline() storage = PipeStorage(pipe_list[2]) e = self.assertRaises(NoSuchPipe, storage.find_pipe, 'not-present') self.assertEqual('There is no pipe with nick "not-present".', str(e)) class TestPipeManager(TestCaseWithPipes): def test_rename_pipe(self): checkout = self.make_branch_and_checkout('old_name') manager = PipeManager(checkout.branch, checkout) manager.rename_pipe('new_name') new_loc = branch.Branch.open('new_name') def test_rename_pipe_with_siblings(self): prev = self.make_branch('prev') checkout = self.make_branch_and_checkout('old_name') next = self.make_branch('next') self.connect_many(prev, checkout.branch, next) PipeManager(checkout.branch, checkout).rename_pipe('new_name') self.assertEndsWith(PipeStorage(prev).get_next(), 'new_name/') new_loc = branch.Branch.open('new_name') self.assertEndsWith(PipeStorage(next).get_prev(), 'new_name/') def test_list_pipes(self): pipe_list = self.create_long_pipeline() self.assertBaseEqual(pipe_list, PipeStorage(pipe_list[2]).list_pipes()) def test_get_first_pipe(self): pipe_list = self.create_long_pipeline() tree = pipe_list[0].bzrdir.create_workingtree() manager = PipeManager(pipe_list[2], tree) self.assertEqual(pipe_list[0].base, manager.get_first_pipe().base) def test_get_first_pipe_from_first(self): pipe_list = self.create_long_pipeline() tree = pipe_list[0].bzrdir.create_workingtree() manager = PipeManager(pipe_list[0], tree) self.assertEqual(pipe_list[0].base, manager.get_first_pipe().base) def test_get_last_pipe(self): pipe_list = self.create_long_pipeline() tree = pipe_list[0].bzrdir.create_workingtree() manager = PipeManager(pipe_list[2], tree) self.assertEqual(pipe_list[-1].base, manager.get_last_pipe().base) def test_get_last_pipe_from_last(self): pipe_list = self.create_long_pipeline() tree = pipe_list[0].bzrdir.create_workingtree() manager = PipeManager(pipe_list[-1], tree) self.assertEqual(pipe_list[-1].base, manager.get_last_pipe().base) def store_uncommitted(self): tree = self.make_branch_and_tree('tree') tree.commit('get root in there') self.build_tree_contents([('tree/file', 'content')]) tree.add('file', 'file-id') manager = PipeManager(tree.branch, tree) manager.store_uncommitted() return tree, manager def test_store_uncommitted(self): self.store_uncommitted() self.assertPathDoesNotExist('tree/file') def test_store_uncommitted_no_change(self): tree = self.make_branch_and_tree('tree') tree.commit('get root in there') manager = PipeManager(tree.branch, tree) manager.store_uncommitted() self.assertFalse(manager.has_stored_changes()) def test_store_uncommitted_errors_on_overwrite(self): branch = self.make_branch('branch') checkout = branch.create_checkout('checkout', lightweight=True) manager = PipeManager(branch, checkout) manager.storage._put_transform(StringIO('Hello')) self.build_tree(['checkout/file']) checkout.add('file') e = self.assertRaises(ChangesAlreadyStored, manager.store_uncommitted) self.assertEqual('Cannot store uncommitted changes because this pipe' ' already stores uncommitted changes.', str(e)) def test_store_all(self): tree = self.make_branch_and_tree('tree') self.build_tree_contents([('tree/file', 'contents1')]) tree.add('file') tree.commit('message') self.build_tree_contents([('tree/file', 'contents2')]) PipeManager(tree.branch, tree).store_all() self.assertFileEqual('contents1', 'tree/file') def test_restore_uncommitted(self): tree, manager = self.store_uncommitted() manager.restore_uncommitted() self.assertPathExists('tree/file') self.assertFalse(None, manager.has_stored_changes()) def test_restore_uncommitted_none(self): tree = self.make_branch_and_tree('tree') manager = PipeManager(tree.branch, tree) manager.restore_uncommitted() def test_restore_uncommitted_no_delete(self): tree, manager = self.store_uncommitted() manager.restore_uncommitted(delete=False) self.assertTrue(manager.has_stored_changes()) def make_checkout_first_second(self): tree = self.make_branch_and_checkout('first') tree.commit('get root in there', rev_id='rev1-id') manager = PipeManager(tree.branch, tree) second = manager.storage.insert_pipe('second') tree.commit('second commit.', rev_id='rev2-id') return tree, manager, second def test_switch_to_pipe(self): tree, manager, second = self.make_checkout_first_second() manager.switch_to_pipe(second) tree = workingtree.WorkingTree.open('checkout') self.assertEqual(second.base, tree.branch.base) self.assertEqual('rev1-id', tree.branch.last_revision()) def test_switch_to_pipe_store_restore(self): tree, manager, second = self.make_checkout_first_second() self.build_tree(['checkout/file']) tree.add('file') manager.switch_to_pipe(second) self.assertPathDoesNotExist('checkout/file') tree = workingtree.WorkingTree.open('checkout') first = branch.Branch.open('first') manager = PipeManager(tree.branch, tree).switch_to_pipe(first) self.assertPathExists('checkout/file') self.assertFalse(manager.storage.has_stored_changes()) tree = workingtree.WorkingTree.open('checkout') self.assertEqual(manager.storage.branch.base, tree.branch.base) self.assertEqual('rev2-id', tree.last_revision()) def test_switch_to_pipe_restore_outdated(self): tree, manager, second = self.make_checkout_first_second() checkout2 = tree.branch.create_checkout('checkout2', lightweight=True) self.build_tree(['checkout/file']) tree.add('file') manager.switch_to_pipe(second) self.build_tree(['checkout2/file2']) checkout2.add('file2') checkout2.commit('rev3', rev_id='rev3-id') tree = workingtree.WorkingTree.open('checkout') first = branch.Branch.open('first') manager = PipeManager(tree.branch, tree).switch_to_pipe(first) self.assertFalse(manager.storage.has_stored_changes()) tree = workingtree.WorkingTree.open('checkout') self.assertEqual(manager.storage.branch.base, tree.branch.base) self.assertEqual('rev3-id', tree.last_revision()) self.assertPathExists('checkout/file2') def test_switch_to_pipe_local_conflicts(self): tree, manager, second = self.make_checkout_first_second() tree.set_conflicts(conflicts.ConflictList( [conflicts.ContentsConflict('lala')])) self.assertRaises(SwitchWithConflicts, manager.switch_to_pipe, second) def do_conflict_free_pipeline_merge(self): one = self.make_branch('one') checkout = one.create_checkout('checkout', lightweight=True) checkout.commit('rev1') pipe_one = PipeManager(one, checkout) two = pipe_one.storage.insert_pipe('two') self.build_tree(['checkout/one']) checkout.add('one') checkout.commit('rev2', rev_id='rev2-id') switch.switch(checkout.bzrdir, two) checkout = workingtree.WorkingTree.open('checkout') self.build_tree(['checkout/two']) checkout.add('two') checkout.commit('rev3', rev_id='rev3-id') switch.switch(checkout.bzrdir, one) checkout = workingtree.WorkingTree.open('checkout') pipe_one.pipeline_merge() checkout = workingtree.WorkingTree.open('checkout') self.assertEqual('one', checkout.branch.nick) return two.basis_tree() def test_pipeline_merge(self): """When we do a pipeline merge, we merge into later pipes. When finished, we return to the initial pipe. """ two_basis = self.do_conflict_free_pipeline_merge() self.assertIsNot(None, two_basis.path2id('two')) self.assertIsNot(None, two_basis.path2id('one')) def test_pipeline_merge_updates_parents(self): """When we do a pipeline merge, we merge into later pipes. When finished, we return to the initial pipe. """ two_basis = self.do_conflict_free_pipeline_merge() self.assertEqual(['rev3-id', 'rev2-id'], two_basis.get_parent_ids()) def pipeline_merge(self, trees): self.connect_many(*[t.branch for t in trees]) checkout = trees[0].branch.create_checkout('checkout', lightweight=True) PipeManager(checkout.branch, checkout).pipeline_merge() return workingtree.WorkingTree.open('checkout') def test_pipeline_merge_stops_on_conflict(self): """When a merge is encountered, pipeline merging stops.""" one = self.make_branch_and_tree('one') self.build_tree_contents([('one/conflictable', 'base')]) one.add('conflictable') one.commit('base commit') two = one.bzrdir.sprout('two').open_workingtree() self.build_tree_contents([('one/conflictable', 'other')]) one.commit('other commit') self.build_tree_contents([('two/conflictable', 'this')]) this_revision = two.commit('this commit') checkout = self.pipeline_merge([one, two]) self.assertEqual('two', checkout.branch.nick) self.assertEqual(this_revision, checkout.branch.last_revision()) self.assertFileEqual('this', 'checkout/conflictable.THIS') self.assertFileEqual('other', 'checkout/conflictable.OTHER') def test_pipeline_merge_skips_unchanged(self): """Changes are only committed if there was an actual change.""" one = self.make_branch_and_tree('one') one.commit('base commit', rev_id='rev1-id') two = one.bzrdir.sprout('two').open_workingtree() three = one.bzrdir.sprout('three').open_workingtree() two.commit('new commit', rev_id='rev2-id') self.pipeline_merge([one, two, three]) self.assertEqual('rev2-id', two.branch.last_revision()) self.assertEqual('rev2-id', three.branch.last_revision()) def test_pipeline_merge_stores_and_restores_uncommitted(self): """When a merge is encountered, pipeline merging stops.""" one = self.make_branch('one') checkout = one.create_checkout('checkout', lightweight=True) self.build_tree(['checkout/file', 'checkout/uncommitted']) checkout.add('file') checkout.commit('base commit') manager = PipeManager(one, checkout) two = manager.storage.insert_pipe('two') checkout.commit('pointless commit') checkout.add('uncommitted') manager.pipeline_merge() self.assertIs(None, two.basis_tree().path2id('uncommitted')) self.assertIsNot(None, checkout.path2id('uncommitted')) def test_pipeline_merge_from_branch(self): submit = self.make_branch_and_tree('submit') submit.commit('empty commit') one = submit.bzrdir.sprout('one').open_branch() self.build_tree(['submit/submit-file']) submit.add('submit-file') submit.commit('added submit-file.') checkout = one.create_checkout('checkout', lightweight=True) manager = PipeManager.from_checkout(checkout) manager.pipeline_merge(from_branch=submit.branch) basis = checkout.branch.basis_tree() self.assertIsNot(None, basis.path2id('submit-file')) self.assertPathExists('checkout/submit-file') def test_pipeline_merge_from_branch_no_change(self): submit = self.make_branch_and_tree('submit') submit.commit('empty commit') one = submit.bzrdir.sprout('one').open_branch() self.build_tree(['submit/submit-file']) checkout = one.create_checkout('checkout', lightweight=True) manager = PipeManager.from_checkout(checkout) manager.pipeline_merge(from_branch=submit.branch) def test_pipeline_merge_pulls_if_possible(self): tree = self.make_branch_and_checkout('first') tree.commit('first commit') first_manager = PipeManager(tree.branch, tree) second = first_manager.storage.insert_pipe('second') third = PipeStorage(second).insert_pipe('third') tree.commit('second commit') first_manager.switch_to_pipe(second) tree = workingtree.WorkingTree.open('checkout') tree.commit('second pipe commit') first_manager.pipeline_merge() self.assertEqual(third.last_revision(), second.last_revision()) def get_merge_text(self, manager, mc): manager.pipeline_merge(merge_config=mc) foo = manager.checkout return foo.get_file_text(foo.path2id('foo')) def test_pipeline_merge_show_base(self): foo = self.create_conflict_pipeline() mc = MergeConfig(show_base=True) manager = PipeManager(foo.branch, foo) text = self.get_merge_text(manager, mc) self.assertEqual(dedent("""\ <<<<<<< TREE tree text ||||||| BASE-REVISION base text ======= merge text >>>>>>> MERGE-SOURCE """) , text) def test_test_pipeline_merge_reprocess(self): foo = self.create_conflict_pipeline('base\n', 'common\none\n', 'common\ntwo\n') manager = PipeManager(foo.branch, foo) text = self.get_merge_text(manager, MergeConfig(reprocess=True)) self.assertEqual(dedent("""\ common <<<<<<< TREE two ======= one >>>>>>> MERGE-SOURCE """) , text) def test_pipeline_merge_merge_type(self): """Exercise LCA merge bug.""" foo = self.create_conflict_pipeline('base\n', 'ace\n', '') manager = PipeManager(foo.branch, foo) mc = MergeConfig(merge_type=merge.LCAMerger) text = self.get_merge_text(manager, mc) self.assertEqual(dedent("""\ ace """) , text) def test_sync_pipeline_creates_branches(self): self.build_tree(['a/', 'b/']) tree = self.make_branch_and_tree('a/foo') a_foo = PipeManager(tree.branch, tree) a_foo.storage.insert_pipe('bar') a_foo.sync_pipeline('b/foo') b_foo = PipeStorage(branch.Branch.open('b/foo')) b_bar = PipeStorage(branch.Branch.open('b/bar')) self.assertEqual('../bar/', b_foo.get_next()) self.assertEqual('../foo/', b_bar.get_prev()) def test_sync_add_pipe(self): self.build_tree(['local/', 'remote/']) tree = self.make_branch_and_tree('local/foo') foo = PipeManager(tree.branch, tree) foo.sync_pipeline('remote/foo') remote_foo_branch = branch.Branch.open('remote/foo') checkout = remote_foo_branch.create_checkout('checkout', lightweight=True) remote_foo = PipeManager(remote_foo_branch, checkout) self.assertIs(None, remote_foo.get_next_pipe()) foo.storage.insert_pipe('bar') foo.sync_pipeline('remote/foo') self.assertEqual('../bar/', remote_foo.storage.get_next()) def test_sync_add_pipe_three(self): self.build_tree(['local/', 'remote/']) tree = self.make_branch_and_tree('local/foo') foo = PipeManager(tree.branch, tree) bar = PipeStorage(foo.storage.insert_pipe('bar')) foo.sync_pipeline('remote/foo') remote_foo_branch = branch.Branch.open('remote/foo') checkout = remote_foo_branch.create_checkout('checkout', lightweight=True) remote_foo = PipeManager(branch.Branch.open('remote/foo'), checkout) self.assertEqual('../bar/', remote_foo.storage.get_next()) bar.insert_pipe('baz') foo.sync_pipeline('remote/foo') self.assertEqual('../bar/', remote_foo.storage.get_next()) def test_sync_pipeline_push_pull(self): self.build_tree(['local/', 'remote/']) tree = self.make_branch_and_tree('local/foo') foo = PipeManager(tree.branch, tree) checkout = foo.storage.branch.create_checkout('local/checkout', lightweight=True) checkout.commit('rev1', rev_id='rev1-id') bar = foo.storage.insert_pipe('bar') foo.storage.insert_pipe('baz') # Not part of the test. Just the easiest way to copy a pipeline. foo.sync_pipeline('remote/foo') checkout.commit('rev2', rev_id='rev2-id') remote_bar = branch.Branch.open('remote/bar') remote_checkout = remote_bar.create_checkout('remote/checkout', lightweight=True) remote_checkout.commit('rev3', rev_id='rev3-id') foo.sync_pipeline('remote/foo') remote_foo = branch.Branch.open('remote/foo') self.assertEqual('rev2-id', remote_foo.last_revision()) self.assertEqual('rev3-id', bar.last_revision()) def test_sync_pipeline_diverged(self): self.build_tree(['local/', 'remote/']) tree = self.make_branch_and_tree('local/foo') foo = PipeManager(tree.branch, tree) foo.sync_pipeline('remote/foo') checkout = foo.storage.branch.create_checkout('local/checkout', lightweight=True) checkout.commit('rev1', rev_id='rev1-id') remote_foo = branch.Branch.open('remote/foo') remote_checkout = remote_foo.create_checkout('remote/checkout', lightweight=True) remote_checkout.commit('rev3', rev_id='rev3-id') self.assertRaises(errors.DivergedBranches, foo.sync_pipeline, 'remote/foo') def test_sync_pipeline_sets_push_location(self): self.build_tree(['local/', 'remote/']) tree = self.make_branch_and_tree('local/foo') foo = tree.branch self.assertIs(None, foo.get_push_location()) foo_manager = PipeManager(foo, tree) foo_manager.sync_pipeline('remote/foo') self.assertContainsRe(foo.get_push_location(), 'remote/foo/$') def test_sync_pipeline_retains_previous_push_location(self): self.build_tree(['local/', 'remote/']) tree = self.make_branch_and_tree('local/foo') foo = tree.branch foo.set_push_location('bar') foo_manager = PipeManager(foo, tree) foo_manager.sync_pipeline('remote/foo') self.assertEqual(foo.get_push_location(), 'bar') def test_sync_pipeline_preserves_revision_id_on_create(self): tree = self.make_branch_and_checkout('first') tree.commit('rev1') first_manager = PipeManager(tree.branch, tree) second_pipe = first_manager.storage.insert_pipe('second') first_manager.switch_to_pipe(second_pipe) tree = workingtree.WorkingTree.open('checkout') tree.commit('rev2', rev_id='rev2-id') os.mkdir('remote') first_manager.sync_pipeline('remote/first') remote_second = branch.Branch.open('remote/second') self.assertEqual('rev2-id', remote_second.last_revision()) def test_sync_pipeline_errors_on_bad_target(self): tree = self.make_branch_and_checkout('first') manager = PipeManager(tree.branch, tree) os.mkdir('remote') e = self.assertRaises(UnknownRemotePipe, manager.sync_pipeline, 'remote/random') self.assertEqual('Pipeline has no pipe named "random".', str(e)) def prepare_use_existing(self): os.mkdir('local') tree = self.make_branch_and_checkout('local/first') manager = PipeManager(tree.branch, tree) manager.storage.insert_pipe('second') os.mkdir('remote') remote_second = self.make_branch_and_tree('remote/second') return tree, manager, remote_second def test_sync_pipeline_uses_existing_remote_branches(self): tree, manager, remote_second = self.prepare_use_existing() manager.sync_pipeline('remote/first') def test_sync_pipeline_errors_if_remote_is_not_related(self): tree, manager, remote_second = self.prepare_use_existing() tree.commit('foo') tree.branch.push(manager.get_next_pipe()) remote_second.commit('bar') e = self.assertRaises(BranchInTheWay, manager.sync_pipeline, 'remote/first') self.assertContainsRe(str(e), 'Branch in the way at .*second') def test_sync_pipeline_errors_if_remote_in_pipeline(self): tree, manager, remote_second = self.prepare_use_existing() PipeStorage(remote_second.branch).insert_pipe('third') e = self.assertRaises(BranchInTheWay, manager.sync_pipeline, 'remote/first') self.assertContainsRe(str(e), 'Branch in the way at .*second') def test_sync_pipeline_pull_updates_checkout(self): os.mkdir('local') os.mkdir('remote') tree = self.make_branch_and_checkout('local/first') tree.commit('empty commit') remote_tree = self.make_branch_and_checkout('remote/first', 'remote_tree') remote_tree.pull(tree.branch) self.build_tree(['remote_tree/file']) remote_tree.add('file') remote_tree.commit('added file') manager = PipeManager(tree.branch, tree) manager.sync_pipeline('remote/first') self.assertPathExists('checkout/file') def test_sync_pipeline_uses_correct_revisions(self): os.mkdir('local') checkout = self.make_branch_and_checkout('local/first') first = checkout.branch first_rev = checkout.commit('first') first_manager = PipeManager.from_checkout(checkout) second = first_manager.storage.insert_pipe('second') first_manager.switch_to_pipe(second) checkout = reload_tree(checkout) second_rev = checkout.commit('second') os.mkdir('remote') PipeManager.from_checkout(checkout).sync_pipeline('remote/first') self.assertEqual(first_rev, branch.Branch.open('remote/first').last_revision()) self.assertEqual(second_rev, branch.Branch.open('remote/second').last_revision()) def test_merge_nick_order(self): """All nicks must appear once in the result. Order is preserved withinin non-matching sections. Order is based on the first item of a non-matching section.""" result = PipeManager._merge_nick_order(['a', 'b', 'd'], ['a', 'c', 'd']) self.assertEqual(['a', 'b', 'c', 'd'], result) result = PipeManager._merge_nick_order(['a', 'c', 'd'], ['a', 'b', 'd']) self.assertEqual(['a', 'b', 'c', 'd'], result) result = PipeManager._merge_nick_order(['a', 'b', 'z'], ['a', 'c', 'y']) self.assertEqual(['a', 'b', 'z', 'c', 'y'], result) def test_connections_to_create(self): connections = PipeManager._connections_to_create(['a', 'b', 'c'], []) self.assertEqual([('a', 'b'), ('b', 'c')], connections) connections = PipeManager._connections_to_create(['a', 'b', 'c'], ['a', 'b']) self.assertEqual([('b', 'c')], connections) connections = PipeManager._connections_to_create(['a', 'c'], ['a', 'b', 'c']) self.assertEqual([('a', 'c')], connections) def test_get_prev_revision_id_second(self): checkout, manager, second = self.make_checkout_first_second() second_manager = PipeManager(second, checkout) self.assertEqual('rev2-id', second_manager.get_prev_revision_id()) def test_get_prev_revision_id_first(self): tree = self.make_branch_and_tree('first') manager = PipeManager(tree.branch, tree) self.assertIs(None, manager.get_prev_revision_id()) def test_get_prev_revision_id_parent_lock(self): parent = self.make_branch_and_tree('parent') lca = parent.commit('common') child = parent.bzrdir.sprout('child').open_workingtree() parent.commit('parent 1') child.commit('child 1') manager = PipeManager(child.branch, child) self.assertEqual(lca, manager.get_prev_revision_id()) def prepare_related(self): parent = self.make_branch_and_tree('parent') parent.commit('parent', rev_id='parent-1') child = self.make_branch_and_tree('child') child.pull(parent.branch) child.lock_write() self.addCleanup(child.unlock) parent.commit('parent', rev_id='parent-2') child.commit('child', rev_id='child-1') return parent, child def test_get_prev_revision_id_parent(self): parent, child = self.prepare_related() child.branch.set_parent(parent.branch.base) manager = PipeManager(child.branch, child) self.assertEqual('parent-1', manager.get_prev_revision_id()) def test_get_prev_revision_id_submit(self): parent, child = self.prepare_related() child.branch.set_submit_branch(parent.branch.base) manager = PipeManager(child.branch, child) self.assertEqual('parent-1', manager.get_prev_revision_id()) def test_remove_pipe(self): checkout = self.make_branch_and_checkout('doomed') manager = PipeManager.from_checkout(checkout) target = manager.storage.insert_pipe('target') manager.remove_pipe(target, False) manager = PipeManager.from_checkout(reload_tree(checkout)) self.assertEqual(target.base, manager.checkout.branch.base) def test_remove_pipe_unshelves(self): checkout = self.make_branch_and_checkout('wip') checkout.commit('empty') self.build_tree(['checkout/new']) checkout.add('new') manager = PipeManager.from_checkout(checkout) doomed = manager.storage.insert_pipe('doomed') manager.switch_to_pipe(doomed) self.assertPathDoesNotExist('checkout/new') branch = checkout.branch manager = PipeManager.from_checkout(reload_tree(checkout)) manager.remove_pipe(branch, False) self.assertPathExists('checkout/new') def reload_tree(tree): return workingtree.WorkingTree.open(tree.bzrdir.root_transport.base) class TestGetDiff(tests.TestCaseWithMemoryTransport): def commit_file(self, branch, path, contents): basis = branch.basis_tree() tt = transform.TransformPreview(basis) if basis.get_root_id() is None: root = tt.new_directory('', transform.ROOT_PARENT, 'root-id') existing_id = basis.path2id(path) if existing_id is None: tt.new_file(path, root, contents, path + '-id') else: trans_id = tt.trans_id_file_id(existing_id) tt.delete_contents(trans_id) tt.create_file(contents, trans_id) tt.commit(branch, 'message') def make_writeable_branch(self, path): branch = self.make_branch(path) branch.lock_write() self.addCleanup(branch.unlock) return branch def make_branch_with_parent(self): parent = self.make_writeable_branch('parent') self.commit_file(parent, 'file', 'A\n') child_url = parent.bzrdir.root_transport.clone('../child').base child = parent.bzrdir.sprout(child_url).open_branch() self.commit_file(parent, 'file', 'B\n') child.lock_write() self.addCleanup(child.unlock) self.commit_file(child, 'file', 'C\n') return child def test_write_patch_no_parent(self): """If no parent is defined for the first pipe, return None.""" first_pipe = self.make_branch('first') checkout = first_pipe.create_checkout('first_checkout', lightweight=True) patch = PipeManager(first_pipe, checkout).get_patch() self.assertIs(None, patch) def test_write_patch_first_parent(self): """If a parent is defined for the first pipe, use that.""" first_pipe = self.make_branch_with_parent() checkout = first_pipe.create_checkout('checkout', lightweight=True) patch = PipeManager(first_pipe, checkout).get_patch() self.assertContainsRe(patch, '-A\n\+C') def test_write_patch_first_submit(self): """If a submit branch is defined, it overrides the parent.""" child = self.make_branch_with_parent() submit = self.make_writeable_branch('submit') submit.pull(child) child.set_submit_branch(submit.base) self.commit_file(child, 'file', 'D\n') checkout = child.create_checkout('child_checkout', lightweight=True) patch = PipeManager(child, checkout).get_patch() self.assertContainsRe(patch, '-C\n\+D') def test_write_patch_second(self): """The previous pipe, if defined, overrides the submit branch.""" submit = self.make_writeable_branch('submit') first = self.make_writeable_branch('first') self.commit_file(first, 'file', 'A\n') second = PipeStorage(first).insert_pipe('second') second.lock_write() self.addCleanup(second.unlock) second.set_submit_branch(submit.base) self.commit_file(second, 'file', 'B\n') checkout = second.create_checkout('second_checkout', lightweight=True) patch = PipeManager(second, checkout).get_patch() self.assertContainsRe(patch, '-A\n\+B') class TestPipeDirectory(TestCaseWithPipes): def test_pipe_directories(self): foo = self.make_branch('foo') bar = self.make_branch('bar') baz = self.make_branch('baz') self.connect_many(foo, bar, baz) os.chdir('bar') self.assertEqual(baz.base, directory_service.directories.dereference(':next')) self.assertEqual(foo.base, directory_service.directories.dereference(':prev')) self.assertEqual(baz.base, directory_service.directories.dereference(':pipe:baz')) self.assertEqual(foo.base, directory_service.directories.dereference(':first')) self.assertEqual(baz.base, directory_service.directories.dereference(':last')) class TestDwimPipe(TestCaseWithPipes): def test_dwim_pipe_no_location(self): checkout = self.make_branch_and_checkout('test') manager = PipeManager(checkout.branch, checkout) self.assertRaises(NoSuchPipe, dwim_pipe, manager, None) class TestTreeToPipeline(TestCaseWithPipes): def test_tree_to_pipeline(self): tree = self.make_branch_and_tree('foo') tree.branch.nick = 'bar' tree.commit('rev1', rev_id='rev1-id') tree_to_pipeline(tree) b = branch.Branch.open('foo/.bzr/branches/bar') self.assertEqual('rev1-id', b.last_revision()) self.assertRaises(errors.NoWorkingTree, workingtree.WorkingTree.open, 'foo/.bzr/branches/bar') self.assertTrue(b.repository.is_shared()) self.assertEqual(b.base, branch.Branch.open('foo').base) self.assertFalse(tree.is_ignored('pipes')) def test_tree_to_pipeline_lightweight_checkout(self): pipe_branch = self.make_branch('branch') pipe_checkout = pipe_branch.create_checkout('checkout', lightweight=True) self.assertRaises(errors.AlreadyLightweightCheckout, tree_to_pipeline, pipe_checkout) def test_tree_to_pipeline_reuse_shared_repo(self): repo = self.make_repository('repo', shared=True) branch = bzrdir.BzrDir.create_branch_convenience('repo/tree') tree = branch.bzrdir.open_workingtree() checkout = tree_to_pipeline(tree) self.assertEqual(repo.bzrdir.root_transport.base, checkout.branch.repository.bzrdir.root_transport.base)