mirror of
https://git.launchpad.net/ubuntu-dev-tools
synced 2025-03-12 15:41:09 +00:00
287 lines
10 KiB
Python
Executable File
287 lines
10 KiB
Python
Executable File
#!/usr/bin/python
|
|
#
|
|
# Copyright (C) 2011, Stefano Rivera <stefanor@ubuntu.com>
|
|
#
|
|
# 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.
|
|
|
|
from collections import defaultdict
|
|
import optparse
|
|
import re
|
|
import sys
|
|
|
|
import apt
|
|
from devscripts.logger import Logger
|
|
from distro_info import UbuntuDistroInfo
|
|
|
|
from ubuntutools.lp.lpapicache import Launchpad, Distribution
|
|
from ubuntutools.lp.udtexceptions import PackageNotFoundException
|
|
from ubuntutools.config import UDTConfig
|
|
from ubuntutools.rdepends import query_rdepends, RDependsException
|
|
from ubuntutools.question import (YesNoQuestion, EditBugReport,
|
|
confirmation_prompt)
|
|
|
|
|
|
class DestinationException(Exception):
|
|
pass
|
|
|
|
|
|
def determine_destinations(source, destination):
|
|
ubuntu_info = UbuntuDistroInfo()
|
|
if destination is None:
|
|
destination = ubuntu_info.stable()
|
|
|
|
if source not in ubuntu_info.all:
|
|
raise DestinationException("Source release %s does not exist" % source)
|
|
if destination not in ubuntu_info.all:
|
|
raise DestinationException("Destination release %s does not exist"
|
|
% destination)
|
|
if destination not in ubuntu_info.supported():
|
|
raise DestinationException("Destination release %s is not supported"
|
|
% destination)
|
|
|
|
found = False
|
|
destinations = []
|
|
support_gap = False
|
|
for release in ubuntu_info.all:
|
|
if release == destination:
|
|
found = True
|
|
if release == source:
|
|
break
|
|
if found:
|
|
if support_gap:
|
|
if ubuntu_info.is_lts(release):
|
|
support_gap = False
|
|
else:
|
|
continue
|
|
if release not in ubuntu_info.supported():
|
|
support_gap = True
|
|
continue
|
|
destinations.append(release)
|
|
|
|
assert found
|
|
assert len(destinations) > 0
|
|
|
|
return destinations
|
|
|
|
|
|
def check_existing(package, destinations):
|
|
"""Search for possible existing bug reports"""
|
|
# The LP bug search is indexed, not substring:
|
|
query = re.findall(r'[a-z]+', package)
|
|
bugs = []
|
|
for release in destinations:
|
|
project = Launchpad.projects[release + '-backports']
|
|
bugs += project.searchTasks(omit_duplicates=True,
|
|
search_text=query,
|
|
status=["Incomplete", "New", "Confirmed",
|
|
"Triaged", "In Progress",
|
|
"Fix Committed"])
|
|
if not bugs:
|
|
return
|
|
|
|
Logger.normal("There are existing bug reports that look similar to your "
|
|
"request. Please check before continuing:")
|
|
|
|
for bug in sorted(set(bug_task.bug for bug_task in bugs)):
|
|
Logger.normal(" * LP: #%-7i: %s %s", bug.id, bug.title, bug.web_link)
|
|
|
|
confirmation_prompt()
|
|
|
|
|
|
def find_rdepends(releases, published_binaries):
|
|
intermediate = defaultdict(lambda: defaultdict(list))
|
|
|
|
# We want to display every pubilshed binary, even if it has no rdepends
|
|
for binpkg in published_binaries:
|
|
intermediate[binpkg]
|
|
|
|
for arch in ('any', 'source'):
|
|
for release in releases:
|
|
for binpkg in published_binaries:
|
|
try:
|
|
raw_rdeps = query_rdepends(binpkg, release, arch)
|
|
except RDependsException:
|
|
# Not published? TODO: Check
|
|
continue
|
|
for relationship, rdeps in raw_rdeps.iteritems():
|
|
for rdep in rdeps:
|
|
if rdep['Package'] in published_binaries:
|
|
continue
|
|
intermediate[binpkg][rdep['Package']] \
|
|
.append((release, relationship))
|
|
|
|
output = []
|
|
for binpkg, rdeps in intermediate.iteritems():
|
|
output += ['', binpkg, '-' * len(binpkg)]
|
|
for pkg, appearences in rdeps.iteritems():
|
|
output += ['* %s' % pkg]
|
|
for release, relationship in appearences:
|
|
output += [' [ ] %s (%s)' % (release, relationship)]
|
|
|
|
found_any = sum(len(rdeps) for rdeps in intermediate.itervalues())
|
|
if found_any:
|
|
output = [
|
|
"Reverse dependencies:",
|
|
"=====================",
|
|
"The following reverse-dependencies need to be tested against the "
|
|
"new version of %(package)s. "
|
|
"For reverse-build-dependencies (-Indep), please test that the "
|
|
"package still builds against the new %(package)s. "
|
|
"For reverse-dependencies, please test that the version of the "
|
|
"package currently in the release still works with the new "
|
|
"%(package)s installed. "
|
|
"Reverse- Recommends, Suggests, and Enhances don't need to be "
|
|
"tested, and are listed for completeness-sake."
|
|
] + output
|
|
else:
|
|
output = ["No reverse dependencies"]
|
|
|
|
return output
|
|
|
|
|
|
def locate_package(package, distribution):
|
|
archive = Distribution('ubuntu').getArchive()
|
|
for pass_ in ('source', 'binary'):
|
|
try:
|
|
package_spph = archive.getSourcePackage(package, distribution)
|
|
return package_spph
|
|
except PackageNotFoundException, e:
|
|
if pass_ == 'binary':
|
|
Logger.error(str(e))
|
|
sys.exit(1)
|
|
|
|
try:
|
|
apt_pkg = apt.Cache()[package]
|
|
except KeyError:
|
|
continue
|
|
package = apt_pkg.candidate.source_name
|
|
Logger.normal("Binary package specified, considering its source "
|
|
"package instead: %s", package)
|
|
|
|
|
|
def request_backport(package_spph, source, destinations):
|
|
|
|
published_binaries = set()
|
|
for bpph in package_spph.getBinaries():
|
|
published_binaries.add(bpph.getPackageName())
|
|
|
|
testing = []
|
|
testing += ["You can test-build the backport in your PPA with "
|
|
"backportpackage:"]
|
|
testing += ["$ backportpackage -u ppa:<lp username>/<ppa name> "
|
|
"-s %s -d %s %s"
|
|
% (source, dest, package_spph.getPackageName())
|
|
for dest in destinations]
|
|
testing += [""]
|
|
for dest in destinations:
|
|
testing += ['* %s:' % dest]
|
|
testing += ["[ ] Package builds without modification"]
|
|
testing += ["[ ] %s installs cleanly and runs" % binary
|
|
for binary in published_binaries]
|
|
|
|
subst = {
|
|
'package': package_spph.getPackageName(),
|
|
'version': package_spph.getVersion(),
|
|
'component': package_spph.getComponent(),
|
|
'source': package_spph.getSeriesAndPocket(),
|
|
'destinations': ', '.join(destinations),
|
|
}
|
|
subject = ("Please backport %(package)s %(version)s (%(component)s) "
|
|
"from %(source)s" % subst)
|
|
body = ('\n'.join(
|
|
[
|
|
"Please backport %(package)s %(version)s (%(component)s) "
|
|
"from %(source)s to %(destinations)s.",
|
|
"",
|
|
"Reason for the backport:",
|
|
"========================",
|
|
">>> Enter your reasoning here <<<",
|
|
"",
|
|
"Testing:",
|
|
"========",
|
|
"Mark off items in the checklist [X] as you test them, "
|
|
"but please leave the checklist so that backporters can quickly "
|
|
"evaluate the state of testing.",
|
|
""
|
|
]
|
|
+ testing
|
|
+ [""]
|
|
+ find_rdepends(destinations, published_binaries)
|
|
+ [""]
|
|
) % subst)
|
|
|
|
editor = EditBugReport(subject, body)
|
|
editor.edit()
|
|
subject, body = editor.get_report()
|
|
|
|
Logger.normal('The final report is:\nSummary: %s\nDescription:\n%s\n',
|
|
subject, body)
|
|
if YesNoQuestion().ask("Request this backport", "yes") == "no":
|
|
sys.exit(1)
|
|
|
|
targets = [Launchpad.projects['%s-backports' % destination]
|
|
for destination in destinations]
|
|
bug = Launchpad.bugs.createBug(title=subject, description=body,
|
|
target=targets[0])
|
|
for target in targets[1:]:
|
|
bug.addTask(target=target)
|
|
|
|
Logger.normal("Backport request filed as %s", bug.web_link)
|
|
|
|
|
|
def main():
|
|
parser = optparse.OptionParser('%prog [options] package')
|
|
parser.add_option('-d', '--destination', metavar='DEST',
|
|
help='Backport to DEST release and necessary '
|
|
'intermediate releases '
|
|
'(default: current stable release)')
|
|
parser.add_option('-s', '--source', metavar='SOURCE',
|
|
help='Backport from SOURCE release '
|
|
'(default: current devel release)')
|
|
parser.add_option('-l', '--lpinstance', metavar='INSTANCE', default=None,
|
|
help='Launchpad instance to connect to '
|
|
'(default: production).')
|
|
parser.add_option('--no-conf', action='store_true',
|
|
dest='no_conf', default=False,
|
|
help="Don't read config files or environment variables")
|
|
options, args = parser.parse_args()
|
|
|
|
if len(args) != 1:
|
|
parser.error("One (and only one) package must be specified")
|
|
package = args[0]
|
|
|
|
config = UDTConfig(options.no_conf)
|
|
|
|
if options.lpinstance is None:
|
|
options.lpinstance = config.get_value('LPINSTANCE')
|
|
Launchpad.login(options.lpinstance)
|
|
|
|
if options.source is None:
|
|
options.source = Distribution('ubuntu').getDevelopmentSeries().name
|
|
|
|
try:
|
|
destinations = determine_destinations(options.source,
|
|
options.destination)
|
|
except DestinationException, e:
|
|
Logger.error(str(e))
|
|
sys.exit(1)
|
|
|
|
check_existing(package, destinations)
|
|
|
|
package_spph = locate_package(package, options.source)
|
|
request_backport(package_spph, options.source, destinations)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|