summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--morphlib/__init__.py1
-rw-r--r--morphlib/gitdir.py34
-rw-r--r--morphlib/gitdir_tests.py11
-rw-r--r--morphlib/gitindex.py99
-rw-r--r--morphlib/gitindex_tests.py56
-rw-r--r--morphlib/plugins/branch_and_merge_new_plugin.py2
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)