Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding setup for git executable #640

Merged
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Contributors are:
-Timothy B. Hartman <tbhartman _at_ gmail.com>
-Konstantin Popov <konstantin.popov.89 _at_ yandex.ru>
-Peter Jones <pjones _at_ redhat.com>
-Ken Odegard <ken.odegard _at_ gmail.com>
-Alexis Horgix Chotard

Portions derived from other open source works and are clearly marked.
21 changes: 21 additions & 0 deletions git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
#################
142 changes: 134 additions & 8 deletions git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import subprocess
import sys
import threading
from textwrap import dedent

from git.compat import (
string_types,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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``"""
Expand Down
41 changes: 31 additions & 10 deletions git/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
9 changes: 9 additions & 0 deletions git/test/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from git import (
Git,
refresh,
GitCommandError,
GitCommandNotFound,
Repo,
Expand Down Expand Up @@ -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()
Expand Down