Hello, I rebased my patch[1] against latest master to use the new “get_dch_default_urgency()” function in my tests.
Regards. The following changes since commit d93c89f081a3d8d66a46cbefee26b0bf11822c7f: import_orig: test error paths of find_source (2014-01-07 18:12:26 +0100) are available in the git repository at: git://git.baby-gnu.net/git-buildpackage.git tags/dad/d93c89f/create-nonexistent-changelog for you to fetch changes up to 91825fb7cfe7f864ab1368abc7a22c0ef070863f: Create debian/changelog if it does not exists (2014-01-08 19:00:42 +0100) ---------------------------------------------------------------- Rebase on latest master: tests OK This new version use the new “get_dch_default_urgency()” introduced by e876beb. ---------------------------------------------------------------- Daniel Dehennin (1): Create debian/changelog if it does not exists debian/rules | 16 ++- gbp/deb/changelog.py | 60 +++++--- gbp/deb/source.py | 14 +- gbp/scripts/dch.py | 45 +++++- tests/18_test_create_changelog.py | 288 ++++++++++++++++++++++++++++++++++++++ tests/test_Changelog.py | 103 ++++++++++++++ 6 files changed, 489 insertions(+), 37 deletions(-) create mode 100644 tests/18_test_create_changelog.py diff --git a/debian/rules b/debian/rules index a80bd52..3b2ef71 100755 --- a/debian/rules +++ b/debian/rules @@ -13,19 +13,25 @@ ZSH_COMPDIR = /usr/share/zsh/vendor-completions/ PYCHECKER_ARGS=-boptparse --no-override --no-shadowbuiltin +TESTS_NAME="Gbp Tests" +TESTS_EMAIL=te...@example.com + %: dh $@ --with python2 override_dh_auto_test: ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) dh_auto_test - export GIT_AUTHOR_NAME="Gbp Tests"; \ - export GIT_AUTHOR_EMAIL=te...@example.com; \ - export GIT_COMMITTER_NAME=$$GIT_AUTHOR_NAME; \ - export GIT_COMMITTER_EMAIL=$$GIT_AUTHOR_EMAIL; \ + export GIT_AUTHOR_NAME=$(TESTS_NAME); \ + export GIT_AUTHOR_EMAIL=$(TESTS_EMAIL); \ + export GIT_COMMITTER_NAME=$(TESTS_NAME); \ + export GIT_COMMITTER_EMAIL=$(TESTS_EMAIL); \ + export DEBFULLNAME=$(TESTS_NAME); \ + export DEBEMAIL=$(TESTS_EMAIL); \ + export GBP_CONF_FILES='nonexistent'; \ PYTHONPATH=. \ python setup.py nosetests - + PYTHONPATH=. pychecker $(PYCHECKER_ARGS) -q \ gbp gbp.scripts gbp.git gbp.deb else diff --git a/gbp/deb/changelog.py b/gbp/deb/changelog.py index 356f74d..8e8d4cc 100644 --- a/gbp/deb/changelog.py +++ b/gbp/deb/changelog.py @@ -20,6 +20,7 @@ import email import os import subprocess from gbp.command_wrappers import Command +from gbp.deb.control import Control class NoChangeLogError(Exception): """No changelog found""" @@ -74,29 +75,33 @@ class ChangeLog(object): self._cp = None self._filename = filename - # 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 filename and not os.access(filename, os.F_OK): - raise NoChangeLogError("Changelog %s not found" % (filename, )) + if not contents and filename and os.access(filename, os.F_OK) \ + and os.stat(filename).st_size == 0: + raise NoChangeLogError("Either %s must be inexistant or not empty" % filename) if contents: self._contents = contents[:] - else: + elif filename and os.access(filename, os.F_OK): self._read() + self._parse() def _parse(self): """Parse a changelog based on the already read contents.""" - cmd = subprocess.Popen(['dpkg-parsechangelog', '-l-'], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - (output, errors) = cmd.communicate(self._contents) - if cmd.returncode: - raise ParseChangeLogError("Failed to parse changelog. " - "dpkg-parsechangelog said:\n%s" % (errors, )) + + # email.message_from_string accept empty strings + output = '' + + if self._contents: + cmd = subprocess.Popen(['dpkg-parsechangelog', '-l-'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + (output, errors) = cmd.communicate(self._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) @@ -110,7 +115,8 @@ class ChangeLog(object): else: cp['Debian-Version'] = cp['NoEpoch-Version'] except TypeError: - raise ParseChangeLogError(output.split('\n')[0]) + if self._contents: + raise ParseChangeLogError(output.split('\n')[0]) self._cp = cp @@ -210,6 +216,15 @@ class ChangeLog(object): """ return list(self.sections_iter) + def is_empty(self): + """ + Whether the changelog is empty + + @return: C{True} if the changelog contents is empty, C{False} otherwise + @rtype: C{bool} + """ + return self._contents == '' + @staticmethod def spawn_dch(msg=[], author=None, email=None, newversion=False, version=None, release=False, distribution=None, dch_options=[]): @@ -251,6 +266,9 @@ class ChangeLog(object): if distribution: args.append("--distribution=%s" % distribution) + if '--create' in dch_options: + env['EDITOR'] = '/bin/true' + args.extend(dch_options) args.append('--') if msg: @@ -302,5 +320,13 @@ class ChangeLog(object): @param dch_options: options passed verbatim to dch @type dch_options: C{list} """ + options = dch_options[:] + if self.is_empty(): + control = Control() + # Option --create must be paste only once + options.extend(['--create', '--package=%s' % control.name]) + if version.has_key('version'): + self['version'] = version['version'] + self.spawn_dch(msg=msg, newversion=True, version=version, author=author, - email=email, distribution=distribution, dch_options=dch_options) + email=email, distribution=distribution, dch_options=options) diff --git a/gbp/deb/source.py b/gbp/deb/source.py index c740361..37ef5e5 100644 --- a/gbp/deb/source.py +++ b/gbp/deb/source.py @@ -68,10 +68,10 @@ class DebianSource(object): except IOError as e: pass # Fall back to changelog parsing - try: - return not '-' in self.changelog.version - except IOError as e: - raise DebianSourceError("Failed to determine source format: %s" % e) + if self.changelog.version is None: + raise DebianSourceError("Failed to determine source format: no version found") + + return not '-' in self.changelog.version @property def changelog(self): @@ -79,11 +79,7 @@ class DebianSource(object): Return the L{gbp.deb.ChangeLog} """ if not self._changelog: - try: - clf = self._vfs.open('debian/changelog') - self._changelog = ChangeLog(clf.read()) - except IOError as err: - raise DebianSourceError('Failed to read changelog: %s' % err) + self._changelog = ChangeLog(filename='debian/changelog') return self._changelog @property diff --git a/gbp/scripts/dch.py b/gbp/scripts/dch.py index a848d6d..444fb3c 100644 --- a/gbp/scripts/dch.py +++ b/gbp/scripts/dch.py @@ -44,7 +44,7 @@ def guess_version_from_upstream(repo, upstream_tag_format, cp): version = repo.debian_version_from_upstream(upstream_tag_format, epoch=cp.epoch) gbp.log.debug("Found upstream version %s." % version) - if compare_versions(version, cp.version) > 0: + if cp.is_empty() or compare_versions(version, cp.version) > 0: return version except GitRepositoryError: gbp.log.debug("No upstream tag found") @@ -225,6 +225,28 @@ def guess_documented_commit(cp, repo, tagformat): # Changelog not touched yet return None +def guess_first_debian_commit(repo, options): + # Changelog is now empty, but it may have been removed + last = repo.get_commits(paths="debian/changelog", num=1) + if last: + since = last[0] + gbp.log.info("Changelog was deleted at '%s'" % since) + else: + upstream = None + try: + pattern = options.upstream_tag % dict(version='*') + upstream = repo.find_tag('HEAD', pattern=pattern) + gbp.log.debug('Found upstream tag %s' % upstream) + except GitRepositoryError: + gbp.log.debug('No upstream tag found, try upstream branch') + + if upstream == None or options.upstream_tree == 'branch': + upstream = options.upstream_branch + + since = repo.get_merge_base('HEAD', upstream) + gbp.log.debug("Found merge-base '%s'" % since) + + return since def has_snapshot_banner(cp): """Whether the changelog has a snapshot banner""" @@ -314,6 +336,8 @@ def main(argv): parser.add_option_group(custom_group) parser.add_boolean_config_file_option(option_name = "ignore-branch", dest="ignore_branch") + naming_group.add_config_file_option(option_name="upstream-tree", dest="upstream_tree") + naming_group.add_config_file_option(option_name="upstream-branch", dest="upstream_branch") naming_group.add_config_file_option(option_name="debian-branch", dest="debian_branch") naming_group.add_config_file_option(option_name="upstream-tag", dest="upstream_tag") naming_group.add_config_file_option(option_name="debian-tag", dest="debian_tag") @@ -403,16 +427,23 @@ def main(argv): since = options.since else: since = '' - if options.auto: + if options.auto and not cp.is_empty(): since = guess_documented_commit(cp, repo, options.debian_tag) if since: msg = "Continuing from commit '%s'" % since else: - msg = "Starting from first commit" + msg = "Couldn't find snapshot header, using version info" gbp.log.info(msg) found_snapshot_banner = has_snapshot_banner(cp) - else: # Fallback: continue from last tag - since = repo.find_version(options.debian_tag, cp['Version']) + if not since: + if cp.is_empty(): + # We may have commits for debian stuffs + since = guess_first_debian_commit(repo, options) + if since and options.snapshot: + # User request a snapshot for first section + found_snapshot_header = True + else: # Fallback: continue from last tag + since = repo.find_version(options.debian_tag, cp['Version']) if not since: raise GbpError("Version %s not found" % cp['Version']) @@ -445,10 +476,12 @@ def main(argv): elif options.snapshot and not found_snapshot_banner: # the user want to switch to snapshot mode add_section = True + elif cp.is_empty(): + add_section = True else: add_section = False - if add_section and not version_change and not source.is_native(): + if add_section and not version_change and (cp.is_empty() or not source.is_native()): # Get version from upstream if none provided v = guess_version_from_upstream(repo, options.upstream_tag, cp) if v: diff --git a/tests/18_test_create_changelog.py b/tests/18_test_create_changelog.py new file mode 100644 index 0000000..5f4f1d0 --- /dev/null +++ b/tests/18_test_create_changelog.py @@ -0,0 +1,288 @@ +# vim: set fileencoding=utf-8 : + +"""Test L{Changelog}'s create""" + +from . import context + +import unittest + +from testutils import DebianGitTestRepo +from testutils import OsReleaseFile +from testutils import get_dch_default_urgency + +from gbp.scripts import dch +from gbp.deb.changelog import ChangeLog + +import os +import re + +snap_header_regex = r'\s{2}\*{2}\sSNAPSHOT\sbuild\s@([0-9a-f]{6})[0-9a-f]{34}\s\*{2}' + +# Default changelog field +default_package = 'test-package' +default_version = '1.0-1' +default_dist = 'UNRELEASED' +# Urgency compatibility with devscripts < 2.13.5 +default_urgency = get_dch_default_urgency() +author_name = 'Gbp Tests' +author_email = 'te...@example.com' + +@unittest.skipIf(not os.path.exists('/usr/bin/dch'), "Dch not found") +class TestScriptDch(DebianGitTestRepo): + """Test git-dch""" + + + @classmethod + def setUpClass(cls): + """Set up environment for all tests. + """ + # git-dch sys.argv + cls.argv = ["git-dch", # Program name + "--debian-branch=debian", + "--no-multimaint", + "--spawn-editor=/bin/true"] + # Ubuntu compliant + os_release = OsReleaseFile('/etc/lsb-release') + if os_release['DISTRIB_ID'] == 'Ubuntu': + cls.os_codename = os_release['DISTRIB_ID'] + cls.os_version_suffix = 'ubuntu1' + else: + cls.os_codename = 'unstable' + cls.os_version_suffix = None + + + def setUp(self): + """Set up environment per test. + """ + DebianGitTestRepo.setUp(self) + # Come back out of context + self.top = os.path.abspath(os.path.curdir) + # reset snapshot mode + self.snapshot = None + # Prepare some upstream + self.add_file("foo", "bar") + self.repo.create_tag("upstream/1.0", msg="upstream version 1.0") + # Prepare debian branch + self.repo.create_branch("debian") + self.repo.set_branch("debian") + os.mkdir(os.path.join(self.repo.path, "debian")) + # We need at least one file + self.add_file("debian/rules", """#!/usr/bin/make -f\n""") + context.chdir(self.repo.path) + + + def tearDown(self): + os.chdir(self.top) + DebianGitTestRepo.tearDown(self) + + + def run_dch(self, dch_options=None): + # Take care to copy the list or multiple calls to run_dch will + # be cumulated in self.argv + argv = self.argv[:] + if dch_options is not None: + argv.extend(dch_options) + ret = dch.main(argv) + self.assertEqual(ret, 0) + self.cl = ChangeLog(filename="debian/changelog") + + + def create_control(self): + """Add a minimalistic debian/control file""" + self.add_file("debian/control", """Source: test-package\nSection: test\n""") + + + def create_remove_fake_changelog(self): + """Add and remove a fake debian/changelog """ + self.add_file("debian/changelog", "fake") + self.repo.remove_files("debian/changelog") + self.repo.commit_files(self.repo.path, "removed debian/changelog") + + + def check_header(self, + name=default_package, + version=default_version, + dist=default_dist, + urgency=default_urgency): + """Check if a changelog header match requested fields""" + if self.os_version_suffix is not None: + version += self.os_version_suffix + if self.snapshot is not None: + snap_header = self.cl['Changes'].split('\n')[2] + match = re.search(snap_header_regex, snap_header) + short_commit_id = match.group(1) + self.assertIsNotNone(short_commit_id) + version += '~{0}.gbp{1}'.format(self.snapshot, short_commit_id) + self.assertEqual(self.cl.name, name) + self.assertEqual(self.cl.version, version) + self.assertEqual(self.cl['Distribution'], dist) + self.assertEqual(self.cl['Urgency'], urgency) + + + def check_author(self, name=author_name, email=author_email): + """Check if a changelog match requested author fields""" + self.assertEqual(self.cl.author, name) + self.assertEqual(self.cl.email, email) + + + def check_changes(self, changes, strict=True): + """Check if a changelog changes match.""" + if self.snapshot is not None: + offset = 4 + else: + offset = 2 + cl_changes = self.cl['Changes'].split('\n')[offset:] + if strict: + self.assertEqual(len(changes), len(cl_changes)) + for index, change in enumerate(changes): + change_line = " * {0}".format(change) + self.assertEqual(change_line, cl_changes[index]) + + + def test_create_from_dch_main(self): + """Test dch.py like git-dch script does: must not include upstream log in debian/changelog""" + self.create_control() + self.run_dch() + self.check_header() + self.check_author() + self.check_changes(["added debian/rules", "added debian/control"]) + + + def test_create_from_dch_main_with_empty_file(self): + """Test dch.py like git-dch script does: must fail if empty debian/changelog present""" + self.create_control() + with file(os.path.join(self.repo.path, "debian/changelog"), "w+") as f: + f.write('') + ret = dch.main(self.argv[:]) + self.assertNotEqual(ret, 0) + self.assertEqual(0, os.stat("debian/changelog").st_size) + + + def test_create_from_dch_main_with_auto(self): + """Test dch.py like git-dch script does: guess last commit""" + self.create_control() + options = ["--auto"] + self.run_dch(options) + self.check_header() + self.check_author() + self.check_changes(["added debian/rules", "added debian/control"]) + + + def test_create_from_dch_main_with_release(self): + """Test dch.py like git-dch script does: release mode""" + self.create_control() + options = ["--release"] + self.run_dch(options) + self.check_header(dist=self.os_codename) + self.check_author() + self.check_changes(["added debian/rules", "added debian/control"]) + + + def test_create_from_dch_main_with_snapshot(self): + """Test dch.py like git-dch script does: snashot mode""" + self.create_control() + options = ["--snapshot"] + self.snapshot = '1' + self.run_dch(options) + self.check_header() + self.check_author() + self.check_changes(["added debian/rules", "added debian/control"]) + + + def test_create_from_dch_main_after_delete(self): + """Test dch.py like git-dch script does: debian/changelog was deleted""" + self.create_remove_fake_changelog() + self.create_control() + self.run_dch() + self.check_header() + self.check_author() + self.check_changes(["added debian/control"]) + + + def test_create_from_dch_main_after_delete_without_commits(self): + """Test dch.py like git-dch script does: debian/changelog was deleted""" + self.create_control() + self.create_remove_fake_changelog() + self.run_dch() + from os import environ + self.check_header() + self.check_author() + self.check_changes(["UNRELEASED"]) + + + def test_create_from_dch_main_with_auto_release(self): + """Test dch.py like git-dch script does: guess last commit, release mode""" + self.create_control() + options = ["--auto", "--release"] + self.run_dch(options) + self.check_header(dist=self.os_codename) + self.check_author() + self.check_changes(["added debian/rules", "added debian/control"]) + + + def test_create_from_dch_main_with_auto_snapshot(self): + """Test dch.py like git-dch script does: guess last commit, snashot""" + self.create_control() + options = ["--auto", "--snapshot"] + self.snapshot = '1' + self.run_dch(options) + self.check_header() + self.check_author() + self.check_changes(["added debian/rules", "added debian/control"]) + + + def test_create_from_dch_main_with_auto_after_delete(self): + """Test dch.py like git-dch script does: guess last commit, debian/changelog was deleted""" + self.create_remove_fake_changelog() + self.create_control() + options = ["--auto"] + self.run_dch(options) + self.check_header() + self.check_author() + self.check_changes(["added debian/control"]) + + + def test_create_from_dch_main_with_release_after_delete(self): + """Test dch.py like git-dch script does: release mode, debian/changelog was deleted""" + self.create_remove_fake_changelog() + self.create_control() + options = ["--release"] + self.run_dch(options) + self.check_header(dist=self.os_codename) + self.check_author() + self.check_changes(["added debian/control"]) + + + def test_create_from_dch_main_with_snapshot_after_delete(self): + """Test dch.py like git-dch script does: snapshot, debian/changelog was deleted""" + self.create_remove_fake_changelog() + self.create_control() + options = ["--snapshot"] + self.snapshot = '1' + self.run_dch(options) + self.check_header() + self.check_author() + self.check_changes(["added debian/control"]) + + + def test_create_from_dch_main_with_auto_release_after_delete(self): + """Test dch.py like git-dch script does: guess last commit, release mode, debian/changelog was deleted""" + self.create_remove_fake_changelog() + self.create_control() + options = ["--auto", "--release"] + self.run_dch(options) + self.check_header(dist=self.os_codename) + self.check_author() + self.check_changes(["added debian/control"]) + + + def test_create_from_dch_main_with_auto_snapshot_after_delete(self): + """Test dch.py like git-dch script does: guess last commit, snapshot, debian/changelog was deleted""" + self.create_remove_fake_changelog() + self.create_control() + options = ["--auto", "--snapshot"] + self.snapshot = '1' + self.run_dch(options) + self.check_header() + self.check_author() + self.check_changes(["added debian/control"]) diff --git a/tests/test_Changelog.py b/tests/test_Changelog.py index 30f25e5..afa82ea 100644 --- a/tests/test_Changelog.py +++ b/tests/test_Changelog.py @@ -307,3 +307,106 @@ def test_add_entry(): True >>> shutil.rmtree(testdir, ignore_errors=True) """ + +def test_parse_empty_changelog(): + """ + Test if we can parse an empty changelog + + - L{gbp.deb.changelog.ChangeLog.__init__} + - L{gbp.deb.changelog.ChangeLog._parse} + + >>> import gbp.deb.changelog + >>> cl = gbp.deb.changelog.ChangeLog() + >>> cl.is_empty() + True + >>> cl.name == None + True + >>> cl.version == None + True + """ + +def test_add_section_to_empty_package(): + """ + Test if we can add a section to an empty package + Only the debian directory is present. + + Methods tested: + - L{gbp.deb.changelog.ChangeLog.__init__} + - L{gbp.deb.changelog.ChangeLog._parse} + - L{gbp.deb.changelog.ChangeLog.add_section} + - L{gbp.deb.changelog.ChangeLog.spawn_dch} + + >>> import os + >>> import tempfile + >>> import shutil + >>> import gbp.deb.changelog + >>> olddir = os.path.abspath(os.path.curdir) + >>> testdir = tempfile.mkdtemp(prefix='gbp-test-changelog-') + >>> testdebdir = os.path.join(testdir, 'debian') + >>> testclname = os.path.join(testdebdir, "changelog") + >>> os.mkdir(testdebdir) + >>> os.chdir(testdir) + >>> os.path.abspath(os.path.curdir) == testdir + True + >>> cl = gbp.deb.changelog.ChangeLog(filename=testclname) + >>> cl.add_section(msg=["Test add section"], distribution=None, version={'version': '1.0'}, author="Debian Maintainer", email="ma...@debian.org") + Traceback (most recent call last): + ... + NoControlError: Control file debian/control does not exist + >>> cl = gbp.deb.changelog.ChangeLog(filename=testclname) + >>> cl.version == None + True + >>> cl.debian_version == None + True + >>> cl['Distribution'] == None + True + >>> cl['Changes'] == None + True + >>> os.chdir(olddir) + >>> os.path.abspath(os.path.curdir) == olddir + True + >>> shutil.rmtree(testdir, ignore_errors=True) + """ + +def test_add_section_to_empty_changelog(): + """ + Test if we can add a section to an empty changelog + + Methods tested: + - L{gbp.deb.changelog.ChangeLog.__init__} + - L{gbp.deb.changelog.ChangeLog._parse} + - L{gbp.deb.changelog.ChangeLog.add_section} + - L{gbp.deb.changelog.ChangeLog.spawn_dch} + + >>> import os + >>> import tempfile + >>> import shutil + >>> import gbp.deb.changelog + >>> olddir = os.path.abspath(os.path.curdir) + >>> testdir = tempfile.mkdtemp(prefix='gbp-test-changelog-') + >>> testdebdir = os.path.join(testdir, 'debian') + >>> testclname = os.path.join(testdebdir, "changelog") + >>> testcontrolname = os.path.join(testdebdir, "control") + >>> os.mkdir(testdebdir) + >>> cth = open(testcontrolname, "w") + >>> cth.write("Source: test-pkg") + >>> cth.close() + >>> os.chdir(testdir) + >>> os.path.abspath(os.path.curdir) == testdir + True + >>> cl = gbp.deb.changelog.ChangeLog(filename=testclname) + >>> cl.add_section(msg=["Test add section"], distribution=None, version={'version': '1.0'}, author="Debian Maintainer", email="ma...@debian.org") + >>> cl = gbp.deb.changelog.ChangeLog(filename=testclname) + >>> cl.version + '1.0' + >>> cl.debian_version + '1.0' + >>> cl['Distribution'] + 'UNRELEASED' + >>> 'Test add section' in cl['Changes'] + True + >>> os.chdir(olddir) + >>> os.path.abspath(os.path.curdir) == olddir + True + >>> shutil.rmtree(testdir, ignore_errors=True) + """ Footnotes: [1] http://git.baby-gnu.net/gitweb/gitweb.cgi?p=git-buildpackage.git;a=tag;h=refs/tags/dad/d93c89f/create-nonexistent-changelog -- Daniel Dehennin Récupérer ma clef GPG: gpg --keyserver pgp.mit.edu --recv-keys 0x7A6FE2DF
pgpDXRi3lp8Nd.pgp
Description: PGP signature