From 2ea761425b9fa1fda57e83a877f4e2fdc336a9a3 Mon Sep 17 00:00:00 2001 From: Jason Schadel Date: Mon, 19 Nov 2012 16:27:46 -0500 Subject: [PATCH 0001/2884] Remove requires in setup.py. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1c58cb656..a1dad3642 100755 --- a/setup.py +++ b/setup.py @@ -73,7 +73,6 @@ def _stamp_version(filename): package_data = {'git.test' : ['fixtures/*']}, package_dir = {'git':'git'}, license = "BSD License", - requires=('gitdb (>=0.5.1)',), install_requires='gitdb >= 0.5.1', zip_safe=False, long_description = """\ From 5991698ee2b3046bbc9cfc3bd2abd3a881f514dd Mon Sep 17 00:00:00 2001 From: "Marcus R. Brown" Date: Fri, 11 Jan 2013 13:43:49 -0700 Subject: [PATCH 0002/2884] Support repos that use the .git-file mechanism. --- git/repo/base.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/git/repo/base.py b/git/repo/base.py index 20c96b228..df52137eb 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -71,6 +71,7 @@ class Repo(object): re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{4,40}$') re_author_committer_start = re.compile(r'^(author|committer)') re_tab_full_line = re.compile(r'^\t(.*)$') + re_git_file_gitdir = re.compile('gitdir: (.*)') # invariants # represents the configuration level of a configuration file @@ -113,6 +114,17 @@ def __init__(self, path=None, odbt = DefaultDBType): self.git_dir = gitpath self._working_tree_dir = curpath break + if isfile(gitpath): + line = open(gitpath, 'r').readline().strip() + match = self.re_git_file_gitdir.match(line) + if match: + gitpath = match.group(1) + if not os.path.isabs(gitpath): + gitpath = os.path.normpath(join(curpath, gitpath)) + if is_git_dir(gitpath): + self.git_dir = gitpath + self._working_tree_dir = curpath + break curpath, dummy = os.path.split(curpath) if not dummy: break From 3621c06c3173bff395645bd416f0efafa20a1da6 Mon Sep 17 00:00:00 2001 From: "Marcus R. Brown" Date: Fri, 11 Jan 2013 13:47:06 -0700 Subject: [PATCH 0003/2884] Add tests for .git-file. --- git/test/fixtures/git_file | 1 + git/test/test_repo.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 git/test/fixtures/git_file diff --git a/git/test/fixtures/git_file b/git/test/fixtures/git_file new file mode 100644 index 000000000..2efda9f50 --- /dev/null +++ b/git/test/fixtures/git_file @@ -0,0 +1 @@ +gitdir: ./.real diff --git a/git/test/test_repo.py b/git/test/test_repo.py index 18d5c1b84..a4d148d18 100644 --- a/git/test/test_repo.py +++ b/git/test/test_repo.py @@ -594,6 +594,23 @@ def test_repo_odbtype(self): target_type = GitCmdObjectDB assert isinstance(self.rorepo.odb, target_type) + @with_rw_repo('HEAD') + def test_git_file(self, rwrepo): + # Move the .git directory to another location and create the .git file. + real_path_abs = os.path.abspath(join_path_native(rwrepo.working_tree_dir, '.real')) + os.rename(rwrepo.git_dir, real_path_abs) + git_file_path = join_path_native(rwrepo.working_tree_dir, '.git') + open(git_file_path, 'wb').write(fixture('git_file')) + + # Create a repo and make sure it's pointing to the relocated .git directory. + git_file_repo = Repo(rwrepo.working_tree_dir) + assert os.path.abspath(git_file_repo.git_dir) == real_path_abs + + # Test using an absolute gitdir path in the .git file. + open(git_file_path, 'wb').write('gitdir: %s\n' % real_path_abs) + git_file_repo = Repo(rwrepo.working_tree_dir) + assert os.path.abspath(git_file_repo.git_dir) == real_path_abs + def test_submodules(self): assert len(self.rorepo.submodules) == 1 # non-recursive assert len(list(self.rorepo.iter_submodules())) >= 2 From 007bd4b8190a6e85831c145e0aed5c68594db556 Mon Sep 17 00:00:00 2001 From: Igor Bondarenko Date: Thu, 14 Feb 2013 15:01:39 +0200 Subject: [PATCH 0004/2884] Fixed parse_actor_and_date with mangled tags --- git/objects/util.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/git/objects/util.py b/git/objects/util.py index 4c9323b85..af46f3c61 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -167,6 +167,7 @@ def parse_date(string_date): # precompiled regex _re_actor_epoch = re.compile(r'^.+? (.*) (\d+) ([+-]\d+).*$') +_re_only_actor = re.compile(r'^.+? (.*)$') def parse_actor_and_date(line): """Parse out the actor (author or committer) info from a line like:: @@ -174,8 +175,13 @@ def parse_actor_and_date(line): author Tom Preston-Werner 1191999972 -0700 :return: [Actor, int_seconds_since_epoch, int_timezone_offset]""" + actor, epoch, offset = '', 0, 0 m = _re_actor_epoch.search(line) - actor, epoch, offset = m.groups() + if m: + actor, epoch, offset = m.groups() + else: + m = _re_only_actor.search(line) + actor = m.group(1) if m else line or '' return (Actor._from_string(actor), int(epoch), utctz_to_altz(offset)) From 53b65e074e4d62ea5d0251b37c35fd055e403110 Mon Sep 17 00:00:00 2001 From: niyaton Date: Mon, 25 Feb 2013 01:22:30 +0900 Subject: [PATCH 0005/2884] Added support for separeted git dir. --- git/repo/base.py | 6 ++++++ git/repo/fun.py | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/git/repo/base.py b/git/repo/base.py index 20c96b228..7dcf409dc 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -32,6 +32,7 @@ from fun import ( rev_parse, is_git_dir, + read_gitfile, touch ) @@ -113,6 +114,11 @@ def __init__(self, path=None, odbt = DefaultDBType): self.git_dir = gitpath self._working_tree_dir = curpath break + gitpath = read_gitfile(gitpath) + if gitpath: + self.git_dir = gitpath + self._working_tree_dir = curpath + break curpath, dummy = os.path.split(curpath) if not dummy: break diff --git a/git/repo/fun.py b/git/repo/fun.py index 03d557164..86d3c6a99 100644 --- a/git/repo/fun.py +++ b/git/repo/fun.py @@ -30,6 +30,17 @@ def is_git_dir(d): os.readlink(headref).startswith('refs')) return False +def read_gitfile(f): + """ This is taken from the git setup.c:read_gitfile function. + :return gitdir path or None if gitfile is invalid.""" + + if not isfile(f): + return None + line = open(f, 'r').readline().rstrip() + if line[0:8] != 'gitdir: ': + return None + path = os.path.realpath(line[8:]) + return path if is_git_dir(path) else None def short_to_long(odb, hexsha): """:return: long hexadecimal sha1 from the given less-than-40 byte hexsha From db82455bd91ce00c22f6ee2b0dc622f117f07137 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Thu, 11 Apr 2013 18:39:03 +0000 Subject: [PATCH 0006/2884] [#6078] #102 Work-around mergetag blocks by ignoring them --- git/objects/commit.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index fd4187b08..8e74f8bfa 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -426,11 +426,18 @@ def _deserialize(self, stream): self.committer, self.committed_date, self.committer_tz_offset = parse_actor_and_date(readline()) + # we might run into one or more mergetag blocks, skip those for now + next_line = readline() + while next_line.startswith('mergetag '): + next_line = readline() + while next_line.startswith(' '): + next_line = readline() + # now we can have the encoding line, or an empty line followed by the optional # message. self.encoding = self.default_encoding # read encoding or empty line to separate message - enc = readline() + enc = next_line enc = enc.strip() if enc: self.encoding = enc[enc.find(' ')+1:] From f122a6aa3eb386914faa58ef3bf336f27b02fab0 Mon Sep 17 00:00:00 2001 From: Tim Van Steenburgh Date: Wed, 17 Apr 2013 18:21:53 +0000 Subject: [PATCH 0007/2884] Return bytes if object name can't be utf8-decoded Signed-off-by: Tim Van Steenburgh --- git/objects/fun.py | 10 +++++++--- git/test/test_fun.py | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/git/objects/fun.py b/git/objects/fun.py index 9b0a377cb..8c3806444 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -70,9 +70,13 @@ def tree_entries_from_data(data): # default encoding for strings in git is utf8 # Only use the respective unicode object if the byte stream was encoded name = data[ns:i] - name_enc = name.decode("utf-8") - if len(name) > len(name_enc): - name = name_enc + try: + name_enc = name.decode("utf-8") + except UnicodeDecodeError: + pass + else: + if len(name) > len(name_enc): + name = name_enc # END handle encoding # byte is NULL, get next 20 diff --git a/git/test/test_fun.py b/git/test/test_fun.py index b7991cdbe..36435ae4d 100644 --- a/git/test/test_fun.py +++ b/git/test/test_fun.py @@ -249,3 +249,8 @@ def test_tree_traversal_single(self): entries = traverse_tree_recursive(odb, commit.tree.binsha, '') assert entries # END for each commit + +def test_tree_entries_from_data(): + from git.objects.fun import tree_entries_from_data + r = tree_entries_from_data(b'100644 \x9f\0aaa') + assert r == [('aaa', 33188, '\x9f')], r From 5869c5c1a51d448a411ae0d51d888793c35db9c0 Mon Sep 17 00:00:00 2001 From: Tim Van Steenburgh Date: Wed, 17 Apr 2013 18:43:19 +0000 Subject: [PATCH 0008/2884] Fix whacky indentation Signed-off-by: Tim Van Steenburgh --- git/objects/fun.py | 14 +++++++------- git/test/test_fun.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/git/objects/fun.py b/git/objects/fun.py index 8c3806444..e2d4df558 100644 --- a/git/objects/fun.py +++ b/git/objects/fun.py @@ -70,13 +70,13 @@ def tree_entries_from_data(data): # default encoding for strings in git is utf8 # Only use the respective unicode object if the byte stream was encoded name = data[ns:i] - try: - name_enc = name.decode("utf-8") - except UnicodeDecodeError: - pass - else: - if len(name) > len(name_enc): - name = name_enc + try: + name_enc = name.decode("utf-8") + except UnicodeDecodeError: + pass + else: + if len(name) > len(name_enc): + name = name_enc # END handle encoding # byte is NULL, get next 20 diff --git a/git/test/test_fun.py b/git/test/test_fun.py index 36435ae4d..bbd5d1597 100644 --- a/git/test/test_fun.py +++ b/git/test/test_fun.py @@ -251,6 +251,6 @@ def test_tree_traversal_single(self): # END for each commit def test_tree_entries_from_data(): - from git.objects.fun import tree_entries_from_data - r = tree_entries_from_data(b'100644 \x9f\0aaa') - assert r == [('aaa', 33188, '\x9f')], r + from git.objects.fun import tree_entries_from_data + r = tree_entries_from_data(b'100644 \x9f\0aaa') + assert r == [('aaa', 33188, '\x9f')], r From d3a728277877924e889e9fef42501127f48a4e77 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Wed, 9 Oct 2013 19:02:56 +0000 Subject: [PATCH 0009/2884] [#5330] Ensure wait() is called on git processes --- git/cmd.py | 1 + git/objects/commit.py | 3 +++ git/remote.py | 17 ++++------------- git/repo/base.py | 7 +++++-- git/util.py | 12 ++++++++++++ 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/git/cmd.py b/git/cmd.py index 63a7134e0..75687a416 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -80,6 +80,7 @@ def __del__(self): # try to kill it try: os.kill(self.proc.pid, 2) # interrupt signal + self.proc.wait() # ensure process goes away except AttributeError: # try windows # for some reason, providing None for stdout/stderr still prints something. This is why diff --git a/git/objects/commit.py b/git/objects/commit.py index 8e74f8bfa..0565b2c0b 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -8,6 +8,7 @@ Actor, Iterable, Stats, + finalize_process ) from git.diff import Diffable from tree import Tree @@ -251,6 +252,8 @@ def _iter_from_process_or_stream(cls, repo, proc_or_stream): assert len(hexsha) == 40, "Invalid line: %s" % hexsha yield Commit(repo, hex_to_bin(hexsha)) # END for each line in stream + if has_attr(proc_or_stream, 'wait'): + finalize_process(proc_or_stream) @classmethod diff --git a/git/remote.py b/git/remote.py index 5e4439fb1..e38b3540d 100644 --- a/git/remote.py +++ b/git/remote.py @@ -24,7 +24,10 @@ TagReference ) -from git.util import join_path +from git.util import ( + join_path, + finalize_process + ) from gitdb.util import join import re @@ -58,18 +61,6 @@ def digest_process_messages(fh, progress): # END while file is not done reading return dropped_lines -def finalize_process(proc): - """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly""" - try: - proc.wait() - except GitCommandError,e: - # if a push has rejected items, the command has non-zero return status - # a return status of 128 indicates a connection error - reraise the previous one - if proc.poll() == 128: - raise - pass - # END exception handling - def add_progress(kwargs, git, progress): """Add the --progress flag to the given kwargs dict if supported by the git command. If the actual progress in the given progress instance is not diff --git a/git/repo/base.py b/git/repo/base.py index 14efabdc6..0bc3c12cf 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -6,7 +6,10 @@ from git.exc import InvalidGitRepositoryError, NoSuchPathError from git.cmd import Git -from git.util import Actor +from git.util import ( + Actor, + finalize_process + ) from git.refs import * from git.index import IndexFile from git.objects import * @@ -14,7 +17,6 @@ from git.remote import ( Remote, digest_process_messages, - finalize_process, add_progress ) @@ -541,6 +543,7 @@ def untracked_files(self): untracked_files.append(untracked_info.replace("#\t", "").rstrip()) # END for each utracked info line # END for each line + finalize_process(proc) return untracked_files @property diff --git a/git/util.py b/git/util.py index a9e87d6f6..130d77628 100644 --- a/git/util.py +++ b/git/util.py @@ -121,6 +121,18 @@ def get_user_id(): # END get username from login return "%s@%s" % (username, platform.node()) +def finalize_process(proc): + """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly""" + try: + proc.wait() + except GitCommandError,e: + # if a push has rejected items, the command has non-zero return status + # a return status of 128 indicates a connection error - reraise the previous one + if proc.poll() == 128: + raise + pass + # END exception handling + #} END utilities #{ Classes From c6b08c27a031f8b8b0bb6c41747ca1bc62b72706 Mon Sep 17 00:00:00 2001 From: Cory Johns Date: Thu, 17 Oct 2013 15:33:59 +0000 Subject: [PATCH 0010/2884] [#5330] Fixed has_attr typo --- git/objects/commit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/objects/commit.py b/git/objects/commit.py index 0565b2c0b..4ccd9d755 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -252,7 +252,7 @@ def _iter_from_process_or_stream(cls, repo, proc_or_stream): assert len(hexsha) == 40, "Invalid line: %s" % hexsha yield Commit(repo, hex_to_bin(hexsha)) # END for each line in stream - if has_attr(proc_or_stream, 'wait'): + if hasattr(proc_or_stream, 'wait'): finalize_process(proc_or_stream) From 3f277ba01f9a93fb040a365eef80f46ce6a9de85 Mon Sep 17 00:00:00 2001 From: BoppreH Date: Thu, 17 Oct 2013 21:47:55 -0300 Subject: [PATCH 0011/2884] Avoid spawning console windows when running from .pyw By adding `shell=True,` to the list of Popen parameters, we avoid spawning console windows when scripts call this method from a windowless (.pyw) Python script. --- git/cmd.py | 1 + 1 file changed, 1 insertion(+) diff --git a/git/cmd.py b/git/cmd.py index 576a5300a..579fbc83a 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -336,6 +336,7 @@ def execute(self, command, stderr=PIPE, stdout=PIPE, close_fds=(os.name=='posix'),# unsupported on linux + shell=True, **subprocess_kwargs ) if as_process: From 2e6957abf8cd88824282a19b74497872fe676a46 Mon Sep 17 00:00:00 2001 From: Dave Brondsema Date: Tue, 28 Jan 2014 21:07:31 -0500 Subject: [PATCH 0012/2884] Fix missed import from d3a7282 The `finalize_process` method was moved but this import wasn't carried with it. --- git/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/git/util.py b/git/util.py index 130d77628..0a533e508 100644 --- a/git/util.py +++ b/git/util.py @@ -13,6 +13,8 @@ import tempfile import platform +from exc import GitCommandError + from gitdb.util import ( make_sha, LockedFD, From 03097c7ace28c5516aacbb1617265e50a9043a84 Mon Sep 17 00:00:00 2001 From: Maxim Syabro Date: Mon, 10 Feb 2014 01:58:27 +0800 Subject: [PATCH 0013/2884] Fixed NameError --- git/refs/reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git/refs/reference.py b/git/refs/reference.py index 29d051a6f..284f4c9ac 100644 --- a/git/refs/reference.py +++ b/git/refs/reference.py @@ -18,7 +18,7 @@ def require_remote_ref_path(func): """A decorator raising a TypeError if we are not a valid remote, based on the path""" def wrapper(self, *args): if not self.path.startswith(self._remote_common_path_default + "/"): - raise ValueError("ref path does not point to a remote reference: %s" % path) + raise ValueError("ref path does not point to a remote reference: %s" % self.path) return func(self, *args) #END wrapper wrapper.__name__ = func.__name__ From f573b6840509bf41be822ab7ed79e0a776005133 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Sun, 9 Feb 2014 21:09:00 +0100 Subject: [PATCH 0014/2884] tabs to 4 spaces - this won't make integrating the patches easier, but it's probably a good idea to go a little more pep8 (and fix sins of my youth ;) ) --- git/__init__.py | 32 +- git/cmd.py | 1062 ++++++------ git/config.py | 790 ++++----- git/db.py | 94 +- git/diff.py | 620 +++---- git/exc.py | 72 +- git/ext/gitdb | 2 +- git/index/base.py | 2232 +++++++++++++------------- git/index/fun.py | 558 +++---- git/index/typ.py | 292 ++-- git/index/util.py | 120 +- git/objects/base.py | 310 ++-- git/objects/blob.py | 36 +- git/objects/commit.py | 864 +++++----- git/objects/fun.py | 364 ++--- git/objects/submodule/base.py | 1784 ++++++++++---------- git/objects/submodule/root.py | 586 +++---- git/objects/submodule/util.py | 150 +- git/objects/tag.py | 124 +- git/objects/tree.py | 490 +++--- git/objects/util.py | 546 +++---- git/refs/__init__.py | 2 +- git/refs/head.py | 450 +++--- git/refs/log.py | 544 +++---- git/refs/reference.py | 216 +-- git/refs/remote.py | 70 +- git/refs/symbolic.py | 1214 +++++++------- git/refs/tag.py | 164 +- git/remote.py | 1228 +++++++------- git/repo/base.py | 1470 ++++++++--------- git/repo/fun.py | 516 +++--- git/test/lib/helper.py | 412 ++--- git/test/performance/lib.py | 92 +- git/test/performance/test_commit.py | 164 +- git/test/performance/test_odb.py | 120 +- git/test/performance/test_streams.py | 224 +-- git/test/performance/test_utils.py | 330 ++-- git/test/test_base.py | 162 +- git/test/test_blob.py | 20 +- git/test/test_commit.py | 490 +++--- git/test/test_db.py | 26 +- git/test/test_diff.py | 188 +-- git/test/test_fun.py | 470 +++--- git/test/test_git.py | 180 +-- git/test/test_index.py | 1302 +++++++-------- git/test/test_reflog.py | 178 +- git/test/test_refs.py | 1048 ++++++------ git/test/test_remote.py | 952 +++++------ git/test/test_repo.py | 1164 +++++++------- git/test/test_submodule.py | 1082 ++++++------- git/test/test_tree.py | 260 +-- git/test/test_util.py | 286 ++-- git/util.py | 1220 +++++++------- setup.py | 130 +- 54 files changed, 13751 insertions(+), 13751 deletions(-) mode change 100755 => 100644 setup.py diff --git a/git/__init__.py b/git/__init__.py index 0658c3306..9ea811123 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -13,15 +13,15 @@ #{ Initialization def _init_externals(): - """Initialize external projects by putting them into the path""" - sys.path.append(os.path.join(os.path.dirname(__file__), 'ext', 'gitdb')) - - try: - import gitdb - except ImportError: - raise ImportError("'gitdb' could not be found in your PYTHONPATH") - #END verify import - + """Initialize external projects by putting them into the path""" + sys.path.append(os.path.join(os.path.dirname(__file__), 'ext', 'gitdb')) + + try: + import gitdb + except ImportError: + raise ImportError("'gitdb' could not be found in your PYTHONPATH") + #END verify import + #} END initialization ################# @@ -41,14 +41,14 @@ def _init_externals(): from git.remote import * from git.index import * from git.util import ( - LockFile, - BlockingLockFile, - Stats, - Actor - ) + LockFile, + BlockingLockFile, + Stats, + Actor + ) #} END imports __all__ = [ name for name, obj in locals().items() - if not (name.startswith('_') or inspect.ismodule(obj)) ] - + if not (name.startswith('_') or inspect.ismodule(obj)) ] + diff --git a/git/cmd.py b/git/cmd.py index 576a5300a..2d4aa7279 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -6,569 +6,569 @@ import os, sys from util import ( - LazyMixin, - stream_copy - ) + LazyMixin, + stream_copy + ) from exc import GitCommandError from subprocess import ( - call, - Popen, - PIPE - ) + call, + Popen, + PIPE + ) execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output', - 'with_exceptions', 'as_process', - 'output_stream' ) + 'with_exceptions', 'as_process', + 'output_stream' ) __all__ = ('Git', ) def dashify(string): - return string.replace('_', '-') + return string.replace('_', '-') class Git(LazyMixin): - """ - The Git class manages communication with the Git binary. - - It provides a convenient interface to calling the Git binary, such as in:: - - g = Git( git_dir ) - g.init() # calls 'git init' program - rval = g.ls_files() # calls 'git ls-files' program - - ``Debugging`` - Set the GIT_PYTHON_TRACE environment variable print each invocation - of the command to stdout. - Set its value to 'full' to see details about the returned values. - """ - __slots__ = ("_working_dir", "cat_file_all", "cat_file_header", "_version_info") - - # CONFIGURATION - # The size in bytes read from stdout when copying git's output to another stream - max_chunk_size = 1024*64 - - git_exec_name = "git" # default that should work on linux and windows - git_exec_name_win = "git.cmd" # alternate command name, windows only - - # Enables debugging of GitPython's git commands - GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) - - # Provide the full path to the git executable. Otherwise it assumes git is in the path - _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" - GIT_PYTHON_GIT_EXECUTABLE = os.environ.get(_git_exec_env_var, git_exec_name) - - - 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. - - The wait method was overridden to perform automatic status code checking - and possibly raise.""" - __slots__= ("proc", "args") - - def __init__(self, proc, args ): - self.proc = proc - self.args = args - - def __del__(self): - # did the process finish already so we have a return code ? - if self.proc.poll() is not None: - return - - # can be that nothing really exists anymore ... - if os is None: - return - - # try to kill it - try: - os.kill(self.proc.pid, 2) # interrupt signal - except AttributeError: - # try windows - # for some reason, providing None for stdout/stderr still prints something. This is why - # we simply use the shell and redirect to nul. Its slower than CreateProcess, question - # is whether we really want to see all these messages. Its annoying no matter what. - call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(self.proc.pid)), shell=True) - # END exception handling - - def __getattr__(self, attr): - return getattr(self.proc, attr) - - def wait(self): - """Wait for the process and return its status code. - - :raise GitCommandError: if the return status is not 0""" - status = self.proc.wait() - if status != 0: - raise GitCommandError(self.args, status, self.proc.stderr.read()) - # END status handling - return status - # END auto interrupt - - class CatFileContentStream(object): - """Object representing a sized read-only stream returning the contents of - an object. - It behaves like a stream, but counts the data read and simulates an empty - stream once our sized content region is empty. - If not all data is read to the end of the objects's lifetime, we read the - rest to assure the underlying stream continues to work""" - - __slots__ = ('_stream', '_nbr', '_size') - - def __init__(self, size, stream): - self._stream = stream - self._size = size - self._nbr = 0 # num bytes read - - # special case: if the object is empty, has null bytes, get the - # final newline right away. - if size == 0: - stream.read(1) - # END handle empty streams - - def read(self, size=-1): - bytes_left = self._size - self._nbr - if bytes_left == 0: - return '' - if size > -1: - # assure we don't try to read past our limit - size = min(bytes_left, size) - else: - # they try to read all, make sure its not more than what remains - size = bytes_left - # END check early depletion - data = self._stream.read(size) - self._nbr += len(data) - - # check for depletion, read our final byte to make the stream usable by others - if self._size - self._nbr == 0: - self._stream.read(1) # final newline - # END finish reading - return data - - def readline(self, size=-1): - if self._nbr == self._size: - return '' - - # clamp size to lowest allowed value - bytes_left = self._size - self._nbr - if size > -1: - size = min(bytes_left, size) - else: - size = bytes_left - # END handle size - - data = self._stream.readline(size) - self._nbr += len(data) - - # handle final byte - if self._size - self._nbr == 0: - self._stream.read(1) - # END finish reading - - return data - - def readlines(self, size=-1): - if self._nbr == self._size: - return list() - - # leave all additional logic to our readline method, we just check the size - out = list() - nbr = 0 - while True: - line = self.readline() - if not line: - break - out.append(line) - if size > -1: - nbr += len(line) - if nbr > size: - break - # END handle size constraint - # END readline loop - return out - - def __iter__(self): - return self - - def next(self): - line = self.readline() - if not line: - raise StopIteration - return line - - def __del__(self): - bytes_left = self._size - self._nbr - if bytes_left: - # read and discard - seeking is impossible within a stream - # includes terminating newline - self._stream.read(bytes_left + 1) - # END handle incomplete read - - - def __init__(self, working_dir=None): - """Initialize this instance with: - - :param working_dir: - Git directory we should work in. If None, we always work in the current - directory as returned by os.getcwd(). - It is meant to be the working tree directory if available, or the - .git directory in case of bare repositories.""" - super(Git, self).__init__() - self._working_dir = working_dir - - # cached command slots - self.cat_file_header = None - self.cat_file_all = None + """ + The Git class manages communication with the Git binary. + + It provides a convenient interface to calling the Git binary, such as in:: + + g = Git( git_dir ) + g.init() # calls 'git init' program + rval = g.ls_files() # calls 'git ls-files' program + + ``Debugging`` + Set the GIT_PYTHON_TRACE environment variable print each invocation + of the command to stdout. + Set its value to 'full' to see details about the returned values. + """ + __slots__ = ("_working_dir", "cat_file_all", "cat_file_header", "_version_info") + + # CONFIGURATION + # The size in bytes read from stdout when copying git's output to another stream + max_chunk_size = 1024*64 + + git_exec_name = "git" # default that should work on linux and windows + git_exec_name_win = "git.cmd" # alternate command name, windows only + + # Enables debugging of GitPython's git commands + GIT_PYTHON_TRACE = os.environ.get("GIT_PYTHON_TRACE", False) + + # Provide the full path to the git executable. Otherwise it assumes git is in the path + _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" + GIT_PYTHON_GIT_EXECUTABLE = os.environ.get(_git_exec_env_var, git_exec_name) + + + 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. + + The wait method was overridden to perform automatic status code checking + and possibly raise.""" + __slots__= ("proc", "args") + + def __init__(self, proc, args ): + self.proc = proc + self.args = args + + def __del__(self): + # did the process finish already so we have a return code ? + if self.proc.poll() is not None: + return + + # can be that nothing really exists anymore ... + if os is None: + return + + # try to kill it + try: + os.kill(self.proc.pid, 2) # interrupt signal + except AttributeError: + # try windows + # for some reason, providing None for stdout/stderr still prints something. This is why + # we simply use the shell and redirect to nul. Its slower than CreateProcess, question + # is whether we really want to see all these messages. Its annoying no matter what. + call(("TASKKILL /F /T /PID %s 2>nul 1>nul" % str(self.proc.pid)), shell=True) + # END exception handling + + def __getattr__(self, attr): + return getattr(self.proc, attr) + + def wait(self): + """Wait for the process and return its status code. + + :raise GitCommandError: if the return status is not 0""" + status = self.proc.wait() + if status != 0: + raise GitCommandError(self.args, status, self.proc.stderr.read()) + # END status handling + return status + # END auto interrupt + + class CatFileContentStream(object): + """Object representing a sized read-only stream returning the contents of + an object. + It behaves like a stream, but counts the data read and simulates an empty + stream once our sized content region is empty. + If not all data is read to the end of the objects's lifetime, we read the + rest to assure the underlying stream continues to work""" + + __slots__ = ('_stream', '_nbr', '_size') + + def __init__(self, size, stream): + self._stream = stream + self._size = size + self._nbr = 0 # num bytes read + + # special case: if the object is empty, has null bytes, get the + # final newline right away. + if size == 0: + stream.read(1) + # END handle empty streams + + def read(self, size=-1): + bytes_left = self._size - self._nbr + if bytes_left == 0: + return '' + if size > -1: + # assure we don't try to read past our limit + size = min(bytes_left, size) + else: + # they try to read all, make sure its not more than what remains + size = bytes_left + # END check early depletion + data = self._stream.read(size) + self._nbr += len(data) + + # check for depletion, read our final byte to make the stream usable by others + if self._size - self._nbr == 0: + self._stream.read(1) # final newline + # END finish reading + return data + + def readline(self, size=-1): + if self._nbr == self._size: + return '' + + # clamp size to lowest allowed value + bytes_left = self._size - self._nbr + if size > -1: + size = min(bytes_left, size) + else: + size = bytes_left + # END handle size + + data = self._stream.readline(size) + self._nbr += len(data) + + # handle final byte + if self._size - self._nbr == 0: + self._stream.read(1) + # END finish reading + + return data + + def readlines(self, size=-1): + if self._nbr == self._size: + return list() + + # leave all additional logic to our readline method, we just check the size + out = list() + nbr = 0 + while True: + line = self.readline() + if not line: + break + out.append(line) + if size > -1: + nbr += len(line) + if nbr > size: + break + # END handle size constraint + # END readline loop + return out + + def __iter__(self): + return self + + def next(self): + line = self.readline() + if not line: + raise StopIteration + return line + + def __del__(self): + bytes_left = self._size - self._nbr + if bytes_left: + # read and discard - seeking is impossible within a stream + # includes terminating newline + self._stream.read(bytes_left + 1) + # END handle incomplete read + + + def __init__(self, working_dir=None): + """Initialize this instance with: + + :param working_dir: + Git directory we should work in. If None, we always work in the current + directory as returned by os.getcwd(). + It is meant to be the working tree directory if available, or the + .git directory in case of bare repositories.""" + super(Git, self).__init__() + self._working_dir = working_dir + + # cached command slots + self.cat_file_header = None + self.cat_file_all = None - def __getattr__(self, name): - """A convenience method as it allows to call the command as if it was - an object. - :return: Callable object that will execute call _call_process with your arguments.""" - if name[0] == '_': - return LazyMixin.__getattr__(self, name) - return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) + def __getattr__(self, name): + """A convenience method as it allows to call the command as if it was + an object. + :return: Callable object that will execute call _call_process with your arguments.""" + if name[0] == '_': + return LazyMixin.__getattr__(self, name) + return lambda *args, **kwargs: self._call_process(name, *args, **kwargs) - def _set_cache_(self, attr): - if attr == '_version_info': - # We only use the first 4 numbers, as everthing else could be strings in fact (on windows) - version_numbers = self._call_process('version').split(' ')[2] - self._version_info = tuple(int(n) for n in version_numbers.split('.')[:4]) - else: - super(Git, self)._set_cache_(attr) - #END handle version info - + def _set_cache_(self, attr): + if attr == '_version_info': + # We only use the first 4 numbers, as everthing else could be strings in fact (on windows) + version_numbers = self._call_process('version').split(' ')[2] + self._version_info = tuple(int(n) for n in version_numbers.split('.')[:4]) + else: + super(Git, self)._set_cache_(attr) + #END handle version info + - @property - def working_dir(self): - """:return: Git directory we are working on""" - return self._working_dir - - @property - def version_info(self): - """ - :return: tuple(int, int, int, int) tuple with integers representing the major, minor - and additional version numbers as parsed from git version. - This value is generated on demand and is cached""" - return self._version_info + @property + def working_dir(self): + """:return: Git directory we are working on""" + return self._working_dir + + @property + def version_info(self): + """ + :return: tuple(int, int, int, int) tuple with integers representing the major, minor + and additional version numbers as parsed from git version. + This value is generated on demand and is cached""" + return self._version_info - def execute(self, command, - istream=None, - with_keep_cwd=False, - with_extended_output=False, - with_exceptions=True, - as_process=False, - output_stream=None, - **subprocess_kwargs - ): - """Handles executing the command on the shell and consumes and returns - the returned information (stdout) + def execute(self, command, + istream=None, + with_keep_cwd=False, + with_extended_output=False, + with_exceptions=True, + as_process=False, + output_stream=None, + **subprocess_kwargs + ): + """Handles executing the command on the shell and consumes and returns + the returned information (stdout) - :param command: - The command argument list to execute. - It should be a string, or a sequence of program arguments. The - program to execute is the first item in the args sequence or string. + :param command: + The command argument list to execute. + It should be a string, or a sequence of program arguments. The + program to execute is the first item in the args sequence or string. - :param istream: - Standard input filehandle passed to subprocess.Popen. + :param istream: + Standard input filehandle passed to subprocess.Popen. - :param with_keep_cwd: - Whether to use the current working directory from os.getcwd(). - The cmd otherwise uses its own working_dir that it has been initialized - with if possible. + :param with_keep_cwd: + Whether to use the current working directory from os.getcwd(). + The cmd otherwise uses its own working_dir that it has been initialized + with if possible. - :param with_extended_output: - Whether to return a (status, stdout, stderr) tuple. + :param with_extended_output: + Whether to return a (status, stdout, stderr) tuple. - :param with_exceptions: - Whether to raise an exception when git returns a non-zero status. + :param with_exceptions: + Whether to raise an exception when git returns a non-zero status. - :param as_process: - Whether to return the created process instance directly from which - 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. - - :param output_stream: - If set to a file-like object, data produced by the git command will be - 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. - - :param subprocess_kwargs: - Keyword arguments to be passed to subprocess.Popen. Please note that - some of the valid kwargs are already set by this method, the ones you - specify may not be the same ones. - - :return: - * str(output) if extended_output = False (Default) - * tuple(int(status), str(stdout), str(stderr)) if extended_output = True - - if ouput_stream is True, the stdout value will be your output stream: - * output_stream if extended_output = False - * tuple(int(status), output_stream, str(stderr)) if extended_output = True - - :raise GitCommandError: - - :note: - If you add additional keyword arguments to the signature of this method, - you must update the execute_kwargs tuple housed in this module.""" - if self.GIT_PYTHON_TRACE and not self.GIT_PYTHON_TRACE == 'full': - print ' '.join(command) + :param as_process: + Whether to return the created process instance directly from which + 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. + + :param output_stream: + If set to a file-like object, data produced by the git command will be + 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. + + :param subprocess_kwargs: + Keyword arguments to be passed to subprocess.Popen. Please note that + some of the valid kwargs are already set by this method, the ones you + specify may not be the same ones. + + :return: + * str(output) if extended_output = False (Default) + * tuple(int(status), str(stdout), str(stderr)) if extended_output = True + + if ouput_stream is True, the stdout value will be your output stream: + * output_stream if extended_output = False + * tuple(int(status), output_stream, str(stderr)) if extended_output = True + + :raise GitCommandError: + + :note: + If you add additional keyword arguments to the signature of this method, + you must update the execute_kwargs tuple housed in this module.""" + if self.GIT_PYTHON_TRACE and not self.GIT_PYTHON_TRACE == 'full': + print ' '.join(command) - # Allow the user to have the command executed in their working dir. - if with_keep_cwd or self._working_dir is None: - cwd = os.getcwd() - else: - cwd=self._working_dir - - # Start the process - proc = Popen(command, - cwd=cwd, - stdin=istream, - stderr=PIPE, - stdout=PIPE, - close_fds=(os.name=='posix'),# unsupported on linux - **subprocess_kwargs - ) - if as_process: - return self.AutoInterrupt(proc, command) - - # Wait for the process to return - status = 0 - stdout_value = '' - stderr_value = '' - try: - if output_stream is None: - stdout_value, stderr_value = proc.communicate() - # strip trailing "\n" - if stdout_value.endswith("\n"): - stdout_value = stdout_value[:-1] - if stderr_value.endswith("\n"): - stderr_value = stderr_value[:-1] - status = proc.returncode - else: - stream_copy(proc.stdout, output_stream, self.max_chunk_size) - stdout_value = output_stream - stderr_value = proc.stderr.read() - # strip trailing "\n" - if stderr_value.endswith("\n"): - stderr_value = stderr_value[:-1] - status = proc.wait() - # END stdout handling - finally: - proc.stdout.close() - proc.stderr.close() + # Allow the user to have the command executed in their working dir. + if with_keep_cwd or self._working_dir is None: + cwd = os.getcwd() + else: + cwd=self._working_dir + + # Start the process + proc = Popen(command, + cwd=cwd, + stdin=istream, + stderr=PIPE, + stdout=PIPE, + close_fds=(os.name=='posix'),# unsupported on linux + **subprocess_kwargs + ) + if as_process: + return self.AutoInterrupt(proc, command) + + # Wait for the process to return + status = 0 + stdout_value = '' + stderr_value = '' + try: + if output_stream is None: + stdout_value, stderr_value = proc.communicate() + # strip trailing "\n" + if stdout_value.endswith("\n"): + stdout_value = stdout_value[:-1] + if stderr_value.endswith("\n"): + stderr_value = stderr_value[:-1] + status = proc.returncode + else: + stream_copy(proc.stdout, output_stream, self.max_chunk_size) + stdout_value = output_stream + stderr_value = proc.stderr.read() + # strip trailing "\n" + if stderr_value.endswith("\n"): + stderr_value = stderr_value[:-1] + status = proc.wait() + # END stdout handling + finally: + proc.stdout.close() + proc.stderr.close() - if self.GIT_PYTHON_TRACE == 'full': - cmdstr = " ".join(command) - if stderr_value: - print "%s -> %d; stdout: '%s'; stderr: '%s'" % (cmdstr, status, stdout_value, stderr_value) - elif stdout_value: - print "%s -> %d; stdout: '%s'" % (cmdstr, status, stdout_value) - else: - print "%s -> %d" % (cmdstr, status) - # END handle debug printing + if self.GIT_PYTHON_TRACE == 'full': + cmdstr = " ".join(command) + if stderr_value: + print "%s -> %d; stdout: '%s'; stderr: '%s'" % (cmdstr, status, stdout_value, stderr_value) + elif stdout_value: + print "%s -> %d; stdout: '%s'" % (cmdstr, status, stdout_value) + else: + print "%s -> %d" % (cmdstr, status) + # END handle debug printing - if with_exceptions and status != 0: - raise GitCommandError(command, status, stderr_value) + if with_exceptions and status != 0: + raise GitCommandError(command, status, stderr_value) - # Allow access to the command's status code - if with_extended_output: - return (status, stdout_value, stderr_value) - else: - return stdout_value + # Allow access to the command's status code + if with_extended_output: + return (status, stdout_value, stderr_value) + else: + return stdout_value - def transform_kwargs(self, **kwargs): - """Transforms Python style kwargs into git command line options.""" - args = list() - for k, v in kwargs.items(): - if len(k) == 1: - if v is True: - args.append("-%s" % k) - elif type(v) is not bool: - args.append("-%s%s" % (k, v)) - else: - if v is True: - args.append("--%s" % dashify(k)) - elif type(v) is not bool: - args.append("--%s=%s" % (dashify(k), v)) - return args + def transform_kwargs(self, **kwargs): + """Transforms Python style kwargs into git command line options.""" + args = list() + for k, v in kwargs.items(): + if len(k) == 1: + if v is True: + args.append("-%s" % k) + elif type(v) is not bool: + args.append("-%s%s" % (k, v)) + else: + if v is True: + args.append("--%s" % dashify(k)) + elif type(v) is not bool: + args.append("--%s=%s" % (dashify(k), v)) + return args - @classmethod - def __unpack_args(cls, arg_list): - if not isinstance(arg_list, (list,tuple)): - return [ str(arg_list) ] - - outlist = list() - for arg in arg_list: - if isinstance(arg_list, (list, tuple)): - outlist.extend(cls.__unpack_args( arg )) - # END recursion - else: - outlist.append(str(arg)) - # END for each arg - return outlist + @classmethod + def __unpack_args(cls, arg_list): + if not isinstance(arg_list, (list,tuple)): + return [ str(arg_list) ] + + outlist = list() + for arg in arg_list: + if isinstance(arg_list, (list, tuple)): + outlist.extend(cls.__unpack_args( arg )) + # END recursion + else: + outlist.append(str(arg)) + # END for each arg + return outlist - def _call_process(self, method, *args, **kwargs): - """Run the given git command with the specified arguments and return - the result as a String + def _call_process(self, method, *args, **kwargs): + """Run the given git command with the specified arguments and return + the result as a String - :param method: - is the command. Contained "_" characters will be converted to dashes, - such as in 'ls_files' to call 'ls-files'. + :param method: + is the command. Contained "_" characters will be converted to dashes, + such as in 'ls_files' to call 'ls-files'. - :param args: - 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 + :param args: + 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 - :param kwargs: - is a dict of keyword arguments. - This function accepts the same optional keyword arguments - as execute(). + :param kwargs: + is a dict of keyword arguments. + This function accepts the same optional keyword arguments + as execute(). - ``Examples``:: - git.rev_list('master', max_count=10, header=True) + ``Examples``:: + git.rev_list('master', max_count=10, header=True) - :return: Same as ``execute``""" - # Handle optional arguments prior to calling transform_kwargs - # otherwise these'll end up in args, which is bad. - _kwargs = dict() - for kwarg in execute_kwargs: - try: - _kwargs[kwarg] = kwargs.pop(kwarg) - except KeyError: - pass + :return: Same as ``execute``""" + # Handle optional arguments prior to calling transform_kwargs + # otherwise these'll end up in args, which is bad. + _kwargs = dict() + for kwarg in execute_kwargs: + try: + _kwargs[kwarg] = kwargs.pop(kwarg) + except KeyError: + pass - # Prepare the argument list - opt_args = self.transform_kwargs(**kwargs) - - ext_args = self.__unpack_args([a for a in args if a is not None]) - args = opt_args + ext_args - - def make_call(): - call = [self.GIT_PYTHON_GIT_EXECUTABLE, dashify(method)] - call.extend(args) - return call - #END utility to recreate call after changes - - if sys.platform == 'win32': - try: - try: - return self.execute(make_call(), **_kwargs) - except WindowsError: - # did we switch to git.cmd already, or was it changed from default ? permanently fail - if self.GIT_PYTHON_GIT_EXECUTABLE != self.git_exec_name: - raise - #END handle overridden variable - type(self).GIT_PYTHON_GIT_EXECUTABLE = self.git_exec_name_win - call = [self.GIT_PYTHON_GIT_EXECUTABLE] + list(args) - - try: - return self.execute(make_call(), **_kwargs) - finally: - import warnings - msg = "WARNING: Automatically switched to use git.cmd as git executable, which reduces performance by ~70%." - msg += "Its recommended to put git.exe into the PATH or to set the %s environment variable to the executable's location" % self._git_exec_env_var - warnings.warn(msg) - #END print of warning - #END catch first failure - except WindowsError: - raise WindowsError("The system cannot find or execute the file at %r" % self.GIT_PYTHON_GIT_EXECUTABLE) - #END provide better error message - else: - return self.execute(make_call(), **_kwargs) - #END handle windows default installation - - def _parse_object_header(self, header_line): - """ - :param header_line: - type_string size_as_int - - :return: (hex_sha, type_string, size_as_int) - - :raise ValueError: if the header contains indication for an error due to - incorrect input sha""" - tokens = header_line.split() - if len(tokens) != 3: - if not tokens: - raise ValueError("SHA could not be resolved, git returned: %r" % (header_line.strip())) - else: - raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip())) - # END handle actual return value - # END error handling - - if len(tokens[0]) != 40: - raise ValueError("Failed to parse header: %r" % header_line) - return (tokens[0], tokens[1], int(tokens[2])) - - def __prepare_ref(self, ref): - # required for command to separate refs on stdin - refstr = str(ref) # could be ref-object - if refstr.endswith("\n"): - return refstr - return refstr + "\n" - - def __get_persistent_cmd(self, attr_name, cmd_name, *args,**kwargs): - cur_val = getattr(self, attr_name) - if cur_val is not None: - return cur_val - - options = { "istream" : PIPE, "as_process" : True } - options.update( kwargs ) - - cmd = self._call_process( cmd_name, *args, **options ) - setattr(self, attr_name, cmd ) - return cmd - - def __get_object_header(self, cmd, ref): - cmd.stdin.write(self.__prepare_ref(ref)) - cmd.stdin.flush() - return self._parse_object_header(cmd.stdout.readline()) - - def get_object_header(self, ref): - """ Use this method to quickly examine the type and size of the object behind - the given ref. - - :note: The method will only suffer from the costs of command invocation - once and reuses the command in subsequent calls. - - :return: (hexsha, type_string, size_as_int)""" - cmd = self.__get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) - return self.__get_object_header(cmd, ref) - - def get_object_data(self, ref): - """ As get_object_header, but returns object data as well - :return: (hexsha, type_string, size_as_int,data_string) - :note: not threadsafe""" - hexsha, typename, size, stream = self.stream_object_data(ref) - data = stream.read(size) - del(stream) - return (hexsha, typename, size, data) - - def stream_object_data(self, ref): - """As get_object_header, but returns the data as a stream - :return: (hexsha, type_string, size_as_int, stream) - :note: This method is not threadsafe, you need one independent Command instance - per thread to be safe !""" - cmd = self.__get_persistent_cmd("cat_file_all", "cat_file", batch=True) - hexsha, typename, size = self.__get_object_header(cmd, ref) - return (hexsha, typename, size, self.CatFileContentStream(size, cmd.stdout)) - - def clear_cache(self): - """Clear all kinds of internal caches to release resources. - - Currently persistent commands will be interrupted. - - :return: self""" - self.cat_file_all = None - self.cat_file_header = None - return self + # Prepare the argument list + opt_args = self.transform_kwargs(**kwargs) + + ext_args = self.__unpack_args([a for a in args if a is not None]) + args = opt_args + ext_args + + def make_call(): + call = [self.GIT_PYTHON_GIT_EXECUTABLE, dashify(method)] + call.extend(args) + return call + #END utility to recreate call after changes + + if sys.platform == 'win32': + try: + try: + return self.execute(make_call(), **_kwargs) + except WindowsError: + # did we switch to git.cmd already, or was it changed from default ? permanently fail + if self.GIT_PYTHON_GIT_EXECUTABLE != self.git_exec_name: + raise + #END handle overridden variable + type(self).GIT_PYTHON_GIT_EXECUTABLE = self.git_exec_name_win + call = [self.GIT_PYTHON_GIT_EXECUTABLE] + list(args) + + try: + return self.execute(make_call(), **_kwargs) + finally: + import warnings + msg = "WARNING: Automatically switched to use git.cmd as git executable, which reduces performance by ~70%." + msg += "Its recommended to put git.exe into the PATH or to set the %s environment variable to the executable's location" % self._git_exec_env_var + warnings.warn(msg) + #END print of warning + #END catch first failure + except WindowsError: + raise WindowsError("The system cannot find or execute the file at %r" % self.GIT_PYTHON_GIT_EXECUTABLE) + #END provide better error message + else: + return self.execute(make_call(), **_kwargs) + #END handle windows default installation + + def _parse_object_header(self, header_line): + """ + :param header_line: + type_string size_as_int + + :return: (hex_sha, type_string, size_as_int) + + :raise ValueError: if the header contains indication for an error due to + incorrect input sha""" + tokens = header_line.split() + if len(tokens) != 3: + if not tokens: + raise ValueError("SHA could not be resolved, git returned: %r" % (header_line.strip())) + else: + raise ValueError("SHA %s could not be resolved, git returned: %r" % (tokens[0], header_line.strip())) + # END handle actual return value + # END error handling + + if len(tokens[0]) != 40: + raise ValueError("Failed to parse header: %r" % header_line) + return (tokens[0], tokens[1], int(tokens[2])) + + def __prepare_ref(self, ref): + # required for command to separate refs on stdin + refstr = str(ref) # could be ref-object + if refstr.endswith("\n"): + return refstr + return refstr + "\n" + + def __get_persistent_cmd(self, attr_name, cmd_name, *args,**kwargs): + cur_val = getattr(self, attr_name) + if cur_val is not None: + return cur_val + + options = { "istream" : PIPE, "as_process" : True } + options.update( kwargs ) + + cmd = self._call_process( cmd_name, *args, **options ) + setattr(self, attr_name, cmd ) + return cmd + + def __get_object_header(self, cmd, ref): + cmd.stdin.write(self.__prepare_ref(ref)) + cmd.stdin.flush() + return self._parse_object_header(cmd.stdout.readline()) + + def get_object_header(self, ref): + """ Use this method to quickly examine the type and size of the object behind + the given ref. + + :note: The method will only suffer from the costs of command invocation + once and reuses the command in subsequent calls. + + :return: (hexsha, type_string, size_as_int)""" + cmd = self.__get_persistent_cmd("cat_file_header", "cat_file", batch_check=True) + return self.__get_object_header(cmd, ref) + + def get_object_data(self, ref): + """ As get_object_header, but returns object data as well + :return: (hexsha, type_string, size_as_int,data_string) + :note: not threadsafe""" + hexsha, typename, size, stream = self.stream_object_data(ref) + data = stream.read(size) + del(stream) + return (hexsha, typename, size, data) + + def stream_object_data(self, ref): + """As get_object_header, but returns the data as a stream + :return: (hexsha, type_string, size_as_int, stream) + :note: This method is not threadsafe, you need one independent Command instance + per thread to be safe !""" + cmd = self.__get_persistent_cmd("cat_file_all", "cat_file", batch=True) + hexsha, typename, size = self.__get_object_header(cmd, ref) + return (hexsha, typename, size, self.CatFileContentStream(size, cmd.stdout)) + + def clear_cache(self): + """Clear all kinds of internal caches to release resources. + + Currently persistent commands will be interrupted. + + :return: self""" + self.cat_file_all = None + self.cat_file_header = None + return self diff --git a/git/config.py b/git/config.py index c71bb8ca4..285ade6b7 100644 --- a/git/config.py +++ b/git/config.py @@ -18,408 +18,408 @@ __all__ = ('GitConfigParser', 'SectionConstraint') class MetaParserBuilder(type): - """Utlity class wrapping base-class methods into decorators that assure read-only properties""" - def __new__(metacls, name, bases, clsdict): - """ - Equip all base-class methods with a needs_values decorator, and all non-const methods - with a set_dirty_and_flush_changes decorator in addition to that.""" - kmm = '_mutating_methods_' - if kmm in clsdict: - mutating_methods = clsdict[kmm] - for base in bases: - methods = ( t for t in inspect.getmembers(base, inspect.ismethod) if not t[0].startswith("_") ) - for name, method in methods: - if name in clsdict: - continue - method_with_values = needs_values(method) - if name in mutating_methods: - method_with_values = set_dirty_and_flush_changes(method_with_values) - # END mutating methods handling - - clsdict[name] = method_with_values - # END for each name/method pair - # END for each base - # END if mutating methods configuration is set - - new_type = super(MetaParserBuilder, metacls).__new__(metacls, name, bases, clsdict) - return new_type - - + """Utlity class wrapping base-class methods into decorators that assure read-only properties""" + def __new__(metacls, name, bases, clsdict): + """ + Equip all base-class methods with a needs_values decorator, and all non-const methods + with a set_dirty_and_flush_changes decorator in addition to that.""" + kmm = '_mutating_methods_' + if kmm in clsdict: + mutating_methods = clsdict[kmm] + for base in bases: + methods = ( t for t in inspect.getmembers(base, inspect.ismethod) if not t[0].startswith("_") ) + for name, method in methods: + if name in clsdict: + continue + method_with_values = needs_values(method) + if name in mutating_methods: + method_with_values = set_dirty_and_flush_changes(method_with_values) + # END mutating methods handling + + clsdict[name] = method_with_values + # END for each name/method pair + # END for each base + # END if mutating methods configuration is set + + new_type = super(MetaParserBuilder, metacls).__new__(metacls, name, bases, clsdict) + return new_type + + def needs_values(func): - """Returns method assuring we read values (on demand) before we try to access them""" - def assure_data_present(self, *args, **kwargs): - self.read() - return func(self, *args, **kwargs) - # END wrapper method - assure_data_present.__name__ = func.__name__ - return assure_data_present - + """Returns method assuring we read values (on demand) before we try to access them""" + def assure_data_present(self, *args, **kwargs): + self.read() + return func(self, *args, **kwargs) + # END wrapper method + assure_data_present.__name__ = func.__name__ + return assure_data_present + def set_dirty_and_flush_changes(non_const_func): - """Return method that checks whether given non constant function may be called. - If so, the instance will be set dirty. - Additionally, we flush the changes right to disk""" - def flush_changes(self, *args, **kwargs): - rval = non_const_func(self, *args, **kwargs) - self.write() - return rval - # END wrapper method - flush_changes.__name__ = non_const_func.__name__ - return flush_changes - + """Return method that checks whether given non constant function may be called. + If so, the instance will be set dirty. + Additionally, we flush the changes right to disk""" + def flush_changes(self, *args, **kwargs): + rval = non_const_func(self, *args, **kwargs) + self.write() + return rval + # END wrapper method + flush_changes.__name__ = non_const_func.__name__ + return flush_changes + class SectionConstraint(object): - """Constrains a ConfigParser to only option commands which are constrained to - always use the section we have been initialized with. - - It supports all ConfigParser methods that operate on an option""" - __slots__ = ("_config", "_section_name") - _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option", - "remove_section", "remove_option", "options") - - def __init__(self, config, section): - self._config = config - self._section_name = section - - def __getattr__(self, attr): - if attr in self._valid_attrs_: - return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) - return super(SectionConstraint,self).__getattribute__(attr) - - def _call_config(self, method, *args, **kwargs): - """Call the configuration at the given method which must take a section name - as first argument""" - return getattr(self._config, method)(self._section_name, *args, **kwargs) - - @property - def config(self): - """return: Configparser instance we constrain""" - return self._config - + """Constrains a ConfigParser to only option commands which are constrained to + always use the section we have been initialized with. + + It supports all ConfigParser methods that operate on an option""" + __slots__ = ("_config", "_section_name") + _valid_attrs_ = ("get_value", "set_value", "get", "set", "getint", "getfloat", "getboolean", "has_option", + "remove_section", "remove_option", "options") + + def __init__(self, config, section): + self._config = config + self._section_name = section + + def __getattr__(self, attr): + if attr in self._valid_attrs_: + return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) + return super(SectionConstraint,self).__getattribute__(attr) + + def _call_config(self, method, *args, **kwargs): + """Call the configuration at the given method which must take a section name + as first argument""" + return getattr(self._config, method)(self._section_name, *args, **kwargs) + + @property + def config(self): + """return: Configparser instance we constrain""" + return self._config + class GitConfigParser(cp.RawConfigParser, object): - """Implements specifics required to read git style configuration files. - - This variation behaves much like the git.config command such that the configuration - will be read on demand based on the filepath given during initialization. - - The changes will automatically be written once the instance goes out of scope, but - can be triggered manually as well. - - The configuration file will be locked if you intend to change values preventing other - instances to write concurrently. - - :note: - The config is case-sensitive even when queried, hence section and option names - must match perfectly.""" - __metaclass__ = MetaParserBuilder - - - #{ Configuration - # The lock type determines the type of lock to use in new configuration readers. - # They must be compatible to the LockFile interface. - # A suitable alternative would be the BlockingLockFile - t_lock = LockFile - re_comment = re.compile('^\s*[#;]') - - #} END configuration - - OPTCRE = re.compile( - r'\s*(?P