From c9a1595a666cc83f9e9e523f0b4f27502f61b998 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Guido=20G=C3=BCnther?= Date: Fri, 25 Nov 2011 15:51:52 +0100 Subject: [PATCH] Add ChangeLog class making it easier to query versions. Add tests. --- gbp/deb/__init__.py | 85 +------------------- gbp/deb/changelog.py | 124 ++++++++++++++++++++++++++++++ gbp/scripts/buildpackage.py | 29 ++++--- gbp/scripts/create_remote_repo.py | 6 +- gbp/scripts/dch.py | 11 +-- gbp/scripts/import_dsc.py | 5 +- gbp/scripts/import_orig.py | 16 ++-- tests/test_Changelog.py | 158 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 319 insertions(+), 115 deletions(-) create mode 100644 gbp/deb/changelog.py create mode 100644 tests/test_Changelog.py diff --git a/gbp/deb/__init__.py b/gbp/deb/__init__.py index 881d1ae..dc3ba8b 100644 --- a/gbp/deb/__init__.py +++ b/gbp/deb/__init__.py @@ -27,6 +27,7 @@ import glob import gbp.command_wrappers as gbpc from gbp.errors import GbpError from gbp.git import GitRepositoryError +from gbp.deb.changelog import ChangeLog, NoChangeLogError # When trying to parse a version-number from a dsc or changes file, these are # the valid characters. @@ -64,15 +65,6 @@ compressor_opts = { 'gzip' : [ '-n', 'gz' ], compressor_aliases = { 'bz2' : 'bzip2', 'gz' : 'gzip', } -class NoChangelogError(Exception): - """no changelog found""" - pass - -class ParseChangeLogError(Exception): - """problem parsing changelog""" - pass - - class DpkgCompareVersions(gbpc.Command): cmd='/usr/bin/dpkg' @@ -390,58 +382,10 @@ def parse_changelog_repo(repo, branch, filename): # repository errors. sha = repo.rev_parse("%s:%s" % (branch, filename)) except GitRepositoryError: - raise NoChangelogError, "Changelog %s not found in branch %s" % (filename, branch) + raise NoChangeLogError, "Changelog %s not found in branch %s" % (filename, branch) lines = repo.show(sha) - return parse_changelog('\n'.join(lines)) - -def parse_changelog(contents=None, filename=None): - """ - Parse the content of a changelog file. Either contents, containing - the contents of a changelog file, or filename, pointing to a - changelog file must be passed. - - Returns: - - cp['Version']: full version string including epoch - cp['Upstream-Version']: upstream version, if not debian native - cp['Debian-Version']: debian release - cp['Epoch']: epoch, if any - cp['NoEpoch-Version']: full version string excluding epoch - """ - # Check that either contents or filename is passed (but not both) - if (not filename and not contents) or (filename and contents): - raise Exception("Either filename or contents must be passed to parse_changelog") - - # If a filename was passed, check if it exists - if filename and not os.access(filename, os.F_OK): - raise NoChangelogError, "Changelog %s not found" % (filename, ) - - # If no filename was passed, let parse_changelog read from stdin - if not filename: - filename = '-' - - # Note that if contents is None, stdin will just be closed right - # away by communicate. - cmd = subprocess.Popen(['dpkg-parsechangelog', '-l%s' % filename], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - (output, errors) = cmd.communicate(contents) - if cmd.returncode: - raise ParseChangeLogError, "Failed to parse changelog. dpkg-parsechangelog said:\n%s" % (errors, ) - # Parse the result of dpkg-parsechangelog (which looks like - # email headers) - cp = email.message_from_string(output) - try: - if ':' in cp['Version']: - cp['Epoch'], cp['NoEpoch-Version'] = cp['Version'].split(':', 1) - else: - cp['NoEpoch-Version'] = cp['Version'] - if '-' in cp['NoEpoch-Version']: - cp['Upstream-Version'], cp['Debian-Version'] = cp['NoEpoch-Version'].rsplit('-', 1) - else: - cp['Debian-Version'] = cp['NoEpoch-Version'] - except TypeError: - raise ParseChangeLogError, output.split('\n')[0] - return cp + return ChangeLog('\n'.join(lines)) def orig_file(cp, compression): @@ -456,18 +400,6 @@ def orig_file(cp, compression): ext = compressor_opts[compression][1] return "%s_%s.orig.tar.%s" % (cp['Source'], cp['Upstream-Version'], ext) - -def is_native(cp): - """ - Is this a debian native package - - >>> is_native(dict(Version="1")) - True - >>> is_native(dict(Version="1-1")) - False - """ - return not '-' in cp['Version'] - def is_valid_packagename(name): "Is this a valid Debian package name?" return packagename_re.match(name) @@ -497,17 +429,6 @@ def get_compression(orig_file): return None -def has_epoch(cp): - """ - Does the topmost version number in the changelog contain an epoch - >>> has_epoch(dict(Epoch="1")) - True - >>> has_epoch(dict()) - False - """ - return cp.has_key("Epoch") - - def has_orig(cp, compression, dir): "Check if orig.tar.gz exists in dir" try: diff --git a/gbp/deb/changelog.py b/gbp/deb/changelog.py new file mode 100644 index 0000000..bba8fa9 --- /dev/null +++ b/gbp/deb/changelog.py @@ -0,0 +1,124 @@ +# vim: set fileencoding=utf-8 : +# +# (C) 2011 Guido Günther +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +"""A Debian Changelog""" + +import email +import os +import subprocess + +class NoChangeLogError(Exception): + """No changelog found""" + pass + +class ParseChangeLogError(Exception): + """Problem parsing changelog""" + pass + +class ChangeLog(object): + """A Debian changelog""" + + def __init__(self, contents=None, filename=None): + """ + Parse an existing changelog, Either contents, containing the contents + of a changelog file, or filename, pointing to a changelog file must be + passed. + """ + # Check that either contents or filename is passed (but not both) + if (not filename and not contents) or (filename and contents): + raise Exception("Either filename or contents must be passed") + + # If a filename was passed, check if it exists + if filename and not os.access(filename, os.F_OK): + raise NoChangeLogError, "Changelog %s not found" % (filename, ) + + # If no filename was passed, let's read from stdin + if not filename: + filename = '-' + + # Note that if contents is None, stdin will just be closed right + # away by communicate. + cmd = subprocess.Popen(['dpkg-parsechangelog', '-l%s' % filename], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (output, errors) = cmd.communicate(contents) + if cmd.returncode: + raise ParseChangeLogError("Failed to parse changelog. " + "dpkg-parsechangelog said:\n%s" % (errors, )) + # Parse the result of dpkg-parsechangelog (which looks like + # email headers) + cp = email.message_from_string(output) + try: + if ':' in cp['Version']: + cp['Epoch'], cp['NoEpoch-Version'] = cp['Version'].split(':', 1) + else: + cp['NoEpoch-Version'] = cp['Version'] + if '-' in cp['NoEpoch-Version']: + cp['Upstream-Version'], cp['Debian-Version'] = cp['NoEpoch-Version'].rsplit('-', 1) + else: + cp['Debian-Version'] = cp['NoEpoch-Version'] + except TypeError: + raise ParseChangeLogError, output.split('\n')[0] + + self._cp = cp + + def __getitem__(self, item): + return self._cp[item] + + def __setitem__(self, item, value): + self._cp[item] = value + + + @property + def version(self): + """The full version string""" + return self._cp['Version'] + + @property + def upstream_version(self): + """The upstream version""" + return self._cp['Upstream-Version'] + + @property + def debian_version(self): + """The Debian part of the version number""" + return self._cp['Debian-Version'] + + @property + def epoch(self): + """The package's epoch""" + return self._cp['Epoch'] + + @property + def noepoch(self): + """The version string without the epoch""" + return self._cp['NoEpoch-Version'] + + def has_epoch(self): + """ + Whether the version has an epoch + + @return: C{True} if the version has an epoch, C{False} otherwise + @rtype: C{bool} + """ + return self._cp.has_key('Epoch') + + def is_native(self): + """ + Whether this is a native Debian package + """ + return not '-' in self.version diff --git a/gbp/scripts/buildpackage.py b/gbp/scripts/buildpackage.py index 9e19937..91347fe 100644 --- a/gbp/scripts/buildpackage.py +++ b/gbp/scripts/buildpackage.py @@ -31,6 +31,7 @@ from gbp.command_wrappers import (Command, RunAtCommand, CommandExecFailed, PristineTar, RemoveTree, CatenateTarArchive) from gbp.config import (GbpOptionParser, GbpOptionGroup) +from gbp.deb.changelog import ChangeLog, NoChangeLogError, ParseChangeLogError from gbp.errors import GbpError from glob import glob import gbp.log @@ -175,7 +176,7 @@ def export_source(repo, cp, options, dest_dir, tarball_dir): # Extract orig tarball if git-overlay option is selected: if options.overlay: - if du.is_native(cp): + if cp.is_native(): raise GbpError, "Cannot overlay Debian native package" extract_orig(os.path.join(tarball_dir, du.orig_file(cp, options.comp_type)), dest_dir) @@ -504,16 +505,14 @@ def main(argv): raise GbpError, "Use --git-ignore-branch to ignore or --git-debian-branch to set the branch name." try: - cp = du.parse_changelog(filename=changelog) - version = cp['Version'] - version_no_epoch = cp['NoEpoch-Version'] - if du.is_native(cp): + cp = ChangeLog(filename=changelog) + if cp.is_native(): major = cp['Debian-Version'] else: major = cp['Upstream-Version'] - except du.NoChangelogError: + except NoChangeLogError: raise GbpError, "'%s' does not exist, not a debian package" % changelog - except du.ParseChangeLogError, err: + except ParseChangeLogError, err: raise GbpError, "Error parsing Changelog: %s" % err except KeyError: raise GbpError, "Can't parse version from changelog" @@ -527,7 +526,7 @@ def main(argv): # sources and create different tarballs (#640382) # We don't delay it in general since we want to fail early if the # tarball is missing. - if not du.is_native(cp): + if not cp.is_native(): if options.postexport: gbp.log.info("Postexport hook set, delaying tarball creation") else: @@ -545,14 +544,14 @@ def main(argv): extra_env={'GBP_GIT_DIR': repo.git_dir, 'GBP_TMP_DIR': tmp_dir})(dir=tmp_dir) - cp = du.parse_changelog(filename=os.path.join(tmp_dir, 'debian', 'changelog')) + cp = ChangeLog(filename=os.path.join(tmp_dir, 'debian', 'changelog')) export_dir = os.path.join(output_dir, "%s-%s" % (cp['Source'], major)) gbp.log.info("Moving '%s' to '%s'" % (tmp_dir, export_dir)) move_old_export(export_dir) os.rename(tmp_dir, export_dir) # Delayed tarball creation in case a postexport hook is used: - if not du.is_native(cp) and options.postexport: + if not cp.is_native() and options.postexport: prepare_upstream_tarball(repo, cp, options, tarball_dir, output_dir) @@ -573,20 +572,20 @@ def main(argv): if options.postbuild: arch = os.getenv('ARCH', None) or du.get_arch() changes = os.path.abspath("%s/../%s_%s_%s.changes" % - (build_dir, cp['Source'], version_no_epoch, arch)) + (build_dir, cp['Source'], cp.noepoch, arch)) gbp.log.debug("Looking for changes file %s" % changes) if not os.path.exists(changes): changes = os.path.abspath("%s/../%s_%s_source.changes" % - (build_dir, cp['Source'], version_no_epoch)) + (build_dir, cp['Source'], cp.noepoch)) Command(options.postbuild, shell=True, extra_env={'GBP_CHANGES_FILE': changes, 'GBP_BUILD_DIR': build_dir})() if options.tag or options.tag_only: - gbp.log.info("Tagging %s" % version) - tag = build_tag(options.debian_tag, version) + gbp.log.info("Tagging %s" % cp.version) + tag = build_tag(options.debian_tag, cp.version) if options.retag and repo.has_tag(tag): repo.delete_tag(tag) - repo.create_tag(name=tag, msg="Debian release %s" % version, + repo.create_tag(name=tag, msg="Debian release %s" % cp.version, sign=options.sign_tags, keyid=options.keyid) if options.posttag: sha = repo.rev_parse("%s^{}" % tag) diff --git a/gbp/scripts/create_remote_repo.py b/gbp/scripts/create_remote_repo.py index 00f3410..0915825 100644 --- a/gbp/scripts/create_remote_repo.py +++ b/gbp/scripts/create_remote_repo.py @@ -26,7 +26,7 @@ import urlparse import subprocess import tty, termios import re -import gbp.deb as du +from gbp.deb.changelog import ChangeLog, NoChangeLogError from gbp.command_wrappers import (CommandExecFailed, PristineTar, GitCommand) from gbp.config import (GbpOptionParser, GbpOptionGroup) from gbp.errors import GbpError @@ -181,9 +181,9 @@ def main(argv): branches += [ PristineTar.branch ] try: - cp = du.parse_changelog(filename=changelog) + cp = ChangeLog(filename=changelog) pkg = cp['Source'] - except gbp.deb.NoChangelogError: + except NoChangeLogError: pkg = None if not pkg: diff --git a/gbp/scripts/dch.py b/gbp/scripts/dch.py index a82c1bd..b502e21 100644 --- a/gbp/scripts/dch.py +++ b/gbp/scripts/dch.py @@ -29,7 +29,8 @@ import gbp.log from gbp.git import (GitRepositoryError, GitRepository, build_tag, tag_to_version) from gbp.config import GbpOptionParser, GbpOptionGroup from gbp.errors import GbpError -from gbp.deb import parse_changelog, NoChangelogError, is_native, compare_versions +from gbp.deb import compare_versions +from gbp.deb.changelog import ChangeLog, NoChangeLogError user_customizations = {} snapshot_re = re.compile("\s*\*\* SNAPSHOT build @(?P[a-z0-9]+)\s+\*\*") @@ -105,7 +106,7 @@ def add_changelog_section(msg, distribution, repo, options, cp, "add a new changelog section" # If no version(change) was specified guess the new version based on the # latest upstream version on the upstream branch - if not version and not is_native(cp): + if not version and not cp.is_native(): pattern = options.upstream_tag.replace('%(version)s', '*') try: tag = repo.find_tag('HEAD', pattern=pattern) @@ -212,7 +213,7 @@ def do_snapshot(changelog, repo, next_snapshot): """ commit = repo.head - cp = parse_changelog(filename=changelog) + cp = ChangeLog(filename=changelog) (release, snapshot) = snapshot_version(cp['Version']) snapshot = int(eval(next_snapshot)) @@ -387,7 +388,7 @@ def main(argv): gbp.log.err("You are not on branch '%s' but on '%s'" % (options.debian_branch, branch)) raise GbpError, "Use --ignore-branch to ignore or --debian-branch to set the branch name." - cp = parse_changelog(filename=changelog) + cp = ChangeLog(filename=changelog) if options.since: since = options.since @@ -488,7 +489,7 @@ def main(argv): if editor_cmd: gbpc.Command(editor_cmd, ["debian/changelog"])() - except (GbpError, GitRepositoryError, NoChangelogError), err: + except (GbpError, GitRepositoryError, NoChangeLogError), err: if len(err.__str__()): gbp.log.err(err) ret = 1 diff --git a/gbp/scripts/import_dsc.py b/gbp/scripts/import_dsc.py index 94013e2..10721c1 100644 --- a/gbp/scripts/import_dsc.py +++ b/gbp/scripts/import_dsc.py @@ -26,8 +26,9 @@ import pipes import time from email.Utils import parseaddr import gbp.command_wrappers as gbpc -from gbp.deb import (debian_version_chars, parse_changelog, +from gbp.deb import (debian_version_chars, parse_dsc, DscFile, UpstreamSource) +from gbp.deb.changelog import ChangeLog from gbp.git import (build_tag, GitRepository, GitRepositoryError, rfc822_date_to_git) from gbp.config import GbpOptionParser, GbpOptionGroup, no_upstream_branch_msg @@ -95,7 +96,7 @@ def apply_debian_patch(repo, unpack_dir, src, options, parents): os.chmod('debian/rules', 0755) os.chdir(repo.path) - dch = parse_changelog(filename=os.path.join(unpack_dir, 'debian/changelog')) + dch = ChangeLog(filename=os.path.join(unpack_dir, 'debian/changelog')) date= rfc822_date_to_git(dch['Date']) author, email = parseaddr(dch['Maintainer']) if not (author and email): diff --git a/gbp/scripts/import_orig.py b/gbp/scripts/import_orig.py index 445d1a6..f6c8aa5 100644 --- a/gbp/scripts/import_orig.py +++ b/gbp/scripts/import_orig.py @@ -25,12 +25,12 @@ import re import subprocess import tempfile import gbp.command_wrappers as gbpc -from gbp.deb import (parse_changelog, UpstreamSource, - NoChangelogError, has_epoch, +from gbp.deb import (UpstreamSource, do_uscan, parse_changelog_repo, is_valid_packagename, packagename_msg, is_valid_upstreamversion, upstreamversion_msg) +from gbp.deb.changelog import ChangeLog, NoChangeLogError from gbp.git import (GitRepositoryError, GitRepository, build_tag) from gbp.config import GbpOptionParser, GbpOptionGroup, no_upstream_branch_msg from gbp.errors import (GbpError, GbpNothingImported) @@ -118,16 +118,16 @@ def detect_name_and_version(repo, source, options): # Try to find the source package name try: - cp = parse_changelog(filename='debian/changelog') + cp = ChangeLog(filename='debian/changelog') sourcepackage = cp['Source'] - except NoChangelogError: + except NoChangeLogError: try: # Check the changelog file from the repository, in case # we're not on the debian-branch (but upstream, for # example). cp = parse_changelog_repo(repo, options.debian_branch, 'debian/changelog') sourcepackage = cp['Source'] - except NoChangelogError: + except NoChangeLogError: if options.interactive: sourcepackage = ask_package_name(guessed_package) else: @@ -422,9 +422,9 @@ def main(argv): # No need to check the changelog file from the # repository, since we're certain that we're on # the debian-branch - cp = parse_changelog(filename='debian/changelog') - if has_epoch(cp): - epoch = '%s:' % cp['Epoch'] + cp = ChangeLog(filename='debian/changelog') + if cp.has_epoch(): + epoch = '%s:' % cp.epoch info = { 'version': "%s%s-1" % (epoch, version) } env = { 'GBP_BRANCH': options.debian_branch } gbpc.Command(options.postimport % info, extra_env=env, shell=True)() diff --git a/tests/test_Changelog.py b/tests/test_Changelog.py new file mode 100644 index 0000000..c4c7c0c --- /dev/null +++ b/tests/test_Changelog.py @@ -0,0 +1,158 @@ +# vim: set fileencoding=utf-8 : + +""" +Test L{gbp.deb.changelog.ChangeLog} +""" + +cl_debian = """git-buildpackage (0.5.32) unstable; urgency=low + + * [efe9220] Use known_compressions in guess_upstream_version too + (Closes: #645477) + * [e984baf] git-import-orig: fix --filter + + -- Guido Günther Mon, 17 Oct 2011 10:15:22 +0200 + +git-buildpackage (0.5.31) unstable; urgency=low + + [ Guido Günther ] + * [3588d88] Fix pristine-tar error message + * [8da98da] gbp-pq: don't fail on missing series file but create an empty + branch instead + + [ Salvatore Bonaccorso ] + * [b33cf74] Fix URL to cl2vcs service. + Refer to https://honk.sigxcpu.org/cl2vcs instead of + https://honk.sigxcpu.org/cl2vcs for the cl2vcs service. (Closes: #640141) + + -- Guido Günther Wed, 28 Sep 2011 20:21:34 +0200 +""" + +cl_upstream="""python-dateutil (1.0-1) unstable; urgency=low + + * Initial release (Closes: #386256) + + -- Guido Günther Wed, 6 Sep 2006 10:33:06 +0200 +""" + +cl_epoch="""xserver-xorg-video-nv (1:1.2.0-3) unstable; urgency=low + + [ Steve Langasek ] + * Upload to unstable + + -- David Nusinow Mon, 18 Sep 2006 19:57:45 -0400 +""" + + +def test_parse_debian_only(): + """ + Parse a the changelog of debian only package + + Methods tested: + - L{gbp.deb.changelog.ChangeLog.__init__} + - L{gbp.deb.changelog.ChangeLog.is_native} + + Properties tested: + - L{gbp.deb.changelog.ChangeLog.version} + - L{gbp.deb.changelog.ChangeLog.debian_version} + - L{gbp.deb.changelog.ChangeLog.upstream_version} + - L{gbp.deb.changelog.ChangeLog.epoch} + - L{gbp.deb.changelog.ChangeLog.noepoch} + + >>> import gbp.deb.changelog + >>> cl = gbp.deb.changelog.ChangeLog(cl_debian) + >>> cl.version + '0.5.32' + >>> cl.version == cl['Version'] + True + >>> cl.debian_version + '0.5.32' + >>> cl.debian_version == cl['Debian-Version'] + True + >>> cl.noepoch + '0.5.32' + >>> cl.noepoch == cl['NoEpoch-Version'] + True + >>> cl.epoch + >>> cl.upstream_version + >>> cl.is_native() + True + """ + +def test_parse_no_eopch(): + """ + Parse a the changelog of a package without eopch + + Methods tested: + - L{gbp.deb.changelog.ChangeLog.__init__} + - L{gbp.deb.changelog.ChangeLog.has_epoch} + - L{gbp.deb.changelog.ChangeLog.is_native} + + Properties tested: + - L{gbp.deb.changelog.ChangeLog.version} + - L{gbp.deb.changelog.ChangeLog.debian_version} + - L{gbp.deb.changelog.ChangeLog.upstream_version} + - L{gbp.deb.changelog.ChangeLog.epoch} + - L{gbp.deb.changelog.ChangeLog.noepoch} + + >>> import gbp.deb.changelog + >>> cl = gbp.deb.changelog.ChangeLog(cl_upstream) + >>> cl.version + '1.0-1' + >>> cl.version == cl['Version'] + True + >>> cl.debian_version + '1' + >>> cl.debian_version == cl['Debian-Version'] + True + >>> cl.noepoch + '1.0-1' + >>> cl.noepoch == cl['NoEpoch-Version'] + True + >>> cl.epoch + >>> cl.upstream_version + '1.0' + >>> cl.has_epoch() + False + >>> cl.is_native() + False + """ + +def test_parse_eopch(): + """ + Parse a the changelog of a package without eopch + + Methods tested: + - L{gbp.deb.changelog.ChangeLog.__init__} + - L{gbp.deb.changelog.ChangeLog.has_epoch} + - L{gbp.deb.changelog.ChangeLog.is_native} + + Properties tested: + - L{gbp.deb.changelog.ChangeLog.version} + - L{gbp.deb.changelog.ChangeLog.debian_version} + - L{gbp.deb.changelog.ChangeLog.upstream_version} + - L{gbp.deb.changelog.ChangeLog.epoch} + - L{gbp.deb.changelog.ChangeLog.noepoch} + + >>> import gbp.deb.changelog + >>> cl = gbp.deb.changelog.ChangeLog(cl_epoch) + >>> cl.version + '1:1.2.0-3' + >>> cl.version == cl['Version'] + True + >>> cl.debian_version + '3' + >>> cl.debian_version == cl['Debian-Version'] + True + >>> cl.noepoch + '1.2.0-3' + >>> cl.noepoch == cl['NoEpoch-Version'] + True + >>> cl.epoch + '1' + >>> cl.upstream_version + '1.2.0' + >>> cl.has_epoch() + True + >>> cl.is_native() + False + """ -- 2.7.4