diff options
| -rw-r--r-- | CHANGES | 8 | ||||
| -rw-r--r-- | TODO | 25 | ||||
| -rw-r--r-- | lib/git/cmd.py | 57 | ||||
| -rw-r--r-- | lib/git/objects/base.py | 24 | ||||
| -rw-r--r-- | lib/git/objects/utils.py | 18 | ||||
| -rw-r--r-- | lib/git/remote.py | 66 | ||||
| -rw-r--r-- | lib/git/repo.py | 25 | ||||
| -rw-r--r-- | test/git/test_base.py | 18 | ||||
| -rw-r--r-- | test/git/test_diff.py | 3 | ||||
| -rw-r--r-- | test/git/test_remote.py | 7 | ||||
| -rw-r--r-- | test/git/test_repo.py | 5 |
11 files changed, 209 insertions, 47 deletions
@@ -30,6 +30,8 @@ General * 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. +* Git.execute does not take the with_raw_output option anymore. It was not used + by anyone within the project and False by default. Item Iteration @@ -50,6 +52,12 @@ Blob ---- * former 'name' member renamed to path as it suits the actual data better +GitCommand +----------- +* git.subcommand call scheme now prunes out None from the argument list, allowing + to be called more confortably as None can never be a valid to the git command + if converted to a string. + Commit ------ * 'count' method is not an instance method to increase its ease of use @@ -14,6 +14,18 @@ General deleted. * References should be parsed 'manually' to get around command invocation, but be sure to be able to read packed refs. + +Object +------ +* DataStream method should read the data itself. This would be easy once you have + the actul loose object, but will be hard if it is in a pack. In a distant future, + we might be able to do that or at least implement direct object reading for loose + objects ( to safe a command call ). Currently object information comes from + persistent commands anyway, so the penalty is not that high. The data_stream + though is not based on persistent commands. + It would be good to improve things there as cat-file keeps all the data in a buffer + before it writes it. Hence it does not write to a stream directly, which can be + bad if files are large, say 1GB :). * Effectively Objects only store hexsha's in their id attributes, so in fact 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. @@ -45,6 +57,11 @@ Index creating several tree objects, so in the end it might be slower. Hmm, probably its okay to use the command unless we go c(++) +Remote +------ +* 'push' method needs a test, a true test repository is required though, a fork + of a fork would do :)! + Repo ---- * Nice fetch/pull handling, at least supported/wired throuhg to the git command @@ -61,5 +78,13 @@ Tree * Should return submodules during iteration ( identifies as commit ) * Work through test and check for test-case cleanup and completeness ( what about testing whether it raises on invalid input ? ). See 6dc7799d44e1e5b9b77fd19b47309df69ec01a99 + +Testing +------- +* Create a test-repository that can be written to and changed in addition to the normal + read-only testing scheme that operates on the own repository. Doing this could be a simple + as forking a shared repo in a tmp directory. In that moment, we probably want to + facility committing and checkouts as well. + - Use these tests for git-remote as we need to test push - Also assure that the test-case setup is a bit more consistent ( Derive from TestCase, possibly make repo a class member instead of an instance member diff --git a/lib/git/cmd.py b/lib/git/cmd.py index d04a2bd0..88d6008a 100644 --- a/lib/git/cmd.py +++ b/lib/git/cmd.py @@ -13,7 +13,7 @@ from errors import GitCommandError GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output', - 'with_exceptions', 'with_raw_output', 'as_process', + 'with_exceptions', 'as_process', 'output_stream' ) extra = {} @@ -105,7 +105,6 @@ class Git(object): with_keep_cwd=False, with_extended_output=False, with_exceptions=True, - with_raw_output=False, as_process=False, output_stream=None ): @@ -132,27 +131,33 @@ class Git(object): ``with_exceptions`` Whether to raise an exception when git returns a non-zero status. - ``with_raw_output`` - Whether to avoid stripping off trailing whitespace. - ``as_process`` Whether to return the created process instance directly from which - streams can be read on demand. This will render with_extended_output, - with_exceptions and with_raw_output ineffective - the caller will have + streams can be read on demand. This will render with_extended_output and + with_exceptions ineffective - the caller will have to deal with the details himself. It is important to note that the process will be placed into an AutoInterrupt wrapper that will interrupt the process once it goes out of scope. If you use the command in iterators, you should pass the whole process instance instead of a single stream. + ``output_stream`` If set to a file-like object, data produced by the git command will be - output to the given stream directly. - Otherwise a new file will be opened. + output to the given stream directly. + This feature only has any effect if as_process is False. Processes will + always be created with a pipe due to issues with subprocess. + This merely is a workaround as data will be copied from the + output pipe to the given output stream directly. + Returns:: - str(output) # extended_output = False (Default) + str(output) # extended_output = False (Default) tuple(int(status), str(stdout), str(stderr)) # extended_output = True + + if ouput_stream is True, the stdout value will be your output stream: + output_stream # extended_output = False + tuple(int(status), output_stream, str(stderr))# extended_output = True Raise GitCommandError @@ -170,37 +175,39 @@ class Git(object): else: cwd=self.git_dir - ostream = subprocess.PIPE - if output_stream is not None: - ostream = output_stream - # Start the process proc = subprocess.Popen(command, cwd=cwd, stdin=istream, stderr=subprocess.PIPE, - stdout=ostream, + stdout=subprocess.PIPE, **extra ) - if as_process: return self.AutoInterrupt(proc) # Wait for the process to return status = 0 try: - stdout_value = proc.stdout.read() + if output_stream is None: + stdout_value = proc.stdout.read() + else: + max_chunk_size = 1024*64 + while True: + chunk = proc.stdout.read(max_chunk_size) + output_stream.write(chunk) + if len(chunk) < max_chunk_size: + break + # END reading output stream + stdout_value = output_stream + # END stdout handling stderr_value = proc.stderr.read() + # waiting here should do nothing as we have finished stream reading status = proc.wait() finally: proc.stdout.close() proc.stderr.close() - # Strip off trailing whitespace by default - if not with_raw_output: - stdout_value = stdout_value.rstrip() - stderr_value = stderr_value.rstrip() - if with_exceptions and status != 0: raise GitCommandError(command, status, stderr_value) @@ -261,7 +268,9 @@ class Git(object): such as in 'ls_files' to call 'ls-files'. ``args`` - is the list of arguments + is the list of arguments. If None is included, it will be pruned. + This allows your commands to call git more conveniently as None + is realized as non-existent ``kwargs`` is a dict of keyword arguments. @@ -287,7 +296,7 @@ class Git(object): # Prepare the argument list opt_args = self.transform_kwargs(**kwargs) - ext_args = self.__unpack_args(args) + ext_args = self.__unpack_args([a for a in args if a is not None]) args = opt_args + ext_args call = ["git", dashify(method)] diff --git a/lib/git/objects/base.py b/lib/git/objects/base.py index ab1da7b0..0dfd1a23 100644 --- a/lib/git/objects/base.py +++ b/lib/git/objects/base.py @@ -16,6 +16,9 @@ class Object(LazyMixin): This Object also serves as a constructor for instances of the correct type:: inst = Object.new(repo,id) + inst.id # objects sha in hex + inst.size # objects uncompressed data size + inst.data # byte string containing the whole data of the object """ TYPES = ("blob", "tree", "commit", "tag") __slots__ = ("repo", "id", "size", "data" ) @@ -115,6 +118,27 @@ class Object(LazyMixin): """ return '<git.%s "%s">' % (self.__class__.__name__, self.id) + @property + def data_stream(self): + """ + Returns + File Object compatible stream to the uncompressed raw data of the object + """ + proc = self.repo.git.cat_file(self.type, self.id, as_process=True) + return utils.ProcessStreamAdapter(proc, "stdout") + + def stream_data(self, ostream): + """ + Writes our data directly to the given output stream + + ``ostream`` + File object compatible stream object. + + Returns + self + """ + self.repo.git.cat_file(self.type, self.id, output_stream=ostream) + return self class IndexObject(Object): """ diff --git a/lib/git/objects/utils.py b/lib/git/objects/utils.py index 367ed2b7..7bb4e8e2 100644 --- a/lib/git/objects/utils.py +++ b/lib/git/objects/utils.py @@ -52,3 +52,21 @@ def parse_actor_and_date(line): m = _re_actor_epoch.search(line) actor, epoch = m.groups() return (Actor._from_string(actor), int(epoch)) + + + +class ProcessStreamAdapter(object): + """ + Class wireing all calls to the contained Process instance. + + Use this type to hide the underlying process to provide access only to a specified + stream. The process is usually wrapped into an AutoInterrupt class to kill + it if the instance goes out of scope. + """ + __slots__ = ("_proc", "_stream") + def __init__(self, process, stream_name): + self._proc = process + self._stream = getattr(process, stream_name) + + def __getattr__(self, attr): + return getattr(self._stream, attr) diff --git a/lib/git/remote.py b/lib/git/remote.py index 6a9c0efb..7febf2ee 100644 --- a/lib/git/remote.py +++ b/lib/git/remote.py @@ -7,7 +7,7 @@ Module implementing a remote object allowing easy access to git remotes """ -from git.utils import LazyMixin, Iterable +from git.utils import LazyMixin, Iterable, IterableList from refs import RemoteReference class _SectionConstraint(object): @@ -121,7 +121,7 @@ class Remote(LazyMixin, Iterable): Returns List of RemoteRef objects """ - out_refs = list() + out_refs = IterableList(RemoteReference._id_attribute_) for ref in RemoteReference.list_items(self.repo): if ref.remote_name == self.name: out_refs.append(ref) @@ -185,7 +185,9 @@ class Remote(LazyMixin, Iterable): def update(self, **kwargs): """ - Fetch all changes for this remote, including new branches + Fetch all changes for this remote, including new branches which will + be forced in ( in case your local remote branch is not part the new remote branches + ancestry anymore ). ``kwargs`` Additional arguments passed to git-remote update @@ -196,6 +198,64 @@ class Remote(LazyMixin, Iterable): self.repo.git.remote("update", self.name) return self + def fetch(self, refspec=None, **kwargs): + """ + Fetch the latest changes for this remote + + ``refspec`` + A "refspec" is used by fetch and push to describe the mapping + between remote ref and local ref. They are combined with a colon in + the format <src>:<dst>, preceded by an optional plus sign, +. + For example: git fetch $URL refs/heads/master:refs/heads/origin means + "grab the master branch head from the $URL and store it as my origin + branch head". And git push $URL refs/heads/master:refs/heads/to-upstream + means "publish my master branch head as to-upstream branch at $URL". + See also git-push(1). + + Taken from the git manual + + ``**kwargs`` + Additional arguments to be passed to git-fetch + + Returns + self + """ + self.repo.git.fetch(self, refspec, **kwargs) + return self + + def pull(self, refspec=None, **kwargs): + """ + Pull changes from the given branch, being the same as a fetch followed + by a merge of branch with your local branch. + + ``refspec`` + see 'fetch' method + + ``**kwargs`` + Additional arguments to be passed to git-pull + + Returns + self + """ + self.repo.git.pull(self, refspec, **kwargs) + return self + + def push(self, refspec=None, **kwargs): + """ + Push changes from source branch in refspec to target branch in refspec. + + ``refspec`` + see 'fetch' method + + ``**kwargs`` + Additional arguments to be passed to git-push + + Returns + self + """ + self.repo.git.push(self, refspec, **kwargs) + return self + @property def config_reader(self): """ diff --git a/lib/git/repo.py b/lib/git/repo.py index 37847c98..b6624d8b 100644 --- a/lib/git/repo.py +++ b/lib/git/repo.py @@ -19,7 +19,7 @@ from config import GitConfigParser from remote import Remote def touch(filename): - fp = open(filename, "w") + fp = open(filename, "a") fp.close() def is_git_dir(d): @@ -432,11 +432,13 @@ class Repo(object): # start from the one which is fastest to evaluate default_args = ('--abbrev=40', '--full-index', '--raw') if index: + # diff index against HEAD if len(self.git.diff('HEAD', '--cached', *default_args)): return True # END index handling if working_tree: - if len(self.git.diff('HEAD', *default_args)): + # diff index against working tree + if len(self.git.diff(*default_args)): return True # END working tree handling if untracked_files: @@ -639,32 +641,23 @@ class Repo(object): Examples:: - >>> repo.archive(open("archive" + >>> repo.archive(open("archive")) <String containing tar.gz archive> - >>> repo.archive_tar_gz('a87ff14') - <String containing tar.gz archive for commit a87ff14> - - >>> repo.archive_tar_gz('master', 'myproject/') - <String containing tar.gz archive and prefixed with 'myproject/'> - Raise GitCommandError in case something went wrong + Returns + self """ if treeish is None: treeish = self.active_branch if prefix and 'prefix' not in kwargs: kwargs['prefix'] = prefix - kwargs['as_process'] = True kwargs['output_stream'] = ostream - proc = self.git.archive(treeish, **kwargs) - status = proc.wait() - if status != 0: - raise GitCommandError( "git-archive", status, proc.stderr.read() ) - - + self.git.archive(treeish, **kwargs) + return self def __repr__(self): return '<git.Repo "%s">' % self.path diff --git a/test/git/test_base.py b/test/git/test_base.py index 71576048..b93e61c1 100644 --- a/test/git/test_base.py +++ b/test/git/test_base.py @@ -4,12 +4,15 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php -from test.testlib import * -from git import * import git.objects.base as base import git.refs as refs +import os + +from test.testlib import * +from git import * from itertools import chain from git.objects.utils import get_object_type_by_name +import tempfile class TestBase(object): @@ -48,6 +51,17 @@ class TestBase(object): assert not item.path.startswith("/") # must be relative assert isinstance(item.mode, int) # END index object check + + # read from stream + data_stream = item.data_stream + data = data_stream.read() + assert data + + tmpfile = os.tmpfile() + assert item == item.stream_data(tmpfile) + tmpfile.seek(0) + assert tmpfile.read() == data + # END stream to file directly # END for each object type to create # each has a unique sha diff --git a/test/git/test_diff.py b/test/git/test_diff.py index deae7cfc..501d937d 100644 --- a/test/git/test_diff.py +++ b/test/git/test_diff.py @@ -74,3 +74,6 @@ class TestDiff(TestCase): assert value, "Did not find diff for %s" % key # END for each iteration type + def test_diff_index_working_tree(self): + self.fail("""Find a good way to diff an index against the working tree +which is not possible with the current interface""") diff --git a/test/git/test_remote.py b/test/git/test_remote.py index 4cbb0b7b..aeb6b4af 100644 --- a/test/git/test_remote.py +++ b/test/git/test_remote.py @@ -32,7 +32,8 @@ class TestRemote(TestCase): # END for each ref # OPTIONS - for opt in ("url", "fetch"): + # cannot use 'fetch' key anymore as it is now a method + for opt in ("url", ): val = getattr(remote, opt) reader = remote.config_reader assert reader.get(opt) == val @@ -61,8 +62,10 @@ class TestRemote(TestCase): assert remote.rename(prev_name).name == prev_name # END for each rename ( back to prev_name ) + remote.fetch() + self.failUnlessRaises(GitCommandError, remote.pull) remote.update() - + self.fail("test push once there is a test-repo") # END for each remote assert num_remotes assert num_remotes == len(remote_set) diff --git a/test/git/test_repo.py b/test/git/test_repo.py index bc2c7094..ff10f6a6 100644 --- a/test/git/test_repo.py +++ b/test/git/test_repo.py @@ -184,6 +184,11 @@ class TestRepo(TestCase): def test_tag(self): assert self.repo.tag('0.1.5').commit + def test_archive(self): + tmpfile = os.tmpfile() + self.repo.archive(tmpfile, '0.1.5') + assert tmpfile.tell() + @patch_object(Git, '_call_process') def test_should_display_blame_information(self, git): git.return_value = fixture('blame') |
