summaryrefslogtreecommitdiff
path: root/morphlib/gitindex.py
diff options
context:
space:
mode:
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