diff --git a/.travis.yml b/.travis.yml index 1fda20189..b53228ca1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,18 +15,19 @@ install: - pip install coveralls flake8 sphinx # generate some reflog as git-python tests need it (in master) + - git tag __testing_point__ - git checkout master - git reset --hard HEAD~1 - git reset --hard HEAD~1 - git reset --hard HEAD~1 - - git reset --hard origin/master + - git reset --hard __testing_point__ # as commits are performed with the default user, it needs to be set for travis too - git config --global user.email "travis@ci.com" - git config --global user.name "Travis Runner" script: # Make sure we limit open handles to see if we are leaking them - - ulimit -n 64 + - ulimit -n 96 - ulimit -n - nosetests -v --with-coverage - flake8 diff --git a/README.md b/README.md index 0019dc179..1895635fd 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,11 @@ The object database implementation is optimized for handling large quantities of ### REQUIREMENTS -* Git ( tested with 1.8.3.4 ) -* Python Nose - used for running the tests - - Tested with nose 1.3.0 -* Mock by Michael Foord used for tests - - Tested with 1.0.1 -* Coverage - used for tests coverage +GitPython needs the `git` executable to be installed on the system and available in your `PATH` for most operations. If it is not in your `PATH`, you can help GitPython find it by setting the `GIT_PYTHON_GIT_EXECUTABLE=` environment variable. -The list of dependencies are listed in /requirements.txt and /test-requirements.txt. The installer takes care of installing them for you though. +* Git (1.7.x or newer) + +The list of dependencies are listed in `./requirements.txt` and `./test-requirements.txt`. The installer takes care of installing them for you. ### INSTALL @@ -62,8 +59,16 @@ You can watch me fix issues or implement new features [live on Twitch][twitch-ch ### INFRASTRUCTURE * [User Documentation](http://gitpython.readthedocs.org) +* [Questions and Answers](http://stackexchange.com/filters/167317/gitpython) + * Please post on stackoverflow and use the `gitpython` tag * [Mailing List](http://groups.google.com/group/git-python) + * Please use it for everything that doesn't fit Stackoverflow. * [Issue Tracker](https://github.com/gitpython-developers/GitPython/issues) + * Post reproducible bugs and feature requests as a new issue. Please be sure to provide the following information if posting bugs: + * GitPython version (e.g. `import git; git.__version__`) + * Python version (e.g. `python --version`) + * The encountered stack-trace, if applicable + * Enough information to allow reproducing the issue ### LICENSE diff --git a/VERSION b/VERSION index 449d7e73a..0f8268533 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.6 +0.3.7 diff --git a/doc/source/changes.rst b/doc/source/changes.rst index 5b85e56ab..d9f44a86c 100644 --- a/doc/source/changes.rst +++ b/doc/source/changes.rst @@ -2,6 +2,24 @@ Changelog ========= +0.3.7 - Fixes +============= +* `IndexFile.add()` will now write the index without any extension data by default. However, you may override this behaviour with the new `write_extension_data` keyword argument. + + - Renamed `ignore_tree_extension_data` keyword argument in `IndexFile.write(...)` to `ignore_extension_data` +* If the git command executed during `Remote.push(...)|fetch(...)` returns with an non-zero exit code and GitPython didn't + obtain any head-information, the corresponding `GitCommandError` will be raised. This may break previous code which expected + these operations to never raise. However, that behavious is undesirable as it would effectively hide the fact that there + was an error. See `this issue `_ for more information. + +* If the git executable can't be found in the PATH or at the path provided by `GIT_PYTHON_GIT_EXECUTABLE`, this is made + obvious by throwing `GitCommandNotFound`, both on unix and on windows. + + - Those who support **GUI on windows** will now have to set `git.Git.USE_SHELL = True` to get the previous behaviour. + +* A list of all issues can be found `on github `_ + + 0.3.6 - Features ================ * **DOCS** diff --git a/doc/source/conf.py b/doc/source/conf.py index 942996b0b..add686d3f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -43,7 +43,7 @@ # General information about the project. project = u'GitPython' -copyright = u'Copyright (C) 2008, 2009 Michael Trier and contributors, 2010 Sebastian Thiel' +copyright = u'Copyright (C) 2008, 2009 Michael Trier and contributors, 2010-2015 Sebastian Thiel' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -94,7 +94,6 @@ # ----------------------- html_theme_options = { - "stickysidebar": "true" } # The style sheet to use for HTML and HTML Help pages. A file of that name diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 0d60f0aae..632d2d0cc 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -331,12 +331,19 @@ You can easily access configuration information for a remote by accessing option :start-after: # [26-test_references_and_objects] :end-before: # ![26-test_references_and_objects] -You can also specify per-call custom environments using a new context manager on the Git command +You can also specify per-call custom environments using a new context manager on the Git command, e.g. for using a specific SSH key. The following example works with `git` starting at *v2.3*:: -.. literalinclude:: ../../git/test/test_docs.py - :language: python - :start-after: # [32-test_references_and_objects] - :end-before: # ![32-test_references_and_objects] + ssh_cmd = 'ssh -i id_deployment_key' + with repo.git.custom_environment(GIT_SSH_COMMAND=ssh_cmd): + repo.remotes.origin.fetch() + +This one sets a custom script to be executed in place of `ssh`, and can be used in `git` prior to *v2.3*:: + + ssh_executable = os.path.join(rw_dir, 'my_ssh_executable.sh') + with repo.git.custom_environment(GIT_SSH=ssh_executable): + repo.remotes.origin.fetch() + +You might also have a look at `Git.update_environment(...)` in case you want to setup a changed environment more permanently. Submodule Handling ****************** diff --git a/git/__init__.py b/git/__init__.py index 5630b91d0..e8dae2723 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -15,7 +15,8 @@ #{ 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')) + if __version__ == 'git': + sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'ext', 'gitdb')) try: import gitdb diff --git a/git/cmd.py b/git/cmd.py index 7e15d4eaa..429046be1 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -26,7 +26,10 @@ stream_copy, WaitGroup ) -from .exc import GitCommandError +from .exc import ( + GitCommandError, + GitCommandNotFound +) from git.compat import ( string_types, defenc, @@ -241,6 +244,12 @@ class Git(LazyMixin): _git_exec_env_var = "GIT_PYTHON_GIT_EXECUTABLE" GIT_PYTHON_GIT_EXECUTABLE = os.environ.get(_git_exec_env_var, git_exec_name) + # If True, a shell will be used when executing git commands. + # This should only be desirable on windows, see https://github.com/gitpython-developers/GitPython/pull/126 + # for more information + # Override this value using `Git.USE_SHELL = True` + USE_SHELL = False + class AutoInterrupt(object): """Kill/Interrupt the stored process instance once this instance goes out of scope. It is @@ -543,18 +552,29 @@ def execute(self, command, env["LC_MESSAGES"] = "C" env.update(self._environment) - proc = Popen(command, - env=env, - cwd=cwd, - stdin=istream, - stderr=PIPE, - stdout=PIPE, - # Prevent cmd prompt popups on windows by using a shell ... . - # See https://github.com/gitpython-developers/GitPython/pull/126 - shell=sys.platform == 'win32', - close_fds=(os.name == 'posix'), # unsupported on windows - **subprocess_kwargs - ) + if sys.platform == 'win32': + cmd_not_found_exception = WindowsError + else: + if sys.version_info[0] > 2: + cmd_not_found_exception = FileNotFoundError # NOQA # this is defined, but flake8 doesn't know + else: + cmd_not_found_exception = OSError + # end handle + + try: + proc = Popen(command, + env=env, + cwd=cwd, + stdin=istream, + stderr=PIPE, + stdout=PIPE, + shell=self.USE_SHELL, + close_fds=(os.name == 'posix'), # unsupported on windows + **subprocess_kwargs + ) + except cmd_not_found_exception as err: + raise GitCommandNotFound(str(err)) + if as_process: return self.AutoInterrupt(proc, command) @@ -753,11 +773,23 @@ def _call_process(self, method, *args, **kwargs): except KeyError: pass + insert_after_this_arg = kwargs.pop('insert_kwargs_after', None) + # 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 + if insert_after_this_arg is None: + args = opt_args + ext_args + else: + try: + index = ext_args.index(insert_after_this_arg) + except ValueError: + raise ValueError("Couldn't find argument '%s' in args %s to insert kwargs after" + % (insert_after_this_arg, str(ext_args))) + # end handle error + args = ext_args[:index + 1] + opt_args + ext_args[index + 1:] + # end handle kwargs def make_call(): call = [self.GIT_PYTHON_GIT_EXECUTABLE] diff --git a/git/diff.py b/git/diff.py index 378823696..dc53f3f71 100644 --- a/git/diff.py +++ b/git/diff.py @@ -168,11 +168,13 @@ class Diff(object): a_mode is None a_blob is None + a_path is None ``Deleted File``:: b_mode is None b_blob is None + b_path is None ``Working Tree Blobs`` @@ -200,8 +202,8 @@ class Diff(object): NULL_HEX_SHA = "0" * 40 NULL_BIN_SHA = b"\0" * 20 - __slots__ = ("a_blob", "b_blob", "a_mode", "b_mode", "new_file", "deleted_file", - "rename_from", "rename_to", "diff") + __slots__ = ("a_blob", "b_blob", "a_mode", "b_mode", "a_path", "b_path", + "new_file", "deleted_file", "rename_from", "rename_to", "diff") def __init__(self, repo, a_path, b_path, a_blob_id, b_blob_id, a_mode, b_mode, new_file, deleted_file, rename_from, @@ -210,6 +212,9 @@ def __init__(self, repo, a_path, b_path, a_blob_id, b_blob_id, a_mode, self.a_mode = a_mode self.b_mode = b_mode + self.a_path = a_path + self.b_path = b_path + if self.a_mode: self.a_mode = mode_str_to_int(self.a_mode) if self.b_mode: diff --git a/git/exc.py b/git/exc.py index 7ee6726e8..f5b52374b 100644 --- a/git/exc.py +++ b/git/exc.py @@ -18,6 +18,12 @@ class NoSuchPathError(OSError): """ Thrown if a path could not be access by the system. """ +class GitCommandNotFound(Exception): + """Thrown if we cannot find the `git` executable in the PATH or at the path given by + the GIT_PYTHON_GIT_EXECUTABLE environment variable""" + pass + + class GitCommandError(Exception): """ Thrown if execution of the git command fails with non-zero status code. """ diff --git a/git/index/base.py b/git/index/base.py index b73edd6f3..10de3358a 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -172,16 +172,17 @@ def _entries_sorted(self): """:return: list of entries, in a sorted fashion, first by path, then by stage""" return sorted(self.entries.values(), key=lambda e: (e.path, e.stage)) - def _serialize(self, stream, ignore_tree_extension_data=False): + def _serialize(self, stream, ignore_extension_data=False): entries = self._entries_sorted() - write_cache(entries, - stream, - (ignore_tree_extension_data and None) or self._extension_data) + extension_data = self._extension_data + if ignore_extension_data: + extension_data = None + write_cache(entries, stream, extension_data) return self #} END serializable interface - def write(self, file_path=None, ignore_tree_extension_data=False): + def write(self, file_path=None, ignore_extension_data=False): """Write the current state to our file path or to the given one :param file_path: @@ -190,9 +191,10 @@ def write(self, file_path=None, ignore_tree_extension_data=False): Please note that this will change the file_path of this index to the one you gave. - :param ignore_tree_extension_data: + :param ignore_extension_data: If True, the TREE type extension data read in the index will not - be written to disk. Use this if you have altered the index and + be written to disk. NOTE that no extension data is actually written. + Use this if you have altered the index and would like to use git-write-tree afterwards to create a tree representing your written changes. If this data is present in the written index, git-write-tree @@ -208,7 +210,7 @@ def write(self, file_path=None, ignore_tree_extension_data=False): lfd = LockedFD(file_path or self._file_path) stream = lfd.open(write=True, stream=True) - self._serialize(stream, ignore_tree_extension_data) + self._serialize(stream, ignore_extension_data) lfd.commit() @@ -577,6 +579,7 @@ def _store_path(self, filepath, fprogress): fprogress(filepath, False, filepath) istream = self.repo.odb.store(IStream(Blob.type, st.st_size, stream)) fprogress(filepath, True, filepath) + stream.close() return BaseIndexEntry((stat_mode_to_index_mode(st.st_mode), istream.binsha, 0, to_native_path_linux(filepath))) @@ -612,7 +615,7 @@ def _entries_for_paths(self, paths, path_rewriter, fprogress, entries): return entries_added def add(self, items, force=True, fprogress=lambda *args: None, path_rewriter=None, - write=True): + write=True, write_extension_data=False): """Add files from the working tree, specific blobs or BaseIndexEntries to the index. @@ -625,6 +628,10 @@ def add(self, items, force=True, fprogress=lambda *args: None, path_rewriter=Non strings denote a relative or absolute path into the repository pointing to an existing file, i.e. CHANGES, lib/myfile.ext, '/home/gitrepo/lib/myfile.ext'. + Absolute paths must start with working tree directory of this index's repository + to be considered valid. For example, if it was initialized with a non-normalized path, like + `/root/repo/../repo`, absolute paths to be added must start with `/root/repo/../repo`. + Paths provided like this must exist. When added, they will be written into the object database. @@ -685,8 +692,19 @@ def add(self, items, force=True, fprogress=lambda *args: None, path_rewriter=Non Please note that entry.path is relative to the git repository. :param write: - If True, the index will be written once it was altered. Otherwise - the changes only exist in memory and are not available to git commands. + If True, the index will be written once it was altered. Otherwise + the changes only exist in memory and are not available to git commands. + + :param write_extension_data: + If True, extension data will be written back to the index. This can lead to issues in case + it is containing the 'TREE' extension, which will cause the `git commit` command to write an + old tree, instead of a new one representing the now changed index. + This doesn't matter if you use `IndexFile.commit()`, which ignores the `TREE` extension altogether. + You should set it to True if you intend to use `IndexFile.commit()` exclusively while maintaining + support for third-party extensions. Besides that, you can usually safely ignore the built-in + extensions when using GitPython on repositories that are not handled manually at all. + All current built-in extensions are listed here: + http://opensource.apple.com/source/Git/Git-26/src/git-htmldocs/technical/index-format.txt :return: List(BaseIndexEntries) representing the entries just actually added. @@ -759,7 +777,7 @@ def handle_null_entries(self): self.entries[(entry.path, 0)] = IndexEntry.from_base(entry) if write: - self.write() + self.write(ignore_extension_data=not write_extension_data) # END handle write return entries_added diff --git a/git/index/fun.py b/git/index/fun.py index f07cf7dc3..c1188ccbc 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -10,8 +10,6 @@ S_IFREG, ) -S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule - from io import BytesIO import os import subprocess @@ -33,7 +31,6 @@ CE_NAMEMASK, CE_STAGESHIFT ) -CE_NAMEMASK_INV = ~CE_NAMEMASK from .util import ( pack, @@ -47,6 +44,9 @@ force_text ) +S_IFGITLINK = S_IFLNK | S_IFDIR # a submodule +CE_NAMEMASK_INV = ~CE_NAMEMASK + __all__ = ('write_cache', 'read_cache', 'write_tree_from_cache', 'entry_key', 'stat_mode_to_index_mode', 'S_IFGITLINK', 'run_commit_hook', 'hook_path') @@ -72,6 +72,7 @@ def run_commit_hook(name, index): env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + cwd=index.repo.working_dir, close_fds=(os.name == 'posix')) stdout, stderr = cmd.communicate() cmd.stdout.close() diff --git a/git/objects/commit.py b/git/objects/commit.py index b9718694f..f13760fdb 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -189,9 +189,12 @@ def iter_items(cls, repo, rev, paths='', **kwargs): if 'pretty' in kwargs: raise ValueError("--pretty cannot be used as parsing expects single sha's only") # END handle pretty - args = list() + + # use -- in any case, to prevent possibility of ambiguous arguments + # see https://github.com/gitpython-developers/GitPython/issues/264 + args = ['--'] if paths: - args.extend(('--', paths)) + args.extend((paths, )) # END if paths proc = repo.git.rev_list(rev, args, as_process=True, **kwargs) diff --git a/git/objects/submodule/base.py b/git/objects/submodule/base.py index be243acc5..f9b0b6ad6 100644 --- a/git/objects/submodule/base.py +++ b/git/objects/submodule/base.py @@ -37,7 +37,7 @@ import os import logging -import tempfile +import uuid __all__ = ["Submodule", "UpdateProgress"] @@ -136,7 +136,7 @@ def _get_intermediate_items(self, item): @classmethod def _need_gitfile_submodules(cls, git): - return git.version_info[:3] >= (1, 8, 0) + return git.version_info[:3] >= (1, 7, 0) def __eq__(self, other): """Compare with another submodule""" @@ -293,7 +293,8 @@ def _write_git_file_and_module_config(cls, working_tree_dir, module_abspath): fp.close() writer = GitConfigParser(os.path.join(module_abspath, 'config'), read_only=False, merge_includes=False) - writer.set_value('core', 'worktree', os.path.relpath(working_tree_dir, start=module_abspath)) + writer.set_value('core', 'worktree', + to_native_path_linux(os.path.relpath(working_tree_dir, start=module_abspath))) writer.release() #{ Edit Interface @@ -578,11 +579,13 @@ def update(self, recursive=False, init=True, to_latest_revision=False, progress= base_commit = mrepo.merge_base(mrepo.head.commit, hexsha) if len(base_commit) == 0 or base_commit[0].hexsha == hexsha: if force: - log.debug("Will force checkout or reset on local branch that is possibly in the future of" - + "the commit it will be checked out to, effectively 'forgetting' new commits") + msg = "Will force checkout or reset on local branch that is possibly in the future of" + msg += "the commit it will be checked out to, effectively 'forgetting' new commits" + log.debug(msg) else: - log.info("Skipping %s on branch '%s' of submodule repo '%s' as it contains " - + "un-pushed commits", is_detached and "checkout" or "reset", mrepo.head, mrepo) + msg = "Skipping %s on branch '%s' of submodule repo '%s' as it contains un-pushed commits" + msg %= (is_detached and "checkout" or "reset", mrepo.head, mrepo) + log.info(msg) may_reset = False # end handle force # end handle if we are in the future @@ -992,7 +995,7 @@ def rename(self, new_name): source_dir = mod.git_dir # Let's be sure the submodule name is not so obviously tied to a directory if destination_module_abspath.startswith(mod.git_dir): - tmp_dir = self._module_abspath(self.repo, self.path, os.path.basename(tempfile.mkdtemp())) + tmp_dir = self._module_abspath(self.repo, self.path, str(uuid.uuid4())) os.renames(source_dir, tmp_dir) source_dir = tmp_dir # end handle self-containment diff --git a/git/objects/util.py b/git/objects/util.py index cefef862d..567b1d5b2 100644 --- a/git/objects/util.py +++ b/git/objects/util.py @@ -216,7 +216,7 @@ def __getattr__(self, attr): class Traversable(object): - """Simple interface to perforam depth-first or breadth-first traversals + """Simple interface to perform depth-first or breadth-first traversals into one direction. Subclasses only need to implement one function. Instances of the Subclass must be hashable""" diff --git a/git/refs/symbolic.py b/git/refs/symbolic.py index fed7006e6..d884250f7 100644 --- a/git/refs/symbolic.py +++ b/git/refs/symbolic.py @@ -140,6 +140,7 @@ def _get_ref_info(cls, repo, ref_path): # Don't only split on spaces, but on whitespace, which allows to parse lines like # 60b64ef992065e2600bfef6187a97f92398a9144 branch 'master' of git-server:/path/to/repo tokens = value.split() + assert(len(tokens) != 0) except (OSError, IOError): # Probably we are just packed, find our entry in the packed refs file # NOTE: We are not a symbolic ref if we are in a packed file, as these @@ -582,6 +583,8 @@ def _iter_items(cls, repo, common_path=None): # END prune non-refs folders for f in files: + if f == 'packed-refs': + continue abs_path = to_native_path_linux(join_path(root, f)) rela_paths.add(abs_path.replace(to_native_path_linux(repo.git_dir) + '/', "")) # END for each file in root directory diff --git a/git/remote.py b/git/remote.py index d048f87bc..4baa2838c 100644 --- a/git/remote.py +++ b/git/remote.py @@ -455,7 +455,13 @@ def stale_refs(self): remote side, but are still available locally. The IterableList is prefixed, hence the 'origin' must be omitted. See - 'refs' property for an example.""" + 'refs' property for an example. + + To make things more complicated, it can be possble for the list to include + other kinds of references, for example, tag references, if these are stale + as well. This is a fix for the issue described here: + https://github.com/gitpython-developers/GitPython/issues/260 + """ out_refs = IterableList(RemoteReference._id_attribute_, "%s/" % self.name) for line in self.repo.git.remote("prune", "--dry-run", self).splitlines()[2:]: # expecting @@ -463,8 +469,14 @@ def stale_refs(self): token = " * [would prune] " if not line.startswith(token): raise ValueError("Could not parse git-remote prune result: %r" % line) - fqhn = "%s/%s" % (RemoteReference._common_path_default, line.replace(token, "")) - out_refs.append(RemoteReference(self.repo, fqhn)) + ref_name = line.replace(token, "") + # sometimes, paths start with a full ref name, like refs/tags/foo, see #260 + if ref_name.startswith(Reference._common_path_default + '/'): + out_refs.append(SymbolicReference.from_path(self.repo, ref_name)) + else: + fqhn = "%s/%s" % (RemoteReference._common_path_default, ref_name) + out_refs.append(RemoteReference(self.repo, fqhn)) + # end special case handlin # END for each line return out_refs @@ -477,7 +489,9 @@ def create(cls, repo, name, url, **kwargs): :param kwargs: Additional arguments to be passed to the git-remote add command :return: New Remote instance :raise GitCommandError: in case an origin with that name already exists""" - repo.git.remote("add", name, url, **kwargs) + scmd = 'add' + kwargs['insert_kwargs_after'] = scmd + repo.git.remote(scmd, name, url, **kwargs) return cls(repo, name) # add is an alias @@ -517,7 +531,9 @@ def update(self, **kwargs): Additional arguments passed to git-remote update :return: self """ - self.repo.git.remote("update", self.name) + scmd = 'update' + kwargs['insert_kwargs_after'] = scmd + self.repo.git.remote(scmd, self.name, **kwargs) return self def _get_fetch_info_from_stderr(self, proc, progress): @@ -552,7 +568,12 @@ def _get_fetch_info_from_stderr(self, proc, progress): # end # We are only interested in stderr here ... - finalize_process(proc) + try: + finalize_process(proc) + except Exception: + if len(fetch_info_lines) == 0: + raise + # end exception handler # read head information fp = open(join(self.repo.git_dir, 'FETCH_HEAD'), 'rb') @@ -585,7 +606,11 @@ def stdout_handler(line): # END exception handling # END for each line - handle_process_output(proc, stdout_handler, progress_handler, finalize_process) + try: + handle_process_output(proc, stdout_handler, progress_handler, finalize_process) + except Exception: + if len(output) == 0: + raise return output def fetch(self, refspec=None, progress=None, **kwargs): diff --git a/git/repo/base.py b/git/repo/base.py index b7a9e29dc..e59cb0c7d 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -121,6 +121,11 @@ def __init__(self, path=None, odbt=DefaultDBType, search_parent_directories=Fals Object DataBase type - a type which is constructed by providing the directory containing the database objects, i.e. .git/objects. It will be used to access all object data + :param search_parent_directories: + if True, all parent directories will be searched for a valid repo as well. + + Please note that this was the default behaviour in older versions of GitPython, + which is considered a bug though. :raise InvalidGitRepositoryError: :raise NoSuchPathError: :return: git.Repo """ diff --git a/git/test/lib/helper.py b/git/test/lib/helper.py index 8300f2722..541b972de 100644 --- a/git/test/lib/helper.py +++ b/git/test/lib/helper.py @@ -15,7 +15,9 @@ from git import Repo, Remote, GitCommandError, Git from git.compat import string_types -GIT_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +osp = os.path.dirname + +GIT_REPO = os.environ.get("GIT_PYTHON_TEST_GIT_REPO_BASE", osp(osp(osp(osp(__file__))))) __all__ = ( 'fixture_path', 'fixture', 'absolute_project_path', 'StringProcessAdapter', @@ -26,7 +28,7 @@ def fixture_path(name): - test_dir = os.path.dirname(os.path.dirname(__file__)) + test_dir = osp(osp(__file__)) return os.path.join(test_dir, "fixtures", name) @@ -35,7 +37,7 @@ def fixture(name): def absolute_project_path(): - return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + return os.path.abspath(os.path.join(osp(__file__), "..", "..")) #} END routines @@ -195,7 +197,7 @@ def remote_repo_creator(self): d_remote.config_writer.set('url', remote_repo_url) - temp_dir = os.path.dirname(_mktemp()) + temp_dir = osp(_mktemp()) # On windows, this will fail ... we deal with failures anyway and default to telling the user to do it try: gd = Git().daemon(temp_dir, enable='receive-pack', as_process=True) @@ -230,7 +232,13 @@ def remote_repo_creator(self): prev_cwd = os.getcwd() os.chdir(rw_repo.working_dir) try: - return func(self, rw_repo, rw_remote_repo) + try: + return func(self, rw_repo, rw_remote_repo) + except: + print("Keeping repos after failure: repo_dir = %s, remote_repo_dir = %s" + % (repo_dir, remote_repo_dir), file=sys.stderr) + repo_dir = remote_repo_dir = None + raise finally: # gd.proc.kill() ... no idea why that doesn't work if gd is not None: @@ -239,8 +247,10 @@ def remote_repo_creator(self): os.chdir(prev_cwd) rw_repo.git.clear_cache() rw_remote_repo.git.clear_cache() - shutil.rmtree(repo_dir, onerror=_rmtree_onerror) - shutil.rmtree(remote_repo_dir, onerror=_rmtree_onerror) + if repo_dir: + shutil.rmtree(repo_dir, onerror=_rmtree_onerror) + if remote_repo_dir: + shutil.rmtree(remote_repo_dir, onerror=_rmtree_onerror) if gd is not None: gd.proc.wait() diff --git a/git/test/test_base.py b/git/test/test_base.py index 91b9d0053..94379ca34 100644 --- a/git/test/test_base.py +++ b/git/test/test_base.py @@ -5,6 +5,7 @@ # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php import os +import sys import tempfile import git.objects.base as base @@ -116,6 +117,14 @@ def test_add_unicode(self, rw_repo): filename = u"שלום.txt" file_path = os.path.join(rw_repo.working_dir, filename) + + # verify first that we could encode file name in this environment + try: + file_path.encode(sys.getfilesystemencoding()) + except UnicodeEncodeError: + from nose import SkipTest + raise SkipTest("Environment doesn't support unicode filenames") + open(file_path, "wb").write(b'something') if os.name == 'nt': diff --git a/git/test/test_commit.py b/git/test/test_commit.py index 1f0f8c561..3e958edf2 100644 --- a/git/test/test_commit.py +++ b/git/test/test_commit.py @@ -19,15 +19,19 @@ Actor, ) from gitdb import IStream +from gitdb.test.lib import with_rw_directory from git.compat import ( string_types, text_type ) +from git import Repo +from git.repo.fun import touch from io import BytesIO import time import sys import re +import os def assert_commit_serialization(rwrepo, commit_id, print_performance_info=False): @@ -219,6 +223,15 @@ def test_rev_list_bisect_all(self): for sha1, commit in zip(expected_ids, commits): assert_equal(sha1, commit.hexsha) + @with_rw_directory + def test_ambiguous_arg_iteration(self, rw_dir): + rw_repo = Repo.init(os.path.join(rw_dir, 'test_ambiguous_arg')) + path = os.path.join(rw_repo.working_tree_dir, 'master') + touch(path) + rw_repo.index.add([path]) + rw_repo.index.commit('initial commit') + list(rw_repo.iter_commits(rw_repo.head.ref)) # should fail unless bug is fixed + def test_count(self): assert self.rorepo.tag('refs/tags/0.1.5').commit.count() == 143 diff --git a/git/test/test_docs.py b/git/test/test_docs.py index 8dfef1c6d..586f0ce4a 100644 --- a/git/test/test_docs.py +++ b/git/test/test_docs.py @@ -437,15 +437,6 @@ def test_references_and_objects(self, rw_dir): git.for_each_ref() # '-' becomes '_' when calling it # ![31-test_references_and_objects] - # [32-test_references_and_objects] - ssh_executable = os.path.join(rw_dir, 'my_ssh_executable.sh') - with repo.git.custom_environment(GIT_SSH=ssh_executable): - # Note that we don't actually make the call here, as our test-setup doesn't permit it to - # succeed. - # It will in your case :) - repo.remotes.origin.fetch - # ![32-test_references_and_objects] - def test_submodules(self): # [1-test_submodules] repo = self.rorepo diff --git a/git/test/test_git.py b/git/test/test_git.py index 8087bc454..742c842db 100644 --- a/git/test/test_git.py +++ b/git/test/test_git.py @@ -19,6 +19,7 @@ from git import ( Git, GitCommandError, + GitCommandNotFound, Repo ) from gitdb.test.lib import with_rw_directory @@ -127,11 +128,7 @@ def test_version(self): def test_cmd_override(self): prev_cmd = self.git.GIT_PYTHON_GIT_EXECUTABLE - if os.name == 'nt': - exc = GitCommandError - else: - exc = OSError - # end handle windows case + exc = GitCommandNotFound try: # set it to something that doens't exist, assure it raises type(self.git).GIT_PYTHON_GIT_EXECUTABLE = os.path.join( @@ -155,6 +152,10 @@ def test_single_char_git_options_are_passed_to_git(self): def test_change_to_transform_kwargs_does_not_break_command_options(self): self.git.log(n=1) + def test_insert_after_kwarg_raises(self): + # This isn't a complete add command, which doesn't matter here + self.failUnlessRaises(ValueError, self.git.remote, 'add', insert_kwargs_after='foo') + def test_env_vars_passed_to_git(self): editor = 'non_existant_editor' with mock.patch.dict('os.environ', {'GIT_EDITOR': editor}): diff --git a/git/test/test_remote.py b/git/test/test_remote.py index 98d74d8be..c419ecee9 100644 --- a/git/test/test_remote.py +++ b/git/test/test_remote.py @@ -313,8 +313,7 @@ def _assert_push_and_pull(self, remote, rw_repo, remote_repo): self._do_test_push_result(res, remote) # invalid refspec - res = remote.push("hellothere") - assert len(res) == 0 + self.failUnlessRaises(GitCommandError, remote.push, "hellothere") # push new tags progress = TestRemoteProgress() @@ -445,6 +444,24 @@ def test_base(self, rw_repo, remote_repo): origin = rw_repo.remote('origin') assert origin == rw_repo.remotes.origin + # Verify we can handle prunes when fetching + # stderr lines look like this: x [deleted] (none) -> origin/experiment-2012 + # These should just be skipped + # If we don't have a manual checkout, we can't actually assume there are any non-master branches + remote_repo.create_head("myone_for_deletion") + # Get the branch - to be pruned later + origin.fetch() + + num_deleted = False + for branch in remote_repo.heads: + if branch.name != 'master': + branch.delete(remote_repo, branch, force=True) + num_deleted += 1 + # end + # end for each branch + assert num_deleted > 0 + assert len(rw_repo.remotes.origin.fetch(prune=True)) == 1, "deleted everything but master" + @with_rw_repo('HEAD', bare=True) def test_creation_and_removal(self, bare_rw_repo): new_name = "test_new_one" @@ -468,6 +485,9 @@ def test_creation_and_removal(self, bare_rw_repo): # END if deleted remote matches existing remote's name # END for each remote + # Issue #262 - the next call would fail if bug wasn't fixed + bare_rw_repo.create_remote('bogus', '/bogus/path', mirror='push') + def test_fetch_info(self): # assure we can handle remote-tracking branches fetch_info_line_fmt = "c437ee5deb8d00cf02f03720693e4c802e99f390 not-for-merge %s '0.3' of " diff --git a/git/util.py b/git/util.py index 02c54bc3e..1147cb535 100644 --- a/git/util.py +++ b/git/util.py @@ -16,10 +16,7 @@ # NOTE: Some of the unused imports might be used/imported by others. # Handle once test-cases are back up and running. -from .exc import ( - GitCommandError, - InvalidGitRepositoryError -) +from .exc import InvalidGitRepositoryError from .compat import ( MAXSIZE, @@ -154,15 +151,7 @@ def get_user_id(): def finalize_process(proc): """Wait for the process (clone, fetch, pull or push) and handle its errors accordingly""" - try: - proc.wait() - except GitCommandError: - # 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 + proc.wait() #} END utilities diff --git a/tox.ini b/tox.ini index bb1e9a85b..da624b5ee 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,9 @@ commands = {posargs} [flake8] #show-source = True # E265 = comment blocks like @{ section, which it can't handle +# E266 = too many leading '#' for block comment +# E731 = do not assign a lambda expression, use a def # W293 = Blank line contains whitespace -ignore = E265,W293 +ignore = E265,W293,E266,E731 max-line-length = 120 exclude = .tox,.venv,build,dist,doc,git/ext/