From 1e126f74d1943ceb9cb36f732cf5acab842898d9 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Sat, 10 Sep 2011 21:28:28 +0200 Subject: [PATCH] sponsor-patch: More refactoring. --- sponsor-patch | 20 +- ubuntutools/sponsor_patch/bugtask.py | 9 +- ubuntutools/sponsor_patch/patch.py | 74 +++- ubuntutools/sponsor_patch/question.py | 30 ++ ubuntutools/sponsor_patch/source_package.py | 364 +++++++++++++++++ ubuntutools/sponsor_patch/sponsor_patch.py | 430 ++------------------ 6 files changed, 498 insertions(+), 429 deletions(-) create mode 100644 ubuntutools/sponsor_patch/question.py create mode 100644 ubuntutools/sponsor_patch/source_package.py diff --git a/sponsor-patch b/sponsor-patch index 77f2d46..4b2a96e 100755 --- a/sponsor-patch +++ b/sponsor-patch @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Copyright (C) 2010, Benjamin Drung +# Copyright (C) 2010-2011, Benjamin Drung # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -26,8 +26,8 @@ from ubuntutools.config import UDTConfig from ubuntutools.builder import get_builder from ubuntutools.sponsor_patch.sponsor_patch import sponsor_patch -def main(): - script_name = os.path.basename(sys.argv[0]) +def parse(script_name): + """Parse the command line parameters.""" usage = ("%s [options] \n" % (script_name) + "One of --upload, --workdir, or --sponsor must be specified.") epilog = "See %s(1) for more info." % (script_name) @@ -90,14 +90,20 @@ def main(): if options.workdir is None: options.workdir = config.get_value("WORKDIR") - builder = get_builder(options.builder) - if not builder: - sys.exit(1) - if options.sponsoring: options.build = True options.upload = "ubuntu" + return (options, bug_number) + +def main(): + script_name = os.path.basename(sys.argv[0]) + (options, bug_number) = parse(script_name) + + builder = get_builder(options.builder) + if not builder: + sys.exit(1) + if not options.upload and not options.workdir: Logger.error("Please specify either a working directory or an upload " "target!") diff --git a/ubuntutools/sponsor_patch/bugtask.py b/ubuntutools/sponsor_patch/bugtask.py index 45e14d5..83f36d8 100644 --- a/ubuntutools/sponsor_patch/bugtask.py +++ b/ubuntutools/sponsor_patch/bugtask.py @@ -1,7 +1,7 @@ # # bugtask.py - Internal helper class for sponsor-patch # -# Copyright (C) 2010, Benjamin Drung +# Copyright (C) 2010-2011, Benjamin Drung # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -54,14 +54,15 @@ class BugTask(object): def download_source(self): source_files = self.get_source().sourceFileUrls() - dsc_file = None + dsc_file = "" for url in source_files: filename = urllib.unquote(os.path.basename(url)) Logger.info("Downloading %s..." % (filename)) urllib.urlretrieve(url, filename) if url.endswith(".dsc"): - dsc_file = filename - return os.path.join(os.getcwd(), dsc_file) + dsc_file = os.path.join(os.getcwd(), filename) + assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) + return dsc_file def get_branch_link(self): return "lp:" + self.project + "/" + self.get_series() + "/" + \ diff --git a/ubuntutools/sponsor_patch/patch.py b/ubuntutools/sponsor_patch/patch.py index 12d3787..c3db20a 100644 --- a/ubuntutools/sponsor_patch/patch.py +++ b/ubuntutools/sponsor_patch/patch.py @@ -1,7 +1,7 @@ # # patch.py - Internal helper class for sponsor-patch # -# Copyright (C) 2010, Benjamin Drung +# Copyright (C) 2010-2011, Benjamin Drung # # Permission to use, copy, modify, and/or distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -16,31 +16,79 @@ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import os +import re + +from devscripts.logger import Logger from ubuntutools import subprocess +from ubuntutools.sponsor_patch.question import ask_for_manual_fixing class Patch(object): - def __init__(self, patch_file): - self.patch_file = patch_file - self.full_path = os.path.realpath(self.patch_file) - assert os.path.isfile(self.full_path), "%s does not exist." % \ - (self.full_path) - cmd = ["diffstat", "-l", "-p0", self.full_path] + """This object represents a patch that can be downloaded from Launchpad.""" + + def __init__(self, patch): + self._patch = patch + self._patch_file = re.sub(" ", "_", patch.title) + if not reduce(lambda r, x: r or self._patch.title.endswith(x), + (".debdiff", ".diff", ".patch"), False): + Logger.info("Patch %s does not have a proper file extension." % \ + (self._patch.title)) + self._patch_file += ".patch" + self._full_path = os.path.realpath(self._patch_file) + self._changed_files = None + + def apply(self, task): + """Applies the patch in the current directory.""" + assert self._changed_files is not None, \ + "You forgot to download the patch." + edit = False + if self.is_debdiff(): + cmd = ["patch", "--merge", "--force", "-p", + str(self.get_strip_level()), "-i", self._full_path] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error("Failed to apply debdiff %s to %s %s.", + self._patch_file, task.package, task.get_version()) + if not edit: + ask_for_manual_fixing() + edit = True + else: + cmd = ["add-patch", self._full_path] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error("Failed to apply diff %s to %s %s.", + self._patch_file, task.package, task.get_version()) + if not edit: + ask_for_manual_fixing() + edit = True + return edit + + def download(self): + """Downloads the patch from Launchpad.""" + Logger.info("Downloading %s." % (self._patch_file)) + patch_f = open(self._patch_file, "w") + patch_f.write(self._patch.data.open().read()) + patch_f.close() + + cmd = ["diffstat", "-l", "-p0", self._full_path] process = subprocess.Popen(cmd, stdout=subprocess.PIPE) changed_files = process.communicate()[0] - self.changed_files = [l for l in changed_files.split("\n") if l != ""] - - def get_name(self): - return self.patch_file + self._changed_files = [l for l in changed_files.split("\n") if l != ""] def get_strip_level(self): + """Returns the stript level for the patch.""" + assert self._changed_files is not None, \ + "You forgot to download the patch." strip_level = None if self.is_debdiff(): - changelog = [f for f in self.changed_files + changelog = [f for f in self._changed_files if f.endswith("debian/changelog")][0] strip_level = len(changelog.split(os.sep)) - 2 return strip_level def is_debdiff(self): - return len([f for f in self.changed_files + """Checks if the patch is a debdiff (= modifies debian/changelog).""" + assert self._changed_files is not None, \ + "You forgot to download the patch." + return len([f for f in self._changed_files if f.endswith("debian/changelog")]) > 0 diff --git a/ubuntutools/sponsor_patch/question.py b/ubuntutools/sponsor_patch/question.py new file mode 100644 index 0000000..a82c815 --- /dev/null +++ b/ubuntutools/sponsor_patch/question.py @@ -0,0 +1,30 @@ +# +# question.py - Internal helper class for sponsor-patch +# +# Copyright (C) 2011, Benjamin Drung +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import sys + +from ubuntutools.question import YesNoQuestion + +def ask_for_manual_fixing(): + answer = YesNoQuestion().ask("Do you want to resolve this issue manually", + "yes") + if answer == "no": + user_abort() + +def user_abort(): + print "User abort." + sys.exit(2) diff --git a/ubuntutools/sponsor_patch/source_package.py b/ubuntutools/sponsor_patch/source_package.py new file mode 100644 index 0000000..0255104 --- /dev/null +++ b/ubuntutools/sponsor_patch/source_package.py @@ -0,0 +1,364 @@ +# +# source_package.py - Internal helper class for sponsor-patch +# +# Copyright (C) 2011, Benjamin Drung +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import os +import re +import sys + +import debian.changelog +import debian.deb822 + +from devscripts.logger import Logger + +from ubuntutools import subprocess +from ubuntutools.harvest import Harvest +from ubuntutools.question import Question + +from ubuntutools.sponsor_patch.question import ask_for_manual_fixing, user_abort + +def _get_series(launchpad): + """Returns a tuple with the development and list of supported series.""" + #pylint: disable=E1101 + ubuntu = launchpad.distributions['ubuntu'] + #pylint: enable=E1101 + devel_series = ubuntu.current_series.name + supported_series = [series.name for series in ubuntu.series + if series.active and series.name != devel_series] + return (devel_series, supported_series) + +def strip_epoch(version): + """Removes the epoch from a Debian version string. + + strip_epoch(1:1.52-1) will return "1.52-1" and strip_epoch(1.1.3-1) will + return "1.1.3-1". + """ + + parts = version.full_version.split(':') + if len(parts) > 1: + del parts[0] + version_without_epoch = ':'.join(parts) + return version_without_epoch + +class SourcePackage(object): + """This class represents a source package.""" + + def __init__(self, package, builder, workdir, branch): + self._package = package + self._builder = builder + self._workdir = workdir + self._branch = branch + self._changelog = None + self._version = None + self._build_log = None + + def ask_and_upload(self, upload): + """Ask the user before uploading the source package. + + Returns true if the source package is uploaded successfully. Returns + false if the user wants to change something. + """ + + # Upload package + if upload: + lintian_filename = self._run_lintian() + print "\nPlease check %s %s carefully:\nfile://%s\nfile://%s" % \ + (self._package, self._version, self._debdiff_filename, + lintian_filename) + if self._build_log: + print "file://%s" % self._build_log + + harvest = Harvest(self._package) + if harvest.data: + print harvest.report() + + if upload == "ubuntu": + target = "the official Ubuntu archive" + else: + target = upload + question = Question(["yes", "edit", "no"]) + answer = question.ask("Do you want to upload the package to %s" % \ + target, "no") + if answer == "edit": + return False + elif answer == "no": + user_abort() + cmd = ["dput", "--force", upload, self._changes_file] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error("Upload of %s to %s failed." % \ + (os.path.basename(self._changes_file), upload)) + sys.exit(1) + + # Push the branch if the package is uploaded to the Ubuntu archive. + if upload == "ubuntu" and self._branch: + cmd = ['debcommit'] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error('Bzr commit failed.') + sys.exit(1) + cmd = ['bzr', 'mark-uploaded'] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error('Bzr tagging failed.') + sys.exit(1) + cmd = ['bzr', 'push', ':parent'] + Logger.command(cmd) + if subprocess.call(cmd) != 0: + Logger.error('Bzr push failed.') + sys.exit(1) + return True + + def build(self, update): + """Tries to build the package. + + Returns true if the package was built successfully. Returns false + if the user wants to change something. + """ + + dist = re.sub("-.*$", "", self._changelog.distributions) + build_name = self._package + "_" + strip_epoch(self._version) + \ + "_" + self._builder.get_architecture() + ".build" + self._build_log = os.path.join(self._buildresult, build_name) + + successful_built = False + while not successful_built: + if update: + ret = self._builder.update(dist) + if ret != 0: + ask_for_manual_fixing() + break + # We want to update the build environment only once, but not + # after every manual fix. + update = False + + # build package + result = self._builder.build(self._dsc_file, dist, + self._buildresult) + if result != 0: + question = Question(["yes", "update", "retry", "no"]) + answer = question.ask("Do you want to resolve this issue " + "manually", "yes") + if answer == "yes": + break + elif answer == "update": + update = True + continue + elif answer == "retry": + continue + else: + user_abort() + successful_built = True + if not successful_built: + # We want to do a manual fix if the build failed. + return False + return True + + @property + def _buildresult(self): + """Returns the directory for the build result.""" + return os.path.join(self._workdir, "buildresult") + + def build_source(self, keyid, upload, previous_version): + """Tries to build the source package. + + Returns true if the source package was built successfully. Returns false + if the user wants to change something. + """ + + if self._branch: + cmd = ['bzr', 'builddeb', '-S', '--', '--no-lintian'] + else: + cmd = ['debuild', '--no-lintian', '-S'] + cmd.append("-v" + previous_version.full_version) + if previous_version.upstream_version == \ + self._changelog.upstream_version and upload == "ubuntu": + # FIXME: Add proper check that catches cases like changed + # compression (.tar.gz -> tar.bz2) and multiple orig source tarballs + cmd.append("-sd") + else: + cmd.append("-sa") + if not keyid is None: + cmd += ["-k" + keyid] + env = os.environ + if upload == 'ubuntu': + env['DEB_VENDOR'] = 'Ubuntu' + Logger.command(cmd) + if subprocess.call(cmd, env=env) != 0: + Logger.error("Failed to build source tarball.") + # TODO: Add a "retry" option + ask_for_manual_fixing() + return False + return True + + @property + def _changes_file(self): + """Returns the file name of the .changes file.""" + return os.path.join(self._workdir, self._package + "_" + + strip_epoch(self._version) + + "_source.changes") + + def check_target(self, upload, launchpad): + """Make sure that the target is correct. + + Returns true if the target is correct. Returns false if the user + wants to change something. + """ + + (devel_series, supported_series) = _get_series(launchpad) + + if upload == "ubuntu": + allowed = [s + "-proposed" for s in supported_series] + \ + [devel_series] + if self._changelog.distributions not in allowed: + Logger.error(("%s is not an allowed series. It needs to be one " + "of %s.") % (self._changelog.distributions, + ", ".join(allowed))) + ask_for_manual_fixing() + return False + elif upload and upload.startswith("ppa/"): + allowed = supported_series + [devel_series] + if self._changelog.distributions not in allowed: + Logger.error(("%s is not an allowed series. It needs to be one " + "of %s.") % (self._changelog.distributions, + ", ".join(allowed))) + ask_for_manual_fixing() + return False + return True + + def check_version(self, previous_version): + """Check if the version of the package is greater than the given one. + + Return true if the version of the package is newer. Returns false + if the user wants to change something. + """ + + if self._version <= previous_version: + Logger.error("The version %s is not greater than the already " + "available %s.", self._version, previous_version) + ask_for_manual_fixing() + return False + return True + + @property + def _debdiff_filename(self): + """Returns the file name of the .debdiff file.""" + debdiff_name = self._package + "_" + strip_epoch(self._version) + \ + ".debdiff" + return os.path.join(self._workdir, debdiff_name) + + @property + def _dsc_file(self): + """Returns the file name of the .dsc file.""" + return os.path.join(self._workdir, self._package + "_" + + strip_epoch(self._version) + ".dsc") + + def generate_debdiff(self, dsc_file): + """Generates a debdiff between the given .dsc file and this source + package.""" + + assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) + assert os.path.isfile(self._dsc_file), "%s does not exist." % \ + (self._dsc_file) + cmd = ["debdiff", dsc_file, self._dsc_file] + if not Logger.verbose: + cmd.insert(1, "-q") + Logger.command(cmd + [">", self._debdiff_filename]) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + debdiff = process.communicate()[0] + + # write debdiff file + debdiff_file = open(self._debdiff_filename, "w") + debdiff_file.writelines(debdiff) + debdiff_file.close() + + def is_fixed(self, bug_number): + """Make sure that the given bug number is closed. + + Returns true if the bug is closed. Returns false if the user wants to + change something. + """ + + assert os.path.isfile(self._changes_file), "%s does not exist." % \ + (self._changes_file) + changes = debian.deb822.Changes(file(self._changes_file)) + fixed_bugs = [] + if "Launchpad-Bugs-Fixed" in changes: + fixed_bugs = changes["Launchpad-Bugs-Fixed"].split(" ") + fixed_bugs = [int(bug) for bug in fixed_bugs] + + if bug_number not in fixed_bugs: + Logger.error("Launchpad bug #%i is not closed by new version." % \ + (bug_number)) + ask_for_manual_fixing() + return False + return True + + def reload_changelog(self): + """Reloads debian/changelog and update version.""" + + # Check the changelog + self._changelog = debian.changelog.Changelog() + try: + self._changelog.parse_changelog(file("debian/changelog"), + max_blocks=1, strict=True) + except debian.changelog.ChangelogParseError, error: + Logger.error("The changelog entry doesn't validate: %s", str(error)) + ask_for_manual_fixing() + return False + + # Get new version of package + try: + self._version = self._changelog.get_version() + except IndexError: + Logger.error("Debian package version could not be determined. " \ + "debian/changelog is probably malformed.") + ask_for_manual_fixing() + return False + + return True + + def _run_lintian(self): + """Runs lintian on either the source or binary changes file. + + Returns the filename of the created lintian output file. + """ + + # Determine whether to use the source or binary build for lintian + if self._build_log: + build_changes = self._package + "_" + strip_epoch(self._version) + \ + "_" + self._builder.get_architecture() + ".changes" + changes_for_lintian = os.path.join(self._buildresult, build_changes) + else: + changes_for_lintian = self._changes_file + + # Check lintian + assert os.path.isfile(changes_for_lintian), "%s does not exist." % \ + (changes_for_lintian) + cmd = ["lintian", "-IE", "--pedantic", "-q", changes_for_lintian] + lintian_filename = os.path.join(self._workdir, + self._package + "_" + + strip_epoch(self._version) + ".lintian") + Logger.command(cmd + [">", lintian_filename]) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE) + report = process.communicate()[0] + + # write lintian report file + lintian_file = open(lintian_filename, "w") + lintian_file.writelines(report) + lintian_file.close() + + return lintian_filename diff --git a/ubuntutools/sponsor_patch/sponsor_patch.py b/ubuntutools/sponsor_patch/sponsor_patch.py index a14e12e..bfdfc49 100644 --- a/ubuntutools/sponsor_patch/sponsor_patch.py +++ b/ubuntutools/sponsor_patch/sponsor_patch.py @@ -17,27 +17,21 @@ import os import pwd -import re import shutil import sys -import debian.changelog -import debian.deb822 import launchpadlib.launchpad from devscripts.logger import Logger from ubuntutools import subprocess -from ubuntutools.harvest import Harvest from ubuntutools.update_maintainer import update_maintainer -from ubuntutools.question import Question, YesNoQuestion, input_number +from ubuntutools.question import input_number from ubuntutools.sponsor_patch.bugtask import BugTask from ubuntutools.sponsor_patch.patch import Patch - -def user_abort(): - print "User abort." - sys.exit(2) +from ubuntutools.sponsor_patch.question import ask_for_manual_fixing +from ubuntutools.sponsor_patch.source_package import SourcePackage def get_source_package_name(bug_task): package = None @@ -67,26 +61,6 @@ process, exit the shell such that it returns an exit code other than zero. Logger.error("Shell exited with exit value %i." % (returncode)) sys.exit(1) -def strip_epoch(version): - """Removes the epoch from a Debian version string. - - strip_epoch(1:1.52-1) will return "1.52-1" and strip_epoch(1.1.3-1) will - return "1.1.3-1". - - """ - - parts = version.full_version.split(':') - if len(parts) > 1: - del parts[0] - version_without_epoch = ':'.join(parts) - return version_without_epoch - -def ask_for_manual_fixing(): - answer = YesNoQuestion().ask("Do you want to resolve this issue manually", - "yes") - if answer == "no": - user_abort() - def ask_for_patch_or_branch(bug, attached_patches, linked_branches): patch = None branch = None @@ -118,7 +92,7 @@ def ask_for_patch_or_branch(bug, attached_patches, linked_branches): if selected <= len(linked_branches): branch = linked_branches[selected - 1].bzr_identity else: - patch = attached_patches[selected - len(linked_branches) - 1] + patch = Patch(attached_patches[selected - len(linked_branches) - 1]) return (patch, branch) def get_patch_or_branch(bug): @@ -136,7 +110,7 @@ def get_patch_or_branch(bug): "attachment as patch.") % bug.id) sys.exit(1) elif len(attached_patches) == 1 and len(linked_branches) == 0: - patch = attached_patches[0] + patch = Patch(attached_patches[0]) elif len(attached_patches) == 0 and len(linked_branches) == 1: branch = linked_branches[0].bzr_identity else: @@ -144,20 +118,6 @@ def get_patch_or_branch(bug): linked_branches) return (patch, branch) -def download_patch(patch): - patch_filename = re.sub(" ", "_", patch.title) - if not reduce(lambda r, x: r or patch.title.endswith(x), - (".debdiff", ".diff", ".patch"), False): - Logger.info("Patch %s does not have a proper file extension." % \ - (patch.title)) - patch_filename += ".patch" - - Logger.info("Downloading %s." % (patch_filename)) - patch_file = open(patch_filename, "w") - patch_file.write(patch.data.open().read()) - patch_file.close() - return Patch(patch_filename) - def download_branch(branch): dir_name = os.path.basename(branch) if os.path.isdir(dir_name): @@ -188,29 +148,6 @@ def extract_source(dsc_file, verbose=False): Logger.error("Extraction of %s failed." % (os.path.basename(dsc_file))) sys.exit(1) -def apply_patch(task, patch): - edit = False - if patch.is_debdiff(): - cmd = ["patch", "--merge", "--force", "-p", - str(patch.get_strip_level()), "-i", patch.full_path] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error("Failed to apply debdiff %s to %s %s." % \ - (patch.get_name(), task.package, task.get_version())) - if not edit: - ask_for_manual_fixing() - edit = True - else: - cmd = ["add-patch", patch.full_path] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error("Failed to apply diff %s to %s %s." % \ - (patch.get_name(), task.package, task.get_version())) - if not edit: - ask_for_manual_fixing() - edit = True - return edit - def get_open_ubuntu_bug_task(launchpad, bug): """Returns an open Ubuntu bug task for a given Launchpad bug. @@ -258,16 +195,6 @@ def _create_and_change_into(workdir): Logger.command(["cd", workdir]) os.chdir(workdir) -def _get_series(launchpad): - """Returns a tuple with the development and list of supported series.""" - #pylint: disable=E1101 - ubuntu = launchpad.distributions['ubuntu'] - #pylint: enable=E1101 - devel_series = ubuntu.current_series.name - supported_series = [series.name for series in ubuntu.series - if series.active and series.name != devel_series] - return (devel_series, supported_series) - def _update_maintainer_field(): """Update the Maintainer field in debian/control.""" Logger.command(["update-maintainer"]) @@ -282,317 +209,28 @@ def _update_timestamp(): if subprocess.call(cmd) != 0: Logger.info("Failed to update timestamp in debian/changelog.") +def _download_and_change_into(task, dsc_file, patch, branch): + """Downloads the patch and branch and changes into the source directory.""" -class SourcePackage(object): - """This class represents a source package.""" + if patch: + patch.download() - def __init__(self, package, builder, workdir, branch): - self._package = package - self._builder = builder - self._workdir = workdir - self._branch = branch - self._changelog = None - self._version = None - self._build_log = None + Logger.info("Ubuntu package: %s" % (task.package)) + if task.is_merge(): + Logger.info("The task is a merge request.") - def ask_and_upload(self, upload): - """Ask the user before uploading the source package. - - Returns true if the source package is uploaded successfully. Returns - false if the user wants to change something. - """ + extract_source(dsc_file, Logger.verbose) - # Upload package - if upload: - lintian_filename = self._run_lintian() - print "\nPlease check %s %s carefully:\nfile://%s\nfile://%s" % \ - (self._package, self._version, self._debdiff_filename, - lintian_filename) - if self._build_log: - print "file://%s" % self._build_log - - harvest = Harvest(self._package) - if harvest.data: - print harvest.report() - - if upload == "ubuntu": - target = "the official Ubuntu archive" - else: - target = upload - question = Question(["yes", "edit", "no"]) - answer = question.ask("Do you want to upload the package to %s" % \ - target, "no") - if answer == "edit": - return False - elif answer == "no": - user_abort() - cmd = ["dput", "--force", upload, self._changes_file] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error("Upload of %s to %s failed." % \ - (os.path.basename(self._changes_file), upload)) - sys.exit(1) - - # Push the branch if the package is uploaded to the Ubuntu archive. - if upload == "ubuntu" and self._branch: - cmd = ['debcommit'] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error('Bzr commit failed.') - sys.exit(1) - cmd = ['bzr', 'mark-uploaded'] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error('Bzr tagging failed.') - sys.exit(1) - cmd = ['bzr', 'push', ':parent'] - Logger.command(cmd) - if subprocess.call(cmd) != 0: - Logger.error('Bzr push failed.') - sys.exit(1) - return True - - def build(self, update): - """Tries to build the package. - - Returns true if the package was built successfully. Returns false - if the user wants to change something. - """ - - dist = re.sub("-.*$", "", self._changelog.distributions) - build_name = self._package + "_" + strip_epoch(self._version) + \ - "_" + self._builder.get_architecture() + ".build" - self._build_log = os.path.join(self._buildresult, build_name) - - successful_built = False - while not successful_built: - if update: - ret = self._builder.update(dist) - if ret != 0: - ask_for_manual_fixing() - break - # We want to update the build environment only once, but not - # after every manual fix. - update = False - - # build package - result = self._builder.build(self._dsc_file, dist, - self._buildresult) - if result != 0: - question = Question(["yes", "update", "retry", "no"]) - answer = question.ask("Do you want to resolve this issue " - "manually", "yes") - if answer == "yes": - break - elif answer == "update": - update = True - continue - elif answer == "retry": - continue - else: - user_abort() - successful_built = True - if not successful_built: - # We want to do a manual fix if the build failed. - return False - return True - - @property - def _buildresult(self): - """Returns the directory for the build result.""" - return os.path.join(self._workdir, "buildresult") - - def build_source(self, keyid, upload, previous_version): - """Tries to build the source package. - - Returns true if the source package was built successfully. Returns false - if the user wants to change something. - """ - - if self._branch: - cmd = ['bzr', 'builddeb', '-S', '--', '--no-lintian'] - else: - cmd = ['debuild', '--no-lintian', '-S'] - cmd.append("-v" + previous_version.full_version) - if previous_version.upstream_version == \ - self._changelog.upstream_version and upload == "ubuntu": - # FIXME: Add proper check that catches cases like changed - # compression (.tar.gz -> tar.bz2) and multiple orig source tarballs - cmd.append("-sd") - else: - cmd.append("-sa") - if not keyid is None: - cmd += ["-k" + keyid] - env = os.environ - if upload == 'ubuntu': - env['DEB_VENDOR'] = 'Ubuntu' - Logger.command(cmd) - if subprocess.call(cmd, env=env) != 0: - Logger.error("Failed to build source tarball.") - # TODO: Add a "retry" option - ask_for_manual_fixing() - return False - return True - - @property - def _changes_file(self): - """Returns the file name of the .changes file.""" - return os.path.join(self._workdir, self._package + "_" + - strip_epoch(self._version) + - "_source.changes") - - def check_target(self, upload, launchpad): - """Make sure that the target is correct. - - Returns true if the target is correct. Returns false if the user - wants to change something. - """ - - (devel_series, supported_series) = _get_series(launchpad) - - if upload == "ubuntu": - allowed = [s + "-proposed" for s in supported_series] + \ - [devel_series] - if self._changelog.distributions not in allowed: - Logger.error(("%s is not an allowed series. It needs to be one " - "of %s.") % (self._changelog.distributions, - ", ".join(allowed))) - ask_for_manual_fixing() - return False - elif upload and upload.startswith("ppa/"): - allowed = supported_series + [devel_series] - if self._changelog.distributions not in allowed: - Logger.error(("%s is not an allowed series. It needs to be one " - "of %s.") % (self._changelog.distributions, - ", ".join(allowed))) - ask_for_manual_fixing() - return False - return True - - def check_version(self, previous_version): - """Check if the version of the package is greater than the given one. - - Return true if the version of the package is newer. Returns false - if the user wants to change something. - """ - - if self._version <= previous_version: - Logger.error("The version %s is not greater than the already " - "available %s.", self._version, previous_version) - ask_for_manual_fixing() - return False - return True - - @property - def _debdiff_filename(self): - """Returns the file name of the .debdiff file.""" - debdiff_name = self._package + "_" + strip_epoch(self._version) + \ - ".debdiff" - return os.path.join(self._workdir, debdiff_name) - - @property - def _dsc_file(self): - """Returns the file name of the .dsc file.""" - return os.path.join(self._workdir, self._package + "_" + - strip_epoch(self._version) + ".dsc") - - def generate_debdiff(self, dsc_file): - """Generates a debdiff between the given .dsc file and this source - package.""" - - assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) - assert os.path.isfile(self._dsc_file), "%s does not exist." % \ - (self._dsc_file) - cmd = ["debdiff", dsc_file, self._dsc_file] - if not Logger.verbose: - cmd.insert(1, "-q") - Logger.command(cmd + [">", self._debdiff_filename]) - process = subprocess.Popen(cmd, stdout=subprocess.PIPE) - debdiff = process.communicate()[0] - - # write debdiff file - debdiff_file = open(self._debdiff_filename, "w") - debdiff_file.writelines(debdiff) - debdiff_file.close() - - def is_fixed(self, bug_number): - """Make sure that the given bug number is closed. - - Returns true if the bug is closed. Returns false if the user wants to - change something. - """ - - assert os.path.isfile(self._changes_file), "%s does not exist." % \ - (self._changes_file) - changes = debian.deb822.Changes(file(self._changes_file)) - fixed_bugs = [] - if "Launchpad-Bugs-Fixed" in changes: - fixed_bugs = changes["Launchpad-Bugs-Fixed"].split(" ") - fixed_bugs = [int(bug) for bug in fixed_bugs] - - if bug_number not in fixed_bugs: - Logger.error("Launchpad bug #%i is not closed by new version." % \ - (bug_number)) - ask_for_manual_fixing() - return False - return True - - def reload_changelog(self): - """Reloads debian/changelog and update version.""" - - # Check the changelog - self._changelog = debian.changelog.Changelog() - try: - self._changelog.parse_changelog(file("debian/changelog"), - max_blocks=1, strict=True) - except debian.changelog.ChangelogParseError, error: - Logger.error("The changelog entry doesn't validate: %s", str(error)) - ask_for_manual_fixing() - return False - - # Get new version of package - try: - self._version = self._changelog.get_version() - except IndexError: - Logger.error("Debian package version could not be determined. " \ - "debian/changelog is probably malformed.") - ask_for_manual_fixing() - return False - - return True - - def _run_lintian(self): - """Runs lintian on either the source or binary changes file. - - Returns the filename of the created lintian output file. - """ - - # Determine whether to use the source or binary build for lintian - if self._build_log: - build_changes = self._package + "_" + strip_epoch(self._version) + \ - "_" + self._builder.get_architecture() + ".changes" - changes_for_lintian = os.path.join(self._buildresult, build_changes) - else: - changes_for_lintian = self._changes_file - - # Check lintian - assert os.path.isfile(changes_for_lintian), "%s does not exist." % \ - (changes_for_lintian) - cmd = ["lintian", "-IE", "--pedantic", "-q", changes_for_lintian] - lintian_filename = os.path.join(self._workdir, - self._package + "_" + - strip_epoch(self._version) + ".lintian") - Logger.command(cmd + [">", lintian_filename]) - process = subprocess.Popen(cmd, stdout=subprocess.PIPE) - report = process.communicate()[0] - - # write lintian report file - lintian_file = open(lintian_filename, "w") - lintian_file.writelines(report) - lintian_file.close() - - return lintian_filename + # change directory + directory = task.package + '-' + task.get_version().upstream_version + Logger.command(["cd", directory]) + os.chdir(directory) + elif branch: + branch_dir = download_branch(task.get_branch_link()) + # change directory + Logger.command(["cd", branch_dir]) + os.chdir(branch_dir) def sponsor_patch(bug_number, build, builder, edit, keyid, lpinstance, update, upload, workdir): @@ -609,30 +247,12 @@ def sponsor_patch(bug_number, build, builder, edit, keyid, lpinstance, update, task = get_open_ubuntu_bug_task(launchpad, bug) dsc_file = task.download_source() - assert os.path.isfile(dsc_file), "%s does not exist." % (dsc_file) + + _download_and_change_into(task, dsc_file, patch, branch) if patch: - patch = download_patch(patch) - - Logger.info("Ubuntu package: %s" % (task.package)) - if task.is_merge(): - Logger.info("The task is a merge request.") - - extract_source(dsc_file, Logger.verbose) - - # change directory - directory = task.package + '-' + task.get_version().upstream_version - Logger.command(["cd", directory]) - os.chdir(directory) - - edit |= apply_patch(task, patch) + edit |= patch.apply(task) elif branch: - branch_dir = download_branch(task.get_branch_link()) - - # change directory - Logger.command(["cd", branch_dir]) - os.chdir(branch_dir) - edit |= merge_branch(branch) source_package = SourcePackage(task.package, builder, workdir, branch)