summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Maw <richard.maw@codethink.co.uk>2013-11-11 17:38:16 +0000
committerRichard Maw <richard.maw@codethink.co.uk>2013-11-22 13:49:25 +0000
commit0b4e147009bffa7b720ee767633e3acf3bb68c3a (patch)
tree12448f87519c8738444618f53eb6ed49f632de92
parentd83d6ad7230eb27afae4169330681967bb20dcfa (diff)
downloadmorph-0b4e147009bffa7b720ee767633e3acf3bb68c3a.tar.gz
GitDir: Add GitIndex class
This represents the state of the index of a GitDirectory. Methods that use the index are now used via the GitIndex class, rather than using the default index, as previously used when the methods were in GitDirectory. GitIndex may be constructed with an alternative path, which can be used to manipulate a git checkout without altering a developer's view of the repository i.e. The working tree and default index. This is needed for `morph build` and `morph deploy` to handle the build without commit logic.
-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)