summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES12
-rw-r--r--lib/git/__init__.py2
-rw-r--r--lib/git/diff.py203
-rw-r--r--lib/git/objects/base.py33
-rw-r--r--lib/git/objects/commit.py57
-rw-r--r--lib/git/objects/tree.py4
-rw-r--r--lib/git/refs.py2
-rw-r--r--lib/git/repo.py40
-rw-r--r--test/git/test_base.py3
-rw-r--r--test/git/test_commit.py150
-rw-r--r--test/git/test_diff.py58
-rw-r--r--test/git/test_repo.py39
-rw-r--r--test/testlib/helper.py32
13 files changed, 326 insertions, 309 deletions
diff --git a/CHANGES b/CHANGES
index 96a1ef56..da396fe0 100644
--- a/CHANGES
+++ b/CHANGES
@@ -27,6 +27,9 @@ General
terms are used everywhere, such as "Reference" ( ref ) and "Revision" ( rev ).
Prevously multiple terms where used making it harder to know which type was allowed
or not.
+* Unified diff interface to allow easy diffing between trees, trees and index, trees
+ and working tree, index and working tree, trees and index. This closely follows
+ the git-diff capabilities.
Item Iteration
@@ -60,6 +63,8 @@ Repo
related repositories, i.e. clones, git-rev-list would be sufficient to find
commits that would need to be transferred for example.
- 'create' method which equals the 'init' method's functionality
+ - 'diff' - it returned a mere string which still had to be parsed
+ - 'commit_diff' - moved to Commit, Tree and Diff types respectively
* Renamed the following methods:
- commits to iter_commits to improve the performance, adjusted signature
- init_bare to init, implying less about the options to be used
@@ -75,6 +80,13 @@ Diff
* Members a a_commit and b_commit renamed to a_blob and b_blob - they are populated
with Blob objects if possible
* Members a_path and b_path removed as this information is kept in the blobs
+* Diffs are now returned as DiffIndex allowing to more quickly find the kind of
+ diffs you are interested in
+
+Diffing
+-------
+* Commit and Tree objects now support diffing natively with a common interface to
+ compare agains other Commits or Trees, against the working tree or against the index.
Blob
----
diff --git a/lib/git/__init__.py b/lib/git/__init__.py
index 6f482128..e2adac62 100644
--- a/lib/git/__init__.py
+++ b/lib/git/__init__.py
@@ -12,7 +12,7 @@ __version__ = 'git'
from git.objects import *
from git.refs import *
from git.actor import Actor
-from git.diff import Diff
+from git.diff import *
from git.errors import InvalidGitRepositoryError, NoSuchPathError, GitCommandError
from git.cmd import Git
from git.repo import Repo
diff --git a/lib/git/diff.py b/lib/git/diff.py
index 0db83b4f..1774597a 100644
--- a/lib/git/diff.py
+++ b/lib/git/diff.py
@@ -7,9 +7,128 @@
import re
import objects.blob as blob
+
+class Diffable(object):
+ """
+ Common interface for all object that can be diffed against another object of compatible type.
+
+ NOTE:
+ Subclasses require a repo member as it is the case for Object instances, for practical
+ reasons we do not derive from Object.
+ """
+ __slots__ = tuple()
+
+ # subclasses provide additional arguments to the git-diff comamnd by supplynig
+ # them in this tuple
+ _diff_args = tuple()
+
+ # Temporary standin for Index type until we have a real index type
+ class Index(object):
+ pass
+
+ def diff(self, other=None, paths=None, create_patch=False, **kwargs):
+ """
+ Creates diffs between two items being trees, trees and index or an
+ index and the working tree.
+
+ ``other``
+ Is the item to compare us with.
+ If None, we will be compared to the working tree.
+ If Index ( type ), it will be compared against the index
+
+ ``paths``
+ is a list of paths or a single path to limit the diff to.
+ It will only include at least one of the givne path or paths.
+
+ ``create_patch``
+ If True, the returned Diff contains a detailed patch that if applied
+ makes the self to other. Patches are somwhat costly as blobs have to be read
+ and diffed.
+
+ ``kwargs``
+ Additional arguments passed to git-diff, such as
+ R=True to swap both sides of the diff.
+
+ Returns
+ git.DiffIndex
+
+ Note
+ Rename detection will only work if create_patch is True
+ """
+ args = list(self._diff_args[:])
+ args.append( "--abbrev=40" ) # we need full shas
+ args.append( "--full-index" ) # get full index paths, not only filenames
+
+ if create_patch:
+ args.append("-p")
+ args.append("-M") # check for renames
+ else:
+ args.append("--raw")
+
+ if paths is not None and not isinstance(paths, (tuple,list)):
+ paths = [ paths ]
+
+ if other is not None and other is not self.Index:
+ args.insert(0, other)
+ if other is self.Index:
+ args.insert(0, "--cached")
+
+ args.insert(0,self)
+
+ # paths is list here or None
+ if paths:
+ args.append("--")
+ args.extend(paths)
+ # END paths handling
+
+ kwargs['as_process'] = True
+ proc = self.repo.git.diff(*args, **kwargs)
+
+ diff_method = Diff._index_from_raw_format
+ if create_patch:
+ diff_method = Diff._index_from_patch_format
+ return diff_method(self.repo, proc.stdout)
+
+
+class DiffIndex(list):
+ """
+ Implements an Index for diffs, allowing a list of Diffs to be queried by
+ the diff properties.
+
+ The class improves the diff handling convenience
+ """
+ # change type invariant identifying possible ways a blob can have changed
+ # A = Added
+ # D = Deleted
+ # R = Renamed
+ # NOTE: 'Modified' mode is impllied as it wouldn't be listed as a diff otherwise
+ change_type = ("A", "D", "R")
+
+
+ def iter_change_type(self, change_type):
+ """
+ Return
+ iterator yieling Diff instances that match the given change_type
+
+ ``change_type``
+ Member of DiffIndex.change_type
+ """
+ if change_type not in self.change_type:
+ raise ValueError( "Invalid change type: %s" % change_type )
+
+ for diff in self:
+ if change_type == "A" and diff.new_file:
+ yield diff
+ elif change_type == "D" and diff.deleted_file:
+ yield diff
+ elif change_type == "R" and diff.renamed:
+ yield diff
+ # END for each diff
+
+
class Diff(object):
"""
- A Diff contains diff information between two commits.
+ A Diff contains diff information between two Trees.
It contains two sides a and b of the diff, members are prefixed with
"a" and "b" respectively to inidcate that.
@@ -27,7 +146,7 @@ class Diff(object):
``Deleted File``::
b_mode is None
- b_blob is NOne
+ b_blob is None
"""
# precompiled regex
@@ -46,7 +165,7 @@ class Diff(object):
""", re.VERBOSE | re.MULTILINE)
re_is_null_hexsha = re.compile( r'^0{40}$' )
__slots__ = ("a_blob", "b_blob", "a_mode", "b_mode", "new_file", "deleted_file",
- "rename_from", "rename_to", "renamed", "diff")
+ "rename_from", "rename_to", "diff")
def __init__(self, repo, a_path, b_path, a_blob_id, b_blob_id, a_mode,
b_mode, new_file, deleted_file, rename_from,
@@ -62,31 +181,45 @@ class Diff(object):
self.a_mode = a_mode
self.b_mode = b_mode
+
if self.a_mode:
self.a_mode = blob.Blob._mode_str_to_int( self.a_mode )
if self.b_mode:
self.b_mode = blob.Blob._mode_str_to_int( self.b_mode )
+
self.new_file = new_file
self.deleted_file = deleted_file
- self.rename_from = rename_from
- self.rename_to = rename_to
- self.renamed = rename_from != rename_to
+
+ # be clear and use None instead of empty strings
+ self.rename_from = rename_from or None
+ self.rename_to = rename_to or None
+
self.diff = diff
+ @property
+ def renamed(self):
+ """
+ Returns:
+ True if the blob of our diff has been renamed
+ """
+ return self.rename_from != self.rename_to
+
@classmethod
- def _list_from_string(cls, repo, text):
+ def _index_from_patch_format(cls, repo, stream):
"""
- Create a new diff object from the given text
+ Create a new DiffIndex from the given text which must be in patch format
``repo``
is the repository we are operating on - it is required
- ``text``
- result of 'git diff' between two commits or one commit and the index
+ ``stream``
+ result of 'git diff' as a stream (supporting file protocol)
Returns
- git.Diff[]
+ git.DiffIndex
"""
- diffs = []
+ # for now, we have to bake the stream
+ text = stream.read()
+ index = DiffIndex()
diff_header = cls.re_header.match
for diff in ('\n' + text).split('\ndiff --git')[1:]:
@@ -97,9 +230,51 @@ class Diff(object):
a_blob_id, b_blob_id, b_mode = header.groups()
new_file, deleted_file = bool(new_file_mode), bool(deleted_file_mode)
- diffs.append(Diff(repo, a_path, b_path, a_blob_id, b_blob_id,
+ index.append(Diff(repo, a_path, b_path, a_blob_id, b_blob_id,
old_mode or deleted_file_mode, new_mode or new_file_mode or b_mode,
new_file, deleted_file, rename_from, rename_to, diff[header.end():]))
- return diffs
+ return index
+
+ @classmethod
+ def _index_from_raw_format(cls, repo, stream):
+ """
+ Create a new DiffIndex from the given stream which must be in raw format.
+
+ NOTE:
+ This format is inherently incapable of detecting renames, hence we only
+ modify, delete and add files
+
+ Returns
+ git.DiffIndex
+ """
+ # handles
+ # :100644 100644 6870991011cc8d9853a7a8a6f02061512c6a8190 37c5e30c879213e9ae83b21e9d11e55fc20c54b7 M .gitignore
+ index = DiffIndex()
+ for line in stream:
+ if not line.startswith(":"):
+ continue
+ # END its not a valid diff line
+ old_mode, new_mode, a_blob_id, b_blob_id, change_type, path = line[1:].split()
+ a_path = path
+ b_path = path
+ deleted_file = False
+ new_file = False
+
+ # NOTE: We cannot conclude from the existance of a blob to change type
+ # as diffs with the working do not have blobs yet
+ if change_type == 'D':
+ b_path = None
+ deleted_file = True
+ elif change_type == 'A':
+ a_path = None
+ new_file = True
+ # END add/remove handling
+
+ diff = Diff(repo, a_path, b_path, a_blob_id, b_blob_id, old_mode, new_mode,
+ new_file, deleted_file, None, None, '')
+ index.append(diff)
+ # END for each line
+
+ return index
diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py
index 3b48e066..ab1da7b0 100644
--- a/lib/git/objects/base.py
+++ b/lib/git/objects/base.py
@@ -15,22 +15,12 @@ class Object(LazyMixin):
This Object also serves as a constructor for instances of the correct type::
- inst = Object(repo,id)
+ inst = Object.new(repo,id)
"""
TYPES = ("blob", "tree", "commit", "tag")
__slots__ = ("repo", "id", "size", "data" )
type = None # to be set by subclass
- def __new__(cls, repo, id, *args, **kwargs):
- if cls is Object:
- hexsha, typename, size = repo.git.get_object_header(id)
- obj_type = utils.get_object_type_by_name(typename)
- inst = super(Object,cls).__new__(obj_type, repo, hexsha, *args, **kwargs)
- inst.size = size
- return inst
- else:
- return super(Object,cls).__new__(cls, repo, id, *args, **kwargs)
-
def __init__(self, repo, id):
"""
Initialize an object by identifying it by its id. All keyword arguments
@@ -45,7 +35,25 @@ class Object(LazyMixin):
super(Object,self).__init__()
self.repo = repo
self.id = id
-
+
+ @classmethod
+ def new(cls, repo, id):
+ """
+ Return
+ New Object instance of a type appropriate to the object type behind
+ id. The id of the newly created object will be a hexsha even though
+ the input id may have been a Reference or Rev-Spec
+
+ Note
+ This cannot be a __new__ method as it would always call __init__
+ with the input id which is not necessarily a hexsha.
+ """
+ hexsha, typename, size = repo.git.get_object_header(id)
+ obj_type = utils.get_object_type_by_name(typename)
+ inst = obj_type(repo, hexsha)
+ inst.size = size
+ return inst
+
def _set_self_from_args_(self, args_dict):
"""
Initialize attributes on self from the given dict that was retrieved
@@ -162,5 +170,4 @@ class IndexObject(Object):
mode += int(char) << iteration*3
# END for each char
return mode
-
diff --git a/lib/git/objects/commit.py b/lib/git/objects/commit.py
index 847f4dec..181cbb52 100644
--- a/lib/git/objects/commit.py
+++ b/lib/git/objects/commit.py
@@ -11,7 +11,7 @@ from tree import Tree
import base
import utils
-class Commit(base.Object, Iterable):
+class Commit(base.Object, Iterable, diff.Diffable):
"""
Wraps a git Commit object.
@@ -176,60 +176,6 @@ class Commit(base.Object, Iterable):
return self.iter_items( self.repo, self, paths, **kwargs )
- @classmethod
- def diff(cls, repo, a, b=None, paths=None):
- """
- Creates diffs between a tree and the index or between two trees:
-
- ``repo``
- is the Repo
-
- ``a``
- is a named commit
-
- ``b``
- is an optional named commit. Passing a list assumes you
- wish to omit the second named commit and limit the diff to the
- given paths.
-
- ``paths``
- is a list of paths to limit the diff to.
-
- Returns
- git.Diff[]::
-
- between tree and the index if only a is given
- between two trees if a and b are given and are commits
- """
- paths = paths or []
-
- if isinstance(b, list):
- paths = b
- b = None
-
- if paths:
- paths.insert(0, "--")
-
- if b:
- paths.insert(0, b)
- paths.insert(0, a)
- text = repo.git.diff('-M', full_index=True, *paths)
- return diff.Diff._list_from_string(repo, text)
-
- @property
- def diffs(self):
- """
- Returns
- git.Diff[]
- Diffs between this commit and its first parent or all changes if this
- commit is the first commit and has no parent.
- """
- if not self.parents:
- d = self.repo.git.show(self.id, '-M', full_index=True, pretty='raw')
- return diff.Diff._list_from_string(self.repo, d)
- else:
- return self.diff(self.repo, self.parents[0].id, self.id)
-
@property
def stats(self):
"""
@@ -268,6 +214,7 @@ class Commit(base.Object, Iterable):
if not hasattr(stream,'next'):
stream = proc_or_stream.stdout
+
for line in stream:
id = line.split()[1]
assert line.split()[0] == "commit"
diff --git a/lib/git/objects/tree.py b/lib/git/objects/tree.py
index abfa9622..c35c075e 100644
--- a/lib/git/objects/tree.py
+++ b/lib/git/objects/tree.py
@@ -8,6 +8,7 @@ import os
import blob
import base
import binascii
+import git.diff as diff
def sha_to_hex(sha):
"""Takes a string and returns the hex of the sha within"""
@@ -15,7 +16,7 @@ def sha_to_hex(sha):
assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % hexsha
return hexsha
-class Tree(base.IndexObject):
+class Tree(base.IndexObject, diff.Diffable):
"""
Tress represent a ordered list of Blobs and other Trees. Hence it can be
accessed like a list.
@@ -169,6 +170,7 @@ class Tree(base.IndexObject):
def traverse(self, max_depth=-1, predicate = lambda i: True):
"""
Returns
+
Iterator to traverse the tree recursively up to the given level.
The iterator returns Blob and Tree objects
diff --git a/lib/git/refs.py b/lib/git/refs.py
index a4d7bbb1..4445f252 100644
--- a/lib/git/refs.py
+++ b/lib/git/refs.py
@@ -72,7 +72,7 @@ class Reference(LazyMixin, Iterable):
"""
# have to be dynamic here as we may be a tag which can point to anything
# Our path will be resolved to the hexsha which will be used accordingly
- return Object(self.repo, self.path)
+ return Object.new(self.repo, self.path)
@classmethod
def iter_items(cls, repo, common_path = "refs", **kwargs):
diff --git a/lib/git/repo.py b/lib/git/repo.py
index 6edb7f62..cc4a6c6b 100644
--- a/lib/git/repo.py
+++ b/lib/git/repo.py
@@ -107,6 +107,15 @@ class Repo(object):
branches = heads
@property
+ def head(self):
+ """
+ Return
+ Head Object, reference pointing to the current head of the repository
+ """
+ return Head(self,'HEAD')
+
+
+ @property
def tags(self):
"""
A list of ``Tag`` objects that are available in this repo
@@ -129,7 +138,7 @@ class Repo(object):
if rev is None:
rev = self.active_branch
- c = Object(self, rev)
+ c = Object.new(self, rev)
assert c.type == "commit", "Revision %s did not point to a commit, but to %s" % (rev, c)
return c
@@ -299,7 +308,7 @@ class Repo(object):
ignored files will not appear here, i.e. files mentioned in .gitignore
"""
# make sure we get all files, no only untracked directores
- proc = self.git.commit(untracked_files=True, as_process=True)
+ proc = self.git.status(untracked_files=True, as_process=True)
stream = iter(proc.stdout)
untracked_files = list()
for line in stream:
@@ -327,34 +336,7 @@ class Repo(object):
"""
return Head( self, self.git.symbolic_ref('HEAD').strip() )
-
- def diff(self, a, b, *paths):
- """
- The diff from commit ``a`` to commit ``b``, optionally restricted to the given file(s)
-
- ``a``
- is the base commit
- ``b``
- is the other commit
-
- ``paths``
- is an optional list of file paths on which to restrict the diff
- Returns
- ``str``
- """
- return self.git.diff(a, b, '--', *paths)
-
- def commit_diff(self, commit):
- """
- The commit diff for the given commit
- ``commit`` is the commit name/id
-
- Returns
- ``git.Diff[]``
- """
- return Commit.diff(self, commit)
-
def blame(self, rev, file):
"""
The blame information for the given file at the given revision.
diff --git a/test/git/test_base.py b/test/git/test_base.py
index 04222e2e..71576048 100644
--- a/test/git/test_base.py
+++ b/test/git/test_base.py
@@ -90,3 +90,6 @@ class TestBase(object):
assert_raises( ValueError, get_object_type_by_name, "doesntexist" )
+ def test_object_resolution(self):
+ # objects must be resolved to shas so they compare equal
+ assert self.repo.head.object == self.repo.active_branch.object
diff --git a/test/git/test_commit.py b/test/git/test_commit.py
index 4e698ed0..c8bca564 100644
--- a/test/git/test_commit.py
+++ b/test/git/test_commit.py
@@ -20,156 +20,6 @@ class TestCommit(object):
assert_equal("byronimo@gmail.com", commit.author.email)
- @patch_object(Git, '_call_process')
- def test_diff(self, git):
- git.return_value = fixture('diff_p')
-
- diffs = Commit.diff(self.repo, 'master')
-
- assert_equal(15, len(diffs))
-
- diff = diffs[0]
- assert_equal('.gitignore', diff.a_blob.path)
- assert_equal('.gitignore', diff.b_blob.path)
- assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diff.a_blob.id)
- assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diff.b_blob.id)
-
- assert_mode_644(diff.b_blob.mode)
-
- assert_equal(False, diff.new_file)
- assert_equal(False, diff.deleted_file)
- assert_equal("--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1,2 @@\n coverage\n+pkg", diff.diff)
-
- diff = diffs[5]
- assert_equal('lib/grit/actor.rb', diff.b_blob.path)
- assert_equal(None, diff.a_blob)
- assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diff.b_blob.id)
- assert_equal( None, diff.a_mode )
- assert_equal(True, diff.new_file)
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', '-M', 'master'), {'full_index': True}))
-
- @patch_object(Git, '_call_process')
- def test_diff_with_rename(self, git):
- git.return_value = fixture('diff_rename')
-
- diffs = Commit.diff(self.repo, 'rename')
-
- assert_equal(1, len(diffs))
-
- diff = diffs[0]
- assert_true(diff.renamed)
- assert_equal(diff.rename_from, 'AUTHORS')
- assert_equal(diff.rename_to, 'CONTRIBUTORS')
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', '-M', 'rename'), {'full_index': True}))
-
- @patch_object(Git, '_call_process')
- def test_diff_with_two_commits(self, git):
- git.return_value = fixture('diff_2')
-
- diffs = Commit.diff(self.repo, '59ddc32', '13d27d5')
-
- assert_equal(3, len(diffs))
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', '-M', '59ddc32', '13d27d5'), {'full_index': True}))
-
- @patch_object(Git, '_call_process')
- def test_diff_with_files(self, git):
- git.return_value = fixture('diff_f')
-
- diffs = Commit.diff(self.repo, '59ddc32', ['lib'])
-
- assert_equal(1, len(diffs))
- assert_equal('lib/grit/diff.rb', diffs[0].a_blob.path)
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', '-M', '59ddc32', '--', 'lib'), {'full_index': True}))
-
- @patch_object(Git, '_call_process')
- def test_diff_with_two_commits_and_files(self, git):
- git.return_value = fixture('diff_2f')
-
- diffs = Commit.diff(self.repo, '59ddc32', '13d27d5', ['lib'])
-
- assert_equal(1, len(diffs))
- assert_equal('lib/grit/commit.rb', diffs[0].a_blob.path)
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', '-M', '59ddc32', '13d27d5', '--', 'lib'), {'full_index': True}))
-
- @patch_object(Git, '_call_process')
- def test_diffs(self, git):
- git.return_value = fixture('diff_p')
-
- commit = Commit(self.repo, id='91169e1f5fa4de2eaea3f176461f5dc784796769', parents=['038af8c329ef7c1bae4568b98bd5c58510465493'])
- diffs = commit.diffs
-
- assert_equal(15, len(diffs))
-
- diff = diffs[0]
- assert_equal('.gitignore', diff.a_blob.path)
- assert_equal('.gitignore', diff.b_blob.path)
- assert_equal('4ebc8aea50e0a67e000ba29a30809d0a7b9b2666', diff.a_blob.id)
- assert_equal('2dd02534615434d88c51307beb0f0092f21fd103', diff.b_blob.id)
- assert_mode_644(diff.b_blob.mode)
- assert_equal(False, diff.new_file)
- assert_equal(False, diff.deleted_file)
- assert_equal("--- a/.gitignore\n+++ b/.gitignore\n@@ -1 +1,2 @@\n coverage\n+pkg", diff.diff)
-
- diff = diffs[5]
- assert_equal('lib/grit/actor.rb', diff.b_blob.path)
- assert_equal(None, diff.a_blob)
- assert_equal('f733bce6b57c0e5e353206e692b0e3105c2527f4', diff.b_blob.id)
- assert_equal(True, diff.new_file)
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', '-M',
- '038af8c329ef7c1bae4568b98bd5c58510465493',
- '91169e1f5fa4de2eaea3f176461f5dc784796769',
- ), {'full_index': True}))
-
- def test_diffs_on_initial_import(self):
- commit = Commit(self.repo, '33ebe7acec14b25c5f84f35a664803fcab2f7781')
-
- for diff in commit.diffs:
- assert isinstance(diff, Diff)
- assert isinstance(diff.a_blob, Blob) or isinstance(diff.b_blob, Blob)
-
- if diff.a_mode is not None:
- assert isinstance(diff.a_mode, int)
- if diff.b_mode is not None:
- isinstance(diff.b_mode, int)
-
- assert diff.diff is not None # can be empty
-
- if diff.renamed:
- assert diff.rename_from and diff.rename_to and diff.rename_from != diff.rename_to
- if diff.a_blob is None:
- assert diff.new_file and isinstance(diff.new_file, bool)
- if diff.b_blob is None:
- assert diff.deleted_file and isinstance(diff.deleted_file, bool)
- # END for each diff in initial import commit
-
- def test_diffs_on_initial_import_without_parents(self):
- commit = Commit(self.repo, id='33ebe7acec14b25c5f84f35a664803fcab2f7781')
- diffs = commit.diffs
- assert diffs
-
- def test_diffs_with_mode_only_change(self):
- commit = Commit(self.repo, id='ccde80b7a3037a004a7807a6b79916ce2a1e9729')
- diffs = commit.diffs
-
- # in case of mode-only changes, there is no blob
- assert_equal(1, len(diffs))
- assert_equal(None, diffs[0].a_blob)
- assert_equal(None, diffs[0].b_blob)
- assert_mode_644(diffs[0].a_mode)
- assert_mode_755(diffs[0].b_mode)
-
def test_stats(self):
commit = Commit(self.repo, id='33ebe7acec14b25c5f84f35a664803fcab2f7781')
stats = commit.stats
diff --git a/test/git/test_diff.py b/test/git/test_diff.py
index b2339455..deae7cfc 100644
--- a/test/git/test_diff.py
+++ b/test/git/test_diff.py
@@ -7,19 +7,19 @@
from test.testlib import *
from git import *
-class TestDiff(object):
- def setup(self):
+class TestDiff(TestCase):
+ def setUp(self):
self.repo = Repo(GIT_REPO)
def test_list_from_string_new_mode(self):
- output = fixture('diff_new_mode')
- diffs = Diff._list_from_string(self.repo, output)
+ output = ListProcessAdapter(fixture('diff_new_mode'))
+ diffs = Diff._index_from_patch_format(self.repo, output.stdout)
assert_equal(1, len(diffs))
assert_equal(10, len(diffs[0].diff.splitlines()))
def test_diff_with_rename(self):
- output = fixture('diff_rename')
- diffs = Diff._list_from_string(self.repo, output)
+ output = ListProcessAdapter(fixture('diff_rename'))
+ diffs = Diff._index_from_patch_format(self.repo, output.stdout)
assert_equal(1, len(diffs))
@@ -28,3 +28,49 @@ class TestDiff(object):
assert_equal(diff.rename_from, 'AUTHORS')
assert_equal(diff.rename_to, 'CONTRIBUTORS')
+ def test_diff_patch_format(self):
+ # test all of the 'old' format diffs for completness - it should at least
+ # be able to deal with it
+ fixtures = ("diff_2", "diff_2f", "diff_f", "diff_i", "diff_mode_only",
+ "diff_new_mode", "diff_numstat", "diff_p", "diff_rename",
+ "diff_tree_numstat_root" )
+
+ for fixture_name in fixtures:
+ diff_proc = ListProcessAdapter(fixture(fixture_name))
+ diffs = Diff._index_from_patch_format(self.repo, diff_proc.stdout)
+ # END for each fixture
+
+ def test_diff_interface(self):
+ # test a few variations of the main diff routine
+ assertion_map = dict()
+ for i, commit in enumerate(self.repo.iter_commits('0.1.6', max_count=10)):
+ diff_item = commit
+ if i%2 == 0:
+ diff_item = commit.tree
+ # END use tree every second item
+
+ for other in (None, commit.Index, commit.parents[0]):
+ for paths in (None, "CHANGES", ("CHANGES", "lib")):
+ for create_patch in range(2):
+ diff_index = diff_item.diff(other, paths, create_patch)
+ assert isinstance(diff_index, DiffIndex)
+
+ if diff_index:
+ for ct in DiffIndex.change_type:
+ key = 'ct_%s'%ct
+ assertion_map.setdefault(key, 0)
+ assertion_map[key] = assertion_map[key]+len(list(diff_index.iter_change_type(ct)))
+ # END for each changetype
+ # END diff index checking
+ # END for each patch option
+ # END for each path option
+ # END for each other side
+ # END for each commit
+
+ # assert we could always find at least one instance of the members we
+ # can iterate in the diff index - if not this indicates its not working correctly
+ # or our test does not span the whole range of possibilities
+ for key,value in assertion_map.items():
+ assert value, "Did not find diff for %s" % key
+ # END for each iteration type
+
diff --git a/test/git/test_repo.py b/test/git/test_repo.py
index 250974a5..87332067 100644
--- a/test/git/test_repo.py
+++ b/test/git/test_repo.py
@@ -43,7 +43,7 @@ class TestRepo(object):
git.return_value = ListProcessAdapter(fixture('rev_list'))
commits = list( self.repo.iter_commits('master', max_count=10) )
-
+
c = commits[0]
assert_equal('4c8124ffcf4039d292442eeccabdeca5af5c5017', c.id)
assert_equal(["634396b2f541a9f2d58b00be1a07f0c358b999b3"], [p.id for p in c.parents])
@@ -116,40 +116,6 @@ class TestRepo(object):
{ 'template': '/awesome'}))
assert_true(repo.called)
- @patch_object(Git, '_call_process')
- def test_diff(self, git):
- self.repo.diff('master^', 'master')
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', 'master^', 'master', '--'), {}))
-
- self.repo.diff('master^', 'master', 'foo/bar')
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', 'master^', 'master', '--', 'foo/bar'), {}))
-
- self.repo.diff('master^', 'master', 'foo/bar', 'foo/baz')
-
- assert_true(git.called)
- assert_equal(git.call_args, (('diff', 'master^', 'master', '--', 'foo/bar', 'foo/baz'), {}))
-
- @patch_object(Git, '_call_process')
- def test_diff_with_parents(self, git):
- git.return_value = fixture('diff_p')
-
- diffs = self.repo.commit_diff('master')
- assert_equal(15, len(diffs))
- assert_true(git.called)
-
- def test_archive(self):
- args = ( tuple(), (self.repo.heads[-1],),(None,"hello") )
- for arg_list in args:
- ftmp = os.tmpfile()
- self.repo.archive(ftmp, *arg_list)
- ftmp.seek(0,2)
- assert ftmp.tell()
- # END for each arg-list
-
@patch('git.utils.touch')
def test_enable_daemon_serve(self, touch):
self.repo.daemon_serve = False
@@ -199,6 +165,9 @@ class TestRepo(object):
assert_equal(self.repo.active_branch.name, 'major-refactoring')
assert_equal(git.call_args, (('symbolic_ref', 'HEAD'), {}))
+ def test_head(self):
+ assert self.repo.head.object == self.repo.active_branch.object
+
@patch_object(Git, '_call_process')
def test_should_display_blame_information(self, git):
git.return_value = fixture('blame')
diff --git a/test/testlib/helper.py b/test/testlib/helper.py
index b66d3eaa..c4c0f2ba 100644
--- a/test/testlib/helper.py
+++ b/test/testlib/helper.py
@@ -23,8 +23,32 @@ class ListProcessAdapter(object):
"""Allows to use lists as Process object as returned by SubProcess.Popen.
Its tailored to work with the test system only"""
+ class Stream(object):
+ """Simple stream emulater meant to work only with tests"""
+ def __init__(self, data):
+ self.data = data
+ self.cur_iter = None
+
+ def __iter__(self):
+ dat = self.data
+ if isinstance(dat, basestring):
+ dat = dat.splitlines()
+ if self.cur_iter is None:
+ self.cur_iter = iter(dat)
+ return self.cur_iter
+
+ def read(self):
+ dat = self.data
+ if isinstance(dat, (tuple,list)):
+ dat = "\n".join(dat)
+ return dat
+
+ def next(self):
+ if self.cur_iter is None:
+ self.cur_iter = iter(self)
+ return self.cur_iter.next()
+
+ # END stream
+
def __init__(self, input_list_or_string):
- l = input_list_or_string
- if isinstance(l,basestring):
- l = l.splitlines()
- self.stdout = iter(l)
+ self.stdout = self.Stream(input_list_or_string)