diff options
-rw-r--r-- | gbp/config.py | 3 | ||||
-rw-r--r-- | gbp/scripts/import_orig.py | 111 | ||||
-rw-r--r-- | tests/24_test_gbp_import_orig.py | 32 | ||||
-rw-r--r-- | tests/component/deb/test_import_orig.py | 91 |
4 files changed, 234 insertions, 3 deletions
diff --git a/gbp/config.py b/gbp/config.py index 3d254ee..8b1d95c 100644 --- a/gbp/config.py +++ b/gbp/config.py @@ -169,6 +169,7 @@ class GbpOptionParser(OptionParser): 'drop': 'False', 'commit': 'False', 'upstream-vcs-tag': '', + 'rollback': 'True', } help = { 'debian-branch': @@ -327,6 +328,8 @@ class GbpOptionParser(OptionParser): "after export. Default is '%(drop)s'"), 'commit': "commit changes after export, Default is '%(commit)s'", + 'rollback': + "Rollback repository changes when encountering an error", } def_config_files = {'/etc/git-buildpackage/gbp.conf': 'system', diff --git a/gbp/scripts/import_orig.py b/gbp/scripts/import_orig.py index 733465c..ba4fc28 100644 --- a/gbp/scripts/import_orig.py +++ b/gbp/scripts/import_orig.py @@ -36,11 +36,28 @@ from gbp.scripts.common.import_orig import (orig_needs_repack, cleanup_tmp_tree, repack_source, is_link_target, download_orig) +class RollbackError(GitRepositoryError): + """ + An error raised if the actual rollback failed + """ + def __init__(self, errors): + self.msg = "Automatic rollback failed" + super(RollbackError, self).__init__(self.msg) + self.errors = errors + + def __str__(self): + return "%s %s" % (self.msg, self.errors) + class ImportOrigDebianGitRepository(DebianGitRepository): """ Like a DebianGitRepository but can also perform rollbacks and knows about some of the inner workings upstream vcs_tag, … """ + def __init__(self, *args, **kwargs): + self.rollbacks = [] + self.rollback_errors = [] + DebianGitRepository.__init__(self, *args, **kwargs) + def vcs_tag_parent(self, vcs_tag_format, version): """If linking to the upstream VCS get the commit id""" if vcs_tag_format: @@ -48,6 +65,84 @@ class ImportOrigDebianGitRepository(DebianGitRepository): else: return None + def rrr(self, refname, action, reftype): + """ + Remember ref for rollback + + @param refname: ref to roll back + @param action: the rollback action (delete, reset, ...) + @param reftype: the reference type (tag, branch, ...) + """ + sha = None + + if action == 'reset': + try: + sha = self.rev_parse(refname) + except GitRepositoryError as err: + gbp.log.warning("Failed to rev-parse %s: %s" % (refname, err)) + elif action == 'delete': + pass + else: + raise GbpError("Unknown action %s for %s %s" % (action, reftype, refname)) + self.rollbacks.append((refname, reftype, action, sha)) + + def rrr_branch(self, branchname, action='reset-or-delete'): + if action == 'reset-or-delete': + if self.has_branch(branchname): + return self.rrr(branchname, 'reset', 'branch') + else: + return self.rrr(branchname, 'delete', 'branch') + else: + return self.rrr(branchname, action, 'branch') + + def rrr_tag(self, tagname, action='delete'): + return self.rrr(tagname, action, 'tag') + + def rollback(self): + """ + Perform a complete rollback + + Try to roll back as much as possible and remember what failed. + """ + for (name, reftype, action, sha) in self.rollbacks: + try: + if action == 'delete': + gbp.log.info('Rolling back %s %s by deleting it' % (reftype, name)) + if reftype == 'tag': + self.delete_tag(name) + elif reftype == 'branch': + gbp.log.info('Rolling back branch %s by deleting it' % name) + self.delete_branch(name) + else: + raise GitRepositoryError("Don't know how to delete %s %s" % (reftype, name)) + elif action == 'reset' and reftype == 'branch': + gbp.log.info('Rolling back branch %s by resetting it to %s' % (name, sha)) + self.update_ref("refs/heads/%s" % name, sha, msg="gbp import-orig: failure rollback of %s" % name) + else: + raise GitRepositoryError("Don't know how to %s %s %s" % (action, reftype, name)) + except GitRepositoryError as e: + self.rollback_errors.append((name, reftype, action, sha, e)) + if self.rollback_errors: + raise RollbackError(self.rollback_errors) + + # Wrapped methods for rollbacks + def create_tag(self, *args, **kwargs): + name = kwargs['name'] + ret = super(ImportOrigDebianGitRepository, self).create_tag(*args, **kwargs) + self.rrr_tag(name) + return ret + + def commit_dir(self, *args, **kwargs): + import_branch = kwargs['branch'] + self.rrr_branch(import_branch) + return super(ImportOrigDebianGitRepository, self).commit_dir(*args, **kwargs) + + def create_branch(self, *args, **kwargs): + branch = kwargs['branch'] + ret = super(ImportOrigDebianGitRepository, self).create_branch(*args, **kwargs) + self.rrr_branch(branch, 'delete') + return ret + def prepare_pristine_tar(archive, pkg, version): """ @@ -300,6 +395,8 @@ def build_parser(name): parser.add_boolean_config_file_option(option_name="interactive", dest='interactive') + parser.add_boolean_config_file_option(option_name="rollback", + dest="rollback") parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False, help="verbose command execution") parser.add_config_file_option(option_name="color", dest="color", type='tristate') @@ -437,6 +534,7 @@ def main(argv): if options.pristine_tar: if pristine_orig: + repo.rrr_branch('pristine-tar') repo.pristine_tar.commit(pristine_orig, import_branch) else: gbp.log.warn("'%s' not an archive, skipping pristine-tar" % source.path) @@ -446,10 +544,12 @@ def main(argv): commit=commit, sign=options.sign_tags, keyid=options.keyid) + if is_empty: repo.create_branch(branch=options.debian_branch, rev=commit) repo.force_head(options.debian_branch, hard=True) elif options.merge: + repo.rrr_branch(options.debian_branch) debian_branch_merge(repo, tag, version, options) # Update working copy and index if we've possibly updated the @@ -465,6 +565,17 @@ def main(argv): if str(err): gbp.log.err(err) ret = 1 + if repo and options.rollback: + gbp.log.err("Error detected, Will roll back changes.") + try: + repo.rollback() + # Make sure the very last line as an error message + gbp.log.err("Rolled back changes after import error.") + except Exception as e: + gbp.log.error("Automatic rollback failed: %s" % e) + gbp.log.error("Clean up manually and please report a bug: %s" % + repo.rollback_errors) + if pristine_orig and linked and not options.symlink_orig: os.unlink(pristine_orig) diff --git a/tests/24_test_gbp_import_orig.py b/tests/24_test_gbp_import_orig.py new file mode 100644 index 0000000..6103178 --- /dev/null +++ b/tests/24_test_gbp_import_orig.py @@ -0,0 +1,32 @@ +# vim: set fileencoding=utf-8 : +"""Test L{gbp.command_wrappers.Command}'s tarball unpack""" + +from gbp.scripts.import_orig import (ImportOrigDebianGitRepository, GbpError) +from . testutils import DebianGitTestRepo + + +class TestImportOrigGitRepository(DebianGitTestRepo): + + def setUp(self): + DebianGitTestRepo.setUp(self, ImportOrigDebianGitRepository) + + def test_empty_rollback(self): + self.repo.rollback() + self.assertEquals(self.repo.rollback_errors, []) + + def test_rrr_delete_tag(self): + self.repo.rrr('doesnotmatter', 'delete', 'tag') + self.assertEquals(self.repo.rollbacks, [('doesnotmatter', 'tag', 'delete', None)]) + self.repo.rollback() + self.assertEquals(self.repo.rollback_errors, []) + + def test_rrr_delete_branch(self): + self.repo.rrr('doesnotmatter', 'delete', 'branch') + self.assertEquals(self.repo.rollbacks, [('doesnotmatter', 'branch', 'delete', None)]) + self.repo.rollback() + self.assertEquals(self.repo.rollback_errors, []) + + def test_rrr_unknown_action(self): + with self.assertRaisesRegexp(GbpError, "Unknown action unknown for tag doesnotmatter"): + self.repo.rrr('doesnotmatter', 'unknown', 'tag') + diff --git a/tests/component/deb/test_import_orig.py b/tests/component/deb/test_import_orig.py index 6ed5da8..f5698ea 100644 --- a/tests/component/deb/test_import_orig.py +++ b/tests/component/deb/test_import_orig.py @@ -18,17 +18,28 @@ import os +from mock import patch, DEFAULT + from tests.component import ComponentTestBase from tests.component.deb import DEB_TEST_DATA_DIR +from gbp.scripts.import_dsc import main as import_dsc from gbp.scripts.import_orig import main as import_orig -from gbp.git.repository import GitRepository +from gbp.git.repository import GitRepository, GitRepositoryError from nose.tools import ok_ +def raise_if_tag_match(match): + def wrapped(*args, **kwargs): + if len(args) > 0 and args[0].startswith(match) or kwargs.get('name', '').startswith(match): + raise GitRepositoryError('this is a create tag error mock for %s %s' % (match, kwargs)) + return DEFAULT + return wrapped + + class TestImportOrig(ComponentTestBase): - """Test importing of new upstream sources""" + """Test importing of new upstream versions""" pkg = "hello-debhelper" def_branches = ['master', 'upstream', 'pristine-tar'] @@ -38,6 +49,11 @@ class TestImportOrig(ComponentTestBase): 'dsc-3.0', '%s_%s.orig.tar.gz' % (self.pkg, version)) + def _dsc(self, version): + return os.path.join(DEB_TEST_DATA_DIR, + 'dsc-3.0', + '%s_%s.dsc' % (self.pkg, version)) + def test_initial_import(self): """Test that importing into an empty repo works""" repo = GitRepository.create(self.pkg) @@ -48,8 +64,20 @@ class TestImportOrig(ComponentTestBase): self._check_repo_state(repo, 'master', self.def_branches, tags=['upstream/2.6']) + def test_update(self): + repo = GitRepository.create(self.pkg) + os.chdir(self.pkg) + + dsc = self._dsc('2.6-2') + ok_(import_dsc(['arg0', '--pristine-tar', dsc]) == 0) + self._check_repo_state(repo, 'master', ['master', 'upstream', 'pristine-tar']) + + self._orig('2.8') + self._check_repo_state(repo, 'master', ['master', 'upstream', 'pristine-tar'], + tags=['debian/2.6-2', 'upstream/2.6']) + def test_tag_exists(self): - """Test that importing an already importet version fails""" + """Test that importing an already imported version fails""" repo = GitRepository.create(self.pkg) os.chdir(self.pkg) orig = self._orig('2.6') @@ -61,3 +89,60 @@ class TestImportOrig(ComponentTestBase): self._check_log(0, "gbp:error: Upstream tag 'upstream/2.6' already exists") # Check that the second import didn't change any refs self.check_refs(repo, heads) + + def test_update_fail_create_upstream_tag(self): + repo = GitRepository.create(self.pkg) + os.chdir(self.pkg) + + dsc = self._dsc('2.6-2') + ok_(import_dsc(['arg0', '--pristine-tar', dsc]) == 0) + self._check_repo_state(repo, 'master', ['master', 'upstream', 'pristine-tar']) + + heads = self.rem_refs(repo, self.def_branches) + + orig = self._orig('2.8') + with patch('gbp.git.repository.GitRepository.create_tag', + side_effect=GitRepositoryError('this is a create tag error mock')): + ok_(import_orig(['arg0', '--no-interactive', '--pristine-tar', orig]) == 1) + self._check_repo_state(repo, 'master', ['master', 'upstream', 'pristine-tar'], + tags=['debian/2.6-2', 'upstream/2.6']) + self.check_refs(repo, heads) + + def test_update_fail_merge(self): + repo = GitRepository.create(self.pkg) + os.chdir(self.pkg) + + dsc = self._dsc('2.6-2') + ok_(import_dsc(['arg0', '--pristine-tar', dsc]) == 0) + self._check_repo_state(repo, 'master', ['master', 'upstream', 'pristine-tar']) + + heads = self.rem_refs(repo, self.def_branches) + + orig = self._orig('2.8') + with patch('gbp.scripts.import_orig.debian_branch_merge', + side_effect=GitRepositoryError('this is a fail merge error mock')): + ok_(import_orig(['arg0', '--no-interactive', '--pristine-tar', orig]) == 1) + self._check_repo_state(repo, 'master', ['master', 'upstream', 'pristine-tar'], + tags=['debian/2.6-2', 'upstream/2.6']) + self.check_refs(repo, heads) + + @patch('gbp.git.repository.GitRepository.create_tag', + side_effect=raise_if_tag_match('upstream/')) + def test_initial_import_fail_create_upstream_tag(self, RepoMock): + repo = GitRepository.create(self.pkg) + os.chdir(self.pkg) + orig = self._orig('2.6') + ok_(import_orig(['arg0', '--no-interactive', orig]) == 1) + + self._check_repo_state(repo, None, [], tags=[]) + + def test_initial_import_fail_create_debian_branch(self): + repo = GitRepository.create(self.pkg) + os.chdir(self.pkg) + orig = self._orig('2.6') + + with patch('gbp.git.repository.GitRepository.create_branch', + side_effect=GitRepositoryError('this is a create branch error mock')): + ok_(import_orig(['arg0', '--no-interactive', '--pristine-tar', orig]) == 1) + + self._check_repo_state(repo, None, [], tags=[]) |