aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES8
-rw-r--r--TODO25
-rw-r--r--lib/git/cmd.py57
-rw-r--r--lib/git/objects/base.py24
-rw-r--r--lib/git/objects/utils.py18
-rw-r--r--lib/git/remote.py66
-rw-r--r--lib/git/repo.py25
-rw-r--r--test/git/test_base.py18
-rw-r--r--test/git/test_diff.py3
-rw-r--r--test/git/test_remote.py7
-rw-r--r--test/git/test_repo.py5
11 files changed, 209 insertions, 47 deletions
diff --git a/CHANGES b/CHANGES
index a60ed237..fde62e29 100644
--- a/CHANGES
+++ b/CHANGES
@@ -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
diff --git a/TODO b/TODO
index 14d65e26..afda659a 100644
--- a/TODO
+++ b/TODO
@@ -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')