summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2013-11-27 14:26:31 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2013-11-29 16:11:44 +0000
commit3eb2b658b1f3a612c78c14f3d2cba2a1f0b1333f (patch)
tree03e8cbb33e59a3cdc6d6d4db9a71631d4b50db30
parentb38e47f413c6651c8953d2bebd99ae0bb80c07f9 (diff)
downloadmorph-3eb2b658b1f3a612c78c14f3d2cba2a1f0b1333f.tar.gz
branchmanager: Allow deferred and optional cleanup on success.
Now it will optionally clean up on success based on a constructor parameter. It can be later cleaned up explicitly by calling close(). It is called close, rather than something more obvious, like cleanup(), since it means the manager can be re-used with contextlib.closing(). This now means that using the Managers without a context manager is less ugly, since you can explicitly call .close() in a finally block.
-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: