diff options
-rw-r--r-- | CHANGES | 12 | ||||
-rw-r--r-- | lib/git/__init__.py | 2 | ||||
-rw-r--r-- | lib/git/diff.py | 203 | ||||
-rw-r--r-- | lib/git/objects/base.py | 33 | ||||
-rw-r--r-- | lib/git/objects/commit.py | 57 | ||||
-rw-r--r-- | lib/git/objects/tree.py | 4 | ||||
-rw-r--r-- | lib/git/refs.py | 2 | ||||
-rw-r--r-- | lib/git/repo.py | 40 | ||||
-rw-r--r-- | test/git/test_base.py | 3 | ||||
-rw-r--r-- | test/git/test_commit.py | 150 | ||||
-rw-r--r-- | test/git/test_diff.py | 58 | ||||
-rw-r--r-- | test/git/test_repo.py | 39 | ||||
-rw-r--r-- | test/testlib/helper.py | 32 |
13 files changed, 326 insertions, 309 deletions
@@ -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) |