diff -Nru silver-platter-0.4.1/.bzrignore silver-platter-0.4.3/.bzrignore --- silver-platter-0.4.1/.bzrignore 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/.bzrignore 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -build -dist -silver_platter.egg-info diff -Nru silver-platter-0.4.1/debian/changelog silver-platter-0.4.3/debian/changelog --- silver-platter-0.4.1/debian/changelog 2021-02-18 13:05:11.000000000 +0000 +++ silver-platter-0.4.3/debian/changelog 2021-07-08 08:25:37.000000000 +0000 @@ -1,3 +1,10 @@ +silver-platter (0.4.3-1) unstable; urgency=low + + * New upstream release. + + Add dependency on python3-jinja2. + + -- Jelmer Vernooij Thu, 08 Jul 2021 09:25:37 +0100 + silver-platter (0.4.1-2) unstable; urgency=medium * Bump minimum breezy-debian version to something that supports diff -Nru silver-platter-0.4.1/debian/control silver-platter-0.4.3/debian/control --- silver-platter-0.4.1/debian/control 2021-02-18 13:05:11.000000000 +0000 +++ silver-platter-0.4.3/debian/control 2021-07-08 08:25:37.000000000 +0000 @@ -10,6 +10,8 @@ python3-distro-info, python3-dulwich (>= 0.19.7), python3-launchpadlib, + python3-jinja2, + python3-pip, python3-yaml, python3-testtools, python3-upstream-ontologist, @@ -28,6 +30,7 @@ python3-breezy (>= 3.1.0), python3-dulwich (>= 0.19.7), python3-distro-info, + python3-jinja2, python3-launchpadlib, python3-yaml, python3-upstream-ontologist, diff -Nru silver-platter-0.4.1/devnotes/command-line.rst silver-platter-0.4.3/devnotes/command-line.rst --- silver-platter-0.4.1/devnotes/command-line.rst 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/devnotes/command-line.rst 2021-07-08 08:20:17.000000000 +0000 @@ -5,7 +5,7 @@ svp run lp:brz-email /tmp/some-script.py svp run --name=blah lp:brz-email /tmp/some-script.py -svp run --mode=attempt-push lp:brz-email /tmp/some-script.py +svp run -f some-script.yaml lp:brz-email svp hosters svp login https://github.com/ @@ -14,10 +14,10 @@ debian-svp run brz-email ./some-script.py -debian-svp lintian-brush samba -debian-svp lintian-brush --mode=propose samba -debian-svp lintian-brush --mode=push samba +debian-svp 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 merge-upstream --no-build-verify tdb +debian-svp run -f new-upstream-release.yaml --no-build-verify tdb diff -Nru silver-platter-0.4.1/devnotes/mutators.rst silver-platter-0.4.3/devnotes/mutators.rst --- silver-platter-0.4.1/devnotes/mutators.rst 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/devnotes/mutators.rst 2021-07-08 08:20:17.000000000 +0000 @@ -1,59 +1,68 @@ -Mutators live in /usr/share/silver-platter/mutators or $(dirname debian-svp)/debian-mutators. +Commands will be run in a clean VCS checkout, where +they can make changes as they deem fit. Changes should be committed; by +default pending changes will be discarded (but silver-platter will +warn about them, and --autocommit can specified). + +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?) + +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). -Each mutators is a binary that will be run in a clean VCS checkout, where -they can make changes as they deem fit. Changes should be committed; pending -changes will be discarded. - -The arguments specified on the command-line to silver-platter are -passed onto the mutator. - -The mutator should write JSON to standard out and use exit code 0. -It can report errors to standard error. If it returns any other result it is -considered to have failed. +Environment variables that will be set: + + * SVP_API: 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: - * result-code: Optional error code - a string of some sort * 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 - * suggested-branch-name: Optional suggested branch name - * auxiliary-branches: Optional list of names of additional branches that - should be included with the change * tags: Optional list of names of tags that should be included with the change - * merge-proposal: Dictionary with information for merge proposal - * sufficient: Boolean indicating whether this change is sufficient to be - proposed as a merge - * commit-message: Optional suggested commit message - * title: Optional title - * description-plain: Description for merge proposal (in plain text) - * description-markdown: Optional description for merge proposal (in markdown) - * mutator: Optional mutator-specific result data + (autodetected if not specified) + * context: Optional command-specific result data, made available + during template expansion -Environment variables that will be set: +Debian operations +----------------- - * SILVER_PLATTER_API: Currently set to 1 - * BASE_METADATA: Set to a file path with JSON results from the last run, if - available +For Debian branches, branches will be provided named according to DEP-13. +The following environment variables will be set as well: -For Debian mutators, the following will be set as well: - - * PACKAGE: Source package name - * UPDATE_CHANGELOG: Set to either update_changelog/leave_changelog (optional) + * DEB_SOURCE: Source package name + * DEB_UPDATE_CHANGELOG: Set to either update_changelog/leave_changelog (optional) * ALLOW_REFORMATTING: boolean indicating whether reformatting is allowed - * COMMITTER: Set to a committer identity (optional) - -Mutators should support --help, so that "svp mutator --help" can be forwarded. Required Changes ================ -1) mutators can be installed in /usr/lib/silver-platter and /usr/lib/silver-platter/debian, possibly just symlinks? - + Also, with an environment variable to override? - + Possibly also just allow specifying the mutator path as an argument? "debian-svp ../lintian-brush" -2) "sv --help" or "debian-svp --help" will list all relevant mutators -3) start running current mutators as scripts from within svp itself -4) move all logic for lintian-brush into actual lintian-brush binary +1) add support for providing SVP_RESULT environment variable and reading it +2) gradually move existing mutators over: + + lintian-brush + + deb-scrub-obsolete + + apply-multiarch-hints +3) move all logic for lintian-brush into actual lintian-brush binary + Add Enhances: silver-platter to lintian-brush -5) move detect_gbp_dch out of lintian-brush -6) avoid add_changelog_entry from lintian-brush in rrr and orphan -7) add ability to specify candidate list to debian-svp and svp +4) move detect_gbp_dch out of lintian-brush +5) add ability to specify candidate list (yaml) to debian-svp and svp diff -Nru silver-platter-0.4.1/examples/debian/base.md silver-platter-0.4.3/examples/debian/base.md --- silver-platter-0.4.1/examples/debian/base.md 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/base.md 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,3 @@ +{% block runner %}{% endblock %} + +This merge proposal was created automatically. diff -Nru silver-platter-0.4.1/examples/debian/cme.yaml silver-platter-0.4.3/examples/debian/cme.yaml --- silver-platter-0.4.1/examples/debian/cme.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/cme.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,16 @@ +--- +# 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 %} diff -Nru silver-platter-0.4.1/examples/debian/debianize.yaml silver-platter-0.4.3/examples/debian/debianize.yaml --- silver-platter-0.4.1/examples/debian/debianize.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/debianize.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,10 @@ +--- +# 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. diff -Nru silver-platter-0.4.1/examples/debian/lintian-brush.yaml silver-platter-0.4.3/examples/debian/lintian-brush.yaml --- silver-platter-0.4.1/examples/debian/lintian-brush.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/lintian-brush.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,17 @@ +--- +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 -%} diff -Nru silver-platter-0.4.1/examples/debian/mia.yaml silver-platter-0.4.3/examples/debian/mia.yaml --- silver-platter-0.4.1/examples/debian/mia.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/mia.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,19 @@ +--- +# 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 %} diff -Nru silver-platter-0.4.1/examples/debian/multiarch.yaml silver-platter-0.4.3/examples/debian/multiarch.yaml --- silver-platter-0.4.1/examples/debian/multiarch.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/multiarch.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,17 @@ +--- +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 %} diff -Nru silver-platter-0.4.1/examples/debian/new-upstream-release.yaml silver-platter-0.4.3/examples/debian/new-upstream-release.yaml --- silver-platter-0.4.1/examples/debian/new-upstream-release.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/new-upstream-release.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,16 @@ +--- +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 %} diff -Nru silver-platter-0.4.1/examples/debian/new-upstream-snapshot.yaml silver-platter-0.4.3/examples/debian/new-upstream-snapshot.yaml --- silver-platter-0.4.1/examples/debian/new-upstream-snapshot.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/new-upstream-snapshot.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,16 @@ +--- +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 %} diff -Nru silver-platter-0.4.1/examples/debian/orphan.yaml silver-platter-0.4.3/examples/debian/orphan.yaml --- silver-platter-0.4.1/examples/debian/orphan.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/orphan.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,28 @@ +--- +name: orphan +command: deb-auto-orphan +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 %} diff -Nru silver-platter-0.4.1/examples/debian/rrr.yaml silver-platter-0.4.3/examples/debian/rrr.yaml --- silver-platter-0.4.1/examples/debian/rrr.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/rrr.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,11 @@ +--- +# 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 %} diff -Nru silver-platter-0.4.1/examples/debian/scrub-obsolete.yaml silver-platter-0.4.3/examples/debian/scrub-obsolete.yaml --- silver-platter-0.4.1/examples/debian/scrub-obsolete.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/debian/scrub-obsolete.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,10 @@ +--- +name: scrub-obsolete +command: deb-scrub-obsolete +merge-proposal: + commit-message: Remove unnecessary constraints + description: |- + {% extends "base.md" %} + {% block runner %} + Remove unnecessary constraints. + {% endblock %} diff -Nru silver-platter-0.4.1/examples/framwork.yaml silver-platter-0.4.3/examples/framwork.yaml --- silver-platter-0.4.1/examples/framwork.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/framwork.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,11 @@ +--- +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*. diff -Nru silver-platter-0.4.1/examples/patch.yaml silver-platter-0.4.3/examples/patch.yaml --- silver-platter-0.4.1/examples/patch.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/examples/patch.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,11 @@ +--- +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 diff -Nru silver-platter-0.4.1/example.yaml silver-platter-0.4.3/example.yaml --- silver-platter-0.4.1/example.yaml 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/example.yaml 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,20 @@ +--- +# 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. diff -Nru silver-platter-0.4.1/.github/workflows/pythonpackage.yml silver-platter-0.4.3/.github/workflows/pythonpackage.yml --- silver-platter-0.4.1/.github/workflows/pythonpackage.yml 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/.github/workflows/pythonpackage.yml 2021-07-08 08:20:17.000000000 +0000 @@ -9,7 +9,7 @@ strategy: matrix: os: [ubuntu-latest] - python-version: [3.6, 3.7, 3.8] + python-version: [3.7, 3.8, 3.9] fail-fast: false steps: @@ -34,6 +34,7 @@ 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: | diff -Nru silver-platter-0.4.1/Makefile silver-platter-0.4.3/Makefile --- silver-platter-0.4.1/Makefile 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/Makefile 2021-07-08 08:20:17.000000000 +0000 @@ -1,4 +1,4 @@ -all: silver_platter/release/config_pb2.py +all: check: flake8 diff -Nru silver-platter-0.4.1/PKG-INFO silver-platter-0.4.3/PKG-INFO --- silver-platter-0.4.1/PKG-INFO 2021-02-17 22:45:13.295229700 +0000 +++ silver-platter-0.4.3/PKG-INFO 2021-07-08 08:20:17.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: silver-platter -Version: 0.4.1 +Version: 0.4.3 Summary: Automatic merge proposal creeator Home-page: https://jelmer.uk/code/silver-platter Author: Jelmer Vernooij diff -Nru silver-platter-0.4.1/README.rst silver-platter-0.4.3/README.rst --- silver-platter-0.4.1/README.rst 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/README.rst 2021-07-08 08:20:17.000000000 +0000 @@ -12,6 +12,11 @@ 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/). 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 ~~~~~~~~~~~~~~~ @@ -21,15 +26,53 @@ The simplest way to create a change as a merge proposal is to run something like:: - svp run --mode=propose https://github.com/jelmer/dulwich ./some-script.sh + svp run --mode=propose ./framwork.sh https://github.com/jelmer/dulwich -where ``some-script.sh`` makes some modifications to a working copy and prints the -body for the pull request to standard out. For example:: +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 this example, create one called ``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 ~~~~~~~~~~~~~~~~~ @@ -38,7 +81,7 @@ * `GitHub `_ * `Launchpad `_ * `GitLab `_ instances, such as Debian's - `Salsa `_ + `Salsa `_ or `GNOME's GitLab `_ Working with Debian packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -48,17 +91,26 @@ packaging repository location for any Debian package names that are specified. -Subcommands that are available include: - -* *lintian-brush*: Run the `lintian-brush - `_ command on the branch. * *upload-pending*: Build and upload a package and push/propose the changelog updates. -* *new-upstream*: Merge in a new upstream release or snapshot. -* *apply-multi-arch-hints*: Apply multi-arch hints. -* *orphan*: Mark a package as orphaned, update its Maintainer +* *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*: Mark a package as "Rules-Requires-Root: no" +* *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. @@ -67,16 +119,29 @@ Examples running ``debian-svp``:: - debian-svp lintian-brush samba - debian-svp lintian-brush --mode=propose samba - debian-svp lintian-brush --mode=push samba + # 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 - debian-svp upload-pending --vcswatch --maintainer jelmer@debian.org - debian-svp new-upstream --no-build-verify tdb + # Upload pending changes for any packages maintained by Jelmer, + # querying vcswatch. + debian-svp upload-pending --vcswatch --maintainer jelmer@debian.org - debian-svp apply-multi-arch-hints tdb + # 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 ~~~~~~~~~~~ diff -Nru silver-platter-0.4.1/setup.py silver-platter-0.4.3/setup.py --- silver-platter-0.4.1/setup.py 2021-02-17 22:45:11.000000000 +0000 +++ silver-platter-0.4.3/setup.py 2021-07-08 08:20:17.000000000 +0000 @@ -20,7 +20,7 @@ debian_deps = [ 'pyyaml', # For reading debian/upstream/metadata 'debmutate>=0.3', - 'lintian-brush>=0.50', + 'lintian-brush>=0.102', 'python_debian', 'distro-info', 'upstream-ontologist', @@ -33,7 +33,7 @@ author_email="jelmer@jelmer.uk", url="https://jelmer.uk/code/silver-platter", description="Automatic merge proposal creeator", - version='0.4.1', + version='0.4.3', license='GNU GPL v2 or later', project_urls={ "Bug Tracker": "https://github.com/jelmer/silver-platter/issues", @@ -62,15 +62,12 @@ 'svp=silver_platter.__main__:main', 'debian-svp=silver_platter.debian.__main__:main', ], - 'silver_platter.debian.changer': [ - 'lintian-brush = ' - 'silver_platter.debian.lintian:LintianBrushChanger', - ], }, test_suite='silver_platter.tests.test_suite', install_requires=[ - 'breezy>=3.1.0', - 'dulwich', + 'breezy>=3.2.0', + 'dulwich>=0.20.23', + 'jinja2', ], extras_require={ 'debian': debian_deps, diff -Nru silver-platter-0.4.1/silver_platter/apply.py silver-platter-0.4.3/silver_platter/apply.py --- silver-platter-0.4.1/silver_platter/apply.py 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/apply.py 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,234 @@ +#!/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 + 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 + + @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'), + tags=tags) + + +def script_runner( + local_tree: WorkingTree, script: str, commit_pending: Optional[bool] = None, + resume_metadata=None, subpath: str = '' +) -> CommandResult: + """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) + 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(f, resume_metadata) + 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) + 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 diff -Nru silver-platter-0.4.1/silver_platter/candidates.py silver-platter-0.4.3/silver_platter/candidates.py --- silver-platter-0.4.1/silver_platter/candidates.py 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/candidates.py 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,64 @@ +#!/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)) diff -Nru silver-platter-0.4.1/silver_platter/debian/apply.py silver-platter-0.4.3/silver_platter/debian/apply.py --- silver-platter-0.4.1/silver_platter/debian/apply.py 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/apply.py 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,328 @@ +#!/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 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 + 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 + + @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', {}), + description=data.get('description'), + 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 +) -> 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 + + with open(local_tree.abspath(os.path.join(debian_path, 'changelog')), 'r') as f: + cl = Changelog(f) + source_name = cl[0].package + + if source_name: + os.environ['DEB_SOURCE'] = source_name + + if update_changelog: + os.environ['DEB_UPDATE_CHANGELOG'] = 'update' + else: + os.environ['DEB_UPDATE_CHANGELOG'] = 'leave' + + env = dict(os.environ) + 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(f, resume_metadata) + 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 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: + add_changelog_entry( + local_tree, + os.path.join(debian_path, 'changelog'), + result.description) + try: + new_revision = local_tree.commit(result.description, allow_pointless=False) + 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) + 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( + "--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: + result = script_runner( + local_tree, script=command, commit_pending=commit_pending, + subpath=subpath, update_changelog=args.update_changelog) + + 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.info("%s: build failed", result.source) + return False + except MissingUpstreamTarball: + logging.info("%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/') + return 0 diff -Nru silver-platter-0.4.1/silver_platter/debian/backport.py silver-platter-0.4.3/silver_platter/debian/backport.py --- silver-platter-0.4.1/silver_platter/debian/backport.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/backport.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,214 +0,0 @@ -#!/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 - -from distro_info import DebianDistroInfo - -import logging -import os -import re -import tempfile - -from . import ( - DEFAULT_BUILDER, -) -from .changer import ( - run_mutator, - DebianChanger, - ChangerResult, -) -from breezy.plugins.debian.cmds import _build_helper -from breezy.plugins.debian.util import ( - dput_changes, - debsign, -) - -from debian.changelog import format_date, get_maintainer -from debmutate.changelog import ChangelogEditor, changeblock_add_line - - -# See https://backports.debian.org/Contribute/ - - -class BackportResult(object): - def __init__(self, target_release): - self.target_release = target_release - - -def backport_suffix(release): - distro_info = DebianDistroInfo() - version = distro_info.version(release) - return "bpo%s" % version - - -def backport_distribution(release): - distro_info = DebianDistroInfo() - if distro_info.codename("stable") == release: - return "%s-backports" % release - elif distro_info.codename("oldstable") == release: - return "%s-backports-sloppy" % release - else: - raise Exception("unable to determine target suite for %s" % release) - - -def create_bpo_version(orig_version, bpo_suffix): - m = re.fullmatch(r"(.*)\~" + bpo_suffix + r"\+([0-9]+)", str(orig_version)) - if m: - base = m.group(1) - buildno = int(m.group(2)) + 1 - else: - base = str(orig_version) - buildno = 1 - return "%s~%s+%d" % (base, bpo_suffix, buildno) - - -def backport_package(local_tree, subpath, target_release, author=None): - changes = [] - # TODO(jelmer): Check that package has a high enough popcon count, - # and warn otherwise? - # TODO(jelmer): Iterate Build-Depends and verify that depends are - # satisfied by target_distribution - # TODO(jelmer): Update Vcs-Git/Vcs-Browser header? - target_distribution = backport_distribution(target_release) - version_suffix = backport_suffix(target_release) - logging.info( - "Using target distribution %s, version suffix %s", - target_distribution, - version_suffix, - ) - clp = local_tree.abspath(os.path.join(subpath, "debian/changelog")) - - if author is None: - author = "%s <%s>" % get_maintainer() - - with ChangelogEditor(clp) as cl: - # TODO(jelmer): If there was an existing backport, use that version - since_version = cl[0].version - cl.new_block( - package=cl[0].package, - distributions=target_distribution, - urgency="low", - author=author, - date=format_date(), - version=create_bpo_version(since_version, version_suffix), - ) - block = cl[0] - changeblock_add_line( - block, - ["Backport to %s." % target_release] + [" +" + line for line in changes], - ) - - return since_version - - -class BackportChanger(DebianChanger): - - name = "backport" - - def __init__(self, dry_run=False, target_release=None, builder=None): - self.dry_run = dry_run - self.target_release = target_release - self.builder = builder - - @classmethod - def setup_parser(cls, parser): - distro_info = DebianDistroInfo() - parser.add_argument( - "--target-release", - type=str, - help="Target release", - default=distro_info.stable(), - ) - parser.add_argument("--dry-run", action="store_true", help="Do a dry run.") - parser.add_argument( - "--builder", - type=str, - help="Build command", - default=( - DEFAULT_BUILDER + " --source --source-only-changes " - "--debbuildopt=-v${LAST_VERSION}" - ), - ) - - @classmethod - def from_args(cls, args): - return cls( - target_release=args.target_release, - dry_run=args.dry_run, - builder=args.builder, - ) - - def suggest_branch_name(self): - return backport_distribution(self.target_release) - - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - - base_revision = local_tree.last_revision() - - since_version = backport_package( - local_tree, subpath, self.target_release, author=committer - ) - - with tempfile.TemporaryDirectory() as td: - builder = self.builder.replace("${LAST_VERSION}", str(since_version)) - target_changes = _build_helper( - local_tree, subpath, local_tree.branch, td, builder=builder - ) - debsign(target_changes) - - if not self.dry_run: - dput_changes(target_changes) - - branches = [("main", None, base_revision, local_tree.last_revision())] - - # TODO(jelmer): Add debian/... tag - tags = [] - - return ChangerResult( - description=None, - mutator=None, - branches=branches, - tags=tags, - proposed_commit_message="Backport to %s." % self.target_release, - sufficient_for_proposal=True, - ) - - def get_proposal_description(self, applied, description_format, existing_proposal): - return "Backport to %s." % self.target_release - - def describe(self, result, publish_result): - if publish_result.is_new: - logging.info( - "Proposed backportg to %s: %s", - result.target_release, - publish_result.proposal.url, - ) - else: - logging.info("No changes for package %s", result.package_name) - - -if __name__ == "__main__": - import sys - - sys.exit(run_mutator(BackportChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/changer.py silver-platter-0.4.3/silver_platter/debian/changer.py --- silver-platter-0.4.1/silver_platter/debian/changer.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/changer.py 2021-07-08 08:20:17.000000000 +0000 @@ -20,10 +20,12 @@ import argparse from functools import partial import logging -import pkg_resources +import os import sys from typing import Any, List, Optional, Dict, Iterable, Tuple, Type +import pkg_resources + from breezy import version_info as breezy_version_info from breezy.branch import Branch from breezy.propose import Hoster, MergeProposal @@ -31,6 +33,7 @@ from breezy.workingtree import WorkingTree from . import ( + control_files_in_root, open_packaging_branch, guess_update_changelog, NoSuchPackage, @@ -50,6 +53,7 @@ from ..publish import ( PublishResult, SUPPORTED_MODES, + InsufficientChangesForNewProposal, ) from ..utils import ( BranchMissing, @@ -62,6 +66,72 @@ ) +class ChangerReporter(object): + def report_context(self, context): + raise NotImplementedError(self.report_context) + + def report_metadata(self, key, value): + raise NotImplementedError(self.report_metadata) + + def get_base_metadata(self, key, default_value=None): + raise NotImplementedError(self.get_base_metadata) + + +class ChangerError(Exception): + def __init__( + self, category: str, summary: str, original: Optional[Exception] = None, details: Any = None + ): + self.category = category + self.summary = summary + self.original = original + self.details = details + + +class ChangerResult(object): + def __init__( + self, + description: Optional[str], + mutator: Any, + branches: Optional[List[Tuple[str, str, bytes, bytes]]] = [], + tags: Optional[Dict[str, bytes]] = None, + value: Optional[int] = None, + proposed_commit_message: Optional[str] = None, + title: Optional[str] = None, + labels: Optional[List[str]] = None, + sufficient_for_proposal: bool = True, + ): + self.description = description + self.mutator = mutator + self.branches = branches or [] + self.tags = tags or {} + self.value = value + self.proposed_commit_message = proposed_commit_message + self.title = title + self.labels = labels + self.sufficient_for_proposal = sufficient_for_proposal + + def show_diff( + self, + repository, + outf, + role="main", + old_label: str = "old/", + new_label: str = "new/", + ) -> None: + from breezy.diff import show_diff_trees + + for (brole, name, base_revision, revision) in self.branches: + if role == brole: + break + else: + raise KeyError + old_tree = repository.revision_tree(base_revision) + new_tree = repository.revision_tree(revision) + show_diff_trees( + old_tree, new_tree, outf, old_label=old_label, new_label=new_label + ) + + def get_package( package: str, branch_name: str, @@ -171,71 +241,6 @@ ) -class ChangerReporter(object): - def report_context(self, context): - raise NotImplementedError(self.report_context) - - def report_metadata(self, key, value): - raise NotImplementedError(self.report_metadata) - - def get_base_metadata(self, key, default_value=None): - raise NotImplementedError(self.get_base_metadata) - - -class ChangerError(Exception): - def __init__( - self, category: str, summary: str, original: Optional[Exception] = None - ): - self.category = category - self.summary = summary - self.original = original - - -class ChangerResult(object): - def __init__( - self, - description: Optional[str], - mutator: Any, - branches: Optional[List[Tuple[str, str, bytes, bytes]]] = [], - tags: Optional[Dict[str, bytes]] = None, - value: Optional[int] = None, - proposed_commit_message: Optional[str] = None, - title: Optional[str] = None, - labels: Optional[List[str]] = None, - sufficient_for_proposal: bool = True, - ): - self.description = description - self.mutator = mutator - self.branches = branches or [] - self.tags = tags or {} - self.value = value - self.proposed_commit_message = proposed_commit_message - self.title = title - self.labels = labels - self.sufficient_for_proposal = sufficient_for_proposal - - def show_diff( - self, - repository, - outf, - role="main", - old_label: str = "old/", - new_label: str = "new/", - ) -> None: - from breezy.diff import show_diff_trees - - for (brole, name, base_revision, revision) in self.branches: - if role == brole: - break - else: - raise KeyError - old_tree = repository.revision_tree(base_revision) - new_tree = repository.revision_tree(revision) - show_diff_trees( - old_tree, new_tree, outf, old_label=old_label, new_label=new_label - ) - - def setup_parser_common(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--dry-run", @@ -301,6 +306,10 @@ ), ) parser.add_argument( + "--install", "-i", + action="store_true", + help="Install built package (implies --build-verify)") + parser.add_argument( "--overwrite", action="store_true", help="Overwrite existing branches." ) parser.add_argument("--name", type=str, help="Proposed branch name", default=None) @@ -310,6 +319,9 @@ 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.") class DebianChanger(object): @@ -377,6 +389,8 @@ diff: bool = False, committer: Optional[str] = None, build_verify: bool = False, + preserve_repositories: bool = False, + install: bool = False, pre_check: Optional[str] = None, post_check: Optional[str] = None, builder: str = DEFAULT_BUILDER, @@ -405,10 +419,14 @@ if ws.refreshed: overwrite = True run_pre_check(ws.local_tree, pre_check) + if control_files_in_root(ws.local_tree, subpath): + debian_path = subpath + else: + debian_path = os.path.join(subpath, "debian") if update_changelog is None: - dch_guess = guess_update_changelog(ws.local_tree) + dch_guess = guess_update_changelog(ws.local_tree, debian_path) if dch_guess: - logging.info(dch_guess[1]) + logging.info('%s', dch_guess[1]) update_changelog = dch_guess[0] else: # Assume yes. @@ -422,7 +440,7 @@ reporter=DummyChangerReporter(), ) except ChangerError as e: - logging.exception(e.summary) + logging.error('%s: %s', e.category, e.summary) return False if not ws.changes_since_main(): @@ -438,7 +456,7 @@ except PostCheckFailed as e: logging.info("%s: %s", pkg, e) return False - if build_verify: + if build_verify or install: try: ws.build(builder=builder, result_dir=build_target_dir) except BuildFailedError: @@ -448,6 +466,10 @@ logging.info("%s: unable to find upstream source", pkg) return False + if install: + from .apply import install_built_package + install_built_package(ws.local_tree, ws.subpath, build_target_dir) + enable_tag_pushing(ws.local_tree.branch) kwargs: Dict[str, Any] = {} @@ -474,7 +496,7 @@ **kwargs ) except UnsupportedHoster as e: - logging.exception( + logging.error( "%s: No known supported hoster for %s. Run 'svp login'?", pkg, full_branch_url(e.branch), @@ -489,8 +511,12 @@ except errors.DivergedBranches: logging.info("%s: a branch exists. Use --overwrite to discard it.", pkg) return False + except InsufficientChangesForNewProposal: + logging.info('%s: insufficient changes for a new merge proposal', + pkg) + return False except HosterLoginRequired as e: - logging.exception( + logging.error( "Credentials for hosting site at %r missing. " "Run 'svp login'?", e.hoster.base_url, ) @@ -499,8 +525,8 @@ if publish_result.proposal: changer.describe(changer_result.mutator, publish_result) if diff: - for entry in changer_result.branches: - role = entry[0] + for branch_entry in changer_result.branches: + role = branch_entry[0] if len(changer_result.branches) > 1: sys.stdout.write("%s\n" % role) sys.stdout.write(("-" * len(role)) + "\n") @@ -510,6 +536,9 @@ ) if len(changer_result.branches) > 1: sys.stdout.write("\n") + if preserve_repositories: + ws.defer_destroy() + logging.info('Workspace preserved in %s', ws.local_tree.abspath(ws.subpath)) return True @@ -569,6 +598,8 @@ diff=args.diff, committer=args.committer, build_verify=args.build_verify, + preserve_repositories=args.preserve_repositories, + install=args.install, pre_check=args.pre_check, builder=args.builder, post_check=args.post_check, @@ -587,49 +618,19 @@ BUILTIN_ENTRYPOINTS = [ pkg_resources.EntryPoint( - "run", "silver_platter.debian.run", attrs=("ScriptChanger",) - ), - pkg_resources.EntryPoint( - "lintian-brush", "silver_platter.debian.lintian", attrs=("LintianBrushChanger",) - ), - pkg_resources.EntryPoint( - "tidy", "silver_platter.debian.tidy", attrs=("TidyChanger",) - ), - pkg_resources.EntryPoint( "new-upstream", "silver_platter.debian.upstream", attrs=("NewUpstreamChanger",) ), - pkg_resources.EntryPoint("cme-fix", "silver_platter.debian.cme", attrs=("CMEFixChanger",)), - pkg_resources.EntryPoint( - "apply-multiarch-hints", - "silver_platter.debian.multiarch", - attrs=("MultiArchHintsChanger",), - ), - pkg_resources.EntryPoint( - "rules-requires-root", - "silver_platter.debian.rrr", - attrs=("RulesRequiresRootChanger",), - ), pkg_resources.EntryPoint( "orphan", "silver_platter.debian.orphan", attrs=("OrphanChanger",) ), pkg_resources.EntryPoint( - "import-upload", - "silver_platter.debian.uncommitted", - attrs=("UncommittedChanger",), - ), - pkg_resources.EntryPoint( - "scrub-obsolete", - "silver_platter.debian.scrub_obsolete", - attrs=("ScrubObsoleteChanger",), - ), - pkg_resources.EntryPoint( "debianize", "silver_platter.debian.debianize", attrs=("DebianizeChanger",) ), ] def changer_subcommands() -> List[str]: - endpoints = pkg_resources.iter_entry_points("silver_platter.debian.changer") + endpoints = pkg_resources.iter_entry_points(__name__) ret = [] for ep in BUILTIN_ENTRYPOINTS + list(endpoints): ret.append(ep.name) @@ -640,7 +641,7 @@ for ep in BUILTIN_ENTRYPOINTS: if ep.name == name: return ep.resolve() - endpoints = pkg_resources.iter_entry_points("silver_platter.debian.changer", name) + endpoints = pkg_resources.iter_entry_points(__name__, name) for ep in endpoints: return ep.load() raise KeyError(name) @@ -704,6 +705,7 @@ result_json = { "result-code": e.category, "description": e.summary, + "details": e.details, } else: result_json = { diff -Nru silver-platter-0.4.1/silver_platter/debian/cme.py silver-platter-0.4.3/silver_platter/debian/cme.py --- silver-platter-0.4.1/silver_platter/debian/cme.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/cme.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,101 +0,0 @@ -#!/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 -import subprocess - -from .changer import ( - run_mutator, - DebianChanger, - ChangerResult, -) - - -BRANCH_NAME = "cme" - - -class CMEResult(object): - def __init__(self): - pass - - -class CMEFixChanger(DebianChanger): - - name = "cme-fix" - - def __init__(self, dry_run=False): - self.dry_run = dry_run - - @classmethod - def setup_parser(cls, parser): - pass - - @classmethod - def from_args(cls, args): - return cls() - - def suggest_branch_name(self): - return BRANCH_NAME - - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - base_revid = local_tree.last_revision() - cwd = local_tree.abspath(subpath or "") - subprocess.check_call(["/usr/bin/cme", "modify", "dpkg", "-save"], cwd=cwd) - local_tree.commit("Reformat for cme.") - subprocess.check_call(["/usr/bin/cme", "fix", "dpkg"], cwd=cwd) - revid = local_tree.commit("Run cme.") - branches = [("main", None, base_revid, revid)] - tags = [] - return ChangerResult( - description=None, - mutator=None, - branches=branches, - tags=tags, - proposed_commit_message="Run cme.", - sufficient_for_proposal=True, - ) - - def get_proposal_description(self, applied, description_format, existing_proposal): - return "Run cme." - - def describe(self, result, publish_result): - if publish_result.is_new: - logging.info( - "Proposed change from 'cme fix dpkg': %s", - publish_result.proposal.url) - else: - logging.info( - "No changes for package %s", - result.package_name) - - @classmethod - def describe_command(cls, command): - return "Apply Configuration Model Editor (CME) fixes" - - -if __name__ == "__main__": - import sys - - sys.exit(run_mutator(CMEFixChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/debianize.py silver-platter-0.4.3/silver_platter/debian/debianize.py --- silver-platter-0.4.1/silver_platter/debian/debianize.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/debianize.py 2021-07-08 08:20:17.000000000 +0000 @@ -15,17 +15,29 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import argparse +import errno import logging +import os import sys import breezy from breezy.revision import NULL_REVISION +from breezy.plugins.debian.upstream.branch import ( + DistCommandFailed, + ) from lintian_brush import ( version_string as lintian_brush_version_string, ) from lintian_brush.debianize import ( debianize, + DebianDirectoryExists, + SourcePackageNameInvalid, + NoBuildToolsFound, + DistCreationFailed, + UnidentifiedError, + DetailedFailure, + NoUpstreamReleases, ) from lintian_brush.config import Config @@ -35,6 +47,7 @@ DebianChanger, run_mutator, ChangerResult, + ChangerError, ) @@ -45,29 +58,61 @@ name = "debianize" - def __init__(self, compat_release=None): + def __init__(self, compat_release=None, schroot=None, diligence=0, trust_package=False, verbose=False, force_new_directory=False, upstream_version=None, upstream_version_kind=None): self.compat_release = compat_release + self.schroot = schroot + self.diligence = diligence + self.trust = trust_package + self.verbose = verbose + self.force_new_directory = force_new_directory + self.upstream_version = upstream_version + self.upstream_version_kind = upstream_version_kind @classmethod def setup_parser(cls, parser): parser.add_argument("--compat-release", type=str, help=argparse.SUPPRESS) + parser.add_argument("--schroot", type=str, help=argparse.SUPPRESS) + parser.add_argument("--diligence", type=int, default=10, help=argparse.SUPPRESS) + parser.add_argument( + "--trust-package", action="store_true", help="Trust package." + ) + parser.add_argument( + "--verbose", action="store_true", help="Be verbose.") + parser.add_argument( + "--upstream-version", type=str, help="Upstream version to package.") + parser.add_argument( + "--upstream-version-kind", type=str, choices=['snapshot', 'release', 'auto'], + help="Upstream version kind.") + parser.add_argument( + '--force-new-directory', action='store_true', + help='Force creation of a new directory.') @classmethod def from_args(cls, args): - return cls(compat_release=args.compat_release) + if args.schroot: + schroot = args.schroot + else: + schroot = os.environ.get('CHROOT') + return cls( + compat_release=args.compat_release, schroot=schroot, + diligence=args.diligence, + trust_package=args.trust_package, + verbose=args.verbose, + force_new_directory=args.force_new_directory, + upstream_version=args.upstream_version, + upstream_version_kind=args.upstream_version_kind) def suggest_branch_name(self): return BRANCH_NAME - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): + def make_changes( # noqa: C901 + self, + local_tree, + subpath, + update_changelog, + reporter, + committer, + base_proposal=None): base_revid = local_tree.last_revision() upstream_base_revid = NULL_REVISION @@ -98,25 +143,85 @@ if compat_release is None: compat_release = debian_info.stable() + # For now... + upstream_branch = local_tree.branch + upstream_subpath = subpath + with local_tree.lock_write(): - debianize(local_tree, subpath=subpath, compat_release=self.compat_release) + try: + result = debianize( + local_tree, subpath=subpath, + upstream_branch=upstream_branch, + upstream_subpath=upstream_subpath, + compat_release=self.compat_release, + schroot=self.schroot, + diligence=self.diligence, + trust=self.trust, + verbose=self.verbose, + force_new_directory=self.force_new_directory, + create_dist=getattr(self, 'create_dist', None)) + except OSError as e: + if e.errno == errno.ENOSPC: + raise ChangerError( + 'no-space-on-device', str(e)) + else: + raise + except DebianDirectoryExists: + raise ChangerError( + 'debian-directory-exists', + "A debian/ directory already exists in the upstream project.") + except SourcePackageNameInvalid as e: + raise ChangerError( + 'invalid-source-package-name', + "Generated source package name %r is not valid." % e.source) + except NoBuildToolsFound: + raise ChangerError( + 'no-build-tools', + "Unable to find any build systems in upstream sources.") + except NoUpstreamReleases: + raise ChangerError( + 'no-upstream-releases', + 'The upstream project does not appear to have made any releases.') + except DistCommandFailed as e: + raise ChangerError("dist-command-failed", str(e), e) + except DetailedFailure as e: + raise ChangerError('dist-%s' % e.error.kind, str(e.error)) + except UnidentifiedError as e: + if e.secondary: + raise ChangerError('dist-command-failed', str(e.secondary.line)) + else: + raise ChangerError('dist-command-failed', e.lines[-1]) + except DistCreationFailed as e: + if e.inner: + raise ChangerError('dist-%s' % e.inner.kind, e.msg) + else: + raise ChangerError('dist-command-failed', e.msg) # TODO(jelmer): Pristine tar branch? branches = [ ("main", None, base_revid, local_tree.last_revision()), - ( - "upstream", + ] + if result.upstream_branch_name: + branches.append(( "upstream", + result.upstream_branch_name, upstream_base_revid, - local_tree.controldir.open_branch("upstream").last_revision(), - ), + local_tree.controldir.open_branch(result.upstream_branch_name).last_revision(), + )) + + tags = [ + (("upstream", str(result.upstream_version), component), tag, + local_tree.branch.tags.lookup_tag(tag)) + for (component, tag) in result.tag_names.items() ] + reporter.report_metadata("wnpp_bugs", result.wnpp_bugs) + return ChangerResult( description="Debianized package.", mutator=None, branches=branches, - tags=[], + tags=tags, value=None, sufficient_for_proposal=True, ) diff -Nru silver-platter-0.4.1/silver_platter/debian/__init__.py silver-platter-0.4.3/silver_platter/debian/__init__.py --- silver-platter-0.4.1/silver_platter/debian/__init__.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/__init__.py 2021-07-08 08:20:17.000000000 +0000 @@ -13,13 +13,18 @@ # 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 -import subprocess -from typing import Optional, Dict, List +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 @@ -28,6 +33,7 @@ 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, @@ -36,7 +42,6 @@ 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.upstream import ( @@ -44,9 +49,8 @@ ) from lintian_brush.detect_gbp_dch import guess_update_changelog -from lintian_brush.changelog import add_changelog_entry -from .. import proposal as _mod_proposal +from .. import workspace as _mod_workspace from ..utils import ( open_branch, ) @@ -54,7 +58,6 @@ __all__ = [ "add_changelog_entry", - "changelog_add_line", "apt_get_source_package", "guess_update_changelog", "source_package_vcs", @@ -65,6 +68,7 @@ ] +DEFAULT_URGENCY = "medium" DEFAULT_BUILDER = "sbuild --no-clean-source" @@ -72,6 +76,51 @@ """No such package.""" +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: Tree, subpath: str = "", @@ -154,7 +203,10 @@ (url, branch_name, subpath) = split_vcs_url(vcs_url) else: url, params = urlutils.split_segment_parameters(location) - branch_name = params.get("branch") + try: + branch_name = urlutils.unquote(params["branch"]) + except KeyError: + branch_name = None subpath = "" probers = select_probers(vcs_type) branch = open_branch( @@ -173,7 +225,7 @@ return ret -class Workspace(_mod_proposal.Workspace): +class Workspace(_mod_workspace.Workspace): def __init__(self, main_branch: Branch, *args, **kwargs) -> None: if isinstance(main_branch.repository, GitRepository): kwargs["additional_colocated_branches"] = kwargs.get( @@ -279,23 +331,9 @@ return probers -def changelog_add_line( - tree: WorkingTree, subpath: str, line: str, email: Optional[str] = None -) -> None: - env = {} - if email: - env["DEBEMAIL"] = email - subprocess.check_call(["dch", "--", line], cwd=tree.abspath(subpath), env=env) - - def is_debcargo_package(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, "debcargo.toml") - if tree.has_filename(control_path): - return True - return False + 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: @@ -319,8 +357,20 @@ Returns: whether control file is present """ - for name in ["debian/control", "debian/control.in", "control", "control.in"]: + for name in ["debian/control", "debian/control.in", "control", + "control.in", "debian/debcargo.toml"]: name = os.path.join(subpath, name) if tree.has_filename(name): return True return False + + +def connect_udd_mirror(): + import psycopg2 + + return psycopg2.connect( + database="udd", + user="udd-mirror", + password="udd-mirror", + host="udd-mirror.debian.net", + ) diff -Nru silver-platter-0.4.1/silver_platter/debian/lintian.py silver-platter-0.4.3/silver_platter/debian/lintian.py --- silver-platter-0.4.1/silver_platter/debian/lintian.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/lintian.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,471 +0,0 @@ -#!/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 argparse -import logging -import sys -from typing import List, Set - -from debian.changelog import ChangelogCreateError - -import breezy - -from lintian_brush import ( - available_lintian_fixers, - run_lintian_fixers, - DEFAULT_MINIMUM_CERTAINTY, - SUPPORTED_CERTAINTIES, - NotDebianPackage, - version_string as lintian_brush_version_string, -) -from lintian_brush.config import Config - -import silver_platter - -from . import ( - control_files_in_root, -) -from .changer import ( - DebianChanger, - ChangerResult, - run_mutator, - ChangerError, -) - -__all__ = [ - "available_lintian_fixers", - "calculate_value", -] - - -DEFAULT_ADDON_FIXERS = [ - "debian-changelog-line-too-long", - "file-contains-trailing-whitespace", - "out-of-date-standards-version", - "package-uses-old-debhelper-compat-version", - "public-upstream-key-not-minimal", - "no-dh-sequencer", -] - -DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY = 10 -DEFAULT_VALUE_LINTIAN_BRUSH = 50 -# Base these scores on the importance as set in Debian? -LINTIAN_BRUSH_TAG_VALUES = { - "file-contains-trailing-whitespace": 0, -} -LINTIAN_BRUSH_TAG_DEFAULT_VALUE = 5 - - -BRANCH_NAME = "lintian-fixes" - - -class UnknownFixer(Exception): - """The specified fixer is unknown.""" - - def __init__(self, fixer): - self.fixer = fixer - super(UnknownFixer, self).__init__("No such fixer: %s." % fixer) - - -def calculate_value(tags: Set[str]) -> int: - if not (set(tags) - set(DEFAULT_ADDON_FIXERS)): - value = DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY - else: - value = DEFAULT_VALUE_LINTIAN_BRUSH - for tag in tags: - value += LINTIAN_BRUSH_TAG_VALUES.get(tag, LINTIAN_BRUSH_TAG_DEFAULT_VALUE) - return value - - -def parse_mp_description(description: str) -> List[str]: - """Parse a merge proposal description. - - Args: - description: The description to parse - Returns: - list of one-line descriptions of changes - """ - existing_lines = description.splitlines() - if len(existing_lines) == 1: - return existing_lines - else: - return [ - line[2:].rstrip("\n") for line in existing_lines if line.startswith("* ") - ] - - -def create_mp_description(description_format: str, lines: List[str]) -> str: - """Create a merge proposal description. - - Args: - lines: List of one-line descriptions of fixes - Returns: - A string with a merge proposal description - """ - if len(lines) > 1: - mp_description = ["Fix some issues reported by lintian\n"] - for line in lines: - line = "* %s\n" % line - if line not in mp_description: - mp_description.append(line) - else: - mp_description = [lines[0]] - return "".join(mp_description) - - -def applied_entry_as_line(description_format, fixed_lintian_tags, line): - if not fixed_lintian_tags: - return line - if description_format == "markdown": - return "%s (%s)" % ( - line, - ", ".join( - [ - "[%s](https://lintian.debian.org/tags/%s.html)" % (tag, tag) - for tag in fixed_lintian_tags - ] - ), - ) - return "%s (%s)" % (line, ", ".join(fixed_lintian_tags)) - - -def update_proposal_description(description_format, existing_proposal, applied): - if existing_proposal: - existing_description = existing_proposal.get_description() - existing_lines = parse_mp_description(existing_description) - else: - existing_lines = [] - return create_mp_description( - description_format, - existing_lines - + [ - applied_entry_as_line(description_format, r.fixed_lintian_tags, l) - for r, l in applied - ], - ) - - -def update_proposal_commit_message(existing_proposal, applied): - existing_commit_message = getattr( - existing_proposal, "get_commit_message", lambda: None - )() - if existing_commit_message and not existing_commit_message.startswith( - "Fix lintian issues: " - ): - # The commit message is something we haven't set - let's leave it - # alone. - return - if existing_commit_message: - existing_applied = existing_commit_message.split(":", 1)[1] - else: - existing_applied = [] - return "Fix lintian issues: " + ( - ", ".join(sorted(existing_applied + [l for r, l in applied])) - ) - - -def has_nontrivial_changes(applied, propose_addon_only: Set[str]) -> bool: - tags = set() - for result, unused_summary in applied: - tags.update(result.fixed_lintian_tags) - # Is there enough to create a new merge proposal? - return bool(tags - set(propose_addon_only)) - - -def get_fixers(available_fixers, names=None, tags=None, exclude=None): - """Get the set of fixers to try. - - Args: - available_fixers: Dictionary mapping fixer names to objects - names: Optional set of fixers to restrict to - tags: Optional set of tags to restrict to - exclude: Optional set of fixers to exclude - Returns: - List of fixer objects - """ - if exclude is None: - exclude = set() - by_tag = {} - by_name = {} - for fixer in available_fixers: - for tag in fixer.lintian_tags: - by_tag[tag] = fixer - by_name[fixer.name] = fixer - - # If it's unknown which fixers are relevant, just try all of them. - if names: - try: - fixers = [by_name[name] for name in names] - except KeyError as e: - raise UnknownFixer(e.args[0]) - elif tags: - fixers = [by_tag[tag] for tag in tags] - else: - fixers = list(by_name.values()) - - if exclude: - fixers = [fixer for fixer in fixers if fixer.name not in exclude] - return fixers - - -class LintianBrushChanger(DebianChanger): - - name = "lintian-brush" - - def __init__( - self, - names=None, - exclude=None, - propose_addon_only=None, - compat_release=None, - allow_reformatting=None, - minimum_certainty=None, - tags=None, - opinionated=False, - trust_package=False, - diligence=0, - ): - self.fixers = get_fixers( - available_lintian_fixers(), names=names, tags=tags, exclude=exclude - ) - self.propose_addon_only = propose_addon_only or [] - self.compat_release = compat_release - self.allow_reformatting = allow_reformatting - self.minimum_certainty = minimum_certainty - self.opinionated = opinionated - self.trust_package = trust_package - self.diligence = diligence - - @classmethod - def setup_parser(cls, parser): - parser.add_argument( - "--fixers", help="Fixers to run.", type=str, action="append" - ) - parser.add_argument( - "--exclude", help="Fixers to exclude.", type=str, action="append" - ) - parser.add_argument( - "--propose-addon-only", - help="Fixers that should be considered add-on-only.", - type=str, - action="append", - default=DEFAULT_ADDON_FIXERS, - ) - parser.add_argument( - "--compat-release", - type=str, - default=None, - help="Oldest Debian release to be compatible with.", - ) - parser.add_argument( - "--allow-reformatting", - default=None, - action="store_true", - help="Whether to allow reformatting.", - ) - parser.add_argument( - "--minimum-certainty", - type=str, - choices=SUPPORTED_CERTAINTIES, - default=None, - help=argparse.SUPPRESS, - ) - parser.add_argument( - "--opinionated", action="store_true", help="Make opinionated changes" - ) - parser.add_argument("--diligence", type=int, default=10, help=argparse.SUPPRESS) - parser.add_argument( - "--trust-package", action="store_true", help="Trust package." - ) - parser.add_argument("tags", nargs="*") - - @classmethod - def from_args(cls, args): - return cls( - names=args.fixers, - exclude=args.exclude, - propose_addon_only=args.propose_addon_only, - compat_release=args.compat_release, - allow_reformatting=args.allow_reformatting, - minimum_certainty=args.minimum_certainty, - tags=args.tags, - opinionated=args.opinionated, - diligence=args.diligence, - trust_package=args.trust_package, - ) - - def suggest_branch_name(self): - return BRANCH_NAME - - def make_changes( # noqa: C901 - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - base_revid = local_tree.last_revision() - - reporter.report_metadata( - "versions", - { - "lintian-brush": lintian_brush_version_string, - "silver-platter": silver_platter.version_string, - "breezy": breezy.version_string, - }, - ) - - import distro_info - - debian_info = distro_info.DebianDistroInfo() - - compat_release = self.compat_release - allow_reformatting = self.allow_reformatting - minimum_certainty = None - try: - cfg = Config.from_workingtree(local_tree, subpath) - except FileNotFoundError: - pass - else: - compat_release = cfg.compat_release() - if compat_release: - compat_release = debian_info.codename( - compat_release, default=compat_release - ) - allow_reformatting = cfg.allow_reformatting() - minimum_certainty = cfg.minimum_certainty() - if compat_release is None: - compat_release = debian_info.stable() - if allow_reformatting is None: - allow_reformatting = False - if minimum_certainty is None: - minimum_certainty = DEFAULT_MINIMUM_CERTAINTY - - with local_tree.lock_write(): - if control_files_in_root(local_tree, subpath): - raise ChangerError( - "control-files-in-root", - "control files live in root rather than debian/ " "(LarstIQ mode)", - ) - - try: - overall_result = run_lintian_fixers( - local_tree, - self.fixers, - committer=committer, - update_changelog=update_changelog, - compat_release=compat_release, - allow_reformatting=allow_reformatting, - minimum_certainty=minimum_certainty, - subpath=subpath, - diligence=self.diligence, - opinionated=self.opinionated, - trust_package=self.trust_package, - ) - except NotDebianPackage: - raise ChangerError("not-debian-package", "Not a Debian package") - except ChangelogCreateError as e: - raise ChangerError( - "changelog-create-error", "Error creating changelog entry: %s" % e - ) - - applied = [] - base_applied = reporter.get_base_metadata("applied", []) - if base_applied: - applied.extend(base_applied) - for result, summary in overall_result.success: - applied.append( - { - "summary": summary, - "description": result.description, - "fixed_lintian_tags": result.fixed_lintian_tags, - "revision_id": result.revision_id.decode("utf-8"), - "certainty": result.certainty, - } - ) - reporter.report_metadata("applied", applied) - - if overall_result.failed_fixers: - for fixer_name, failure in overall_result.failed_fixers.items(): - logging.info("Fixer %r failed to run:", fixer_name) - sys.stderr.write(str(failure)) - reporter.report_metadata( - "failed", - {name: str(e) for (name, e) in overall_result.failed_fixers.items()}, - ) - - if not overall_result.success: - raise ChangerError("nothing-to-do", "no fixers to apply") - - fixed_lintian_tags = set() - for result, summary in overall_result.success: - fixed_lintian_tags.update(result.fixed_lintian_tags) - - add_on_only = not has_nontrivial_changes( - overall_result.success, self.propose_addon_only - ) - - if not reporter.get_base_metadata("add_on_only", False): - add_on_only = False - - if not add_on_only: - if overall_result.success: - logging.info("only add-on fixers found") - sufficient_for_proposal = False - reporter.report_metadata("add_on_only", True) - else: - sufficient_for_proposal = True - reporter.report_metadata("add_on_only", False) - - branches = [("main", None, base_revid, local_tree.last_revision())] - - return ChangerResult( - description="Applied fixes for %r" % fixed_lintian_tags, - mutator=overall_result.success, - branches=branches, - tags=[], - value=calculate_value(fixed_lintian_tags), - sufficient_for_proposal=sufficient_for_proposal, - proposed_commit_message=update_proposal_commit_message( - base_proposal, overall_result.success - ), - ) - - def get_proposal_description(self, applied, description_format, existing_proposal): - return update_proposal_description( - description_format, existing_proposal, applied - ) - - def describe(self, applied, publish_result): - tags = set() - for brush_result, unused_summary in applied: - tags.update(brush_result.fixed_lintian_tags) - if publish_result.is_new: - logging.info( - "Proposed fixes %r: %s", tags, publish_result.proposal.url) - elif tags: - logging.info( - "Updated proposal %s with fixes %r", publish_result.proposal.url, tags) - else: - logging.info( - "No new fixes for proposal %s", publish_result.proposal.url) - - -if __name__ == "__main__": - sys.exit(run_mutator(LintianBrushChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/__main__.py silver-platter-0.4.3/silver_platter/debian/__main__.py --- silver-platter-0.4.1/silver_platter/debian/__main__.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/__main__.py 2021-07-08 08:20:17.000000000 +0000 @@ -30,7 +30,11 @@ changer_subcommand, ) -from . import uploader as debian_uploader +from . import ( + apply as debian_apply, + run as debian_run, + uploader as debian_uploader, + ) def run_changer_subcommand(name, changer_cls, argv): @@ -55,6 +59,8 @@ 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) @@ -64,13 +70,16 @@ 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" ) - subcommands.update(main_subcommands.items()) - - # We have a debian-specific run command - del subcommands["run"] + 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()) + changer_subcommands() @@ -82,7 +91,11 @@ 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 @@ -93,7 +106,6 @@ except KeyError: pass else: - logging.basicConfig(level=logging.INFO) return run_changer_subcommand(args.subcommand, subcmd, rest) parser.print_usage() return 1 diff -Nru silver-platter-0.4.1/silver_platter/debian/multiarch.py silver-platter-0.4.3/silver_platter/debian/multiarch.py --- silver-platter-0.4.1/silver_platter/debian/multiarch.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/multiarch.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,209 +0,0 @@ -#!/usr/bin/python3 -# 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 - -"""Support for integration multi-arch hints.""" - -import argparse -import logging - -import silver_platter # noqa: F401 - -from lintian_brush import run_lintian_fixer, SUPPORTED_CERTAINTIES -from lintian_brush.config import Config -from debmutate.reformatting import GeneratedFile, FormattingUnpreservable - -from . import control_files_in_root, control_file_present, is_debcargo_package - -from .changer import ( - DebianChanger, - ChangerResult, - ChangerError, - run_mutator, -) - -BRANCH_NAME = "multi-arch-fixes" - - -DEFAULT_VALUE_MULTIARCH_HINT = 50 -MULTIARCH_HINTS_VALUE = { - "ma-foreign": 20, - "file-conflict": 50, - "ma-foreign-library": 20, - "dep-any": 20, - "ma-same": 20, - "arch-all": 20, -} - - -def calculate_value(hints): - return sum(map(MULTIARCH_HINTS_VALUE.__getitem__, hints)) + ( - DEFAULT_VALUE_MULTIARCH_HINT - ) - - -class MultiArchHintsChanger(DebianChanger): - - name: str = "apply-multiarch-hints" - - @classmethod - def setup_parser(cls, parser: argparse.ArgumentParser) -> None: - # Hide the minimum-certainty option for the moment. - parser.add_argument( - "--minimum-certainty", - type=str, - choices=SUPPORTED_CERTAINTIES, - default=None, - help=argparse.SUPPRESS, - ) - parser.add_argument( - "--allow-reformatting", - default=None, - action="store_true", - help=argparse.SUPPRESS, - ) - - @classmethod - def from_args(cls, args): - return cls(args.minimum_certainty, args.allow_reformatting) - - def __init__(self, minimum_certainty=None, allow_reformatting=None): - from lintian_brush.multiarch_hints import ( - cache_download_multiarch_hints, - multiarch_hints_by_binary, - parse_multiarch_hints, - ) - - with cache_download_multiarch_hints() as f: - self.hints = multiarch_hints_by_binary(parse_multiarch_hints(f)) - self.minimum_certainty = minimum_certainty - self.allow_reformatting = allow_reformatting - - def suggest_branch_name(self): - return BRANCH_NAME - - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - from lintian_brush import NoChanges - from lintian_brush.multiarch_hints import ( - MultiArchHintFixer, - ) - - base_revid = local_tree.last_revision() - minimum_certainty = self.minimum_certainty - allow_reformatting = self.allow_reformatting - try: - cfg = Config.from_workingtree(local_tree, subpath) - except FileNotFoundError: - pass - else: - if minimum_certainty is None: - minimum_certainty = cfg.minimum_certainty() - if allow_reformatting is None: - allow_reformatting = cfg.allow_reformatting() - if update_changelog is None: - update_changelog = cfg.update_changelog() - - if control_files_in_root(local_tree, subpath): - raise ChangerError( - "control-files-in-root", - "control files live in root rather than debian/ " "(LarstIQ mode)", - ) - - if not control_file_present(local_tree, subpath): - if is_debcargo_package(local_tree, subpath): - raise ChangerError("debcargo-package", "Package uses debcargo") - raise ChangerError("missing-control-file", "Unable to find debian/control") - - try: - with local_tree.lock_write(): - result, summary = run_lintian_fixer( - local_tree, - MultiArchHintFixer(self.hints), - update_changelog=update_changelog, - minimum_certainty=minimum_certainty, - subpath=subpath, - allow_reformatting=allow_reformatting, - net_access=True, - committer=committer, - changes_by="apply-multiarch-hints", - ) - except NoChanges: - raise ChangerError("nothing-to-do", "no hints to apply") - except FormattingUnpreservable as e: - raise ChangerError( - "formatting-unpreservable", - "unable to preserve formatting while editing %s" % e.path, - ) - except GeneratedFile as e: - raise ChangerError( - "generated-file", "unable to edit generated file: %r" % e - ) - - applied_hints = [] - hint_names = [] - for (binary, hint, description, certainty) in result.changes: - hint_names.append(hint["link"].split("#")[-1]) - entry = dict(hint.items()) - hint_names.append(entry["link"].split("#")[-1]) - entry["action"] = description - entry["certainty"] = certainty - applied_hints.append(entry) - logging.info("%s: %s" % (binary["Package"], description)) - - reporter.report_metadata("applied-hints", applied_hints) - - branches = [("main", None, base_revid, local_tree.last_revision())] - - tags = [] - - return ChangerResult( - description="Applied multi-arch hints.", - mutator=result, - branches=branches, - tags=tags, - value=calculate_value(hint_names), - sufficient_for_proposal=True, - proposed_commit_message="Apply multi-arch hints.", - ) - - def get_proposal_description(self, applied, description_format, existing_proposal): - ret = ["Apply multi-arch hints.\n"] - for binary, hint, description, certainty in applied.changes: - ret.append("* %s: %s\n" % (binary["Package"], description)) - return "".join(ret) - - def describe(self, applied, publish_result): - logging.info("Applied multi-arch hints.") - for binary, hint, description, certainty in applied.changes: - logging.info("* %s: %s", binary["Package"], description) - - @classmethod - def describe_command(cls, command): - return "Apply multi-arch hints" - - -if __name__ == "__main__": - import sys - - sys.exit(run_mutator(MultiArchHintsChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/orphan.py silver-platter-0.4.3/silver_platter/debian/orphan.py --- silver-platter-0.4.1/silver_platter/debian/orphan.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/orphan.py 2021-07-08 08:20:17.000000000 +0000 @@ -19,13 +19,16 @@ from urllib.parse import urlparse from breezy import osutils +from breezy.branch import Branch from debmutate.control import ControlEditor +from debmutate.deb822 import ChangeConflict from debmutate.reformatting import GeneratedFile, FormattingUnpreservable from . import ( pick_additional_colocated_branches, + connect_udd_mirror, add_changelog_entry, ) from .changer import ( @@ -40,16 +43,43 @@ BRANCH_NAME = "orphan" -def push_to_salsa(local_tree, user, name, dry_run=False): +def push_to_salsa(local_tree, orig_branch, user, name, dry_run=False): + from breezy import urlutils from breezy.branch import Branch + from breezy.errors import PermissionDenied + from breezy.propose import UnsupportedHoster, get_hoster, HosterLoginRequired from breezy.plugins.gitlab.hoster import GitLab - salsa = GitLab.probe_from_url("https://salsa.debian.org/") - # TODO(jelmer): Fork if the old branch was hosted on salsa if dry_run: logging.info("Creating and pushing to salsa project %s/%s", user, name) return - salsa.create_project("%s/%s" % (user, name)) + + try: + salsa = GitLab.probe_from_url("https://salsa.debian.org/") + except HosterLoginRequired: + logging.warning("No login for salsa known, not pushing branch.") + return + + try: + orig_hoster = get_hoster(orig_branch) + except UnsupportedHoster: + logging.debug("Original branch %r not hosted on salsa.") + from_project = None + else: + if orig_hoster == salsa: + from_project = urlutils.from_string(orig_branch.controldir.user_url).path + else: + from_project = None + + if from_project is not None: + salsa.fork_project(from_project, owner=user) + else: + try: + salsa.create_project("%s/%s" % (user, name)) + except PermissionDenied as e: + logging.info('No permission to create new project under %s: %s', + user, e) + return target_branch = Branch.open( "git+ssh://git@salsa.debian.org/%s/%s.git" % (user, name) ) @@ -67,13 +97,29 @@ class OrphanResult(object): def __init__( - self, package=None, old_vcs_url=None, new_vcs_url=None, salsa_user=None + self, + package=None, + old_vcs_url=None, + new_vcs_url=None, + salsa_user=None, + wnpp_bug=None, ): self.package = package self.old_vcs_url = old_vcs_url self.new_vcs_url = new_vcs_url self.pushed = False self.salsa_user = salsa_user + self.wnpp_bug = wnpp_bug + + +def find_wnpp_bug(source): + conn = connect_udd_mirror() + cursor = conn.cursor() + cursor.execute("select id from wnpp where type = 'O' and source = %s", (source,)) + entry = cursor.fetchone() + if entry is None: + raise KeyError + return entry[0] class OrphanChanger(DebianChanger): @@ -81,12 +127,18 @@ name = "orphan" def __init__( - self, update_vcs=True, salsa_push=True, salsa_user="debian", dry_run=False + self, + update_vcs=True, + salsa_push=True, + salsa_user="debian", + dry_run=False, + check_wnpp=True, ): self.update_vcs = update_vcs self.salsa_push = salsa_push self.salsa_user = salsa_user self.dry_run = dry_run + self.check_wnpp = check_wnpp @classmethod def setup_parser(cls, parser): @@ -107,7 +159,9 @@ help="Update the VCS-* headers, but don't actually " "clone the repository.", ) - parser.add_argument("--dry-run", action="store_true", help="Dry run changes.") + parser.add_argument( + "--no-check-wnpp", action="store_true", help="Do not check for WNPP bug." + ) @classmethod def from_args(cls, args): @@ -116,12 +170,13 @@ dry_run=args.dry_run, salsa_user=args.salsa_user, salsa_push=not args.just_update_headers, + check_wnpp=not args.no_check_wnpp, ) def suggest_branch_name(self): return BRANCH_NAME - def make_changes( + def make_changes( # noqa: C901 self, local_tree, subpath, @@ -132,15 +187,31 @@ ): base_revid = local_tree.last_revision() control_path = local_tree.abspath(osutils.pathjoin(subpath, "debian/control")) + changelog_entries = [] try: with ControlEditor(path=control_path) as editor: + if self.check_wnpp: + try: + wnpp_bug = find_wnpp_bug(editor.source["Source"]) + except KeyError: + raise ChangerError( + "nothing-to-do", + "Package is purported to be orphaned, " + "but no open wnpp bug exists.", + ) + else: + wnpp_bug = None editor.source["Maintainer"] = "Debian QA Group " try: del editor.source["Uploaders"] except KeyError: pass - - result = OrphanResult() + if editor.changed: + if wnpp_bug is not None: + changelog_entries.append("Orphan package - see bug %d." % wnpp_bug) + else: + changelog_entries.append("Orphan package.") + result = OrphanResult(wnpp_bug=wnpp_bug) if self.update_vcs: with ControlEditor(path=control_path) as editor: @@ -158,11 +229,17 @@ result.salsa_user = self.salsa_user if result.old_vcs_url == result.new_vcs_url: result.old_vcs_url = result.new_vcs_url = None + if editor.changed: + changelog_entries.append( + "Update VCS URLs to point to Debian group." + ) + if not changelog_entries: + raise ChangerError("nothing-to-do", "Already orphaned") if update_changelog in (True, None): add_changelog_entry( local_tree, osutils.pathjoin(subpath, "debian/changelog"), - ["QA Upload.", "Move package to QA team."], + ["QA Upload."] + changelog_entries, ) local_tree.commit( "Move package to QA team.", committer=committer, allow_pointless=False @@ -172,19 +249,31 @@ "formatting-unpreservable", "unable to preserve formatting while editing %s" % e.path, ) - except GeneratedFile as e: + except (ChangeConflict, GeneratedFile) as e: raise ChangerError( "generated-file", "unable to edit generated file: %r" % e ) + result.pushed = False if self.update_vcs and self.salsa_push and result.new_vcs_url: - push_to_salsa( - local_tree, self.salsa_user, result.package_name, dry_run=self.dry_run + parent_branch_url = local_tree.branch.get_parent() + if parent_branch_url is not None: + parent_branch = Branch.open(parent_branch_url) + else: + parent_branch = local_tree.branch + push_result = push_to_salsa( + local_tree, + parent_branch, + self.salsa_user, + result.package_name, + dry_run=self.dry_run, ) - result.pushed = True - reporter.report_metadata("old_vcs_url", result.old_vcs_url) - reporter.report_metadata("new_vcs_url", result.new_vcs_url) - reporter.report_metadata("pushed", result.pushed) + if push_result: + result.pushed = True + reporter.report_metadata("old_vcs_url", result.old_vcs_url) + reporter.report_metadata("new_vcs_url", result.new_vcs_url) + reporter.report_metadata("pushed", result.pushed) + reporter.report_metadata("wnpp_bug", result.wnpp_bug) branches = [("main", None, base_revid, local_tree.last_revision())] @@ -209,9 +298,7 @@ publish_result.proposal.url, ) else: - logging.info( - "No changes for orphaned package %s", - result.package_name) + logging.info("No changes for orphaned package %s", result.package_name) if result.pushed: logging.info("Pushed new package to %s.", result.new_vcs_url) elif result.new_vcs_url: diff -Nru silver-platter-0.4.1/silver_platter/debian/rrr.py silver-platter-0.4.3/silver_platter/debian/rrr.py --- silver-platter-0.4.1/silver_platter/debian/rrr.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/rrr.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,113 +0,0 @@ -#!/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 breezy import osutils - -from debmutate.control import ControlEditor - -from . import add_changelog_entry -from .changer import ( - run_mutator, - DebianChanger, - ChangerResult, -) - -BRANCH_NAME = "rules-requires-root" - - -class RulesRequiresRootResult(object): - def __init__(self, package=None): - self.package = package - - -class RulesRequiresRootChanger(DebianChanger): - - name = "rules-requires-root" - - def __init__(self, dry_run=False): - self.dry_run = dry_run - - @classmethod - def setup_parser(cls, parser): - pass - - @classmethod - def from_args(cls, args): - return cls() - - def suggest_branch_name(self): - return BRANCH_NAME - - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - base_revid = local_tree.last_revision() - with ControlEditor.from_tree(local_tree, subpath) as updater: - updater.source["Rules-Requires-Root"] = "no" - result = RulesRequiresRootResult(updater.source["Source"]) - if update_changelog in (True, None): - add_changelog_entry( - local_tree, - osutils.pathjoin(subpath, "debian/changelog"), - ["Set Rules-Requires-Root: no."], - ) - revid = local_tree.commit( - "Set Rules-Requires-Root.", committer=committer, allow_pointless=False - ) - - branches = [("main", None, base_revid, revid)] - - tags = [] - - return ChangerResult( - description="Set Rules-Requires-Root", - mutator=result, - branches=branches, - tags=tags, - sufficient_for_proposal=True, - proposed_commit_message="Set Rules-Requires-Root.", - ) - - def get_proposal_description(self, applied, description_format, existing_proposal): - return "Set Rules-Requires-Root." - - def describe(self, result, publish_result): - if publish_result.is_new: - logging.info( - "Proposed change to enable Rules-Requires-Root: %s", - publish_result.proposal.url, - ) - else: - logging.info("No changes for package %s", result.package_name) - - @classmethod - def describe_command(cls, command): - return "Set rules-requires-root" - - -if __name__ == "__main__": - import sys - - sys.exit(run_mutator(RulesRequiresRootChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/run.py silver-platter-0.4.3/silver_platter/debian/run.py --- silver-platter-0.4.1/silver_platter/debian/run.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/run.py 2021-07-08 08:20:17.000000000 +0000 @@ -17,86 +17,350 @@ """Support for updating with a script.""" +import argparse import logging - -from .changer import ( - ChangerError, - ChangerResult, - DebianChanger, -) -from ..run import ( - ScriptMadeNoChanges, - derived_branch_name, +import os +import sys +from typing import Optional, List + +from breezy import osutils +from breezy import propose as _mod_propose + +import silver_platter # noqa: F401 + +from . import ( + DEFAULT_BUILDER, + BuildFailedError, + MissingUpstreamTarball, + build, + ) +from .apply import ( script_runner, + 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, ) -class ScriptChanger(DebianChanger): +def derived_branch_name(script: str) -> str: + return os.path.splitext(osutils.basename(script.split(" ")[0]))[0] - name = "run" - def _init__(self, script, commit_pending=None): - self.script = script - self.commit_pending = commit_pending - - @classmethod - def setup_parser(cls, parser): - parser.add_argument("script", help="Path to script to run.", type=str) - parser.add_argument( - "--commit-pending", - help="Commit pending changes after script.", - choices=["yes", "no", "auto"], - default="auto", - type=str, +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.exception("%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), ) + 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 - @classmethod - def from_args(cls, args): - commit_pending = {"auto": None, "yes": True, "no": False}[args.commit_pending] - return cls(script=args.script, commit_pending=commit_pending) + 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 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) - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - base_revid = local_tree.last_revision() + enable_tag_pushing(ws.local_tree.branch) try: - description = script_runner(local_tree, self.script, self.commit_pending) - except ScriptMadeNoChanges as e: - raise ChangerError("nothing-to-do", "Script did not make any changes.", e) - - branches = [("main", None, base_revid, local_tree.last_revision())] - - tags = [] - - # TODO(jelmer): Compare old and new tags/branches? - - return ChangerResult( - description=description, - mutator=description, - sufficient_for_proposal=True, - branches=branches, - tags=tags, - proposed_commit_message=None, - ) + 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) - def get_proposal_description( - self, description, description_format, existing_proposal - ): - if description is not None: - return description + 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") - def describe(self, description, publish_result): - logging.info("%s", description) + 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 + - def suggest_branch_name(self): - return derived_branch_name(self.script) +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff -Nru silver-platter-0.4.1/silver_platter/debian/scrub_obsolete.py silver-platter-0.4.3/silver_platter/debian/scrub_obsolete.py --- silver-platter-0.4.1/silver_platter/debian/scrub_obsolete.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/scrub_obsolete.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,164 +0,0 @@ -#!/usr/bin/python3 -# 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 - -"""Support for scrubbing obsolete settings.""" - -import argparse -import logging - -from debmutate.reformatting import GeneratedFile, FormattingUnpreservable - -import silver_platter # noqa: F401 - -from lintian_brush.config import Config - -from .changer import ( - DebianChanger, - ChangerError, - ChangerResult, - run_mutator, -) - - -BRANCH_NAME = "scrub-obsolete" -DEFAULT_VALUE_MULTIARCH_HINT = 30 - - -def calculate_value(result): - value = DEFAULT_VALUE_MULTIARCH_HINT - for para, changes in result.control_removed: - for field, packages in changes: - value += len(packages) * 2 - for path, removed in result.maintscript_removed: - value += len(removed) - return value - - -class ScrubObsoleteChanger(DebianChanger): - - name: str = "scrub-obsolete" - - @classmethod - def setup_parser(cls, parser: argparse.ArgumentParser) -> None: - parser.add_argument( - "--allow-reformatting", - default=None, - action="store_true", - help=argparse.SUPPRESS, - ) - parser.add_argument( - "--upgrade-release", - metavar="UPGRADE-RELEASE", - help="Release to allow upgrading from.", - default="oldstable", - ) - - @classmethod - def from_args(cls, args): - import distro_info - - debian_info = distro_info.DebianDistroInfo() - upgrade_release = debian_info.codename(args.upgrade_release) - return cls( - allow_reformatting=args.allow_reformatting, upgrade_release=upgrade_release - ) - - def __init__(self, upgrade_release, allow_reformatting=None): - self.allow_reformatting = allow_reformatting - self.upgrade_release = upgrade_release - - def suggest_branch_name(self): - return BRANCH_NAME - - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - from lintian_brush.scrub_obsolete import scrub_obsolete - - base_revid = local_tree.last_revision() - allow_reformatting = self.allow_reformatting - try: - cfg = Config.from_workingtree(local_tree, subpath) - except FileNotFoundError: - pass - else: - if allow_reformatting is None: - allow_reformatting = cfg.allow_reformatting() - if update_changelog is None: - update_changelog = cfg.update_changelog() - - try: - result = scrub_obsolete( - local_tree, - subpath, - self.upgrade_release, - update_changelog=update_changelog, - ) - except FormattingUnpreservable as e: - raise ChangerError( - "formatting-unpreservable", - "unable to preserve formatting while editing %s" % e.path, - ) - except GeneratedFile as e: - raise ChangerError( - "generated-file", "unable to edit generated file: %r" % e - ) - - if not result: - raise ChangerError("nothing-to-do", "no obsolete constraints") - - branches = [("main", None, base_revid, local_tree.last_revision())] - - tags = [] - - return ChangerResult( - description="Scrub obsolete settings.", - mutator=result, - branches=branches, - tags=tags, - value=calculate_value(result), - sufficient_for_proposal=True, - proposed_commit_message="Scrub obsolete settings.", - ) - - def get_proposal_description(self, result, description_format, existing_proposal): - ret = [ - "Remove constraints unnecessary since %s." % self.upgrade_release, - "", - ] + ["* " + line for line in result.itemized()] - return "".join(ret) - - def describe(self, applied, publish_result): - logging.info("Scrub obsolete settings.") - for line in applied.itemized(): - logging.info("* %s", line) - - @classmethod - def describe_command(cls, command): - return "Remove obsolete dependencies" - - -if __name__ == "__main__": - import sys - - sys.exit(run_mutator(ScrubObsoleteChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/tidy.py silver-platter-0.4.3/silver_platter/debian/tidy.py --- silver-platter-0.4.1/silver_platter/debian/tidy.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/tidy.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,129 +0,0 @@ -#!/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 logging - -from .changer import ( - run_mutator, - DebianChanger, - ChangerResult, -) - -from .lintian import LintianBrushChanger -from .multiarch import MultiArchHintsChanger - - -BRANCH_NAME = "tidy" - - -class TidyChanger(DebianChanger): - - name = "tidy" - - SUBCHANGERS = [ - LintianBrushChanger, - MultiArchHintsChanger, - ] - - def __init__(self) -> None: - self.subchangers = [kls() for kls in self.SUBCHANGERS] - - @classmethod - def setup_parser(cls, parser): - pass - - @classmethod - def from_args(cls, args): - return cls() - - def suggest_branch_name(self): - return BRANCH_NAME - - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - base_revid = local_tree.last_revision() - result = {} - tags = [] - sufficient_for_proposal = False - branches = [] - for subchanger in self.subchangers: - subresult = subchanger.make_changes( - local_tree, subpath, update_changelog, committer - ) - result[subchanger] = subresult.mutator - if subresult.sufficient_for_proposal: - sufficient_for_proposal = True - if subresult.tags: - tags.extend(subresult.tags) - if subresult.branches: - branches.extend( - [entry for entry in subresult.branches if entry[0] != "main"] - ) - - commit_items = [] - for subchanger in result: - if isinstance(subchanger, LintianBrushChanger): - commit_items.append("fix some lintian tags") - if isinstance(subchanger, MultiArchHintsChanger): - commit_items.append("apply multi-arch hints") - proposed_commit_message = (", ".join(commit_items) + ".").capitalize() - - branches.insert(0, ("main", None, base_revid, local_tree.last_revision())) - - return ChangerResult( - mutator=result, - description="Fix various small issues.", - tags=tags, - branches=branches, - sufficient_for_proposal=sufficient_for_proposal, - proposed_commit_message=proposed_commit_message, - ) - - def get_proposal_description(self, result, description_format, existing_proposal): - entries = [] - for subchanger, memo in result.items(): - # TODO(jelmer): Does passing existing proposal in here work? - entries.append( - subchanger.get_proposal_description( - memo, description_format, existing_proposal - ) - ) - return "\n".join(entries) - - def describe(self, result, publish_result): - if publish_result.is_new: - logging.info( - "Create merge proposal: %s", publish_result.proposal.url) - elif result: - logging.info( - "Updated proposal %s", publish_result.proposal.url) - else: - logging.info( - "No new fixes for proposal %s", publish_result.proposal.url) - - -if __name__ == "__main__": - import sys - - sys.exit(run_mutator(TidyChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/uncommitted.py silver-platter-0.4.3/silver_platter/debian/uncommitted.py --- silver-platter-0.4.1/silver_platter/debian/uncommitted.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/uncommitted.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,308 +0,0 @@ -#!/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 contextlib -import json -import logging -import os -import subprocess -import tempfile - -from urllib.request import urlopen - -from debian.changelog import Changelog - -from . import NoAptSources -from .changer import ( - run_mutator, - DebianChanger, - ChangerResult, - ChangerError, -) -from breezy.plugins.debian.upstream import PackageVersionNotPresent - - -BRANCH_NAME = "missing-commits" - - -class AptSourceError(Exception): - """An error occured while running 'apt source'.""" - - def __init__(self, reason): - self.reason = reason - - -def select_vcswatch_packages(): - import psycopg2 - - conn = psycopg2.connect( - database="udd", - user="udd-mirror", - password="udd-mirror", - host="udd-mirror.debian.net", - ) - cursor = conn.cursor() - args = [] - query = """\ - SELECT sources.source, vcswatch.url - FROM vcswatch JOIN sources ON sources.source = vcswatch.source - WHERE - vcswatch.status IN ('OLD', 'UNREL') AND - sources.release = 'sid' -""" - cursor.execute(query, tuple(args)) - packages = [] - for package, vcs_url in cursor.fetchall(): - packages.append(package) - return packages - - -def download_snapshot(package, version, output_dir, no_preparation=False): - logging.info("Downloading %s %s", package, version) - srcfiles_url = ( - "https://snapshot.debian.org/mr/package/%s/%s/" - "srcfiles?fileinfo=1" % (package, version) - ) - files = {} - for hsh, entries in json.load(urlopen(srcfiles_url))["fileinfo"].items(): - for entry in entries: - files[entry["name"]] = hsh - for filename, hsh in files.items(): - local_path = os.path.join(output_dir, os.path.basename(filename)) - with open(local_path, "wb") as f: - url = "https://snapshot.debian.org/file/%s" % hsh - with urlopen(url) as g: - f.write(g.read()) - args = [] - if no_preparation: - args.append("--no-preparation") - subprocess.check_call( - ["dpkg-source"] + args + ["-x", "%s_%s.dsc" % (package, version)], - cwd=output_dir, - ) - - -class NoMissingVersions(Exception): - def __init__(self, vcs_version, archive_version): - self.vcs_version = vcs_version - self.archive_version = archive_version - super(NoMissingVersions, self).__init__( - "No missing versions after all. Archive has %s, VCS has %s" - % (archive_version, vcs_version) - ) - - -class TreeVersionNotInArchiveChangelog(Exception): - def __init__(self, tree_version): - self.tree_version = tree_version - super(TreeVersionNotInArchiveChangelog, self).__init__( - "tree version %s does not appear in archive changelog" % tree_version - ) - - -class TreeUpstreamVersionMissing(Exception): - def __init__(self, upstream_version): - self.upstream_version = upstream_version - super(TreeUpstreamVersionMissing, self).__init__( - "unable to find upstream version %r" % upstream_version - ) - - -def retrieve_source(package_name, target): - try: - subprocess.run( - ["apt", "source", package_name], - cwd=target, - check=True, - stderr=subprocess.PIPE, - ) - except subprocess.CalledProcessError as e: - stderr = e.stderr.splitlines() - if stderr[-1] == ( - b"E: You must put some 'source' URIs in your " b"sources.list" - ): - raise NoAptSources() - CS = b"\x1b[1;31mE: \x1b[0m" - CE = b"\x1b[0m" - if stderr[-1] == ( - CS + b"You must put some 'deb-src' URIs in your sources.list" + CE - ): - raise NoAptSources() - if stderr[-1].startswith(b"E: "): - raise AptSourceError(stderr[-1][3:].decode()) - if stderr[-1].startswith(CS): - raise AptSourceError(stderr[-1][len(CS) : -len(CE)]) - raise AptSourceError( - [line.decode("utf-8", "surrogateescape") for line in stderr] - ) - - -def import_uncommitted(tree, subpath): - from breezy.plugins.debian.import_dsc import ( - DistributionBranch, - DistributionBranchSet, - ) - - cl_path = os.path.join(subpath, "debian/changelog") - with tree.get_file(cl_path) as f: - tree_cl = Changelog(f) - package_name = tree_cl.package - with contextlib.ExitStack() as es: - archive_source = es.enter_context(tempfile.TemporaryDirectory()) - try: - retrieve_source(package_name, archive_source) - except AptSourceError as e: - if isinstance(e.reason, list): - reason = e.reason[-1] - else: - reason = e.reason - raise ChangerError("apt-source-error", reason) - except NoAptSources: - raise ChangerError( - "no-apt-sources", "No sources configured in /etc/apt/sources.list" - ) - [subdir] = [e.path for e in os.scandir(archive_source) if e.is_dir()] - with open(os.path.join(subdir, "debian", "changelog"), "r") as f: - archive_cl = Changelog(f) - missing_versions = [] - for block in archive_cl: - if block.version == tree_cl.version: - break - missing_versions.append(block.version) - else: - raise TreeVersionNotInArchiveChangelog(tree_cl.version) - if len(missing_versions) == 0: - raise NoMissingVersions(tree_cl.version, archive_cl.version) - logging.info("Missing versions: %s", ", ".join(map(str, missing_versions))) - ret = [] - dbs = DistributionBranchSet() - db = DistributionBranch(tree.branch, tree.branch, tree=tree) - dbs.add_branch(db) - if tree_cl.version.debian_revision: - logging.info("Extracting upstream version %s.", tree_cl.version.upstream_version) - upstream_dir = es.enter_context(tempfile.TemporaryDirectory()) - try: - upstream_tips = db.pristine_upstream_source.version_as_revisions( - tree_cl.package, tree_cl.version.upstream_version - ) - except PackageVersionNotPresent: - raise TreeUpstreamVersionMissing(tree_cl.version.upstream_version) - db.extract_upstream_tree(upstream_tips, upstream_dir) - no_preparation = not tree.has_filename(".pc/applied-patches") - version_path = {} - for version in missing_versions: - output_dir = es.enter_context(tempfile.TemporaryDirectory()) - download_snapshot( - package_name, version, output_dir, no_preparation=no_preparation - ) - version_path[version] = output_dir - for version in reversed(missing_versions): - logging.info("Importing %s", version) - dsc_path = os.path.join( - version_path[version], "%s_%s.dsc" % (package_name, version) - ) - tag_name = db.import_package(dsc_path) - revision = db.version_as_revisions(version) - ret.append((tag_name, version, revision)) - return ret - - -class UncommittedChanger(DebianChanger): - - name = "import-upload" - - @classmethod - def setup_parser(cls, parser): - pass - - @classmethod - def from_args(cls, args): - return cls() - - def suggest_branch_name(self): - return BRANCH_NAME - - def make_changes( - self, - local_tree, - subpath, - update_changelog, - reporter, - committer, - base_proposal=None, - ): - base_revid = local_tree.last_revision() - try: - ret = import_uncommitted(local_tree, subpath) - except TreeUpstreamVersionMissing as e: - raise ChangerError("tree-upstream-version-missing", str(e)) - except TreeVersionNotInArchiveChangelog as e: - raise ChangerError("tree-version-not-in-archive-changelog", str(e)) - except NoMissingVersions as e: - raise ChangerError("nothing-to-do", str(e)) - tags = [(None, tag_name, revid) for (tag_name, version, revid) in ret] - # TODO(jelmer): Include upstream tags - proposed_commit_message = "Import missing uploads: %s." % ( - ", ".join([str(v) for t, v in ret]) - ) - reporter.report_metadata( - "tags", [(tag_name, str(version)) for (tag_name, version, revid) in ret] - ) - - branches = [("main", None, base_revid, local_tree.last_revision())] - - # TODO(jelmer): Include branches for upstream/pristine-tar - - return ChangerResult( - description="Import archive changes missing from the VCS.", - branches=branches, - mutator=ret, - tags=tags, - sufficient_for_proposal=True, - proposed_commit_message=proposed_commit_message, - ) - - def get_proposal_description(self, applied, description_format, existing_proposal): - return "Import missing uploads: %s." % (", ".join([str(v) for t, v in applied])) - - def describe(self, applied, publish_result): - if publish_result.is_new: - logging.info( - "Proposed import of versions %s: %s", - ", ".join([str(v) for t, v in applied]), - publish_result.proposal.url, - ) - elif applied: - logging.info( - "Updated proposal %s with versions %s.", - publish_result.proposal.url, - ", ".join([str(v) for t, v in applied]), - ) - else: - logging.info( - "No new versions imported for proposal %s", publish_result.proposal.url - ) - - @classmethod - def describe_command(cls, command): - return "Import archive changes missing from VCS" - - -if __name__ == "__main__": - import sys - - sys.exit(run_mutator(UncommittedChanger)) diff -Nru silver-platter-0.4.1/silver_platter/debian/uploader.py silver-platter-0.4.3/silver_platter/debian/uploader.py --- silver-platter-0.4.1/silver_platter/debian/uploader.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/uploader.py 2021-07-08 08:20:17.000000000 +0000 @@ -50,7 +50,6 @@ changelog_find_previous_upload, dput_changes, find_changelog, - debsign, MissingChangelogError, ) @@ -58,6 +57,7 @@ from . import ( apt_get_source_package, + connect_udd_mirror, source_package_vcs, split_vcs_url, Workspace, @@ -73,6 +73,15 @@ ) +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.""" @@ -281,14 +290,7 @@ def select_vcswatch_packages( packages: List[str], maintainer: List[str], autopkgtest_only: bool ): - import psycopg2 - - conn = psycopg2.connect( - database="udd", - user="udd-mirror", - password="udd-mirror", - host="udd-mirror.debian.net", - ) + conn = connect_udd_mirror() cursor = conn.cursor() args = [] query = """\ @@ -377,6 +379,7 @@ ) args = parser.parse_args(argv) + ret = 0 if not args.packages and not args.maintainer: @@ -427,7 +430,9 @@ try: vcs_type, vcs_url = source_package_vcs(pkg_source) except KeyError: - logging.info("%s: no declared vcs location, skipping", pkg_source["Package"]) + logging.info( + "%s: no declared vcs location, skipping", pkg_source["Package"] + ) ret = 1 continue source_name = pkg_source["Package"] @@ -471,8 +476,7 @@ os.path.join(subpath, "debian/tests/control") ) ): - logging.info( - "%s: Skipping, package has no autopkgtest.", source_name) + logging.info("%s: Skipping, package has no autopkgtest.", source_name) continue branch_config = ws.local_tree.branch.get_config_stack() if args.gpg_verification: @@ -532,17 +536,14 @@ continue except RecentCommits as e: logging.info( - "%s: Recent commits (%d days), skipping.", - source_name, e.commit_age + "%s: Recent commits (%d days), skipping.", source_name, e.commit_age ) continue except NoUnuploadedChanges: - logging.info( - "%s: No unuploaded changes, skipping.", source_name) + logging.info("%s: No unuploaded changes, skipping.", source_name) continue except NoUnreleasedChanges: - logging.info( - "%s: No unreleased changes, skipping.", source_name) + logging.info("%s: No unreleased changes, skipping.", source_name) continue tags = [] @@ -553,8 +554,8 @@ ws.push(dry_run=args.dry_run, tags=tags) except PermissionDenied: logging.info( - "%s: Permission denied pushing to branch, skipping.", - source_name) + "%s: Permission denied pushing to branch, skipping.", source_name + ) ret = 1 continue if not args.dry_run: diff -Nru silver-platter-0.4.1/silver_platter/debian/upstream.py silver-platter-0.4.3/silver_platter/debian/upstream.py --- silver-platter-0.4.1/silver_platter/debian/upstream.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/debian/upstream.py 2021-07-08 08:20:17.000000000 +0000 @@ -20,6 +20,7 @@ import silver_platter # noqa: F401 import argparse +from email.utils import parseaddr import errno import logging import os @@ -29,7 +30,7 @@ import traceback from typing import List, Optional, Callable, Union -from debian.changelog import Version, ChangelogParseError +from debian.changelog import Version, ChangelogParseError, get_maintainer from ..utils import ( full_branch_url, @@ -40,7 +41,6 @@ ) from . import ( - changelog_add_line, control_files_in_root, ) from .changer import ( @@ -75,7 +75,6 @@ from breezy.transform import MalformedTransform from breezy.plugins.debian.merge_upstream import ( - changelog_add_new_version, do_import, do_merge, get_tarballs, @@ -122,6 +121,16 @@ DistCommandFailed, run_dist_command, ) + +from debmutate.changelog import ChangelogEditor, upstream_merge_changelog_line +from debmutate.versions import ( + new_upstream_package_version, + initial_debian_revision, + debianize_upstream_version, + ) + +from debmutate.watch import WatchSyntaxError + from breezy.tree import Tree from lintian_brush.vcs import sanitize_url as sanitize_vcs_url @@ -151,9 +160,18 @@ "UpstreamMetadataSyntaxError", "QuiltPatchPushFailure", "WatchLineWithoutMatches", + "BigVersionJump", ] +class BigVersionJump(Exception): + """There was a big version jump.""" + + def __init__(self, old_upstream_version, new_upstream_version): + self.old_upstream_version = old_upstream_version + self.new_upstream_version = new_upstream_version + + class UpstreamMergeConflicted(Exception): """The upstream merge resulted in conflicts.""" @@ -249,9 +267,25 @@ DEFAULT_DISTRIBUTION = "unstable" +def is_big_version_jump(old_upstream_version, new_upstream_version): + try: + old_major_version = int(str(old_upstream_version).split('.')[0]) + except ValueError: + return False + try: + new_major_version = int(str(new_upstream_version).split('.')[0]) + except ValueError: + return False + if old_major_version > 0 and new_major_version > 5*old_major_version: + return True + return False + + def get_upstream_branch_location(tree, subpath, config, trust_package=False): if config.upstream_branch is not None: - logging.info("Using upstream branch %s (from configuration)", config.upstream_branch) + logging.info( + "Using upstream branch %s (from configuration)", config.upstream_branch + ) # TODO(jelmer): Make brz-debian sanitize the URL? upstream_branch_location = sanitize_vcs_url(config.upstream_branch) upstream_branch_browse = getattr(config, "upstream_branch_browse", None) @@ -259,6 +293,7 @@ from upstream_ontologist.guess import ( guess_upstream_metadata, ) + guessed_upstream_metadata = guess_upstream_metadata( tree.abspath(subpath), trust_package=trust_package, @@ -305,12 +340,10 @@ if m and getattr(patches, "delete", None): assert m.group(1) == name patches.delete(name, remove=True) - changelog_add_line( - local_tree, - subpath, - "Drop patch %s, present upstream." % name, - email=committer, - ) + with ChangelogEditor( + local_tree.abspath(os.path.join(subpath, 'debian/changelog')) + ) as cl: + cl.add_entry(["Drop patch %s, present upstream." % name]) debcommit( local_tree, committer=committer, @@ -427,6 +460,8 @@ top_level=False, create_dist=None, include_upstream_history: Optional[bool] = None, + force_big_version_jump: bool = False, + require_uscan: bool = False, ): # TODO(jelmer): Find the lastest upstream present in the upstream branch @@ -460,7 +495,7 @@ config=config, local_dir=tree.controldir, create_dist=create_dist, - snapshot=snapshot, + version_kind=("snapshot" if snapshot else "release") ) except InvalidHttpResponse as e: raise UpstreamBranchUnavailable(upstream_branch_location, str(e)) @@ -480,7 +515,7 @@ config=config, local_dir=tree.controldir, create_dist=create_dist, - snapshot=snapshot, + version_kind=("snapshot" if snapshot else "release") ) else: if snapshot: @@ -497,16 +532,27 @@ # watch file. if upstream_branch_source is None: raise NoUpstreamLocationsKnown(package) + if require_uscan: + raise primary_upstream_source = upstream_branch_source if new_upstream_version is None and primary_upstream_source is not None: - new_upstream_version = primary_upstream_source.get_latest_version( + unmangled_new_upstream_version, new_upstream_version = primary_upstream_source.get_latest_version( package, old_upstream_version ) + else: + new_upstream_version = debianize_upstream_version( + new_upstream_version) if new_upstream_version is None: raise NewUpstreamMissing() + if not new_upstream_version[0].isdigit(): + # dpkg forbids this, let's just refuse it early + raise InvalidFormatUpstreamVersion( + new_upstream_version, primary_upstream_source + ) + try: new_upstream_version = Version(new_upstream_version) except ValueError: @@ -525,6 +571,8 @@ raise NewerUpstreamAlreadyImported( old_upstream_version, new_upstream_version ) + if is_big_version_jump(old_upstream_version, new_upstream_version) and not force_big_version_jump: + raise BigVersionJump(old_upstream_version, new_upstream_version) # TODO(jelmer): Check if new_upstream_version is already imported @@ -549,11 +597,11 @@ # The branch is our primary upstream source, so if it can't # find the version then there's nothing we can do. raise UpstreamVersionMissingInUpstreamBranch( - upstream_branch_source.upstream_branch, new_upstream_version + upstream_branch_source.upstream_branch, str(new_upstream_version) ) elif not allow_ignore_upstream_branch: raise UpstreamVersionMissingInUpstreamBranch( - upstream_branch_source.upstream_branch, new_upstream_version + upstream_branch_source.upstream_branch, str(new_upstream_version) ) else: logging.warn( @@ -597,6 +645,7 @@ subpath: str = "", include_upstream_history: Optional[bool] = None, create_dist: Optional[Callable[[Tree, str, Version, str], Optional[str]]] = None, + force_big_version_jump: bool = False, ) -> ImportUpstreamResult: """Import a new upstream version into a tree. @@ -668,6 +717,7 @@ top_level=top_level, include_upstream_history=include_upstream_history, create_dist=create_dist, + force_big_version_jump=force_big_version_jump, ) with tempfile.TemporaryDirectory() as target_dir: @@ -782,6 +832,9 @@ subpath: str = "", include_upstream_history: Optional[bool] = None, create_dist: Optional[Callable[[Tree, str, Version, str], Optional[str]]] = None, + force_big_version_jump: bool = False, + debian_revision: Optional[str] = None, + require_uscan: bool = False ) -> MergeUpstreamResult: """Merge a new upstream version into a tree. @@ -807,6 +860,7 @@ NoUpstreamLocationsKnown UpstreamMetadataSyntaxError NewerUpstreamAlreadyImported + WatchSyntaxError Returns: MergeUpstreamResult object """ @@ -853,6 +907,8 @@ top_level=top_level, include_upstream_history=include_upstream_history, create_dist=create_dist, + force_big_version_jump=force_big_version_jump, + require_uscan=require_uscan, ) if need_upstream_tarball: @@ -934,33 +990,47 @@ # Re-read changelog, since it may have been changed by the merge # from upstream. - (changelog, top_level) = find_changelog(tree, subpath, False, max_blocks=2) - package = changelog.package + try: + (changelog, top_level) = find_changelog(tree, subpath, False, max_blocks=2) + except (ChangelogParseError, MissingChangelogError): + # If there was a conflict that affected debian/changelog, then that might be + # to blame. + if conflicts: + raise UpstreamMergeConflicted(old_upstream_version, conflicts) + raise + if top_level: + debian_path = subpath + else: + debian_path = os.path.join(subpath, "debian") if Version(old_upstream_version) >= Version(new_upstream_version): if conflicts: raise UpstreamMergeConflicted(old_upstream_version, conflicts) raise UpstreamAlreadyMerged(new_upstream_version) - if update_changelog: - changelog_add_new_version( - tree, subpath, new_upstream_version, distribution_name, changelog, package - ) + + with ChangelogEditor( + tree.abspath(os.path.join(debian_path, "changelog"))) as cl: + if debian_revision is None: + debian_revision = initial_debian_revision(distribution_name) + new_version = str(new_upstream_package_version( + new_upstream_version, debian_revision, cl[0].version.epoch)) + if not update_changelog: + # We need to run "gbp dch" here, since the next "gbp dch" runs + # won't pick up the pending changes, as we're about to change + # debian/changelog. + from debmutate.changelog import gbp_dch + gbp_dch(tree.basedir) + cl.auto_version(new_version) + + cl.add_entry([upstream_merge_changelog_line(new_upstream_version)]) + if not need_upstream_tarball: - logging.info( - "An entry for the new upstream version has been " "added to the changelog." - ) + logging.info("The changelog has been updated for the new version.") else: if conflicts: raise UpstreamMergeConflicted(new_upstream_version, conflicts) - if update_changelog: - debcommit(tree, subpath=subpath, committer=committer) - else: - tree.commit( - committer=committer, - message="Merge new upstream release %s." % new_upstream_version, - specific_files=([subpath] if len(tree.get_parent_ids()) <= 1 else None), - ) + debcommit(tree, subpath=subpath, committer=committer) return MergeUpstreamResult( include_upstream_history=include_upstream_history, @@ -1009,6 +1079,10 @@ old_tree: Old tree committer: Optional committer to use for changes """ + if committer is None: + maintainer = get_maintainer() + else: + maintainer = parseaddr(committer) notes = [] tree_delta = tree.changes_from(old_tree, specific_files=[subpath]) for delta in tree_delta.added: @@ -1020,13 +1094,12 @@ path = path[len(subpath) :] if path == "autogen.sh": if override_dh_autoreconf_add_arguments(tree.basedir, [b"./autogen.sh"]): - logging.info("Modifying debian/rules: " "Invoke autogen.sh from dh_autoreconf.") - changelog_add_line( - tree, - subpath, - "Invoke autogen.sh from dh_autoreconf.", - email=committer, + logging.info( + "Modifying debian/rules: " "Invoke autogen.sh from dh_autoreconf." ) + with ChangelogEditor( + tree.abspath(os.path.join(subpath, 'debian/changelog'))) as cl: + cl.add_entry(["Invoke autogen.sh from dh_autoreconf."], maintainer=maintainer) debcommit( tree, committer=committer, @@ -1051,6 +1124,10 @@ dist_command, import_only=False, include_upstream_history=None, + chroot=None, + force_big_version_jump=False, + debian_revision=None, + require_uscan=False, ): self.snapshot = snapshot self.trust_package = trust_package @@ -1059,6 +1136,10 @@ self.dist_command = dist_command self.import_only = import_only self.include_upstream_history = include_upstream_history + self.schroot = chroot + self.force_big_version_jump = force_big_version_jump + self.debian_revision = debian_revision + self.require_uscan = require_uscan @classmethod def setup_parser(cls, parser): @@ -1114,6 +1195,20 @@ help="force inclusion of upstream history", default=None, ) + parser.add_argument( + "--force-big-version-jump", + action="store_true", + help="force through big version jumps") + parser.add_argument( + "--debian-revision", + type=str, + help="Debian revision to use (e.g. '1' or '0ubuntu1')", + default=None) + parser.add_argument( + "--require-uscan", + action="store_true", + help=("Require that uscan provides a tarball" + "(if --snapshot is not specified)")) def suggest_branch_name(self): if self.snapshot: @@ -1131,11 +1226,12 @@ dist_command=args.dist_command, import_only=args.import_only, include_upstream_history=args.include_upstream_history, + chroot=args.chroot, + force_big_version_jump=args.force_big_version_jump, + debian_revision=args.debian_revision, + require_uscan=args.require_uscan, ) - def create_dist_from_command(self, tree, package, version, target_dir): - return run_dist_command(tree, package, version, target_dir, self.dist_command) - def make_changes( # noqa: C901 self, local_tree, @@ -1157,9 +1253,10 @@ ) if self.dist_command: - create_dist = self.create_dist_from_command + def create_dist(self, tree, package, version, target_dir): + return run_dist_command(tree, package, version, target_dir, self.dist_command) else: - create_dist = None + create_dist = getattr(self, 'create_dist', None) try: if not self.import_only: @@ -1173,6 +1270,9 @@ committer=committer, include_upstream_history=self.include_upstream_history, create_dist=create_dist, + force_big_version_jump=self.force_big_version_jump, + debian_revision=self.debian_revision, + require_uscan=self.require_uscan, ) except MalformedTransform: traceback.print_exc() @@ -1190,6 +1290,7 @@ committer=committer, include_upstream_history=self.include_upstream_history, create_dist=create_dist, + force_big_version_jump=self.force_big_version_jump, ) except UpstreamAlreadyImported as e: reporter.report_context(str(e.version)) @@ -1217,6 +1318,10 @@ "Last upstream version %s already merged." % e.version, e, ) + except NoWatchFile: + raise ChangerError( + "no-watch-file", + "No watch file is present, but --require-uscan was specified") except PreviousVersionTagMissing as e: raise ChangerError( "previous-upstream-missing", @@ -1264,12 +1369,17 @@ except UpstreamMergeConflicted as e: reporter.report_context(str(e.version)) reporter.report_metadata("upstream_version", str(e.version)) - reporter.report_metadata("conflicts", e.conflicts) + details = {} + if isinstance(e.conflicts, int): + conflicts = e.conflicts + else: + conflicts = [[c.path, c.typestring] for c in e.conflicts] + details['conflicts'] = conflicts + reporter.report_metadata("conflicts", conflicts) raise ChangerError( "upstream-merged-conflicts", "Merging upstream version %s resulted in conflicts." % e.version, - e, - ) + e, details=details) except PackageIsNative as e: raise ChangerError( "native-package", @@ -1324,7 +1434,7 @@ except MissingChangelogError as e: raise ChangerError("missing-changelog", "Missing changelog %s" % e, e) except DistCommandFailed as e: - raise ChangerError("dist-command-failed", "Dist command failed: %s" % e, e) + raise ChangerError("dist-command-failed", str(e), e) except MissingUpstreamTarball as e: raise ChangerError( "missing-upstream-tarball", "Missing upstream tarball: %s" % e, e @@ -1342,7 +1452,7 @@ raise ChangerError( "no-upstream-locations-known", "No debian/watch file or Repository in " - "debian/upstream/metadata to retrieve new upstream version" + "debian/upstream/metadata to retrieve new upstream version " "from.", e, ) @@ -1353,6 +1463,16 @@ "A newer upstream release (%s) has already been imported. " "Found: %s" % (e.old_upstream_version, e.new_upstream_version), ) + except WatchSyntaxError as e: + raise ChangerError('watch-syntax-error', str(e)) + except BigVersionJump as e: + raise ChangerError( + "big-version-jump", + "There was a big jump in upstream versions: %s ⇒ %s" % ( + e.old_upstream_version, e.new_upstream_version), + details={ + 'old_upstream_version': str(e.old_upstream_version), + 'new_upstream_version': str(e.new_upstream_version)}) except OSError as e: if e.errno == errno.ENOSPC: raise ChangerError("no-space-on-device", str(e)) @@ -1439,7 +1559,7 @@ old_tree = local_tree.branch.repository.revision_tree( result.old_revision ) - notes = update_packaging(local_tree, old_tree) + notes = update_packaging(local_tree, old_tree, committer=committer) reporter.report_metadata("notes", notes) for n in notes: logging.info("%s", n) @@ -1502,7 +1622,9 @@ def describe(self, merge_upstream_result, publish_result): if publish_result.proposal: if publish_result.is_new: - logging.info("Created new merge proposal %s.", publish_result.proposal.url) + logging.info( + "Created new merge proposal %s.", publish_result.proposal.url + ) else: logging.info("Updated merge proposal %s.", publish_result.proposal.url) diff -Nru silver-platter-0.4.1/silver_platter/__init__.py silver-platter-0.4.3/silver_platter/__init__.py --- silver-platter-0.4.1/silver_platter/__init__.py 2021-02-17 22:45:11.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/__init__.py 2021-07-08 08:20:17.000000000 +0000 @@ -25,5 +25,5 @@ import breezy.plugins.github # For github support # noqa: F401 import breezy.plugins.debian # For apt: URL support # noqa: F401 -__version__ = (0, 4, 1) +__version__ = (0, 4, 3) version_string = ".".join(map(str, __version__)) diff -Nru silver-platter-0.4.1/silver_platter/__main__.py silver-platter-0.4.3/silver_platter/__main__.py --- silver-platter-0.4.1/silver_platter/__main__.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/__main__.py 2021-07-08 08:20:17.000000000 +0000 @@ -21,6 +21,7 @@ import sys from typing import Optional, List, Callable, Dict from . import ( + apply, run, version_string, ) @@ -106,6 +107,7 @@ "login": login_main, "proposals": proposals_main, "run": run.main, + "apply": apply.main, } @@ -121,7 +123,7 @@ "--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) + logging.basicConfig(level=logging.INFO, format="%(message)s") args, rest = parser.parse_known_args(argv) if args.help: if args.subcommand is None: diff -Nru silver-platter-0.4.1/silver_platter/proposal.py silver-platter-0.4.3/silver_platter/proposal.py --- silver-platter-0.4.1/silver_platter/proposal.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/proposal.py 2021-07-08 08:20:17.000000000 +0000 @@ -41,19 +41,10 @@ HosterLoginRequired, ) -try: - from breezy.propose import ( - iter_hoster_instances, - SourceNotDerivedFromTarget, - ) -except ImportError: # breezy < 3.1.1 - - def iter_hoster_instances(): - for name, hoster_cls in hosters.items(): - for instance in hoster_cls.iter_instances(): - yield instance - - SourceNotDerivedFromTarget = None +from breezy.propose import ( + iter_hoster_instances, + SourceNotDerivedFromTarget, +) import breezy.plugins.gitlab # noqa: F401 import breezy.plugins.github # noqa: F401 diff -Nru silver-platter-0.4.1/silver_platter/publish.py silver-platter-0.4.3/silver_platter/publish.py --- silver-platter-0.4.1/silver_platter/publish.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/publish.py 2021-07-08 08:20:17.000000000 +0000 @@ -25,6 +25,7 @@ revision as _mod_revision, ) from breezy.errors import PermissionDenied +from breezy.memorybranch import MemoryBranch from breezy.propose import ( get_hoster, Hoster, @@ -35,17 +36,13 @@ ) from breezy.transport import Transport -try: - from breezy.propose import ( - SourceNotDerivedFromTarget, - ) -except ImportError: # breezy < 3.1.1 - SourceNotDerivedFromTarget = None +from breezy.propose import ( + SourceNotDerivedFromTarget, +) from .utils import ( open_branch, - MemoryBranch, full_branch_url, ) @@ -61,6 +58,7 @@ "NoSuchProject", "PermissionDenied", "UnsupportedHoster", + "SourceNotDerivedFromTarget", ] @@ -256,8 +254,7 @@ try: resume_proposal.reopen() except ReopenFailed: - logging.info( - "Reopening existing proposal failed. Creating new proposal.") + logging.info("Reopening existing proposal failed. Creating new proposal.") resume_proposal = None if resume_proposal is None: if not dry_run: @@ -278,8 +275,7 @@ raise resume_proposal = e.existing_proposal except errors.PermissionDenied: - logging.info( - "Permission denied while trying to create " "proposal.") + logging.info("Permission denied while trying to create " "proposal.") raise else: return (mp, True) @@ -435,6 +431,7 @@ 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. @@ -444,13 +441,21 @@ 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: - existing_branch = hoster.get_derived_branch(main_branch, name=name, owner=owner) + 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: @@ -548,6 +553,10 @@ 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, @@ -651,8 +660,7 @@ assert mode == "propose" if not resume_branch and not allow_create_proposal: - # TODO(jelmer): Raise an exception of some sort here? - return PublishResult(mode) + raise InsufficientChangesForNewProposal() mp_description = get_proposal_description( getattr(hoster, "merge_proposal_description_format", "plain"), diff -Nru silver-platter-0.4.1/silver_platter/recipe.py silver-platter-0.4.3/silver_platter/recipe.py --- silver-platter-0.4.1/silver_platter/recipe.py 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/recipe.py 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,78 @@ +#!/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)) diff -Nru silver-platter-0.4.1/silver_platter/run.py silver-platter-0.4.3/silver_platter/run.py --- silver-platter-0.4.1/silver_platter/run.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/run.py 2021-07-08 08:20:17.000000000 +0000 @@ -24,13 +24,12 @@ import sys from typing import Optional, List -import silver_platter # noqa: F401 - from breezy import osutils -from breezy import errors -from breezy.commit import PointlessCommit -from breezy.workingtree import WorkingTree 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, @@ -42,6 +41,7 @@ ) from .publish import ( SUPPORTED_MODES, + InsufficientChangesForNewProposal, ) from .utils import ( open_branch, @@ -52,113 +52,30 @@ ) -class ScriptMadeNoChanges(errors.BzrError): - - _fmt = "Script made no changes." - - -def script_runner( - local_tree: WorkingTree, script: str, commit_pending: Optional[bool] = None -) -> str: - """Run a script in a tree and commit the result. - - This ignores newly added files. - - :param local_tree: Local tree to run script in - :param script: Script to run - :param commit_pending: Whether to commit pending changes - (True, False or None: only commit if there were no commits by the - script) - :return: Description as reported by script - """ - last_revision = local_tree.last_revision() - p = subprocess.Popen( - script, cwd=local_tree.basedir, stdout=subprocess.PIPE, shell=True - ) - (description_encoded, err) = p.communicate(b"") - if p.returncode != 0: - raise errors.BzrCommandError( - "Script %s failed with error code %d" % (script, p.returncode) - ) - new_revision = local_tree.last_revision() - description = description_encoded.decode() - if last_revision == new_revision and commit_pending is None: - # Automatically commit pending changes if the script did not - # touch the branch. - commit_pending = True - if commit_pending: - try: - new_revision = local_tree.commit(description, allow_pointless=False) - except PointlessCommit: - pass - if new_revision == last_revision: - raise ScriptMadeNoChanges() - return description - - def derived_branch_name(script: str) -> str: return os.path.splitext(osutils.basename(script.split(" ")[0]))[0] -def main(argv: List[str]) -> Optional[int]: # noqa: C901 - parser = argparse.ArgumentParser() - parser.add_argument("script", help="Path to script to run.", type=str) - parser.add_argument("url", help="URL of branch to work on.", type=str) - parser.add_argument( - "--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="auto", - type=str, - ) - parser.add_argument( - "--dry-run", - help="Create branches but don't push or propose anything.", - action="store_true", - default=False, - ) - args = parser.parse_args(argv) - +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(args.url) + main_branch = open_branch(url) except (BranchUnavailable, BranchMissing, BranchUnsupported) as e: - logging.exception("%s: %s", args.url, e) + logging.exception("%s: %s", url, e) return 1 - if args.name is None: - name = derived_branch_name(args.script) - else: - name = args.name - commit_pending = {"auto": None, "yes": True, "no": False}[args.commit_pending] - overwrite = False try: hoster = get_hoster(main_branch) except UnsupportedHoster as e: - if args.mode != "push": + if mode != "push": raise # We can't figure out what branch to resume from when there's no hoster # that can tell us. @@ -171,38 +88,46 @@ ) else: (resume_branch, resume_overwrite, existing_proposal) = find_existing_proposed( - main_branch, hoster, name, owner=args.derived_owner + main_branch, hoster, name, owner=derived_owner ) if resume_overwrite is not None: overwrite = resume_overwrite - if args.refresh: + if refresh: resume_branch = None + with Workspace(main_branch, resume_branch=resume_branch) as ws: try: - description = script_runner(ws.local_tree, args.script, commit_pending) + result = script_runner(ws.local_tree, command, commit_pending) except ScriptMadeNoChanges: - logging.exception("Script did not make any changes.") + logging.error("Script did not make any changes.") + return 1 + except ScriptFailed: + logging.error("Script failed to run.") return 1 - def get_description(description_format, existing_proposal): - if description is not None: - return description - if existing_proposal is not None: - return existing_proposal.get_description() - raise ValueError("No description available") + 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 1 enable_tag_pushing(ws.local_tree.branch) try: publish_result = ws.publish_changes( - args.mode, + mode, name, - get_proposal_description=get_description, - dry_run=args.dry_run, + 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=args.label, + labels=labels, overwrite_existing=overwrite, - derived_owner=args.derived_owner, + derived_owner=derived_owner, existing_proposal=existing_proposal, ) except UnsupportedHoster as e: @@ -211,6 +136,9 @@ 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'?", @@ -227,10 +155,145 @@ logging.info("URL: %s", publish_result.proposal.url) logging.info("Description: %s", publish_result.proposal.get_description()) - if args.diff: + if diff: ws.show_diff(sys.stdout.buffer) - return None + +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: + if 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 = 1 + + return retcode if __name__ == "__main__": diff -Nru silver-platter-0.4.1/silver_platter/tests/__init__.py silver-platter-0.4.3/silver_platter/tests/__init__.py --- silver-platter-0.4.1/silver_platter/tests/__init__.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/tests/__init__.py 2021-07-08 08:20:17.000000000 +0000 @@ -20,11 +20,12 @@ def test_suite(): names = [ + "candidates", "debian", - "debian_lintian", "debian_upstream", "proposal", "publish", + "recipe", "run", "utils", "version", diff -Nru silver-platter-0.4.1/silver_platter/tests/test_candidates.py silver-platter-0.4.3/silver_platter/tests/test_candidates.py --- silver-platter-0.4.1/silver_platter/tests/test_candidates.py 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/tests/test_candidates.py 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,34 @@ +#!/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) diff -Nru silver-platter-0.4.1/silver_platter/tests/test_debian.py silver-platter-0.4.3/silver_platter/tests/test_debian.py --- silver-platter-0.4.1/silver_platter/tests/test_debian.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/tests/test_debian.py 2021-07-08 08:20:17.000000000 +0000 @@ -15,10 +15,15 @@ # 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, ) @@ -29,6 +34,7 @@ select_probers, convert_debian_vcs_url, UnsupportedVCSProber, + add_changelog_entry, ) @@ -57,7 +63,390 @@ 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.assertEqual( - "ssh://git@git.kali.org/jelmer/blah.git", + 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", ) diff -Nru silver-platter-0.4.1/silver_platter/tests/test_recipe.py silver-platter-0.4.3/silver_platter/tests/test_recipe.py --- silver-platter-0.4.1/silver_platter/tests/test_recipe.py 1970-01-01 00:00:00.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/tests/test_recipe.py 2021-07-08 08:20:17.000000000 +0000 @@ -0,0 +1,34 @@ +#!/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') diff -Nru silver-platter-0.4.1/silver_platter/tests/test_run.py silver-platter-0.4.3/silver_platter/tests/test_run.py --- silver-platter-0.4.1/silver_platter/tests/test_run.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/tests/test_run.py 2021-07-08 08:20:17.000000000 +0000 @@ -44,18 +44,18 @@ os.chmod("foo.sh", 0o755) def test_simple_with_commit(self): - description = script_runner( + result = script_runner( self.tree, os.path.abspath("foo.sh"), commit_pending=True ) - self.assertEqual(description, "Some message\n") + self.assertEqual(result.description, "Some message\n") def test_simple_with_autocommit(self): - description = script_runner(self.tree, os.path.abspath("foo.sh")) + 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(description, "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: @@ -69,12 +69,12 @@ """ ) os.chmod("foo.sh", 0o755) - description = script_runner(self.tree, os.path.abspath("foo.sh")) + 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(description, "Some message\n") + self.assertEqual(result.description, "Some message\n") def test_simple_without_commit(self): self.assertRaises( diff -Nru silver-platter-0.4.1/silver_platter/utils.py silver-platter-0.4.3/silver_platter/utils.py --- silver-platter-0.4.1/silver_platter/utils.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/utils.py 2021-07-08 08:20:17.000000000 +0000 @@ -22,24 +22,18 @@ from typing import Callable, Tuple, Optional, List from breezy import ( - config as _mod_config, errors, osutils, urlutils, ) -try: - from breezy.bzr import LineEndingError -except ImportError: # brz < 3.1.1 - from breezy.errors import LineEndingError +from breezy.bzr import LineEndingError from breezy.branch import ( Branch, - BranchWriteLockResult, ) from breezy.controldir import ControlDir, Prober from breezy.git.remote import RemoteGitError -from breezy.revision import NULL_REVISION from breezy.transport import Transport, get_transport from breezy.workingtree import WorkingTree @@ -286,74 +280,6 @@ raise e -try: - from breezy.memorybranch import MemoryBranch -except ImportError: # breezy < 3.1.1 - from breezy.lock import _RelockDebugMixin, LogicalLockResult - - class MemoryBranch(Branch, _RelockDebugMixin): # type: ignore - def __init__(self, repository, last_revision_info, tags): - from breezy.tag import DisabledTags, MemoryTags - - self.repository = repository - self._last_revision_info = last_revision_info - self._revision_history_cache = None - if tags is not None: - self.tags = MemoryTags(tags) - else: - self.tags = DisabledTags(self) - self._partial_revision_history_cache = [] - self._last_revision_info_cache = None - self._revision_id_to_revno_cache = None - self._partial_revision_id_to_revno_cache = {} - self._partial_revision_history_cache = [] - self.base = "memory://" + osutils.rand_chars(10) - - def get_config(self): - return _mod_config.Config() - - def lock_read(self): - self.repository.lock_read() - return LogicalLockResult(self.unlock) - - def lock_write(self, token=None): - self.repository.lock_write() - return BranchWriteLockResult(self.unlock, None) - - def unlock(self): - self.repository.unlock() - - def last_revision_info(self): - return self._last_revision_info - - def _gen_revision_history(self): - """Generate the revision history from last revision""" - last_revno, last_revision = self.last_revision_info() - self._extend_partial_history() - return list(reversed(self._partial_revision_history_cache)) - - def get_rev_id(self, revno, history=None): - """Find the revision id of the specified revno.""" - with self.lock_read(): - if revno == 0: - return NULL_REVISION - last_revno, last_revid = self.last_revision_info() - if revno == last_revno: - return last_revid - if last_revno is None: - self._extend_partial_history() - return self._partial_revision_history_cache[ - len(self._partial_revision_history_cache) - revno - ] - else: - if revno <= 0 or revno > last_revno: - raise errors.NoSuchRevision(self, revno) - distance_from_last = last_revno - revno - if len(self._partial_revision_history_cache) <= distance_from_last: - self._extend_partial_history(distance_from_last) - return self._partial_revision_history_cache[distance_from_last] - - def full_branch_url(branch): """Get the full URL for a branch. diff -Nru silver-platter-0.4.1/silver_platter/workspace.py silver-platter-0.4.3/silver_platter/workspace.py --- silver-platter-0.4.1/silver_platter/workspace.py 2021-02-17 22:45:01.000000000 +0000 +++ silver-platter-0.4.3/silver_platter/workspace.py 2021-07-08 08:20:17.000000000 +0000 @@ -55,6 +55,9 @@ ] +logger = logging.getLogger(__name__) + + class Workspace(object): """Workspace for creating changes to a branch. @@ -75,6 +78,7 @@ resume_branch: Optional[Branch] = None, cached_branch: Optional[Branch] = None, additional_colocated_branches: Optional[List[str]] = None, + resume_branch_additional_colocated_branches: Optional[List[str]] = None, dir: Optional[str] = None, path: Optional[str] = None, ) -> None: @@ -83,6 +87,7 @@ 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 @@ -99,22 +104,33 @@ def __repr__(self): return ( "%s(%r, resume_branch=%r, cached_branch=%r, " - "additional_colocated_branches=%r, dir=%r, path=%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 __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( - self.cached_branch or self.resume_branch or self.main_branch, - self.additional_colocated_branches, + sprout_base, + sprout_coloc, dir=self._dir, path=self._path, ) @@ -122,15 +138,26 @@ self.refreshed = False with self.local_tree.branch.lock_write(): 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 ) if self.resume_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: pass - for branch_name in self.additional_colocated_branches: + logger.debug( + "Fetching colocated branches: %r", + self.additional_colocated_branches, + ) + for branch_name in self.resume_branch_additional_colocated_branches or []: try: remote_colo_branch = self.main_branch.controldir.open_branch( name=branch_name @@ -141,12 +168,13 @@ name=branch_name, source=remote_colo_branch, overwrite=True ) if merge_conflicts(self.main_branch, self.local_tree.branch): - logging.info("restarting branch") + logger.info("restarting branch") self.local_tree.update(revision=self.main_branch_revid) self.local_tree.branch.generate_revision_history( self.main_branch_revid ) self.resume_branch = None + self.resume_branch_additional_colocated_branches = None self.refreshed = True self.orig_revid = self.local_tree.last_revision() return self diff -Nru silver-platter-0.4.1/silver_platter.egg-info/entry_points.txt silver-platter-0.4.3/silver_platter.egg-info/entry_points.txt --- silver-platter-0.4.1/silver_platter.egg-info/entry_points.txt 2021-02-17 22:45:13.000000000 +0000 +++ silver-platter-0.4.3/silver_platter.egg-info/entry_points.txt 2021-07-08 08:20:17.000000000 +0000 @@ -2,6 +2,3 @@ debian-svp = silver_platter.debian.__main__:main svp = silver_platter.__main__:main -[silver_platter.debian.changer] -lintian-brush = silver_platter.debian.lintian:LintianBrushChanger - diff -Nru silver-platter-0.4.1/silver_platter.egg-info/PKG-INFO silver-platter-0.4.3/silver_platter.egg-info/PKG-INFO --- silver-platter-0.4.1/silver_platter.egg-info/PKG-INFO 2021-02-17 22:45:13.000000000 +0000 +++ silver-platter-0.4.3/silver_platter.egg-info/PKG-INFO 2021-07-08 08:20:17.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: silver-platter -Version: 0.4.1 +Version: 0.4.3 Summary: Automatic merge proposal creeator Home-page: https://jelmer.uk/code/silver-platter Author: Jelmer Vernooij diff -Nru silver-platter-0.4.1/silver_platter.egg-info/requires.txt silver-platter-0.4.3/silver_platter.egg-info/requires.txt --- silver-platter-0.4.1/silver_platter.egg-info/requires.txt 2021-02-17 22:45:13.000000000 +0000 +++ silver-platter-0.4.3/silver_platter.egg-info/requires.txt 2021-07-08 08:20:17.000000000 +0000 @@ -1,10 +1,11 @@ -breezy>=3.1.0 -dulwich +breezy>=3.2.0 +dulwich>=0.20.23 +jinja2 [debian] debmutate>=0.3 distro-info -lintian-brush>=0.50 +lintian-brush>=0.102 python_debian pyyaml upstream-ontologist diff -Nru silver-platter-0.4.1/silver_platter.egg-info/SOURCES.txt silver-platter-0.4.3/silver_platter.egg-info/SOURCES.txt --- silver-platter-0.4.1/silver_platter.egg-info/SOURCES.txt 2021-02-17 22:45:13.000000000 +0000 +++ silver-platter-0.4.3/silver_platter.egg-info/SOURCES.txt 2021-07-08 08:20:17.000000000 +0000 @@ -9,6 +9,7 @@ README.rst TODO debian-svp +example.yaml releaser.conf setup.cfg setup.py @@ -18,13 +19,29 @@ 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 @@ -36,26 +53,21 @@ silver_platter.egg-info/top_level.txt silver_platter/debian/__init__.py silver_platter/debian/__main__.py -silver_platter/debian/backport.py +silver_platter/debian/apply.py silver_platter/debian/changer.py -silver_platter/debian/cme.py silver_platter/debian/debianize.py -silver_platter/debian/lintian.py -silver_platter/debian/multiarch.py silver_platter/debian/orphan.py -silver_platter/debian/rrr.py silver_platter/debian/run.py -silver_platter/debian/scrub_obsolete.py -silver_platter/debian/tidy.py -silver_platter/debian/uncommitted.py silver_platter/debian/uploader.py silver_platter/debian/upstream.py silver_platter/tests/__init__.py +silver_platter/tests/test_candidates.py silver_platter/tests/test_debian.py silver_platter/tests/test_debian_lintian.py silver_platter/tests/test_debian_upstream.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.py \ No newline at end of file