diff --git a/AUTHORS b/AUTHORS index f0c02e338..0d3bdacfe 100644 --- a/AUTHORS +++ b/AUTHORS @@ -19,6 +19,7 @@ Contributors are: -Timothy B. Hartman -Konstantin Popov -Peter Jones +-Ken Odegard -Alexis Horgix Chotard Portions derived from other open source works and are clearly marked. diff --git a/git/__init__.py b/git/__init__.py index 75247dbf8..74609e79f 100644 --- a/git/__init__.py +++ b/git/__init__.py @@ -60,3 +60,24 @@ def _init_externals(): __all__ = [name for name, obj in locals().items() if not (name.startswith('_') or inspect.ismodule(obj))] + + +#{ Initialize git executable path +GIT_OK = None + +def refresh(path=None): + """Convenience method for setting the git executable path.""" + global GIT_OK + GIT_OK = False + + if not Git.refresh(path=path): + return + if not FetchInfo.refresh(): + return + + GIT_OK = True +#} END initialize git executable path + +################# +refresh() +################# diff --git a/git/cmd.py b/git/cmd.py index a2fcf50d9..312f933a6 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -17,6 +17,7 @@ import subprocess import sys import threading +from textwrap import dedent from git.compat import ( string_types, @@ -182,16 +183,141 @@ def __setstate__(self, d): # 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) - # 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 # and check `git/test_repo.py:TestRepo.test_untracked_files()` TC for an example where it is required. # Override this value using `Git.USE_SHELL = True` USE_SHELL = 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" + _refresh_env_var = "GIT_PYTHON_REFRESH" + GIT_PYTHON_GIT_EXECUTABLE = None + # note that the git executable is actually found during the refresh step in + # the top level __init__ + + @classmethod + def refresh(cls, path=None): + """This gets called by the refresh function (see the top level + __init__). + """ + # discern which path to refresh with + if path is not None: + new_git = os.path.expanduser(path) + new_git = os.path.abspath(new_git) + else: + new_git = os.environ.get(cls._git_exec_env_var, cls.git_exec_name) + + # keep track of the old and new git executable path + old_git = cls.GIT_PYTHON_GIT_EXECUTABLE + cls.GIT_PYTHON_GIT_EXECUTABLE = new_git + + # test if the new git executable path is valid + + if sys.version_info < (3,): + # - a GitCommandNotFound error is spawned by ourselves + # - a OSError is spawned if the git executable provided + # cannot be executed for whatever reason + exceptions = (GitCommandNotFound, OSError) + else: + # - a GitCommandNotFound error is spawned by ourselves + # - a PermissionError is spawned if the git executable provided + # cannot be executed for whatever reason + exceptions = (GitCommandNotFound, PermissionError) + + has_git = False + try: + cls().version() + has_git = True + except exceptions: + pass + + # warn or raise exception if test failed + if not has_git: + err = dedent("""\ + Bad git executable. + The git executable must be specified in one of the following ways: + - be included in your $PATH + - be set via $%s + - explicitly set via git.refresh() + """) % cls._git_exec_env_var + + # revert to whatever the old_git was + cls.GIT_PYTHON_GIT_EXECUTABLE = old_git + + if old_git is None: + # on the first refresh (when GIT_PYTHON_GIT_EXECUTABLE is + # None) we only are quiet, warn, or error depending on the + # GIT_PYTHON_REFRESH value + + # determine what the user wants to happen during the initial + # refresh we expect GIT_PYTHON_REFRESH to either be unset or + # be one of the following values: + # 0|q|quiet|s|silence + # 1|w|warn|warning + # 2|r|raise|e|error + + mode = os.environ.get(cls._refresh_env_var, "raise").lower() + + quiet = ["quiet", "q", "silence", "s", "none", "n", "0"] + warn = ["warn", "w", "warning", "1"] + error = ["error", "e", "raise", "r", "2"] + + if mode in quiet: + pass + elif mode in warn or mode in error: + err = dedent("""\ + %s + All git commands will error until this is rectified. + + This initial warning can be silenced or aggravated in the future by setting the + $%s environment variable. Use one of the following values: + - %s: for no warning or exception + - %s: for a printed warning + - %s: for a raised exception + + Example: + export %s=%s + """) % ( + err, + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error), + cls._refresh_env_var, + quiet[0]) + + if mode in warn: + print("WARNING: %s" % err) + else: + raise ImportError(err) + else: + err = dedent("""\ + %s environment variable has been set but it has been set with an invalid value. + + Use only the following values: + - %s: for no warning or exception + - %s: for a printed warning + - %s: for a raised exception + """) % ( + cls._refresh_env_var, + "|".join(quiet), + "|".join(warn), + "|".join(error)) + raise ImportError(err) + + # we get here if this was the init refresh and the refresh mode + # was not error, go ahead and set the GIT_PYTHON_GIT_EXECUTABLE + # such that we discern the difference between a first import + # and a second import + cls.GIT_PYTHON_GIT_EXECUTABLE = cls.git_exec_name + else: + # after the first refresh (when GIT_PYTHON_GIT_EXECUTABLE + # is no longer None) we raise an exception + raise GitCommandNotFound("git", err) + + return has_git + @classmethod def is_cygwin(cls): return is_cygwin_git(cls.GIT_PYTHON_GIT_EXECUTABLE) @@ -828,13 +954,13 @@ def _call_process(self, method, *args, **kwargs): - "command options" to be converted by :meth:`transform_kwargs()`; - the `'insert_kwargs_after'` key which its value must match one of ``*args``, and any cmd-options will be appended after the matched arg. - + Examples:: - + git.rev_list('master', max_count=10, header=True) - + turns into:: - + git rev-list max-count 10 --header master :return: Same as ``execute``""" diff --git a/git/remote.py b/git/remote.py index 81719f4b3..7261be813 100644 --- a/git/remote.py +++ b/git/remote.py @@ -211,17 +211,38 @@ class FetchInfo(object): _re_fetch_result = re.compile(r'^\s*(.) (\[?[\w\s\.$@]+\]?)\s+(.+) -> ([^\s]+)( \(.*\)?$)?') - _flag_map = {'!': ERROR, - '+': FORCED_UPDATE, - '*': 0, - '=': HEAD_UPTODATE, - ' ': FAST_FORWARD} + _flag_map = { + '!': ERROR, + '+': FORCED_UPDATE, + '*': 0, + '=': HEAD_UPTODATE, + ' ': FAST_FORWARD, + '-': TAG_UPDATE, + } - v = Git().version_info[:2] - if v >= (2, 10): - _flag_map['t'] = TAG_UPDATE - else: - _flag_map['-'] = TAG_UPDATE + @classmethod + def refresh(cls): + """This gets called by the refresh function (see the top level + __init__). + """ + # clear the old values in _flag_map + try: + del cls._flag_map["t"] + except KeyError: + pass + + try: + del cls._flag_map["-"] + except KeyError: + pass + + # set the value given the git version + if Git().version_info[:2] >= (2, 10): + cls._flag_map["t"] = cls.TAG_UPDATE + else: + cls._flag_map["-"] = cls.TAG_UPDATE + + return True def __init__(self, ref, flags, note='', old_commit=None, remote_ref_path=None): """ diff --git a/git/test/test_git.py b/git/test/test_git.py index 3c8b6f828..00577e33a 100644 --- a/git/test/test_git.py +++ b/git/test/test_git.py @@ -10,6 +10,7 @@ from git import ( Git, + refresh, GitCommandError, GitCommandNotFound, Repo, @@ -156,6 +157,14 @@ def test_cmd_override(self): type(self.git).GIT_PYTHON_GIT_EXECUTABLE = prev_cmd # END undo adjustment + def test_refresh(self): + # test a bad git path refresh + self.assertRaises(GitCommandNotFound, refresh, "yada") + + # test a good path refresh + path = os.popen("which git").read().strip() + refresh(path) + def test_options_are_passed_to_git(self): # This work because any command after git --version is ignored git_version = self.git(version=True).NoOp()