summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO20
-rw-r--r--lib/git/actor.py9
-rw-r--r--lib/git/index.py333
-rw-r--r--lib/git/objects/base.py1
-rw-r--r--lib/git/objects/commit.py40
-rw-r--r--test/git/test_actor.py8
-rw-r--r--test/git/test_commit.py13
-rw-r--r--test/git/test_index.py104
8 files changed, 425 insertions, 103 deletions
diff --git a/TODO b/TODO
index d44842f6..69fbc307 100644
--- a/TODO
+++ b/TODO
@@ -31,6 +31,12 @@ Object
it should be renamed to 'sha'. There was a time when references where allowed as
well, but now objects will be 'baked' to the actual sha to assure comparisons work.
+Commit
+------
+* message is stipped during parsing, which is wrong unless we parse from
+ rev-list output. In fact we don't know that, and can't really tell either.
+ Currently we strip away white space that might actually belong to the message
+
Config
------
* Expand .get* methods of GitConfigParser to support default value. If it is not None,
@@ -62,6 +68,20 @@ Index
* Proper merge handling with index and working copy
* Checkout individual blobs using the index and git-checkout. Blobs can already
be written using their stream_data method.
+* index.add: could be implemented in python together with hash-object, allowing
+ to keep the internal entry cache and write once everything is done. Problem
+ would be that all other git commands are unaware of the changes unless the index
+ gets written. Its worth an evaluation at least.
+* index.remove: On windows, there can be a command line length overflow
+ as we pass the paths directly as argv. This is as we use git-rm to be able
+ to remove whole directories easily. This could be implemented using
+ git-update-index if this becomes an issue, but then we had to do all the globbing
+ and directory removal ourselves
+* commit: advance head = False - tree object should get the base commit wrapping
+ that index uses after writing itself as tree. Perhaps it would even be better
+ to have a Commit.create method from a tree or from an index. Allowing the
+ latter would be quite flexible and would fit into the system as refs have
+ create methods as well
Refs
-----
diff --git a/lib/git/actor.py b/lib/git/actor.py
index fe4a47e5..04872f1c 100644
--- a/lib/git/actor.py
+++ b/lib/git/actor.py
@@ -18,6 +18,15 @@ class Actor(object):
self.name = name
self.email = email
+ def __eq__(self, other):
+ return self.name == other.name and self.email == other.email
+
+ def __ne__(self, other):
+ return not (self == other)
+
+ def __hash__(self):
+ return hash((self.name, self.email))
+
def __str__(self):
return self.name
diff --git a/lib/git/index.py b/lib/git/index.py
index 9fd1fad2..86bdfd39 100644
--- a/lib/git/index.py
+++ b/lib/git/index.py
@@ -17,7 +17,7 @@ import sys
import stat
import git.diff as diff
-from git.objects import Blob, Tree, Object
+from git.objects import Blob, Tree, Object, Commit
from git.utils import SHA1Writer, LazyMixin, ConcurrentWriteOperation
@@ -41,7 +41,57 @@ class _TemporaryFileSwap(object):
# END temp file exists
-class IndexEntry(tuple):
+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.
+ """
+
+ @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.id, stage, blob.path))
+
+
+class IndexEntry(BaseIndexEntry):
"""
Allows convenient access to IndexEntry data without completely unpacking it.
@@ -57,49 +107,42 @@ class IndexEntry(tuple):
Tuple(int_time_seconds_since_epoch, int_nano_seconds) of the
file's creation time
"""
- return struct.unpack(">LL", self[0])
+ return struct.unpack(">LL", self[4])
@property
def mtime(self):
"""
See ctime property, but returns modification time
"""
- return struct.unpack(">LL", self[1])
+ return struct.unpack(">LL", self[5])
@property
def dev(self):
"""
Device ID
"""
- return self[2]
+ return self[6]
@property
def inode(self):
"""
Inode ID
"""
- return self[3]
-
- @property
- def mode(self):
- """
- File Mode, compatible to stat module constants
- """
- return self[4]
-
+ return self[7]
+
@property
def uid(self):
"""
User ID
"""
- return self[5]
+ return self[8]
@property
def gid(self):
"""
Group ID
"""
- return self[6]
+ return self[9]
@property
def size(self):
@@ -109,33 +152,8 @@ class IndexEntry(tuple):
Note
Will be 0 if the stage is not 0 ( hence it is an unmerged entry )
"""
- return self[7]
-
- @property
- def sha(self):
- """
- hex sha of the blob
- """
- return self[8]
-
- @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[9]
-
- @property
- def path(self):
return self[10]
-
@classmethod
def from_blob(cls, blob):
"""
@@ -143,8 +161,28 @@ class IndexEntry(tuple):
Minimal entry resembling the given blob objecft
"""
time = struct.pack(">LL", 0, 0)
- return IndexEntry((time, time, 0, 0, blob.mode, 0, 0, blob.size, blob.id, 0, blob.path))
+ return IndexEntry((blob.mode, blob.id, 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)
+ del(self.entries)
+ return rval
+
+ # END wrapper method
+ clear_cache_if_not_raised.__name__ = func.__name__
+ return clear_cache_if_not_raised
+
def default_index(func):
"""
@@ -176,10 +214,8 @@ class IndexFile(LazyMixin, diff.Diffable):
The index contains an entries dict whose keys are tuples of type IndexEntry
to facilitate access.
- As opposed to the Index type, the IndexFile represents the index on file level.
- This can be considered an alternate, file-based implementation of the Index class
- with less support for common functions. Use it for very special and custom index
- handling.
+ You may only read the entries dict or manipulate it through designated methods.
+ 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
@@ -243,8 +279,7 @@ class IndexFile(LazyMixin, diff.Diffable):
real_size = ((stream.tell() - beginoffset + 8) & ~7)
data = stream.read((beginoffset + real_size) - stream.tell())
- return IndexEntry((ctime, mtime, dev, ino, mode, uid, gid, size,
- binascii.hexlify(sha), flags >> 12, path))
+ return IndexEntry((mode, binascii.hexlify(sha), flags >> 12, path, ctime, mtime, dev, ino, uid, gid, size))
@classmethod
def _read_header(cls, stream):
@@ -286,14 +321,14 @@ class IndexFile(LazyMixin, diff.Diffable):
Write an IndexEntry to a stream
"""
beginoffset = stream.tell()
- stream.write(entry[0]) # ctime
- stream.write(entry[1]) # mtime
- path = entry[10]
+ 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[10]
- flags = plen | (entry[9] << 12)# stage and path length are 2 byte flags
- stream.write(struct.pack(">LLLLLL20sH", entry[2], entry[3], entry[4],
- entry[5], entry[6], entry[7], binascii.unhexlify(entry[8]), flags))
+ 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()))
@@ -325,7 +360,7 @@ class IndexFile(LazyMixin, diff.Diffable):
# body
entries_sorted = self.entries.values()
- entries_sorted.sort(key=lambda e: (e[10], e[9])) # use path/stage as sort key
+ 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
@@ -483,6 +518,10 @@ class IndexFile(LazyMixin, diff.Diffable):
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)
@@ -511,13 +550,13 @@ class IndexFile(LazyMixin, diff.Diffable):
Note:
This is a possibly dangerious operations as it will discard your changes
- to index.endtries
+ to index.entries
Returns
self
"""
del(self.entries)
- self.entries
+ # allows to lazily reread on demand
return self
def write_tree(self):
@@ -544,69 +583,183 @@ class IndexFile(LazyMixin, diff.Diffable):
# 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.git.git_dir+"/", "")
+ if relative_path == path:
+ raise ValueError("Absolute path %r is not in git repository at %r" % (path,self.repo.git.git_dir))
+ return relative_path
+
+ @clear_cache
@default_index
def add(self, items, **kwargs):
"""
- Add files from the working copy, specific blobs or IndexEntries
- to the index.
-
- TODO: Its important to specify a way to add symlinks directly, even
- on systems that do not support it, like ... erm ... windows.
+ 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.
+
+ 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
+
+ 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.
+
+ They are added at stage 0
+
+ - BaseIndexEntry or type
+ Handling equals the one of Blob objects, but the stage may be
+ explicitly set.
+
``**kwargs``
- Additional keyword arguments to be passed to git-update-index
+ Additional keyword arguments to be passed to git-update-index, such
+ as index_only.
Returns
- List(IndexEntries) representing the entries just added
+ List(BaseIndexEntries) representing the entries just actually added.
"""
raise NotImplementedError("todo")
+ @clear_cache
@default_index
- def remove(self, items, affect_working_tree=False, **kwargs):
+ def remove(self, items, working_tree=False, **kwargs):
"""
- Remove the given file_paths or blobs from the index and optionally from
+ Remove the given items from the index and optionally from
the working tree as well.
``items``
- TODO
+ 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.
- ``affect_working_tree``
+ ``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-update-index
+ Additional keyword arguments to be passed to git-rm, such
+ as 'r' to allow recurive removal of
Returns
- self
- """
- raise NotImplementedError("todo")
- return self
+ List(path_string, ...) list of 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
+ """
+ args = list()
+ if not working_tree:
+ args.append("--cached")
+ args.append("--")
+
+ # preprocess paths
+ 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
+
+ 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 ]
@default_index
- def commit(self, message=None, parent_commits=None, **kwargs):
+ def commit(self, message, parent_commits=None):
"""
Commit the current index, creating a commit object.
``message``
- Commit message
+ Commit message. It may be an empty string if no message is provided.
+ It will be converted to a string in any case.
``parent_commits``
- Optional Commit objects to use as parents for the new commit.
- If None or empty, the current head commit will be the parent of the
+ Optional Commit objects to use as parents for the new commit.
+ If empty list, the commit will have no parents at all and become
+ a root commit.
+ If None , the current head commit will be the parent of the
new commit object
- ``**kwargs``
- Additional keyword arguments passed to git-commit
-
Returns
Commit object representing the new commit
+
+ Note:
+ Additional information about hte committer and Author are taken from the
+ environment or from the git configuration, see git-commit-tree for
+ more information
"""
- raise NotImplementedError("todo")
+ parents = parent_commits
+ if parent_commits is None:
+ parent_commits = [ self.repo.head.commit ]
+
+ parent_args = [ ("-p", str(commit)) for commit in parent_commits ]
+ # create message stream
+ tmp_file_path = tempfile.mktemp()
+ fp = open(tmp_file_path,"w")
+ fp.write(str(message))
+ fp.close()
+ fp = open(tmp_file_path,"r")
+ fp.seek(0)
+
+ try:
+ # write the current index as tree
+ tree_sha = self.repo.git.write_tree()
+ commit_sha = self.repo.git.commit_tree(tree_sha, parent_args, istream=fp)
+ return Commit(self.repo, commit_sha)
+ finally:
+ fp.close()
+ os.remove(tmp_file_path)
+
+ @clear_cache
@default_index
- def reset(self, commit='HEAD', working_tree=False, **kwargs):
+ def reset(self, commit='HEAD', working_tree=False, paths=None, **kwargs):
"""
Reset the index to reflect the tree at the given commit. This will not
adjust our HEAD reference as opposed to HEAD.reset.
@@ -619,11 +772,23 @@ class IndexFile(LazyMixin, diff.Diffable):
``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 !
``**kwargs``
Additional keyword arguments passed to git-reset
+
+ Returns
+ self
"""
- raise NotImplementedError("todo")
+ head = self.repo.head
+ prev_commit = head.commit
+
+ # reset to get the tree/working copy
+ head.reset(commit, index=True, working_tree=working_tree, paths=paths, **kwargs)
+ # put the head back
+ head.reset(prev_commit, index=False, working_tree=False)
+ return self
@default_index
def diff(self, other=diff.Diffable.Index, paths=None, create_patch=False, **kwargs):
diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py
index 0dfd1a23..0bece6f1 100644
--- a/lib/git/objects/base.py
+++ b/lib/git/objects/base.py
@@ -20,6 +20,7 @@ class Object(LazyMixin):
inst.size # objects uncompressed data size
inst.data # byte string containing the whole data of the object
"""
+ NULL_HEX_SHA = '0'*40
TYPES = ("blob", "tree", "commit", "tag")
__slots__ = ("repo", "id", "size", "data" )
type = None # to be set by subclass
diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py
index 4080305f..00f3d0e4 100644
--- a/lib/git/objects/commit.py
+++ b/lib/git/objects/commit.py
@@ -83,7 +83,7 @@ class Commit(base.Object, Iterable, diff.Diffable):
# prepare our data lines to match rev-list
data_lines = self.data.splitlines()
data_lines.insert(0, "commit %s" % self.id)
- temp = self._iter_from_process_or_stream(self.repo, iter(data_lines)).next()
+ temp = self._iter_from_process_or_stream(self.repo, iter(data_lines), False).next()
self.parents = temp.parents
self.tree = temp.tree
self.author = temp.author
@@ -111,7 +111,8 @@ class Commit(base.Object, Iterable, diff.Diffable):
to commits actually containing the paths
``kwargs``
- Additional options to be passed to git-rev-list
+ Additional options to be passed to git-rev-list. They must not alter
+ the ouput style of the command, or parsing will yield incorrect results
Returns
int
"""
@@ -144,9 +145,8 @@ class Commit(base.Object, Iterable, diff.Diffable):
options = {'pretty': 'raw', 'as_process' : True }
options.update(kwargs)
- # the test system might confront us with string values -
proc = repo.git.rev_list(rev, '--', paths, **options)
- return cls._iter_from_process_or_stream(repo, proc)
+ return cls._iter_from_process_or_stream(repo, proc, True)
def iter_parents(self, paths='', **kwargs):
"""
@@ -191,7 +191,7 @@ class Commit(base.Object, Iterable, diff.Diffable):
return stats.Stats._list_from_string(self.repo, text)
@classmethod
- def _iter_from_process_or_stream(cls, repo, proc_or_stream):
+ def _iter_from_process_or_stream(cls, repo, proc_or_stream, from_rev_list):
"""
Parse out commit information into a list of Commit objects
@@ -201,6 +201,9 @@ class Commit(base.Object, Iterable, diff.Diffable):
``proc``
git-rev-list process instance (raw format)
+ ``from_rev_list``
+ If True, the stream was created by rev-list in which case we parse
+ the message differently
Returns
iterator returning Commit objects
"""
@@ -208,10 +211,10 @@ class Commit(base.Object, Iterable, diff.Diffable):
if not hasattr(stream,'next'):
stream = proc_or_stream.stdout
-
for line in stream:
- id = line.split()[1]
- assert line.split()[0] == "commit"
+ commit_tokens = line.split()
+ id = commit_tokens[1]
+ assert commit_tokens[0] == "commit"
tree = stream.next().split()[1]
parents = []
@@ -231,13 +234,20 @@ class Commit(base.Object, Iterable, diff.Diffable):
stream.next()
message_lines = []
- next_line = None
- for msg_line in stream:
- if not msg_line.startswith(' '):
- break
- # END abort message reading
- message_lines.append(msg_line.strip())
- # END while there are message lines
+ if from_rev_list:
+ for msg_line in stream:
+ if not msg_line.startswith(' '):
+ # and forget about this empty marker
+ break
+ # END abort message reading
+ # strip leading 4 spaces
+ message_lines.append(msg_line[4:])
+ # END while there are message lines
+ else:
+ # a stream from our data simply gives us the plain message
+ for msg_line in stream:
+ message_lines.append(msg_line)
+ # END message parsing
message = '\n'.join(message_lines)
yield Commit(repo, id=id, parents=tuple(parents), tree=tree, author=author, authored_date=authored_date,
diff --git a/test/git/test_actor.py b/test/git/test_actor.py
index b7c2af7c..2941468d 100644
--- a/test/git/test_actor.py
+++ b/test/git/test_actor.py
@@ -13,6 +13,14 @@ class TestActor(object):
a = Actor._from_string("Michael Trier <mtrier@example.com>")
assert_equal("Michael Trier", a.name)
assert_equal("mtrier@example.com", a.email)
+
+ # base type capabilities
+ assert a == a
+ assert not ( a != a )
+ m = set()
+ m.add(a)
+ m.add(a)
+ assert len(m) == 1
def test_from_string_should_handle_just_name(self):
a = Actor._from_string("Michael Trier")
diff --git a/test/git/test_commit.py b/test/git/test_commit.py
index c4ed4b72..da636a2b 100644
--- a/test/git/test_commit.py
+++ b/test/git/test_commit.py
@@ -16,6 +16,9 @@ class TestCommit(TestBase):
assert_equal("Sebastian Thiel", commit.author.name)
assert_equal("byronimo@gmail.com", commit.author.email)
+ assert commit.author == commit.committer
+ assert isinstance(commit.authored_date, int) and isinstance(commit.committed_date, int)
+ assert commit.message == "Added missing information to docstrings of commit and stats module"
def test_stats(self):
@@ -37,6 +40,14 @@ class TestCommit(TestBase):
check_entries(d)
# END for each stated file
+ # assure data is parsed properly
+ michael = Actor._from_string("Michael Trier <mtrier@gmail.com>")
+ assert commit.author == michael
+ assert commit.committer == michael
+ assert commit.authored_date == 1210193388
+ assert commit.committed_date == 1210193388
+ assert commit.message == "initial project"
+
@patch_object(Git, '_call_process')
def test_rev_list_bisect_all(self, git):
"""
@@ -52,7 +63,7 @@ class TestCommit(TestBase):
bisect_all=True)
assert_true(git.called)
- commits = Commit._iter_from_process_or_stream(self.rorepo, ListProcessAdapter(revs))
+ commits = Commit._iter_from_process_or_stream(self.rorepo, ListProcessAdapter(revs), True)
expected_ids = (
'cf37099ea8d1d8c7fbf9b6d12d7ec0249d3acb8b',
'33ebe7acec14b25c5f84f35a664803fcab2f7781',
diff --git a/test/git/test_index.py b/test/git/test_index.py
index 36e57d5c..0a5f4c35 100644
--- a/test/git/test_index.py
+++ b/test/git/test_index.py
@@ -9,6 +9,7 @@ from git import *
import inspect
import os
import tempfile
+import glob
class TestTree(TestBase):
@@ -146,10 +147,107 @@ class TestTree(TestBase):
# against something unusual
self.failUnlessRaises(ValueError, index.diff, int)
- self.fail( "Test IndexFile.reset" )
+ # adjust the index to match an old revision
+ cur_branch = rw_repo.active_branch
+ cur_commit = cur_branch.commit
+ rev_head_parent = 'HEAD~1'
+ assert index.reset(rev_head_parent) is index
+
+ assert cur_branch == rw_repo.active_branch
+ assert cur_commit == rw_repo.head.commit
+
+ # there must be differences towards the working tree which is in the 'future'
+ assert index.diff(None)
+
+ # reset the working copy as well to current head,to pull 'back' as well
+ new_data = "will be reverted"
+ file_path = os.path.join(rw_repo.git.git_dir, "CHANGES")
+ fp = open(file_path, "w")
+ fp.write(new_data)
+ fp.close()
+ index.reset(rev_head_parent, working_tree=True)
+ assert not index.diff(None)
+ assert cur_branch == rw_repo.active_branch
+ assert cur_commit == rw_repo.head.commit
+ fp = open(file_path)
+ try:
+ assert fp.read() != new_data
+ finally:
+ fp.close()
+
+
+ def _count_existing(self, repo, files):
+ existing = 0
+ basedir = repo.git.git_dir
+ for f in files:
+ existing += os.path.isfile(os.path.join(basedir, f))
+ # END for each deleted file
+ return existing
+ # END num existing helper
+
+
@with_rw_repo('0.1.6')
def test_index_mutation(self, rw_repo):
- # add / remove / commit / Working Tree Handling
- self.fail( "add, remove, commit, working tree handling" )
+ index = rw_repo.index
+ num_entries = len(index.entries)
+ # remove all of the files, provide a wild mix of paths, BaseIndexEntries,
+ # IndexEntries
+ def mixed_iterator():
+ count = 0
+ for entry in index.entries.itervalues():
+ type_id = count % 4
+ if type_id == 0: # path
+ yield entry.path
+ elif type_id == 1: # blob
+ yield Blob(rw_repo, entry.sha, entry.mode, entry.path)
+ elif type_id == 2: # BaseIndexEntry
+ yield BaseIndexEntry(entry[:4])
+ elif type_id == 3: # IndexEntry
+ yield entry
+ else:
+ raise AssertionError("Invalid Type")
+ count += 1
+ # END for each entry
+ # END mixed iterator
+ deleted_files = index.remove(mixed_iterator(), working_tree=False)
+ assert deleted_files
+ assert self._count_existing(rw_repo, deleted_files) == len(deleted_files)
+ assert len(index.entries) == 0
+
+ # reset the index to undo our changes
+ index.reset()
+ assert len(index.entries) == num_entries
+
+ # remove with working copy
+ deleted_files = index.remove(mixed_iterator(), working_tree=True)
+ assert deleted_files
+ assert self._count_existing(rw_repo, deleted_files) == 0
+
+ # reset everything
+ index.reset(working_tree=True)
+ assert self._count_existing(rw_repo, deleted_files) == len(deleted_files)
+
+ # invalid type
+ self.failUnlessRaises(TypeError, index.remove, [1])
+
+ # absolute path
+ deleted_files = index.remove([os.path.join(rw_repo.git.git_dir,"lib")], r=True)
+ assert len(deleted_files) > 1
+ self.failUnlessRaises(ValueError, index.remove, ["/doesnt/exists"])
+
+ # test committing
+ # commit changed index
+ cur_commit = rw_repo.head.commit
+ commit_message = "commit default head"
+ new_commit = index.commit(commit_message)
+ assert new_commit.message == commit_message
+ assert new_commit.parents[0] == cur_commit
+
+ self.fail("commit with no parents")
+ self.fail("commit multiple parents")
+
+
+ # re-add all files in lib
+ self.fail( "add, commit, working tree handling" )