summaryrefslogtreecommitdiff
path: root/morphlib/gitindex.py
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 /morphlib/gitindex.py
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.
Diffstat (limited to 'morphlib/gitindex.py')
-rw-r--r--morphlib/gitindex.py99
1 files changed, 99 insertions, 0 deletions
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