silver-platter_0.4.5.orig/.flake80000644000000000000000000000017014010641672013616 0ustar00[flake8] extend-ignore = E203, E266, E501, W293, W291 max-line-length = 88 max-complexity = 18 select = B,C,E,F,W,T4,B9 silver-platter_0.4.5.orig/.github/0000755000000000000000000000000013662112571014010 5ustar00silver-platter_0.4.5.orig/.gitignore0000644000000000000000000000013613657763333014454 0ustar00silver_platter.egg-info/* __pycache__ *~ .eggs/ silver_platter.dist-info *_pb2.py .mypy_cache silver-platter_0.4.5.orig/AUTHORS0000644000000000000000000000011214164061666013520 0ustar00Jelmer Vernooij Filippo Giunchedi silver-platter_0.4.5.orig/LICENSE0000644000000000000000000004325413360705112013457 0ustar00 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.4.5.orig/MANIFEST.in0000644000000000000000000000013213430055075014200 0ustar00include AUTHORS include LICENSE include README.rst include TODO recursive-include man *.1 silver-platter_0.4.5.orig/Makefile0000644000000000000000000000023614071532501014103 0ustar00all: check: flake8 mypy silver_platter/ python3 setup.py test typing: mypy silver_platter/ style: flake8 %_pb2.py: %.proto protoc --python_out=. $< silver-platter_0.4.5.orig/NEWS0000644000000000000000000000015014006107524013136 0ustar000.4.0 2020-02-01 * Factored out most of the mutators into separate scripts. See devnotes/mutators.rst silver-platter_0.4.5.orig/PKG-INFO0000644000000000000000000000207014164061666013552 0ustar00Metadata-Version: 2.1 Name: silver-platter Version: 0.4.5 Summary: Automatic merge proposal creeator Home-page: https://jelmer.uk/code/silver-platter Author: Jelmer Vernooij Author-email: jelmer@jelmer.uk License: GNU GPL v2 or later Project-URL: Bug Tracker, https://github.com/jelmer/silver-platter/issues Project-URL: Repository, https://jelmer.uk/code/silver-platter Project-URL: GitHub, https://github.com/jelmer/silver-platter Keywords: git bzr vcs github gitlab launchpad Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: POSIX Classifier: Topic :: Software Development :: Version Control Provides-Extra: debian License-File: LICENSE License-File: AUTHORS UNKNOWN silver-platter_0.4.5.orig/README.rst0000644000000000000000000001465514164061666014160 0ustar00Silver-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. Silver-Platter powers the Debian Janitor (https://janitor.debian.org/) and Kali Janitor (https://kali.janitor.org/). However, it is an independent project and can be used fine as a standalone tool. The UI is still a bit rough around the edges, I'd be grateful for any feedback from people using it - please file bugs in the issue tracker at https://github.com/jelmer/silver-platter/issues/new. 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 ./framwork.sh https://github.com/jelmer/dulwich where ``framwork.sh`` makes some modifications to a working copy and prints the commit message and 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" If you leave pending changes, silver-platter will automatically create a commit and use the output from the script as the commit message. Scripts also create their own commits if they prefer - this is especially useful if they would like to create multiple commits. Recipes ~~~~~~~ To make this process a little bit easier to repeat, recipe files can be used. For the example above, we could create a ``framwork.yaml`` with the following contents:: --- name: framwork command: ./framwork.sh mode: propose merge-request: commit-message: Fix a typo description: markdown: |- I spotted that we often mistype *framework* as *framwork*. To execute this recipe, run:: svp run --recipe=framwork.yaml https://github.com/jelmer/dulwich See `example.yaml` for an example recipe with plenty of comments. In addition, you can run a particular recipe over a set of repositories by specifying a candidate list. For example, if *candidates.yaml* looked like this:: --- - url: https://github.com/dulwich/dulwich - url: https://github.com/jelmer/xandikos then the following command would process each repository in turn:: svp run --recipe=framwork.yaml --candidates=candidates.yaml Supported hosters ~~~~~~~~~~~~~~~~~ At the moment, the following code hosters are supported: * `GitHub `_ * `Launchpad `_ * `GitLab `_ instances, such as Debian's `Salsa `_ or `GNOME's GitLab `_ 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. * *upload-pending*: Build and upload a package and push/propose the changelog updates. * *run*: Similar to *svp run* but specific to Debian packages: it ensures that the *upstream* and *pristine-tar* branches are available as well, can optionally update the changelog, and can test that the branch still builds. Some Debian-specific example recipes are provided in `examples/debian/`: * *lintian-fixes.yaml*: Run the `lintian-brush `_ command to fix common issues reported by `lintian `_. * *new-upstream-release.yaml*: Merge in a new upstream release. * *multi-arch-hints.yaml*: Apply multi-arch hints. * *orphan.yaml*: Mark a package as orphaned, update its Maintainer field and move it to the common Debian salsa group. * *rules-requires-root.yaml*: Mark a package as "Rules-Requires-Root: no" * *cme.yaml*: Run "cme fix dpkg", from the `cme package `_. *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``:: # Create merge proposal running lintian-brush against Samba debian-svp run --recipe=examples/lintian-brush.yaml samba # Upload pending changes for tdb debian-svp upload-pending tdb # Upload pending changes for any packages maintained by Jelmer, # querying vcswatch. debian-svp upload-pending --vcswatch --maintainer jelmer@debian.org # Import the latest upstream release for tdb, without testing # the build afterwards. debian-svp run --recipe=examples/debian/new-upstream-release.yaml \ --no-build-verify tdb # Apply multi-arch hints to tdb debian-svp run --recipe=examples/debian/multiarch-hints.yaml tdb The following environment variables are provided for Debian packages: * ``DEB_SOURCE``: the source package name * ``DEB_UPDATE_CHANGELOG``: indicates whether a changelog entry should be added. Either "leave" (leave alone) or "update" (update changelog). 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/ Exit status ~~~~~~~~~~~ ``svp run`` will exit 0 if no changes have been made, 1 if at least one repository has been changed and 2 in case of trouble. Python API ~~~~~~~~~~ Other than the command-line API, silver-platter also has a Python API. The core class is the ``Workspace`` context manager, which exists in two forms: * ``silver_platter.workspace.Workspace`` (for generic projects) * ``silver_platter.debian.Workspace`` (for Debian packages) An example, adding a new entry to a changelog file in the ``dulwich`` Debian package and creating a merge proposal with that change:: from silver_platter.debian import Workspace import subprocess with Workspace.from_apt_package(package="dulwich") as ws: subprocess.check_call(['dch', 'some change'], cwd=ws.path) ws.commit() # Behaves like debcommit ws.publish(mode='propose') silver-platter_0.4.5.orig/TODO0000644000000000000000000000076613646437374013166 0ustar00Backends (in Breezy upstream) - Support for Mercurial - Support for Svn 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 - support --mode=debian-bts silver-platter_0.4.5.orig/debian-svp0000755000000000000000000000163613757562016014444 0ustar00#!/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.4.5.orig/devnotes/0000755000000000000000000000000013410754533014300 5ustar00silver-platter_0.4.5.orig/example.yaml0000644000000000000000000000120314071532501014755 0ustar00--- # Name of the recipe; used e.g. as part of the branch name when # creating merge requests. name: example # Command to run, in a pristine clone of the specified branch. command: example --flag # Supported modes: # - propose: create merge request # - push: Push changes to main branch # - attempt-push: Try to push changes to main branch, but create a merge # request if there are not enough permissions # (optional, defaults to attempt-push) mode: propose merge-request: commit-message: Make a change labels: - some-label description: This field contains the body of the merge request, and supports jinja2 templating. silver-platter_0.4.5.orig/examples/0000755000000000000000000000000014071532501014260 5ustar00silver-platter_0.4.5.orig/helpers/0000755000000000000000000000000013410765270014113 5ustar00silver-platter_0.4.5.orig/man/0000755000000000000000000000000013427660237013231 5ustar00silver-platter_0.4.5.orig/releaser.conf0000644000000000000000000000101514164061666015124 0ustar00# How to use this file: # - Install silver-platter (apt install silver-platter / pip install silver-platter) # - Run: svp releaser git+ssh://git@github.com/jelmer/silver-platter # - Done! name: "silver-platter" timeout_days: 5 tag_name: "$VERSION" verify_command: "make check" update_version { path: "setup.py" match: "^ version=\'(.*)\',$" new_line: " version='$VERSION'," } update_version { path: "silver_platter/__init__.py" match: "^__version__ = \((.*)\)$" new_line: "__version__ = $TUPLED_VERSION" } silver-platter_0.4.5.orig/setup.cfg0000644000000000000000000000027114013316335014264 0ustar00[flake8] filename = *.py,svp exclude = *_pb2.py,.eggs banned-modules = dulwich = Don't use dulwich directly [mypy] ignore_missing_imports = True [egg_info] tag_build = tag_date = 0 silver-platter_0.4.5.orig/setup.py0000755000000000000000000000467614164061666014210 0ustar00#!/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 debian_deps = [ 'pyyaml', 'debmutate>=0.3', 'python_debian', 'distro-info', ] 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.4.5', 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.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', '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>=3.2.0', 'dulwich>=0.20.23', 'jinja2', ], extras_require={ 'debian': debian_deps, }, tests_require=['testtools'] + debian_deps, ) silver-platter_0.4.5.orig/silver_platter.egg-info/0000755000000000000000000000000014006107524017174 5ustar00silver-platter_0.4.5.orig/silver_platter/0000755000000000000000000000000013360607636015515 5ustar00silver-platter_0.4.5.orig/svp0000755000000000000000000000162613514623102013204 0ustar00#!/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.4.5.orig/.github/workflows/0000755000000000000000000000000013662112571016045 5ustar00silver-platter_0.4.5.orig/.github/workflows/pythonpackage.yml0000644000000000000000000000264714164061666021444 0ustar00name: Python package on: push: pull_request: schedule: - cron: '0 6 * * *' # Daily 6AM UTC build jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest] python-version: [3.7, 3.8, 3.9] fail-fast: false steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | sudo apt install devscripts cython3 bzr python -m pip install --upgrade pip pip install -U pip setuptools flake8 mypy debmutate pyyaml testtools mkdir $HOME/.config/breezy/plugins -p bzr branch lp:brz-debian ~/.config/breezy/plugins/debian pip install -U git+https://salsa.debian.org/python-debian-team/python-debian \ git+https://salsa.debian.org/jelmer/lintian-brush \ "git+https://salsa.debian.org/debian/distro-info#egg=distro-info&subdirectory=python" python setup.py develop - name: Style checks run: | python -m flake8 - name: Typing checks run: | python -m pip install types-setuptools types-PyYAML types-dataclasses types-chardet python -m mypy silver_platter - name: Test suite run run: | python setup.py test env: PYTHONHASHSEED: random silver-platter_0.4.5.orig/devnotes/command-line.rst0000644000000000000000000000114014071532501017362 0ustar00Command-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 -f some-script.yaml lp:brz-email 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 run -f lintian-brush.yaml samba debian-svp run -f lintian-brush.yaml --mode=propose samba debian-svp run -f lintian-brush.yaml --mode=push samba debian-svp upload-pending tdb debian-svp run -f new-upstream-release.yaml --no-build-verify tdb silver-platter_0.4.5.orig/devnotes/design.rst0000644000000000000000000000142513575466534016322 0ustar00Releaser Tool ============= * Specify timeout for a release * Ability to manually trigger a release * Use a custom PGP key (trusted by mine) * Prometheus metrics Specify a bit of config (protobuf?) that determines: * Repository URL * Bug Database * Tag format * Timeout * NEWS file path(s) * Tarball location * Pypi location * PGP key Once we've determined we want to do a release: * Check if CI state is green * Clone master * Commit: * Update NEWS to mark the new release * Make sure version strings elsewhere are correct * Tag && sign tag * Create a tarball * Sign the tarball * Upload the tarball + SCP + pypi * Mark any news bugs in NEWS as fixed [later] * Commit: * Update NEWS and version strings for next version * Push changes to master and new tag Use silver-platter? silver-platter_0.4.5.orig/devnotes/mp-status.rst0000644000000000000000000000206313413531470016764 0ustar00Closing 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.4.5.orig/devnotes/mutators.rst0000644000000000000000000000551714164061666016725 0ustar00The core of silver-platter are changer commands, which get run in version control checkouts to make changes. Commands will be run in a clean VCS checkout, where they can make changes as they deem fit. Changes should ideally be committed; by default pending changes will be discarded (but silver-platter will warn about them, and --autocommit can specified). However, if commands just make changes and don't touch the VCS at all, silver-platter will function in "autocommit" mode and create a single commit on their behalf with a reasonable commit message. Flags can be specified on the command-line or in a recipe: * name (if not specified, taken from filename?) * command to run * merge proposal commit message (with jinja2 templating) * merge proposal description, markdown/plain (with jinja2 templating) * whether the command can resume * mode ('push', 'attempt-push', 'propose') - defaults to 'attempt-push' * optional propose threshold, with minimum value before merge proposals are created * whether to autocommit (defaults to true?) * optional URL to target (if different from base URL) The command should exit with code 0 when successful, and 1 otherwise. In the case of failure, the branch is discarded. If it is known that the command supports resuming, then a previous branch may be loaded if present. The SVP_RESUME environment variable will be set to a path to a JSON file with the previous runs metadata. The command is expected to import any metadata about the older changes and carry it forward. If resuming is not supported then all older changes will be discarded (and possibly made again by the command). Environment variables that will be set: * SVP_API: Silver-platter API major version number. Currently set to 1 * COMMITTER: Set to a committer identity (optional) * SVP_RESUME: Set to a file path with JSON results from the last run, if available and if --resume is enabled. * SVP_RESULT: Set to a (optional) path that should be created by the command with extra details The output JSON should include the following fields: * description: Optional one-line text description of the error or changes made * value: Optional integer with an indicator of the value of the changes made * tags: Optional list of names of tags that should be included with the change (autodetected if not specified) * context: Optional command-specific result data, made available during template expansion * target-branch-url: URL for branch to target, if different from original URL Debian operations ----------------- For Debian branches, branches will be provided named according to DEP-13. The following environment variables will be set as well: * DEB_SOURCE: Source package name * DEB_UPDATE_CHANGELOG: Set to either update_changelog/leave_changelog (optional) * ALLOW_REFORMATTING: boolean indicating whether reformatting is allowed silver-platter_0.4.5.orig/examples/debian/0000755000000000000000000000000014071532501015502 5ustar00silver-platter_0.4.5.orig/examples/framwork.yaml0000644000000000000000000000043514071532501016776 0ustar00--- name: framwork command: |- sed -i 's/framwork/framework/' README.rst echo "Fix common typo: framwork => framework" mode: propose merge-request: commit-message: Fix a typo description: markdown: |- I spotted that we commonly mistype *framework* as *framwork*. silver-platter_0.4.5.orig/examples/patch.yaml0000644000000000000000000000033314071532501016242 0ustar00--- name: apply-patch command: |- patch -p1 < $PATCH echo "Apply patch $PATCH" mode: propose merge-request: commit-message: Apply patch $PATCH description: markdown: |- Apply the patch file $PATCH silver-platter_0.4.5.orig/examples/debian/base.md0000644000000000000000000000012114071532501016730 0ustar00{% block runner %}{% endblock %} This merge proposal was created automatically. silver-platter_0.4.5.orig/examples/debian/cme.yaml0000644000000000000000000000100314071532501017124 0ustar00--- # This runs the "cme fix" command, which makes a number of improvements # to Debian packages. This requires the "cme" package. # # Since CME doesn't provide an easily consumable report of the changes # it made, the commit message and merge proposal description created # are currently a bit generic and unhelpful ("Run CME"). name: cme-fix command: cme fix dpkg merge-proposal: commit-message: Run CME fix. description: |- {% extends "base.md" %} {% block runner -%} Run CME. {% endblock %} silver-platter_0.4.5.orig/examples/debian/debianize.yaml0000644000000000000000000000035414071532501020322 0ustar00--- # Generate a Debian package for an upstream source repository # # Requires the "lintian-brush" package. name: debianize command: debianize merge-proposal: commit-message: Debianize package description: |- Debianize package. silver-platter_0.4.5.orig/examples/debian/lintian-brush.yaml0000644000000000000000000000116614071532501021151 0ustar00--- name: lintian-fixes command: lintian-brush merge-proposal: commit-message: "Fix lintian issues: {{ ', '.join(sorted(applied)) }}" description: |- {% extends "base.md" %} {% block runner -%} {% if applied|length > 1 -%} Fix some issues reported by lintian {% endif -%} {% for entry in applied %} {% if applied|length > 1 %}* {% endif -%} {{ entry.summary }} {%- if entry.fixed_lintian_tags %} ({% for tag in entry.fixed_lintian_tags %}[{{ tag }}](https://lintian.debian.org/tags/{{ tag }}){% if not loop.last %}, {% endif %}{% endfor %}){% endif %} {% endfor -%} {% endblock -%} silver-platter_0.4.5.orig/examples/debian/mia.yaml0000644000000000000000000000100614071532501017131 0ustar00--- # This uses the drop-mia-uploaders command from the debmutate package. # # It scans the Debian BTS for bugs filed by the MIA team, extracts # the e-mail addresses of MIA uploaders and drops those from the Uploaders # field. name: mia command: drop-mia-uploaders merge-proposal: commit-message: Remove MIA uploaders description: |- {% extends "base.md" %} {% block runner %} Remove MIA uploaders: {% for uploader in removed_uploaders %} * {{ uploader }} {% endfor %} {% endblock %} silver-platter_0.4.5.orig/examples/debian/multiarch.yaml0000644000000000000000000000121014071532501020350 0ustar00--- name: multiarch-fixes command: apply-multiarch-hints merge-proposal: commit-message: Apply multi-arch hints description: |- {% extends "base.md" %} {% block runner %} Apply hints suggested by the multi-arch hinter. {% for entry in applied %} {% set kind = entry.link.split("#")[-1] %} * {{ entry.binary }}: {% if entry.action %}{{ entry.action }}. This fixes: {{ entry.description }}. ([{{ kind }}]({{ entry.link }})){% else %}Fix: {{ entry.description }}. ([{{ kind }}]({{ entry.link }})){% endif %} {% endfor %} These changes were suggested on https://wiki.debian.org/MultiArch/Hints. {% endblock %} silver-platter_0.4.5.orig/examples/debian/new-upstream-release.yaml0000644000000000000000000000103014071532501022425 0ustar00--- name: new-upstream-release command: deb-new-upstream merge-proposal: commit-message: "Merge new upstream release {{ new_upstream_version }}" description: |- {% extends "base.md" %} {% block runner %} {% if role == 'pristine-tar' %} pristine-tar data for new upstream version {{ upstream_version }}. {% elif role == 'upstream' %} Import of new upstream version {{ upstream_version }}. {% elif role == 'main' %} Merge new upstream version {{ upstream_version }}. {% endif %} {% endblock %} silver-platter_0.4.5.orig/examples/debian/new-upstream-snapshot.yaml0000644000000000000000000000104514071532501022652 0ustar00--- name: new-upstream-snapshot command: deb-new-upstream --snapshot merge-proposal: commit-message: "Merge new upstream snapshot {{ new_upstream_version }}" description: |- {% extends "base.md" %} {% block runner %} {% if role == 'pristine-tar' %} pristine-tar data for new upstream version {{ upstream_version }}. {% elif role == 'upstream' %} Import of new upstream version {{ upstream_version }}. {% elif role == 'main' %} Merge new upstream version {{ upstream_version }}. {% endif %} {% endblock %} silver-platter_0.4.5.orig/examples/debian/orphan.yaml0000644000000000000000000000151714164061666017675 0ustar00--- name: orphan command: deb-move-orphaned proposal: commit-message: Move orphaned package to the QA team description: |- {% extends "base.md" %} {% block runner %} Move orphaned package to the QA team. {% if wnpp_bug %} For details, see the [orphan bug](https://bugs.debian.org/{{ wnpp_bug }}). {% endif %} {% if pushed and new_vcs_url %} Please move the repository from {{ old_vcs_url }} to {{ new_vcs_url }}. {% if old_vcs_url.startswith('https://salsa.debian.org/') %} If you have the salsa(1) tool installed, run: salsa fork --group={{ salsa_user }} {{ path }} {% else %} If you have the salsa(1) tool installed, run: git clone {{ old_vcs_url }} {{ package_name }} salsa --group={{ salsa_user }} push_repo {{ package_name }} {% endif %} {% endblock %} silver-platter_0.4.5.orig/examples/debian/rrr.yaml0000644000000000000000000000044414071532501017175 0ustar00--- # This runs the deb-enable-rrr command from the debmutate package. name: rrr command: deb-enable-rrr merge-proposal: commit-message: Set the Rules-Requires-Root field. description: |- {% extends "base.md" %} {% block runner -%} Set Rules-Requires-Root. {% endblock %} silver-platter_0.4.5.orig/examples/debian/scrub-obsolete.yaml0000644000000000000000000000036214071532501021317 0ustar00--- name: scrub-obsolete command: deb-scrub-obsolete merge-proposal: commit-message: Remove unnecessary constraints description: |- {% extends "base.md" %} {% block runner %} Remove unnecessary constraints. {% endblock %} silver-platter_0.4.5.orig/helpers/needs-changelog-update.py0000755000000000000000000000223313420635240020765 0ustar00#!/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.4.5.orig/man/debian-svp.10000644000000000000000000000537513757562016015357 0ustar00.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 to 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/python-team/packages/dulwich\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.4.5.orig/man/svp.10000644000000000000000000000504714164061666014131 0ustar00.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 to the repository at the original URL or proposed as a change to the repository at the original URL. svp will exit 0 if no changes have been made, 1 if at least one repository has been changed and 2 in case of trouble. .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.4.5.orig/silver_platter.egg-info/PKG-INFO0000644000000000000000000000207014164061666020303 0ustar00Metadata-Version: 2.1 Name: silver-platter Version: 0.4.5 Summary: Automatic merge proposal creeator Home-page: https://jelmer.uk/code/silver-platter Author: Jelmer Vernooij Author-email: jelmer@jelmer.uk License: GNU GPL v2 or later Project-URL: Bug Tracker, https://github.com/jelmer/silver-platter/issues Project-URL: Repository, https://jelmer.uk/code/silver-platter Project-URL: GitHub, https://github.com/jelmer/silver-platter Keywords: git bzr vcs github gitlab launchpad Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Operating System :: POSIX Classifier: Topic :: Software Development :: Version Control Provides-Extra: debian License-File: LICENSE License-File: AUTHORS UNKNOWN silver-platter_0.4.5.orig/silver_platter.egg-info/SOURCES.txt0000644000000000000000000000324414164061666021076 0ustar00.bzrignore .flake8 .gitignore AUTHORS LICENSE MANIFEST.in Makefile NEWS README.rst TODO debian-svp example.yaml releaser.conf setup.cfg setup.py svp .github/workflows/pythonpackage.yml devnotes/command-line.rst devnotes/design.rst devnotes/mp-status.rst devnotes/mutators.rst examples/framwork.yaml examples/patch.yaml examples/debian/base.md examples/debian/cme.yaml examples/debian/debianize.yaml examples/debian/lintian-brush.yaml examples/debian/mia.yaml examples/debian/multiarch.yaml examples/debian/new-upstream-release.yaml examples/debian/new-upstream-snapshot.yaml examples/debian/orphan.yaml examples/debian/rrr.yaml examples/debian/scrub-obsolete.yaml helpers/needs-changelog-update.py man/debian-svp.1 man/svp.1 silver_platter/__init__.py silver_platter/__main__.py silver_platter/apply.py silver_platter/candidates.py silver_platter/proposal.py silver_platter/publish.py silver_platter/recipe.py silver_platter/run.py silver_platter/utils.py silver_platter/workspace.py silver_platter.egg-info/PKG-INFO silver_platter.egg-info/SOURCES.txt silver_platter.egg-info/dependency_links.txt silver_platter.egg-info/entry_points.txt silver_platter.egg-info/requires.txt silver_platter.egg-info/top_level.txt silver_platter/debian/__init__.py silver_platter/debian/__main__.py silver_platter/debian/apply.py silver_platter/debian/run.py silver_platter/debian/uploader.py silver_platter/tests/__init__.py silver_platter/tests/test_candidates.py silver_platter/tests/test_debian.py silver_platter/tests/test_proposal.py silver_platter/tests/test_publish.py silver_platter/tests/test_recipe.py silver_platter/tests/test_run.py silver_platter/tests/test_utils.py silver_platter/tests/test_version.pysilver-platter_0.4.5.orig/silver_platter.egg-info/dependency_links.txt0000644000000000000000000000000114006107524023242 0ustar00 silver-platter_0.4.5.orig/silver_platter.egg-info/entry_points.txt0000644000000000000000000000014714071532501022473 0ustar00[console_scripts] debian-svp = silver_platter.debian.__main__:main svp = silver_platter.__main__:main silver-platter_0.4.5.orig/silver_platter.egg-info/requires.txt0000644000000000000000000000014014164061666021602 0ustar00breezy>=3.2.0 dulwich>=0.20.23 jinja2 [debian] debmutate>=0.3 distro-info python_debian pyyaml silver-platter_0.4.5.orig/silver_platter.egg-info/top_level.txt0000644000000000000000000000001714006107524021724 0ustar00silver_platter silver-platter_0.4.5.orig/silver_platter/__init__.py0000644000000000000000000000240714164061666017631 0ustar00#!/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 breezy # noqa: F401 import breezy.git # For git support # noqa: F401 import breezy.bzr # For bzr support # noqa: F401 import breezy.plugins.launchpad # For lp: URL support # noqa: F401 import breezy.plugins.gitlab # For gitlab support # noqa: F401 import breezy.plugins.github # For github support # noqa: F401 import breezy.plugins.debian # For apt: URL support # noqa: F401 __version__ = (0, 4, 5) version_string = ".".join(map(str, __version__)) silver-platter_0.4.5.orig/silver_platter/__main__.py0000644000000000000000000001041314164061666017606 0ustar00#!/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 logging import silver_platter # noqa: F401 import sys from typing import Optional, List, Callable, Dict from . import ( apply, run, version_string, ) def hosters_main(argv: List[str]) -> Optional[int]: from breezy.propose import hosters parser = argparse.ArgumentParser(prog="svp hosters") parser.parse_args(argv) for name, hoster_cls in hosters.items(): for instance in hoster_cls.iter_instances(): print("%s (%s)" % (instance.base_url, name)) return None def login_main(argv: List[str]) -> Optional[int]: parser = argparse.ArgumentParser(prog="svp login") parser.add_argument("url", help="URL of branch to work on.", type=str) args = parser.parse_args(argv) 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" if hoster == "gitlab": from breezy.plugins.gitlab.cmds import cmd_gitlab_login cmd_gl = cmd_gitlab_login() cmd_gl._setup_outf() return cmd_gl.run(args.url) elif hoster == "github": from breezy.plugins.github.cmds import cmd_github_login cmd_gh = cmd_github_login() cmd_gh._setup_outf() return cmd_gh.run() elif hoster == "launchpad": from breezy.plugins.launchpad.cmds import cmd_launchpad_login cmd_lp = cmd_launchpad_login() cmd_lp._setup_outf() cmd_lp.run() from breezy.plugins.launchpad import lp_api lp_api.connect_launchpad(lp_service_root, version="devel") return None else: logging.exception("Unknown hoster %r.", hoster) return 1 def proposals_main(argv: List[str]) -> None: from .proposal import iter_all_mps parser = argparse.ArgumentParser() parser.add_argument( "--status", default="open", choices=["open", "merged", "closed"], type=str, help="Only display proposals with this status.", ) args = parser.parse_args(argv) for hoster, proposal, status in iter_all_mps([args.status]): print(proposal.url) subcommands: Dict[str, Callable[[List[str]], Optional[int]]] = { "hosters": hosters_main, "login": login_main, "proposals": proposals_main, "run": run.main, "apply": apply.main, } def main(argv: Optional[List[str]] = None) -> Optional[int]: import breezy breezy.initialize() parser = argparse.ArgumentParser(prog="svp", add_help=False) parser.add_argument( "--version", action="version", version="%(prog)s " + version_string ) parser.add_argument( "--help", action="store_true", help="show this help message and exit" ) parser.add_argument("subcommand", type=str, choices=list(subcommands.keys())) logging.basicConfig(level=logging.INFO, format="%(message)s") args, rest = parser.parse_known_args(argv) if args.help: if args.subcommand is None: parser.print_help() parser.exit() else: rest.append("--help") if args.subcommand is None: parser.print_usage() return 1 return subcommands[args.subcommand](rest) if __name__ == "__main__": sys.exit(main()) silver-platter_0.4.5.orig/silver_platter/apply.py0000644000000000000000000001763114164061666017224 0ustar00#!/usr/bin/python # Copyright (C) 2021 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 dataclasses import dataclass, field import json import logging import os import subprocess import sys import tempfile from typing import Optional, Dict, List, Tuple from breezy.commit import PointlessCommit from breezy.workspace import reset_tree, check_clean_tree from breezy.workingtree import WorkingTree class ScriptMadeNoChanges(Exception): "Script made no changes." class ScriptFailed(Exception): """Script failed to run.""" class DetailedFailure(Exception): """Detailed failure""" def __init__(self, result_code, description, details=None): self.result_code = result_code self.description = description self.details = details @classmethod def from_json(cls, json): return cls( result_code=json.get('result_code'), description=json.get('description'), details=json.get('details')) class ResultFileFormatError(Exception): """The result file was invalid.""" def __init__(self, inner_error): self.inner_error = inner_error @dataclass class CommandResult(object): description: Optional[str] = None value: Optional[int] = None serialized_context: Optional[str] = None context: Dict[str, str] = field(default_factory=dict) tags: List[Tuple[str, bytes]] = field(default_factory=list) old_revision: Optional[bytes] = None new_revision: Optional[bytes] = None target_branch_url: Optional[str] = None @classmethod def from_json(cls, data): if 'tags' in data: tags = [] for name, revid in data['tags']: tags.append((name, revid.encode('utf-8'))) else: tags = None return cls( value=data.get('value', None), context=data.get('context', {}), description=data.get('description'), serialized_context=data.get('serialized_context', None), target_branch_url=data.get('target-branch-url', None), tags=tags) def script_runner( # noqa: C901 local_tree: WorkingTree, script: str, commit_pending: Optional[bool] = None, resume_metadata=None, subpath: str = '', committer: Optional[str] = None, extra_env: Optional[Dict[str, str]] = None, ) -> CommandResult: # noqa: C901 """Run a script in a tree and commit the result. This ignores newly added files. Args: local_tree: Local tree to run script in script: Script to run commit_pending: Whether to commit pending changes (True, False or None: only commit if there were no commits by the script) """ env = dict(os.environ) if extra_env: env.update(extra_env) env['SVP_API'] = '1' last_revision = local_tree.last_revision() orig_tags = local_tree.branch.tags.get_tag_dict() with tempfile.TemporaryDirectory() as td: env['SVP_RESULT'] = os.path.join(td, 'result.json') if resume_metadata: env['SVP_RESUME'] = os.path.join(td, 'resume-metadata.json') with open(env['SVP_RESUME'], 'w') as f: json.dump(resume_metadata, f) p = subprocess.Popen( script, cwd=local_tree.abspath(subpath), stdout=subprocess.PIPE, shell=True, env=env) (description_encoded, err) = p.communicate(b"") try: with open(env['SVP_RESULT'], 'r') as f: try: result_json = json.load(f) except json.decoder.JSONDecodeError as e: raise ResultFileFormatError(e) except FileNotFoundError: result_json = None if p.returncode != 0: if result_json is not None: raise DetailedFailure.from_json(result_json) raise ScriptFailed(script, p.returncode) if result_json is not None: result = CommandResult.from_json(result_json) else: result = CommandResult() if not result.description: result.description = description_encoded.decode() new_revision = local_tree.last_revision() if result.tags is None: result.tags = [] for name, revid in local_tree.branch.tags.get_tag_dict().items(): if orig_tags.get(name) != revid: result.tags.append((name, revid)) 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( result.description, allow_pointless=False, committer=committer) except PointlessCommit: pass if new_revision == last_revision: raise ScriptMadeNoChanges() result.old_revision = last_revision result.new_revision = local_tree.last_revision() return result def main(argv: List[str]) -> Optional[int]: # noqa: C901 import argparse parser = argparse.ArgumentParser() parser.add_argument( "command", help="Path to script to run.", type=str, nargs='?') parser.add_argument( "--diff", action="store_true", help="Show diff of generated changes." ) parser.add_argument( "--commit-pending", help="Commit pending changes after script.", choices=["yes", "no", "auto"], default=None, type=str, ) parser.add_argument( "--verify-command", type=str, help="Command to run to verify changes." ) parser.add_argument( "--recipe", type=str, help="Recipe to use.") args = parser.parse_args(argv) if args.recipe: from .recipe import Recipe recipe = Recipe.from_path(args.recipe) else: recipe = None if args.commit_pending: commit_pending = {"auto": None, "yes": True, "no": False}[args.commit_pending] elif recipe: commit_pending = recipe.commit_pending else: commit_pending = None if args.command: command = args.command elif recipe.command: command = recipe.command else: logging.exception('No command specified.') return 1 local_tree, subpath = WorkingTree.open_containing('.') check_clean_tree(local_tree) try: result = script_runner( local_tree, script=command, commit_pending=commit_pending, subpath=subpath) if result.description: logging.info('Succeeded: %s', result.description) if args.verify_command: try: subprocess.check_call( args.verify_command, shell=True, cwd=local_tree.abspath(subpath) ) except subprocess.CalledProcessError: logging.exception("Verify command failed.") return 1 except Exception: reset_tree(local_tree, subpath) raise if args.diff: from breezy.diff import show_diff_trees old_tree = local_tree.revision_tree(result.old_revision) new_tree = local_tree.revision_tree(result.new_revision) show_diff_trees( old_tree, new_tree, sys.stdout.buffer, old_label='old/', new_label='new/') return 0 silver-platter_0.4.5.orig/silver_platter/candidates.py0000644000000000000000000000343314071532501020155 0ustar00#!/usr/bin/python # Copyright (C) 2021 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 dataclasses import dataclass from typing import Optional, List import yaml @dataclass class Candidate(object): """Candidate.""" url: str branch: Optional[str] = None subpath: str = '' @classmethod def from_yaml(cls, d): if isinstance(d, dict): return cls( url=d.get('url'), branch=d.get('branch'), subpath=d.get('path'), ) elif isinstance(d, str): return cls(url=d) else: raise TypeError(d) @dataclass class CandidateList(object): """Candidate list.""" candidates: List[Candidate] def __iter__(self): return iter(self.candidates) @classmethod def from_yaml(cls, d): candidates = [] for entry in d: candidates.append(Candidate.from_yaml(entry)) return cls(candidates=candidates) @classmethod def from_path(cls, path): with open(path, 'r') as f: return cls.from_yaml(yaml.full_load(f)) silver-platter_0.4.5.orig/silver_platter/debian/0000755000000000000000000000000013361501341016722 5ustar00silver-platter_0.4.5.orig/silver_platter/proposal.py0000644000000000000000000001167714164061666017742 0ustar00#!/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 typing import ( List, Optional, Tuple, Iterator, ) from breezy.branch import Branch from breezy.errors import ( PermissionDenied, ) from breezy.merge_directive import ( MergeDirective, MergeDirective2, ) from breezy.transport import Transport from breezy.propose import ( get_hoster, hosters, Hoster, MergeProposal, NoSuchProject, UnsupportedHoster, HosterLoginRequired, ) from breezy.propose import ( iter_hoster_instances, SourceNotDerivedFromTarget, ) import breezy.plugins.gitlab # noqa: F401 import breezy.plugins.github # noqa: F401 import breezy.plugins.launchpad # noqa: F401 from .utils import ( open_branch, full_branch_url, ) from .publish import ( push_changes, push_derived_changes, propose_changes, EmptyMergeProposal, check_proposal_diff, DryRunProposal, find_existing_proposed, SUPPORTED_MODES, ) __all__ = [ "HosterLoginRequired", "UnsupportedHoster", "PermissionDenied", "NoSuchProject", "get_hoster", "hosters", "iter_all_mps", "push_changes", "SUPPORTED_MODES", "push_derived_changes", "propose_changes", "DryRunProposal", "check_proposal_diff", "EmptyMergeProposal", "find_existing_proposed", ] if SourceNotDerivedFromTarget is not None: __all__.append("SourceNotDerivedFromTarget") def enable_tag_pushing(branch: Branch) -> None: stack = branch.get_config() stack.set_user_option("branch.fetch_tags", True) def merge_directive_changes( local_branch: Branch, main_branch: Branch, hoster: Hoster, name: str, message: str, include_patch: bool = False, include_bundle: bool = False, overwrite_existing: bool = False, ) -> MergeDirective: from breezy import 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 = 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 iter_all_mps( statuses: Optional[List[str]] = None, ) -> Iterator[Tuple[Hoster, MergeProposal, str]]: """iterate over all existing merge proposals.""" if statuses is None: statuses = ["open", "merged", "closed"] for instance in iter_hoster_instances(): for status in statuses: try: for mp in instance.iter_my_proposals(status=status): yield instance, mp, status except HosterLoginRequired: pass def iter_conflicted( branch_name: str, ) -> Iterator[Tuple[str, Branch, str, Branch, Hoster, MergeProposal, bool]]: """Find conflicted branches owned by the current user. Args: branch_name: Branch name to search for """ possible_transports: List[Transport] = [] 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 ( # type: ignore not resume_branch.name and resume_branch.user_url.endswith(branch_name) # type: ignore ): continue # TODO(jelmer): Find out somehow whether we need to modify a subpath? subpath = "" yield ( full_branch_url(resume_branch), main_branch, subpath, resume_branch, hoster, mp, True, ) silver-platter_0.4.5.orig/silver_platter/publish.py0000644000000000000000000006054614164061666017550 0ustar00#!/usr/bin/python # Copyright (C) 2020 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 logging from typing import List, Union, Dict, Optional, Tuple, Any, Callable from breezy.branch import Branch from breezy import ( errors, merge as _mod_merge, revision as _mod_revision, ) from breezy.errors import PermissionDenied from breezy.memorybranch import MemoryBranch from breezy.propose import ( get_hoster, Hoster, MergeProposal, MergeProposalExists, NoSuchProject, UnsupportedHoster, ) from breezy.transport import Transport from breezy.propose import ( SourceNotDerivedFromTarget, ) from .utils import ( open_branch, full_branch_url, ) __all__ = [ "push_changes", "push_derived_changes", "propose_changes", "EmptyMergeProposal", "check_proposal_diff", "DryRunProposal", "find_existing_proposed", "NoSuchProject", "PermissionDenied", "UnsupportedHoster", "SourceNotDerivedFromTarget", ] MODE_PUSH = "push" MODE_ATTEMPT_PUSH = "attempt-push" MODE_PROPOSE = "propose" MODE_PUSH_DERIVED = "push-derived" SUPPORTED_MODES: List[str] = [ MODE_PUSH, MODE_ATTEMPT_PUSH, MODE_PROPOSE, MODE_PUSH_DERIVED, ] def _tag_selector_from_tags(tags): # TODO(jelmer): Select dict return tags.__contains__ def push_result( local_branch: Branch, remote_branch: Branch, additional_colocated_branches: Optional[Union[List[str], Dict[str, str]]] = None, tags: Optional[Union[Dict[str, bytes], List[str]]] = None, stop_revision: Optional[bytes] = None, ) -> None: kwargs = {} if tags is not None: kwargs["tag_selector"] = _tag_selector_from_tags(tags) try: local_branch.push( remote_branch, overwrite=False, stop_revision=stop_revision, **kwargs ) except errors.LockFailed as e: # Almost certainly actually a PermissionDenied error.. raise errors.PermissionDenied(path=full_branch_url(remote_branch), extra=e) for from_branch_name in additional_colocated_branches or []: try: add_branch = local_branch.controldir.open_branch(name=from_branch_name) # type: ignore except errors.NotBranchError: pass else: if isinstance(additional_colocated_branches, dict): to_branch_name = additional_colocated_branches[from_branch_name] else: to_branch_name = from_branch_name remote_branch.controldir.push_branch(add_branch, name=to_branch_name, **kwargs) # type: ignore def push_changes( local_branch: Branch, main_branch: Branch, hoster: Optional[Hoster], possible_transports: Optional[List[Transport]] = None, additional_colocated_branches: Optional[Union[List[str], Dict[str, str]]] = None, dry_run: bool = False, tags: Optional[Union[Dict[str, bytes], List[str]]] = None, stop_revision: Optional[bytes] = None, ) -> None: """Push changes to a branch.""" if hoster is None: push_url = main_branch.user_url else: push_url = hoster.get_push_url(main_branch) logging.info("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, tags=tags, stop_revision=stop_revision, ) def push_derived_changes( local_branch: Branch, main_branch: Branch, hoster: Hoster, name: str, overwrite_existing: Optional[bool] = False, owner: Optional[str] = None, tags: Optional[Union[Dict[str, bytes], List[str]]] = None, stop_revision: Optional[bytes] = None, ) -> Tuple[Branch, str]: kwargs = {} if tags is not None: kwargs["tag_selector"] = _tag_selector_from_tags(tags) remote_branch, public_branch_url = hoster.publish_derived( local_branch, main_branch, name=name, overwrite=overwrite_existing, owner=owner, revision_id=stop_revision, **kwargs ) return remote_branch, public_branch_url def propose_changes( # noqa: C901 local_branch: Branch, main_branch: Branch, hoster: Hoster, name: str, mp_description: str, resume_branch: Optional[Branch] = None, resume_proposal: Optional[MergeProposal] = None, overwrite_existing: Optional[bool] = True, labels: Optional[List[str]] = None, dry_run: bool = False, commit_message: Optional[str] = None, additional_colocated_branches: Optional[Union[List[str], Dict[str, str]]] = None, allow_empty: bool = False, reviewers: Optional[List[str]] = None, tags: Optional[Union[Dict[str, bytes], List[str]]] = None, owner: Optional[str] = None, stop_revision: Optional[bytes] = None, allow_collaboration: bool = False, ) -> Tuple[MergeProposal, bool]: """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 reviewers: List of reviewers tags: Tags to push (None for default behaviour) owner: Derived branch owner stop_revision: Revision to stop pushing at allow_collaboration: Allow target branch owners to modify source branch Returns: Tuple with (proposal, is_new) """ if not allow_empty: check_proposal_diff(local_branch, main_branch, stop_revision) push_kwargs = {} if tags is not None: push_kwargs["tag_selector"] = _tag_selector_from_tags(tags) if not dry_run: if resume_branch is not None: local_branch.push( resume_branch, overwrite=overwrite_existing, stop_revision=stop_revision, **push_kwargs ) remote_branch = resume_branch else: remote_branch, public_branch_url = hoster.publish_derived( local_branch, main_branch, name=name, overwrite=overwrite_existing, revision_id=stop_revision, owner=owner, **push_kwargs ) for from_branch_name in additional_colocated_branches or []: try: local_colo_branch = local_branch.controldir.open_branch( # type: ignore name=from_branch_name ) except errors.NotBranchError: pass else: if isinstance(additional_colocated_branches, dict): to_branch_name = additional_colocated_branches[from_branch_name] else: to_branch_name = from_branch_name remote_branch.controldir.push_branch( # type: ignore source=local_colo_branch, overwrite=overwrite_existing, name=to_branch_name, **push_kwargs ) 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.propose import ( ReopenFailed, ) try: resume_proposal.reopen() # type: ignore except ReopenFailed: logging.info("Reopening existing proposal failed. Creating new proposal.") resume_proposal = None if resume_proposal is None: if not dry_run: proposal_builder = hoster.get_proposer(remote_branch, main_branch) kwargs: Dict[str, Any] = {} kwargs["commit_message"] = commit_message kwargs["allow_collaboration"] = allow_collaboration try: mp = proposal_builder.create_proposal( description=mp_description, labels=labels, reviewers=reviewers, **kwargs ) except MergeProposalExists as e: if getattr(e, "existing_proposal", None) is None: # Hoster didn't tell us where the actual proposal is. raise resume_proposal = e.existing_proposal except errors.PermissionDenied: logging.info("Permission denied while trying to create " "proposal.") raise else: return (mp, True) else: mp = DryRunProposal( local_branch, main_branch, labels=labels, description=mp_description, commit_message=commit_message, reviewers=reviewers, owner=owner, stop_revision=stop_revision, ) return (mp, True) # 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 resume_proposal.get_commit_message() != commit_message: try: resume_proposal.set_commit_message(commit_message) except errors.UnsupportedOperation: pass return (resume_proposal, False) class EmptyMergeProposal(Exception): """Merge proposal does not have any changes.""" def __init__(self, local_branch: Branch, main_branch: Branch): self.local_branch = local_branch self.main_branch = main_branch def check_proposal_diff( other_branch: Branch, main_branch: Branch, stop_revision: Optional[bytes] = None ) -> None: if stop_revision is None: stop_revision = other_branch.last_revision() 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() tree_branch = MemoryBranch(other_branch.repository, (None, main_revid), None) merger = _mod_merge.Merger( tree_branch, this_tree=main_tree, revision_graph=revision_graph ) merger.set_other_revision(stop_revision, other_branch) try: merger.find_base() except errors.UnrelatedBranches: merger.set_base_revision(_mod_revision.NULL_REVISION, other_branch) merger.merge_type = _mod_merge.Merge3Merger # type: ignore 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) class DryRunProposal(MergeProposal): """A merge proposal that is not actually created. :ivar url: URL for the merge proposal """ def __init__( self, source_branch: Branch, target_branch: Branch, labels: Optional[List[str]] = None, description: Optional[str] = None, commit_message: Optional[str] = None, reviewers: Optional[List[str]] = None, owner: Optional[str] = None, stop_revision: Optional[bytes] = 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 self.reviewers = reviewers self.owner = owner self.stop_revision = stop_revision @classmethod def from_existing( cls, mp: MergeProposal, source_branch: Optional[Branch] = None ) -> MergeProposal: if source_branch is None: source_branch = open_branch(mp.get_source_branch_url()) 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) -> str: return "%s(%r, %r)" % ( self.__class__.__name__, self.source_branch, self.target_branch, ) def get_description(self) -> Optional[str]: """Get the description of the merge proposal.""" return self.description def set_description(self, description: str) -> None: self.description = description def get_commit_message(self) -> Optional[str]: return self.commit_message def set_commit_message(self, commit_message: str) -> None: self.commit_message = commit_message def get_source_branch_url(self) -> str: """Return the source branch.""" return full_branch_url(self.source_branch) def get_target_branch_url(self) -> str: """Return the target branch.""" return full_branch_url(self.target_branch) def close(self) -> None: """Close the merge proposal (without merging it).""" self.closed = True def is_merged(self) -> bool: """Check whether this merge proposal has been merged.""" return False def is_closed(self) -> bool: """Check whether this merge proposal has been closed.""" return False def reopen(self) -> None: pass def find_existing_proposed( main_branch: Branch, hoster: Hoster, name: str, overwrite_unrelated: bool = False, owner: Optional[str] = None, preferred_schemes: Optional[List[str]] = None, ) -> Tuple[Optional[Branch], Optional[bool], Optional[MergeProposal]]: """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 preferred_schemes: List of preferred schemes 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: if preferred_schemes is not None: existing_branch = hoster.get_derived_branch( main_branch, name=name, owner=owner, preferred_schemes=preferred_schemes ) else: # TODO: Support older versions of breezy without preferred_schemes existing_branch = hoster.get_derived_branch( main_branch, name=name, owner=owner ) except errors.NotBranchError: return (None, None, None) else: logging.info( "Branch %s already exists (branch at %s)", name, full_branch_url(existing_branch), ) # 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() and not mp.is_merged(): return (existing_branch, False, mp) else: merged_proposal = mp else: if merged_proposal is not None: logging.info( "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) def merge_conflicts( main_branch: Branch, other_branch: Branch, other_revision: Optional[bytes] = None ) -> bool: """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) other_revision: Other revision to check Returns: boolean indicating whether the merge would result in conflicts """ if other_revision is None: other_revision = other_branch.last_revision() if other_branch.repository.get_graph().is_ancestor( main_branch.last_revision(), other_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"] = [] other_tree = other_branch.repository.revision_tree(other_revision) try: try: merger = _mod_merge.Merger.from_revision_ids( other_tree, other_branch=other_branch, other=main_branch.last_revision(), tree_branch=other_branch, ) except errors.UnrelatedBranches: # Unrelated branches don't technically *have* to lead to # conflicts, but there's not a lot to be salvaged here, either. return True 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 PublishResult(object): """A object describing the result of a publish action.""" def __init__( self, mode: str, proposal: Optional[MergeProposal] = None, is_new: bool = False ) -> None: self.mode = mode self.proposal = proposal self.is_new = is_new def __tuple__(self) -> Tuple[Optional[MergeProposal], bool]: # Backwards compatibility return (self.proposal, self.is_new) class InsufficientChangesForNewProposal(Exception): """There were not enough changes for a new merge proposal.""" def publish_changes( local_branch: Branch, main_branch: Branch, resume_branch: Optional[Branch], mode: str, name: str, get_proposal_description: Callable[[str, Optional[MergeProposal]], str], get_proposal_commit_message: Callable[ [Optional[MergeProposal]], Optional[str] ] = None, dry_run: bool = False, hoster: Optional[Hoster] = None, allow_create_proposal: bool = True, labels: Optional[List[str]] = None, overwrite_existing: Optional[bool] = True, existing_proposal: Optional[MergeProposal] = None, reviewers: Optional[List[str]] = None, tags: Optional[Union[List[str], Dict[str, bytes]]] = None, derived_owner: Optional[str] = None, allow_collaboration: bool = False, stop_revision: Optional[bytes] = None, ) -> PublishResult: """Publish a set of changes. Args: ws: Workspace to push from mode: Mode to use ('push', 'push-derived', 'propose') name: Branch name to push get_proposal_description: Function to retrieve proposal description get_proposal_commit_message: Function to retrieve proposal commit message dry_run: Whether to dry run hoster: Hoster, if known allow_create_proposal: Whether to allow creating proposals labels: Labels to set for any merge proposals overwrite_existing: Whether to overwrite existing (but unrelated) branch existing_proposal: Existing proposal to update reviewers: List of reviewers for merge proposal tags: Tags to push (None for default behaviour) derived_owner: Name of any derived branch allow_collaboration: Whether to allow target branch owners to modify source branch. """ if mode not in SUPPORTED_MODES: raise ValueError("invalid mode %r" % mode) if stop_revision is None: stop_revision = local_branch.last_revision() if stop_revision == main_branch.last_revision(): if existing_proposal is not None: logging.info("closing existing merge proposal - no new revisions") existing_proposal.close() return PublishResult(mode) if resume_branch and resume_branch.last_revision() == stop_revision: # 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. logging.info("No changes added; making sure merge proposal is up to date.") if hoster is None: hoster = get_hoster(main_branch) if mode == MODE_PUSH_DERIVED: (remote_branch, public_url) = push_derived_changes( local_branch, main_branch, hoster=hoster, name=name, overwrite_existing=overwrite_existing, tags=tags, owner=derived_owner, stop_revision=stop_revision, ) return PublishResult(mode) if mode in (MODE_PUSH, MODE_ATTEMPT_PUSH): try: # breezy would do this check too, but we want to be *really* sure. with local_branch.lock_read(): graph = local_branch.repository.get_graph() if not graph.is_ancestor(main_branch.last_revision(), stop_revision): raise errors.DivergedBranches(main_branch, local_branch) push_changes( local_branch, main_branch, hoster=hoster, dry_run=dry_run, tags=tags, stop_revision=stop_revision, ) except errors.PermissionDenied: if mode == MODE_ATTEMPT_PUSH: logging.info("push access denied, falling back to propose") mode = MODE_PROPOSE else: logging.info("permission denied during push") raise else: return PublishResult(mode=mode) assert mode == "propose" if not resume_branch and not allow_create_proposal: raise InsufficientChangesForNewProposal() mp_description = get_proposal_description( getattr(hoster, "merge_proposal_description_format", "plain"), existing_proposal if resume_branch else None, ) if get_proposal_commit_message is not None: commit_message = get_proposal_commit_message( existing_proposal if resume_branch else None ) (proposal, is_new) = propose_changes( local_branch, main_branch, hoster=hoster, name=name, mp_description=mp_description, resume_branch=resume_branch, resume_proposal=existing_proposal, overwrite_existing=overwrite_existing, labels=labels, dry_run=dry_run, commit_message=commit_message, reviewers=reviewers, tags=tags, owner=derived_owner, allow_collaboration=allow_collaboration, stop_revision=stop_revision, ) return PublishResult(mode, proposal, is_new) silver-platter_0.4.5.orig/silver_platter/recipe.py0000644000000000000000000000603014071532501017321 0ustar00#!/usr/bin/python # Copyright (C) 2021 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 dataclasses import dataclass from jinja2 import Template from typing import Optional, Dict, Union, List import yaml @dataclass class Recipe(object): """Recipe to use.""" name: str command: Union[str, List[str]] merge_request_description_template: Dict[Optional[str], Template] merge_request_commit_message_template: Template resume: bool = False commit_pending: Optional[bool] = True propose_threshold: Optional[int] = None @classmethod def from_yaml(cls, d): merge_request = d.get('merge-request', {}) if merge_request: description = merge_request.get('description', {}) if isinstance(description, dict): merge_request_description_template = description else: merge_request_description_template = {None: description} merge_request_commit_message_template = merge_request.get('commit-message') propose_threshold = merge_request.get('propose-threshold') else: merge_request_description_template = {} merge_request_commit_message_template = None propose_threshold = None return cls( name=d.get('name'), command=d.get('command'), resume=d.get('resume', False), commit_pending=d.get('commit-pending'), merge_request_description_template=merge_request_description_template, merge_request_commit_message_template=merge_request_commit_message_template, propose_threshold=propose_threshold) def render_merge_request_commit_message(self, context): template = self.merge_request_commit_message_template if template: return Template(template).render(context) return None def render_merge_request_description(self, description_format, context): template = self.merge_request_description_template.get(description_format) if template is None: try: template = self.merge_request_description_template[None] except KeyError: return None return Template(template).render(context) @classmethod def from_path(cls, path): with open(path, 'r') as f: return cls.from_yaml(yaml.full_load(f)) silver-platter_0.4.5.orig/silver_platter/run.py0000755000000000000000000002330414164061666016700 0ustar00#!/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.""" import argparse import logging import os import subprocess import sys from typing import Optional, List from breezy import osutils from breezy import propose as _mod_propose import silver_platter # noqa: F401 from .apply import script_runner, ScriptMadeNoChanges, ScriptFailed from .proposal import ( UnsupportedHoster, enable_tag_pushing, find_existing_proposed, get_hoster, ) from .workspace import ( Workspace, ) from .publish import ( SUPPORTED_MODES, InsufficientChangesForNewProposal, ) from .utils import ( open_branch, BranchMissing, BranchUnsupported, BranchUnavailable, full_branch_url, ) def derived_branch_name(script: str) -> str: return os.path.splitext(osutils.basename(script.split(" ")[0]))[0] def apply_and_publish( # noqa: C901 url: str, name: str, command: str, mode: str, commit_pending: Optional[bool] = None, dry_run: bool = False, labels: Optional[List[str]] = None, diff: bool = False, verify_command: Optional[str] = None, derived_owner: Optional[str] = None, refresh: bool = False, allow_create_proposal=None, get_commit_message=None, get_description=None): try: main_branch = open_branch(url) except (BranchUnavailable, BranchMissing, BranchUnsupported) as e: logging.exception("%s: %s", url, e) return 2 overwrite = False try: hoster = get_hoster(main_branch) except UnsupportedHoster as e: if 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 logging.warn( "Unsupported hoster (%s), will attempt to push to %s", e, full_branch_url(main_branch), ) else: (resume_branch, resume_overwrite, existing_proposal) = find_existing_proposed( main_branch, hoster, name, owner=derived_owner ) if resume_overwrite is not None: overwrite = resume_overwrite if refresh: if resume_branch: overwrite = True resume_branch = None with Workspace(main_branch, resume_branch=resume_branch) as ws: try: result = script_runner(ws.local_tree, command, commit_pending) except ScriptMadeNoChanges: logging.error("Script did not make any changes.") return 0 except ScriptFailed: logging.error("Script failed to run.") return 2 if verify_command: try: subprocess.check_call( verify_command, shell=True, cwd=ws.local_tree.abspath(".") ) except subprocess.CalledProcessError: logging.error("Verify command failed.") return 2 enable_tag_pushing(ws.local_tree.branch) try: publish_result = ws.publish_changes( mode, name, get_proposal_description=lambda df, ep: get_description(result, df, ep), get_proposal_commit_message=lambda ep: get_commit_message(result, ep), allow_create_proposal=lambda: allow_create_proposal(result), dry_run=dry_run, hoster=hoster, labels=labels, overwrite_existing=overwrite, derived_owner=derived_owner, existing_proposal=existing_proposal, ) except UnsupportedHoster as e: logging.exception( "No known supported hoster for %s. Run 'svp login'?", full_branch_url(e.branch), ) return 2 except InsufficientChangesForNewProposal: logging.info('Insufficient changes for a new merge proposal') return 1 except _mod_propose.HosterLoginRequired as e: logging.exception( "Credentials for hosting site at %r missing. " "Run 'svp login'?", e.hoster.base_url, ) return 2 if publish_result.proposal: if publish_result.is_new: logging.info("Merge proposal created.") else: logging.info("Merge proposal updated.") if publish_result.proposal.url: logging.info("URL: %s", publish_result.proposal.url) logging.info("Description: %s", publish_result.proposal.get_description()) if diff: ws.show_diff(sys.stdout.buffer) return 1 def main(argv: List[str]) -> Optional[int]: # noqa: C901 parser = argparse.ArgumentParser() parser.add_argument("url", help="URL of branch to work on.", type=str, nargs="?") parser.add_argument( "--command", help="Path to script to run.", type=str) parser.add_argument( "--derived-owner", type=str, default=None, help="Owner for derived branches." ) 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=None, 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( "--verify-command", type=str, help="Command to run to verify changes." ) parser.add_argument( "--recipe", type=str, help="Recipe to use.") parser.add_argument( "--candidates", type=str, help="File with candidate list.") args = parser.parse_args(argv) if args.recipe: from .recipe import Recipe recipe = Recipe.from_path(args.recipe) else: recipe = None if not args.url and not args.candidates: parser.error("url or candidates are required") urls = [] if args.url: urls = [args.url] if args.candidates: from .candidates import CandidateList candidatelist = CandidateList.from_path(args.candidates) urls.extend([candidate.url for candidate in candidatelist]) if args.commit_pending: commit_pending = {"auto": None, "yes": True, "no": False}[args.commit_pending] elif recipe: commit_pending = recipe.commit_pending else: commit_pending = None if args.command: command = args.command elif recipe.command: command = recipe.command else: logging.exception('No command specified.') return 1 if args.name is not None: name = args.name elif recipe and recipe.name: name = recipe.name else: name = derived_branch_name(command) refresh = args.refresh if recipe and not recipe.resume: refresh = True def allow_create_proposal(result): if result.value is None: return True if recipe.propose_threshold is not None: return result.value >= recipe.propose_threshold return True def get_commit_message(result, existing_proposal): if recipe: return recipe.render_merge_request_commit_message(result.context) if existing_proposal is not None: return existing_proposal.get_commit_message() return None def get_description(result, description_format, existing_proposal): if recipe: description = recipe.render_merge_request_description( description_format, result.context) if description: return description if result.description is not None: return result.description if existing_proposal is not None: return existing_proposal.get_description() raise ValueError("No description available") retcode = 0 for url in urls: result = apply_and_publish( url, name=name, command=command, mode=args.mode, commit_pending=commit_pending, dry_run=args.dry_run, labels=args.label, diff=args.diff, derived_owner=args.derived_owner, refresh=refresh, allow_create_proposal=allow_create_proposal, get_commit_message=get_commit_message, get_description=get_description) retcode = max(retcode, result) return retcode if __name__ == "__main__": sys.exit(main(sys.argv)) silver-platter_0.4.5.orig/silver_platter/tests/0000755000000000000000000000000013363052725016653 5ustar00silver-platter_0.4.5.orig/silver_platter/utils.py0000644000000000000000000002402514164061666017232 0ustar00#!/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 logging import os import shutil import socket import subprocess import tempfile from typing import Callable, Tuple, Optional, List, Union, Dict from breezy import ( errors, urlutils, ) from breezy.bzr import LineEndingError from breezy.branch import ( Branch, ) from breezy.controldir import ControlDir, Prober from breezy.git.remote import RemoteGitError from breezy.transport import Transport, get_transport from breezy.workingtree import WorkingTree from breezy.transport import UnusableRedirect def create_temp_sprout( branch: Branch, additional_colocated_branches: Optional[Union[List[str], Dict[str, str]]] = None, dir: Optional[str] = None, path: Optional[str] = None, ) -> Tuple[WorkingTree, Callable[[], None]]: """Create a temporary sprout of a branch. This attempts to fetch the least amount of history as possible. """ if path is None: td = tempfile.mkdtemp(dir=dir) else: td = path os.mkdir(path) def destroy() -> None: 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 # type: ignore branch.repository._format.supports_chks ) try: # preserve whatever source format we have. to_dir = branch.controldir.sprout( # type: ignore td, None, create_tree_if_local=True, source_branch=branch, stacked=use_stacking, ) # TODO(jelmer): Fetch these during the initial clone for from_branch_name in set(additional_colocated_branches or []): try: add_branch = branch.controldir.open_branch( # type: ignore name=from_branch_name) except (errors.NotBranchError, errors.NoColocatedBranchSupport): pass else: if isinstance(additional_colocated_branches, dict): to_branch_name = additional_colocated_branches[from_branch_name] else: to_branch_name = from_branch_name local_add_branch = to_dir.create_branch(name=to_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: Branch, additional_colocated_branches: Optional[List[str]] = None, dir: Optional[str] = None, ): self.branch = branch self.additional_colocated_branches = additional_colocated_branches self.dir = dir def __enter__(self) -> WorkingTree: 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: WorkingTree, script: Optional[str]) -> None: """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: WorkingTree, script: Optional[str], since_revid: bytes ) -> None: """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: str, description: str): self.url = url self.description = description def __str__(self) -> str: return self.description class BranchRateLimited(Exception): """Opening branch was rate-limited.""" def __init__(self, url: str, description: str, retry_after: Optional[int] = None): self.url = url self.description = description self.retry_after = retry_after def __str__(self) -> str: if self.retry_after is not None: return "%s (retry after %s)" % (self.description, self.retry_after) else: return self.description class BranchMissing(Exception): """Branch did not exist.""" def __init__(self, url: str, description: str): self.url = url self.description = description def __str__(self) -> str: return self.description class BranchUnsupported(Exception): """The branch uses a VCS or protocol that is unsupported.""" def __init__(self, url: str, description: str): self.url = url self.description = description def __str__(self) -> str: return self.description def _convert_exception(url: str, e: Exception) -> Optional[Exception]: if isinstance(e, socket.error): return BranchUnavailable(url, "Socket error: %s" % e) if isinstance(e, errors.NotBranchError): return BranchMissing(url, "Branch does not exist: %s" % e) if isinstance(e, errors.UnsupportedProtocol): return BranchUnsupported(url, str(e)) if isinstance(e, errors.ConnectionError): return BranchUnavailable(url, str(e)) if isinstance(e, errors.PermissionDenied): return BranchUnavailable(url, str(e)) if isinstance(e, errors.InvalidHttpResponse): if "Unexpected HTTP status 429" in str(e): if hasattr(e, 'headers'): try: retry_after = int(e.headers['Retry-After']) # type: ignore except TypeError: logging.warning( 'Unable to parse retry-after header: %s', e.headers['Retry-After']) # type: ignore retry_after = None else: retry_after = None else: # Breezy < 3.2.1 retry_after = None raise BranchRateLimited(url, str(e), retry_after=retry_after) return BranchUnavailable(url, str(e)) if isinstance(e, errors.TransportError): return BranchUnavailable(url, str(e)) if UnusableRedirect is not None and isinstance(e, UnusableRedirect): return BranchUnavailable(url, str(e)) if isinstance(e, errors.UnsupportedFormatError): return BranchUnsupported(url, str(e)) if isinstance(e, errors.UnknownFormatError): return BranchUnsupported(url, str(e)) if isinstance(e, RemoteGitError): return BranchUnavailable(url, str(e)) if isinstance(e, LineEndingError): return BranchUnavailable(url, str(e)) return None def open_branch( url: str, possible_transports: Optional[List[Transport]] = None, probers: Optional[List[Prober]] = None, name: str = None, ) -> Branch: """Open a branch by URL.""" url, params = urlutils.split_segment_parameters(url) if name is None: try: name = urlutils.unquote(params["branch"]) except KeyError: name = None try: transport = get_transport(url, possible_transports=possible_transports) dir = ControlDir.open_from_transport(transport, probers) return dir.open_branch(name=name) except Exception as e: converted = _convert_exception(url, e) if converted is not None: raise converted raise e def open_branch_containing( url: str, possible_transports: Optional[List[Transport]] = None, probers: Optional[List[Prober]] = None, ) -> Tuple[Branch, str]: """Open a branch by URL.""" try: transport = get_transport(url, possible_transports=possible_transports) dir, subpath = ControlDir.open_containing_from_transport(transport, probers) # type: ignore return dir.open_branch(), subpath except Exception as e: converted = _convert_exception(url, e) if converted is not None: raise converted raise e def full_branch_url(branch): """Get the full URL for a branch. Ideally this should just return Branch.user_url, but that currently exclude the branch name in some situations. """ if branch.name is None: return branch.user_url url, params = urlutils.split_segment_parameters(branch.user_url) if branch.name != "": params["branch"] = urlutils.quote(branch.name, "") return urlutils.join_segment_parameters(url, params) silver-platter_0.4.5.orig/silver_platter/workspace.py0000644000000000000000000003337414164061666020077 0ustar00#!/usr/bin/python # Copyright (C) 2018-2020 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 logging from typing import Optional, Callable, List, Union, Dict, BinaryIO, Any, Tuple, Iterator from breezy.branch import Branch from breezy.tree import Tree from breezy.workingtree import WorkingTree from breezy.diff import show_diff_trees from breezy.errors import ( DivergedBranches, NotBranchError, NoColocatedBranchSupport, ) from breezy.propose import ( get_hoster, Hoster, MergeProposal, UnsupportedHoster, ) from breezy.transport.local import LocalTransport from .publish import ( propose_changes, push_changes, push_derived_changes, publish_changes as _publish_changes, PublishResult, ) from .utils import ( create_temp_sprout, full_branch_url, ) __all__ = [ "Workspace", ] logger = logging.getLogger(__name__) def pull_colocated(tree, from_branch, additional_colocated_branches): logger.debug( "Fetching colocated branches: %r", additional_colocated_branches, ) for from_branch_name in additional_colocated_branches or []: try: remote_colo_branch = from_branch.controldir.open_branch( name=from_branch_name ) except (NotBranchError, NoColocatedBranchSupport): continue if isinstance(additional_colocated_branches, dict): to_branch_name = additional_colocated_branches[from_branch_name] else: to_branch_name = from_branch_name tree.branch.controldir.push_branch( name=to_branch_name, source=remote_colo_branch, overwrite=True ) class Workspace(object): """Workspace for creating changes to a branch. Args: main_branch: The upstream branch resume_branch: Optional in-progress branch that we previously made changes on, and should ideally continue from. resume_branch_additional_colocated_branches: Additional list of colocated branches to fetch cached_branch: Branch to copy revisions from, if possible. local_tree: The tree the user can work in """ _destroy: Optional[Callable[[], None]] local_tree: WorkingTree main_branch_revid: Optional[bytes] main_colo_revid: Dict[Optional[str], bytes] @classmethod def from_url(cls, url, dir=None): return cls(main_branch=Branch.open(url), dir=dir) def __init__( self, main_branch: Branch, resume_branch: Optional[Branch] = None, cached_branch: Optional[Branch] = None, additional_colocated_branches: Optional[Union[List[str], Dict[str, str]]] = None, resume_branch_additional_colocated_branches: Optional[Union[List[str], Dict[str, str]]] = None, dir: Optional[str] = None, path: Optional[str] = None, ) -> None: self.main_branch = main_branch self.main_branch_revid = None self.cached_branch = cached_branch self.resume_branch = resume_branch self.additional_colocated_branches = additional_colocated_branches or {} self.resume_branch_additional_colocated_branches = resume_branch_additional_colocated_branches self._destroy = None self._dir = dir self._path = path def _iter_additional_colocated(self) -> Iterator[Tuple[Optional[str], str]]: if isinstance(self.additional_colocated_branches, dict): return iter(self.additional_colocated_branches.items()) else: return iter(zip(self.additional_colocated_branches, self.additional_colocated_branches)) @property def path(self): return self.local_tree.abspath('.') def __str__(self): if self._path is None: return "Workspace for %s" % full_branch_url(self.main_branch) else: return "Workspace for %s at %s" % ( full_branch_url(self.main_branch), self._path, ) def __repr__(self): return ( "%s(%r, resume_branch=%r, cached_branch=%r, " "additional_colocated_branches=%r, " "resume_branch_additional_colocated_branches=%r, dir=%r, path=%r)" % ( type(self).__name__, self.main_branch, self.resume_branch, self.cached_branch, self.additional_colocated_branches, self.resume_branch_additional_colocated_branches, self._dir, self._path, ) ) def _inverse_additional_colocated_branches(self): return { to_name: from_name for from_name, to_name in self._iter_additional_colocated()} def __enter__(self) -> Any: for (sprout_base, sprout_coloc) in [ (self.cached_branch, self.additional_colocated_branches), (self.resume_branch, self.resume_branch_additional_colocated_branches), (self.main_branch, self.additional_colocated_branches)]: if sprout_base: break else: raise ValueError('main branch needs to be specified') logger.debug("Creating sprout from %r", sprout_base) self.local_tree, self._destroy = create_temp_sprout( sprout_base, sprout_coloc, dir=self._dir, path=self._path, ) self.main_branch_revid = self.main_branch.last_revision() self.main_colo_revid = {} for from_name, to_name in self._iter_additional_colocated(): try: branch = self.main_branch.controldir.open_branch(name=from_name) # type: ignore except (NotBranchError, NoColocatedBranchSupport): continue self.main_colo_revid[to_name] = branch.last_revision() self.refreshed = False if self.cached_branch: logger.debug( "Pulling in missing revisions from resume/main branch %r", self.resume_branch or self.main_branch, ) self.local_tree.pull( self.resume_branch or self.main_branch, overwrite=True ) # At this point, we're either on the tip of the main branch or the tip # of the resume branch if self.resume_branch: # If there's a resume branch at play, make sure it's derived from # the main branch *or* reset back to the main branch. logger.debug( "Pulling in missing revisions from main branch %r", self.main_branch ) try: self.local_tree.pull(self.main_branch, overwrite=False) except DivergedBranches: logger.info("restarting branch") self.refreshed = True self.resume_branch = None self.resume_branch_additional_colocated_branches = None self.local_tree.pull(self.main_branch, overwrite=True) pull_colocated(self.local_tree, self.main_branch, self.additional_colocated_branches) else: pull_colocated(self.local_tree, self.resume_branch, self.resume_branch_additional_colocated_branches) else: pull_colocated(self.local_tree, self.main_branch, self.additional_colocated_branches) self.base_revid = self.local_tree.last_revision() return self def defer_destroy(self) -> Optional[Callable[[], None]]: ret = self._destroy self._destroy = None return ret def changes_since_main(self) -> bool: return self.local_tree.branch.last_revision() != self.main_branch_revid def changes_since_base(self) -> bool: return self.base_revid != self.local_tree.branch.last_revision() def any_branch_changes(self): for name, br, r in self.result_branches(): if br != r: return True return False def result_branches(self) -> List[ Tuple[Optional[str], Optional[bytes], Optional[bytes]]]: branches = [ (self.main_branch.name, self.main_branch_revid, # type: ignore self.local_tree.last_revision())] # TODO(jelmer): Perhaps include resume colocated branches that don't # appear in additional_colocated_branches ? for from_name, to_name in self._iter_additional_colocated(): to_revision: Optional[bytes] try: to_branch = self.local_tree.controldir.open_branch(name=to_name) except (NotBranchError, NoColocatedBranchSupport): to_revision = None else: to_revision = to_branch.last_revision() from_revision = self.main_colo_revid.get(from_name) if from_revision is None and to_revision is None: continue branches.append((from_name, from_revision, to_revision)) return branches def push( self, hoster: Optional[Hoster] = None, dry_run: bool = False, tags: Optional[Union[Dict[str, bytes], List[str]]] = None, stop_revision: Optional[bytes] = None, ) -> None: if hoster is None: try: hoster = get_hoster(self.main_branch) except UnsupportedHoster: if not isinstance(self.main_branch.control_transport, LocalTransport): logging.warning( 'Unable to find hoster for %s to determine push url, ' 'trying anyway.', self.main_branch.user_url) hoster = None return push_changes( self.local_tree.branch, self.main_branch, hoster=hoster, additional_colocated_branches=self._inverse_additional_colocated_branches(), dry_run=dry_run, tags=tags, stop_revision=stop_revision, ) def propose( self, name: str, description: str, hoster: Optional[Hoster] = None, existing_proposal: Optional[MergeProposal] = None, overwrite_existing: Optional[bool] = None, labels: Optional[List[str]] = None, dry_run: bool = False, commit_message: Optional[str] = None, reviewers: Optional[List[str]] = None, tags: Optional[Union[Dict[str, bytes], List[str]]] = None, owner: Optional[str] = None, allow_collaboration: bool = False, stop_revision: Optional[bytes] = None, ) -> Tuple[MergeProposal, bool]: 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 or False), labels=labels, dry_run=dry_run, commit_message=commit_message, reviewers=reviewers, owner=owner, additional_colocated_branches=self._inverse_additional_colocated_branches(), tags=tags, allow_collaboration=allow_collaboration, stop_revision=stop_revision, ) def push_derived( self, name: str, hoster: Optional[Hoster] = None, overwrite_existing: Optional[bool] = False, owner: Optional[str] = None, tags: Optional[Union[Dict[str, bytes], List[str]]] = None, stop_revision: Optional[bytes] = None, ) -> Tuple[Branch, str]: """Push a derived branch. Args: name: Branch name hoster: Optional hoster to use overwrite_existing: Whether to overwrite an existing branch tags: Tags list to push owner: Owner name 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, owner=owner, tags=tags, stop_revision=stop_revision, ) def publish_changes(self, *args, **kwargs) -> PublishResult: """Publish a set of changes.""" return _publish_changes( self.local_tree.branch, self.main_branch, self.resume_branch, *args, **kwargs ) def base_tree(self) -> Tree: return self.local_tree.branch.repository.revision_tree(self.base_revid) def show_diff( self, outf: BinaryIO, old_label: str = "old/", new_label: str = "new/" ) -> None: base_tree = self.base_tree() show_diff_trees( base_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 silver-platter_0.4.5.orig/silver_platter/debian/__init__.py0000644000000000000000000002503214164061666021052 0ustar00# # 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 datetime import datetime from debian.deb822 import Deb822 from debian.changelog import Version import os from typing import Optional, Dict, List, Tuple from debmutate.vcs import split_vcs_url from debmutate.changelog import ( Changelog, changelog_add_entry as _changelog_add_entry, ) from breezy import urlutils from breezy.branch import Branch from breezy.errors import UnsupportedFormatError from breezy.controldir import Prober, ControlDirFormat from breezy.bzr import RemoteBzrProber from breezy.git import RemoteGitProber from breezy.git.repository import GitRepository from breezy.mutabletree import MutableTree from breezy.plugins.debian.cmds import cmd_builddeb from breezy.plugins.debian.directory import ( source_package_vcs, vcs_field_to_bzr_url_converters, ) from breezy.tree import Tree from breezy.urlutils import InvalidURL from breezy.workingtree import WorkingTree from breezy.plugins.debian.builder import BuildFailedError from breezy.plugins.debian.changelog import debcommit from breezy.plugins.debian.upstream import ( MissingUpstreamTarball, ) from lintian_brush.detect_gbp_dch import guess_update_changelog from .. import workspace as _mod_workspace from ..utils import ( open_branch, ) __all__ = [ "add_changelog_entry", "apt_get_source_package", "guess_update_changelog", "source_package_vcs", "build", "BuildFailedError", "MissingUpstreamTarball", "vcs_field_to_bzr_url_converters", ] DEFAULT_URGENCY = "medium" DEFAULT_BUILDER = "sbuild --no-clean-source" class NoSuchPackage(Exception): """No such package.""" class NoVcsInformation(Exception): """Package does not have any Vcs headers.""" def add_changelog_entry( tree: MutableTree, path: str, summary: List[str], maintainer: Optional[Tuple[str, str]] = None, timestamp: Optional[datetime] = None, urgency: str = DEFAULT_URGENCY, ) -> None: """Add a changelog entry. Args: tree: Tree to edit path: Path to the changelog file summary: Entry to add maintainer: Maintainer details; tuple of fullname and email suppress_warnings: Whether to suppress any warnings from 'dch' """ # TODO(jelmer): This logic should ideally be in python-debian. with tree.get_file(path) as f: cl = Changelog() cl.parse_changelog(f, max_blocks=None, allow_empty_author=True, strict=False) _changelog_add_entry( cl, summary=summary, maintainer=maintainer, timestamp=timestamp, urgency=urgency, ) # Workaround until # https://salsa.debian.org/python-debian-team/python-debian/-/merge_requests/22 # lands. pieces = [] for line in cl.initial_blank_lines: pieces.append(line.encode(cl._encoding) + b"\n") for block in cl._blocks: try: serialized = block._format(allow_missing_author=True).encode( block._encoding ) except TypeError: # older python-debian serialized = bytes(block) pieces.append(serialized) tree.put_file_bytes_non_atomic(path, b"".join(pieces)) def build( tree: WorkingTree, subpath: str = "", builder: Optional[str] = None, result_dir: Optional[str] = None, ) -> None: """Build a debian package in a directory. Args: tree: Working tree subpath: Subpath to build in 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.abspath(subpath)], builder=builder, result_dir=result_dir) class NoAptSources(Exception): """No apt sources were configured.""" def apt_get_source_package(name: str) -> Deb822: """Get source package metadata. Args: name: Name of the source package Returns: A `Deb822` object """ import apt_pkg apt_pkg.init() try: sources = apt_pkg.SourceRecords() except apt_pkg.Error as e: if e.args[0] == ("E:You must put some 'deb-src' URIs in your sources.list"): raise NoAptSources() raise by_version: Dict[str, Deb822] = {} while sources.lookup(name): by_version[sources.version] = sources.record # type: ignore 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 convert_debian_vcs_url(vcs_type: str, vcs_url: str) -> str: 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 and ":" not in location: pkg_source = apt_get_source_package(location) try: (vcs_type, vcs_url) = source_package_vcs(pkg_source) except KeyError: raise NoVcsInformation(location) (url, branch_name, subpath) = split_vcs_url(vcs_url) else: url, params = urlutils.split_segment_parameters(location) try: branch_name = urlutils.unquote(params["branch"]) except KeyError: branch_name = None subpath = "" probers = select_probers(vcs_type) branch = open_branch( url, possible_transports=possible_transports, probers=probers, name=branch_name ) return branch, subpath or "" def pick_additional_colocated_branches( main_branch: Branch) -> Dict[str, str]: ret = { "pristine-tar": "pristine-tar", "pristine-lfs": "pristine-lfs", "upstream": "upstream", } ret["patch-queue/" + main_branch.name] = "patch-queue" # type: ignore if main_branch.name.startswith("debian/"): # type: ignore parts = main_branch.name.split("/") # type: ignore parts[0] = "upstream" ret["/".join(parts)] = "upstream" return ret class Workspace(_mod_workspace.Workspace): def __init__(self, main_branch: Branch, *args, **kwargs) -> None: if isinstance(main_branch.repository, GitRepository): if "additional_colocated_branches" not in kwargs: kwargs["additional_colocated_branches"] = {} kwargs["additional_colocated_branches"].update( pick_additional_colocated_branches(main_branch)) super(Workspace, self).__init__(main_branch, *args, **kwargs) @classmethod def from_apt_package(cls, package, dir=None): main_branch = open_packaging_branch(package) return cls(main_branch=main_branch, dir=dir) def build( self, builder: Optional[str] = None, result_dir: Optional[str] = None, subpath: str = "", ) -> None: return build( tree=self.local_tree, subpath=subpath, builder=builder, result_dir=result_dir, ) def commit(self, message=None, subpath="", paths=None, committer=None, reporter=None): return debcommit( self.local_tree, committer=committer, subpath=subpath, paths=paths, reporter=reporter, message=message) class UnsupportedVCSProber(Prober): def __init__(self, vcs_type): self.vcs_type = vcs_type def __eq__(self, other): return isinstance(other, type(self)) and other.vcs_type == self.vcs_type def __call__(self): # The prober expects to be registered as a class. return self def priority(self, transport): return 200 def probe_transport(self, transport): raise UnsupportedFormatError( "This VCS %s is not currently supported." % self.vcs_type ) @classmethod def known_formats(klass): return [] prober_registry = { "bzr": RemoteBzrProber, "git": RemoteGitProber, } try: from breezy.plugins.fossil import RemoteFossilProber except ImportError: pass else: prober_registry["fossil"] = RemoteFossilProber try: from breezy.plugins.svn import SvnRepositoryProber except ImportError: pass else: prober_registry["svn"] = SvnRepositoryProber try: from breezy.plugins.hg import SmartHgProber except ImportError: pass else: prober_registry["hg"] = SmartHgProber try: from breezy.plugins.darcs import DarcsProber except ImportError: pass else: prober_registry["darcs"] = DarcsProber try: from breezy.plugins.cvs import CVSProber except ImportError: pass else: prober_registry["cvs"] = CVSProber def select_probers(vcs_type=None): if vcs_type is None: return None try: return [prober_registry[vcs_type.lower()]] except KeyError: return [UnsupportedVCSProber(vcs_type)] def select_preferred_probers(vcs_type: Optional[str] = None) -> List[Prober]: probers = list(ControlDirFormat.all_probers()) if vcs_type: try: probers.insert(0, prober_registry[vcs_type.lower()]) except KeyError: pass return probers def is_debcargo_package(tree: Tree, subpath: str) -> bool: control_path = os.path.join(subpath, "debian", "debcargo.toml") return tree.has_filename(control_path) def control_files_in_root(tree: Tree, subpath: str) -> bool: debian_path = os.path.join(subpath, "debian") if tree.has_filename(debian_path): return False control_path = os.path.join(subpath, "control") if tree.has_filename(control_path): return True if tree.has_filename(control_path + ".in"): return True return False silver-platter_0.4.5.orig/silver_platter/debian/__main__.py0000644000000000000000000000507614164061666021041 0ustar00#!/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 logging from typing import Optional, Dict, List, Callable import silver_platter # noqa: F401 import argparse import sys from . import ( apply as debian_apply, run as debian_run, uploader as debian_uploader, ) def main(argv: Optional[List[str]] = None) -> Optional[int]: import breezy breezy.initialize() from ..__main__ import subcommands as main_subcommands subcommands: Dict[str, Callable[[List[str]], Optional[int]]] = { "upload-pending": debian_uploader.main, "apply": debian_apply.main, "run": debian_run.main, } parser = argparse.ArgumentParser(prog="debian-svp", add_help=False) parser.add_argument( "--version", action="version", version="%(prog)s " + silver_platter.version_string, ) parser.add_argument( "--debug", action="store_true", help="Be more verbose") parser.add_argument( "--help", action="store_true", help="show this help message and exit" ) for name, cmd in main_subcommands.items(): if name not in subcommands: subcommands[name] = cmd parser.add_argument( "subcommand", type=str, choices=list(subcommands.keys()) ) args, rest = parser.parse_known_args() if args.help: if args.subcommand is None: parser.print_help() parser.exit() else: rest.append("--help") if args.debug: level = logging.DEBUG else: level = logging.INFO logging.basicConfig(level=level, format="%(message)s") if args.subcommand is None: parser.print_usage() return 1 if args.subcommand in subcommands: return subcommands[args.subcommand](rest) parser.print_usage() return 1 if __name__ == "__main__": sys.exit(main()) silver-platter_0.4.5.orig/silver_platter/debian/apply.py0000644000000000000000000003017314164061666020442 0ustar00#!/usr/bin/python # Copyright (C) 2021 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 dataclasses import dataclass, field import logging import json import os import sys import tempfile from typing import List, Optional, Tuple, Dict import subprocess from debian.changelog import Changelog from debian.deb822 import Deb822 from breezy.commit import PointlessCommit from breezy.workingtree import WorkingTree from breezy.workspace import reset_tree, check_clean_tree from ..apply import ( ResultFileFormatError, ScriptFailed, ScriptMadeNoChanges, ) from . import ( add_changelog_entry, build, control_files_in_root, guess_update_changelog, BuildFailedError, MissingUpstreamTarball, DEFAULT_BUILDER, ) class MissingChangelog(Exception): """No changelog file is present.""" class DetailedFailure(Exception): """Detailed failure""" def __init__(self, source_name, result_code, description, details=None): self.source = source_name self.result_code = result_code self.description = description self.details = details @classmethod def from_json(cls, source_name, json): return cls( source_name, result_code=json.get('result_code'), description=json.get('description'), details=json.get('details')) @dataclass class CommandResult(object): source: Optional[str] description: Optional[str] = None value: Optional[int] = None serialized_context: Optional[str] = None context: Dict[str, str] = field(default_factory=dict) tags: List[Tuple[str, bytes]] = field(default_factory=list) old_revision: Optional[bytes] = None new_revision: Optional[bytes] = None target_branch_url: Optional[str] = None @classmethod def from_json(cls, source, data): if 'tags' in data: tags = [] for name, revid in data['tags']: tags.append((name, revid.encode('utf-8'))) else: tags = None return cls( source=source, value=data.get('value', None), context=data.get('context', {}), serialized_context=data.get('serialized_context', None), description=data.get('description'), target_branch_url=data.get('target-branch-url', None), tags=tags) def install_built_package(local_tree, subpath, build_target_dir): import re import subprocess with open(local_tree.abspath(os.path.join(subpath, 'debian/changelog')), 'r') as f: cl = Changelog(f) non_epoch_version = cl[0].version.upstream_version if cl[0].version.debian_version is not None: non_epoch_version += "-%s" % cl[0].version.debian_version c = re.compile('%s_%s_(.*).changes' % (re.escape(cl[0].package), re.escape(non_epoch_version))) # type: ignore for entry in os.scandir(build_target_dir): if not c.match(entry.name): continue with open(entry.path, 'rb') as g: changes = Deb822(g) if changes.get('Binary'): subprocess.check_call(['debi', entry.path]) def script_runner( # noqa: C901 local_tree: WorkingTree, script: str, commit_pending: Optional[bool] = None, resume_metadata=None, subpath: str = '', update_changelog: Optional[bool] = None, extra_env: Optional[Dict[str, str]] = None, committer: Optional[str] = None ) -> CommandResult: # noqa: C901 """Run a script in a tree and commit the result. This ignores newly added files. Args: local_tree: Local tree to run script in script: Script to run commit_pending: Whether to commit pending changes (True, False or None: only commit if there were no commits by the script) """ if control_files_in_root(local_tree, subpath): debian_path = subpath else: debian_path = os.path.join(subpath, "debian") if update_changelog is None: dch_guess = guess_update_changelog(local_tree, debian_path) if dch_guess: logging.info('%s', dch_guess[1]) update_changelog = dch_guess[0] else: # Assume yes. update_changelog = True cl_path = os.path.join(debian_path, 'changelog') try: with open(local_tree.abspath(cl_path), 'r') as f: cl = Changelog(f) source_name = cl[0].package except FileNotFoundError: source_name = None env = dict(os.environ) if extra_env: env.update(extra_env) env['SVP_API'] = '1' if source_name: env['DEB_SOURCE'] = source_name if update_changelog: env['DEB_UPDATE_CHANGELOG'] = 'update' else: env['DEB_UPDATE_CHANGELOG'] = 'leave' last_revision = local_tree.last_revision() orig_tags = local_tree.branch.tags.get_tag_dict() with tempfile.TemporaryDirectory() as td: env['SVP_RESULT'] = os.path.join(td, 'result.json') if resume_metadata: env['SVP_RESUME'] = os.path.join(td, 'resume-metadata.json') with open(env['SVP_RESUME'], 'w') as f: json.dump(resume_metadata, f) p = subprocess.Popen( script, cwd=local_tree.abspath(subpath), stdout=subprocess.PIPE, shell=True, env=env) (description_encoded, err) = p.communicate(b"") try: with open(env['SVP_RESULT'], 'r') as f: try: result_json = json.load(f) except json.decoder.JSONDecodeError as e: raise ResultFileFormatError(e) except FileNotFoundError: result_json = None if p.returncode != 0: if result_json is not None: raise DetailedFailure.from_json(source_name, result_json) raise ScriptFailed(script, p.returncode) # If the changelog didn't exist earlier, then hopefully it was created # now. if source_name is None: try: with open(local_tree.abspath(cl_path), 'r') as f: cl = Changelog(f) source_name = cl[0].package except FileNotFoundError: raise MissingChangelog(cl_path) if result_json is not None: result = CommandResult.from_json(source_name, result_json) else: result = CommandResult(source=source_name) if not result.description: result.description = description_encoded.decode().replace("\r", "") new_revision = local_tree.last_revision() if result.tags is None: result.tags = [] for name, revid in local_tree.branch.tags.get_tag_dict().items(): if orig_tags.get(name) != revid: result.tags.append((name, revid)) 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: if update_changelog and result.description and local_tree.has_changes(): add_changelog_entry( local_tree, os.path.join(debian_path, 'changelog'), [result.description]) try: new_revision = local_tree.commit( result.description, allow_pointless=False, committer=committer) except PointlessCommit: pass if new_revision == last_revision: raise ScriptMadeNoChanges() result.old_revision = last_revision result.new_revision = new_revision return result def main(argv: List[str]) -> Optional[int]: # noqa: C901 import argparse parser = argparse.ArgumentParser() parser.add_argument( "--command", help="Path to script to run.", type=str) parser.add_argument( "--diff", action="store_true", help="Show diff of generated changes." ) 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( "--commit-pending", help="Commit pending changes after script.", choices=["yes", "no", "auto"], default=None, type=str, ) parser.add_argument( "--build-verify", help="Build package to verify it.", dest="build_verify", action="store_true", ) parser.add_argument( "--builder", default=DEFAULT_BUILDER, type=str, help="Build command to use when verifying build.", ) parser.add_argument( "--build-target-dir", type=str, help=( "Store built Debian files in specified directory " "(with --build-verify)" ), ) parser.add_argument( "--install", "-i", action="store_true", help="Install built package (implies --build-verify)") parser.add_argument( "--dump-context", action="store_true", help="Report context on success") parser.add_argument( "--recipe", type=str, help="Recipe to use.") args = parser.parse_args(argv) if args.recipe: from ..recipe import Recipe recipe = Recipe.from_path(args.recipe) else: recipe = None if args.commit_pending: commit_pending = {"auto": None, "yes": True, "no": False}[args.commit_pending] elif recipe: commit_pending = recipe.commit_pending else: commit_pending = None if args.command: command = args.command elif recipe and recipe.command: command = recipe.command else: logging.exception('No command or recipe specified.') return 1 local_tree, subpath = WorkingTree.open_containing('.') check_clean_tree(local_tree) try: try: result = script_runner( local_tree, script=command, commit_pending=commit_pending, subpath=subpath, update_changelog=args.update_changelog) except MissingChangelog as e: logging.error('No debian changelog file (%s) present', e.args[0]) return False except ScriptMadeNoChanges: logging.info('Script made no changes') return False if result.description: logging.info('Succeeded: %s', result.description) if args.build_verify or args.install: try: build(local_tree, subpath, builder=args.builder, result_dir=args.build_target_dir) except BuildFailedError: logging.error("%s: build failed", result.source) return False except MissingUpstreamTarball: logging.error("%s: unable to find upstream source", result.source) return False except Exception: reset_tree(local_tree, subpath) raise if args.install: install_built_package(local_tree, subpath, args.build_target_dir) if args.diff: from breezy.diff import show_diff_trees old_tree = local_tree.revision_tree(result.old_revision) new_tree = local_tree.revision_tree(result.new_revision) show_diff_trees( old_tree, new_tree, sys.stdout.buffer, old_label='old/', new_label='new/') if args.dump_context: json.dump(result.context, sys.stdout, indent=5) return 0 silver-platter_0.4.5.orig/silver_platter/debian/run.py0000644000000000000000000003010614164061666020115 0ustar00#!/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 argparse import logging import os import sys from typing import Optional, List from breezy import osutils from breezy import propose as _mod_propose from breezy.urlutils import InvalidURL import silver_platter # noqa: F401 from . import ( DEFAULT_BUILDER, BuildFailedError, MissingUpstreamTarball, build, ) from .apply import ( script_runner, MissingChangelog, ScriptMadeNoChanges, ScriptFailed, install_built_package, ) from ..candidates import CandidateList, Candidate from ..proposal import ( UnsupportedHoster, enable_tag_pushing, find_existing_proposed, get_hoster, ) from . import ( Workspace, ) from ..publish import ( SUPPORTED_MODES, InsufficientChangesForNewProposal, ) from ..utils import ( open_branch, BranchMissing, BranchUnsupported, BranchUnavailable, full_branch_url, ) def derived_branch_name(script: str) -> str: return os.path.splitext(osutils.basename(script.split(" ")[0]))[0] def apply_and_publish( # noqa: C901 url: str, name: str, command: str, mode: str, subpath: str = '', commit_pending: Optional[bool] = None, dry_run: bool = False, labels: Optional[List[str]] = None, diff: bool = False, verify_command: Optional[str] = None, derived_owner: Optional[str] = None, refresh: bool = False, allow_create_proposal=None, get_commit_message=None, get_description=None, build_verify=False, builder=DEFAULT_BUILDER, install=False, build_target_dir=None, update_changelog: Optional[bool] = None, preserve_repositories: bool = False): try: main_branch = open_branch(url) except (BranchUnavailable, BranchMissing, BranchUnsupported) as e: logging.fatal("%s: %s", url, e) return 1 except InvalidURL as e: logging.fatal('%s: %s', url, e) return 1 overwrite = False try: hoster = get_hoster(main_branch) except UnsupportedHoster as e: if 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 logging.warn( "Unsupported hoster (%s), will attempt to push to %s", e, full_branch_url(main_branch), ) except _mod_propose.HosterLoginRequired as e: logging.error( '%s: Hoster login required: %s', full_branch_url(main_branch), e) return 1 else: (resume_branch, resume_overwrite, existing_proposal) = find_existing_proposed( main_branch, hoster, name, owner=derived_owner ) if resume_overwrite is not None: overwrite = resume_overwrite if refresh: resume_branch = None with Workspace(main_branch, resume_branch=resume_branch) as ws: try: result = script_runner( ws.local_tree, command, commit_pending, update_changelog=update_changelog) except MissingChangelog as e: logging.error("No debian changelog (%s) present", e.args[0]) return 1 except ScriptMadeNoChanges: logging.error("Script did not make any changes.") return 1 except ScriptFailed: logging.error("Script failed to run.") return 1 if build_verify or install: try: build(ws.local_tree, subpath, builder=builder, result_dir=build_target_dir) except BuildFailedError: logging.info("%s: build failed", result.source) return False except MissingUpstreamTarball: logging.info("%s: unable to find upstream source", result.source) return False if install: install_built_package(ws.local_tree, subpath, build_target_dir) enable_tag_pushing(ws.local_tree.branch) try: publish_result = ws.publish_changes( mode, name, get_proposal_description=lambda df, ep: get_description(result, df, ep), get_proposal_commit_message=lambda ep: get_commit_message(result, ep), allow_create_proposal=lambda: allow_create_proposal(result), dry_run=dry_run, hoster=hoster, labels=labels, overwrite_existing=overwrite, derived_owner=derived_owner, existing_proposal=existing_proposal, ) except UnsupportedHoster as e: logging.exception( "No known supported hoster for %s. Run 'svp login'?", full_branch_url(e.branch), ) return 1 except InsufficientChangesForNewProposal: logging.info('Insufficient changes for a new merge proposal') return 0 except _mod_propose.HosterLoginRequired as e: logging.exception( "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: logging.info("Merge proposal created.") else: logging.info("Merge proposal updated.") if publish_result.proposal.url: logging.info("URL: %s", publish_result.proposal.url) logging.info("Description: %s", publish_result.proposal.get_description()) if diff: ws.show_diff(sys.stdout.buffer) if preserve_repositories: ws.defer_destroy() logging.info('Workspace preserved in %s', ws.local_tree.abspath(ws.subpath)) def main(argv: List[str]) -> Optional[int]: # noqa: C901 parser = argparse.ArgumentParser() parser.add_argument("url", help="URL of branch to work on.", type=str) parser.add_argument( "--command", help="Path to script to run.", type=str) parser.add_argument( "--derived-owner", type=str, default=None, help="Owner for derived branches." ) 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( "--preserve-repositories", action="store_true", help="Preserve temporary repositories.") 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=None, 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.", dest="build_verify", action="store_true", ) parser.add_argument( "--builder", default=DEFAULT_BUILDER, type=str, help="Build command to use when verifying build.", ) parser.add_argument( "--build-target-dir", type=str, help=( "Store built Debian files in specified directory " "(with --build-verify)" ), ) parser.add_argument( "--install", "-i", action="store_true", help="Install built package (implies --build-verify)") parser.add_argument( "--recipe", type=str, help="Recipe to use.") parser.add_argument( "--candidates", type=str, help="File with candidate list.") 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, ) args = parser.parse_args(argv) if args.recipe: from ..recipe import Recipe recipe = Recipe.from_path(args.recipe) else: recipe = None candidates = [] if args.url: candidates = [Candidate(url=args.url)] if args.candidates: candidatelist = CandidateList.from_path(args.candidates) candidates.extend(candidatelist) if args.commit_pending: commit_pending = {"auto": None, "yes": True, "no": False}[args.commit_pending] elif recipe: commit_pending = recipe.commit_pending else: commit_pending = None if args.command: command = args.command elif recipe and recipe.command: command = recipe.command else: logging.exception('No command specified.') return 1 if args.name is not None: name = args.name elif recipe and recipe.name: name = recipe.name else: name = derived_branch_name(command) refresh = args.refresh if recipe and not recipe.resume: refresh = True def allow_create_proposal(result): if result.value is None: return True if recipe.propose_threshold is not None: return result.value >= recipe.propose_threshold return True def get_commit_message(result, existing_proposal): if recipe: return recipe.render_merge_request_commit_message(result.context) if existing_proposal is not None: return existing_proposal.get_commit_message() return None def get_description(result, description_format, existing_proposal): if recipe: description = recipe.render_merge_request_description( description_format, result.context) if description: return description if result.description is not None: return result.description if existing_proposal is not None: return existing_proposal.get_description() raise ValueError("No description available") retcode = 0 for candidate in candidates: if apply_and_publish( candidate.url, name=name, command=command, mode=args.mode, subpath=candidate.subpath, commit_pending=commit_pending, dry_run=args.dry_run, labels=args.label, diff=args.diff, derived_owner=args.derived_owner, refresh=refresh, allow_create_proposal=allow_create_proposal, get_commit_message=get_commit_message, get_description=get_description, build_verify=args.build_verify, builder=args.builder, install=args.install, build_target_dir=args.build_target_dir, update_changelog=args.update_changelog, preserve_repositories=args.preserve_repositories): retcode = 1 return retcode if __name__ == "__main__": sys.exit(main(sys.argv)) silver-platter_0.4.5.orig/silver_platter/debian/uploader.py0000644000000000000000000005372614164061666021141 0ustar00#!/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 from email.utils import parseaddr import logging import os import subprocess import sys import tempfile from typing import List from debmutate.changelog import ( ChangelogEditor, ChangelogParseError, changeblock_ensure_first_line, ) from debmutate.control import ControlEditor from breezy import gpg from breezy.config import extract_email_address from breezy.errors import NoSuchTag, PermissionDenied from breezy.commit import NullCommitReporter from breezy.plugins.debian.builder import BuildFailedError 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, MissingChangelogError, NoPreviousUpload, ) from breezy.plugins.debian.upstream import MissingUpstreamTarball from debian.changelog import get_maintainer from . import ( apt_get_source_package, source_package_vcs, split_vcs_url, Workspace, DEFAULT_BUILDER, select_probers, NoSuchPackage, ) from ..utils import ( open_branch, BranchUnavailable, BranchMissing, BranchUnsupported, BranchRateLimited, ) def connect_udd_mirror(): import psycopg2 return psycopg2.connect( database="udd", user="udd-mirror", password="udd-mirror", host="udd-mirror.debian.net", ) def debsign(path, keyid=None): (bd, changes_file) = os.path.split(path) args = ["debsign"] if keyid: args.append("-k%s" % keyid) args.append(changes_file) subprocess.check_call(args, cwd=bd) class LastUploadMoreRecent(Exception): """Last version in archive is newer than vcs version.""" def __init__(self, archive_version, vcs_version): self.archive_version = archive_version self.vcs_version = vcs_version super(LastUploadMoreRecent, self).__init__( "last upload (%s) is more recent than vcs (%s)" % (archive_version, vcs_version) ) class NoUnuploadedChanges(Exception): """Indicates there are no unuploaded changes for a package.""" def __init__(self, archive_version): self.archive_version = archive_version super(NoUnuploadedChanges, self).__init__( "nothing to upload, latest version is in archive: %s" % archive_version ) class NoUnreleasedChanges(Exception): """Indicates there are no unreleased changes for a package.""" def __init__(self, version): self.version = version super(NoUnreleasedChanges, self).__init__( "nothing to upload, latest version in vcs is not unreleased: %s" % version ) class RecentCommits(Exception): """Indicates there are too recent commits for a package.""" def __init__(self, commit_age, min_commit_age): self.commit_age = commit_age self.min_commit_age = min_commit_age super(RecentCommits, self).__init__( "Last commit is only %d days old (< %d)" % (self.commit_age, self.min_commit_age) ) class CommitterNotAllowed(Exception): """Specified committer is not allowed.""" def __init__(self, committer, allowed_committers): self.committer = committer self.allowed_committers = allowed_committers super(CommitterNotAllowed, self).__init__( "Committer %s not in allowed committers: %r" % (self.committer, self.allowed_committers) ) class LastReleaseRevisionNotFound(Exception): """The revision for the last uploaded release can't be found.""" def __init__(self, package, version): self.package = package self.version = version super(LastReleaseRevisionNotFound, self).__init__( "Unable to find revision matching version %r for %s" % (version, package) ) def check_revision(rev, min_commit_age, allowed_committers): """Check whether a revision can be included in an upload. Args: rev: revision to check min_commit_age: Minimum age for revisions allowed_committers: List of allowed committers Raises: RecentCommits: When there are commits younger than min_commit_age """ # TODO(jelmer): deal with timezone if min_commit_age is not None: commit_time = datetime.datetime.fromtimestamp(rev.timestamp) time_delta = datetime.datetime.now() - commit_time if time_delta.days < min_commit_age: raise RecentCommits(time_delta.days, min_commit_age) # TODO(jelmer): Allow tag to prevent automatic uploads committer_email = extract_email_address(rev.committer) if allowed_committers and committer_email not in allowed_committers: raise CommitterNotAllowed(committer_email, allowed_committers) 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 class GbpDchFailed(Exception): """gbp dch failed to run""" def prepare_upload_package( # noqa: C901 local_tree, subpath, pkg, last_uploaded_version, builder, gpg_strategy=None, min_commit_age=None, allowed_committers=None, ): if local_tree.has_filename(os.path.join(subpath, "debian/gbp.conf")): try: subprocess.check_call( ["gbp", "dch", "--ignore-branch"], cwd=local_tree.abspath(".") ) except subprocess.CalledProcessError: # TODO(jelmer): gbp dch sometimes fails when there is no existing # open changelog entry; it fails invoking "dpkg --lt None " raise GbpDchFailed() cl, top_level = find_changelog(local_tree, merge=False, max_blocks=None) if cl.version == last_uploaded_version: raise NoUnuploadedChanges(cl.version) try: previous_version_in_branch = changelog_find_previous_upload(cl) except NoPreviousUpload: pass else: if last_uploaded_version > previous_version_in_branch: raise LastUploadMoreRecent(last_uploaded_version, previous_version_in_branch) logging.info("Checking revisions since %s" % last_uploaded_version) with local_tree.lock_read(): try: last_release_revid = find_last_release_revid( local_tree.branch, last_uploaded_version ) except NoSuchTag: raise LastReleaseRevisionNotFound(pkg, 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: logging.info("No pending changes") return if gpg_strategy: logging.info("Verifying GPG signatures...") 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): if rev is not None: check_revision(rev, min_commit_age, allowed_committers) if cl.distributions != "UNRELEASED": raise NoUnreleasedChanges(cl.version) qa_upload = False team_upload = False control_path = local_tree.abspath(os.path.join(subpath, "debian/control")) with ControlEditor(control_path) as e: maintainer = parseaddr(e.source["Maintainer"]) if maintainer[1] == "packages@qa.debian.org": qa_upload = True # TODO(jelmer): Check whether this is a team upload # TODO(jelmer): determine whether this is a NMU upload if qa_upload or team_upload: changelog_path = local_tree.abspath(os.path.join(subpath, "debian/changelog")) with ChangelogEditor(changelog_path) as e: if qa_upload: changeblock_ensure_first_line(e[0], "QA upload.") elif team_upload: changeblock_ensure_first_line(e[0], "Team upload.") local_tree.commit( specific_files=[os.path.join(subpath, "debian/changelog")], message="Mention QA Upload.", allow_pointless=False, reporter=NullCommitReporter(), ) tag_name = release(local_tree, subpath) target_dir = tempfile.mkdtemp() builder = builder.replace("${LAST_VERSION}", last_uploaded_version) target_changes = _build_helper( local_tree, subpath, local_tree.branch, target_dir, builder=builder ) debsign(target_changes['source']) return target_changes['source'], tag_name def select_apt_packages(package_names, maintainer): packages = [] import apt_pkg apt_pkg.init() sources = apt_pkg.SourceRecords() while sources.step(): if maintainer: fullname, email = parseaddr(sources.maintainer) if email not in maintainer: continue if package_names and sources.package not in package_names: continue packages.append(sources.package) return packages def select_vcswatch_packages( packages: List[str], maintainer: List[str], autopkgtest_only: bool ): conn = connect_udd_mirror() cursor = conn.cursor() args = [] query = """\ SELECT sources.source, vcswatch.url FROM vcswatch JOIN sources ON sources.source = vcswatch.source WHERE vcswatch.status IN ('COMMITS', 'NEW') AND sources.release = 'sid' """ if autopkgtest_only: query += " AND sources.testsuite != '' " if maintainer: query += " AND sources.maintainer_email in %s" args.append(tuple(maintainer)) if packages: query += " AND sources.source IN %s" args.append(tuple(packages)) cursor.execute(query, tuple(args)) packages = [] for package, vcs_url in cursor.fetchall(): packages.append(package) return packages def main(argv): # noqa: C901 import argparse parser = argparse.ArgumentParser(prog="upload-pending-commits") parser.add_argument("packages", nargs="*") parser.add_argument("--dry-run", action="store_true", help="Dry run changes.") parser.add_argument( "--acceptable-keys", help="List of acceptable GPG keys", action="append", default=[], type=str, ) parser.add_argument( "--gpg-verification", help="Verify GPG signatures on commits", 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("--diff", action="store_true", help="Show diff.") 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.", ) parser.add_argument( "--vcswatch", action="store_true", default=False, help="Use vcswatch to determine what packages need uploading.", ) parser.add_argument( "--exclude", type=str, action="append", default=[], help="Ignore source package" ) parser.add_argument( "--autopkgtest-only", action="store_true", help="Only process packages with autopkgtests.", ) parser.add_argument( "--allowed-committer", type=str, action="append", help="Require that all new commits are from specified committers", ) args = parser.parse_args(argv) ret = 0 if not args.packages and not args.maintainer: (name, email) = get_maintainer() if email: logging.info("Processing packages maintained by %s", email) args.maintainer = [email] else: parser.print_usage() sys.exit(1) if args.vcswatch: packages = select_vcswatch_packages( args.packages, args.maintainer, args.autopkgtest_only ) else: logging.info( "Use --vcswatch to only process packages for which " "vcswatch found pending commits." ) if args.maintainer: packages = select_apt_packages(args.packages, args.maintainer) else: packages = args.packages if not packages: logging.info("No packages found.") parser.print_usage() sys.exit(1) # TODO(jelmer): Sort packages by last commit date; least recently changed # commits are more likely to be successful. stats = { 'not-in-apt': 0, 'not-in-vcs': 0, 'vcs-inaccessible': 0, 'gbp-dch-failed': 0, 'missing-upstream-tarball': 0, 'committer-not-allowed': 0, 'build-failed': 0, 'last-release-missing': 0, 'last-upload-not-in-vcs': 0, 'missing-changelog': 0, 'recent-commits': 0, 'no-unuploaded-changes': 0, 'no-unreleased-changes': 0, 'vcs-permission-denied': 0, 'changelog-parse-error': 0, } if args.autopkgtest_only: stats['no-autopkgtest'] = 0 if len(packages) > 1: logging.info("Uploading packages: %s", ", ".join(packages)) for package in packages: logging.info("Processing %s", package) # Can't use open_packaging_branch here, since we want to use pkg_source # later on. if "/" not in package: try: pkg_source = apt_get_source_package(package) except NoSuchPackage: stats['not-in-apt'] += 1 logging.info("%s: package not found in apt", package) ret = 1 continue try: vcs_type, vcs_url = source_package_vcs(pkg_source) except KeyError: stats['not-in-vcs'] += 1 logging.info( "%s: no declared vcs location, skipping", pkg_source["Package"] ) ret = 1 continue source_name = pkg_source["Package"] if source_name in args.exclude: continue source_version = pkg_source["Version"] has_testsuite = "Testsuite" in pkg_source else: vcs_url = package vcs_type = None source_name = None source_version = None has_testsuite = None (location, branch_name, subpath) = split_vcs_url(vcs_url) if subpath is None: subpath = "" probers = select_probers(vcs_type) try: main_branch = open_branch(location, probers=probers, name=branch_name) except (BranchUnavailable, BranchMissing, BranchUnsupported) as e: stats['vcs-inaccessible'] += 1 logging.exception("%s: %s", vcs_url, e) ret = 1 continue with Workspace(main_branch) as ws: if source_name is None: with ControlEditor( ws.local_tree.abspath(os.path.join(subpath, "debian/control")) ) as ce: source_name = ce.source["Source"] with ChangelogEditor( ws.local_tree.abspath(os.path.join(subpath, "debian/changelog")) ) as cle: source_version = cle[0].version has_testsuite = "Testsuite" in ce.source if source_name in args.exclude: continue if ( args.autopkgtest_only and not has_testsuite and not ws.local_tree.has_filename( os.path.join(subpath, "debian/tests/control") ) ): logging.info("%s: Skipping, package has no autopkgtest.", source_name) stats['no-autopkgtest'] += 1 continue branch_config = ws.local_tree.branch.get_config_stack() if args.gpg_verification: 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)) else: gpg_strategy = None try: target_changes, tag_name = prepare_upload_package( ws.local_tree, subpath, source_name, source_version, builder=args.builder, gpg_strategy=gpg_strategy, min_commit_age=args.min_commit_age, allowed_committers=args.allowed_committer, ) except GbpDchFailed as e: logging.warn("%s: 'gbp dch' failed to run: %s", source_name, e) stats['gbp-dch-failed'] += 1 continue except MissingUpstreamTarball as e: stats['missing-upstream-tarball'] += 1 logging.warning("%s: missing upstream tarball: %s", source_name, e) continue except BranchRateLimited as e: stats['rate-limited'] += 1 logging.warning( '%s: rate limited by server (retrying after %s)', source_name, e.retry_after) ret = 1 continue except CommitterNotAllowed as e: stats['committer-not-allowed'] += 1 logging.warn( "%s: committer %s not in allowed list: %r", source_name, e.committer, e.allowed_committers, ) continue except BuildFailedError as e: logging.warn("%s: package failed to build: %s", source_name, e) stats['build-failed'] += 1 ret = 1 continue except LastReleaseRevisionNotFound as e: logging.warn( "%s: Unable to find revision matching last release " "%s, skipping.", source_name, e.version, ) stats['last-release-missing'] += 1 ret = 1 continue except LastUploadMoreRecent as e: stats['last-upload-not-in-vcs'] += 1 logging.warn( "%s: Last upload (%s) was more recent than VCS (%s)", source_name, e.archive_version, e.vcs_version, ) ret = 1 continue except ChangelogParseError as e: stats['changelog-parse-error'] += 1 logging.info("%s: Error parsing changelog: %s", source_name, e) ret = 1 continue except MissingChangelogError: stats['missing-changelog'] += 1 logging.info("%s: No changelog found, skipping.", source_name) ret = 1 continue except RecentCommits as e: stats['recent-commits'] += 1 logging.info( "%s: Recent commits (%d days), skipping.", source_name, e.commit_age ) continue except NoUnuploadedChanges: stats['no-unuploaded-changes'] += 1 logging.info("%s: No unuploaded changes, skipping.", source_name) continue except NoUnreleasedChanges: stats['no-unreleased-changes'] += 1 logging.info("%s: No unreleased changes, skipping.", source_name) continue tags = [] if tag_name is not None: logging.info("Pushing tag %s", tag_name) tags.append(tag_name) try: ws.push(dry_run=args.dry_run, tags=tags) except PermissionDenied: stats['vcs-permission-denied'] += 1 logging.info( "%s: Permission denied pushing to branch, skipping.", source_name ) ret = 1 continue if not args.dry_run: dput_changes(target_changes) if args.diff: sys.stdout.flush() ws.show_diff(sys.stdout.buffer) sys.stdout.buffer.flush() if len(packages) > 1: logging.info('Results:') for error, c in stats.items(): logging.info(' %s: %d', error, c) return ret if __name__ == "__main__": sys.exit(main(sys.argv)) silver-platter_0.4.5.orig/silver_platter/tests/__init__.py0000644000000000000000000000216014164061666020767 0ustar00#!/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 def test_suite(): names = [ "candidates", "debian", "proposal", "publish", "recipe", "run", "utils", "version", ] module_names = [__name__ + ".test_" + name for name in names] loader = unittest.TestLoader() return loader.loadTestsFromNames(module_names) silver-platter_0.4.5.orig/silver_platter/tests/test_candidates.py0000644000000000000000000000225314071532501022355 0ustar00#!/usr/bin/python # Copyright (C) 2021 Jelmer Vernooij # Filippo Giunchedi # # 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 silver_platter.candidates import CandidateList class TestReadCandidates(TestCaseWithTransport): def test_read(self): self.build_tree_contents([('candidates.yaml', """\ --- - url: https://foo """)]) candidates = CandidateList.from_path('candidates.yaml') self.assertEqual(len(candidates.candidates), 1) silver-platter_0.4.5.orig/silver_platter/tests/test_debian.py0000644000000000000000000003121314071532501021476 0ustar00#!/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 datetime import datetime from debian.changelog import ChangelogCreateError import breezy from breezy.tests import ( TestCase, TestCaseWithTransport, ) from breezy.bzr import RemoteBzrProber from breezy.git import RemoteGitProber from ..debian import ( select_probers, convert_debian_vcs_url, UnsupportedVCSProber, add_changelog_entry, ) class SelectProbersTests(TestCase): def test_none(self): self.assertIs(None, select_probers()) self.assertIs(None, select_probers(None)) def test_bzr(self): self.assertEqual([RemoteBzrProber], select_probers("bzr")) def test_git(self): self.assertEqual([RemoteGitProber], select_probers("git")) def test_unsupported(self): self.assertEqual([UnsupportedVCSProber("foo")], select_probers("foo")) class ConvertDebianVcsUrlTests(TestCase): def test_git(self): self.assertEqual( "https://salsa.debian.org/jelmer/blah.git", convert_debian_vcs_url("Git", "https://salsa.debian.org/jelmer/blah.git"), ) def test_git_ssh(self): if breezy.version_info < (3, 1, 1): self.knownFailure("breezy < 3.1.1 can not deal with ssh:// URLs") self.assertIn( convert_debian_vcs_url("Git", "ssh://git@git.kali.org/jelmer/blah.git"), ("git+ssh://git@git.kali.org/jelmer/blah.git", "ssh://git@git.kali.org/jelmer/blah.git") ) class ChangelogAddEntryTests(TestCaseWithTransport): def test_edit_existing_new_author(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) UNRELEASED; urgency=medium * Initial change. * Support updating templated debian/control files that use cdbs template. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Jane Example") self.overrideEnv("DEBEMAIL", "jane@example.com") add_changelog_entry(tree, "debian/changelog", ["Add a foo"]) self.assertFileEqual( """\ lintian-brush (0.35) UNRELEASED; urgency=medium [ Joe Example ] * Initial change. * Support updating templated debian/control files that use cdbs template. [ Jane Example ] * Add a foo -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, "debian/changelog", ) def test_edit_existing_multi_new_author(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) UNRELEASED; urgency=medium [ Jane Example ] * Support updating templated debian/control files that use cdbs template. [ Joe Example ] * Another change -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Jane Example") self.overrideEnv("DEBEMAIL", "jane@example.com") add_changelog_entry(tree, "debian/changelog", ["Add a foo"]) self.assertFileEqual( """\ lintian-brush (0.35) UNRELEASED; urgency=medium [ Jane Example ] * Support updating templated debian/control files that use cdbs template. [ Joe Example ] * Another change [ Jane Example ] * Add a foo -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, "debian/changelog", ) def test_edit_existing_existing_author(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) UNRELEASED; urgency=medium * Support updating templated debian/control files that use cdbs template. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Joe Example") self.overrideEnv("DEBEMAIL", "joe@example.com") add_changelog_entry(tree, "debian/changelog", ["Add a foo"]) self.assertFileEqual( """\ lintian-brush (0.35) UNRELEASED; urgency=medium * Support updating templated debian/control files that use cdbs template. * Add a foo -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, "debian/changelog", ) def test_add_new(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) unstable; urgency=medium * Support updating templated debian/control files that use cdbs template. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Jane Example") self.overrideEnv("DEBEMAIL", "jane@example.com") self.overrideEnv("DEBCHANGE_VENDOR", "debian") add_changelog_entry( tree, "debian/changelog", ["Add a foo"], timestamp=datetime(2020, 5, 24, 15, 27, 26), ) self.assertFileEqual( """\ lintian-brush (0.36) UNRELEASED; urgency=medium * Add a foo -- Jane Example Sun, 24 May 2020 15:27:26 -0000 lintian-brush (0.35) unstable; urgency=medium * Support updating templated debian/control files that use cdbs template. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, "debian/changelog", ) def test_edit_broken_first_line(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ THIS IS NOT A PARSEABLE LINE lintian-brush (0.35) UNRELEASED; urgency=medium * Support updating templated debian/control files that use cdbs template. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Jane Example") self.overrideEnv("DEBEMAIL", "jane@example.com") add_changelog_entry(tree, "debian/changelog", ["Add a foo", "+ Bar"]) self.assertFileEqual( """\ THIS IS NOT A PARSEABLE LINE lintian-brush (0.35) UNRELEASED; urgency=medium [ Joe Example ] * Support updating templated debian/control files that use cdbs template. [ Jane Example ] * Add a foo + Bar -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, "debian/changelog", ) def test_add_long_line(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) UNRELEASED; urgency=medium * Support updating templated debian/control files that use cdbs template. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Joe Example") self.overrideEnv("DEBEMAIL", "joe@example.com") add_changelog_entry( tree, "debian/changelog", [ "This is adding a very long sentence that is longer than " "would fit on a single line in a 80-character-wide line." ], ) self.assertFileEqual( """\ lintian-brush (0.35) UNRELEASED; urgency=medium * Support updating templated debian/control files that use cdbs template. * This is adding a very long sentence that is longer than would fit on a single line in a 80-character-wide line. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, "debian/changelog", ) def test_add_long_subline(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) UNRELEASED; urgency=medium * Support updating templated debian/control files that use cdbs template. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Joe Example") self.overrideEnv("DEBEMAIL", "joe@example.com") add_changelog_entry( tree, "debian/changelog", [ "This is the main item.", "+ This is adding a very long sentence that is longer than " "would fit on a single line in a 80-character-wide line.", ], ) self.assertFileEqual( """\ lintian-brush (0.35) UNRELEASED; urgency=medium * Support updating templated debian/control files that use cdbs template. * This is the main item. + This is adding a very long sentence that is longer than would fit on a single line in a 80-character-wide line. -- Joe Example Fri, 04 Oct 2019 02:36:13 +0000 """, "debian/changelog", ) def test_trailer_only(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) unstable; urgency=medium * This line already existed. -- """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Joe Example") self.overrideEnv("DEBEMAIL", "joe@example.com") try: add_changelog_entry(tree, "debian/changelog", ["And this one is new."]) except ChangelogCreateError: self.skipTest( "python-debian does not allow serializing changelog " "with empty trailer" ) self.assertFileEqual( """\ lintian-brush (0.35) unstable; urgency=medium * This line already existed. * And this one is new. -- """, "debian/changelog", ) def test_trailer_only_existing_author(self): tree = self.make_branch_and_tree(".") self.build_tree_contents( [ ("debian/",), ( "debian/changelog", """\ lintian-brush (0.35) unstable; urgency=medium * This line already existed. [ Jane Example ] * And this one has an existing author. -- """, ), ] ) tree.add(["debian", "debian/changelog"]) self.overrideEnv("DEBFULLNAME", "Joe Example") self.overrideEnv("DEBEMAIL", "joe@example.com") try: add_changelog_entry(tree, "debian/changelog", ["And this one is new."]) except ChangelogCreateError: self.skipTest( "python-debian does not allow serializing changelog " "with empty trailer" ) self.assertFileEqual( """\ lintian-brush (0.35) unstable; urgency=medium * This line already existed. [ Jane Example ] * And this one has an existing author. [ Joe Example ] * And this one is new. -- """, "debian/changelog", ) silver-platter_0.4.5.orig/silver_platter/tests/test_proposal.py0000644000000000000000000000766414164061666022144 0ustar00#!/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 ..workspace import ( Workspace, ) 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_base()) 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_base()) ws.local_tree.commit("foo") self.assertTrue(ws.changes_since_main()) self.assertTrue(ws.changes_since_base()) 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(ws.base_revid, b.branch.last_revision()) self.assertEqual( b.branch.last_revision(), ws.local_tree.branch.last_revision() ) self.assertFalse(ws.changes_since_main()) self.assertFalse(ws.changes_since_base()) ws.local_tree.commit("foo") self.assertTrue(ws.changes_since_main()) self.assertTrue(ws.changes_since_base()) def test_base_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.base_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_base()) f = BytesIO() ws.show_diff(outf=f) self.assertContainsRe(f.getvalue().decode("utf-8"), "\\+some content") silver-platter_0.4.5.orig/silver_platter/tests/test_publish.py0000644000000000000000000000713714013316335021733 0ustar00#!/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 ..publish import ( EmptyMergeProposal, check_proposal_diff, push_result, ) 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 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.4.5.orig/silver_platter/tests/test_recipe.py0000644000000000000000000000213314071532501021522 0ustar00#!/usr/bin/python # Copyright (C) 2021 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 silver_platter.recipe import Recipe class TestReadRecipe(TestCaseWithTransport): def test_read(self): self.build_tree_contents([('recipe.yaml', """\ --- name: foo resume: true """)]) recipe = Recipe.from_path('recipe.yaml') self.assertEqual(recipe.name, 'foo') silver-platter_0.4.5.orig/silver_platter/tests/test_run.py0000644000000000000000000000562614071532501021071 0ustar00#!/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): result = script_runner( self.tree, os.path.abspath("foo.sh"), commit_pending=True ) self.assertEqual(result.description, "Some message\n") def test_simple_with_autocommit(self): result = 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(result.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) result = 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(result.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.4.5.orig/silver_platter/tests/test_utils.py0000644000000000000000000000721414013316335021421 0ustar00#!/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.4.5.orig/silver_platter/tests/test_version.py0000644000000000000000000000275414013316335021752 0ustar00#!/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 line in f: m = re.match(r'[ ]*version=["\'](.*)["\'],', line) if m: setup_version = m.group(1) break else: raise AssertionError("setup version not found") self.assertEqual(version_string, setup_version)