silver-platter-0.2.0+git20191022.7591492/.gitignore0000644000000000000000000000011113553676177017064 0ustar 00000000000000silver_platter.egg-info/* __pycache__ *~ .eggs/ silver_platter.dist-info silver-platter-0.2.0+git20191022.7591492/.travis.yml0000644000000000000000000000067613553676177017225 0ustar 00000000000000language: python sudo: false cache: pip sudo: true dist: xenial addons: apt: update: true python: - 3.4 - 3.5 - 3.6 - pypy3.5 - 3.7 install: - sudo apt install devscripts bzr python3-flake8 flake8 python3-apt python3-debian - mkdir $HOME/.config/breezy/plugins -p - bzr branch lp:brz-debian ~/.config/breezy/plugins/debian - python setup.py develop script: - python silver_platter/tests/test_debian.py - make check silver-platter-0.2.0+git20191022.7591492/AUTHORS0000644000000000000000000000004313553676177016150 0ustar 00000000000000Jelmer Vernooij silver-platter-0.2.0+git20191022.7591492/LICENSE0000644000000000000000000004325413553676177016120 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. silver-platter-0.2.0+git20191022.7591492/MANIFEST.in0000644000000000000000000000013213553676177016635 0ustar 00000000000000include AUTHORS include LICENSE include README.rst include TODO recursive-include man *.1 silver-platter-0.2.0+git20191022.7591492/Makefile0000644000000000000000000000004613553676177016543 0ustar 00000000000000check: flake8 python3 setup.py test silver-platter-0.2.0+git20191022.7591492/README.rst0000644000000000000000000000507413553676177016600 0ustar 00000000000000Silver-Platter ============== Silver-Platter makes it possible to contribute automatable changes to source code in a version control system. It automatically creates a local checkout of a remote repository, makes user-specified changes, publishes those changes on the remote hosting site and then creates a pull request. In addition to that, it can also perform basic maintenance on branches that have been proposed for merging - such as restarting them if they have conflicts due to upstream changes. Getting started ~~~~~~~~~~~~~~~ To log in to a code-hosting site, use ``svp login``:: svp login https://github.com/ The simplest way to create a change as a merge proposal is to run something like:: svp run --mode propose https://github.com/jelmer/dulwich ./some-script.sh where ``some-script.sh`` makes some modifications to a working copy and prints the body for the pull request to standard out. For example:: #!/bin/sh sed -i 's/framwork/framework/' README.rst echo "Fix common typo: framwork => framework" Supported hosters ~~~~~~~~~~~~~~~~~ At the moment, the following code hosters are supported: * `GitHub `_ * `Launchpad `_ * `GitLab `_ instances, such as Debian's `Salsa `_ Working with Debian packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Several common operations for Debian packages have dedicated subcommands under the ``debian-svp`` command. These will also automatically look up packaging repository location for any Debian package names that are specified. Subcommands that are available include: * *lintian-brush*: Run the `lintian-brush `_ command on the branch. * *upload-pending*: Build and upload a package and push/propose the changelog updates. * *new-upstream*: Merge in a new upstream release or snapshot. *debian-svp run* takes package name arguments that will be resolved to repository locations from the *Vcs-Git* field in the package. See ``debian-svp COMMAND --help`` for more details. Examples running ``debian-svp``:: debian-svp lintian-brush samba debian-svp lintian-brush --mode=propose samba debian-svp lintian-brush --mode=push samba debian-svp upload-pending tdb debian-svp new-upstream --no-build-verify tdb Credentials ~~~~~~~~~~~ The ``svp hosters`` subcommand can be used to display the hosting sites that silver-platter is aware of:: svp hosters And to log into a new hosting site, simply run ``svp login BASE-URL``, e.g.:: svp login https://launchpad.net/ silver-platter-0.2.0+git20191022.7591492/TODO0000644000000000000000000000124513553676177015575 0ustar 00000000000000Backends (in Breezy upstream) - Support for Mercurial - Support for Svn * For "push" packages, don't require an associated hoster - make changes, commit, push back + Still need a way to convert "public read-only URL" to "writeable URL". - For git, just use pushInsteadOf Upstream changes: * pngcrush * FSF Debian ====== * Submit patches against non-Vcs packages to the BTS? * Create cherry-pick merge proposals for bug fixes that are forwarded upstream + especially for stable debian releases - use debian.deb822.GPGV_DEFAULT_KEYRING in upload-pending-changes.py ? upload-pending-commits.py: * Improve speed of verifying signatures - support --mode=merge-directive silver-platter-0.2.0+git20191022.7591492/debian-svp0000755000000000000000000000163513553676177017066 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 os import sys sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) from silver_platter.debian.__main__ import main # noqa: E402 sys.exit(main()) silver-platter-0.2.0+git20191022.7591492/devnotes/0000755000000000000000000000000013553676177016732 5ustar 00000000000000silver-platter-0.2.0+git20191022.7591492/helpers/0000755000000000000000000000000013553676177016545 5ustar 00000000000000silver-platter-0.2.0+git20191022.7591492/man/0000755000000000000000000000000013553676177015656 5ustar 00000000000000silver-platter-0.2.0+git20191022.7591492/setup.cfg0000644000000000000000000000006613553676177016726 0ustar 00000000000000[flake8] filename = *.py,svp exclude = *_pb2.py,.eggs silver-platter-0.2.0+git20191022.7591492/setup.py0000755000000000000000000000470213553676177016623 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 setuptools import setup setup( name='silver-platter', author="Jelmer Vernooij", author_email="jelmer@jelmer.uk", url="https://jelmer.uk/code/silver-platter", description="Automatic merge proposal creeator", version="0.2.0", license='GNU GPL v2 or later', project_urls={ "Bug Tracker": "https://github.com/jelmer/silver-platter/issues", "Repository": "https://jelmer.uk/code/silver-platter", "GitHub": "https://github.com/jelmer/silver-platter", }, keywords="git bzr vcs github gitlab launchpad", packages=[ 'silver_platter', 'silver_platter.debian', 'silver_platter.tests', ], classifiers=[ 'Development Status :: 3 - Alpha', 'License :: OSI Approved :: GNU General Public License (GPL)', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Operating System :: POSIX', 'Topic :: Software Development :: Version Control', ], entry_points={ 'console_scripts': [ 'svp=silver_platter.__main__:main', 'debian-svp=silver_platter.debian.__main__:main', ], }, test_suite='silver_platter.tests.test_suite', install_requires=[ 'breezy', 'dulwich', 'testtools', 'lintian-brush>=0.16', ], ) silver-platter-0.2.0+git20191022.7591492/silver_platter/0000755000000000000000000000000013553676177020142 5ustar 00000000000000silver-platter-0.2.0+git20191022.7591492/svp0000755000000000000000000000162613553676177015646 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 os import sys sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) from silver_platter.__main__ import main # noqa: E402 sys.exit(main()) silver-platter-0.2.0+git20191022.7591492/devnotes/command-line.rst0000644000000000000000000000107613553676177022033 0ustar 00000000000000Command-line interface ====================== Example commands: svp run lp:brz-email /tmp/some-script.py svp run --name=blah lp:brz-email /tmp/some-script.py svp run --mode=attempt-push lp:brz-email /tmp/some-script.py svp hosters svp login https://github.com/ svp login https://gitlab.com/ svp login https://salsa.debian.org/ debian-svp run brz-email ./some-script.py debian-svp lintian-brush samba debian-svp lintian-brush --mode=propose samba debian-svp lintian-brush --mode=push samba debian-svp upload-pending tdb debian-svp merge-upstream --no-build-verify tdb silver-platter-0.2.0+git20191022.7591492/devnotes/mp-status.rst0000644000000000000000000000206313553676177021422 0ustar 00000000000000Closing Merge Proposals ======================= Merge proposals can have a number of statuses: status: Open (Work In Progress) "wip" More work is being done by the original author; changes have not been merged and the merge proposal is not ready for review. status: Closed (Merged) "merged" The change has been merged. No further actions are expected. On Launchpad, this means that the merge proposal is frozen - the branch can be reused. status: Closed (Rejected) "rejected" The branch has been rejected. Changes were not merged. The branch and merge proposal should be kept around so we don't create a new branch proposal with the same changes. Requires human follow-up. status: Closed (Obsolete) "obsolete" Can happen e.g. when the changes are made independently by somebody else. status: Open (Waiting Review) "waiting-review" The merge proposal is ready and waiting for review from a reviewer. status: Open (Waiting Follow-up) "waiting-followup" The merge proposal is waiting for the original author to follow up to comments from a reviewer. silver-platter-0.2.0+git20191022.7591492/helpers/needs-changelog-update.py0000755000000000000000000000223313553676177023425 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 silver_platter # noqa: F401 from silver_platter.debian import ( _changelog_stats, ) from breezy.branch import Branch import argparse parser = argparse.ArgumentParser() parser.add_argument('location', help='Branch location to check.', type=str, default='.') args = parser.parse_args() branch = Branch.open(args.location) print(_changelog_stats(branch, 200)) silver-platter-0.2.0+git20191022.7591492/man/debian-svp.10000644000000000000000000000536613553676177020002 0ustar 00000000000000.TH DEBIAN-SVP "1" "February 2019" "debian-svp 0.0.1" "User Commands" .SH NAME debian-svp \- create and manage changes against Debian packaging branches .SH SYNOPSIS debian\-svp [\-h] [\-\-version] {run,new-upstream,upload-pending,lintian\-brush} ... .SH DESCRIPTION debian-svp is a specialized version of \&\fIsvp\fR\|(1) that automatically resolves Debian package names to the URLs of packaging branches. It also provides support for a couple of Debian-specific operations. .SS "COMMAND OVERVIEW" .TP .B debian\-svp run [\-h] [\-\-refresh] [\-\-label LABEL] [\-\-name NAME] [\-\-mode {push,attempt\-push,propose}] [\-\-dry\-run] [\-\-commit-pending {auto,yes,no}] package script Make a change by running a script. \fBURL\fR should be the URL of a repository to make changes to. Script will be run in a checkout of the URL, with the opportunity to make changes. Depending on the specified mode, the changes will be committed and pushed back the the repository at the original URL or proposed as a change to the repository at the original URL. .TP .B debian\-svp new\-upstream [\-h] [\-\-snapshot] [\-\-no\-build\-verify] [\-\-pre\-check PRE_CHECK] [\-\-dry\-run] [\-\-mode {push,attempt\-push,propose}] packages [packages ...] Create a merge proposal merging a new upstream version. The location of the upstream repository is retrieved from the \fBdebian/upstream/metadata\fR file, and the tarball is fetched using \&\fIuscan\fR\|(1). .TP .B "debian-svp upload-pending" Upload pending commits in a packaging branch. .TP .B debian\-svp lintian\-brush [\-\-fixers FIXERS] [\-\-dry\-run] [\-\-propose\-addon\-only PROPOSE_ADDON_ONLY] [\-\-pre\-check PRE_CHECK] [\-\-post\-check POST_CHECK] [\-\-build\-verify] [\-\-refresh] [\-\-committer COMMITTER] [\-\-mode {push,attempt\-push,propose}] [\-\-no\-update\-changelog] [\-\-update\-changelog] [packages [packages ...]] Create a merge proposal fixing lintian issues. .SS "optional arguments:" .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-\-version\fR show program's version number and exit .SH EXAMPLES .TP .B debian\-svp lintian\-brush \fBhttps://salsa.debian.org/jelmer/lintian-brush\fR Run \&\fIlintian\-brush\fR\|(1) on the \fBdulwich\fR package and create a merge proposal with the resulting changes. .TP .B debian\-svp lintian\-brush \fBdulwich\fR Run \&\fIlintian\-brush\fR\|(1) on the \fBdulwich\fR package and create a merge proposal with the resulting changes. .TP .B debian\-svp new\-upstream \fBdulwich\fR Create a new merge proposal merging the latest upstream version of \fBdulwich\fR into the packaging branch. .SH "SEE ALSO" \&\fIsvp\fR\|(1), \&\fIgit\fR\|(1), \&\fIbrz\fR\|(1), \&\fIlintian-brush\fR\|(1) .SH "LICENSE" GNU General Public License, version 2 or later. .SH AUTHORS Jelmer Vernooij silver-platter-0.2.0+git20191022.7591492/man/svp.10000644000000000000000000000466213553676177016560 0ustar 00000000000000.TH SVP "1" "February 2019" "svp 0.0.1" "User Commands" .SH NAME svp \- create and manage changes to VCS repositories .SH SYNOPSIS svp [\-h] [\-\-version] {run,hosters,login,proposals} ... .SH DESCRIPTION Silver-Platter makes it possible to contribute automatable changes to source code in a version control system. It automatically creates a local checkout of a remote repository, make user-specified changes, publish those changes on the remote hosting site and then creates a pull request. In addition to that, it can also perform basic maintenance on branches that have been proposed for merging - such as restarting them if they have conflicts due to upstream changes. .SS "COMMAND OVERVIEW" .TP .B svp run [\-\-refresh] [\-\-label LABEL] [\-\-name NAME] [\-\-mode {push,attempt\-push,propose}] [\-\-commit-pending {auto,yes,no}] [\-\-dry\-run] url script Make a change by running a script. \fBURL\fR should be the URL of a repository to make changes to. Script will be run in a checkout of the URL, with the opportunity to make changes. Depending on the specified mode, the changes will be committed and pushed back the the repository at the original URL or proposed as a change to the repository at the original URL. .TP .B svp hosters Display known hosting sites. .TP .B svp login BASE-URL Log into a new hosting site. .TP .B svp proposals [\-\-status {open,merged,closed}] Print URLs of all proposals of a specified status that are owned by the current user. .SS "optional arguments:" .TP \fB\-h\fR, \fB\-\-help\fR show this help message and exit .TP \fB\-\-version\fR show program's version number and exit .SH "SUPPORTED HOSTERS" At the moment \fBGitHub\fR, \fBLaunchpad\fR and any instances of \fBGitLab\fR are supported. .SH "EXAMPLES" .TP .B svp login \fBhttps://github.com/\fR Log in to GitHub .TP .B svp hosters List all known hosting sites .TP .B svp proposals --status merged List all merged proposals owned by the current user. .TP .B svp run --mode=attempt-push \fBgit://github.com/dulwich/dulwich\fR \fB./fix-typo.py\fR Run the script \fB./fix-typo.py\fR in a checkout of the Dulwich repository. Any changes the script makes will be pushed back to the main repository if the current user has the right permissions, and otherwise they will be proposed as a pull request. .SH "SEE ALSO" \&\fIdebian-svp\fR\|(1), \&\fIgit\fR\|(1), \&\fIbrz\fR\|(1) .SH "LICENSE" GNU General Public License, version 2 or later. .SH AUTHORS Jelmer Vernooij silver-platter-0.2.0+git20191022.7591492/silver_platter/__init__.py0000644000000000000000000000301513553676177022252 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 # TODO(jelmer): Imports with side-effects are bad... import os import sys if os.name == "posix": import locale locale.setlocale(locale.LC_ALL, '') # Use better default than ascii with posix filesystems that deal in bytes # natively even when the C locale or no locale at all is given. Note that # we need an immortal string for the hack, hence the lack of a hyphen. sys._brz_default_fs_enc = "utf8" import breezy # noqa: E402 breezy.initialize() import breezy.git # For git support # noqa: E402 import breezy.bzr # For bzr support # noqa: E402 import breezy.plugins.launchpad # For lp: URL support # noqa: E402 import breezy.plugins.debian # For apt: URL support # noqa: E402 __version__ = (0, 2, 0) version_string = '.'.join(map(str, __version__)) silver-platter-0.2.0+git20191022.7591492/silver_platter/__main__.py0000644000000000000000000000715013553676177022237 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018-2019 Jelmer Vernooij # # 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 argparse import silver_platter # noqa: F401 import sys from . import ( run, version_string, ) from breezy.trace import show_error def hosters_main(args): from breezy.plugins.propose.propose import hosters for name, hoster_cls in hosters.items(): for instance in hoster_cls.iter_instances(): print('%s (%s)' % (instance.base_url, name)) def login_setup_parser(parser): parser.add_argument('url', help='URL of branch to work on.', type=str) def login_main(args): from launchpadlib import uris as lp_uris hoster = None # TODO(jelmer): Don't special case various hosters here if args.url.startswith('https://github.com'): hoster = 'github' for key, root in lp_uris.web_roots.items(): if args.url.startswith(root) or args.url == root.rstrip('/'): hoster = 'launchpad' lp_service_root = lp_uris.service_roots[key] if hoster is None: hoster = 'gitlab' from breezy.plugins.propose.cmds import cmd_github_login, cmd_gitlab_login if hoster == 'gitlab': cmd = cmd_gitlab_login() cmd._setup_outf() return cmd.run(args.url) elif hoster == 'github': cmd = cmd_github_login() cmd._setup_outf() return cmd.run() elif hoster == 'launchpad': from breezy.plugins.launchpad.cmds import cmd_launchpad_login cmd = cmd_launchpad_login() cmd._setup_outf() cmd.run() from breezy.plugins.launchpad import lp_api lp_api.connect_launchpad(lp_service_root, version='devel') else: show_error('Unknown hoster %r.', hoster) return 1 def proposals_setup_parser(parser): parser.add_argument( '--status', default='open', choices=['open', 'merged', 'closed'], type=str, help='Only display proposals with this status.') def proposals_main(args): from .proposal import iter_all_mps for hoster, proposal, status in iter_all_mps([args.status]): print(proposal.url) subcommands = [ ('hosters', None, hosters_main), ('login', login_setup_parser, login_main), ('proposals', proposals_setup_parser, proposals_main), ('run', run.setup_parser, run.main), ] def main(argv=None): parser = argparse.ArgumentParser(prog='svp') parser.add_argument( '--version', action='version', version='%(prog)s ' + version_string) subparsers = parser.add_subparsers(dest='subcommand') callbacks = {} for name, setup_parser, run_fn in subcommands: subparser = subparsers.add_parser(name) if setup_parser is not None: setup_parser(subparser) callbacks[name] = run_fn args = parser.parse_args(argv) if args.subcommand is None: parser.print_usage() return 1 return callbacks[args.subcommand](args) if __name__ == '__main__': sys.exit(main()) silver-platter-0.2.0+git20191022.7591492/silver_platter/debian/0000755000000000000000000000000013553676177021364 5ustar 00000000000000silver-platter-0.2.0+git20191022.7591492/silver_platter/proposal.py0000644000000000000000000005447313553676177022370 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 breezy.diff import show_diff_trees from breezy.errors import ( DivergedBranches, PermissionDenied, UnsupportedOperation, ) from breezy.trace import ( note, ) from breezy import ( errors, merge as _mod_merge, ) from breezy.plugins.propose.propose import ( get_hoster, hosters, MergeProposal, NoSuchProject, UnsupportedHoster, ) from .utils import ( create_temp_sprout, open_branch, MinimalMemoryBranch, ) __all__ = [ 'UnsupportedHoster', 'PermissionDenied', 'NoSuchProject', 'get_hoster', 'hosters', 'iter_all_mps', ] SUPPORTED_MODES = ['push', 'attempt-push', 'propose', 'push-derived'] def merge_conflicts(main_branch, other_branch): """Check whether two branches are conflicted when merged. Args: main_branch: Main branch to merge into other_branch: Branch to merge (and use for scratch access, needs write access) Returns: boolean indicating whether the merge would result in conflicts """ if other_branch.repository.get_graph().is_ancestor( main_branch.last_revision(), other_branch.last_revision()): return False other_branch.repository.fetch( main_branch.repository, revision_id=main_branch.last_revision()) # Reset custom merge hooks, since they could make it harder to detect # conflicted merges that would appear on the hosting site. old_file_content_mergers = _mod_merge.Merger.hooks['merge_file_content'] _mod_merge.Merger.hooks['merge_file_content'] = [] try: merger = _mod_merge.Merger.from_revision_ids( other_branch.basis_tree(), other_branch=other_branch, other=main_branch.last_revision(), tree_branch=other_branch) merger.merge_type = _mod_merge.Merge3Merger tree_merger = merger.make_merger() with tree_merger.make_preview_transform(): return bool(tree_merger.cooked_conflicts) finally: _mod_merge.Merger.hooks['merge_file_content'] = ( old_file_content_mergers) class DryRunProposal(MergeProposal): """A merge proposal that is not actually created. :ivar url: URL for the merge proposal """ def __init__(self, source_branch, target_branch, labels=None, description=None, commit_message=None): self.description = description self.closed = False self.labels = (labels or []) self.source_branch = source_branch self.target_branch = target_branch self.commit_message = commit_message self.url = None @classmethod def from_existing(cls, mp, source_branch=None): if source_branch is None: source_branch = open_branch(mp.get_source_branch_url()) commit_message = None if getattr(mp, 'get_commit_message', None): # brz >= 3.1 only commit_message = mp.get_commit_message() return cls( source_branch=source_branch, target_branch=open_branch(mp.get_target_branch_url()), description=mp.get_description(), commit_message=commit_message) def __repr__(self): return "%s(%r, %r)" % ( self.__class__.__name__, self.source_branch, self.target_branch) def get_description(self): """Get the description of the merge proposal.""" return self.description def set_description(self, description): self.description = description def get_commit_message(self): return self.commit_message def set_commit_message(self, commit_message): self.commit_message = commit_message def get_source_branch_url(self): """Return the source branch.""" return self.source_branch.user_url def get_target_branch_url(self): """Return the target branch.""" return self.target_branch.user_url def close(self): """Close the merge proposal (without merging it).""" self.closed = True def is_merged(self): """Check whether this merge proposal has been merged.""" return False def is_closed(self): """Check whether this merge proposal has been closed.""" return False def reopen(self): pass def push_result(local_branch, remote_branch, additional_colocated_branches=None): try: local_branch.push(remote_branch) except errors.LockFailed as e: # Almost certainly actually a PermissionDenied error.. raise PermissionDenied(path=remote_branch.user_url, extra=e) for branch_name in additional_colocated_branches or []: try: add_branch = local_branch.controldir.open_branch( name=branch_name) except errors.NotBranchError: pass else: remote_branch.controldir.push_branch( add_branch, name=branch_name) def find_existing_proposed(main_branch, hoster, name, overwrite_unrelated=False): """Find an existing derived branch with the specified name, and proposal. Args: main_branch: Main branch hoster: The hoster name: Name of the derived branch overwrite_unrelated: Whether to overwrite existing (but unrelated) branches Returns: Tuple with (resume_branch, overwrite_existing, existing_proposal) The resume_branch is the branch to continue from; overwrite_existing means there is an existing branch in place that should be overwritten. """ try: existing_branch = hoster.get_derived_branch(main_branch, name=name) except errors.NotBranchError: return (None, None, None) else: note('Branch %s already exists (branch at %s)', name, existing_branch.user_url) # If there is an open or rejected merge proposal, resume that. merged_proposal = None for mp in hoster.iter_proposals( existing_branch, main_branch, status='all'): if not mp.is_closed(): return (existing_branch, False, mp) else: merged_proposal = mp else: if merged_proposal is not None: note('There is a proposal that has already been merged at %s.', merged_proposal.url) return (None, True, None) else: # No related merge proposals found, but there is an existing # branch (perhaps for a different target branch?) if overwrite_unrelated: return (None, True, None) else: # TODO(jelmer): What to do in this case? return (None, False, None) class Workspace(object): """Workspace for creating changes to a branch. main_branch: The upstream branch resume_branch: Optional in-progress branch that we previously made changes on, and should ideally continue from. cached_branch: Branch to copy revisions from, if possible. local_tree: The tree the user can work in """ def __init__(self, main_branch, resume_branch=None, cached_branch=None, additional_colocated_branches=None, dir=None, path=None): self.main_branch = main_branch self.main_branch_revid = main_branch.last_revision() self.cached_branch = cached_branch self.resume_branch = resume_branch self.additional_colocated_branches = ( additional_colocated_branches or []) self._destroy = None self.local_tree = None self._dir = dir self._path = path def __enter__(self): self.local_tree, self._destroy = create_temp_sprout( self.cached_branch or self.resume_branch or self.main_branch, self.additional_colocated_branches, dir=self._dir, path=self._path) self.refreshed = False with self.local_tree.branch.lock_write(): if self.cached_branch: self.local_tree.pull( self.resume_branch or self.main_branch, overwrite=True) if self.resume_branch: try: self.local_tree.pull(self.main_branch, overwrite=False) except DivergedBranches: pass if merge_conflicts( self.main_branch, self.local_tree.branch): note('restarting branch') self.local_tree.update(revision=self.main_branch_revid) self.local_tree.branch.generate_revision_history( self.main_branch_revid) self.resume_branch = None self.refreshed = True self.orig_revid = self.local_tree.last_revision() return self def defer_destroy(self): ret = self._destroy self._destroy = None return ret def changes_since_main(self): return self.local_tree.branch.last_revision() != self.main_branch_revid def changes_since_resume(self): return self.orig_revid != self.local_tree.branch.last_revision() def push(self, hoster=None, dry_run=False): if hoster is None: hoster = get_hoster(self.main_branch) return push_changes( self.local_tree.branch, self.main_branch, hoster=hoster, additional_colocated_branches=self.additional_colocated_branches, dry_run=dry_run) def propose(self, name, description, hoster=None, existing_proposal=None, overwrite_existing=None, labels=None, dry_run=False, commit_message=None): if hoster is None: hoster = get_hoster(self.main_branch) return propose_changes( self.local_tree.branch, self.main_branch, hoster=hoster, name=name, mp_description=description, resume_branch=self.resume_branch, resume_proposal=existing_proposal, overwrite_existing=overwrite_existing, labels=labels, dry_run=dry_run, commit_message=commit_message, additional_colocated_branches=self.additional_colocated_branches) def push_derived(self, name, hoster=None, overwrite_existing=False): """Push a derived branch. Args: name: Branch name hoster: Optional hoster to use overwrite_existing: Whether to overwrite an existing branch Returns: tuple with remote_branch and public_branch_url """ if hoster is None: hoster = get_hoster(self.main_branch) return push_derived_changes( self.local_tree.branch, self.main_branch, hoster, name, overwrite_existing=overwrite_existing) def orig_tree(self): return self.local_tree.branch.repository.revision_tree(self.orig_revid) def show_diff(self, outf, old_label='old/', new_label='new/'): orig_tree = self.orig_tree() show_diff_trees( orig_tree, self.local_tree.basis_tree(), outf, old_label=old_label, new_label=new_label) def __exit__(self, exc_type, exc_val, exc_tb): if self._destroy: self._destroy() self._destroy = None return False def enable_tag_pushing(branch): stack = branch.get_config() stack.set_user_option('branch.fetch_tags', True) class PublishResult(object): """A object describing the result of a publish action.""" def __init__(self, mode, proposal=None, is_new=False): self.mode = mode self.proposal = proposal self.is_new = is_new def __tuple__(self): # Backwards compatibility return (self.proposal, self.is_new) def publish_changes(ws, mode, name, get_proposal_description, get_proposal_commit_message=None, dry_run=False, hoster=None, allow_create_proposal=True, labels=None, overwrite_existing=True, existing_proposal=None): if mode not in SUPPORTED_MODES: raise ValueError("invalid mode %r" % mode) if not ws.changes_since_main(): if existing_proposal is not None: note('closing existing merge proposal - no new revisions') existing_proposal.close() return PublishResult(mode) if not ws.changes_since_resume(): # No new revisions added on this iteration, but changes since main # branch. We may not have gotten round to updating/creating the # merge proposal last time. note('No changes added; making sure merge proposal is up to date.') if hoster is None: hoster = get_hoster(ws.main_branch) if mode == 'push-derived': (remote_branch, public_url) = ws.push_derived( name=name, overwrite_existing=overwrite_existing) return PublishResult(mode) if mode in ('push', 'attempt-push'): try: ws.push(hoster, dry_run=dry_run) except PermissionDenied: if mode == 'attempt-push': note('push access denied, falling back to propose') mode = 'propose' else: note('permission denied during push') raise else: return PublishResult(mode=mode) assert mode == 'propose' if not ws.resume_branch and not allow_create_proposal: # TODO(jelmer): Raise an exception of some sort here? return PublishResult(mode) mp_description = get_proposal_description( existing_proposal if ws.resume_branch else None) if get_proposal_commit_message is not None: commit_message = get_proposal_commit_message( existing_proposal if ws.resume_branch else None) (proposal, is_new) = ws.propose( name, mp_description, hoster=hoster, existing_proposal=existing_proposal, labels=labels, dry_run=dry_run, overwrite_existing=overwrite_existing, commit_message=commit_message) return PublishResult(mode, proposal, is_new) def push_changes(local_branch, main_branch, hoster, possible_transports=None, additional_colocated_branches=None, dry_run=False): """Push changes to a branch.""" push_url = hoster.get_push_url(main_branch) note('pushing to %s', push_url) target_branch = open_branch( push_url, possible_transports=possible_transports) if not dry_run: push_result(local_branch, target_branch, additional_colocated_branches) class EmptyMergeProposal(Exception): """Merge proposal does not have any changes.""" def __init__(self, local_branch, main_branch): self.local_branch = local_branch self.main_branch = main_branch def check_proposal_diff(other_branch, main_branch): from breezy import merge as _mod_merge main_revid = main_branch.last_revision() other_branch.repository.fetch(main_branch.repository, main_revid) with other_branch.lock_read(): main_tree = other_branch.repository.revision_tree(main_revid) revision_graph = other_branch.repository.get_graph() merger = _mod_merge.Merger.from_revision_ids( main_tree, other_branch=other_branch, other=other_branch.last_revision(), tree_branch=MinimalMemoryBranch( other_branch.repository, (None, main_branch.last_revision()), None), revision_graph=revision_graph) merger.merge_type = _mod_merge.Merge3Merger tree_merger = merger.make_merger() with tree_merger.make_preview_transform() as tt: changes = tt.iter_changes() if not any(changes): raise EmptyMergeProposal(other_branch, main_branch) def propose_changes( local_branch, main_branch, hoster, name, mp_description, resume_branch=None, resume_proposal=None, overwrite_existing=True, labels=None, dry_run=False, commit_message=None, additional_colocated_branches=None, allow_empty=False): """Create or update a merge proposal. Args: local_branch: Local branch with changes to propose main_branch: Target branch to propose against hoster: Associated hoster for main branch mp_description: Merge proposal description resume_branch: Existing derived branch resume_proposal: Existing merge proposal to resume overwrite_existing: Whether to overwrite any other existing branch labels: Labels to add dry_run: Whether to just dry-run the change commit_message: Optional commit message additional_colocated_branches: Additional colocated branches to propose allow_empty: Whether to allow empty merge proposals Returns: Tuple with (proposal, is_new) """ if not allow_empty: check_proposal_diff(local_branch, main_branch) # TODO(jelmer): Actually push additional_colocated_branches if not dry_run: if resume_branch is not None: local_branch.push(resume_branch, overwrite=overwrite_existing) remote_branch = resume_branch else: remote_branch, public_branch_url = hoster.publish_derived( local_branch, main_branch, name=name, overwrite=overwrite_existing) if resume_proposal is not None and dry_run: resume_proposal = DryRunProposal.from_existing( resume_proposal, source_branch=local_branch) if (resume_proposal is not None and getattr(resume_proposal, 'is_closed', None) and resume_proposal.is_closed()): from breezy.plugins.propose.propose import ( ReopenFailed, ) try: resume_proposal.reopen() except ReopenFailed: note('Reopening existing proposal failed. Creating new proposal.') resume_proposal = None if resume_proposal is not None: # Check that the proposal doesn't already has this description. # Setting the description (regardless of whether it changes) # causes Launchpad to send emails. if resume_proposal.get_description() != mp_description: resume_proposal.set_description(mp_description) if getattr(resume_proposal, 'get_commit_message', None): # brz >= 3.1 only if resume_proposal.get_commit_message() != commit_message: try: resume_proposal.set_commit_message(commit_message) except UnsupportedOperation: pass return (resume_proposal, False) else: if not dry_run: proposal_builder = hoster.get_proposer( remote_branch, main_branch) kwargs = {} if getattr( hoster, 'supports_merge_proposal_commit_message', False): # brz >= 3.1 only kwargs['commit_message'] = commit_message try: mp = proposal_builder.create_proposal( description=mp_description, labels=labels, **kwargs) except PermissionDenied: note('Permission denied while trying to create ' 'proposal.') raise else: mp = DryRunProposal( local_branch, main_branch, labels=labels, description=mp_description, commit_message=commit_message) return (mp, True) def merge_directive_changes( local_branch, main_branch, hoster, name, message, include_patch=False, include_bundle=False, overwrite_existing=False): from breezy import merge_directive, osutils import time remote_branch, public_branch_url = hoster.publish_derived( local_branch, main_branch, name=name, overwrite=overwrite_existing) public_branch = open_branch(public_branch_url) directive = merge_directive.MergeDirective2.from_objects( local_branch.repository, local_branch.last_revision(), time.time(), osutils.local_time_offset(), main_branch, public_branch=public_branch, include_patch=include_patch, include_bundle=include_bundle, message=message, base_revision_id=main_branch.last_revision()) return directive def push_derived_changes( local_branch, main_branch, hoster, name, overwrite_existing=False): remote_branch, public_branch_url = hoster.publish_derived( local_branch, main_branch, name=name, overwrite=overwrite_existing) return remote_branch, public_branch_url def iter_all_mps(statuses=None): """iterate over all existing merge proposals.""" if statuses is None: statuses = ['open', 'merged', 'closed'] for name, hoster_cls in hosters.items(): for instance in hoster_cls.iter_instances(): for status in statuses: for mp in instance.iter_my_proposals(status=status): yield instance, mp, status def iter_conflicted(branch_name): possible_transports = [] for hoster, mp, status in iter_all_mps(['open']): try: if mp.can_be_merged(): continue except (NotImplementedError, AttributeError): # TODO(jelmer): Check some other way that the branch is conflicted? continue main_branch = open_branch( mp.get_target_branch_url(), possible_transports=possible_transports) resume_branch = open_branch( mp.get_source_branch_url(), possible_transports=possible_transports) if resume_branch.name != branch_name and not ( not resume_branch.name and resume_branch.user_url.endswith(branch_name)): continue yield (resume_branch.user_url, main_branch, resume_branch, hoster, mp, True) silver-platter-0.2.0+git20191022.7591492/silver_platter/run.py0000755000000000000000000001564213553676177021333 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 """Automatic proposal/push creation.""" from __future__ import absolute_import import os import subprocess import sys import silver_platter # noqa: F401 from breezy import osutils from breezy import errors from breezy.commit import PointlessCommit from breezy.trace import note, warning, show_error from breezy.plugins.propose import ( propose as _mod_propose, ) from .proposal import ( UnsupportedHoster, enable_tag_pushing, find_existing_proposed, get_hoster, publish_changes, Workspace, SUPPORTED_MODES, ) from .utils import ( open_branch, BranchMissing, BranchUnavailable, ) class ScriptMadeNoChanges(errors.BzrError): _fmt = "Script made no changes." def script_runner(local_tree, script, commit_pending=None): """Run a script in a tree and commit the result. This ignores newly added files. :param local_tree: Local tree to run script in :param script: Script to run :param commit_pending: Whether to commit pending changes (True, False or None: only commit if there were no commits by the script) :return: Description as reported by script """ last_revision = local_tree.last_revision() p = subprocess.Popen(script, cwd=local_tree.basedir, stdout=subprocess.PIPE, shell=True) (description, err) = p.communicate("") if p.returncode != 0: raise errors.BzrCommandError( "Script %s failed with error code %d" % ( script, p.returncode)) new_revision = local_tree.last_revision() description = description.decode() if last_revision == new_revision and commit_pending is None: # Automatically commit pending changes if the script did not # touch the branch. commit_pending = True if commit_pending: try: new_revision = local_tree.commit( description, allow_pointless=False) except PointlessCommit: pass if new_revision == last_revision: raise ScriptMadeNoChanges() return description def derived_branch_name(script): return os.path.splitext(osutils.basename(script.split(' ')[0]))[0] def setup_parser(parser): parser.add_argument('script', help='Path to script to run.', type=str) parser.add_argument('url', help='URL of branch to work on.', type=str) parser.add_argument('--refresh', action="store_true", help='Refresh changes if branch already exists') parser.add_argument('--label', type=str, help='Label to attach', action="append", default=[]) parser.add_argument('--name', type=str, help='Proposed branch name', default=None) parser.add_argument('--diff', action="store_true", help="Show diff of generated changes.") parser.add_argument( '--mode', help='Mode for pushing', choices=SUPPORTED_MODES, default="propose", type=str) parser.add_argument( '--commit-pending', help='Commit pending changes after script.', choices=['yes', 'no', 'auto'], default='auto', type=str) parser.add_argument( "--dry-run", help="Create branches but don't push or propose anything.", action="store_true", default=False) def main(args): try: main_branch = open_branch(args.url) except (BranchUnavailable, BranchMissing) as e: show_error('%s: %s', args.url, e) return 1 if args.name is None: name = derived_branch_name(args.script) else: name = args.name commit_pending = {'auto': None, 'yes': True, 'no': False}[ args.commit_pending] overwrite = False try: hoster = get_hoster(main_branch) except UnsupportedHoster as e: if args.mode != 'push': raise # We can't figure out what branch to resume from when there's no hoster # that can tell us. resume_branch = None existing_proposal = None warning('Unsupported hoster (%s), will attempt to push to %s', e, main_branch.user_url) else: (resume_branch, overwrite, existing_proposal) = ( find_existing_proposed(main_branch, hoster, name)) if args.refresh: resume_branch = None with Workspace(main_branch, resume_branch=resume_branch) as ws: try: description = script_runner( ws.local_tree, args.script, commit_pending) except ScriptMadeNoChanges: show_error('Script did not make any changes.') return 1 def get_description(existing_proposal): if description is not None: return description if existing_proposal is not None: return existing_proposal.get_description() raise ValueError("No description available") enable_tag_pushing(ws.local_tree.branch) try: publish_result = publish_changes( ws, args.mode, name, get_proposal_description=get_description, dry_run=args.dry_run, hoster=hoster, labels=args.label, overwrite_existing=overwrite, existing_proposal=existing_proposal) except UnsupportedHoster as e: show_error('No known supported hoster for %s. Run \'svp login\'?', e.branch.user_url) return 1 except _mod_propose.HosterLoginRequired as e: show_error( 'Credentials for hosting site at %r missing. ' 'Run \'svp login\'?', e.hoster.base_url) return 1 if publish_result.proposal: if publish_result.is_new: note('Merge proposal created.') else: note('Merge proposal updated.') if publish_result.proposal.url: note('URL: %s', publish_result.proposal.url) note('Description: %s', publish_result.proposal.get_description()) if args.diff: ws.show_diff(sys.stdout.buffer) if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() setup_parser(parser) args = parser.parse_args() main(args) silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/0000755000000000000000000000000013553676177021304 5ustar 00000000000000silver-platter-0.2.0+git20191022.7591492/silver_platter/utils.py0000644000000000000000000002103713553676177021657 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 os import shutil import socket import subprocess from breezy import errors, osutils from breezy.branch import ( Branch, BranchWriteLockResult, ) from breezy.controldir import ControlDir from breezy.lock import _RelockDebugMixin, LogicalLockResult from breezy.revision import NULL_REVISION import breezy.transport from breezy.bzr import RemoteBzrProber from breezy.git import RemoteGitProber def create_temp_sprout(branch, additional_colocated_branches=None, dir=None, path=None): """Create a temporary sprout of a branch. This attempts to fetch the least amount of history as possible. """ if path is None: td = osutils.mkdtemp(dir=dir) else: td = path os.mkdir(path) def destroy(): shutil.rmtree(td) # Only use stacking if the remote repository supports chks because of # https://bugs.launchpad.net/bzr/+bug/375013 use_stacking = ( branch._format.supports_stacking() and branch.repository._format.supports_chks) try: # preserve whatever source format we have. to_dir = branch.controldir.sprout( td, None, create_tree_if_local=True, source_branch=branch, stacked=use_stacking) # TODO(jelmer): Fetch these during the initial clone for branch_name in additional_colocated_branches or []: try: add_branch = branch.controldir.open_branch( name=branch_name) except (errors.NotBranchError, errors.NoColocatedBranchSupport): pass else: local_add_branch = to_dir.create_branch( name=branch_name) add_branch.push(local_add_branch) assert (add_branch.last_revision() == local_add_branch.last_revision()) return to_dir.open_workingtree(), destroy except BaseException as e: destroy() raise e class TemporarySprout(object): """Create a temporary sprout of a branch. This attempts to fetch the least amount of history as possible. """ def __init__(self, branch, additional_colocated_branches=None, dir=None): self.branch = branch self.additional_colocated_branches = additional_colocated_branches self.dir = dir def __enter__(self): tree, self._destroy = create_temp_sprout( self.branch, additional_colocated_branches=self.additional_colocated_branches, dir=self.dir) return tree def __exit__(self, exc_type, exc_val, exc_tb): self._destroy() return False class PreCheckFailed(Exception): """The post check failed.""" def run_pre_check(tree, script): """Run a script ahead of making any changes to a tree. Args: tree: The working tree to operate in script: Command to run Raises: PreCheckFailed: If the pre-check failed """ if not script: return try: subprocess.check_call(script, shell=True, cwd=tree.basedir) except subprocess.CalledProcessError: raise PreCheckFailed() class PostCheckFailed(Exception): """The post check failed.""" def run_post_check(tree, script, since_revid): """Run a script after making any changes to a tree. Args: tree: The working tree to operate in script: Command to run since_revid: Revision id since which changes were made Raises: PostCheckFailed: If the pre-check failed """ if not script: return try: subprocess.check_call( script, shell=True, cwd=tree.basedir, env={'SINCE_REVID': since_revid}) except subprocess.CalledProcessError: raise PostCheckFailed() class BranchUnavailable(Exception): """Opening branch failed.""" def __init__(self, url, description): self.url = url self.description = description def __str__(self): return self.description class BranchMissing(Exception): """Branch did not exist.""" def __init__(self, url, description): self.url = url self.description = description def __str__(self): return self.description def open_branch(url, possible_transports=None, vcs_type=None): """Open a branch by URL.""" try: transport = breezy.transport.get_transport( url, possible_transports=possible_transports) if vcs_type is None: probers = None elif vcs_type.lower() == 'bzr': probers = [RemoteBzrProber] elif vcs_type.lower() == 'git': probers = [RemoteGitProber] else: probers = None dir = ControlDir.open_from_transport(transport, probers) return dir.open_branch() except socket.error as e: raise BranchUnavailable(url, 'Socket error: %s' % e) except errors.NotBranchError as e: raise BranchMissing(url, 'Branch does not exist: %s' % e) except errors.UnsupportedProtocol as e: raise BranchUnavailable(url, str(e)) except errors.ConnectionError as e: raise BranchUnavailable(url, str(e)) except errors.PermissionDenied as e: raise BranchUnavailable(url, str(e)) except errors.InvalidHttpResponse as e: raise BranchUnavailable(url, str(e)) except errors.TransportError as e: raise BranchUnavailable(url, str(e)) except breezy.transport.UnusableRedirect as e: raise BranchUnavailable(url, str(e)) # TODO(jelmer): This should be in breezy class MinimalMemoryBranch(Branch, _RelockDebugMixin): def __init__(self, repository, last_revision_info, tags): from breezy.tag import DisabledTags, MemoryTags self.repository = repository self._last_revision_info = last_revision_info self._revision_history_cache = None if tags is not None: self.tags = MemoryTags(tags) else: self.tags = DisabledTags(self) self._partial_revision_history_cache = [] self._last_revision_info_cache = None self._revision_id_to_revno_cache = None self._partial_revision_id_to_revno_cache = {} self._partial_revision_history_cache = [] def lock_read(self): self.repository.lock_read() return LogicalLockResult(self.unlock) def lock_write(self, token=None): self.repository.lock_write() return BranchWriteLockResult(self.unlock, None) def unlock(self): self.repository.unlock() def last_revision_info(self): return self._last_revision_info def _gen_revision_history(self): """Generate the revision history from last revision """ last_revno, last_revision = self.last_revision_info() self._extend_partial_history() return list(reversed(self._partial_revision_history_cache)) def get_rev_id(self, revno, history=None): """Find the revision id of the specified revno.""" with self.lock_read(): if revno == 0: return NULL_REVISION last_revno, last_revid = self.last_revision_info() if revno == last_revno: return last_revid if last_revno is None: self._extend_partial_history() return self._partial_revision_history_cache[ len(self._partial_revision_history_cache) - revno] else: if revno <= 0 or revno > last_revno: raise errors.NoSuchRevision(self, revno) distance_from_last = last_revno - revno if len(self._partial_revision_history_cache) <= \ distance_from_last: self._extend_partial_history(distance_from_last) return self._partial_revision_history_cache[distance_from_last] silver-platter-0.2.0+git20191022.7591492/silver_platter/debian/__init__.py0000644000000000000000000001541213553676177023500 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 __future__ import absolute_import from debian.deb822 import Deb822 from debian.changelog import Version import itertools from breezy import version_info as breezy_version from breezy.plugins.debian.cmds import cmd_builddeb from breezy.plugins.debian.directory import ( source_package_vcs_url, vcs_field_to_bzr_url_converters, ) from breezy.urlutils import InvalidURL from breezy.plugins.debian.changelog import ( changelog_commit_message, ) try: from breezy.plugins.debian.builder import BuildFailedError except ImportError: from breezy.plugins.debian.errors import BuildFailedError from breezy.plugins.debian.errors import ( MissingUpstreamTarball, ) from .. import proposal as _mod_proposal from ..utils import ( open_branch, ) __all__ = [ 'get_source_package', 'should_update_changelog', 'source_package_vcs_url', 'build', 'BuildFailedError', 'MissingUpstreamTarball', 'vcs_field_to_bzr_url_converters', ] DEFAULT_BUILDER = 'sbuild --no-clean-source' class NoSuchPackage(Exception): """No such package.""" def build(tree, builder=None, result_dir=None): """Build a debian package in a directory. Args: tree: Working tree builder: Builder command (e.g. 'sbuild', 'debuild') result_dir: Directory to copy results to """ if builder is None: builder = DEFAULT_BUILDER # TODO(jelmer): Refactor brz-debian so it's not necessary # to call out to cmd_builddeb, but to lower-level # functions instead. cmd_builddeb().run([tree.basedir], builder=builder, result_dir=result_dir) def get_source_package(name): """Get source package metadata. Args: name: Name of the source package Returns: A `Deb822` object """ import apt_pkg apt_pkg.init() sources = apt_pkg.SourceRecords() by_version = {} while sources.lookup(name): by_version[sources.version] = sources.record if len(by_version) == 0: raise NoSuchPackage(name) # Try the latest version version = sorted(by_version, key=Version)[-1] return Deb822(by_version[version]) def _changelog_stats(branch, history): mixed = 0 changelog_only = 0 other_only = 0 dch_references = 0 with branch.lock_read(): graph = branch.repository.get_graph() revids = list(itertools.islice( graph.iter_lefthand_ancestry(branch.last_revision()), history)) revs = [] for revid, rev in branch.repository.iter_revisions(revids): if rev is None: # Ghost continue if 'Git-Dch: ' in rev.message: dch_references += 1 revs.append(rev) for delta in branch.repository.get_deltas_for_revisions(revs): if breezy_version >= (3, 1): filenames = set( [a.path[1] for a in delta.added] + [r.path[0] for r in delta.removed] + [r.path[0] for r in delta.renamed] + [r.path[1] for r in delta.renamed] + [m.path[0] for m in delta.modified]) else: filenames = set([a[0] for a in delta.added] + [r[0] for r in delta.removed] + [r[1] for r in delta.renamed] + [m[0] for m in delta.modified]) if not set([f for f in filenames if f.startswith('debian/')]): continue if 'debian/changelog' in filenames: if len(filenames) > 1: mixed += 1 else: changelog_only += 1 else: other_only += 1 return (changelog_only, other_only, mixed, dch_references) def should_update_changelog(branch, history=200): """Guess whether the changelog should be updated manually. Args: branch: A branch object history: Number of revisions back to analyze Returns: boolean indicating whether changelog should be updated """ # Two indications this branch may be doing changelog entries at # release time: # - "Git-Dch: " is used in the commit messages # - The vast majority of lines in changelog get added in # commits that only touch the changelog (changelog_only, other_only, mixed, dch_references) = _changelog_stats( branch, history) if dch_references: return False if changelog_only > mixed: # Is this a reasonable threshold? return False # Assume yes return True def convert_debian_vcs_url(vcs_type, vcs_url): converters = dict(vcs_field_to_bzr_url_converters) try: return converters[vcs_type](vcs_url) except KeyError: raise ValueError('unknown vcs %s' % vcs_type) except InvalidURL as e: raise ValueError('invalid URL: %s' % e) def open_packaging_branch(location, possible_transports=None, vcs_type=None): """Open a packaging branch from a location string. location can either be a package name or a full URL """ if '/' not in location: pkg_source = get_source_package(location) vcs_type, location = source_package_vcs_url(pkg_source) return open_branch( location, possible_transports=possible_transports, vcs_type=vcs_type) class Workspace(_mod_proposal.Workspace): def __init__(self, main_branch, *args, **kwargs): if getattr(main_branch.repository, '_git', None): kwargs['additional_colocated_branches'] = ( kwargs.get('additional_colocated_branches', []) + ["pristine-tar", "upstream"]) super(Workspace, self).__init__(main_branch, *args, **kwargs) def build(self, builder=None, result_dir=None): return build(tree=self.local_tree, builder=builder, result_dir=result_dir) def debcommit(tree, committer=None, paths=None): tree.commit( committer=committer, message=changelog_commit_message(tree, tree.basis_tree()), specific_files=paths) silver-platter-0.2.0+git20191022.7591492/silver_platter/debian/__main__.py0000644000000000000000000000432313553676177023460 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 __future__ import absolute_import import silver_platter # noqa: F401 import argparse import sys def main(argv=None): from . import ( lintian as debian_lintian, run as debian_run, upstream as debian_upstream, uploader as debian_uploader, ) from ..__main__ import subcommands as main_subcommands subcommands = [ ('run', debian_run.setup_parser, debian_run.main), ('new-upstream', debian_upstream.setup_parser, debian_upstream.main), ('upload-pending', debian_uploader.setup_parser, debian_uploader.main), ('lintian-brush', debian_lintian.setup_parser, debian_lintian.main), ] for cmd in main_subcommands: if cmd[0] not in [subcmd[0] for subcmd in subcommands]: subcommands.append(cmd) parser = argparse.ArgumentParser(prog='debian-svp') parser.add_argument( '--version', action='version', version='%(prog)s ' + silver_platter.version_string) subparsers = parser.add_subparsers(dest='subcommand') callbacks = {} for name, setup_parser, run in subcommands: subparser = subparsers.add_parser(name) if setup_parser is not None: setup_parser(subparser) callbacks[name] = run args = parser.parse_args(argv) if args.subcommand is None: parser.print_usage() sys.exit(1) return callbacks[args.subcommand](args) if __name__ == '__main__': sys.exit(main()) silver-platter-0.2.0+git20191022.7591492/silver_platter/debian/lintian.py0000644000000000000000000003607213553676177023404 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 __future__ import absolute_import import sys from breezy.errors import BzrError from . import ( open_packaging_branch, should_update_changelog, DEFAULT_BUILDER, NoSuchPackage, ) from ..proposal import ( get_hoster, find_existing_proposed, enable_tag_pushing, publish_changes, SUPPORTED_MODES, iter_conflicted, ) from ..utils import ( run_pre_check, run_post_check, PostCheckFailed, BranchMissing, BranchUnavailable, ) from lintian_brush import ( available_lintian_fixers, run_lintian_fixers, DEFAULT_MINIMUM_CERTAINTY, ) __all__ = [ 'available_lintian_fixers', ] DEFAULT_ADDON_FIXERS = [ 'file-contains-trailing-whitespace', 'package-uses-old-debhelper-compat-version', ] BRANCH_NAME = "lintian-fixes" class UnknownFixer(BzrError): """The specified fixer is unknown.""" _fmt = "No such fixer: %s." def __init__(self, fixer): super(UnknownFixer, self).__init__(fixer=fixer) def parse_mp_description(description): """Parse a merge proposal description. Args: description: The description to parse Returns: list of one-line descriptions of changes """ existing_lines = description.splitlines() if len(existing_lines) == 1: return existing_lines else: return [l[2:].rstrip('\n') for l in existing_lines if l.startswith('* ')] def create_mp_description(lines): """Create a merge proposal description. Args: lines: List of one-line descriptions of fixes Returns: A string with a merge proposal description """ if len(lines) > 1: mp_description = ["Fix some issues reported by lintian\n"] for line in lines: line = "* %s\n" % line if line not in mp_description: mp_description.append(line) else: mp_description = lines[0] return ''.join(mp_description) def update_proposal_description(existing_proposal, applied): if existing_proposal: existing_description = existing_proposal.get_description() existing_lines = parse_mp_description(existing_description) else: existing_lines = [] return create_mp_description( existing_lines + [l for r, l in applied]) def update_proposal_commit_message(existing_proposal, applied): existing_commit_message = getattr( existing_proposal, 'get_commit_message', lambda: None)() if existing_commit_message and not existing_commit_message.startswith( 'Fix lintian issues: '): # The commit message is something we haven't set - let's leave it # alone. return if existing_commit_message: existing_applied = existing_commit_message.split(':', 1)[1] else: existing_applied = [] return "Fix lintian issues: " + ( ', '.join(sorted(existing_applied + [l for r, l in applied]))) def has_nontrivial_changes(applied, propose_addon_only): tags = set() for result, unused_summary in applied: tags.update(result.fixed_lintian_tags) # Is there enough to create a new merge proposal? return bool(tags - set(propose_addon_only)) def setup_parser(parser): parser.add_argument("packages", nargs='*') parser.add_argument( "--fixers", help="Fixers to run.", type=str, action='append') parser.add_argument( "--dry-run", help="Create branches but don't push or propose anything.", action="store_true", default=False) parser.add_argument( '--propose-addon-only', help='Fixers that should be considered add-on-only.', type=str, action='append', default=DEFAULT_ADDON_FIXERS) parser.add_argument( '--pre-check', help='Command to run to check whether to process package.', type=str) parser.add_argument( '--post-check', help='Command to run to check package before pushing.', type=str) parser.add_argument( '--build-verify', help='Build package to verify it.', action='store_true') parser.add_argument( '--builder', default=DEFAULT_BUILDER, type=str, help='Build command to use when verifying build.') parser.add_argument( '--refresh', help='Discard old branch and apply fixers from scratch.', action='store_true') parser.add_argument( '--committer', help='Committer identity', type=str) parser.add_argument( '--mode', help='Mode for pushing', choices=SUPPORTED_MODES, default="propose", type=str) parser.add_argument( '--no-update-changelog', action="store_false", default=None, dest="update_changelog", help="do not update the changelog") parser.add_argument( '--update-changelog', action="store_true", dest="update_changelog", help="force updating of the changelog", default=None) parser.add_argument( '--diff', action="store_true", help="Output diff of created merge proposal.") parser.add_argument( '--build-target-dir', type=str, help=("Store built Debian files in specified directory " "(with --build-verify)")) parser.add_argument( '--overwrite', action='store_true', help='Overwrite existing branches.') parser.add_argument( '--fix-conflicted', action='store_true', help='Fix existing merge proposals that are conflicted.') def get_fixers(available_fixers, names=None, tags=None): """Get the set of fixers to try. Args: available_fixers: Dictionary mapping fixer names to objects names: Optional set of fixers to restrict to tags: Optional set of tags to restrict to Returns: List of fixer objects """ by_tag = {} by_name = {} for fixer in available_fixers: for tag in fixer.lintian_tags: by_tag[tag] = fixer by_name[fixer.name] = fixer # If it's unknown which fixers are relevant, just try all of them. if names: try: return [by_name[name] for name in names] except KeyError as e: raise UnknownFixer(e.args[0]) elif tags: return [by_tag[tag] for tag in tags] else: return by_name.values() def iter_packages(packages, overwrite_unrelated=False, refresh=False): from breezy.trace import note, warning from breezy.plugins.propose.propose import ( UnsupportedHoster, ) possible_transports = [] possible_hosters = [] for pkg in packages: note('Processing: %s', pkg) try: main_branch = open_packaging_branch( pkg, possible_transports=possible_transports) except NoSuchPackage: note('%s: no such package', pkg) continue except (BranchMissing, BranchUnavailable): note('%s: ignoring, socket error', pkg) continue overwrite = False try: hoster = get_hoster(main_branch, possible_hosters=possible_hosters) except UnsupportedHoster as e: if args.mode != 'push': raise # We can't figure out what branch to resume from when there's no # hoster that can tell us. resume_branch = None existing_proposal = None warning('Unsupported hoster (%s), will attempt to push to %s', e, main_branch.user_url) hoster = None else: (resume_branch, overwrite, existing_proposal) = ( find_existing_proposed( main_branch, hoster, BRANCH_NAME, overwrite_unrelated=overwrite_unrelated)) if refresh: overwrite = True resume_branch = None yield (pkg, main_branch, resume_branch, hoster, existing_proposal, overwrite) def main(args): import distro_info import itertools import silver_platter # noqa: F401 from . import ( BuildFailedError, MissingUpstreamTarball, Workspace, ) from breezy import ( errors, ) from breezy.plugins.propose.propose import ( NoSuchProject, UnsupportedHoster, ) from breezy.trace import note ret = 0 try: fixers = get_fixers(available_lintian_fixers(), names=args.fixers) except UnknownFixer as e: note('Unknown fixer: %s', e.fixer) return 1 debian_info = distro_info.DebianDistroInfo() package_iter = iter_packages(args.packages, args.overwrite, args.refresh) if args.fix_conflicted: package_iter = itertools.chain( package_iter, iter_conflicted(BRANCH_NAME)) for (pkg, main_branch, resume_branch, hoster, existing_proposal, overwrite) in package_iter: with Workspace(main_branch, resume_branch=resume_branch) as ws: with ws.local_tree.lock_write(): if ws.refreshed: overwrite = True run_pre_check(ws.local_tree, args.pre_check) if args.update_changelog is None: update_changelog = should_update_changelog( ws.local_tree.branch) else: update_changelog = args.update_changelog compat_release = None allow_reformatting = None minimum_certainty = None try: from lintian_brush.config import Config except ImportError: # lintian-brush < 0.30 pass else: try: cfg = Config.from_workingtree(ws.local_tree, '') except FileNotFoundError: pass else: compat_release = cfg.compat_release() if compat_release: compat_release = debian_info.codename( compat_release, default=compat_release) allow_reformatting = cfg.allow_reformatting() minimum_certainty = cfg.minimum_certainty() if compat_release is None: compat_release = debian_info.stable() if allow_reformatting is None: allow_reformatting = False if minimum_certainty is None: minimum_certainty = DEFAULT_MINIMUM_CERTAINTY applied, failed = run_lintian_fixers( ws.local_tree, fixers, committer=args.committer, update_changelog=update_changelog, compat_release=compat_release, allow_reformatting=allow_reformatting, minimum_certainty=minimum_certainty) if failed: note('%s: some fixers failed to run: %r', pkg, set(failed)) if not applied: note('%s: no fixers to apply', pkg) continue try: run_post_check(ws.local_tree, args.post_check, ws.orig_revid) except PostCheckFailed as e: note('%s: %s', pkg, e) continue if args.build_verify: try: ws.build(builder=args.builder, result_dir=args.build_target_dir) except BuildFailedError: note('%s: build failed', pkg) ret = 1 continue except MissingUpstreamTarball: note('%s: unable to find upstream source', pkg) ret = 1 continue enable_tag_pushing(ws.local_tree.branch) def get_proposal_description(existing_proposal): return update_proposal_description( existing_proposal, applied) def get_proposal_commit_message(existing_proposal): return update_proposal_commit_message( existing_proposal, applied) if not has_nontrivial_changes(applied, args.propose_addon_only): note('%s: only add-on fixers found', pkg) allow_create_proposal = False else: allow_create_proposal = True try: publish_result = publish_changes( ws, args.mode, BRANCH_NAME, get_proposal_description=get_proposal_description, get_proposal_commit_message=get_proposal_commit_message, dry_run=args.dry_run, hoster=hoster, allow_create_proposal=allow_create_proposal, overwrite_existing=overwrite, existing_proposal=existing_proposal) except UnsupportedHoster: note('%s: Hoster unsupported', pkg) ret = 1 continue except NoSuchProject as e: note('%s: project %s was not found', pkg, e.project) ret = 1 continue except errors.PermissionDenied as e: note('%s: %s', pkg, e) ret = 1 continue except errors.DivergedBranches: note('%s: a branch exists. Use --overwrite to discard it.', pkg) ret = 1 continue if publish_result.proposal: proposal = publish_result.proposal tags = set() for brush_result, unused_summary in applied: tags.update(brush_result.fixed_lintian_tags) if publish_result.is_new: note('%s: Proposed fixes %r: %s', pkg, tags, proposal.url) elif tags: note('%s: Updated proposal %s with fixes %r', pkg, proposal.url, tags) else: note('%s: No new fixes for proposal %s', pkg, proposal.url) if args.diff: ws.show_diff(sys.stdout.buffer) return ret if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(prog='propose-lintian-fixes') setup_parser(parser) args = parser.parse_args() main(args) silver-platter-0.2.0+git20191022.7591492/silver_platter/debian/run.py0000644000000000000000000001320613553676177022544 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 """Support for updating with a script.""" import sys from breezy.trace import warning from ..proposal import ( enable_tag_pushing, find_existing_proposed, publish_changes, get_hoster, UnsupportedHoster, SUPPORTED_MODES, ) from ..run import ( ScriptMadeNoChanges, derived_branch_name, script_runner, ) from . import ( open_packaging_branch, Workspace, DEFAULT_BUILDER, ) def setup_parser(parser): parser.add_argument('script', help='Path to script to run.', type=str) parser.add_argument( 'package', help='Package name or URL of branch to work on.', type=str) parser.add_argument('--refresh', action="store_true", help='Refresh branch (discard current branch) and ' 'create from scratch') parser.add_argument('--label', type=str, help='Label to attach', action="append", default=[]) parser.add_argument('--name', type=str, help='Proposed branch name', default=None) parser.add_argument('--diff', action="store_true", help="Show diff of generated changes.") parser.add_argument( '--mode', help='Mode for pushing', choices=SUPPORTED_MODES, default="propose", type=str) parser.add_argument( "--dry-run", help="Create branches but don't push or propose anything.", action="store_true", default=False) parser.add_argument( '--build-verify', help='Build package to verify it.', action='store_true') parser.add_argument( '--builder', type=str, default=DEFAULT_BUILDER, help='Build command to run.') parser.add_argument( '--build-target-dir', type=str, help=("Store built Debian files in specified directory " "(with --build-verify)")) parser.add_argument( '--commit-pending', help='Commit pending changes after script.', choices=['yes', 'no', 'auto'], default='auto', type=str) def main(args): from breezy.plugins.propose import propose as _mod_propose from breezy.trace import note, show_error main_branch = open_packaging_branch(args.package) if args.name is None: name = derived_branch_name(args.script) else: name = args.name commit_pending = {'auto': None, 'yes': True, 'no': False}[ args.commit_pending] overwrite = False try: hoster = get_hoster(main_branch) except UnsupportedHoster as e: if args.mode != 'push': raise # We can't figure out what branch to resume from when there's no hoster # that can tell us. resume_branch = None existing_proposal = None warning('Unsupported hoster (%s), will attempt to push to %s', e, main_branch.user_url) else: (resume_branch, overwrite, existing_proposal) = ( find_existing_proposed(main_branch, hoster, name)) if args.refresh: resume_branch = None with Workspace(main_branch, resume_branch=resume_branch) as ws: try: description = script_runner( ws.local_tree, args.script, commit_pending) except ScriptMadeNoChanges: show_error('Script did not make any changes.') return 1 if args.build_verify: ws.build(builder=args.builder, result_dir=args.build_target_dir) def get_description(existing_proposal): if description is not None: return description if existing_proposal is not None: return existing_proposal.get_description() raise ValueError("No description available") enable_tag_pushing(ws.local_tree.branch) try: publish_result = publish_changes( ws, args.mode, name, get_proposal_description=get_description, dry_run=args.dry_run, hoster=hoster, labels=args.label, overwrite_existing=overwrite, existing_proposal=existing_proposal) except UnsupportedHoster as e: show_error('No known supported hoster for %s. Run \'svp login\'?', e.branch.user_url) return 1 except _mod_propose.HosterLoginRequired as e: show_error( 'Credentials for hosting site at %r missing. ' 'Run \'svp login\'?', e.hoster.base_url) return 1 if publish_result.proposal: if publish_result.is_new: note('Merge proposal created.') else: note('Merge proposal updated.') if publish_result.proposal.url: note('URL: %s', publish_result.proposal.url) note('Description: %s', publish_result.proposal.get_description()) if args.diff: ws.show_diff(sys.stdout.buffer) silver-platter-0.2.0+git20191022.7591492/silver_platter/debian/uploader.py0000644000000000000000000001643313553676177023560 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 """Support for uploading packages.""" import silver_platter # noqa: F401 import datetime import tempfile from breezy import gpg from breezy.plugins.debian.cmds import _build_helper from breezy.plugins.debian.import_dsc import ( DistributionBranch, ) from breezy.plugins.debian.release import ( release, ) from breezy.plugins.debian.util import ( changelog_find_previous_upload, dput_changes, find_changelog, debsign, ) from breezy.trace import note, show_error from . import ( get_source_package, source_package_vcs_url, Workspace, DEFAULT_BUILDER, ) from ..utils import ( open_branch, BranchUnavailable, BranchMissing, ) def check_revision(rev, min_commit_age): print("checking %r" % rev) # TODO(jelmer): deal with timezone commit_time = datetime.datetime.fromtimestamp(rev.timestamp) time_delta = datetime.datetime.now() - commit_time if time_delta.days < min_commit_age: raise Exception("Last commit is only %d days old" % time_delta.days) # TODO(jelmer): Allow tag to prevent automatic uploads def find_last_release_revid(branch, version): db = DistributionBranch(branch, None) return db.revid_of_version(version) def get_maintainer_keys(context): for key in context.keylist( source='/usr/share/keyrings/debian-keyring.gpg'): yield key.fpr for subkey in key.subkeys: yield subkey.keyid def prepare_upload_package( local_tree, pkg, last_uploaded_version, gpg_strategy, min_commit_age, builder): cl, top_level = find_changelog( local_tree, merge=False, max_blocks=None) if cl.version == last_uploaded_version: raise Exception( "nothing to upload, latest version is in archive: %s" % cl.version) previous_version_in_branch = changelog_find_previous_upload(cl) if last_uploaded_version > previous_version_in_branch: raise Exception( "last uploaded version more recent than previous " "version in branch: %r > %r" % ( last_uploaded_version, previous_version_in_branch)) print("Checking revisions since %s" % last_uploaded_version) with local_tree.lock_read(): last_release_revid = find_last_release_revid( local_tree.branch, last_uploaded_version) graph = local_tree.branch.repository.get_graph() revids = list(graph.iter_lefthand_ancestry( local_tree.branch.last_revision(), [last_release_revid])) if not revids: print("No pending changes") return if gpg_strategy: count, result, all_verifiables = gpg.bulk_verify_signatures( local_tree.branch.repository, revids, gpg_strategy) for revid, code, key in result: if code != gpg.SIGNATURE_VALID: raise Exception( "No valid GPG signature on %r: %d" % (revid, code)) for revid, rev in local_tree.branch.repository.iter_revisions( revids): check_revision(rev, min_commit_age) if cl.distributions != "UNRELEASED": raise Exception("Nothing left to release") release(local_tree) target_dir = tempfile.mkdtemp() target_changes = _build_helper( local_tree, local_tree.branch, target_dir, builder=builder) debsign(target_changes) return target_changes def setup_parser(parser): parser.add_argument("packages", nargs='*') parser.add_argument( '--acceptable-keys', help='List of acceptable GPG keys', action='append', default=[], type=str) parser.add_argument( '--no-gpg-verification', help='Do not verify GPG signatures', action='store_true') parser.add_argument( '--min-commit-age', help='Minimum age of the last commit, in days', type=int, default=0) parser.add_argument( '--builder', type=str, help='Build command', default=(DEFAULT_BUILDER + ' --source --source-only-changes ' '--debbuildopt=-v${LAST_VERSION}')) parser.add_argument( '--maintainer', type=str, action='append', help='Select all packages maintainer by specified maintainer.') def main(args): ret = 0 packages = [] if args.maintainer: import apt_pkg from email.utils import parseaddr apt_pkg.init() sources = apt_pkg.SourceRecords() while sources.step(): fullname, email = parseaddr(sources.maintainer) if email in args.maintainer: packages.append(sources.package) packages.extend(args.packages) # TODO(jelmer): Sort packages by last commit date; least recently changed # commits are more likely to be successful. note('Uploading packages: %s', ', '.join(packages)) for package in packages: # Can't use open_packaging_branch here, since we want to use pkg_source # later on. pkg_source = get_source_package(package) vcs_type, vcs_url = source_package_vcs_url(pkg_source) try: main_branch = open_branch(vcs_url, vcs_type=vcs_type) except (BranchUnavailable, BranchMissing) as e: show_error('%s: %s', vcs_url, e) ret = 1 continue with Workspace(main_branch) as ws: branch_config = ws.local_tree.branch.get_config_stack() if args.no_gpg_verification: gpg_strategy = None else: gpg_strategy = gpg.GPGStrategy(branch_config) if args.acceptable_keys: acceptable_keys = args.acceptable_keys else: acceptable_keys = list(get_maintainer_keys( gpg_strategy.context)) gpg_strategy.set_acceptable_keys(','.join(acceptable_keys)) target_changes = prepare_upload_package( ws.local_tree, pkg_source["Package"], pkg_source["Version"], gpg_strategy, args.min_commit_age, args.builder) ws.push(dry_run=args.dry_run) if not args.dry_run: dput_changes(target_changes) return ret if __name__ == '__main__': import argparse import sys parser = argparse.ArgumentParser(prog='upload-pending-commits') setup_parser(parser) # TODO(jelmer): Support requiring that autopkgtest is present and passing args = parser.parse_args() sys.exit(main(args)) silver-platter-0.2.0+git20191022.7591492/silver_platter/debian/upstream.py0000644000000000000000000005764113553676177023613 0ustar 00000000000000#!/usr/bin/python3 # Copyright (C) 2018 Jelmer Vernooij # # 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 """Support for merging new upstream versions.""" import silver_platter # noqa: F401 from debian.changelog import Version import re import subprocess import sys import tempfile from ..proposal import ( get_hoster, find_existing_proposed, publish_changes, UnsupportedHoster, SUPPORTED_MODES, ) from ..utils import ( open_branch, run_pre_check, BranchMissing, BranchUnavailable, ) from . import ( open_packaging_branch, NoSuchPackage, Workspace, DEFAULT_BUILDER, debcommit, ) from breezy.commit import ( PointlessCommit, ) from breezy.errors import ( FileExists, PointlessMerge, ) from breezy.plugins.debian.config import ( UpstreamMetadataSyntaxError ) from breezy.plugins.debian.errors import ( InconsistentSourceFormatError, MissingUpstreamTarball, PackageVersionNotPresent, UpstreamAlreadyImported, UpstreamBranchAlreadyMerged, UnparseableChangelog, ) from breezy.trace import note, show_error, warning from breezy.plugins.debian.merge_upstream import ( changelog_add_new_version, do_merge, get_tarballs, PreviousVersionTagMissing, ) from breezy.plugins.debian.upstream.pristinetar import ( PristineTarError, PristineTarSource, ) try: from breezy.plugins.quilt.quilt import ( QuiltError, QuiltPatches, ) except ImportError: from breezy.plugins.debian.quilt.quilt import ( QuiltError, QuiltPatches, ) from breezy.plugins.debian.util import ( debuild_config, guess_build_type, tree_contains_upstream_source, BUILD_TYPE_MERGE, BUILD_TYPE_NATIVE, find_changelog, MissingChangelogError, ) from breezy.plugins.debian.upstream import ( UScanSource, TarfileSource, UScanError, ) from breezy.plugins.debian.upstream.branch import ( UpstreamBranchSource, ) __all__ = [ 'PreviousVersionTagMissing', 'merge_upstream', 'InvalidFormatUpstreamVersion', 'MissingChangelogError', 'MissingUpstreamTarball', 'NewUpstreamMissing', 'UpstreamBranchUnavailable', 'UpstreamAlreadyMerged', 'UpstreamAlreadyImported', 'UpstreamMergeConflicted', 'QuiltError', 'UpstreamVersionMissingInUpstreamBranch', 'UpstreamBranchUnknown', 'PackageIsNative', 'UnparseableChangelog', 'UScanError', 'UpstreamMetadataSyntaxError', 'QuiltPatchPushFailure', ] class NewUpstreamMissing(Exception): """Unable to find upstream version to merge.""" class UpstreamBranchUnavailable(Exception): """Snapshot merging was requested by upstream branch is unavailable.""" def __init__(self, location, error): self.location = location self.error = error class UpstreamMergeConflicted(Exception): """The upstream merge resulted in conflicts.""" def __init__(self, upstream_version, conflicts): self.version = upstream_version self.conflicts = conflicts class UpstreamAlreadyMerged(Exception): """Upstream release (or later version) has already been merged.""" def __init__(self, upstream_version): self.version = upstream_version class UpstreamVersionMissingInUpstreamBranch(Exception): """The upstream version is missing in the upstream branch.""" def __init__(self, upstream_branch, upstream_version): self.branch = upstream_branch self.version = upstream_version class UpstreamBranchUnknown(Exception): """The location of the upstream branch is unknown.""" class PackageIsNative(Exception): """Unable to merge upstream version.""" def __init__(self, package, version): self.package = package self.version = version class InvalidFormatUpstreamVersion(Exception): """Invalid format upstream version string.""" def __init__(self, version, source): self.version = version self.source = source class QuiltPatchPushFailure(Exception): def __init__(self, patch_name, actual_error): self.patch_name = patch_name self.actual_error = actual_error RELEASE_BRANCH_NAME = "new-upstream-release" SNAPSHOT_BRANCH_NAME = "new-upstream-snapshot" ORIG_DIR = '..' DEFAULT_DISTRIBUTION = 'unstable' def check_quilt_patches_apply(local_tree): from lintian_brush import reset_tree # lintian-brush < 0.16. assert not local_tree.has_changes() if local_tree.has_filename('debian/patches/series'): patches = QuiltPatches(local_tree, 'debian/patches') patches.push_all() patches.pop_all() reset_tree(local_tree) def refresh_quilt_patches(local_tree, committer=None): patches = QuiltPatches(local_tree, 'debian/patches') patches.upgrade() for name in patches.unapplied(): try: patches.push(name, refresh=True) except QuiltError as e: lines = e.stdout.splitlines() m = re.match( 'Patch debian/patches/(.*) can be reverse-applied', lines[-1]) if m and getattr(patches, 'delete', None): assert m.group(1) == name patches.delete(name, remove=True) subprocess.check_call( ['dch', 'Drop patch %s, present upstream.' % name], cwd=local_tree.basedir) debcommit(local_tree, committer=committer, paths=[ 'debian/patches/series', 'debian/patches/' + name, 'debian/changelog']) else: raise QuiltPatchPushFailure(name, e) patches.pop_all() try: local_tree.commit( 'Refresh patches.', committer=committer, allow_pointless=False) except PointlessCommit: pass class MergeUpstreamResult(object): """Object representing the result of a merge_upstream operation.""" __slots__ = [ 'old_upstream_version', 'new_upstream_version', 'upstream_branch', 'upstream_branch_browse', 'upstream_revisions'] def __init__(self, old_upstream_version, new_upstream_version, upstream_branch, upstream_branch_browse, upstream_revisions): self.old_upstream_version = old_upstream_version self.new_upstream_version = new_upstream_version self.upstream_branch = upstream_branch self.upstream_branch_browse = upstream_branch_browse self.upstream_revisions = upstream_revisions def __tuple__(self): # Backwards compatibility return (self.old_upstream_version, self.new_upstream_version) def merge_upstream(tree, snapshot=False, location=None, new_upstream_version=None, force=False, distribution_name=DEFAULT_DISTRIBUTION, allow_ignore_upstream_branch=True, trust_package=False, committer=None): """Merge a new upstream version into a tree. Raises: InvalidFormatUpstreamVersion PreviousVersionTagMissing MissingChangelogError MissingUpstreamTarball NewUpstreamMissing UpstreamBranchUnavailable UpstreamAlreadyMerged UpstreamAlreadyImported UpstreamMergeConflicted QuiltError UpstreamVersionMissingInUpstreamBranch UpstreamBranchUnknown PackageIsNative InconsistentSourceFormatError UnparseableChangelog UScanError UpstreamMetadataSyntaxError Returns: MergeUpstreamResult object """ config = debuild_config(tree) (changelog, top_level) = find_changelog(tree, False, max_blocks=2) old_upstream_version = changelog.version.upstream_version package = changelog.package contains_upstream_source = tree_contains_upstream_source(tree) build_type = config.build_type if build_type is None: build_type = guess_build_type( tree, changelog.version, contains_upstream_source) need_upstream_tarball = (build_type != BUILD_TYPE_MERGE) if build_type == BUILD_TYPE_NATIVE: raise PackageIsNative(changelog.package, changelog.version) if config.upstream_branch is not None: from lintian_brush.vcs import sanitize_url as sanitize_vcs_url note("Using upstream branch %s (from configuration)", config.upstream_branch) # TODO(jelmer): Make brz-debian sanitize the URL? upstream_branch_location = sanitize_vcs_url(config.upstream_branch) upstream_branch_browse = getattr( config, 'upstream_branch_browse', None) else: from lintian_brush.upstream_metadata import ( guess_upstream_metadata, ) guessed_upstream_metadata = guess_upstream_metadata( tree.basedir, trust_package=trust_package) upstream_branch_location = guessed_upstream_metadata.get( 'Repository') upstream_branch_browse = guessed_upstream_metadata.get( 'Repository-Browse') if upstream_branch_location: note("Using upstream branch %s (guessed)", upstream_branch_location) if upstream_branch_location: try: upstream_branch = open_branch(upstream_branch_location) except (BranchUnavailable, BranchMissing) as e: if not snapshot and allow_ignore_upstream_branch: warning('Upstream branch %s inaccessible; ignoring. %s', upstream_branch_location, e) else: raise UpstreamBranchUnavailable(upstream_branch_location, e) upstream_branch = None upstream_branch_browse = None else: upstream_branch = None if upstream_branch is not None: upstream_branch_source = UpstreamBranchSource.from_branch( upstream_branch, config=config, local_dir=tree.controldir) else: upstream_branch_source = None if location is not None: try: primary_upstream_source = UpstreamBranchSource.from_branch( open_branch(location), config=config, local_dir=tree.controldir) except (BranchUnavailable, BranchMissing): primary_upstream_source = TarfileSource( location, new_upstream_version) else: if snapshot: if upstream_branch_source is None: raise UpstreamBranchUnknown() primary_upstream_source = upstream_branch_source else: primary_upstream_source = UScanSource(tree, top_level) if new_upstream_version is None and primary_upstream_source is not None: new_upstream_version = primary_upstream_source.get_latest_version( package, old_upstream_version) try: Version(new_upstream_version) except ValueError: raise InvalidFormatUpstreamVersion( new_upstream_version, primary_upstream_source) if new_upstream_version is None: raise NewUpstreamMissing() note("Using version string %s.", new_upstream_version) # Look up the revision id from the version string if upstream_branch_source is not None: try: upstream_revisions = upstream_branch_source.version_as_revisions( package, new_upstream_version) except PackageVersionNotPresent: if upstream_branch_source is primary_upstream_source: # The branch is our primary upstream source, so if it can't # find the version then there's nothing we can do. raise UpstreamVersionMissingInUpstreamBranch( upstream_branch, new_upstream_version) elif not allow_ignore_upstream_branch: raise UpstreamVersionMissingInUpstreamBranch( upstream_branch, new_upstream_version) else: warning( 'Upstream version %s is not in upstream branch %s. ' 'Not merging from upstream branch. ', new_upstream_version, upstream_branch) upstream_revisions = None upstream_branch_source = None else: upstream_revisions = None if need_upstream_tarball: with tempfile.TemporaryDirectory() as target_dir: try: locations = primary_upstream_source.fetch_tarballs( package, new_upstream_version, target_dir, components=[None]) except PackageVersionNotPresent: if upstream_revisions is not None: locations = upstream_branch_source.fetch_tarballs( package, new_upstream_version, target_dir, components=[None], revisions=upstream_revisions) else: raise try: tarball_filenames = get_tarballs( ORIG_DIR, tree, package, new_upstream_version, upstream_branch, upstream_revisions, locations) except FileExists as e: raise AssertionError( "The target file %s already exists, and is either " "different to the new upstream tarball, or they " "are of different formats. Either delete the target " "file, or use it as the argument to import." % e.path) try: conflicts = do_merge( tree, tarball_filenames, package, new_upstream_version, old_upstream_version, upstream_branch, upstream_revisions, merge_type=None, force=force, committer=committer) except UpstreamBranchAlreadyMerged: # TODO(jelmer): Perhaps reconcile these two exceptions? raise UpstreamAlreadyMerged(new_upstream_version) except UpstreamAlreadyImported: pristine_tar_source = PristineTarSource.from_tree( tree.branch, tree) try: conflicts = tree.merge_from_branch( pristine_tar_source.branch, to_revision=pristine_tar_source.version_as_revisions( package, new_upstream_version)[None]) except PointlessMerge: raise UpstreamAlreadyMerged(new_upstream_version) else: conflicts = 0 # Re-read changelog, since it may have been changed by the merge # from upstream. (changelog, top_level) = find_changelog(tree, False, max_blocks=2) old_upstream_version = changelog.version.upstream_version package = changelog.package if Version(old_upstream_version) >= Version(new_upstream_version): if conflicts: raise UpstreamMergeConflicted(old_upstream_version, conflicts) raise UpstreamAlreadyMerged(new_upstream_version) changelog_add_new_version( tree, new_upstream_version, distribution_name, changelog, package) if not need_upstream_tarball: note("An entry for the new upstream version has been " "added to the changelog.") else: if conflicts: raise UpstreamMergeConflicted(new_upstream_version, conflicts) debcommit(tree, committer=committer) return MergeUpstreamResult( old_upstream_version=old_upstream_version, new_upstream_version=new_upstream_version, upstream_branch=upstream_branch, upstream_branch_browse=upstream_branch_browse, upstream_revisions=upstream_revisions) def setup_parser(parser): import argparse parser.add_argument("packages", nargs='+') parser.add_argument( '--snapshot', help='Merge a new upstream snapshot rather than a release', action='store_true') parser.add_argument( '--no-build-verify', help='Do not build package to verify it.', dest='build_verify', action='store_false') parser.add_argument( '--builder', type=str, default=DEFAULT_BUILDER, help='Build command.') parser.add_argument( '--pre-check', help='Command to run to check whether to process package.', type=str) parser.add_argument( "--dry-run", help="Create branches but don't push or propose anything.", action="store_true", default=False) parser.add_argument( '--mode', help='Mode for pushing', choices=SUPPORTED_MODES, default="propose", type=str) parser.add_argument( '--build-target-dir', type=str, help=("Store built Debian files in specified directory " "(with --build-verify)")) parser.add_argument( '--diff', action="store_true", help="Output diff of created merge proposal.") parser.add_argument( '--refresh-patches', action="store_true", help="Refresh quilt patches after upstream merge.") parser.add_argument( '--trust-package', action='store_true', default=False, help=argparse.SUPPRESS) def main(args): possible_hosters = [] ret = 0 if args.snapshot: branch_name = SNAPSHOT_BRANCH_NAME else: branch_name = RELEASE_BRANCH_NAME for package in args.packages: try: main_branch = open_packaging_branch(package) except NoSuchPackage: show_error('No such package: %s', package) ret = 1 continue try: hoster = get_hoster(main_branch, possible_hosters=possible_hosters) except UnsupportedHoster as e: if args.mode != 'push': raise # We can't figure out what branch to resume from when there's no # hoster that can tell us. warning('Unsupported hoster (%s), will attempt to push to %s', e, main_branch.user_url) overwrite_existing = False else: (resume_branch, overwrite_existing, existing_proposal) = find_existing_proposed( main_branch, hoster, branch_name) with Workspace(main_branch) as ws, ws.local_tree.lock_write(): run_pre_check(ws.local_tree, args.pre_check) try: merge_upstream_result = merge_upstream( tree=ws.local_tree, snapshot=args.snapshot, trust_package=args.trust_package) except UpstreamAlreadyImported as e: show_error( 'Last upstream version %s already imported.', e.version) ret = 1 continue except NewUpstreamMissing: show_error('Unable to find new upstream for %s.', package) ret = 1 continue except UpstreamAlreadyMerged as e: show_error('Last upstream version %s already merged.', e.version) ret = 1 # Continue, since we may want to close the existing merge # proposal. build_verify = False refresh_patches = False except PreviousVersionTagMissing as e: show_error( 'Unable to find tag %s for previous upstream version %s.', e.tag_name, e.version) ret = 1 continue except InvalidFormatUpstreamVersion as e: show_error( '%r reported invalid format version string %s.', e.source, e.version) ret = 1 continue except PristineTarError as e: show_error('Pristine tar error: %s', e) ret = 1 continue except UpstreamBranchUnavailable as e: show_error('Upstream branch %s unavailable: %s. ', e.location, e.error) ret = 1 continue except UpstreamBranchUnknown: show_error( 'Upstream branch location unknown. ' 'Set \'Repository\' field in debian/upstream/metadata?') ret = 1 continue except UpstreamMergeConflicted: show_error('Merging upstream resulted in conflicts.') ret = 1 continue except PackageIsNative as e: show_error( 'Package %s is native; unable to merge new upstream.', e.package) ret = 1 continue except InconsistentSourceFormatError as e: show_error('Inconsistencies in type of package: %s', e) ret = 1 continue except UScanError as e: show_error('UScan failed: %s', e) ret = 1 continue except UpstreamMetadataSyntaxError as e: show_error('Unable to parse %s', e.path) ret = 1 continue except MissingChangelogError as e: show_error('Missing changelog %s', e) ret = 1 continue except MissingUpstreamTarball as e: show_error('Missing upstream tarball: %s', e) ret = 1 continue else: note('Merged new upstream version %s (previous: %s)', merge_upstream_result.new_upstream_version, merge_upstream_result.old_upstream_version) build_verify = args.build_verify refresh_patches = args.refresh_patches if refresh_patches and \ ws.local_tree.has_filename('debian/patches/series'): note('Refresh quilt patches.') try: refresh_quilt_patches(ws.local_tree) except QuiltError as e: show_error('Quilt error while refreshing patches: %s', e) ret = 1 continue if build_verify: ws.build(builder=args.builder, result_dir=args.build_target_dir) def get_proposal_description(unused_proposal): return ("Merge new upstream release %s" % merge_upstream_result.new_upstream_version) publish_result = publish_changes( ws, args.mode, branch_name, get_proposal_description=get_proposal_description, get_proposal_commit_message=get_proposal_description, dry_run=args.dry_run, hoster=hoster, existing_proposal=existing_proposal, overwrite_existing=overwrite_existing) if publish_result.proposal: if publish_result.is_new: note('%s: Created new merge proposal %s.', package, publish_result.proposal.url) else: note('%s: Updated merge proposal %s.', package, publish_result.proposal.url) elif existing_proposal: note('%s: Closed merge proposal %s', package, existing_proposal.url) if args.diff: ws.show_diff(sys.stdout.buffer) return ret if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(prog='propose-new-upstream') setup_parser(parser) args = parser.parse_args() main(args) silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/__init__.py0000644000000000000000000000217313553676177023420 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 __future__ import absolute_import import unittest def test_suite(): names = [ 'debian', 'debian_lintian', 'proposal', 'run', 'utils', 'version', ] module_names = [__name__ + '.test_' + name for name in names] loader = unittest.TestLoader() return loader.loadTestsFromNames(module_names) silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/test_debian.py0000644000000000000000000001004513553676177024137 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 breezy.tests import TestCaseWithTransport from ..debian import ( should_update_changelog, ) def make_changelog(entries): return ("""\ lintian-brush (0.1) UNRELEASED; urgency=medium """ + ''.join([" * %s\n" % entry for entry in entries]) + """ -- Jelmer Vernooij Sat, 13 Oct 2018 11:21:39 +0100 """).encode('utf-8') class ShouldUpdateChangelogTests(TestCaseWithTransport): def test_empty(self): branch = self.make_branch('.') self.assertTrue(should_update_changelog(branch)) def test_update_with_change(self): builder = self.make_branch_builder('.') builder.start_series() builder.build_snapshot(None, [ ('add', ('', None, 'directory', '')), ('add', ('upstream', None, 'file', b'upstream')), ('add', ('debian/', None, 'directory', '')), ('add', ('debian/changelog', None, 'file', make_changelog(['initial release']))), ('add', ('debian/control', None, 'file', b'initial'))], message='Initial\n') changelog_entries = ['initial release'] for i in range(20): builder.build_snapshot(None, [ ('modify', ('upstream', b'upstream %d' % i))], message='Upstream') changelog_entries.append('next entry %d' % i) builder.build_snapshot(None, [ ('modify', ('debian/changelog', make_changelog(changelog_entries))), ('modify', ('debian/control', b'next %d' % i))], message='Next') builder.finish_series() branch = builder.get_branch() self.assertTrue(should_update_changelog(branch)) def test_changelog_updated_separately(self): builder = self.make_branch_builder('.') builder.start_series() builder.build_snapshot(None, [ ('add', ('', None, 'directory', '')), ('add', ('debian/', None, 'directory', '')), ('add', ('debian/changelog', None, 'file', make_changelog(['initial release']))), ('add', ('debian/control', None, 'file', b'initial'))], message='Initial\n') changelog_entries = ['initial release'] for i in range(20): changelog_entries.append('next entry %d' % i) builder.build_snapshot(None, [ ('modify', ('debian/control', b'next %d' % i))], message='Next\n') builder.build_snapshot(None, [ ('modify', ('debian/changelog', make_changelog(changelog_entries)))]) changelog_entries.append('final entry') builder.build_snapshot(None, [ ('modify', ('debian/control', b'more'))], message='Next\n') builder.build_snapshot(None, [ ('modify', ('debian/changelog', make_changelog(changelog_entries)))]) builder.finish_series() branch = builder.get_branch() self.assertFalse(should_update_changelog(branch)) def test_has_dch_in_messages(self): builder = self.make_branch_builder('.') builder.build_snapshot(None, [ ('add', ('', None, 'directory', ''))], message='Git-Dch: ignore\n') branch = builder.get_branch() self.assertFalse(should_update_changelog(branch)) silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/test_debian_lintian.py0000644000000000000000000000443213553676177025660 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2018 Jelmer Vernooij # # 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 unittest from ..debian.lintian import ( parse_mp_description, create_mp_description, get_fixers, UnknownFixer, ) class ParseMPDescriptionTests(unittest.TestCase): def test_single_line(self): self.assertEqual(['some change'], parse_mp_description('some change')) def test_multiple_lines(self): self.assertEqual( ['some change', 'some other change'], parse_mp_description("""Lintian fixes: * some change * some other change """)) class CreateMPDescription(unittest.TestCase): def test_single_line(self): self.assertEqual("some change", create_mp_description(['some change'])) def test_multiple_lines(self): self.assertEqual("""\ Fix some issues reported by lintian * some change * some other change """, create_mp_description(['some change', 'some other change'])) class GetFixersTests(unittest.TestCase): def setUp(self): super(GetFixersTests, self).setUp() from lintian_brush import Fixer self.fixers = [Fixer('foo', ['atag'])] def test_get_all(self): self.assertEqual([self.fixers[0]], list(get_fixers(self.fixers))) def test_get_specified(self): self.assertEqual( [self.fixers[0]], list(get_fixers(self.fixers, names=['foo']))) def test_get_specified_tag(self): self.assertEqual( [self.fixers[0]], list(get_fixers(self.fixers, tags=['atag']))) def test_get_unknown(self): self.assertRaises(UnknownFixer, get_fixers, self.fixers, names=['bar']) silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/test_proposal.py0000644000000000000000000001532213553676177024557 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2019 Jelmer Vernooij # # 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 io import BytesIO import os from breezy.tests import TestCaseWithTransport from ..proposal import ( EmptyMergeProposal, check_proposal_diff, push_result, Workspace, ) class PushResultTests(TestCaseWithTransport): def test_simple(self): target = self.make_branch('target') source = self.make_branch_and_tree('source') revid = source.commit('Some change') push_result(source.branch, target) self.assertEqual(target.last_revision(), revid) class WorkspaceTests(TestCaseWithTransport): def test_simple(self): b = self.make_branch('target') with Workspace(b, dir=self.test_dir) as ws: self.assertIs(ws.resume_branch, None) self.assertFalse(ws.changes_since_main()) self.assertFalse(ws.changes_since_resume()) ws.local_tree.commit('foo') self.assertTrue(ws.changes_since_main()) self.assertTrue(ws.changes_since_main()) def test_with_resume(self): b = self.make_branch_and_tree('target') c = b.controldir.sprout('resume').open_workingtree() c.commit('some change') with Workspace(b.branch, resume_branch=c.branch, dir=self.test_dir) as ws: self.assertEqual( ws.local_tree.branch.last_revision(), c.branch.last_revision()) self.assertIs(ws.resume_branch, c.branch) self.assertTrue(ws.changes_since_main()) self.assertFalse(ws.changes_since_resume()) ws.local_tree.commit('foo') self.assertTrue(ws.changes_since_main()) self.assertTrue(ws.changes_since_resume()) def test_with_resume_conflicting(self): b = self.make_branch_and_tree('target') self.build_tree_contents([('target/foo', 'somecontents\n')]) b.add(['foo']) b.commit('initial') c = b.controldir.sprout('resume').open_workingtree() self.build_tree_contents([('target/foo', 'new contents in main\n')]) b.commit('add conflict in main') self.build_tree_contents([('resume/foo', 'new contents in resume\n')]) c.commit('add conflict in resume') with Workspace(b.branch, resume_branch=c.branch, dir=self.test_dir) as ws: self.assertIs(ws.resume_branch, None) self.assertEqual( b.branch.last_revision(), ws.local_tree.branch.last_revision()) self.assertFalse(ws.changes_since_main()) self.assertFalse(ws.changes_since_resume()) ws.local_tree.commit('foo') self.assertTrue(ws.changes_since_main()) self.assertTrue(ws.changes_since_resume()) def test_orig_tree(self): b = self.make_branch_and_tree('target') cid = b.commit('some change') with Workspace(b.branch, dir=self.test_dir) as ws: ws.local_tree.commit('blah') self.assertEqual(cid, ws.orig_tree().get_revision_id()) def test_show_diff(self): b = self.make_branch_and_tree('target') with Workspace(b.branch, dir=self.test_dir) as ws: self.build_tree_contents([ (os.path.join(ws.local_tree.basedir, 'foo'), 'some content\n')]) ws.local_tree.add(['foo']) ws.local_tree.commit('blah') self.assertTrue(ws.changes_since_main()) self.assertTrue(ws.changes_since_resume()) f = BytesIO() ws.show_diff(outf=f) self.assertContainsRe( f.getvalue().decode('utf-8'), '\\+some content') class CheckProposalDiffBase(object): def test_no_new_commits(self): orig = self.make_branch_and_tree('orig', format=self.format) self.build_tree(['orig/a']) orig.add(['a']) orig.commit('blah') proposal = orig.controldir.sprout('proposal').open_branch() self.addCleanup(proposal.lock_write().unlock) self.assertRaises( EmptyMergeProposal, check_proposal_diff, proposal, orig.branch) def test_no_op_commits(self): orig = self.make_branch_and_tree('orig', format=self.format) self.build_tree(['orig/a']) orig.add(['a']) orig.commit('blah') proposal = orig.controldir.sprout('proposal').open_workingtree() proposal.commit('another commit that is pointless') self.addCleanup(proposal.lock_write().unlock) self.assertRaises( EmptyMergeProposal, check_proposal_diff, proposal.branch, orig.branch) def test_indep(self): orig = self.make_branch_and_tree('orig', format=self.format) self.build_tree(['orig/a']) orig.add(['a']) orig.commit('blah') proposal = orig.controldir.sprout('proposal').open_workingtree() self.build_tree_contents([('orig/b', 'b'), ('orig/c', 'c')]) orig.add(['b', 'c']) orig.commit('independent') self.build_tree_contents([('proposal/b', 'b')]) if proposal.supports_setting_file_ids(): proposal.add(['b'], [orig.path2id('b')]) else: proposal.add(['b']) proposal.commit('not pointless') self.addCleanup(proposal.lock_write().unlock) self.assertRaises( EmptyMergeProposal, check_proposal_diff, proposal.branch, orig.branch) def test_changes(self): orig = self.make_branch_and_tree('orig', format=self.format) self.build_tree(['orig/a']) orig.add(['a']) orig.commit('blah') proposal = orig.controldir.sprout('proposal').open_workingtree() self.build_tree(['proposal/b']) proposal.add(['b']) proposal.commit('not pointless') self.addCleanup(proposal.lock_write().unlock) check_proposal_diff(proposal.branch, orig.branch) class CheckProposalDiffGitTests(TestCaseWithTransport, CheckProposalDiffBase): format = 'git' class CheckProposalDiffBzrTests(TestCaseWithTransport, CheckProposalDiffBase): format = 'bzr' silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/test_run.py0000644000000000000000000000536113553676177023526 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2019 Jelmer Vernooij # # 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 os from breezy.tests import ( TestCaseWithTransport, ) from ..run import ( ScriptMadeNoChanges, script_runner, ) class ScriptRunnerTests(TestCaseWithTransport): def setUp(self): super(ScriptRunnerTests, self).setUp() self.tree = self.make_branch_and_tree('tree') with open('foo.sh', 'w') as f: f.write("""\ #!/bin/sh echo Foo > bar echo "Some message" brz add --quiet bar """) os.chmod('foo.sh', 0o755) def test_simple_with_commit(self): description = script_runner( self.tree, os.path.abspath('foo.sh'), commit_pending=True) self.assertEqual(description, 'Some message\n') def test_simple_with_autocommit(self): description = script_runner(self.tree, os.path.abspath('foo.sh')) self.assertEqual( self.tree.branch.repository.get_revision( self.tree.last_revision()).message, "Some message\n") self.assertEqual(description, 'Some message\n') def test_simple_with_autocommit_and_script_commits(self): with open('foo.sh', 'w') as f: f.write("""\ #!/bin/sh echo Foo > bar echo "Some message" brz add --quiet bar brz commit --quiet -m blah """) os.chmod('foo.sh', 0o755) description = script_runner( self.tree, os.path.abspath('foo.sh')) self.assertEqual( self.tree.branch.repository.get_revision( self.tree.last_revision()).message, "blah") self.assertEqual(description, 'Some message\n') def test_simple_without_commit(self): self.assertRaises( ScriptMadeNoChanges, script_runner, self.tree, os.path.abspath('foo.sh'), commit_pending=False) def test_no_changes(self): with open('foo.sh', 'w') as f: f.write("""\ #!/bin/sh echo "Some message" """) self.assertRaises( ScriptMadeNoChanges, script_runner, self.tree, os.path.abspath('foo.sh'), commit_pending=True) silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/test_utils.py0000644000000000000000000000702713553676177024063 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2019 Jelmer Vernooij # # 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 breezy.tests import TestCaseWithTransport from ..utils import ( TemporarySprout, run_pre_check, run_post_check, PreCheckFailed, PostCheckFailed, ) class TemporarySproutTests(TestCaseWithTransport): def test_simple(self): builder = self.make_branch_builder('.') builder.start_series() orig_revid = builder.build_snapshot(None, [ ('add', ('', None, 'directory', '')), ('add', ('debian/', None, 'directory', '')), ('add', ('debian/control', None, 'file', b'initial'))], message='Initial\n') builder.finish_series() branch = builder.get_branch() with TemporarySprout(branch, dir=self.test_dir) as tree: self.assertNotEqual(branch.control_url, tree.branch.control_url) tree.commit('blah') # Commits in the temporary sprout don't affect the original branch. self.assertEqual(branch.last_revision(), orig_revid) def test_nonexistent_colocated(self): # Colocated branches that are specified but don't exist are ignored. builder = self.make_branch_builder('.') builder.start_series() orig_revid = builder.build_snapshot(None, [ ('add', ('', None, 'directory', '')), ('add', ('debian/', None, 'directory', ''))], message='Initial\n') builder.finish_series() branch = builder.get_branch() with TemporarySprout(branch, ['foo'], dir=self.test_dir) as tree: self.assertNotEqual(branch.control_url, tree.branch.control_url) tree.commit('blah') # Commits in the temporary sprout don't affect the original branch. self.assertEqual(branch.last_revision(), orig_revid) class RunPreCheckTests(TestCaseWithTransport): def test_none(self): tree = self.make_branch_and_tree('tree') self.assertIs(run_pre_check(tree, None), None) def test_false(self): tree = self.make_branch_and_tree('tree') self.assertRaises(PreCheckFailed, run_pre_check, tree, "/bin/false") def test_true(self): tree = self.make_branch_and_tree('tree') self.assertIs(run_pre_check(tree, "/bin/true"), None) class RunPostCheckTests(TestCaseWithTransport): def test_none(self): tree = self.make_branch_and_tree('tree') self.assertIs(run_post_check(tree, None, None), None) def test_false(self): tree = self.make_branch_and_tree('tree') cid = tree.commit('a') self.assertRaises( PostCheckFailed, run_post_check, tree, "/bin/false", since_revid=cid) def test_true(self): tree = self.make_branch_and_tree('tree') cid = tree.commit('a') self.assertIs(run_post_check(tree, "/bin/true", since_revid=cid), None) silver-platter-0.2.0+git20191022.7591492/silver_platter/tests/test_version.py0000644000000000000000000000301013553676177024374 0ustar 00000000000000#!/usr/bin/python # Copyright (C) 2019 Jelmer Vernooij # # 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 os import re from unittest import TestCase from silver_platter import version_string class VersionMatchTest(TestCase): def test_matches_setup_version(self): if not os.path.exists('setup.py'): self.skipTest( 'no setup.py available. ' 'Running outside of source tree?') # TODO(jelmer): Surely there's a better way of doing this? with open('setup.py', 'r') as f: for l in f: m = re.match(r'[ ]*version=["\'](.*)["\'],', l) if m: setup_version = m.group(1) break else: raise AssertionError('setup version not found') self.assertEqual(version_string, setup_version)