bzr-upload-1.1.0/0000775000175000017500000000000011730425762013511 5ustar vilavila00000000000000bzr-upload-1.1.0/info.py0000664000175000017500000000216711730422734015020 0ustar vilavila00000000000000#!/usr/bin/env python # Copyright (C) 2008-2010 by Canonical Ltd # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA bzr_plugin_name = 'upload' # versions ending in 'dev' mean development version # versions ending in 'final' mean release (well tested, etc) bzr_plugin_version = (1, 1, 0, 'final', 0) bzr_commands = ['upload',] bzr_compatible_versions = [(2, x, 0) for x in [2, 3, 4, 5]] bzr_minimum_version = bzr_compatible_versions[0] bzr_maximum_version = bzr_compatible_versions[-1] bzr-upload-1.1.0/COPYING0000664000175000017500000004310311672125103014534 0ustar vilavila00000000000000 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-upload-1.1.0/README0000664000175000017500000000103311672125103014355 0ustar vilavila00000000000000Bazaar Upload ============= Overview -------- bzr-upload is a plugin for Bazaar which lets you upload your working tree to a remote location using ftp/sftp. The main target audience is web developers who keep their web pages version controlled with bzr. Please report any bugs to: http://bugs.launchpad.net/bzr-upload/ Our home page is located at: https://launchpad.net/bzr-upload/ The original authors are: Vincent Ladeuil Martin Albisetti Use bzr help plugins/upload to get more info. bzr-upload-1.1.0/PKG-INFO0000664000175000017500000000145011730425762014606 0ustar vilavila00000000000000Metadata-Version: 1.1 Name: bzr-upload Version: 1.1.0 Summary: Incrementally uploads changes to a dumb server Home-page: http://launchpad.net/bzr-upload/1.1/1.1.0/bzr-upload-1.1.0.tar.gz Author: Vincent Ladeuil, Martin Albisetti Author-email: v.ladeuil@canonical.com License: GPL Download-URL: http://launchpad.net/bzr-upload Description: Web sites are often hosted on servers where bzr can't be installed. In other cases, the web site must not give access to its corresponding branch (for security reasons for example). Finally, web hosting providers often provides only ftp access to upload sites. This plugin uploads only the relevant changes since the last upload using ftp or sftp protocols. Keywords: plugin bzr upload dumb protocol Platform: UNKNOWN bzr-upload-1.1.0/setup.py0000775000175000017500000000407611672125103015224 0ustar vilavila00000000000000#!/usr/bin/env python # Copyright (C) 2008-2010 by Canonical Ltd # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from info import * if __name__ == '__main__': from distutils.core import setup series = bzr_plugin_version[:2] series_string = ".".join([str(x) for x in series]) version = bzr_plugin_version[:3] version_string = ".".join([str(x) for x in version]) setup(name='bzr-upload', description='Incrementally uploads changes to a dumb server', keywords='plugin bzr upload dumb protocol', version=version_string, url='http://launchpad.net/bzr-upload/%s/%s/bzr-upload-%s.tar.gz' % ( series_string, version_string, version_string), download_url='http://launchpad.net/bzr-upload', author='Vincent Ladeuil, Martin Albisetti', author_email='v.ladeuil@canonical.com', license='GPL', long_description="""\ Web sites are often hosted on servers where bzr can't be installed. In other cases, the web site must not give access to its corresponding branch (for security reasons for example). Finally, web hosting providers often provides only ftp access to upload sites. This plugin uploads only the relevant changes since the last upload using ftp or sftp protocols. """, package_dir={'bzrlib.plugins.upload':'.'}, packages=['bzrlib.plugins.upload', 'bzrlib.plugins.upload.tests'] ) bzr-upload-1.1.0/cmds.py0000664000175000017500000005151011730422677015015 0ustar vilavila00000000000000# Copyright (C) 2011, 2012 Canonical Ltd # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """bzr-upload command implementations.""" from bzrlib import ( branch, commands, lazy_import, option, ) lazy_import.lazy_import(globals(), """ import stat import sys from bzrlib import ( bzrdir, errors, globbing, ignores, osutils, revision, revisionspec, trace, transport, urlutils, workingtree, ) """) def _get_branch_option(branch, option): return branch.get_config().get_user_option(option) # FIXME: Get rid of that as soon as we depend on a bzr API that includes # get_user_option_as_bool def _get_branch_bool_option(branch, option): conf = branch.get_config() if hasattr(conf, 'get_user_option_as_bool'): value = conf.get_user_option_as_bool(option) else: value = conf.get_user_option(option) if value is not None: if value.lower().strip() == 'true': value = True else: value = False return value def _set_branch_option(branch, option, value): branch.get_config().set_user_option(option, value) def get_upload_location(branch): return _get_branch_option(branch, 'upload_location') def set_upload_location(branch, location): _set_branch_option(branch, 'upload_location', location) # FIXME: Add more tests around invalid paths used here or relative paths that # doesn't exist on remote (if only to get proper error messages) def get_upload_revid_location(branch): loc = _get_branch_option(branch, 'upload_revid_location') if loc is None: loc = '.bzr-upload.revid' return loc def set_upload_revid_location(branch, location): _set_branch_option(branch, 'upload_revid_location', location) def get_upload_auto(branch): auto = _get_branch_bool_option(branch, 'upload_auto') if auto is None: auto = False # Default to False if not specified return auto def set_upload_auto(branch, auto): # FIXME: What's the point in allowing a boolean here instead of requiring # the callers to use strings instead ? if auto: auto_str = "True" else: auto_str = "False" _set_branch_option(branch, 'upload_auto', auto_str) def get_upload_auto_quiet(branch): quiet = _get_branch_bool_option(branch, 'upload_auto_quiet') if quiet is None: quiet = False # Default to False if not specified return quiet def set_upload_auto_quiet(branch, quiet): _set_branch_option(branch, 'upload_auto_quiet', quiet) class BzrUploader(object): def __init__(self, branch, to_transport, outf, tree, rev_id, quiet=False): self.branch = branch self.to_transport = to_transport self.outf = outf self.tree = tree self.rev_id = rev_id self.quiet = quiet self._pending_deletions = [] self._pending_renames = [] self._uploaded_revid = None self._ignored = None def _up_stat(self, relpath): return self.to_transport.stat(urlutils.escape(relpath)) def _up_rename(self, old_path, new_path): return self.to_transport.rename(urlutils.escape(old_path), urlutils.escape(new_path)) def _up_delete(self, relpath): return self.to_transport.delete(urlutils.escape(relpath)) def _up_delete_tree(self, relpath): return self.to_transport.delete_tree(urlutils.escape(relpath)) def _up_mkdir(self, relpath, mode): return self.to_transport.mkdir(urlutils.escape(relpath), mode) def _up_rmdir(self, relpath): return self.to_transport.rmdir(urlutils.escape(relpath)) def _up_put_bytes(self, relpath, bytes, mode): self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode) def _up_get_bytes(self, relpath): return self.to_transport.get_bytes(urlutils.escape(relpath)) def set_uploaded_revid(self, rev_id): # XXX: Add tests for concurrent updates, etc. revid_path = get_upload_revid_location(self.branch) self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id) self._uploaded_revid = rev_id def get_uploaded_revid(self): if self._uploaded_revid is None: revid_path = get_upload_revid_location(self.branch) try: self._uploaded_revid = self._up_get_bytes(revid_path) except errors.NoSuchFile: # We have not upload to here. self._uploaded_revid = revision.NULL_REVISION return self._uploaded_revid def _get_ignored(self): if self._ignored is None: try: ignore_file_path = '.bzrignore-upload' ignore_file_id = self.tree.path2id(ignore_file_path) ignore_file = self.tree.get_file(ignore_file_id, ignore_file_path) ignored_patterns = ignores.parse_ignore_file(ignore_file) except errors.NoSuchId: ignored_patterns = [] self._ignored = globbing.Globster(ignored_patterns) return self._ignored def is_ignored(self, relpath): glob = self._get_ignored() ignored = glob.match(relpath) import os if not ignored: # We still need to check that all parents are not ignored dir = os.path.dirname(relpath) while dir and not ignored: ignored = glob.match(dir) if not ignored: dir = os.path.dirname(dir) return ignored def upload_file(self, relpath, id, mode=None): if mode is None: if self.tree.is_executable(id): mode = 0775 else: mode = 0664 if not self.quiet: self.outf.write('Uploading %s\n' % relpath) self._up_put_bytes(relpath, self.tree.get_file_text(id), mode) def upload_file_robustly(self, relpath, id, mode=None): """Upload a file, clearing the way on the remote side. When doing a full upload, it may happen that a directory exists where we want to put our file. """ try: st = self._up_stat(relpath) if stat.S_ISDIR(st.st_mode): # A simple rmdir may not be enough if not self.quiet: self.outf.write('Clearing %s/%s\n' % ( self.to_transport.external_url(), relpath)) self._up_delete_tree(relpath) except errors.PathError: pass self.upload_file(relpath, id, mode) def make_remote_dir(self, relpath, mode=None): if mode is None: mode = 0775 self._up_mkdir(relpath, mode) def make_remote_dir_robustly(self, relpath, mode=None): """Create a remote directory, clearing the way on the remote side. When doing a full upload, it may happen that a file exists where we want to create our directory. """ try: st = self._up_stat(relpath) if not stat.S_ISDIR(st.st_mode): if not self.quiet: self.outf.write('Deleting %s/%s\n' % ( self.to_transport.external_url(), relpath)) self._up_delete(relpath) else: # Ok the remote dir already exists, nothing to do return except errors.PathError: pass self.make_remote_dir(relpath, mode) def delete_remote_file(self, relpath): if not self.quiet: self.outf.write('Deleting %s\n' % relpath) self._up_delete(relpath) def delete_remote_dir(self, relpath): if not self.quiet: self.outf.write('Deleting %s\n' % relpath) self._up_rmdir(relpath) # XXX: Add a test where a subdir is ignored but we still want to # delete the dir -- vila 100106 def delete_remote_dir_maybe(self, relpath): """Try to delete relpath, keeping failures to retry later.""" try: self._up_rmdir(relpath) # any kind of PathError would be OK, though we normally expect # DirectoryNotEmpty except errors.PathError: self._pending_deletions.append(relpath) def finish_deletions(self): if self._pending_deletions: # Process the previously failed deletions in reverse order to # delete children before parents for relpath in reversed(self._pending_deletions): self._up_rmdir(relpath) # The following shouldn't be needed since we use it once per # upload, but better safe than sorry ;-) self._pending_deletions = [] def rename_remote(self, old_relpath, new_relpath): """Rename a remote file or directory taking care of collisions. To avoid collisions during bulk renames, each renamed target is temporarily assigned a unique name. When all renames have been done, each target get its proper name. """ # We generate a sufficiently random name to *assume* that # no collisions will occur and don't worry about it (nor # handle it). import os import random import time stamp = '.tmp.%.9f.%d.%d' % (time.time(), os.getpid(), random.randint(0,0x7FFFFFFF)) if not self.quiet: self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath)) self._up_rename(old_relpath, stamp) self._pending_renames.append((stamp, new_relpath)) def finish_renames(self): for (stamp, new_path) in self._pending_renames: self._up_rename(stamp, new_path) # The following shouldn't be needed since we use it once per upload, # but better safe than sorry ;-) self._pending_renames = [] def upload_full_tree(self): self.to_transport.ensure_base() # XXX: Handle errors (add # --create-prefix option ?) self.tree.lock_read() try: for relpath, ie in self.tree.iter_entries_by_dir(): if relpath in ('', '.bzrignore', '.bzrignore-upload'): # skip root ('') # .bzrignore and .bzrignore-upload have no meaning outside # a working tree so do not upload them continue if self.is_ignored(relpath): if not self.quiet: self.outf.write('Ignoring %s\n' % relpath) continue if ie.kind == 'file': self.upload_file_robustly(relpath, ie.file_id) elif ie.kind == 'directory': self.make_remote_dir_robustly(relpath) elif ie.kind == 'symlink': if not self.quiet: target = self.tree.path_content_summary(relpath)[3] self.outf.write('Not uploading symlink %s -> %s\n' % (relpath, target)) else: raise NotImplementedError self.set_uploaded_revid(self.rev_id) finally: self.tree.unlock() def upload_tree(self): # If we can't find the revid file on the remote location, upload the # full tree instead rev_id = self.get_uploaded_revid() if rev_id == revision.NULL_REVISION: if not self.quiet: self.outf.write('No uploaded revision id found,' ' switching to full upload\n') self.upload_full_tree() # We're done return # Check if the revision hasn't already been uploaded if rev_id == self.rev_id: if not self.quiet: self.outf.write('Remote location already up to date\n') from_tree = self.branch.repository.revision_tree(rev_id) self.to_transport.ensure_base() # XXX: Handle errors (add # --create-prefix option ?) changes = self.tree.changes_from(from_tree) self.tree.lock_read() try: for (path, id, kind) in changes.removed: if self.is_ignored(path): if not self.quiet: self.outf.write('Ignoring %s\n' % path) continue if kind is 'file': self.delete_remote_file(path) elif kind is 'directory': self.delete_remote_dir_maybe(path) elif kind == 'symlink': if not self.quiet: target = self.tree.path_content_summary(path)[3] self.outf.write('Not deleting remote symlink %s -> %s\n' % (path, target)) else: raise NotImplementedError for (old_path, new_path, id, kind, content_change, exec_change) in changes.renamed: if self.is_ignored(old_path) and self.is_ignored(new_path): if not self.quiet: self.outf.write('Ignoring %s\n' % old_path) self.outf.write('Ignoring %s\n' % new_path) continue if content_change: # We update the old_path content because renames and # deletions are differed. self.upload_file(old_path, id) if kind == 'symlink': if not self.quiet: self.outf.write('Not renaming remote symlink %s to %s\n' % (old_path, new_path)) else: self.rename_remote(old_path, new_path) self.finish_renames() self.finish_deletions() for (path, id, old_kind, new_kind) in changes.kind_changed: if self.is_ignored(path): if not self.quiet: self.outf.write('Ignoring %s\n' % path) continue if old_kind == 'file': self.delete_remote_file(path) elif old_kind == 'directory': self.delete_remote_dir(path) else: raise NotImplementedError if new_kind == 'file': self.upload_file(path, id) elif new_kind is 'directory': self.make_remote_dir(path) else: raise NotImplementedError for (path, id, kind) in changes.added: if self.is_ignored(path): if not self.quiet: self.outf.write('Ignoring %s\n' % path) continue if kind == 'file': self.upload_file(path, id) elif kind == 'directory': self.make_remote_dir(path) elif kind == 'symlink': if not self.quiet: target = self.tree.path_content_summary(path)[3] self.outf.write('Not uploading symlink %s -> %s\n' % (path, target)) else: raise NotImplementedError # XXX: Add a test for exec_change for (path, id, kind, content_change, exec_change) in changes.modified: if self.is_ignored(path): if not self.quiet: self.outf.write('Ignoring %s\n' % path) continue if kind is 'file': self.upload_file(path, id) else: raise NotImplementedError self.set_uploaded_revid(self.rev_id) finally: self.tree.unlock() class CannotUploadToWorkingTree(errors.BzrCommandError): _fmt = 'Cannot upload to a bzr managed working tree: %(url)s".' class DivergedUploadedTree(errors.BzrCommandError): _fmt = ("Your branch (%(revid)s)" " and the uploaded tree (%(uploaded_revid)s) have diverged: ") class cmd_upload(commands.Command): """Upload a working tree, as a whole or incrementally. If no destination is specified use the last one used. If no revision is specified upload the changes since the last upload. Changes include files added, renamed, modified or removed. """ _see_also = ['plugins/upload'] takes_args = ['location?'] takes_options = [ 'revision', 'remember', 'overwrite', option.Option('full', 'Upload the full working tree.'), option.Option('quiet', 'Do not output what is being done.', short_name='q'), option.Option('directory', help='Branch to upload from, ' 'rather than the one containing the working directory.', short_name='d', type=unicode, ), option.Option('auto', 'Trigger an upload from this branch whenever the tip ' 'revision changes.') ] def run(self, location=None, full=False, revision=None, remember=None, directory=None, quiet=False, auto=None, overwrite=False ): if directory is None: directory = u'.' (wt, branch, relpath) = bzrdir.BzrDir.open_containing_tree_or_branch(directory) if wt: wt.lock_read() locked = wt else: branch.lock_read() locked = branch try: if wt: changes = wt.changes_from(wt.basis_tree()) if revision is None and changes.has_changed(): raise errors.UncommittedChanges(wt) if location is None: stored_loc = get_upload_location(branch) if stored_loc is None: raise errors.BzrCommandError( 'No upload location known or specified.') else: # FIXME: Not currently tested display_url = urlutils.unescape_for_display(stored_loc, self.outf.encoding) self.outf.write("Using saved location: %s\n" % display_url) location = stored_loc to_transport = transport.get_transport(location) # Check that we are not uploading to a existing working tree. try: to_bzr_dir = bzrdir.BzrDir.open_from_transport(to_transport) has_wt = to_bzr_dir.has_workingtree() except errors.NotBranchError: has_wt = False except errors.NotLocalUrl: # The exception raised is a bit weird... but that's life. has_wt = True if has_wt: raise CannotUploadToWorkingTree(url=location) if revision is None: rev_id = branch.last_revision() else: if len(revision) != 1: raise errors.BzrCommandError( 'bzr upload --revision takes exactly 1 argument') rev_id = revision[0].in_history(branch).rev_id tree = branch.repository.revision_tree(rev_id) uploader = BzrUploader(branch, to_transport, self.outf, tree, rev_id, quiet=quiet) if not overwrite: prev_uploaded_rev_id = uploader.get_uploaded_revid() graph = branch.repository.get_graph() if not graph.is_ancestor(prev_uploaded_rev_id, rev_id): raise DivergedUploadedTree( revid=rev_id, uploaded_revid=prev_uploaded_rev_id) if full: uploader.upload_full_tree() else: uploader.upload_tree() finally: locked.unlock() # We uploaded successfully, remember it if get_upload_location(branch) is None or remember: set_upload_location(branch, urlutils.unescape(to_transport.base)) if auto is not None: set_upload_auto(branch, auto) bzr-upload-1.1.0/__init__.py0000664000175000017500000001702211672125103015613 0ustar vilavila00000000000000# Copyright (C) 2008, 2009, 2010 Canonical Ltd # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Upload a working tree, incrementally. Quickstart ---------- To get started, it's as simple as running:: bzr upload sftp://user@host/location/on/webserver This will initially upload the whole working tree, and leave a file on the remote location indicating the last revision that was uploaded (.bzr-upload.revid), in order to avoid uploading unnecessary information the next time. If you would like to upload a specific revision, you just do: bzr upload -r X sftp://user@host/location/on/webserver bzr-upload, just as bzr does, will remember the location where you upload the first time, so you don't need to specify it every time. If you need to re-upload the whole working tree for some reason, you can: bzr upload --full sftp://user@host/location/on/webserver This command only works on the revision beening uploaded is a decendent of the revision that was previously uploaded, and that they are hence from branches that have not diverged. Branches are considered diverged if the destination branch's most recent commit is one that has not been merged (directly or indirectly) by the source branch. If branches have diverged, you can use 'bzr upload --overwrite' to replace the other branch completely, discarding its unmerged changes. Automatically Uploading ----------------------- bzr-upload comes with a hook that can be used to trigger an upload whenever the tip of the branch changes, including on commit, push, uncommit etc. This would allow you to keep the code on the target up to date automatically. The easiest way to enable this is to run upload with the --auto option. bzr upload --auto will enable the hook for this branch. If you were to do a commit in this branch now you would see it trigger the upload automatically. If you wish to disable this for a branch again then you can use the --no-auto option. bzr upload --no-auto will disable the feature for that branch. Since the auto hook is triggered automatically, you can't use the --quiet option available for the upload command. Instead, you can set the 'upload_auto_quiet' configuration variable to True or False in either bazaar.conf, locations.conf or branch.conf. Storing the '.bzr-upload.revid' file ------------------------------------ The only bzr-related info uploaded with the working tree is the corresponding revision id. The uploaded working tree is not linked to any other bzr data. If the layout of your remote server is such that you can't write in the root directory but only in the directories inside that root, you will need to use the 'upload_revid_location' configuration variable to specify the relative path to be used. That configuration variable can be specified in locations.conf or branch.conf. For example, given the following layout: Project/ private/ public/ you may have write access in 'private' and 'public' but in 'Project' itself. In that case, you can add the following in your locations.conf or branch.conf file: upload_revid_location = private/.bzr-upload.revid Upload from Remote Location --------------------------- It is possible to upload to a remote location from another remote location by specifying it with the --directory option: bzr upload ftp://public.example.com --directory sftp://private.example.com This, together with --auto, can be used to upload when you push to your central branch, rather than when you commit to your local branch. Note that you will consume more bandwith this way than uploading from a local branch. Ignoring certain files ----------------------- If you want to version a file, but not upload it, you can create a file called .bzrignore-upload, which works in the same way as the regular .bzrignore file, but only applies to bzr-upload. Collaborating ------------- While we don't have any platform setup, you can branch from trunk: bzr branch lp:bzr-upload And change anything you'd like, and file a merge proposal on Launchpad. Known Issues ------------ * Symlinks are not supported (warnings are emitted when they are encountered). """ # TODO: the chmod bits *can* be supported via the upload protocols # (i.e. poorly), but since the web developers use these protocols to upload # manually, it is expected that the associated web server is coherent with # their presence/absence. In other words, if a web hosting provider requires # chmod bits but don't provide an ftp server that support them, well, better # find another provider ;-) # TODO: The message emitted in verbose mode displays local paths. That may be # scary for the user when we say 'Deleting ' and are referring to # remote files... import bzrlib import bzrlib.api from bzrlib import ( branch, ) from info import ( bzr_plugin_version as version_info, bzr_compatible_versions, ) if version_info[3] == 'final': version_string = '%d.%d.%d' % version_info[:3] else: version_string = '%d.%d.%d%s%d' % version_info __version__ = version_string bzrlib.api.require_any_api(bzrlib, bzr_compatible_versions) from bzrlib.commands import plugin_cmds plugin_cmds.register_lazy('cmd_upload', [], 'bzrlib.plugins.upload.cmds') def auto_upload_hook(params): from bzrlib import ( osutils, trace, transport, urlutils, ) from bzrlib.plugins.upload.cmds import ( BzrUploader, get_upload_location, get_upload_auto, get_upload_auto_quiet, ) import sys source_branch = params.branch destination = get_upload_location(source_branch) if destination is None: return auto_upload = get_upload_auto(source_branch) if not auto_upload: return quiet = get_upload_auto_quiet(source_branch) if not quiet: display_url = urlutils.unescape_for_display( destination, osutils.get_terminal_encoding()) trace.note('Automatically uploading to %s', display_url) to_transport = transport.get_transport(destination) last_revision = source_branch.last_revision() last_tree = source_branch.repository.revision_tree(last_revision) uploader = BzrUploader(source_branch, to_transport, sys.stdout, last_tree, last_revision, quiet=quiet) uploader.upload_tree() def install_auto_upload_hook(): branch.Branch.hooks.install_named_hook('post_change_branch_tip', auto_upload_hook, 'Auto upload code from a branch when it is changed.') install_auto_upload_hook() def load_tests(basic_tests, module, loader): # This module shouldn't define any tests but I don't know how to report # that. I prefer to update basic_tests with the other tests to detect # unwanted tests and I think that's sufficient. testmod_names = [ 'tests', ] basic_tests.addTest(loader.loadTestsFromModuleNames( ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) return basic_tests bzr-upload-1.1.0/tests/0000775000175000017500000000000011730425762014653 5ustar vilavila00000000000000bzr-upload-1.1.0/tests/test_auto_upload_hook.py0000664000175000017500000000524411672125103021614 0ustar vilavila00000000000000# Copyright (C) 2008, 2009, 2011 Canonical Ltd # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os from bzrlib import ( tests, ) from bzrlib.plugins import ( upload, ) from bzrlib.plugins.upload import ( cmds, ) class AutoPushHookTests(tests.TestCaseWithTransport): def setUp(self): super(AutoPushHookTests, self).setUp() upload.install_auto_upload_hook() def make_start_branch(self): self.wt = self.make_branch_and_tree('.') self.build_tree(['a']) self.wt.add(['a']) self.wt.commit("one") class AutoPushWithLocation(AutoPushHookTests): def setUp(self): super(AutoPushWithLocation, self).setUp() self.make_start_branch() cmds.set_upload_auto(self.wt.branch, True) cmds.set_upload_location(self.wt.branch, self.get_url('target')) cmds.set_upload_auto_quiet(self.wt.branch, 'True') def test_auto_push_on_commit(self): self.assertPathDoesNotExist('target') self.build_tree(['b']) self.wt.add(['b']) self.wt.commit("two") self.assertPathExists('target') self.assertPathExists(os.path.join('target', 'a')) self.assertPathExists(os.path.join('target', 'b')) def test_disable_auto_push(self): self.assertPathDoesNotExist('target') self.build_tree(['b']) self.wt.add(['b']) self.wt.commit("two") cmds.set_upload_auto(self.wt.branch, False) self.build_tree(['c']) self.wt.add(['c']) self.wt.commit("three") self.assertPathDoesNotExist(os.path.join('target', 'c')) class AutoPushWithoutLocation(AutoPushHookTests): def setUp(self): super(AutoPushWithoutLocation, self).setUp() self.make_start_branch() cmds.set_upload_auto(self.wt.branch, True) def test_dont_push_if_no_location(self): self.assertPathDoesNotExist('target') self.build_tree(['b']) self.wt.add(['b']) self.wt.commit("two") self.assertPathDoesNotExist('target') bzr-upload-1.1.0/tests/test_upload.py0000664000175000017500000007017111672125103017545 0ustar vilavila00000000000000# Copyright (C) 2008-2011 Canonical Ltd # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import sys from bzrlib import ( bzrdir, config, errors, osutils, revisionspec, tests, transport, workingtree, uncommit, ) from bzrlib.tests import ( features, per_branch, per_transport, stub_sftp, ) from bzrlib.transport import ( ftp, sftp, ) from bzrlib.plugins.upload import ( cmds, ) def get_transport_scenarios(): result = [] basis = per_transport.transport_test_permutations() # Keep only the interesting ones for upload for name, d in basis: t_class = d['transport_class'] if t_class in (ftp.FtpTransport, sftp.SFTPTransport): result.append((name, d)) try: import bzrlib.plugins.local_test_server from bzrlib.plugins.local_test_server import test_server if False: # XXX: Disable since we can't get chmod working for anonymous # user scenario = ('vsftpd', {'transport_class': test_server.FtpTransport, 'transport_server': test_server.Vsftpd, }) result.append(scenario) from test_server import ProftpdFeature if ProftpdFeature().available(): scenario = ('proftpd', {'transport_class': test_server.FtpTransport, 'transport_server': test_server.Proftpd, }) result.append(scenario) # XXX: add support for pyftpdlib except ImportError: pass return result def load_tests(standard_tests, module, loader): """Multiply tests for tranport implementations.""" result = loader.suiteClass() # one for each transport implementation t_tests, remaining_tests = tests.split_suite_by_condition( standard_tests, tests.condition_isinstance(( TestFullUpload, TestIncrementalUpload, TestUploadFromRemoteBranch, ))) tests.multiply_tests(t_tests, get_transport_scenarios(), result) # one for each branch format b_tests, remaining_tests = tests.split_suite_by_condition( remaining_tests, tests.condition_isinstance(( TestBranchUploadLocations, ))) tests.multiply_tests(b_tests, per_branch.branch_scenarios(), result) # No parametrization for the remaining tests result.addTests(remaining_tests) return result class UploadUtilsMixin(object): """Helper class to write upload tests. This class provides helpers to simplify test writing. The emphasis is on easy test writing, so each tree modification is committed. This doesn't preclude writing tests spawning several revisions to upload more complex changes. """ upload_dir = 'upload' branch_dir = 'branch' def make_branch_and_working_tree(self): t = transport.get_transport(self.branch_dir) t.ensure_base() branch = bzrdir.BzrDir.create_branch_convenience( t.base, format=bzrdir.format_registry.make_bzrdir('default'), force_new_tree=False) self.tree = branch.bzrdir.create_workingtree() self.tree.commit('initial empty tree') def assertUpFileEqual(self, content, path, base=upload_dir): self.assertFileEqual(content, osutils.pathjoin(base, path)) def assertUpPathModeEqual(self, path, expected_mode, base=upload_dir): # FIXME: the tests needing that assertion should depend on the server # ability to handle chmod so that they don't fail (or be skipped) # against servers that can't. Note that some bzrlib transports define # _can_roundtrip_unix_modebits in a incomplete way, this property # should depend on both the client and the server, not the client only. # But the client will know or can find if the server support chmod so # that's the client that will report it anyway. full_path = osutils.pathjoin(base, path) st = os.stat(full_path) mode = st.st_mode & 0777 if expected_mode == mode: return raise AssertionError( 'For path %s, mode is %s not %s' % (full_path, oct(mode), oct(expected_mode))) def assertUpPathDoesNotExist(self, path, base=upload_dir): self.assertPathDoesNotExist(osutils.pathjoin(base, path)) def assertUpPathExists(self, path, base=upload_dir): self.assertPathExists(osutils.pathjoin(base, path)) def set_file_content(self, path, content, base=branch_dir): f = file(osutils.pathjoin(base, path), 'wb') try: f.write(content) finally: f.close() def add_file(self, path, content, base=branch_dir): self.set_file_content(path, content, base) self.tree.add(path) self.tree.commit('add file %s' % path) def modify_file(self, path, content, base=branch_dir): self.set_file_content(path, content, base) self.tree.commit('modify file %s' % path) def chmod_file(self, path, mode, base=branch_dir): full_path = osutils.pathjoin(base, path) os.chmod(full_path, mode) self.tree.commit('change file %s mode to %s' % (path, oct(mode))) def delete_any(self, path, base=branch_dir): self.tree.remove([path], keep_files=False) self.tree.commit('delete %s' % path) def add_dir(self, path, base=branch_dir): os.mkdir(osutils.pathjoin(base, path)) self.tree.add(path) self.tree.commit('add directory %s' % path) def rename_any(self, old_path, new_path): self.tree.rename_one(old_path, new_path) self.tree.commit('rename %s into %s' % (old_path, new_path)) def transform_dir_into_file(self, path, content, base=branch_dir): osutils.delete_any(osutils.pathjoin(base, path)) self.set_file_content(path, content, base) self.tree.commit('change %s from dir to file' % path) def transform_file_into_dir(self, path, base=branch_dir): # bzr can't handle that kind change in a single commit without an # intervening bzr status (see bug #205636). self.tree.remove([path], keep_files=False) os.mkdir(osutils.pathjoin(base, path)) self.tree.add(path) self.tree.commit('change %s from file to dir' % path) def add_symlink(self, path, target, base=branch_dir): self.requireFeature(features.SymlinkFeature) os.symlink(target, osutils.pathjoin(base, path)) self.tree.add(path) self.tree.commit('add symlink %s -> %s' % (path, target)) def modify_symlink(self, path, target, base=branch_dir): self.requireFeature(features.SymlinkFeature) full_path = osutils.pathjoin(base, path) os.unlink(full_path) os.symlink(target, full_path) self.tree.commit('modify symlink %s -> %s' % (path, target)) def _get_cmd_upload(self): cmd = cmds.cmd_upload() # We don't want to use run_bzr here because redirected output are a # pain to debug. But we need to provides a valid outf. # XXX: Should a bug against bzr be filled about that ? # Short story: we don't expect any output so we may just use stdout cmd.outf = sys.stdout return cmd def do_full_upload(self, *args, **kwargs): upload = self._get_cmd_upload() up_url = self.get_url(self.upload_dir) if kwargs.get('directory', None) is None: kwargs['directory'] = self.branch_dir kwargs['full'] = True kwargs['quiet'] = True upload.run(up_url, *args, **kwargs) def do_incremental_upload(self, *args, **kwargs): upload = self._get_cmd_upload() up_url = self.get_url(self.upload_dir) if kwargs.get('directory', None) is None: kwargs['directory'] = self.branch_dir kwargs['quiet'] = True upload.run(up_url, *args, **kwargs) class TestUploadMixin(UploadUtilsMixin): """Helper class to share tests between full and incremental uploads.""" def _test_create_file(self, file_name): self.make_branch_and_working_tree() self.do_full_upload() self.add_file(file_name, 'foo') self.do_upload() self.assertUpFileEqual('foo', file_name) def test_create_file(self): self._test_create_file('hello') def test_unicode_create_file(self): self.requireFeature(features.UnicodeFilenameFeature) self._test_create_file(u'hell\u00d8') def _test_create_file_in_dir(self, dir_name, file_name): self.make_branch_and_working_tree() self.do_full_upload() self.add_dir(dir_name) fpath = '%s/%s' % (dir_name, file_name) self.add_file(fpath, 'baz') self.assertUpPathDoesNotExist(fpath) self.do_upload() self.assertUpFileEqual('baz', fpath) self.assertUpPathModeEqual(dir_name, 0775) def test_create_file_in_dir(self): self._test_create_file_in_dir('dir', 'goodbye') def test_unicode_create_file_in_dir(self): self.requireFeature(features.UnicodeFilenameFeature) self._test_create_file_in_dir(u'dir\u00d8', u'goodbye\u00d8') def test_modify_file(self): self.make_branch_and_working_tree() self.add_file('hello', 'foo') self.do_full_upload() self.modify_file('hello', 'bar') self.assertUpFileEqual('foo', 'hello') self.do_upload() self.assertUpFileEqual('bar', 'hello') def _test_rename_one_file(self, old_name, new_name): self.make_branch_and_working_tree() self.add_file(old_name, 'foo') self.do_full_upload() self.rename_any(old_name, new_name) self.assertUpFileEqual('foo', old_name) self.do_upload() self.assertUpFileEqual('foo', new_name) def test_rename_one_file(self): self._test_rename_one_file('hello', 'goodbye') def test_unicode_rename_one_file(self): self.requireFeature(features.UnicodeFilenameFeature) self._test_rename_one_file(u'hello\u00d8', u'goodbye\u00d8') def test_rename_and_change_file(self): self.make_branch_and_working_tree() self.add_file('hello', 'foo') self.do_full_upload() self.rename_any('hello', 'goodbye') self.modify_file('goodbye', 'bar') self.assertUpFileEqual('foo', 'hello') self.do_upload() self.assertUpFileEqual('bar', 'goodbye') def test_rename_two_files(self): self.make_branch_and_working_tree() self.add_file('a', 'foo') self.add_file('b', 'qux') self.do_full_upload() # We rely on the assumption that bzr will topologically sort the # renames which will cause a -> b to appear *before* b -> c self.rename_any('b', 'c') self.rename_any('a', 'b') self.assertUpFileEqual('foo', 'a') self.assertUpFileEqual('qux', 'b') self.do_upload() self.assertUpFileEqual('foo', 'b') self.assertUpFileEqual('qux', 'c') def test_upload_revision(self): self.make_branch_and_working_tree() # rev1 self.do_full_upload() self.add_file('hello', 'foo') # rev2 self.modify_file('hello', 'bar') # rev3 self.assertUpPathDoesNotExist('hello') revspec = revisionspec.RevisionSpec.from_string('2') self.do_upload(revision=[revspec]) self.assertUpFileEqual('foo', 'hello') def test_no_upload_when_changes(self): self.make_branch_and_working_tree() self.add_file('a', 'foo') self.set_file_content('a', 'bar') self.assertRaises(errors.UncommittedChanges, self.do_upload) def test_no_upload_when_conflicts(self): self.make_branch_and_working_tree() self.add_file('a', 'foo') self.run_bzr('branch branch other') self.modify_file('a', 'bar') other_tree = workingtree.WorkingTree.open('other') self.set_file_content('a', 'baz', 'other/') other_tree.commit('modify file a') self.run_bzr('merge -d branch other', retcode=1) self.assertRaises(errors.UncommittedChanges, self.do_upload) def _test_change_file_into_dir(self, file_name): self.make_branch_and_working_tree() self.add_file(file_name, 'foo') self.do_full_upload() self.transform_file_into_dir(file_name) fpath = '%s/%s' % (file_name, 'file') self.add_file(fpath, 'bar') self.assertUpFileEqual('foo', file_name) self.do_upload() self.assertUpFileEqual('bar', fpath) def test_change_file_into_dir(self): self._test_change_file_into_dir('hello') def test_unicode_change_file_into_dir(self): self.requireFeature(features.UnicodeFilenameFeature) self._test_change_file_into_dir(u'hello\u00d8') def test_change_dir_into_file(self): self.make_branch_and_working_tree() self.add_dir('hello') self.add_file('hello/file', 'foo') self.do_full_upload() self.delete_any('hello/file') self.transform_dir_into_file('hello', 'bar') self.assertUpFileEqual('foo', 'hello/file') self.do_upload() self.assertUpFileEqual('bar', 'hello') def _test_make_file_executable(self, file_name): self.make_branch_and_working_tree() self.add_file(file_name, 'foo') self.chmod_file(file_name, 0664) self.do_full_upload() self.chmod_file(file_name, 0755) self.assertUpPathModeEqual(file_name, 0664) self.do_upload() self.assertUpPathModeEqual(file_name, 0775) def test_make_file_executable(self): self._test_make_file_executable('hello') def test_unicode_make_file_executable(self): self.requireFeature(features.UnicodeFilenameFeature) self._test_make_file_executable(u'hello\u00d8') def test_create_symlink(self): self.make_branch_and_working_tree() self.do_full_upload() self.add_symlink('link', 'target') self.do_upload() self.assertUpPathDoesNotExist('link') def test_rename_symlink(self): self.make_branch_and_working_tree() old_name, new_name = 'old-link', 'new-link' self.add_symlink(old_name, 'target') self.do_full_upload() self.rename_any(old_name, new_name) self.do_upload() self.assertUpPathDoesNotExist(old_name) self.assertUpPathDoesNotExist(new_name) def get_upload_auto(self): return cmds.get_upload_auto(self.tree.branch) def test_upload_auto(self): """Test that upload --auto sets the upload_auto option""" self.make_branch_and_working_tree() self.add_file('hello', 'foo') self.assertFalse(self.get_upload_auto()) self.do_full_upload(auto=True) self.assertUpFileEqual('foo', 'hello') self.assertTrue(self.get_upload_auto()) # and check that it stays set until it is unset self.add_file('bye', 'bar') self.do_full_upload() self.assertUpFileEqual('bar', 'bye') self.assertTrue(self.get_upload_auto()) def test_upload_noauto(self): """Test that upload --no-auto unsets the upload_auto option""" self.make_branch_and_working_tree() self.add_file('hello', 'foo') self.do_full_upload(auto=True) self.assertUpFileEqual('foo', 'hello') self.assertTrue(self.get_upload_auto()) self.add_file('bye', 'bar') self.do_full_upload(auto=False) self.assertUpFileEqual('bar', 'bye') self.assertFalse(self.get_upload_auto()) # and check that it stays unset until it is set self.add_file('again', 'baz') self.do_full_upload() self.assertUpFileEqual('baz', 'again') self.assertFalse(self.get_upload_auto()) def test_upload_from_subdir(self): self.make_branch_and_working_tree() self.build_tree(['branch/foo/', 'branch/foo/bar']) self.tree.add(['foo/', 'foo/bar']) self.tree.commit("Add directory") self.do_full_upload(directory='branch/foo') def test_upload_revid_path_in_dir(self): self.make_branch_and_working_tree() self.add_dir('dir') self.add_file('dir/goodbye', 'baz') revid_path = 'dir/revid-path' cmds.set_upload_revid_location(self.tree.branch, revid_path) self.assertUpPathDoesNotExist(revid_path) self.do_full_upload() self.add_file('dir/hello', 'foo') self.do_upload() self.assertUpPathExists(revid_path) self.assertUpFileEqual('baz', 'dir/goodbye') self.assertUpFileEqual('foo', 'dir/hello') def test_ignore_file(self): self.make_branch_and_working_tree() self.do_full_upload() self.add_file('.bzrignore-upload', 'foo') self.add_file('foo', 'bar') self.do_upload() self.assertUpPathDoesNotExist('foo') def test_ignore_regexp(self): self.make_branch_and_working_tree() self.do_full_upload() self.add_file('.bzrignore-upload', 'f*') self.add_file('foo', 'bar') self.do_upload() self.assertUpPathDoesNotExist('foo') def test_ignore_directory(self): self.make_branch_and_working_tree() self.do_full_upload() self.add_file('.bzrignore-upload', 'dir') self.add_dir('dir') self.do_upload() self.assertUpPathDoesNotExist('dir') def test_ignore_nested_directory(self): self.make_branch_and_working_tree() self.do_full_upload() self.add_file('.bzrignore-upload', 'dir') self.add_dir('dir') self.add_dir('dir/foo') self.add_file('dir/foo/bar', 'bar contents') self.do_upload() self.assertUpPathDoesNotExist('dir') self.assertUpPathDoesNotExist('dir/foo/bar') def test_ignore_change_file_into_dir(self): self.make_branch_and_working_tree() self.add_file('hello', 'foo') self.do_full_upload() self.add_file('.bzrignore-upload', 'hello') self.transform_file_into_dir('hello') self.add_file('hello/file', 'bar') self.assertUpFileEqual('foo', 'hello') self.do_upload() self.assertUpFileEqual('foo', 'hello') def test_ignore_change_dir_into_file(self): self.make_branch_and_working_tree() self.add_dir('hello') self.add_file('hello/file', 'foo') self.do_full_upload() self.add_file('.bzrignore-upload', 'hello') self.delete_any('hello/file') self.transform_dir_into_file('hello', 'bar') self.assertUpFileEqual('foo', 'hello/file') self.do_upload() self.assertUpFileEqual('foo', 'hello/file') def test_ignore_delete_dir_in_subdir(self): self.make_branch_and_working_tree() self.add_dir('dir') self.add_dir('dir/subdir') self.add_file('dir/subdir/a', 'foo') self.do_full_upload() self.add_file('.bzrignore-upload', 'dir/subdir') self.rename_any('dir/subdir/a', 'dir/a') self.delete_any('dir/subdir') self.assertUpFileEqual('foo', 'dir/subdir/a') self.do_upload() # The file in the dir is not ignored. This a bit contrived but # indicates that we may encounter problems when ignored items appear # and disappear... -- vila 100106 self.assertUpFileEqual('foo', 'dir/a') class TestFullUpload(tests.TestCaseWithTransport, TestUploadMixin): do_upload = TestUploadMixin.do_full_upload def test_full_upload_empty_tree(self): self.make_branch_and_working_tree() self.do_full_upload() revid_path = cmds.get_upload_revid_location(self.tree.branch) self.assertUpPathExists(revid_path) def test_invalid_revspec(self): self.make_branch_and_working_tree() rev1 = revisionspec.RevisionSpec.from_string('1') rev2 = revisionspec.RevisionSpec.from_string('2') self.assertRaises(errors.BzrCommandError, self.do_incremental_upload, revision=[rev1, rev2]) def test_create_remote_dir_twice(self): self.make_branch_and_working_tree() self.add_dir('dir') self.do_full_upload() self.add_file('dir/goodbye', 'baz') self.assertUpPathDoesNotExist('dir/goodbye') self.do_full_upload() self.assertUpFileEqual('baz', 'dir/goodbye') self.assertUpPathModeEqual('dir', 0775) class TestIncrementalUpload(tests.TestCaseWithTransport, TestUploadMixin): do_upload = TestUploadMixin.do_incremental_upload # XXX: full upload doesn't handle deletions.... def test_delete_one_file(self): self.make_branch_and_working_tree() self.add_file('hello', 'foo') self.do_full_upload() self.delete_any('hello') self.assertUpFileEqual('foo', 'hello') self.do_upload() self.assertUpPathDoesNotExist('hello') def test_delete_dir_and_subdir(self): self.make_branch_and_working_tree() self.add_dir('dir') self.add_dir('dir/subdir') self.add_file('dir/subdir/a', 'foo') self.do_full_upload() self.rename_any('dir/subdir/a', 'a') self.delete_any('dir/subdir') self.delete_any('dir') self.assertUpFileEqual('foo', 'dir/subdir/a') self.do_upload() self.assertUpPathDoesNotExist('dir/subdir/a') self.assertUpPathDoesNotExist('dir/subdir') self.assertUpPathDoesNotExist('dir') self.assertUpFileEqual('foo', 'a') def test_delete_one_file_rename_to_deleted(self): self.make_branch_and_working_tree() self.add_file('a', 'foo') self.add_file('b', 'bar') self.do_full_upload() self.delete_any('a') self.rename_any('b', 'a') self.assertUpFileEqual('foo', 'a') self.do_upload() self.assertUpPathDoesNotExist('b') self.assertUpFileEqual('bar', 'a') def test_rename_outside_dir_delete_dir(self): self.make_branch_and_working_tree() self.add_dir('dir') self.add_file('dir/a', 'foo') self.do_full_upload() self.rename_any('dir/a', 'a') self.delete_any('dir') self.assertUpFileEqual('foo', 'dir/a') self.do_upload() self.assertUpPathDoesNotExist('dir/a') self.assertUpPathDoesNotExist('dir') self.assertUpFileEqual('foo', 'a') def test_delete_symlink(self): self.make_branch_and_working_tree() self.add_symlink('link', 'target') self.do_full_upload() self.delete_any('link') self.do_upload() self.assertUpPathDoesNotExist('link') def test_upload_for_the_first_time_do_a_full_upload(self): self.make_branch_and_working_tree() self.add_file('hello', 'bar') revid_path = cmds.get_upload_revid_location(self.tree.branch) self.assertUpPathDoesNotExist(revid_path) self.do_upload() self.assertUpFileEqual('bar', 'hello') def test_ignore_delete_one_file(self): self.make_branch_and_working_tree() self.add_file('hello', 'foo') self.do_full_upload() self.add_file('.bzrignore-upload', 'hello') self.delete_any('hello') self.assertUpFileEqual('foo', 'hello') self.do_upload() self.assertUpFileEqual('foo', 'hello') class TestBranchUploadLocations(per_branch.TestCaseWithBranch): def test_get_upload_location_unset(self): config = self.get_branch().get_config() self.assertEqual(None, config.get_user_option('upload_location')) def test_get_push_location_exact(self): config.ensure_config_dir_exists() fn = config.locations_config_filename() b = self.get_branch() open(fn, 'wt').write(("[%s]\n" "upload_location=foo\n" % b.base.rstrip("/"))) conf = b.get_config() self.assertEqual("foo", conf.get_user_option('upload_location')) def test_set_push_location(self): conf = self.get_branch().get_config() conf.set_user_option('upload_location', 'foo') self.assertEqual('foo', conf.get_user_option('upload_location')) class TestUploadFromRemoteBranch(tests.TestCaseWithTransport, UploadUtilsMixin): remote_branch_dir = 'remote_branch' def setUp(self): super(TestUploadFromRemoteBranch, self).setUp() self.remote_branch_url = self.make_remote_branch_without_working_tree() def make_remote_branch_without_working_tree(self): """Creates a branch without working tree to upload from. It's created from the existing self.branch_dir one which still has its working tree. """ self.make_branch_and_working_tree() self.add_file('hello', 'foo') remote_branch_url = self.get_url(self.remote_branch_dir) if self.transport_server is stub_sftp.SFTPHomeDirServer: # FIXME: Some policy search ends up above the user home directory # and are seen as attemps to escape test isolation raise tests.TestNotApplicable('Escaping test isolation') self.run_bzr(['push', remote_branch_url, '--directory', self.branch_dir]) return remote_branch_url def test_no_upload_to_remote_working_tree(self): cmd = self._get_cmd_upload() up_url = self.get_url(self.branch_dir) # Let's try to upload from the just created remote branch into the # branch (which has a working tree). self.assertRaises(cmds.CannotUploadToWorkingTree, cmd.run, up_url, directory=self.remote_branch_url) def test_upload_without_working_tree(self): self.do_full_upload(directory=self.remote_branch_url) self.assertUpFileEqual('foo', 'hello') class TestUploadDiverged(tests.TestCaseWithTransport, UploadUtilsMixin): def setUp(self): super(TestUploadDiverged, self).setUp() self.diverged_tree = self.make_diverged_tree_and_upload_location() def make_diverged_tree_and_upload_location(self): tree_a = self.make_branch_and_tree('tree_a') tree_a.commit('message 1', rev_id='rev1') tree_a.commit('message 2', rev_id='rev2a') tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree() uncommit.uncommit(tree_b.branch, tree=tree_b) tree_b.commit('message 2', rev_id='rev2b') # upload tree a self.do_full_upload(directory=tree_a.basedir) return tree_b def assertRevidUploaded(self, revid): t = self.get_transport(self.upload_dir) uploaded_revid = t.get_bytes('.bzr-upload.revid') self.assertEqual(revid, uploaded_revid) def test_cant_upload_diverged(self): self.assertRaises(cmds.DivergedUploadedTree, self.do_incremental_upload, directory=self.diverged_tree.basedir) self.assertRevidUploaded('rev2a') def test_upload_diverged_with_overwrite(self): self.do_incremental_upload(directory=self.diverged_tree.basedir, overwrite=True) self.assertRevidUploaded('rev2b') class TestUploadBadRemoteReivd(tests.TestCaseWithTransport, UploadUtilsMixin): def test_raises_on_wrong_revid(self): tree = self.make_branch_and_working_tree() self.do_full_upload() # Put a fake revid on the remote branch t = self.get_transport(self.upload_dir) t.put_bytes('.bzr-upload.revid', 'fake') # Make a change self.add_file('foo', 'bar\n') self.assertRaises(cmds.DivergedUploadedTree, self.do_full_upload) bzr-upload-1.1.0/tests/__init__.py0000664000175000017500000000231011672125103016747 0ustar vilavila00000000000000# Copyright (C) 2008 by Canonical Ltd # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA def load_tests(basic_tests, module, loader): # This module shouldn't define any tests but I don't know how to report # that. I prefer to update basic_tests with the other tests to detect # unwanted tests and I think that's sufficient. testmod_names = [ 'test_auto_upload_hook', 'test_upload', ] basic_tests.addTest(loader.loadTestsFromModuleNames( ["%s.%s" % (__name__, tmn) for tmn in testmod_names])) return basic_tests bzr-upload-1.1.0/NEWS0000664000175000017500000000604011730422717014205 0ustar vilavila00000000000000######################## bzr-upload Release Notes ######################## .. contents:: List of Releases :depth: 1 bzr-upload 1.1.0 ################ :1.1.0: 2012-03-15 New Features ************ .. New commands, options, etc that users may wish to try out. Improvements ************ .. Improvements to existing commands, especially improved performance or memory usage, or better results. Bug Fixes ********* .. Fixes for situations where bzr would previously crash or give incorrect or undesirable results. Documentation ************* .. Improved or updated documentation. Testing ******* * Now requires bzr >= 2.5 for testing. The plugin itself should still work with previous versions. (Vincent Ladeuil) * Avoid deprecation warning with bzr-2.5 by using tree.iter_entries_by_dir avoiding direct inventory access, this should still be compatible with older bzr versions. (Vincent Ladeuil) bzr-upload 1.0.1 ################ :1.0.1: 2012-03-15 Bug Fixes ********* * Fix a typo to avoid crashing when encountering symlinks during a full upload. (Jonathan Paugh) Testing ******* * Use assertPathDoesNotExist and assertPathExist instead of failIfExists and failUnlessExists in the test suite. This requires bzr-2.4 to run the tests but doesn't affect the plugin compatibility itself with previous verions of bzr. (Vincent Ladeuil) bzr-upload 1.0.0 ################ :1.0.0: 2010-12-10 New Features ************ * ``.bzrignore-upload`` can be used to avoid uploading some files or directories. It uses the same syntax as ``.bzrignore`` including regular expressions. (Martin Albisetti, Vincent Ladeuil, #499525, #499941) * Remote branches can be used to upload from. (Gary van der Merwe, Vincent Ladeuil) * The auto hook verbosity is now controlled by the 'upload_auto_quiet' config variable. If defaults to False if not set. (Vincent Ladeuil, #312686) * The file where the revision id is stored on the remote server is now controlled by the 'upload_revid_location' configuration variable. It defaults to '.bzr-upload.revid'. (Vincent Ladeuil, #423331) * Upload now checks that the revision we are uploading is a descendent from the revision that was uploaded, and hence that the branchs that they were uploaded from have not diverged. This can be ignored by passing the --overwrite option. (Gary van der Merwe) Bug Fixes ********* * Fix auto hook trying to display an url without using the right encoding. (Vincent Ladeuil, #312686) * Fix compatibility with bzr versions that don't provide get_user_option_as_bool(). (Vincent Ladeuil, #423791) * Emit warnings instead of backtrace when symlinks are encountered. (Vincent Ladeuil, #477224) Documentation ************* * Clarify 'changes' definition in online help. (Vincent Ladeuil, #275538) * Move the README file into the module doc string so that it becomes available through the 'bzr help plugins/upload' command. (Vincent Ladeuil, #424193) Testing ******* * Make tests requiring a unicode file system skip where applicable. (Vincent Ladeuil, #671964)