summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--morphlib/branchmanager.py94
-rw-r--r--morphlib/branchmanager_tests.py99
2 files changed, 179 insertions, 14 deletions
diff --git a/morphlib/branchmanager.py b/morphlib/branchmanager.py
index 87a75ddc..a33b4ccb 100644
--- a/morphlib/branchmanager.py
+++ b/morphlib/branchmanager.py
@@ -34,23 +34,58 @@ class RefCleanupError(cliapp.AppException):
class LocalRefManager(object):
'''Provide atomic update over a set of refs in a set of repositories.
- Any ref changes made with the update, add and delete methods will
- be reversed after the end of the with statement the LocalRefManager
- is used in, if an exception is raised in the aforesaid with statement.
+ When used in a with statement, if an exception is raised in the
+ body, then any ref changes are reverted, so deletes get replaced,
+ new branches get deleted and ref changes are changed back to the
+ value before the LocalRefManager was created.
+
+ By default, changes are kept after the with statement ends. This can
+ be overridden to revert after the manager exits by passing True to
+ the construcor.
+
+ with LocalRefManager(True) as lrm:
+ # Update refs with lrm.update, lrm.add or lrm.delete
+ # Use changed refs
+ # refs are back to their previous value
+
+ There is also an explicit .close() method to clean up after the
+ context has exited like so:
+
+ with LocalRefManager() as lrm:
+ # update refs
+ # Do something with altered refs
+ lrm.close() # Explicitly clean up
+
+ The name .close() was chosen for the cleanup method, so the
+ LocalRefManager object may also be used again in a second with
+ statement using contextlib.closing().
+
+ with LocalRefManager() as lrm:
+ # update refs
+ with contextlib.closing(lrm) as lrm:
+ # Do something with pushed refs and clean up if there is an
+ # exception
+
+ This is also useful if the LocalRefManager is nested in another
+ object, since the .close() method can be called in that object's
+ cleanup method.
'''
- def __init__(self):
- self._cleanup = None
+ def __init__(self, cleanup_on_success=False):
+ self._cleanup_on_success = cleanup_on_success
+ self._cleanup = collections.deque()
def __enter__(self):
- self._cleanup = collections.deque()
return self
def __exit__(self, etype, evalue, estack):
# No exception was raised, so no cleanup is required
- if (etype, evalue, estack) == (None, None, None):
+ if not self._cleanup_on_success and evalue is None:
return
+ self.close(evalue)
+
+ def close(self, primary=None):
exceptions = []
d = self._cleanup
while d:
@@ -60,7 +95,7 @@ class LocalRefManager(object):
except Exception, e:
exceptions.append((op, args, e))
if exceptions:
- raise RefCleanupError(evalue, exceptions)
+ raise RefCleanupError(primary, exceptions)
def update(self, gd, ref, commit, old_commit, message=None):
'''Update a git repository's ref, reverting it on failure.
@@ -116,19 +151,50 @@ class LocalRefManager(object):
class RemoteRefManager(object):
'''Provide temporary pushes to remote repositories.
- Any ref changes made with the push method will be reversed after
- the end of the with statement the RemoteRefManager is used in.
+ When used in a with statement, if an exception is raised in the body,
+ then any pushed refs are reverted, so deletes get replaced and new
+ branches get deleted.
+
+ By default it will also undo pushed refs when an exception is not
+ raised, this can be overridden by passing False to the constructor.
+
+ There is also an explicit .close() method to clean up after the
+ context has exited like so:
+
+ with RemoteRefManager(False) as rrm:
+ # push refs with rrm.push(...)
+ # Do something with pushed refs
+ rrm.close() # Explicitly clean up
+
+ The name .close() was chosen for the cleanup method, so the
+ RemoteRefManager object may also be used again in a second with
+ statement using contextlib.closing().
+
+ with RemoteRefManager(False) as rrm:
+ rrm.push(...)
+ with contextlib.closing(rrm) as rrm:
+ # Do something with pushed refs and clean up if there is an
+ # exception
+
+ This is also useful if the RemoteRefManager is nested in another
+ object, since the .close() method can be called in that object's
+ cleanup method.
'''
- def __init__(self):
- self._cleanup = None
+ def __init__(self, cleanup_on_success=True):
+ self._cleanup_on_success = cleanup_on_success
+ self._cleanup = collections.deque()
def __enter__(self):
- self._cleanup = collections.deque()
return self
def __exit__(self, etype, evalue, estack):
+ if not self._cleanup_on_success and evalue is None:
+ return
+ self.close(evalue)
+
+ def close(self, primary=None):
exceptions = []
d = self._cleanup
while d:
@@ -138,7 +204,7 @@ class RemoteRefManager(object):
except Exception, e:
exceptions.append((remote, refspecs, e))
if exceptions:
- raise RefCleanupError(evalue, exceptions)
+ raise RefCleanupError(primary, exceptions)
def push(self, remote, *refspecs):
'''Push refspecs to remote and revert on failure.
diff --git a/morphlib/branchmanager_tests.py b/morphlib/branchmanager_tests.py
index 9bba7f2e..a7988c96 100644
--- a/morphlib/branchmanager_tests.py
+++ b/morphlib/branchmanager_tests.py
@@ -74,6 +74,25 @@ class LocalRefManagerTests(unittest.TestCase):
with self.assertRaises(morphlib.gitdir.InvalidRefError):
gd.resolve_ref_to_commit('refs/heads/create%d' % i)
+ def test_add_rollback_on_success(self):
+ with self.lrm(True) as lrm:
+ for i, gd in enumerate(self.repos):
+ commit = gd.resolve_ref_to_commit('refs/heads/master')
+ lrm.add(gd, 'refs/heads/create%d' % i, commit)
+ for i, gd in enumerate(self.repos):
+ with self.assertRaises(morphlib.gitdir.InvalidRefError):
+ gd.resolve_ref_to_commit('refs/heads/create%d' % i)
+
+ def test_add_rollback_deferred(self):
+ with self.lrm(False) as lrm:
+ for i, gd in enumerate(self.repos):
+ commit = gd.resolve_ref_to_commit('refs/heads/master')
+ lrm.add(gd, 'refs/heads/create%d' % i, commit)
+ lrm.close()
+ for i, gd in enumerate(self.repos):
+ with self.assertRaises(morphlib.gitdir.InvalidRefError):
+ gd.resolve_ref_to_commit('refs/heads/create%d' % i)
+
def test_add_rollback_failure(self):
failure_exception = Exception()
with self.assertRaises(morphlib.branchmanager.RefCleanupError) as cm:
@@ -117,6 +136,31 @@ class LocalRefManagerTests(unittest.TestCase):
self.assertEqual(gd.resolve_ref_to_commit('refs/heads/master'),
refinfo[i])
+ def test_update_rollback_on_success(self):
+ refinfo = []
+ with self.lrm(True) as lrm:
+ for i, gd in enumerate(self.repos):
+ old_master = gd.resolve_ref_to_commit('refs/heads/master')
+ commit = gd.resolve_ref_to_commit('refs/heads/dev-branch')
+ refinfo.append(old_master)
+ lrm.update(gd, 'refs/heads/master', commit, old_master)
+ for i, gd in enumerate(self.repos):
+ self.assertEqual(gd.resolve_ref_to_commit('refs/heads/master'),
+ refinfo[i])
+
+ def test_update_rollback_deferred(self):
+ refinfo = []
+ with self.lrm(False) as lrm:
+ for i, gd in enumerate(self.repos):
+ old_master = gd.resolve_ref_to_commit('refs/heads/master')
+ commit = gd.resolve_ref_to_commit('refs/heads/dev-branch')
+ refinfo.append(old_master)
+ lrm.update(gd, 'refs/heads/master', commit, old_master)
+ lrm.close()
+ for i, gd in enumerate(self.repos):
+ self.assertEqual(gd.resolve_ref_to_commit('refs/heads/master'),
+ refinfo[i])
+
def test_update_rollback_failure(self):
failure_exception = Exception()
with self.assertRaises(morphlib.branchmanager.RefCleanupError) as cm:
@@ -154,6 +198,29 @@ class LocalRefManagerTests(unittest.TestCase):
self.assertEqual(gd.resolve_ref_to_commit('refs/heads/master'),
refinfo[i])
+ def test_delete_rollback_on_success(self):
+ refinfo = []
+ with self.lrm(True) as lrm:
+ for i, gd in enumerate(self.repos):
+ commit = gd.resolve_ref_to_commit('refs/heads/master')
+ refinfo.append(commit)
+ lrm.delete(gd, 'refs/heads/master', commit)
+ for i, gd in enumerate(self.repos):
+ self.assertEqual(gd.resolve_ref_to_commit('refs/heads/master'),
+ refinfo[i])
+
+ def test_delete_rollback_deferred(self):
+ refinfo = []
+ with self.lrm(False) as lrm:
+ for i, gd in enumerate(self.repos):
+ commit = gd.resolve_ref_to_commit('refs/heads/master')
+ refinfo.append(commit)
+ lrm.delete(gd, 'refs/heads/master', commit)
+ lrm.close()
+ for i, gd in enumerate(self.repos):
+ self.assertEqual(gd.resolve_ref_to_commit('refs/heads/master'),
+ refinfo[i])
+
def test_delete_rollback_failure(self):
failure_exception = Exception()
with self.assertRaises(morphlib.branchmanager.RefCleanupError) as cm:
@@ -250,6 +317,17 @@ class RemoteRefManagerTests(unittest.TestCase):
self.assert_remote_branches()
self.assert_no_remote_branches()
+ def test_keep_after_create_success(self):
+ with morphlib.branchmanager.RemoteRefManager(False) as rrm:
+ self.push_creates(rrm)
+ self.assert_remote_branches()
+
+ def test_deferred_rollback_after_create_success(self):
+ with morphlib.branchmanager.RemoteRefManager(False) as rrm:
+ self.push_creates(rrm)
+ rrm.close()
+ self.assert_no_remote_branches()
+
def test_rollback_after_create_failure(self):
failure_exception = Exception()
with self.assertRaises(Exception) as cm:
@@ -292,6 +370,27 @@ class RemoteRefManagerTests(unittest.TestCase):
self.assert_no_remote_branches()
self.assert_remote_branches()
+ def test_keep_after_deletes_success(self):
+ for name, dirname, gd in self.remotes:
+ self.sgd.get_remote(name).push(
+ morphlib.gitdir.RefSpec('master'),
+ morphlib.gitdir.RefSpec('dev-branch'))
+ self.assert_remote_branches()
+ with morphlib.branchmanager.RemoteRefManager(False) as rrm:
+ self.push_deletes(rrm)
+ self.assert_no_remote_branches()
+
+ def test_deferred_rollback_after_deletes_success(self):
+ for name, dirname, gd in self.remotes:
+ self.sgd.get_remote(name).push(
+ morphlib.gitdir.RefSpec('master'),
+ morphlib.gitdir.RefSpec('dev-branch'))
+ self.assert_remote_branches()
+ with morphlib.branchmanager.RemoteRefManager(False) as rrm:
+ self.push_deletes(rrm)
+ rrm.close()
+ self.assert_remote_branches()
+
def test_rollback_after_deletes_failure(self):
failure_exception = Exception()
for name, dirname, gd in self.remotes: