summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/intro.rst4
-rw-r--r--lib/git/index.py2663
-rw-r--r--lib/git/refs.py37
-rw-r--r--lib/git/repo.py3
-rw-r--r--lib/git/utils.py2
-rw-r--r--test/git/test_refs.py10
-rw-r--r--test/git/test_repo.py34
7 files changed, 1392 insertions, 1361 deletions
diff --git a/doc/intro.rst b/doc/intro.rst
index 448b7deb..476ab4ec 100644
--- a/doc/intro.rst
+++ b/doc/intro.rst
@@ -6,8 +6,8 @@ Overview / Install
GitPython is a python library used to interact with Git repositories.
-GitPython is a port of the grit_ library in Ruby created by
-Tom Preston-Werner and Chris Wanstrath.
+GitPython was a port of the grit_ library in Ruby created by
+Tom Preston-Werner and Chris Wanstrath, but grew beyond its heritage through its improved design and performance.
.. _grit: http://grit.rubyforge.org
diff --git a/lib/git/index.py b/lib/git/index.py
index 0741d71a..8e8799e6 100644
--- a/lib/git/index.py
+++ b/lib/git/index.py
@@ -25,1357 +25,1364 @@ from git.utils import SHA1Writer, LazyMixin, ConcurrentWriteOperation, join_path
class CheckoutError( Exception ):
- """Thrown if a file could not be checked out from the index as it contained
- changes.
-
- The .failed_files attribute contains a list of relative paths that failed
- to be checked out as they contained changes that did not exist in the index.
-
- The .failed_reasons attribute contains a string informing about the actual
- cause of the issue.
-
- The .valid_files attribute contains a list of relative paths to files that
- were checked out successfully and hence match the version stored in the
- index"""
- def __init__(self, message, failed_files, valid_files, failed_reasons):
- Exception.__init__(self, message)
- self.failed_files = failed_files
- self.failed_reasons = failed_reasons
- self.valid_files = valid_files
+ """Thrown if a file could not be checked out from the index as it contained
+ changes.
+
+ The .failed_files attribute contains a list of relative paths that failed
+ to be checked out as they contained changes that did not exist in the index.
+
+ The .failed_reasons attribute contains a string informing about the actual
+ cause of the issue.
+
+ The .valid_files attribute contains a list of relative paths to files that
+ were checked out successfully and hence match the version stored in the
+ index"""
+ def __init__(self, message, failed_files, valid_files, failed_reasons):
+ Exception.__init__(self, message)
+ self.failed_files = failed_files
+ self.failed_reasons = failed_reasons
+ self.valid_files = valid_files
- def __str__(self):
- return Exception.__str__(self) + ":%s" % self.failed_files
-
+ def __str__(self):
+ return Exception.__str__(self) + ":%s" % self.failed_files
+
class _TemporaryFileSwap(object):
- """
- Utility class moving a file to a temporary location within the same directory
- and moving it back on to where on object deletion.
- """
- __slots__ = ("file_path", "tmp_file_path")
-
- def __init__(self, file_path):
- self.file_path = file_path
- self.tmp_file_path = self.file_path + tempfile.mktemp('','','')
- # it may be that the source does not exist
- try:
- os.rename(self.file_path, self.tmp_file_path)
- except OSError:
- pass
-
- def __del__(self):
- if os.path.isfile(self.tmp_file_path):
- if os.name == 'nt' and os.path.exists(self.file_path):
- os.remove(self.file_path)
- os.rename(self.tmp_file_path, self.file_path)
- # END temp file exists
+ """
+ Utility class moving a file to a temporary location within the same directory
+ and moving it back on to where on object deletion.
+ """
+ __slots__ = ("file_path", "tmp_file_path")
+
+ def __init__(self, file_path):
+ self.file_path = file_path
+ self.tmp_file_path = self.file_path + tempfile.mktemp('','','')
+ # it may be that the source does not exist
+ try:
+ os.rename(self.file_path, self.tmp_file_path)
+ except OSError:
+ pass
+
+ def __del__(self):
+ if os.path.isfile(self.tmp_file_path):
+ if os.name == 'nt' and os.path.exists(self.file_path):
+ os.remove(self.file_path)
+ os.rename(self.tmp_file_path, self.file_path)
+ # END temp file exists
-
+
class BlobFilter(object):
- """
- Predicate to be used by iter_blobs allowing to filter only return blobs which
- match the given list of directories or files.
-
- The given paths are given relative to the repository.
- """
- __slots__ = 'paths'
-
- def __init__(self, paths):
- """
- ``paths``
- tuple or list of paths which are either pointing to directories or
- to files relative to the current repository
- """
- self.paths = paths
-
- def __call__(self, stage_blob):
- path = stage_blob[1].path
- for p in self.paths:
- if path.startswith(p):
- return True
- # END for each path in filter paths
- return False
+ """
+ Predicate to be used by iter_blobs allowing to filter only return blobs which
+ match the given list of directories or files.
+
+ The given paths are given relative to the repository.
+ """
+ __slots__ = 'paths'
+
+ def __init__(self, paths):
+ """
+ ``paths``
+ tuple or list of paths which are either pointing to directories or
+ to files relative to the current repository
+ """
+ self.paths = paths
+
+ def __call__(self, stage_blob):
+ path = stage_blob[1].path
+ for p in self.paths:
+ if path.startswith(p):
+ return True
+ # END for each path in filter paths
+ return False
class BaseIndexEntry(tuple):
- """
- Small Brother of an index entry which can be created to describe changes
- done to the index in which case plenty of additional information is not requried.
-
- As the first 4 data members match exactly to the IndexEntry type, methods
- expecting a BaseIndexEntry can also handle full IndexEntries even if they
- use numeric indices for performance reasons.
- """
-
- def __str__(self):
- return "%o %s %i\t%s\n" % (self.mode, self.sha, self.stage, self.path)
-
- @property
- def mode(self):
- """
- File Mode, compatible to stat module constants
- """
- return self[0]
-
- @property
- def sha(self):
- """
- hex sha of the blob
- """
- return self[1]
-
- @property
- def stage(self):
- """
- Stage of the entry, either:
- 0 = default stage
- 1 = stage before a merge or common ancestor entry in case of a 3 way merge
- 2 = stage of entries from the 'left' side of the merge
- 3 = stage of entries from the right side of the merge
- Note:
- For more information, see http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html
- """
- return self[2]
+ """
+ Small Brother of an index entry which can be created to describe changes
+ done to the index in which case plenty of additional information is not requried.
+
+ As the first 4 data members match exactly to the IndexEntry type, methods
+ expecting a BaseIndexEntry can also handle full IndexEntries even if they
+ use numeric indices for performance reasons.
+ """
+
+ def __str__(self):
+ return "%o %s %i\t%s\n" % (self.mode, self.sha, self.stage, self.path)
+
+ @property
+ def mode(self):
+ """
+ File Mode, compatible to stat module constants
+ """
+ return self[0]
+
+ @property
+ def sha(self):
+ """
+ hex sha of the blob
+ """
+ return self[1]
+
+ @property
+ def stage(self):
+ """
+ Stage of the entry, either:
+ 0 = default stage
+ 1 = stage before a merge or common ancestor entry in case of a 3 way merge
+ 2 = stage of entries from the 'left' side of the merge
+ 3 = stage of entries from the right side of the merge
+ Note:
+ For more information, see http://www.kernel.org/pub/software/scm/git/docs/git-read-tree.html
+ """
+ return self[2]
- @property
- def path(self):
- return self[3]
-
- @classmethod
- def from_blob(cls, blob, stage = 0):
- """
- Returns
- Fully equipped BaseIndexEntry at the given stage
- """
- return cls((blob.mode, blob.sha, stage, blob.path))
-
+ @property
+ def path(self):
+ return self[3]
+
+ @classmethod
+ def from_blob(cls, blob, stage = 0):
+ """
+ Returns
+ Fully equipped BaseIndexEntry at the given stage
+ """
+ return cls((blob.mode, blob.sha, stage, blob.path))
+
class IndexEntry(BaseIndexEntry):
- """
- Allows convenient access to IndexEntry data without completely unpacking it.
-
- Attributes usully accessed often are cached in the tuple whereas others are
- unpacked on demand.
-
- See the properties for a mapping between names and tuple indices.
- """
- @property
- def ctime(self):
- """
- Returns
- Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the
- file's creation time
- """
- return struct.unpack(">LL", self[4])
-
- @property
- def mtime(self):
- """
- See ctime property, but returns modification time
- """
- return struct.unpack(">LL", self[5])
-
- @property
- def dev(self):
- """
- Device ID
- """
- return self[6]
-
- @property
- def inode(self):
- """
- Inode ID
- """
- return self[7]
-
- @property
- def uid(self):
- """
- User ID
- """
- return self[8]
-
- @property
- def gid(self):
- """
- Group ID
- """
- return self[9]
+ """
+ Allows convenient access to IndexEntry data without completely unpacking it.
+
+ Attributes usully accessed often are cached in the tuple whereas others are
+ unpacked on demand.
+
+ See the properties for a mapping between names and tuple indices.
+ """
+ @property
+ def ctime(self):
+ """
+ Returns
+ Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the
+ file's creation time
+ """
+ return struct.unpack(">LL", self[4])
+
+ @property
+ def mtime(self):
+ """
+ See ctime property, but returns modification time
+ """
+ return struct.unpack(">LL", self[5])
+
+ @property
+ def dev(self):
+ """
+ Device ID
+ """
+ return self[6]
+
+ @property
+ def inode(self):
+ """
+ Inode ID
+ """
+ return self[7]
+
+ @property
+ def uid(self):
+ """
+ User ID
+ """
+ return self[8]
+
+ @property
+ def gid(self):
+ """
+ Group ID
+ """
+ return self[9]
- @property
- def size(self):
- """
- Uncompressed size of the blob
-
- Note
- Will be 0 if the stage is not 0 ( hence it is an unmerged entry )
- """
- return self[10]
-
- @classmethod
- def from_base(cls, base):
- """
- Returns
- Minimal entry as created from the given BaseIndexEntry instance.
- Missing values will be set to null-like values
-
- ``base``
- Instance of type BaseIndexEntry
- """
- time = struct.pack(">LL", 0, 0)
- return IndexEntry((base.mode, base.sha, base.stage, base.path, time, time, 0, 0, 0, 0, 0))
-
- @classmethod
- def from_blob(cls, blob):
- """
- Returns
- Minimal entry resembling the given blob objecft
- """
- time = struct.pack(">LL", 0, 0)
- return IndexEntry((blob.mode, blob.sha, 0, blob.path, time, time, 0, 0, 0, 0, blob.size))
+ @property
+ def size(self):
+ """
+ Uncompressed size of the blob
+
+ Note
+ Will be 0 if the stage is not 0 ( hence it is an unmerged entry )
+ """
+ return self[10]
+
+ @classmethod
+ def from_base(cls, base):
+ """
+ Returns
+ Minimal entry as created from the given BaseIndexEntry instance.
+ Missing values will be set to null-like values
+
+ ``base``
+ Instance of type BaseIndexEntry
+ """
+ time = struct.pack(">LL", 0, 0)
+ return IndexEntry((base.mode, base.sha, base.stage, base.path, time, time, 0, 0, 0, 0, 0))
+
+ @classmethod
+ def from_blob(cls, blob):
+ """
+ Returns
+ Minimal entry resembling the given blob objecft
+ """
+ time = struct.pack(">LL", 0, 0)
+ return IndexEntry((blob.mode, blob.sha, 0, blob.path, time, time, 0, 0, 0, 0, blob.size))
def clear_cache(func):
- """
- Decorator for functions that alter the index using the git command. This would
- invalidate our possibly existing entries dictionary which is why it must be
- deleted to allow it to be lazily reread later.
-
- Note
- This decorator will not be required once all functions are implemented
- natively which in fact is possible, but probably not feasible performance wise.
- """
- def clear_cache_if_not_raised(self, *args, **kwargs):
- rval = func(self, *args, **kwargs)
- self._delete_entries_cache()
- return rval
-
- # END wrapper method
- clear_cache_if_not_raised.__name__ = func.__name__
- return clear_cache_if_not_raised
-
+ """
+ Decorator for functions that alter the index using the git command. This would
+ invalidate our possibly existing entries dictionary which is why it must be
+ deleted to allow it to be lazily reread later.
+
+ Note
+ This decorator will not be required once all functions are implemented
+ natively which in fact is possible, but probably not feasible performance wise.
+ """
+ def clear_cache_if_not_raised(self, *args, **kwargs):
+ rval = func(self, *args, **kwargs)
+ self._delete_entries_cache()
+ return rval
+
+ # END wrapper method
+ clear_cache_if_not_raised.__name__ = func.__name__
+ return clear_cache_if_not_raised
+
def default_index(func):
- """
- Decorator assuring the wrapped method may only run if we are the default
- repository index. This is as we rely on git commands that operate
- on that index only.
- """
- def check_default_index(self, *args, **kwargs):
- if self._file_path != self._index_path():
- raise AssertionError( "Cannot call %r on indices that do not represent the default git index" % func.__name__ )
- return func(self, *args, **kwargs)
- # END wrpaper method
-
- check_default_index.__name__ = func.__name__
- return check_default_index
+ """
+ Decorator assuring the wrapped method may only run if we are the default
+ repository index. This is as we rely on git commands that operate
+ on that index only.
+ """
+ def check_default_index(self, *args, **kwargs):
+ if self._file_path != self._index_path():
+ raise AssertionError( "Cannot call %r on indices that do not represent the default git index" % func.__name__ )
+ return func(self, *args, **kwargs)
+ # END wrpaper method
+
+ check_default_index.__name__ = func.__name__
+ return check_default_index
class IndexFile(LazyMixin, diff.Diffable):
- """
- Implements an Index that can be manipulated using a native implementation in
- order to save git command function calls wherever possible.
-
- It provides custom merging facilities allowing to merge without actually changing
- your index or your working tree. This way you can perform own test-merges based
- on the index only without having to deal with the working copy. This is useful
- in case of partial working trees.
-
- ``Entries``
- The index contains an entries dict whose keys are tuples of type IndexEntry
- to facilitate access.
-
- You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
- index.entries[index.get_entries_key(index_entry_instance)] = index_entry_instance
- Otherwise changes to it will be lost when changing the index using its methods.
- """
- __slots__ = ( "repo", "version", "entries", "_extension_data", "_file_path" )
- _VERSION = 2 # latest version we support
- S_IFGITLINK = 0160000
-
- def __init__(self, repo, file_path=None):
- """
- Initialize this Index instance, optionally from the given ``file_path``.
- If no file_path is given, we will be created from the current index file.
-
- If a stream is not given, the stream will be initialized from the current
- repository's index on demand.
- """
- self.repo = repo
- self.version = self._VERSION
- self._extension_data = ''
- self._file_path = file_path or self._index_path()
-
- def _set_cache_(self, attr):
- if attr == "entries":
- # read the current index
- # try memory map for speed
- fp = open(self._file_path, "rb")
- stream = fp
- try:
- raise Exception()
- stream = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
- except Exception:
- pass
- # END memory mapping
-
- try:
- self._read_from_stream(stream)
- finally:
- pass
- # make sure we close the stream ( possibly an mmap )
- # and the file
- #stream.close()
- #if stream is not fp:
- # fp.close()
- # END read from default index on demand
- else:
- super(IndexFile, self)._set_cache_(attr)
-
- def _index_path(self):
- return join_path_native(self.repo.git_dir, "index")
-
-
- @property
- def path(self):
- """
- Returns
- Path to the index file we are representing
- """
- return self._file_path
-
- def _delete_entries_cache(self):
- """Safely clear the entries cache so it can be recreated"""
- try:
- del(self.entries)
- except AttributeError:
- # fails in python 2.6.5 with this exception
- pass
- # END exception handling
-
- @classmethod
- def _read_entry(cls, stream):
- """Return: One entry of the given stream"""
- beginoffset = stream.tell()
- ctime = struct.unpack(">8s", stream.read(8))[0]
- mtime = struct.unpack(">8s", stream.read(8))[0]
- (dev, ino, mode, uid, gid, size, sha, flags) = \
- struct.unpack(">LLLLLL20sH", stream.read(20 + 4 * 6 + 2))
- path_size = flags & 0x0fff
- path = stream.read(path_size)
-
- real_size = ((stream.tell() - beginoffset + 8) & ~7)
- data = stream.read((beginoffset + real_size) - stream.tell())
- return IndexEntry((mode, binascii.hexlify(sha), flags >> 12, path, ctime, mtime, dev, ino, uid, gid, size))
-
- @classmethod
- def _read_header(cls, stream):
- """Return tuple(version_long, num_entries) from the given stream"""
- type_id = stream.read(4)
- if type_id != "DIRC":
- raise AssertionError("Invalid index file header: %r" % type_id)
- version, num_entries = struct.unpack(">LL", stream.read(4 * 2))
- assert version in (1, 2)
- return version, num_entries
-
- def _read_from_stream(self, stream):
- """
- Initialize this instance with index values read from the given stream
- """
- self.version, num_entries = self._read_header(stream)
- count = 0
- self.entries = dict()
- while count < num_entries:
- entry = self._read_entry(stream)
- self.entries[self.get_entries_key(entry)] = entry
- count += 1
- # END for each entry
-
- # the footer contains extension data and a sha on the content so far
- # Keep the extension footer,and verify we have a sha in the end
- # Extension data format is:
- # 4 bytes ID
- # 4 bytes length of chunk
- # repeated 0 - N times
- self._extension_data = stream.read(~0)
- assert len(self._extension_data) > 19, "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(self._extension_data)
-
- content_sha = self._extension_data[-20:]
-
- # truncate the sha in the end as we will dynamically create it anyway
- self._extension_data = self._extension_data[:-20]
-
-
- @classmethod
- def _write_cache_entry(cls, stream, entry):
- """
- Write an IndexEntry to a stream
- """
- beginoffset = stream.tell()
- stream.write(entry[4]) # ctime
- stream.write(entry[5]) # mtime
- path = entry[3]
- plen = len(path) & 0x0fff # path length
- assert plen == len(path), "Path %s too long to fit into index" % entry[3]
- flags = plen | (entry[2] << 12)# stage and path length are 2 byte flags
- stream.write(struct.pack(">LLLLLL20sH", entry[6], entry[7], entry[0],
- entry[8], entry[9], entry[10], binascii.unhexlify(entry[1]), flags))
- stream.write(path)
- real_size = ((stream.tell() - beginoffset + 8) & ~7)
- stream.write("\0" * ((beginoffset + real_size) - stream.tell()))
+ """
+ Implements an Index that can be manipulated using a native implementation in
+ order to save git command function calls wherever possible.
+
+ It provides custom merging facilities allowing to merge without actually changing
+ your index or your working tree. This way you can perform own test-merges based
+ on the index only without having to deal with the working copy. This is useful
+ in case of partial working trees.
+
+ ``Entries``
+ The index contains an entries dict whose keys are tuples of type IndexEntry
+ to facilitate access.
+
+ You may read the entries dict or manipulate it using IndexEntry instance, i.e.::
+ index.entries[index.get_entries_key(index_entry_instance)] = index_entry_instance
+ Otherwise changes to it will be lost when changing the index using its methods.
+ """
+ __slots__ = ( "repo", "version", "entries", "_extension_data", "_file_path" )
+ _VERSION = 2 # latest version we support
+ S_IFGITLINK = 0160000
+
+ def __init__(self, repo, file_path=None):
+ """
+ Initialize this Index instance, optionally from the given ``file_path``.
+ If no file_path is given, we will be created from the current index file.
+
+ If a stream is not given, the stream will be initialized from the current
+ repository's index on demand.
+ """
+ self.repo = repo
+ self.version = self._VERSION
+ self._extension_data = ''
+ self._file_path = file_path or self._index_path()
+
+ def _set_cache_(self, attr):
+ if attr == "entries":
+ # read the current index
+ # try memory map for speed
+ try:
+ fp = open(self._file_path, "rb")
+ except IOError:
+ # in new repositories, there may be no index, which means we are empty
+ self.entries = dict()
+ return
+ # END exception handling
+
+ stream = fp
+ try:
+ raise Exception()
+ stream = mmap.mmap(fp.fileno(), 0, access=mmap.ACCESS_READ)
+ except Exception:
+ pass
+ # END memory mapping
+
+ try:
+ self._read_from_stream(stream)
+ finally:
+ pass
+ # make sure we close the stream ( possibly an mmap )
+ # and the file
+ #stream.close()
+ #if stream is not fp:
+ # fp.close()
+ # END read from default index on demand
+ else:
+ super(IndexFile, self)._set_cache_(attr)
+
+ def _index_path(self):
+ return join_path_native(self.repo.git_dir, "index")
+
+
+ @property
+ def path(self):
+ """
+ Returns
+ Path to the index file we are representing
+ """
+ return self._file_path
+
+ def _delete_entries_cache(self):
+ """Safely clear the entries cache so it can be recreated"""
+ try:
+ del(self.entries)
+ except AttributeError:
+ # fails in python 2.6.5 with this exception
+ pass
+ # END exception handling
+
+ @classmethod
+ def _read_entry(cls, stream):
+ """Return: One entry of the given stream"""
+ beginoffset = stream.tell()
+ ctime = struct.unpack(">8s", stream.read(8))[0]
+ mtime = struct.unpack(">8s", stream.read(8))[0]
+ (dev, ino, mode, uid, gid, size, sha, flags) = \
+ struct.unpack(">LLLLLL20sH", stream.read(20 + 4 * 6 + 2))
+ path_size = flags & 0x0fff
+ path = stream.read(path_size)
+
+ real_size = ((stream.tell() - beginoffset + 8) & ~7)
+ data = stream.read((beginoffset + real_size) - stream.tell())
+ return IndexEntry((mode, binascii.hexlify(sha), flags >> 12, path, ctime, mtime, dev, ino, uid, gid, size))
+
+ @classmethod
+ def _read_header(cls, stream):
+ """Return tuple(version_long, num_entries) from the given stream"""
+ type_id = stream.read(4)
+ if type_id != "DIRC":
+ raise AssertionError("Invalid index file header: %r" % type_id)
+ version, num_entries = struct.unpack(">LL", stream.read(4 * 2))
+ assert version in (1, 2)
+ return version, num_entries
+
+ def _read_from_stream(self, stream):
+ """
+ Initialize this instance with index values read from the given stream
+ """
+ self.version, num_entries = self._read_header(stream)
+ count = 0
+ self.entries = dict()
+ while count < num_entries:
+ entry = self._read_entry(stream)
+ self.entries[self.get_entries_key(entry)] = entry
+ count += 1
+ # END for each entry
+
+ # the footer contains extension data and a sha on the content so far
+ # Keep the extension footer,and verify we have a sha in the end
+ # Extension data format is:
+ # 4 bytes ID
+ # 4 bytes length of chunk
+ # repeated 0 - N times
+ self._extension_data = stream.read(~0)
+ assert len(self._extension_data) > 19, "Index Footer was not at least a sha on content as it was only %i bytes in size" % len(self._extension_data)
+
+ content_sha = self._extension_data[-20:]
+
+ # truncate the sha in the end as we will dynamically create it anyway
+ self._extension_data = self._extension_data[:-20]
+
+
+ @classmethod
+ def _write_cache_entry(cls, stream, entry):
+ """
+ Write an IndexEntry to a stream
+ """
+ beginoffset = stream.tell()
+ stream.write(entry[4]) # ctime
+ stream.write(entry[5]) # mtime
+ path = entry[3]
+ plen = len(path) & 0x0fff # path length
+ assert plen == len(path), "Path %s too long to fit into index" % entry[3]
+ flags = plen | (entry[2] << 12)# stage and path length are 2 byte flags
+ stream.write(struct.pack(">LLLLLL20sH", entry[6], entry[7], entry[0],
+ entry[8], entry[9], entry[10], binascii.unhexlify(entry[1]), flags))
+ stream.write(path)
+ real_size = ((stream.tell() - beginoffset + 8) & ~7)
+ stream.write("\0" * ((beginoffset + real_size) - stream.tell()))
- def write(self, file_path = None, ignore_tree_extension_data=False):
- """
- Write the current state to our file path or to the given one
-
- ``file_path``
- If None, we will write to our stored file path from which we have
- been initialized. Otherwise we write to the given file path.
- Please note that this will change the file_path of this index to
- the one you gave.
-
- ``ignore_tree_extension_data``
- If True, the TREE type extension data read in the index will not
- be written to disk. Use this if you have altered the index and
- would like to use git-write-tree afterwards to create a tree
- representing your written changes.
- If this data is present in the written index, git-write-tree
- will instead write the stored/cached tree.
- Alternatively, use IndexFile.write_tree() to handle this case
- automatically
-
- Returns
- self
-
- Note
- Index writing based on the dulwich implementation
- """
- write_op = ConcurrentWriteOperation(file_path or self._file_path)
- stream = write_op._begin_writing()
-
- stream = SHA1Writer(stream)
-
- # header
- stream.write("DIRC")
- stream.write(struct.pack(">LL", self.version, len(self.entries)))
-
- # body
- entries_sorted = self.entries.values()
- entries_sorted.sort(key=lambda e: (e[3], e[2])) # use path/stage as sort key
- for entry in entries_sorted:
- self._write_cache_entry(stream, entry)
- # END for each entry
-
- stored_ext_data = None
- if ignore_tree_extension_data and self._extension_data and self._extension_data[:4] == 'TREE':
- stored_ext_data = self._extension_data
- self._extension_data = ''
- # END extension data special handling
-
- # write previously cached extensions data
- stream.write(self._extension_data)
-
- if stored_ext_data:
- self._extension_data = stored_ext_data
- # END reset previous ext data
-
- # write the sha over the content
- stream.write_sha()
- write_op._end_writing()
-
- # make sure we represent what we have written
- if file_path is not None:
- self._file_path = file_path
-
- @clear_cache
- @default_index
- def merge_tree(self, rhs, base=None):
- """Merge the given rhs treeish into the current index, possibly taking
- a common base treeish into account.
-
- As opposed to the from_tree_ method, this allows you to use an already
- existing tree as the left side of the merge
-
- ``rhs``
- treeish reference pointing to the 'other' side of the merge.
-
- ``base``
- optional treeish reference pointing to the common base of 'rhs' and
- this index which equals lhs
-
- Returns
- self ( containing the merge and possibly unmerged entries in case of
- conflicts )
-
- Raise
- GitCommandError in case there is a merge conflict. The error will
- be raised at the first conflicting path. If you want to have proper
- merge resolution to be done by yourself, you have to commit the changed
- index ( or make a valid tree from it ) and retry with a three-way
- index.from_tree call.
- """
- # -i : ignore working tree status
- # --aggressive : handle more merge cases
- # -m : do an actual merge
- args = ["--aggressive", "-i", "-m"]
- if base is not None:
- args.append(base)
- args.append(rhs)
-
- self.repo.git.read_tree(args)
- return self
-
- @classmethod
- def from_tree(cls, repo, *treeish, **kwargs):
- """
- Merge the given treeish revisions into a new index which is returned.
- The original index will remain unaltered
-
- ``repo``
- The repository treeish are located in.
-
- ``*treeish``
- One, two or three Tree Objects or Commits. The result changes according to the
- amount of trees.
- If 1 Tree is given, it will just be read into a new index
- If 2 Trees are given, they will be merged into a new index using a
- two way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
- one. It behaves like a fast-forward.
- If 3 Trees are given, a 3-way merge will be performed with the first tree
- being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree,
- tree 3 is the 'other' one
-
- ``**kwargs``
- Additional arguments passed to git-read-tree
-
- Returns
- New IndexFile instance. It will point to a temporary index location which
- does not exist anymore. If you intend to write such a merged Index, supply
- an alternate file_path to its 'write' method.
-
- Note:
- In the three-way merge case, --aggressive will be specified to automatically
- resolve more cases in a commonly correct manner. Specify trivial=True as kwarg
- to override that.
-
- As the underlying git-read-tree command takes into account the current index,
- it will be temporarily moved out of the way to assure there are no unsuspected
- interferences.
- """
- if len(treeish) == 0 or len(treeish) > 3:
- raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
-
- arg_list = list()
- # ignore that working tree and index possibly are out of date
- if len(treeish)>1:
- # drop unmerged entries when reading our index and merging
- arg_list.append("--reset")
- # handle non-trivial cases the way a real merge does
- arg_list.append("--aggressive")
- # END merge handling
-
- # tmp file created in git home directory to be sure renaming
- # works - /tmp/ dirs could be on another device
- tmp_index = tempfile.mktemp('','',repo.git_dir)
- arg_list.append("--index-output=%s" % tmp_index)
- arg_list.extend(treeish)
-
- # move current index out of the way - otherwise the merge may fail
- # as it considers existing entries. moving it essentially clears the index.
- # Unfortunately there is no 'soft' way to do it.
- # The _TemporaryFileSwap assure the original file get put back
- index_handler = _TemporaryFileSwap(join_path_native(repo.git_dir, 'index'))
- try:
- repo.git.read_tree(*arg_list, **kwargs)
- index = cls(repo, tmp_index)
- index.entries # force it to read the file as we will delete the temp-file
- del(index_handler) # release as soon as possible
- finally:
- if os.path.exists(tmp_index):
- os.remove(tmp_index)
- # END index merge handling
-
- return index
-
- @classmethod
- def _index_mode_to_tree_index_mode(cls, index_mode):
- """
- Cleanup a index_mode value.
- This will return a index_mode that can be stored in a tree object.
-
- ``index_mode``
- Index_mode to clean up.
- """
- if stat.S_ISLNK(index_mode):
- return stat.S_IFLNK
- elif stat.S_ISDIR(index_mode):
- return stat.S_IFDIR
- elif stat.S_IFMT(index_mode) == cls.S_IFGITLINK:
- return cls.S_IFGITLINK
- ret = stat.S_IFREG | 0644
- ret |= (index_mode & 0111)
- return ret
-
-
- # UTILITIES
- def _iter_expand_paths(self, paths):
- """Expand the directories in list of paths to the corresponding paths accordingly,
-
- Note: git will add items multiple times even if a glob overlapped
- with manually specified paths or if paths where specified multiple
- times - we respect that and do not prune"""
- def raise_exc(e):
- raise e
- r = self.repo.working_tree_dir
- rs = r + '/'
- for path in paths:
- abs_path = path
- if not os.path.isabs(abs_path):
- abs_path = os.path.join(r, path)
- # END make absolute path
-
- # resolve globs if possible
- if '?' in path or '*' in path or '[' in path:
- for f in self._iter_expand_paths(glob.glob(abs_path)):
- yield f.replace(rs, '')
- continue
- # END glob handling
- try:
- for root, dirs, files in os.walk(abs_path, onerror=raise_exc):
- for rela_file in files:
- # add relative paths only
- yield os.path.join(root.replace(rs, ''), rela_file)
- # END for each file in subdir
- # END for each subdirectory
- except OSError:
- # was a file or something that could not be iterated
- yield path.replace(rs, '')
- # END path exception handling
- # END for each path
-
- def _write_path_to_stdin(self, proc, filepath, item, fmakeexc, fprogress, read_from_stdout=True):
- """Write path to proc.stdin and make sure it processes the item, including progress.
- @return: stdout string
- @param read_from_stdout: if True, proc.stdout will be read after the item
- was sent to stdin. In that case, it will return None
- @note: There is a bug in git-update-index that prevents it from sending
- reports just in time. This is why we have a version that tries to
- read stdout and one which doesn't. In fact, the stdout is not
- important as the piped-in files are processed anyway and just in time"""
- fprogress(filepath, False, item)
- rval = None
- try:
- proc.stdin.write("%s\n" % filepath)
- except IOError:
- # pipe broke, usually because some error happend
- raise fmakeexc()
- # END write exception handling
- proc.stdin.flush()
- if read_from_stdout:
- rval = proc.stdout.readline().strip()
- fprogress(filepath, True, item)
- return rval
-
- def iter_blobs(self, predicate = lambda t: True):
- """
- Returns
- Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob)
-
- ``predicate``
- Function(t) returning True if tuple(stage, Blob) should be yielded by the
- iterator. A default filter, the BlobFilter, allows you to yield blobs
- only if they match a given list of paths.
- """
- for entry in self.entries.itervalues():
- mode = self._index_mode_to_tree_index_mode(entry.mode)
- blob = Blob(self.repo, entry.sha, mode, entry.path)
- blob.size = entry.size
- output = (entry.stage, blob)
- if predicate(output):
- yield output
- # END for each entry
-
- def unmerged_blobs(self):
- """
- Returns
- Iterator yielding dict(path : list( tuple( stage, Blob, ...))), being
- a dictionary associating a path in the index with a list containing
- sorted stage/blob pairs
-
- Note:
- Blobs that have been removed in one side simply do not exist in the
- given stage. I.e. a file removed on the 'other' branch whose entries
- are at stage 3 will not have a stage 3 entry.
- """
- is_unmerged_blob = lambda t: t[0] != 0
- path_map = dict()
- for stage, blob in self.iter_blobs(is_unmerged_blob):
- path_map.setdefault(blob.path, list()).append((stage, blob))
- # END for each unmerged blob
- for l in path_map.itervalues():
- l.sort()
- return path_map
-
- @classmethod
- def get_entries_key(cls, *entry):
- """
- Returns
- Key suitable to be used for the index.entries dictionary
-
- ``entry``
- One instance of type BaseIndexEntry or the path and the stage
- """
- if len(entry) == 1:
- return (entry[0].path, entry[0].stage)
- else:
- return tuple(entry)
-
-
- def resolve_blobs(self, iter_blobs):
- """
- Resolve the blobs given in blob iterator. This will effectively remove the
- index entries of the respective path at all non-null stages and add the given
- blob as new stage null blob.
-
- For each path there may only be one blob, otherwise a ValueError will be raised
- claiming the path is already at stage 0.
-
- Raise
- ValueError if one of the blobs already existed at stage 0
-
- Returns:
- self
-
- Note
- You will have to write the index manually once you are done, i.e.
- index.resolve_blobs(blobs).write()
- """
- for blob in iter_blobs:
- stage_null_key = (blob.path, 0)
- if stage_null_key in self.entries:
- raise ValueError( "Path %r already exists at stage 0" % blob.path )
- # END assert blob is not stage 0 already
-
- # delete all possible stages
- for stage in (1, 2, 3):
- try:
- del( self.entries[(blob.path, stage)] )
- except KeyError:
- pass
- # END ignore key errors
- # END for each possible stage
-
- self.entries[stage_null_key] = IndexEntry.from_blob(blob)
- # END for each blob
-
- return self
-
- def update(self):
- """
- Reread the contents of our index file, discarding all cached information
- we might have.
-
- Note:
- This is a possibly dangerious operations as it will discard your changes
- to index.entries
-
- Returns
- self
- """
- self._delete_entries_cache()
- # allows to lazily reread on demand
- return self
-
- def write_tree(self, missing_ok=False):
- """
- Writes the Index in self to a corresponding Tree file into the repository
- object database and returns it as corresponding Tree object.
-
- ``missing_ok``
- If True, missing objects referenced by this index will not result
- in an error.
-
- Returns
- Tree object representing this index
- """
- index_path = self._index_path()
- tmp_index_mover = _TemporaryFileSwap(index_path)
-
- self.write(index_path, ignore_tree_extension_data=True)
- tree_sha = self.repo.git.write_tree(missing_ok=missing_ok)
-
- del(tmp_index_mover) # as soon as possible
-
- return Tree(self.repo, tree_sha, 0, '')
-
- def _process_diff_args(self, args):
- try:
- args.pop(args.index(self))
- except IndexError:
- pass
- # END remove self
- return args
-
-
- def _to_relative_path(self, path):
- """
- Return
- Version of path relative to our git directory or raise ValueError
- if it is not within our git direcotory
- """
- if not os.path.isabs(path):
- return path
- relative_path = path.replace(self.repo.working_tree_dir+os.sep, "")
- if relative_path == path:
- raise ValueError("Absolute path %r is not in git repository at %r" % (path,self.repo.working_tree_dir))
- return relative_path
-
- def _preprocess_add_items(self, items):
- """
- Split the items into two lists of path strings and BaseEntries.
- """
- paths = list()
- entries = list()
-
- for item in items:
- if isinstance(item, basestring):
- paths.append(self._to_relative_path(item))
- elif isinstance(item, Blob):
- entries.append(BaseIndexEntry.from_blob(item))
- elif isinstance(item, BaseIndexEntry):
- entries.append(item)
- else:
- raise TypeError("Invalid Type: %r" % item)
- # END for each item
- return (paths, entries)
-
-
- @clear_cache
- @default_index
- def add(self, items, force=True, fprogress=lambda *args: None):
- """
- Add files from the working tree, specific blobs or BaseIndexEntries
- to the index. The underlying index file will be written immediately, hence
- you should provide as many items as possible to minimize the amounts of writes
-
- ``items``
- Multiple types of items are supported, types can be mixed within one call.
- Different types imply a different handling. File paths may generally be
- relative or absolute.
-
- - path string
- strings denote a relative or absolute path into the repository pointing to
- an existing file, i.e. CHANGES, lib/myfile.ext, '/home/gitrepo/lib/myfile.ext'.
-
- Paths provided like this must exist. When added, they will be written
- into the object database.
-
- PathStrings may contain globs, such as 'lib/__init__*' or can be directories
- like 'lib', the latter ones will add all the files within the dirctory and
- subdirectories.
-
- This equals a straight git-add.
-
- They are added at stage 0
-
- - Blob object
- Blobs are added as they are assuming a valid mode is set.
- The file they refer to may or may not exist in the file system, but
- must be a path relative to our repository.
-
- If their sha is null ( 40*0 ), their path must exist in the file system
- as an object will be created from the data at the path.The handling
- now very much equals the way string paths are processed, except that
- the mode you have set will be kept. This allows you to create symlinks
- by settings the mode respectively and writing the target of the symlink
- directly into the file. This equals a default Linux-Symlink which
- is not dereferenced automatically, except that it can be created on
- filesystems not supporting it as well.
-
- Please note that globs or directories are not allowed in Blob objects.
-
- They are added at stage 0
-
+ def write(self, file_path = None, ignore_tree_extension_data=False):
+ """
+ Write the current state to our file path or to the given one
+
+ ``file_path``
+ If None, we will write to our stored file path from which we have
+ been initialized. Otherwise we write to the given file path.
+ Please note that this will change the file_path of this index to
+ the one you gave.
+
+ ``ignore_tree_extension_data``
+ If True, the TREE type extension data read in the index will not
+ be written to disk. Use this if you have altered the index and
+ would like to use git-write-tree afterwards to create a tree
+ representing your written changes.
+ If this data is present in the written index, git-write-tree
+ will instead write the stored/cached tree.
+ Alternatively, use IndexFile.write_tree() to handle this case
+ automatically
+
+ Returns
+ self
+
+ Note
+ Index writing based on the dulwich implementation
+ """
+ write_op = ConcurrentWriteOperation(file_path or self._file_path)
+ stream = write_op._begin_writing()
+
+ stream = SHA1Writer(stream)
+
+ # header
+ stream.write("DIRC")
+ stream.write(struct.pack(">LL", self.version, len(self.entries)))
+
+ # body
+ entries_sorted = self.entries.values()
+ entries_sorted.sort(key=lambda e: (e[3], e[2])) # use path/stage as sort key
+ for entry in entries_sorted:
+ self._write_cache_entry(stream, entry)
+ # END for each entry
+
+ stored_ext_data = None
+ if ignore_tree_extension_data and self._extension_data and self._extension_data[:4] == 'TREE':
+ stored_ext_data = self._extension_data
+ self._extension_data = ''
+ # END extension data special handling
+
+ # write previously cached extensions data
+ stream.write(self._extension_data)
+
+ if stored_ext_data:
+ self._extension_data = stored_ext_data
+ # END reset previous ext data
+
+ # write the sha over the content
+ stream.write_sha()
+ write_op._end_writing()
+
+ # make sure we represent what we have written
+ if file_path is not None:
+ self._file_path = file_path
+
+ @clear_cache
+ @default_index
+ def merge_tree(self, rhs, base=None):
+ """Merge the given rhs treeish into the current index, possibly taking
+ a common base treeish into account.
+
+ As opposed to the from_tree_ method, this allows you to use an already
+ existing tree as the left side of the merge
+
+ ``rhs``
+ treeish reference pointing to the 'other' side of the merge.
+
+ ``base``
+ optional treeish reference pointing to the common base of 'rhs' and
+ this index which equals lhs
+
+ Returns
+ self ( containing the merge and possibly unmerged entries in case of
+ conflicts )
+
+ Raise
+ GitCommandError in case there is a merge conflict. The error will
+ be raised at the first conflicting path. If you want to have proper
+ merge resolution to be done by yourself, you have to commit the changed
+ index ( or make a valid tree from it ) and retry with a three-way
+ index.from_tree call.
+ """
+ # -i : ignore working tree status
+ # --aggressive : handle more merge cases
+ # -m : do an actual merge
+ args = ["--aggressive", "-i", "-m"]
+ if base is not None:
+ args.append(base)
+ args.append(rhs)
+
+ self.repo.git.read_tree(args)
+ return self
+
+ @classmethod
+ def from_tree(cls, repo, *treeish, **kwargs):
+ """
+ Merge the given treeish revisions into a new index which is returned.
+ The original index will remain unaltered
+
+ ``repo``
+ The repository treeish are located in.
+
+ ``*treeish``
+ One, two or three Tree Objects or Commits. The result changes according to the
+ amount of trees.
+ If 1 Tree is given, it will just be read into a new index
+ If 2 Trees are given, they will be merged into a new index using a
+ two way merge algorithm. Tree 1 is the 'current' tree, tree 2 is the 'other'
+ one. It behaves like a fast-forward.
+ If 3 Trees are given, a 3-way merge will be performed with the first tree
+ being the common ancestor of tree 2 and tree 3. Tree 2 is the 'current' tree,
+ tree 3 is the 'other' one
+
+ ``**kwargs``
+ Additional arguments passed to git-read-tree
+
+ Returns
+ New IndexFile instance. It will point to a temporary index location which
+ does not exist anymore. If you intend to write such a merged Index, supply
+ an alternate file_path to its 'write' method.
+
+ Note:
+ In the three-way merge case, --aggressive will be specified to automatically
+ resolve more cases in a commonly correct manner. Specify trivial=True as kwarg
+ to override that.
+
+ As the underlying git-read-tree command takes into account the current index,
+ it will be temporarily moved out of the way to assure there are no unsuspected
+ interferences.
+ """
+ if len(treeish) == 0 or len(treeish) > 3:
+ raise ValueError("Please specify between 1 and 3 treeish, got %i" % len(treeish))
+
+ arg_list = list()
+ # ignore that working tree and index possibly are out of date
+ if len(treeish)>1:
+ # drop unmerged entries when reading our index and merging
+ arg_list.append("--reset")
+ # handle non-trivial cases the way a real merge does
+ arg_list.append("--aggressive")
+ # END merge handling
+
+ # tmp file created in git home directory to be sure renaming
+ # works - /tmp/ dirs could be on another device
+ tmp_index = tempfile.mktemp('','',repo.git_dir)
+ arg_list.append("--index-output=%s" % tmp_index)
+ arg_list.extend(treeish)
+
+ # move current index out of the way - otherwise the merge may fail
+ # as it considers existing entries. moving it essentially clears the index.
+ # Unfortunately there is no 'soft' way to do it.
+ # The _TemporaryFileSwap assure the original file get put back
+ index_handler = _TemporaryFileSwap(join_path_native(repo.git_dir, 'index'))
+ try:
+ repo.git.read_tree(*arg_list, **kwargs)
+ index = cls(repo, tmp_index)
+ index.entries # force it to read the file as we will delete the temp-file
+ del(index_handler) # release as soon as possible
+ finally:
+ if os.path.exists(tmp_index):
+ os.remove(tmp_index)
+ # END index merge handling
+
+ return index
+
+ @classmethod
+ def _index_mode_to_tree_index_mode(cls, index_mode):
+ """
+ Cleanup a index_mode value.
+ This will return a index_mode that can be stored in a tree object.
+
+ ``index_mode``
+ Index_mode to clean up.
+ """
+ if stat.S_ISLNK(index_mode):
+ return stat.S_IFLNK
+ elif stat.S_ISDIR(index_mode):
+ return stat.S_IFDIR
+ elif stat.S_IFMT(index_mode) == cls.S_IFGITLINK:
+ return cls.S_IFGITLINK
+ ret = stat.S_IFREG | 0644
+ ret |= (index_mode & 0111)
+ return ret
+
+
+ # UTILITIES
+ def _iter_expand_paths(self, paths):
+ """Expand the directories in list of paths to the corresponding paths accordingly,
+
+ Note: git will add items multiple times even if a glob overlapped
+ with manually specified paths or if paths where specified multiple
+ times - we respect that and do not prune"""
+ def raise_exc(e):
+ raise e
+ r = self.repo.working_tree_dir
+ rs = r + '/'
+ for path in paths:
+ abs_path = path
+ if not os.path.isabs(abs_path):
+ abs_path = os.path.join(r, path)
+ # END make absolute path
+
+ # resolve globs if possible
+ if '?' in path or '*' in path or '[' in path:
+ for f in self._iter_expand_paths(glob.glob(abs_path)):
+ yield f.replace(rs, '')
+ continue
+ # END glob handling
+ try:
+ for root, dirs, files in os.walk(abs_path, onerror=raise_exc):
+ for rela_file in files:
+ # add relative paths only
+ yield os.path.join(root.replace(rs, ''), rela_file)
+ # END for each file in subdir
+ # END for each subdirectory
+ except OSError:
+ # was a file or something that could not be iterated
+ yield path.replace(rs, '')
+ # END path exception handling
+ # END for each path
+
+ def _write_path_to_stdin(self, proc, filepath, item, fmakeexc, fprogress, read_from_stdout=True):
+ """Write path to proc.stdin and make sure it processes the item, including progress.
+ @return: stdout string
+ @param read_from_stdout: if True, proc.stdout will be read after the item
+ was sent to stdin. In that case, it will return None
+ @note: There is a bug in git-update-index that prevents it from sending
+ reports just in time. This is why we have a version that tries to
+ read stdout and one which doesn't. In fact, the stdout is not
+ important as the piped-in files are processed anyway and just in time"""
+ fprogress(filepath, False, item)
+ rval = None
+ try:
+ proc.stdin.write("%s\n" % filepath)
+ except IOError:
+ # pipe broke, usually because some error happend
+ raise fmakeexc()
+ # END write exception handling
+ proc.stdin.flush()
+ if read_from_stdout:
+ rval = proc.stdout.readline().strip()
+ fprogress(filepath, True, item)
+ return rval
+
+ def iter_blobs(self, predicate = lambda t: True):
+ """
+ Returns
+ Iterator yielding tuples of Blob objects and stages, tuple(stage, Blob)
+
+ ``predicate``
+ Function(t) returning True if tuple(stage, Blob) should be yielded by the
+ iterator. A default filter, the BlobFilter, allows you to yield blobs
+ only if they match a given list of paths.
+ """
+ for entry in self.entries.itervalues():
+ mode = self._index_mode_to_tree_index_mode(entry.mode)
+ blob = Blob(self.repo, entry.sha, mode, entry.path)
+ blob.size = entry.size
+ output = (entry.stage, blob)
+ if predicate(output):
+ yield output
+ # END for each entry
+
+ def unmerged_blobs(self):
+ """
+ Returns
+ Iterator yielding dict(path : list( tuple( stage, Blob, ...))), being
+ a dictionary associating a path in the index with a list containing
+ sorted stage/blob pairs
+
+ Note:
+ Blobs that have been removed in one side simply do not exist in the
+ given stage. I.e. a file removed on the 'other' branch whose entries
+ are at stage 3 will not have a stage 3 entry.
+ """
+ is_unmerged_blob = lambda t: t[0] != 0
+ path_map = dict()
+ for stage, blob in self.iter_blobs(is_unmerged_blob):
+ path_map.setdefault(blob.path, list()).append((stage, blob))
+ # END for each unmerged blob
+ for l in path_map.itervalues():
+ l.sort()
+ return path_map
+
+ @classmethod
+ def get_entries_key(cls, *entry):
+ """
+ Returns
+ Key suitable to be used for the index.entries dictionary
+
+ ``entry``
+ One instance of type BaseIndexEntry or the path and the stage
+ """
+ if len(entry) == 1:
+ return (entry[0].path, entry[0].stage)
+ else:
+ return tuple(entry)
+
+
+ def resolve_blobs(self, iter_blobs):
+ """
+ Resolve the blobs given in blob iterator. This will effectively remove the
+ index entries of the respective path at all non-null stages and add the given
+ blob as new stage null blob.
+
+ For each path there may only be one blob, otherwise a ValueError will be raised
+ claiming the path is already at stage 0.
+
+ Raise
+ ValueError if one of the blobs already existed at stage 0
+
+ Returns:
+ self
+
+ Note
+ You will have to write the index manually once you are done, i.e.
+ index.resolve_blobs(blobs).write()
+ """
+ for blob in iter_blobs:
+ stage_null_key = (blob.path, 0)
+ if stage_null_key in self.entries:
+ raise ValueError( "Path %r already exists at stage 0" % blob.path )
+ # END assert blob is not stage 0 already
+
+ # delete all possible stages
+ for stage in (1, 2, 3):
+ try:
+ del( self.entries[(blob.path, stage)] )
+ except KeyError:
+ pass
+ # END ignore key errors
+ # END for each possible stage
+
+ self.entries[stage_null_key] = IndexEntry.from_blob(blob)
+ # END for each blob
+
+ return self
+
+ def update(self):
+ """
+ Reread the contents of our index file, discarding all cached information
+ we might have.
+
+ Note:
+ This is a possibly dangerious operations as it will discard your changes
+ to index.entries
+
+ Returns
+ self
+ """
+ self._delete_entries_cache()
+ # allows to lazily reread on demand
+ return self
+
+ def write_tree(self, missing_ok=False):
+ """
+ Writes the Index in self to a corresponding Tree file into the repository
+ object database and returns it as corresponding Tree object.
+
+ ``missing_ok``
+ If True, missing objects referenced by this index will not result
+ in an error.
+
+ Returns
+ Tree object representing this index
+ """
+ index_path = self._index_path()
+ tmp_index_mover = _TemporaryFileSwap(index_path)
+
+ self.write(index_path, ignore_tree_extension_data=True)
+ tree_sha = self.repo.git.write_tree(missing_ok=missing_ok)
+
+ del(tmp_index_mover) # as soon as possible
+
+ return Tree(self.repo, tree_sha, 0, '')
+
+ def _process_diff_args(self, args):
+ try:
+ args.pop(args.index(self))
+ except IndexError:
+ pass
+ # END remove self
+ return args
+
+
+ def _to_relative_path(self, path):
+ """
+ Return
+ Version of path relative to our git directory or raise ValueError
+ if it is not within our git direcotory
+ """
+ if not os.path.isabs(path):
+ return path
+ relative_path = path.replace(self.repo.working_tree_dir+os.sep, "")
+ if relative_path == path:
+ raise ValueError("Absolute path %r is not in git repository at %r" % (path,self.repo.working_tree_dir))
+ return relative_path
+
+ def _preprocess_add_items(self, items):
+ """
+ Split the items into two lists of path strings and BaseEntries.
+ """
+ paths = list()
+ entries = list()
+
+ for item in items:
+ if isinstance(item, basestring):
+ paths.append(self._to_relative_path(item))
+ elif isinstance(item, Blob):
+ entries.append(BaseIndexEntry.from_blob(item))
+ elif isinstance(item, BaseIndexEntry):
+ entries.append(item)
+ else:
+ raise TypeError("Invalid Type: %r" % item)
+ # END for each item
+ return (paths, entries)
+
+
+ @clear_cache
+ @default_index
+ def add(self, items, force=True, fprogress=lambda *args: None):
+ """
+ Add files from the working tree, specific blobs or BaseIndexEntries
+ to the index. The underlying index file will be written immediately, hence
+ you should provide as many items as possible to minimize the amounts of writes
+
+ ``items``
+ Multiple types of items are supported, types can be mixed within one call.
+ Different types imply a different handling. File paths may generally be
+ relative or absolute.
+
+ - path string
+ strings denote a relative or absolute path into the repository pointing to
+ an existing file, i.e. CHANGES, lib/myfile.ext, '/home/gitrepo/lib/myfile.ext'.
+
+ Paths provided like this must exist. When added, they will be written
+ into the object database.
+
+ PathStrings may contain globs, such as 'lib/__init__*' or can be directories
+ like 'lib', the latter ones will add all the files within the dirctory and
+ subdirectories.
+
+ This equals a straight git-add.
+
+ They are added at stage 0
+
+ - Blob object
+ Blobs are added as they are assuming a valid mode is set.
+ The file they refer to may or may not exist in the file system, but
+ must be a path relative to our repository.
+
+ If their sha is null ( 40*0 ), their path must exist in the file system
+ as an object will be created from the data at the path.The handling
+ now very much equals the way string paths are processed, except that
+ the mode you have set will be kept. This allows you to create symlinks
+ by settings the mode respectively and writing the target of the symlink
+ directly into the file. This equals a default Linux-Symlink which
+ is not dereferenced automatically, except that it can be created on
+ filesystems not supporting it as well.
+
+ Please note that globs or directories are not allowed in Blob objects.
+
+ They are added at stage 0
+
- BaseIndexEntry or type
Handling equals the one of Blob objects, but the stage may be
explicitly set.
-
- ``force``
- If True, otherwise ignored or excluded files will be
- added anyway.
- As opposed to the git-add command, we enable this flag by default
- as the API user usually wants the item to be added even though
- they might be excluded.
-
- ``fprogress``
- Function with signature f(path, done=False, item=item) called for each
- path to be added, once once it is about to be added where done==False
- and once after it was added where done=True.
- item is set to the actual item we handle, either a Path or a BaseIndexEntry
- Please note that the processed path is not guaranteed to be present
- in the index already as the index is currently being processed.
-
- Returns
- List(BaseIndexEntries) representing the entries just actually added.
-
- Raises
- GitCommandError if a supplied Path did not exist. Please note that BaseIndexEntry
- Objects that do not have a null sha will be added even if their paths
- do not exist.
- """
- # sort the entries into strings and Entries, Blobs are converted to entries
- # automatically
- # paths can be git-added, for everything else we use git-update-index
- entries_added = list()
- paths, entries = self._preprocess_add_items(items)
-
-
- # HANDLE PATHS
- if paths:
- # to get suitable progress information, pipe paths to stdin
- args = ("--add", "--replace", "--verbose", "--stdin")
- proc = self.repo.git.update_index(*args, **{'as_process':True, 'istream':subprocess.PIPE})
- make_exc = lambda : GitCommandError(("git-update-index",)+args, 128, proc.stderr.read())
- added_files = list()
-
- for filepath in self._iter_expand_paths(paths):
- self._write_path_to_stdin(proc, filepath, filepath, make_exc, fprogress, read_from_stdout=False)
- added_files.append(filepath)
- # END for each filepath
- self._flush_stdin_and_wait(proc, ignore_stdout=True) # ignore stdout
-
- # force rereading our entries once it is all done
- self._delete_entries_cache()
- entries_added.extend(self.entries[(f,0)] for f in added_files)
- # END path handling
-
- # HANDLE ENTRIES
- if entries:
- null_mode_entries = [ e for e in entries if e.mode == 0 ]
- if null_mode_entries:
- raise ValueError("At least one Entry has a null-mode - please use index.remove to remove files for clarity")
- # END null mode should be remove
-
- # HANLDE ENTRY OBJECT CREATION
- # create objects if required, otherwise go with the existing shas
- null_entries_indices = [ i for i,e in enumerate(entries) if e.sha == Object.NULL_HEX_SHA ]
- if null_entries_indices:
- # creating object ids is the time consuming part. Hence we will
- # send progress for these now.
- args = ("-w", "--stdin-paths")
- proc = self.repo.git.hash_object(*args, **{'istream':subprocess.PIPE, 'as_process':True})
- make_exc = lambda : GitCommandError(("git-hash-object",)+args, 128, proc.stderr.read())
- obj_ids = list()
- for ei in null_entries_indices:
- entry = entries[ei]
- obj_ids.append(self._write_path_to_stdin(proc, entry.path, entry, make_exc, fprogress))
- # END for each entry index
- assert len(obj_ids) == len(null_entries_indices), "git-hash-object did not produce all requested objects: want %i, got %i" % ( len(null_entries_indices), len(obj_ids) )
-
- # update IndexEntries with new object id
- for i,new_sha in zip(null_entries_indices, obj_ids):
- e = entries[i]
- new_entry = BaseIndexEntry((e.mode, new_sha, e.stage, e.path))
- entries[i] = new_entry
- # END for each index
- # END null_entry handling
-
- # feed pure entries to stdin
- proc = self.repo.git.update_index(index_info=True, istream=subprocess.PIPE, as_process=True)
- for i, entry in enumerate(entries):
- progress_sent = i in null_entries_indices
- if not progress_sent:
- fprogress(entry.path, False, entry)
- # it cannot handle too-many newlines in this mode
- if i != 0:
- proc.stdin.write('\n')
- proc.stdin.write(str(entry))
- proc.stdin.flush()
- if not progress_sent:
- fprogress(entry.path, True, entry)
- # END for each enty
- self._flush_stdin_and_wait(proc, ignore_stdout=True)
- entries_added.extend(entries)
- # END if there are base entries
-
- return entries_added
-
- def _items_to_rela_paths(self, items):
- """Returns a list of repo-relative paths from the given items which
- may be absolute or relative paths, entries or blobs"""
- paths = list()
- for item in items:
- if isinstance(item, (BaseIndexEntry,Blob)):
- paths.append(self._to_relative_path(item.path))
- elif isinstance(item, basestring):
- paths.append(self._to_relative_path(item))
- else:
- raise TypeError("Invalid item type: %r" % item)
- # END for each item
- return paths
-
- @clear_cache
- @default_index
- def remove(self, items, working_tree=False, **kwargs):
- """
- Remove the given items from the index and optionally from
- the working tree as well.
-
- ``items``
- Multiple types of items are supported which may be be freely mixed.
-
- - path string
- Remove the given path at all stages. If it is a directory, you must
- specify the r=True keyword argument to remove all file entries
- below it. If absolute paths are given, they will be converted
- to a path relative to the git repository directory containing
- the working tree
-
- The path string may include globs, such as *.c.
-
- - Blob object
- Only the path portion is used in this case.
-
- - BaseIndexEntry or compatible type
- The only relevant information here Yis the path. The stage is ignored.
-
- ``working_tree``
- If True, the entry will also be removed from the working tree, physically
- removing the respective file. This may fail if there are uncommited changes
- in it.
-
- ``**kwargs``
- Additional keyword arguments to be passed to git-rm, such
- as 'r' to allow recurive removal of
-
- Returns
- List(path_string, ...) list of repository relative paths that have
- been removed effectively.
- This is interesting to know in case you have provided a directory or
- globs. Paths are relative to the repository.
- """
- args = list()
- if not working_tree:
- args.append("--cached")
- args.append("--")
-
- # preprocess paths
- paths = self._items_to_rela_paths(items)
- removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
-
- # process output to gain proper paths
- # rm 'path'
- return [ p[4:-1] for p in removed_paths ]
-
- @clear_cache
- @default_index
- def move(self, items, skip_errors=False, **kwargs):
- """
- Rename/move the items, whereas the last item is considered the destination of
- the move operation. If the destination is a file, the first item ( of two )
- must be a file as well. If the destination is a directory, it may be preceeded
- by one or more directories or files.
-
- The working tree will be affected in non-bare repositories.
-
- ``items``
- Multiple types of items are supported, please see the 'remove' method
- for reference.
- ``skip_errors``
- If True, errors such as ones resulting from missing source files will
- be skpped.
- ``**kwargs``
- Additional arguments you would like to pass to git-mv, such as dry_run
- or force.
-
- Returns
- List(tuple(source_path_string, destination_path_string), ...)
- A list of pairs, containing the source file moved as well as its
- actual destination. Relative to the repository root.
-
- Raises
- ValueErorr: If only one item was given
- GitCommandError: If git could not handle your request
- """
- args = list()
- if skip_errors:
- args.append('-k')
-
- paths = self._items_to_rela_paths(items)
- if len(paths) < 2:
- raise ValueError("Please provide at least one source and one destination of the move operation")
-
- was_dry_run = kwargs.pop('dry_run', kwargs.pop('n', None))
- kwargs['dry_run'] = True
-
- # first execute rename in dryrun so the command tells us what it actually does
- # ( for later output )
- out = list()
- mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
-
- # parse result - first 0:n/2 lines are 'checking ', the remaining ones
- # are the 'renaming' ones which we parse
- for ln in xrange(len(mvlines)/2, len(mvlines)):
- tokens = mvlines[ln].split(' to ')
- assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
-
- # [0] = Renaming x
- # [1] = y
- out.append((tokens[0][9:], tokens[1]))
- # END for each line to parse
-
- # either prepare for the real run, or output the dry-run result
- if was_dry_run:
- return out
- # END handle dryrun
-
-
- # now apply the actual operation
- kwargs.pop('dry_run')
- self.repo.git.mv(args, paths, **kwargs)
-
- return out
-
+
+ ``force``
+ If True, otherwise ignored or excluded files will be
+ added anyway.
+ As opposed to the git-add command, we enable this flag by default
+ as the API user usually wants the item to be added even though
+ they might be excluded.
+
+ ``fprogress``
+ Function with signature f(path, done=False, item=item) called for each
+ path to be added, once once it is about to be added where done==False
+ and once after it was added where done=True.
+ item is set to the actual item we handle, either a Path or a BaseIndexEntry
+ Please note that the processed path is not guaranteed to be present
+ in the index already as the index is currently being processed.
+
+ Returns
+ List(BaseIndexEntries) representing the entries just actually added.
+
+ Raises
+ GitCommandError if a supplied Path did not exist. Please note that BaseIndexEntry
+ Objects that do not have a null sha will be added even if their paths
+ do not exist.
+ """
+ # sort the entries into strings and Entries, Blobs are converted to entries
+ # automatically
+ # paths can be git-added, for everything else we use git-update-index
+ entries_added = list()
+ paths, entries = self._preprocess_add_items(items)
+
+
+ # HANDLE PATHS
+ if paths:
+ # to get suitable progress information, pipe paths to stdin
+ args = ("--add", "--replace", "--verbose", "--stdin")
+ proc = self.repo.git.update_index(*args, **{'as_process':True, 'istream':subprocess.PIPE})
+ make_exc = lambda : GitCommandError(("git-update-index",)+args, 128, proc.stderr.read())
+ added_files = list()
+
+ for filepath in self._iter_expand_paths(paths):
+ self._write_path_to_stdin(proc, filepath, filepath, make_exc, fprogress, read_from_stdout=False)
+ added_files.append(filepath)
+ # END for each filepath
+ self._flush_stdin_and_wait(proc, ignore_stdout=True) # ignore stdout
+
+ # force rereading our entries once it is all done
+ self._delete_entries_cache()
+ entries_added.extend(self.entries[(f,0)] for f in added_files)
+ # END path handling
+
+ # HANDLE ENTRIES
+ if entries:
+ null_mode_entries = [ e for e in entries if e.mode == 0 ]
+ if null_mode_entries:
+ raise ValueError("At least one Entry has a null-mode - please use index.remove to remove files for clarity")
+ # END null mode should be remove
+
+ # HANLDE ENTRY OBJECT CREATION
+ # create objects if required, otherwise go with the existing shas
+ null_entries_indices = [ i for i,e in enumerate(entries) if e.sha == Object.NULL_HEX_SHA ]
+ if null_entries_indices:
+ # creating object ids is the time consuming part. Hence we will
+ # send progress for these now.
+ args = ("-w", "--stdin-paths")
+ proc = self.repo.git.hash_object(*args, **{'istream':subprocess.PIPE, 'as_process':True})
+ make_exc = lambda : GitCommandError(("git-hash-object",)+args, 128, proc.stderr.read())
+ obj_ids = list()
+ for ei in null_entries_indices:
+ entry = entries[ei]
+ obj_ids.append(self._write_path_to_stdin(proc, entry.path, entry, make_exc, fprogress))
+ # END for each entry index
+ assert len(obj_ids) == len(null_entries_indices), "git-hash-object did not produce all requested objects: want %i, got %i" % ( len(null_entries_indices), len(obj_ids) )
+
+ # update IndexEntries with new object id
+ for i,new_sha in zip(null_entries_indices, obj_ids):
+ e = entries[i]
+ new_entry = BaseIndexEntry((e.mode, new_sha, e.stage, e.path))
+ entries[i] = new_entry
+ # END for each index
+ # END null_entry handling
+
+ # feed pure entries to stdin
+ proc = self.repo.git.update_index(index_info=True, istream=subprocess.PIPE, as_process=True)
+ for i, entry in enumerate(entries):
+ progress_sent = i in null_entries_indices
+ if not progress_sent:
+ fprogress(entry.path, False, entry)
+ # it cannot handle too-many newlines in this mode
+ if i != 0:
+ proc.stdin.write('\n')
+ proc.stdin.write(str(entry))
+ proc.stdin.flush()
+ if not progress_sent:
+ fprogress(entry.path, True, entry)
+ # END for each enty
+ self._flush_stdin_and_wait(proc, ignore_stdout=True)
+ entries_added.extend(entries)
+ # END if there are base entries
+
+ return entries_added
+
+ def _items_to_rela_paths(self, items):
+ """Returns a list of repo-relative paths from the given items which
+ may be absolute or relative paths, entries or blobs"""
+ paths = list()
+ for item in items:
+ if isinstance(item, (BaseIndexEntry,Blob)):
+ paths.append(self._to_relative_path(item.path))
+ elif isinstance(item, basestring):
+ paths.append(self._to_relative_path(item))
+ else:
+ raise TypeError("Invalid item type: %r" % item)
+ # END for each item
+ return paths
+
+ @clear_cache
+ @default_index
+ def remove(self, items, working_tree=False, **kwargs):
+ """
+ Remove the given items from the index and optionally from
+ the working tree as well.
+
+ ``items``
+ Multiple types of items are supported which may be be freely mixed.
+
+ - path string
+ Remove the given path at all stages. If it is a directory, you must
+ specify the r=True keyword argument to remove all file entries
+ below it. If absolute paths are given, they will be converted
+ to a path relative to the git repository directory containing
+ the working tree
+
+ The path string may include globs, such as *.c.
+
+ - Blob object
+ Only the path portion is used in this case.
+
+ - BaseIndexEntry or compatible type
+ The only relevant information here Yis the path. The stage is ignored.
+
+ ``working_tree``
+ If True, the entry will also be removed from the working tree, physically
+ removing the respective file. This may fail if there are uncommited changes
+ in it.
+
+ ``**kwargs``
+ Additional keyword arguments to be passed to git-rm, such
+ as 'r' to allow recurive removal of
+
+ Returns
+ List(path_string, ...) list of repository relative paths that have
+ been removed effectively.
+ This is interesting to know in case you have provided a directory or
+ globs. Paths are relative to the repository.
+ """
+ args = list()
+ if not working_tree:
+ args.append("--cached")
+ args.append("--")
+
+ # preprocess paths
+ paths = self._items_to_rela_paths(items)
+ removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
+
+ # process output to gain proper paths
+ # rm 'path'
+ return [ p[4:-1] for p in removed_paths ]
+
+ @clear_cache
+ @default_index
+ def move(self, items, skip_errors=False, **kwargs):
+ """
+ Rename/move the items, whereas the last item is considered the destination of
+ the move operation. If the destination is a file, the first item ( of two )
+ must be a file as well. If the destination is a directory, it may be preceeded
+ by one or more directories or files.
+
+ The working tree will be affected in non-bare repositories.
+
+ ``items``
+ Multiple types of items are supported, please see the 'remove' method
+ for reference.
+ ``skip_errors``
+ If True, errors such as ones resulting from missing source files will
+ be skpped.
+ ``**kwargs``
+ Additional arguments you would like to pass to git-mv, such as dry_run
+ or force.
+
+ Returns
+ List(tuple(source_path_string, destination_path_string), ...)
+ A list of pairs, containing the source file moved as well as its
+ actual destination. Relative to the repository root.
+
+ Raises
+ ValueErorr: If only one item was given
+ GitCommandError: If git could not handle your request
+ """
+ args = list()
+ if skip_errors:
+ args.append('-k')
+
+ paths = self._items_to_rela_paths(items)
+ if len(paths) < 2:
+ raise ValueError("Please provide at least one source and one destination of the move operation")
+
+ was_dry_run = kwargs.pop('dry_run', kwargs.pop('n', None))
+ kwargs['dry_run'] = True
+
+ # first execute rename in dryrun so the command tells us what it actually does
+ # ( for later output )
+ out = list()
+ mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
+
+ # parse result - first 0:n/2 lines are 'checking ', the remaining ones
+ # are the 'renaming' ones which we parse
+ for ln in xrange(len(mvlines)/2, len(mvlines)):
+ tokens = mvlines[ln].split(' to ')
+ assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
+
+ # [0] = Renaming x
+ # [1] = y
+ out.append((tokens[0][9:], tokens[1]))
+ # END for each line to parse
+
+ # either prepare for the real run, or output the dry-run result
+ if was_dry_run:
+ return out
+ # END handle dryrun
+
+
+ # now apply the actual operation
+ kwargs.pop('dry_run')
+ self.repo.git.mv(args, paths, **kwargs)
+
+ return out
+
- @default_index
- def commit(self, message, parent_commits=None, head=True):
- """
- Commit the current default index file, creating a commit object.
-
- For more information on the arguments, see tree.commit.
-
- ``NOTE``:
- If you have manually altered the .entries member of this instance,
- don't forget to write() your changes to disk beforehand.
-
- Returns
- Commit object representing the new commit
- """
- tree_sha = self.repo.git.write_tree()
- return Commit.create_from_tree(self.repo, tree_sha, message, parent_commits, head)
-
- @classmethod
- def _flush_stdin_and_wait(cls, proc, ignore_stdout = False):
- proc.stdin.flush()
- proc.stdin.close()
- stdout = ''
- if not ignore_stdout:
- stdout = proc.stdout.read()
- proc.stdout.close()
- proc.wait()
- return stdout
-
- @default_index
- def checkout(self, paths=None, force=False, fprogress=lambda *args: None, **kwargs):
- """
- Checkout the given paths or all files from the version known to the index into
- the working tree.
-
- ``paths``
- If None, all paths in the index will be checked out. Otherwise an iterable
- of relative or absolute paths or a single path pointing to files or directories
- in the index is expected.
-
- ``force``
- If True, existing files will be overwritten even if they contain local modifications.
- If False, these will trigger a CheckoutError.
-
- ``fprogress``
- see Index.add_ for signature and explanation.
- The provided progress information will contain None as path and item if no
- explicit paths are given. Otherwise progress information will be send
- prior and after a file has been checked out
-
- ``**kwargs``
- Additional arguments to be pasesd to git-checkout-index
-
- Returns
- iterable yielding paths to files which have been checked out and are
- guaranteed to match the version stored in the index
-
- Raise CheckoutError
- If at least one file failed to be checked out. This is a summary,
- hence it will checkout as many files as it can anyway.
- If one of files or directories do not exist in the index
- ( as opposed to the original git command who ignores them ).
- Raise GitCommandError if error lines could not be parsed - this truly is
- an exceptional state
- """
- args = ["--index"]
- if force:
- args.append("--force")
-
- def handle_stderr(proc, iter_checked_out_files):
- stderr = proc.stderr.read()
- if not stderr:
- return
- # line contents:
- # git-checkout-index: this already exists
- failed_files = list()
- failed_reasons = list()
- unknown_lines = list()
- endings = (' already exists', ' is not in the cache', ' does not exist at stage', ' is unmerged')
- for line in stderr.splitlines():
- if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "):
- is_a_dir = " is a directory"
- unlink_issue = "unable to unlink old '"
- if line.endswith(is_a_dir):
- failed_files.append(line[:-len(is_a_dir)])
- failed_reasons.append(is_a_dir)
- elif line.startswith(unlink_issue):
- failed_files.append(line[len(unlink_issue):line.rfind("'")])
- failed_reasons.append(unlink_issue)
- else:
- unknown_lines.append(line)
- continue
- # END special lines parsing
-
- for e in endings:
- if line.endswith(e):
- failed_files.append(line[20:-len(e)])
- failed_reasons.append(e)
- break
- # END if ending matches
- # END for each possible ending
- # END for each line
- if unknown_lines:
- raise GitCommandError(("git-checkout-index", ), 128, stderr)
- if failed_files:
- valid_files = list(set(iter_checked_out_files) - set(failed_files))
- raise CheckoutError("Some files could not be checked out from the index due to local modifications", failed_files, valid_files, failed_reasons)
- # END stderr handler
-
-
- if paths is None:
- args.append("--all")
- kwargs['as_process'] = 1
- fprogress(None, False, None)
- proc = self.repo.git.checkout_index(*args, **kwargs)
- proc.wait()
- fprogress(None, True, None)
- rval_iter = ( e.path for e in self.entries.itervalues() )
- handle_stderr(proc, rval_iter)
- return rval_iter
- else:
- if isinstance(paths, basestring):
- paths = [paths]
-
- args.append("--stdin")
- kwargs['as_process'] = True
- kwargs['istream'] = subprocess.PIPE
- proc = self.repo.git.checkout_index(args, **kwargs)
- make_exc = lambda : GitCommandError(("git-checkout-index",)+tuple(args), 128, proc.stderr.read())
- checked_out_files = list()
- for path in paths:
- path = self._to_relative_path(path)
- # if the item is not in the index, it could be a directory
- path_is_directory = False
- try:
- self.entries[(path, 0)]
- except KeyError:
- dir = path
- if not dir.endswith('/'):
- dir += '/'
- for entry in self.entries.itervalues():
- if entry.path.startswith(dir):
- p = entry.path
- self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
- checked_out_files.append(p)
- path_is_directory = True
- # END if entry is in directory
- # END for each entry
- # END path exception handlnig
-
- if not path_is_directory:
- self._write_path_to_stdin(proc, path, path, make_exc, fprogress, read_from_stdout=False)
- checked_out_files.append(path)
- # END path is a file
- # END for each path
- self._flush_stdin_and_wait(proc, ignore_stdout=True)
-
- handle_stderr(proc, checked_out_files)
- return checked_out_files
- # END directory handling
- # END paths handling
- assert "Should not reach this point"
-
- @clear_cache
- @default_index
- def reset(self, commit='HEAD', working_tree=False, paths=None, head=False, **kwargs):
- """
- Reset the index to reflect the tree at the given commit. This will not
- adjust our HEAD reference as opposed to HEAD.reset by default.
-
- ``commit``
- Revision, Reference or Commit specifying the commit we should represent.
- If you want to specify a tree only, use IndexFile.from_tree and overwrite
- the default index.
-
- ``working_tree``
- If True, the files in the working tree will reflect the changed index.
- If False, the working tree will not be touched
- Please note that changes to the working copy will be discarded without
- warning !
-
- ``head``
- If True, the head will be set to the given commit. This is False by default,
- but if True, this method behaves like HEAD.reset.
-
- ``**kwargs``
- Additional keyword arguments passed to git-reset
-
- Returns
- self
- """
- cur_head = self.repo.head
- prev_commit = cur_head.commit
-
- # reset to get the tree/working copy
- cur_head.reset(commit, index=True, working_tree=working_tree, paths=paths, **kwargs)
-
- # put the head back, possibly
- if not head:
- cur_head.reset(prev_commit, index=False, working_tree=False)
- # END reset head
-
- return self
-
- @default_index
- def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwargs):
- """
- Diff this index against the working copy or a Tree or Commit object
-
- For a documentation of the parameters and return values, see
- Diffable.diff
-
- Note
- Will only work with indices that represent the default git index as
- they have not been initialized with a stream.
- """
- # index against index is always empty
- if other is self.Index:
- return diff.DiffIndex()
-
- # index against anything but None is a reverse diff with the respective
- # item. Handle existing -R flags properly. Transform strings to the object
- # so that we can call diff on it
- if isinstance(other, basestring):
- other = Object.new(self.repo, other)
- # END object conversion
-
- if isinstance(other, Object):
- # invert the existing R flag
- cur_val = kwargs.get('R', False)
- kwargs['R'] = not cur_val
- return other.diff(self.Index, paths, create_patch, **kwargs)
- # END diff against other item handlin
-
- # if other is not None here, something is wrong
- if other is not None:
- raise ValueError( "other must be None, Diffable.Index, a Tree or Commit, was %r" % other )
-
- # diff against working copy - can be handled by superclass natively
- return super(IndexFile, self).diff(other, paths, create_patch, **kwargs)
-
+ @default_index
+ def commit(self, message, parent_commits=None, head=True):
+ """
+ Commit the current default index file, creating a commit object.
+
+ For more information on the arguments, see tree.commit.
+
+ ``NOTE``:
+ If you have manually altered the .entries member of this instance,
+ don't forget to write() your changes to disk beforehand.
+
+ Returns
+ Commit object representing the new commit
+ """
+ tree_sha = self.repo.git.write_tree()
+ return Commit.create_from_tree(self.repo, tree_sha, message, parent_commits, head)
+
+ @classmethod
+ def _flush_stdin_and_wait(cls, proc, ignore_stdout = False):
+ proc.stdin.flush()
+ proc.stdin.close()
+ stdout = ''
+ if not ignore_stdout:
+ stdout = proc.stdout.read()
+ proc.stdout.close()
+ proc.wait()
+ return stdout
+
+ @default_index
+ def checkout(self, paths=None, force=False, fprogress=lambda *args: None, **kwargs):
+ """
+ Checkout the given paths or all files from the version known to the index into
+ the working tree.
+
+ ``paths``
+ If None, all paths in the index will be checked out. Otherwise an iterable
+ of relative or absolute paths or a single path pointing to files or directories
+ in the index is expected.
+
+ ``force``
+ If True, existing files will be overwritten even if they contain local modifications.
+ If False, these will trigger a CheckoutError.
+
+ ``fprogress``
+ see Index.add_ for signature and explanation.
+ The provided progress information will contain None as path and item if no
+ explicit paths are given. Otherwise progress information will be send
+ prior and after a file has been checked out
+
+ ``**kwargs``
+ Additional arguments to be pasesd to git-checkout-index
+
+ Returns
+ iterable yielding paths to files which have been checked out and are
+ guaranteed to match the version stored in the index
+
+ Raise CheckoutError
+ If at least one file failed to be checked out. This is a summary,
+ hence it will checkout as many files as it can anyway.
+ If one of files or directories do not exist in the index
+ ( as opposed to the original git command who ignores them ).
+ Raise GitCommandError if error lines could not be parsed - this truly is
+ an exceptional state
+ """
+ args = ["--index"]
+ if force:
+ args.append("--force")
+
+ def handle_stderr(proc, iter_checked_out_files):
+ stderr = proc.stderr.read()
+ if not stderr:
+ return
+ # line contents:
+ # git-checkout-index: this already exists
+ failed_files = list()
+ failed_reasons = list()
+ unknown_lines = list()
+ endings = (' already exists', ' is not in the cache', ' does not exist at stage', ' is unmerged')
+ for line in stderr.splitlines():
+ if not line.startswith("git checkout-index: ") and not line.startswith("git-checkout-index: "):
+ is_a_dir = " is a directory"
+ unlink_issue = "unable to unlink old '"
+ if line.endswith(is_a_dir):
+ failed_files.append(line[:-len(is_a_dir)])
+ failed_reasons.append(is_a_dir)
+ elif line.startswith(unlink_issue):
+ failed_files.append(line[len(unlink_issue):line.rfind("'")])
+ failed_reasons.append(unlink_issue)
+ else:
+ unknown_lines.append(line)
+ continue
+ # END special lines parsing
+
+ for e in endings:
+ if line.endswith(e):
+ failed_files.append(line[20:-len(e)])
+ failed_reasons.append(e)
+ break
+ # END if ending matches
+ # END for each possible ending
+ # END for each line
+ if unknown_lines:
+ raise GitCommandError(("git-checkout-index", ), 128, stderr)
+ if failed_files:
+ valid_files = list(set(iter_checked_out_files) - set(failed_files))
+ raise CheckoutError("Some files could not be checked out from the index due to local modifications", failed_files, valid_files, failed_reasons)
+ # END stderr handler
+
+
+ if paths is None:
+ args.append("--all")
+ kwargs['as_process'] = 1
+ fprogress(None, False, None)
+ proc = self.repo.git.checkout_index(*args, **kwargs)
+ proc.wait()
+ fprogress(None, True, None)
+ rval_iter = ( e.path for e in self.entries.itervalues() )
+ handle_stderr(proc, rval_iter)
+ return rval_iter
+ else:
+ if isinstance(paths, basestring):
+ paths = [paths]
+
+ args.append("--stdin")
+ kwargs['as_process'] = True
+ kwargs['istream'] = subprocess.PIPE
+ proc = self.repo.git.checkout_index(args, **kwargs)
+ make_exc = lambda : GitCommandError(("git-checkout-index",)+tuple(args), 128, proc.stderr.read())
+ checked_out_files = list()
+ for path in paths:
+ path = self._to_relative_path(path)
+ # if the item is not in the index, it could be a directory
+ path_is_directory = False
+ try:
+ self.entries[(path, 0)]
+ except KeyError:
+ dir = path
+ if not dir.endswith('/'):
+ dir += '/'
+ for entry in self.entries.itervalues():
+ if entry.path.startswith(dir):
+ p = entry.path
+ self._write_path_to_stdin(proc, p, p, make_exc, fprogress, read_from_stdout=False)
+ checked_out_files.append(p)
+ path_is_directory = True
+ # END if entry is in directory
+ # END for each entry
+ # END path exception handlnig
+
+ if not path_is_directory:
+ self._write_path_to_stdin(proc, path, path, make_exc, fprogress, read_from_stdout=False)
+ checked_out_files.append(path)
+ # END path is a file
+ # END for each path
+ self._flush_stdin_and_wait(proc, ignore_stdout=True)
+
+ handle_stderr(proc, checked_out_files)
+ return checked_out_files
+ # END directory handling
+ # END paths handling
+ assert "Should not reach this point"
+
+ @clear_cache
+ @default_index
+ def reset(self, commit='HEAD', working_tree=False, paths=None, head=False, **kwargs):
+ """
+ Reset the index to reflect the tree at the given commit. This will not
+ adjust our HEAD reference as opposed to HEAD.reset by default.
+
+ ``commit``
+ Revision, Reference or Commit specifying the commit we should represent.
+ If you want to specify a tree only, use IndexFile.from_tree and overwrite
+ the default index.
+
+ ``working_tree``
+ If True, the files in the working tree will reflect the changed index.
+ If False, the working tree will not be touched
+ Please note that changes to the working copy will be discarded without
+ warning !
+
+ ``head``
+ If True, the head will be set to the given commit. This is False by default,
+ but if True, this method behaves like HEAD.reset.
+
+ ``**kwargs``
+ Additional keyword arguments passed to git-reset
+
+ Returns
+ self
+ """
+ cur_head = self.repo.head
+ prev_commit = cur_head.commit
+
+ # reset to get the tree/working copy
+ cur_head.reset(commit, index=True, working_tree=working_tree, paths=paths, **kwargs)
+
+ # put the head back, possibly
+ if not head:
+ cur_head.reset(prev_commit, index=False, working_tree=False)
+ # END reset head
+
+ return self
+
+ @default_index
+ def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwargs):
+ """
+ Diff this index against the working copy or a Tree or Commit object
+
+ For a documentation of the parameters and return values, see
+ Diffable.diff
+
+ Note
+ Will only work with indices that represent the default git index as
+ they have not been initialized with a stream.
+ """
+ # index against index is always empty
+ if other is self.Index:
+ return diff.DiffIndex()
+
+ # index against anything but None is a reverse diff with the respective
+ # item. Handle existing -R flags properly. Transform strings to the object
+ # so that we can call diff on it
+ if isinstance(other, basestring):
+ other = Object.new(self.repo, other)
+ # END object conversion
+
+ if isinstance(other, Object):
+ # invert the existing R flag
+ cur_val = kwargs.get('R', False)
+ kwargs['R'] = not cur_val
+ return other.diff(self.Index, paths, create_patch, **kwargs)
+ # END diff against other item handlin
+
+ # if other is not None here, something is wrong
+ if other is not None:
+ raise ValueError( "other must be None, Diffable.Index, a Tree or Commit, was %r" % other )
+
+ # diff against working copy - can be handled by superclass natively
+ return super(IndexFile, self).diff(other, paths, create_patch, **kwargs)
+
diff --git a/lib/git/refs.py b/lib/git/refs.py
index ddf358fe..d88a2331 100644
--- a/lib/git/refs.py
+++ b/lib/git/refs.py
@@ -11,7 +11,7 @@ from objects.utils import get_object_type_by_name
from utils import LazyMixin, Iterable, join_path, join_path_native, to_native_path_linux
-class SymbolicReference(object):
+class SymbolicReference(object):
"""
Represents a special case of a reference such that this reference is symbolic.
It does not point to a specific commit, but to another Head, which itself
@@ -138,7 +138,7 @@ class SymbolicReference(object):
if sha:
return Commit(self.repo, sha)
- return Reference.from_path(self.repo, target_ref_path).commit
+ return self.from_path(self.repo, target_ref_path).commit
def _set_commit(self, commit):
"""
@@ -160,7 +160,7 @@ class SymbolicReference(object):
sha, target_ref_path = self._get_ref_info()
if target_ref_path is None:
raise TypeError("%s is a detached symbolic reference as it points to %r" % (self, sha))
- return Reference.from_path(self.repo, target_ref_path)
+ return self.from_path(self.repo, target_ref_path)
def _set_reference(self, ref):
"""
@@ -240,30 +240,11 @@ class SymbolicReference(object):
except TypeError:
return True
- @classmethod
- def from_path(cls, repo, path):
- """
- Return
- Instance of SymbolicReference or HEAD
- depending on the given path
-
- Note
- It enforces that symbolic refs in git are only found in the
- root of the .git repository, never within a folder.
- """
- if not path:
- raise ValueError("Cannot create Symbolic Reference from %r" % path)
-
- if path == 'HEAD':
- return HEAD(repo, path)
-
- if '/' not in path:
- return SymbolicReference(repo, path)
-
- raise ValueError("Could not find symbolic reference type suitable to handle path %r" % path)
@classmethod
- def _to_full_path(cls, repo, path):
+ def to_full_path(cls, path):
+ """:return: string with a full path name which can be used to initialize
+ a Reference instance, for instance by using ``Reference.from_path``"""
if isinstance(path, SymbolicReference):
path = path.path
full_ref_path = path
@@ -285,7 +266,7 @@ class SymbolicReference(object):
or just "myreference", hence 'refs/' is implied.
Alternatively the symbolic reference to be deleted
"""
- full_ref_path = cls._to_full_path(repo, path)
+ full_ref_path = cls.to_full_path(path)
abs_path = os.path.join(repo.git_dir, full_ref_path)
if os.path.exists(abs_path):
os.remove(abs_path)
@@ -331,7 +312,7 @@ class SymbolicReference(object):
a proper symbolic reference. Otherwise it will be resolved to the
corresponding object and a detached symbolic reference will be created
instead"""
- full_ref_path = cls._to_full_path(repo, path)
+ full_ref_path = cls.to_full_path(path)
abs_ref_path = os.path.join(repo.git_dir, full_ref_path)
# figure out target data
@@ -402,7 +383,7 @@ class SymbolicReference(object):
Raises OSError:
In case a file at path but a different contents already exists
"""
- new_path = self._to_full_path(self.repo, new_path)
+ new_path = self.to_full_path(new_path)
if self.path == new_path:
return self
diff --git a/lib/git/repo.py b/lib/git/repo.py
index 2beda9be..4ab656c4 100644
--- a/lib/git/repo.py
+++ b/lib/git/repo.py
@@ -526,7 +526,8 @@ class Repo(object):
default_args = ('--abbrev=40', '--full-index', '--raw')
if index:
# diff index against HEAD
- if len(self.git.diff('HEAD', '--cached', *default_args)):
+ if os.path.isfile(self.index.path) and \
+ len(self.git.diff('HEAD', '--cached', *default_args)):
return True
# END index handling
if working_tree:
diff --git a/lib/git/utils.py b/lib/git/utils.py
index 15102fec..1fb58861 100644
--- a/lib/git/utils.py
+++ b/lib/git/utils.py
@@ -366,7 +366,7 @@ class IterableList(list):
try:
return getattr(self, index)
except AttributeError:
- raise IndexError( "No item found with id %r" % self._prefix + index )
+ raise IndexError( "No item found with id %r" % (self._prefix + index) )
class Iterable(object):
"""
diff --git a/test/git/test_refs.py b/test/git/test_refs.py
index 3d85356f..58a51f4a 100644
--- a/test/git/test_refs.py
+++ b/test/git/test_refs.py
@@ -14,6 +14,16 @@ import os
class TestRefs(TestBase):
+ def test_from_path(self):
+ # should be able to create any reference directly
+ for ref_type in ( Reference, Head, TagReference, RemoteReference ):
+ for name in ('rela_name', 'path/rela_name'):
+ full_path = ref_type.to_full_path(name)
+ instance = ref_type.from_path(self.rorepo, full_path)
+ assert isinstance(instance, ref_type)
+ # END for each name
+ # END for each type
+
def test_tag_base(self):
tag_object_refs = list()
for tag in self.rorepo.tags:
diff --git a/test/git/test_repo.py b/test/git/test_repo.py
index b3ce74cb..9a762bd9 100644
--- a/test/git/test_repo.py
+++ b/test/git/test_repo.py
@@ -89,7 +89,35 @@ class TestRepo(TestBase):
assert isinstance(tree, Tree)
# END for each tree
assert num_trees == mc
-
+
+
+ def _test_empty_repo(self, repo):
+ # test all kinds of things with an empty, freshly initialized repo.
+ # It should throw good errors
+
+ # entries should be empty
+ assert len(repo.index.entries) == 0
+
+ # head is accessible
+ assert repo.head
+ assert repo.head.ref
+ assert not repo.head.is_valid()
+
+ # we can change the head to some other ref
+ head_ref = Head.from_path(repo, Head.to_full_path('some_head'))
+ assert not head_ref.is_valid()
+ repo.head.ref = head_ref
+
+ # is_dirty can handle all kwargs
+ for args in ((1, 0, 0), (0, 1, 0), (0, 0, 1)):
+ assert not repo.is_dirty(*args)
+ # END for each arg
+
+ # we can add a file to the index ( if we are not bare )
+ if not repo.bare:
+ pass
+ # END test repos with working tree
+
def test_init(self):
prev_cwd = os.getcwd()
@@ -104,6 +132,8 @@ class TestRepo(TestBase):
assert isinstance(r, Repo)
assert r.bare == True
assert os.path.isdir(r.git_dir)
+
+ self._test_empty_repo(r)
shutil.rmtree(git_dir_abs)
# END for each path
@@ -111,6 +141,8 @@ class TestRepo(TestBase):
os.chdir(git_dir_rela)
r = Repo.init(bare=False)
r.bare == False
+
+ self._test_empty_repo(r)
finally:
try:
shutil.rmtree(del_dir_abs)