Skip to content

Commit

Permalink
cmd: added option to return the process directly, allowing to read th…
Browse files Browse the repository at this point in the history
…e output directly from the output stream

commit: now reads commit information directly from the output stream of the process by implementing its iterator method
repo: removed log method as it was redundant ( equal to the commits method )
  • Loading branch information
Byron committed Oct 14, 2009
1 parent ac1cec7 commit ead94f2
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 67 deletions.
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Repo
of the active branch.
* tree method now requires a Ref instance as input and defaults to the active_branche
instead of master
* Removed 'log' method as it as effectively the same as the 'commits' method

Diff
----
Expand Down
46 changes: 45 additions & 1 deletion lib/git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False)

execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output',
'with_exceptions', 'with_raw_output')
'with_exceptions', 'with_raw_output', 'as_process')

extra = {}
if sys.platform == 'win32':
Expand All @@ -34,6 +34,35 @@ class Git(object):
of the command to stdout.
Set its value to 'full' to see details about the returned values.
"""

class AutoInterrupt(object):
"""
Kill/Interrupt the stored process instance once this instance goes out of scope. It is
used to prevent processes piling up in case iterators stop reading.
Besides all attributes are wired through to the contained process object
"""
__slots__= "proc"

def __init__(self, proc ):
self.proc = proc

def __del__(self):
# did the process finish already so we have a return code ?
if self.proc.poll() is not None:
return

# try to kill it
try:
os.kill(self.proc.pid, 2) # interrupt signal
except AttributeError:
# try windows
subprocess.call(("TASKKILL", "/T", "/PID", self.proc.pid))
# END exception handling

def __getattr__(self, attr):
return getattr(self.proc, attr)


def __init__(self, git_dir=None):
"""
Initialize this instance with:
Expand Down Expand Up @@ -70,6 +99,7 @@ def execute(self, command,
with_extended_output=False,
with_exceptions=True,
with_raw_output=False,
as_process=False
):
"""
Handles executing the command on the shell and consumes and returns
Expand All @@ -96,6 +126,16 @@ def execute(self, command,
``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
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.
Returns::
Expand Down Expand Up @@ -127,7 +167,11 @@ def execute(self, command,
**extra
)

if as_process:
return self.AutoInterrupt(proc)

# Wait for the process to return
status = 0
try:
stdout_value = proc.stdout.read()
stderr_value = proc.stderr.read()
Expand Down
14 changes: 8 additions & 6 deletions lib/git/objects/commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,26 +142,28 @@ def iter_items(cls, repo, ref, path='', **kwargs):
Returns
iterator yielding Commit items
"""
options = {'pretty': 'raw'}
options = {'pretty': 'raw', 'as_process' : True }
options.update(kwargs)

output = repo.git.rev_list(ref, '--', path, **options)
return cls._iter_from_stream(repo, iter(output.splitlines(False)))
# the test system might confront us with string values -
proc = repo.git.rev_list(ref, '--', path, **options)
return cls._iter_from_process(repo, proc)

@classmethod
def _iter_from_stream(cls, repo, stream):
def _iter_from_process(cls, repo, proc):
"""
Parse out commit information into a list of Commit objects
``repo``
is the Repo
``stream``
output stream from the git-rev-list command (raw format)
``proc``
git-rev-list process instance (raw format)
Returns
iterator returning Commit objects
"""
stream = proc.stdout
for line in stream:
id = line.split()[1]
assert line.split()[0] == "commit"
Expand Down
20 changes: 0 additions & 20 deletions lib/git/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,26 +347,6 @@ def tree(self, treeish=None):
return root


def log(self, commit='master', path=None, **kwargs):
"""
The Commit for a treeish, and all commits leading to it.
``kwargs``
keyword arguments specifying flags to be used in git-log command,
i.e.: max_count=1 to limit the amount of commits returned
Returns
``git.Commit[]``
"""
options = {'pretty': 'raw'}
options.update(kwargs)
arg = [commit, '--']
if path:
arg.append(path)
commits = self.git.log(*arg, **options)
print commits.splitlines(False)
return list(Commit._iter_from_stream(self, iter(commits.splitlines())))

def diff(self, a, b, *paths):
"""
The diff from commit ``a`` to commit ``b``, optionally restricted to the given file(s)
Expand Down
28 changes: 8 additions & 20 deletions test/git/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,13 @@ class TestCommit(object):
def setup(self):
self.repo = Repo(GIT_REPO)

@patch_object(Git, '_call_process')
def test_bake(self, git):
git.return_value = fixture('rev_list_single')
def test_bake(self):

commit = Commit(self.repo, **{'id': '4c8124ffcf4039d292442eeccabdeca5af5c5017'})
commit = Commit(self.repo, **{'id': '2454ae89983a4496a445ce347d7a41c0bb0ea7ae'})
commit.author # bake

assert_equal("Tom Preston-Werner", commit.author.name)
assert_equal("tom@mojombo.com", commit.author.email)

assert_true(git.called)
assert_equal(git.call_args, (('rev_list', '4c8124ffcf4039d292442eeccabdeca5af5c5017', '--', ''), {'pretty': 'raw', 'max_count': 1}))
assert_equal("Sebastian Thiel", commit.author.name)
assert_equal("byronimo@gmail.com", commit.author.email)


@patch_object(Git, '_call_process')
Expand Down Expand Up @@ -159,17 +154,10 @@ def test_diffs_on_initial_import(self):
assert diff.deleted_file and isinstance(diff.deleted_file, bool)
# END for each diff in initial import commit

@patch_object(Git, '_call_process')
def test_diffs_on_initial_import_with_empty_commit(self, git):
git.return_value = fixture('show_empty_commit')

commit = Commit(self.repo, id='634396b2f541a9f2d58b00be1a07f0c358b999b3')
def test_diffs_on_initial_import_without_parents(self):
commit = Commit(self.repo, id='33ebe7acec14b25c5f84f35a664803fcab2f7781')
diffs = commit.diffs

assert_equal([], diffs)

assert_true(git.called)
assert_equal(git.call_args, (('show', '634396b2f541a9f2d58b00be1a07f0c358b999b3', '-M'), {'full_index': True, 'pretty': 'raw'}))
assert diffs

def test_diffs_with_mode_only_change(self):
commit = Commit(self.repo, id='ccde80b7a3037a004a7807a6b79916ce2a1e9729')
Expand Down Expand Up @@ -216,7 +204,7 @@ def test_rev_list_bisect_all(self, git):
bisect_all=True)
assert_true(git.called)

commits = Commit._iter_from_stream(self.repo, iter(revs.splitlines(False)))
commits = Commit._iter_from_process(self.repo, ListProcessAdapter(revs))
expected_ids = (
'cf37099ea8d1d8c7fbf9b6d12d7ec0249d3acb8b',
'33ebe7acec14b25c5f84f35a664803fcab2f7781',
Expand Down
22 changes: 2 additions & 20 deletions test/git/test_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_heads_should_populate_head_data(self):

@patch_object(Git, '_call_process')
def test_commits(self, git):
git.return_value = fixture('rev_list')
git.return_value = ListProcessAdapter(fixture('rev_list'))

commits = self.repo.commits('master', max_count=10)

Expand All @@ -65,7 +65,6 @@ def test_commits(self, git):
assert_equal("Merge branch 'site'", c.summary)

assert_true(git.called)
assert_equal(git.call_args, (('rev_list', 'master', '--', ''), {'skip': 0, 'pretty': 'raw', 'max_count': 10}))

@patch_object(Git, '_call_process')
def test_commit_count(self, git):
Expand All @@ -78,14 +77,13 @@ def test_commit_count(self, git):

@patch_object(Git, '_call_process')
def test_commit(self, git):
git.return_value = fixture('rev_list_single')
git.return_value = ListProcessAdapter(fixture('rev_list_single'))

commit = self.repo.commit('4c8124ffcf4039d292442eeccabdeca5af5c5017')

assert_equal("4c8124ffcf4039d292442eeccabdeca5af5c5017", commit.id)

assert_true(git.called)
assert_equal(git.call_args, (('rev_list', '4c8124ffcf4039d292442eeccabdeca5af5c5017', '--', ''), {'pretty': 'raw', 'max_count': 1}))

@patch_object(Git, '_call_process')
def test_tree(self, git):
Expand Down Expand Up @@ -217,22 +215,6 @@ def test_repr(self):
path = os.path.join(os.path.abspath(GIT_REPO), '.git')
assert_equal('<git.Repo "%s">' % path, repr(self.repo))

@patch_object(Git, '_call_process')
def test_log(self, git):
git.return_value = fixture('rev_list')
assert_equal('4c8124ffcf4039d292442eeccabdeca5af5c5017', self.repo.log()[0].id)
assert_equal('ab25fd8483882c3bda8a458ad2965d2248654335', self.repo.log()[-1].id)
assert_true(git.called)
assert_equal(git.call_count, 2)
assert_equal(git.call_args, (('log', 'master', '--'), {'pretty': 'raw'}))

@patch_object(Git, '_call_process')
def test_log_with_path_and_options(self, git):
git.return_value = fixture('rev_list')
self.repo.log('master', 'file.rb', **{'max_count': 1})
assert_true(git.called)
assert_equal(git.call_args, (('log', 'master', '--', 'file.rb'), {'pretty': 'raw', 'max_count': 1}))

def test_is_dirty_with_bare_repository(self):
self.repo.bare = True
assert_false(self.repo.is_dirty)
Expand Down
11 changes: 11 additions & 0 deletions test/testlib/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,14 @@ def fixture(name):

def absolute_project_path():
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))


class ListProcessAdapter(object):
"""Allows to use lists as Process object as returned by SubProcess.Popen.
Its tailored to work with the test system only"""

def __init__(self, input_list_or_string):
l = input_list_or_string
if isinstance(l,basestring):
l = l.splitlines()
self.stdout = iter(l)

0 comments on commit ead94f2

Please sign in to comment.