diff options
-rw-r--r-- | morphlib/__init__.py | 1 | ||||
-rw-r--r-- | morphlib/gitdir.py | 34 | ||||
-rw-r--r-- | morphlib/gitdir_tests.py | 11 | ||||
-rw-r--r-- | morphlib/gitindex.py | 99 | ||||
-rw-r--r-- | morphlib/gitindex_tests.py | 56 | ||||
-rw-r--r-- | morphlib/plugins/branch_and_merge_new_plugin.py | 2 |
6 files changed, 165 insertions, 38 deletions
diff --git a/morphlib/__init__.py b/morphlib/__init__.py index 4954f812..76b7a989 100644 --- a/morphlib/__init__.py +++ b/morphlib/__init__.py @@ -60,6 +60,7 @@ import extractedtarball import fsutils import git import gitdir +import gitindex import localartifactcache import localrepocache import mountableimage diff --git a/morphlib/gitdir.py b/morphlib/gitdir.py index 4ba4cd9c..9020506a 100644 --- a/morphlib/gitdir.py +++ b/morphlib/gitdir.py @@ -16,9 +16,7 @@ # =*= License: GPL-2 =*= -import collections import cliapp -import glob import os import morphlib @@ -226,36 +224,8 @@ class GitDirectory(object): output = self._runcmd(['git', 'rev-parse', '--abbrev-ref', 'HEAD']) return output.strip() - def _get_status(self): - '''Runs git status and formats its output into something more useful. - - This runs git status such that unusual filenames are preserved - and returns its output in a sequence of (status_code, to_path, - from_path). - - from_path is None unless the status_code says there was a rename, - in which case it is the path it was renamed from. - - Untracked and ignored changes are also included in the output, - their status codes are '??' and '!!' respectively. - - ''' - status = self._runcmd(['git', 'status', '-z', '--ignored']) - tokens = collections.deque(status.split('\0')) - while True: - tok = tokens.popleft() - # Terminates with an empty token, since status ends with a \0 - if not tok: - return - - code = tok[:2] - to_path = tok[3:] - yield code, to_path, tokens.popleft() if code[0] == 'R' else None - - def get_uncommitted_changes(self): - for code, to_path, from_path in self._get_status(): - if code not in ('??', '!!'): - yield code, to_path, from_path + def get_index(self, index_file=None): + return morphlib.gitindex.GitIndex(self, index_file) def store_blob(self, blob_contents): '''Hash `blob_contents`, store it in git and return the sha1. diff --git a/morphlib/gitdir_tests.py b/morphlib/gitdir_tests.py index 395ee2e5..a56df7c8 100644 --- a/morphlib/gitdir_tests.py +++ b/morphlib/gitdir_tests.py @@ -64,6 +64,12 @@ class GitDirectoryTests(unittest.TestCase): gitdir.set_remote_fetch_url('origin', url) self.assertEqual(gitdir.get_remote_fetch_url('origin'), url) + def test_gets_index(self): + os.mkdir(self.dirname) + gitdir = morphlib.gitdir.init(self.dirname) + self.assertIsInstance(gitdir.get_index(), morphlib.gitindex.GitIndex) + + class GitDirectoryContentsTests(unittest.TestCase): def setUp(self): @@ -172,8 +178,3 @@ class GitDirectoryContentsTests(unittest.TestCase): with open(os.path.join(self.tempdir, 'blob'), 'r') as f: sha1 = gd.store_blob(f) self.assertEqual('test string', gd.get_blob_contents(sha1)) - - def test_uncommitted_changes(self): - gd = morphlib.gitdir.GitDirectory(self.dirname) - self.assertEqual(sorted(gd.get_uncommitted_changes()), - [(' D', 'foo', None)]) diff --git a/morphlib/gitindex.py b/morphlib/gitindex.py new file mode 100644 index 00000000..d8de8824 --- /dev/null +++ b/morphlib/gitindex.py @@ -0,0 +1,99 @@ +# Copyright (C) 2013 Codethink Limited +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# =*= License: GPL-2 =*= + + +import collections + + +STATUS_UNTRACKED = '??' +STATUS_IGNORED = '!!' + + +class GitIndex(object): + '''An object that represents operations on the working tree. + + Index objects can be constructed with a different path to the + index file, which can be used to construct commits without + altering the working tree, index or HEAD. + + The file must either be a previously initialised index, or a + non-existant file. + + Git creates a lock file and atomically alters the index by + renaming a temporary file into place, so `index_file` must be + in a writable directory. + + ''' + + def __init__(self, gd, index_file): + self._gd = gd + self._index_file = index_file + + def _run_git(self, *args, **kwargs): + if self._index_file is not None: + kwargs['env'] = kwargs.get('env', {}) + kwargs['env']['GIT_INDEX_FILE'] = self._index_file + return self._gd._runcmd(['git'] + list(args), **kwargs) + + def _get_status(self): + '''Return git status output in a Python useful format + + This runs git status such that unusual filenames are preserved + and returns its output in a sequence of (status_code, to_path, + from_path). + + from_path is None unless the status_code says there was a + rename, in which case it is the path it was renamed from. + + Untracked and ignored changes are also included in the output, + their status codes are '??' and '!!' respectively. + + ''' + + # git status -z will NUL terminate paths, so we don't have to + # unescape the paths it outputs. Unfortunately each status entry + # can have 1 or 2 paths, so extra parsing is required. + # To handle this, we split it into NUL delimited tokens. + # The first token of an entry is the 2 character status code, + # a space, then the path. + # If our status code starts with R then it's a rename, hence + # has a second path, requiring us to pop an extra token. + status = self._run_git('status', '-z', '--ignored') + tokens = collections.deque(status.split('\0')) + while True: + tok = tokens.popleft() + # Status output is NUL terminated rather than delimited, + # and split is for delimited output. A side effect of this is + # that we get an empty token as the last output. This suits + # us fine, as it gives us a sentinel value to terminate with. + if not tok: + return + + # The first token of an entry is 2 character status, a space, + # then the path + code = tok[:2] + to_path = tok[3:] + + # If the code starts with R then it's a rename, and + # the next token says where the file was renamed from + from_path = tokens.popleft() if code[0] == 'R' else None + yield code, to_path, from_path + + def get_uncommitted_changes(self): + for code, to_path, from_path in self._get_status(): + if code not in (STATUS_UNTRACKED, STATUS_IGNORED): + yield code, to_path, from_path diff --git a/morphlib/gitindex_tests.py b/morphlib/gitindex_tests.py new file mode 100644 index 00000000..db1e0e9b --- /dev/null +++ b/morphlib/gitindex_tests.py @@ -0,0 +1,56 @@ +# Copyright (C) 2013 Codethink Limited +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; version 2 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +# =*= License: GPL-2 =*= + + +import os +import shutil +import tempfile +import unittest + +import morphlib + + +class GitIndexTests(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']) + self.mirror = os.path.join(self.tempdir, 'mirror') + gd._runcmd(['git', 'clone', '--mirror', self.dirname, self.mirror]) + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def test_uncommitted_changes(self): + idx = morphlib.gitdir.GitDirectory(self.dirname).get_index() + self.assertEqual(list(idx.get_uncommitted_changes()), []) + os.unlink(os.path.join(self.dirname, 'foo')) + self.assertEqual(sorted(idx.get_uncommitted_changes()), + [(' D', 'foo', None)]) + + def test_uncommitted_alt_index(self): + gd = morphlib.gitdir.GitDirectory(self.dirname) + idx = gd.get_index(os.path.join(self.tempdir, 'index')) + self.assertEqual(sorted(idx.get_uncommitted_changes()), + [('D ', 'foo', None)]) + # 'D ' means not in the index, but in the working tree diff --git a/morphlib/plugins/branch_and_merge_new_plugin.py b/morphlib/plugins/branch_and_merge_new_plugin.py index e7b5abc1..8ad9effd 100644 --- a/morphlib/plugins/branch_and_merge_new_plugin.py +++ b/morphlib/plugins/branch_and_merge_new_plugin.py @@ -781,7 +781,7 @@ class SimpleBranchAndMergePlugin(cliapp.Plugin): if head != branch: self.app.output.write( ' %s: unexpected ref checked out %r\n' % (repo, head)) - if any(gd.get_uncommitted_changes()): + if any(gd.get_index().get_uncommitted_changes()): has_uncommitted_changes = True self.app.output.write(' %s: uncommitted changes\n' % repo) |