summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2013-11-12 14:46:48 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2013-11-22 13:49:26 +0000
commit7f4662c38ac529faaed94734c3e7fab25d9bcc8b (patch)
tree7866b91fbc02c38b619b4bc47201c06cdf7aaf01
parent77d75eb06fa07c64b7e14c376fb6e3742ef33fb3 (diff)
downloadmorph-7f4662c38ac529faaed94734c3e7fab25d9bcc8b.tar.gz
GitDir: Add methods for ref management
-rw-r--r--morphlib/gitdir.py123
-rw-r--r--morphlib/gitdir_tests.py78
2 files changed, 201 insertions, 0 deletions
diff --git a/morphlib/gitdir.py b/morphlib/gitdir.py
index aad507a6..df2fde61 100644
--- a/morphlib/gitdir.py
+++ b/morphlib/gitdir.py
@@ -38,6 +38,58 @@ class InvalidRefError(cliapp.AppException):
'at ref %s.' %(repo.dirname, ref))
+class ExpectedSha1Error(cliapp.AppException):
+
+ def __init__(self, ref):
+ self.ref = ref
+ cliapp.AppException.__init__(
+ self, 'SHA1 expected, got %s' % ref)
+
+
+class RefChangeError(cliapp.AppException):
+ pass
+
+
+class RefAddError(RefChangeError):
+
+ def __init__(self, gd, ref, sha1, original_exception):
+ self.gd = gd
+ self.dirname = dirname = gd.dirname
+ self.ref = ref
+ self.sha1 = sha1
+ self.original_exception = original_exception
+ RefChangeError.__init__(self, 'Adding ref %(ref)s '\
+ 'with commit %(sha1)s failed in git repository '\
+ 'located at %(dirname)s: %(original_exception)r' % locals())
+
+
+class RefUpdateError(RefChangeError):
+
+ def __init__(self, gd, ref, old_sha1, new_sha1, original_exception):
+ self.gd = gd
+ self.dirname = dirname = gd.dirname
+ self.ref = ref
+ self.old_sha1 = old_sha1
+ self.new_sha1 = new_sha1
+ self.original_exception = original_exception
+ RefChangeError.__init__(self, 'Updating ref %(ref)s '\
+ 'from %(old_sha1)s to %(new_sha1)s failed in git repository '\
+ 'located at %(dirname)s: %(original_exception)r' % locals())
+
+
+class RefDeleteError(RefChangeError):
+
+ def __init__(self, gd, ref, sha1, original_exception):
+ self.gd = gd
+ self.dirname = dirname = gd.dirname
+ self.ref = ref
+ self.sha1 = sha1
+ self.original_exception = original_exception
+ RefChangeError.__init__(self, 'Deleting ref %(ref)s '\
+ 'expecting commit %(sha1)s failed in git repository '\
+ 'located at %(dirname)s: %(original_exception)r' % locals())
+
+
class GitDirectory(object):
'''Represent a git working tree + .git directory.
@@ -262,6 +314,77 @@ class GitDirectory(object):
'-p', parent, '-m', message],
env=env).strip()
+ @staticmethod
+ def _check_is_sha1(string):
+ if not morphlib.git.is_valid_sha1(string):
+ raise ExpectedSha1Error(string)
+
+ def _update_ref(self, ref_args, message):
+ args = ['git', 'update-ref']
+ # No test coverage, since while this functionality is useful,
+ # morph does not need an API for inspecting the reflog, so
+ # it existing purely to test ref updates is a tad overkill.
+ if message is not None: # pragma: no cover
+ args.extend(('-m', message))
+ args.extend(ref_args)
+ self._runcmd(args)
+
+ def add_ref(self, ref, sha1, message=None):
+ '''Create a ref called `ref` in the repository pointing to `sha1`.
+
+ `message` is a string to add to the reflog about this change
+ `ref` must not already exist, if it does, use `update_ref`
+ `sha1` must be a 40 character hexadecimal string representing
+ the SHA1 of the commit or tag this ref will point to, this is
+ the result of the commit_tree or resolve_ref_to_commit methods.
+
+ '''
+ self._check_is_sha1(sha1)
+ # 40 '0' characters is code for no previous value
+ # this ensures it will fail if the branch already exists
+ try:
+ return self._update_ref((ref, sha1, '0' * 40), message)
+ except Exception, e:
+ raise RefAddError(self, ref, sha1, e)
+
+ def update_ref(self, ref, sha1, old_sha1, message=None):
+ '''Change the commit the ref `ref` points to, to `sha1`.
+
+ `message` is a string to add to the reflog about this change
+ `sha1` and `old_sha` must be 40 character hexadecimal strings
+ representing the SHA1 of the commit or tag this ref will point
+ to and currently points to respectively. This is the result of
+ the commit_tree or resolve_ref_to_commit methods.
+ `ref` must exist, and point to `old_sha1`.
+ This is to avoid unexpected results when multiple processes
+ attempt to change refs.
+
+ '''
+ self._check_is_sha1(sha1)
+ self._check_is_sha1(old_sha1)
+ try:
+ return self._update_ref((ref, sha1, old_sha1), message)
+ except Exception, e:
+ raise RefUpdateError(self, ref, old_sha1, sha1, e)
+
+ def delete_ref(self, ref, old_sha1, message=None):
+ '''Remove the ref `ref`.
+
+ `message` is a string to add to the reflog about this change
+ `old_sha1` must be a 40 character hexadecimal string representing
+ the SHA1 of the commit or tag this ref will point to, this is
+ the result of the commit_tree or resolve_ref_to_commit methods.
+ `ref` must exist, and point to `old_sha1`.
+ This is to avoid unexpected results when multiple processes
+ attempt to change refs.
+
+ '''
+ self._check_is_sha1(old_sha1)
+ try:
+ return self._update_ref(('-d', ref, old_sha1), message)
+ except Exception, e:
+ raise RefDeleteError(self, ref, old_sha1, e)
+
def init(dirname):
'''Initialise a new git repository.'''
diff --git a/morphlib/gitdir_tests.py b/morphlib/gitdir_tests.py
index 3063f5cf..7a251d23 100644
--- a/morphlib/gitdir_tests.py
+++ b/morphlib/gitdir_tests.py
@@ -211,3 +211,81 @@ class GitDirectoryContentsTests(unittest.TestCase):
author_date=pseudo_now,
)
self.assertEqual(expected, gd.get_commit_contents(commit).split('\n'))
+
+
+class GitDirectoryRefTwiddlingTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tempdir = tempfile.mkdtemp()
+ self.dirname = os.path.join(self.tempdir, 'foo')
+ os.mkdir(self.dirname)
+ gd = morphlib.gitdir.init(self.dirname)
+ with open(os.path.join(self.dirname, 'foo'), 'w') as f:
+ f.write('dummy text\n')
+ gd._runcmd(['git', 'add', '.'])
+ gd._runcmd(['git', 'commit', '-m', 'Initial commit'])
+ # Add a second commit for update_ref test, so it has another
+ # commit to roll back from
+ with open(os.path.join(self.dirname, 'bar'), 'w') as f:
+ f.write('dummy text\n')
+ gd._runcmd(['git', 'add', '.'])
+ gd._runcmd(['git', 'commit', '-m', 'Second commit'])
+
+ def tearDown(self):
+ shutil.rmtree(self.tempdir)
+
+ def test_expects_sha1s(self):
+ gd = morphlib.gitdir.GitDirectory(self.dirname)
+ self.assertRaises(morphlib.gitdir.ExpectedSha1Error,
+ gd.add_ref, 'refs/heads/foo', 'HEAD')
+ self.assertRaises(morphlib.gitdir.ExpectedSha1Error,
+ gd.update_ref, 'refs/heads/foo', 'HEAD', 'HEAD')
+ self.assertRaises(morphlib.gitdir.ExpectedSha1Error,
+ gd.update_ref, 'refs/heads/master',
+ gd._rev_parse(gd.HEAD), 'HEAD')
+ self.assertRaises(morphlib.gitdir.ExpectedSha1Error,
+ gd.update_ref, 'refs/heads/master',
+ 'HEAD', gd._rev_parse(gd.HEAD))
+ self.assertRaises(morphlib.gitdir.ExpectedSha1Error,
+ gd.delete_ref, 'refs/heads/master', 'HEAD')
+
+ def test_add_ref(self):
+ gd = morphlib.gitdir.GitDirectory(self.dirname)
+ head_commit = gd.resolve_ref_to_commit(gd.HEAD)
+ gd.add_ref('refs/heads/foo', head_commit)
+ self.assertEqual(gd.resolve_ref_to_commit('refs/heads/foo'),
+ head_commit)
+
+ def test_add_ref_fail(self):
+ gd = morphlib.gitdir.GitDirectory(self.dirname)
+ head_commit = gd.resolve_ref_to_commit('refs/heads/master')
+ self.assertRaises(morphlib.gitdir.RefAddError,
+ gd.add_ref, 'refs/heads/master', head_commit)
+
+ def test_update_ref(self):
+ gd = morphlib.gitdir.GitDirectory(self.dirname)
+ head_commit = gd._rev_parse('refs/heads/master')
+ prev_commit = gd._rev_parse('refs/heads/master^')
+ gd.update_ref('refs/heads/master', prev_commit, head_commit)
+ self.assertEqual(gd._rev_parse('refs/heads/master'), prev_commit)
+
+ def test_update_ref_fail(self):
+ gd = morphlib.gitdir.GitDirectory(self.dirname)
+ head_commit = gd._rev_parse('refs/heads/master')
+ prev_commit = gd._rev_parse('refs/heads/master^')
+ gd.delete_ref('refs/heads/master', head_commit)
+ with self.assertRaises(morphlib.gitdir.RefUpdateError):
+ gd.update_ref('refs/heads/master', prev_commit, head_commit)
+
+ def test_delete_ref(self):
+ gd = morphlib.gitdir.GitDirectory(self.dirname)
+ head_commit = gd._rev_parse('refs/heads/master')
+ gd.delete_ref('refs/heads/master', head_commit)
+ self.assertRaises(morphlib.gitdir.InvalidRefError,
+ gd._rev_parse, 'refs/heads/master')
+
+ def test_delete_ref_fail(self):
+ gd = morphlib.gitdir.GitDirectory(self.dirname)
+ prev_commit = gd._rev_parse('refs/heads/master^')
+ with self.assertRaises(morphlib.gitdir.RefDeleteError):
+ gd.delete_ref('refs/heads/master', prev_commit)