From 7f4662c38ac529faaed94734c3e7fab25d9bcc8b Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Tue, 12 Nov 2013 14:46:48 +0000 Subject: GitDir: Add methods for ref management --- morphlib/gitdir.py | 123 +++++++++++++++++++++++++++++++++++++++++++++++ morphlib/gitdir_tests.py | 78 ++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) 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) -- cgit v1.2.1