aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGuido Günther <agx@sigxcpu.org>2016-06-28 17:03:33 +0200
committerGuido Günther <agx@sigxcpu.org>2016-06-28 17:27:58 +0200
commitebc6b918ff885fa206910afb81dd5b49b9bb69b0 (patch)
treeda2d7982b3b843bcb3b22660c13de834d2ab6573
parent0f1426faaa6386ef9d3c8cabab6b45c47eb0e063 (diff)
import_orig: Recover from import errors
by winding back branches and tags to their pre-error states Closes: #828838
-rw-r--r--gbp/config.py3
-rw-r--r--gbp/scripts/import_orig.py111
-rw-r--r--tests/24_test_gbp_import_orig.py32
-rw-r--r--tests/component/deb/test_import_orig.py91
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=[])