diff --git a/git/cmd.py b/git/cmd.py index 08e25af52..ebb52e263 100644 --- a/git/cmd.py +++ b/git/cmd.py @@ -167,7 +167,7 @@ class Git(LazyMixin): Set its value to 'full' to see details about the returned values. """ __slots__ = ("_working_dir", "cat_file_all", "cat_file_header", "_version_info", - "_git_options", "_persistent_git_options", "_environment") + "_git_options", "_persistent_git_options", "_environment", "isolated") _excluded_ = ('cat_file_all', 'cat_file_header', '_version_info') @@ -522,7 +522,7 @@ def __del__(self): self._stream.read(bytes_left + 1) # END handle incomplete read - def __init__(self, working_dir=None): + def __init__(self, working_dir=None, isolated=False): """Initialize this instance with: :param working_dir: @@ -537,6 +537,7 @@ def __init__(self, working_dir=None): # Extra environment variables to pass to git commands self._environment = {} + self.isolated = isolated # cached command slots self.cat_file_header = None @@ -696,7 +697,7 @@ def execute(self, command, # Start the process inline_env = env - env = os.environ.copy() + env = {} if self.isolated else os.environ.copy() # Attempt to force all output to plain ascii english, which is what some parsing code # may expect. # According to stackoverflow (http://goo.gl/l74GC8), we are setting LANGUAGE as well diff --git a/git/index/base.py b/git/index/base.py index b8c9d5e66..41bd61db1 100644 --- a/git/index/base.py +++ b/git/index/base.py @@ -945,10 +945,10 @@ def commit(self, message, parent_commits=None, head=True, author=None, or `--no-verify` on the command line. :return: Commit object representing the new commit""" if not skip_hooks: - run_commit_hook('pre-commit', self) + run_commit_hook('pre-commit', self, isolated=self.repo.isolated) self._write_commit_editmsg(message) - run_commit_hook('commit-msg', self, self._commit_editmsg_filepath()) + run_commit_hook('commit-msg', self, self._commit_editmsg_filepath(), isolated=self.repo.isolated) message = self._read_commit_editmsg() self._remove_commit_editmsg() tree = self.write_tree() @@ -956,7 +956,7 @@ def commit(self, message, parent_commits=None, head=True, author=None, head, author=author, committer=committer, author_date=author_date, commit_date=commit_date) if not skip_hooks: - run_commit_hook('post-commit', self) + run_commit_hook('post-commit', self, isolated=self.repo.isolated) return rval def _write_commit_editmsg(self, message): diff --git a/git/index/fun.py b/git/index/fun.py index 5906a358b..496ef1716 100644 --- a/git/index/fun.py +++ b/git/index/fun.py @@ -62,17 +62,18 @@ def hook_path(name, git_dir): return osp.join(git_dir, 'hooks', name) -def run_commit_hook(name, index, *args): +def run_commit_hook(name, index, *args, isolated=False): """Run the commit hook of the given name. Silently ignores hooks that do not exist. :param name: name of hook, like 'pre-commit' :param index: IndexFile instance :param args: arguments passed to hook file + :param isolated: if true, the parent environment is not passed to the git command. :raises HookExecutionError: """ hp = hook_path(name, index.repo.git_dir) if not os.access(hp, os.X_OK): return - env = os.environ.copy() + env = {} if isolated else os.environ.copy() env['GIT_INDEX_FILE'] = safe_decode(index.path) if PY3 else safe_encode(index.path) env['GIT_EDITOR'] = ':' try: diff --git a/git/objects/commit.py b/git/objects/commit.py index f7201d90e..32fbb20bd 100644 --- a/git/objects/commit.py +++ b/git/objects/commit.py @@ -279,7 +279,7 @@ def _iter_from_process_or_stream(cls, repo, proc_or_stream): @classmethod def create_from_tree(cls, repo, tree, message, parent_commits=None, head=False, author=None, committer=None, - author_date=None, commit_date=None): + author_date=None, commit_date=None, isolated=False): """Commit the given tree, creating a commit object. :param repo: Repo object the commit should be part of @@ -303,6 +303,7 @@ def create_from_tree(cls, repo, tree, message, parent_commits=None, head=False, repository configuration is used to obtain this value. :param author_date: The timestamp for the author field :param commit_date: The timestamp for the committer field + :param isolated: if true, the parent environment is not passed to the git command. :return: Commit object representing the new commit @@ -332,10 +333,10 @@ def create_from_tree(cls, repo, tree, message, parent_commits=None, head=False, # COMMITER AND AUTHOR INFO cr = repo.config_reader() - env = os.environ + env = {} if isolated else os.environ - committer = committer or Actor.committer(cr) - author = author or Actor.author(cr) + committer = committer or Actor.committer(cr, isolated=isolated) + author = author or Actor.author(cr, isolated=isolated) # PARSE THE DATES unix_time = int(time()) diff --git a/git/repo/base.py b/git/repo/base.py index 05c55eddb..8747a7d13 100644 --- a/git/repo/base.py +++ b/git/repo/base.py @@ -72,6 +72,7 @@ class Repo(object): _working_tree_dir = None git_dir = None _common_dir = None + isolated = False # precompiled regex re_whitespace = re.compile(r'\s+') @@ -89,7 +90,8 @@ class Repo(object): # Subclasses may easily bring in their own custom types by placing a constructor or type here GitCommandWrapperType = Git - def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=False, expand_vars=True): + def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=False, expand_vars=True, + isolated=False): """Create a new Repo instance :param path: @@ -113,11 +115,21 @@ def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=Fal Please note that this was the default behaviour in older versions of GitPython, which is considered a bug though. + :param isolated: + If True, the current environment variables will be isolated from git as much as possible. + Specifically the GIT_* variables are ignored as much as possible. + :raise InvalidGitRepositoryError: :raise NoSuchPathError: :return: git.Repo """ - epath = path or os.getenv('GIT_DIR') + self.isolated = isolated + + if isolated: + if path is None: + raise ValueError('When isolated, the repo path must be specified.') + + epath = path or os.environ.get('GIT_DIR') if not epath: epath = os.getcwd() if Git.is_cygwin(): @@ -152,12 +164,12 @@ def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=Fal # If GIT_DIR is specified but none of GIT_WORK_TREE and core.worktree is specified, # the current working directory is regarded as the top level of your working tree. self._working_tree_dir = os.path.dirname(self.git_dir) - if os.environ.get('GIT_COMMON_DIR') is None: + if isolated or os.environ.get('GIT_COMMON_DIR') is None: gitconf = self.config_reader("repository") if gitconf.has_option('core', 'worktree'): self._working_tree_dir = gitconf.get('core', 'worktree') - if 'GIT_WORK_TREE' in os.environ: - self._working_tree_dir = os.getenv('GIT_WORK_TREE') + if not isolated and 'GIT_WORK_TREE' in os.environ: + self._working_tree_dir = os.environ.get('GIT_WORK_TREE') break dotgit = osp.join(curpath, '.git') @@ -204,7 +216,7 @@ def __init__(self, path=None, odbt=GitCmdObjectDB, search_parent_directories=Fal # END working dir handling self.working_dir = self._working_tree_dir or self.common_dir - self.git = self.GitCommandWrapperType(self.working_dir) + self.git = self.GitCommandWrapperType(self.working_dir, isolated=self.isolated) # special handling, in special times args = [osp.join(self.common_dir, 'objects')] @@ -892,7 +904,7 @@ def blame(self, rev, file, incremental=False, **kwargs): return blames @classmethod - def init(cls, path=None, mkdir=True, odbt=GitCmdObjectDB, expand_vars=True, **kwargs): + def init(cls, path=None, mkdir=True, odbt=GitCmdObjectDB, expand_vars=True, isolated=False, **kwargs): """Initialize a git repository at the given path if specified :param path: @@ -915,6 +927,10 @@ def init(cls, path=None, mkdir=True, odbt=GitCmdObjectDB, expand_vars=True, **kw can lead to information disclosure, allowing attackers to access the contents of environment variables + :param isolated: + If True, the current environment variables will be isolated from git as much as possible. + Specifically the GIT_* variables are ignored as much as possible. + :param kwargs: keyword arguments serving as additional options to the git-init command @@ -925,12 +941,12 @@ def init(cls, path=None, mkdir=True, odbt=GitCmdObjectDB, expand_vars=True, **kw os.makedirs(path, 0o755) # git command automatically chdir into the directory - git = Git(path) + git = Git(path, isolated=isolated) git.init(**kwargs) return cls(path, odbt=odbt) @classmethod - def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, **kwargs): + def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, isolated=False, **kwargs): if progress is not None: progress = to_progress_instance(progress) @@ -969,7 +985,7 @@ def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, if not osp.isabs(path) and git.working_dir: path = osp.join(git._working_dir, path) - repo = cls(path, odbt=odbt) + repo = cls(path, odbt=odbt, isolated=isolated) # retain env values that were passed to _clone() repo.git.update_environment(**git.environment()) @@ -985,7 +1001,7 @@ def _clone(cls, git, url, path, odb_default_type, progress, multi_options=None, # END handle remote repo return repo - def clone(self, path, progress=None, multi_options=None, **kwargs): + def clone(self, path, progress=None, multi_options=None, isolated=False, **kwargs): """Create a clone from this repository. :param path: is the full path of the new repo (traditionally ends with ./.git). @@ -1000,10 +1016,10 @@ def clone(self, path, progress=None, multi_options=None, **kwargs): * All remaining keyword arguments are given to the git-clone command :return: ``git.Repo`` (the newly cloned repo)""" - return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options, **kwargs) + return self._clone(self.git, self.common_dir, path, type(self.odb), progress, multi_options, isolated, **kwargs) @classmethod - def clone_from(cls, url, to_path, progress=None, env=None, multi_options=None, **kwargs): + def clone_from(cls, url, to_path, progress=None, env=None, multi_options=None, isolated=False, **kwargs): """Create a clone from the given URL :param url: valid git url, see http://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS @@ -1017,11 +1033,12 @@ def clone_from(cls, url, to_path, progress=None, env=None, multi_options=None, * as its value. :param multi_options: See ``clone`` method :param kwargs: see the ``clone`` method + :param isolated: see the ``clone`` method :return: Repo instance pointing to the cloned directory""" - git = Git(os.getcwd()) + git = Git(os.getcwd(), isolated=isolated) if env is not None: git.update_environment(**env) - return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs) + return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, isolated, **kwargs) def archive(self, ostream, treeish=None, prefix=None, **kwargs): """Archive the tree at the given revision. diff --git a/git/util.py b/git/util.py index 974657e6f..09fe60521 100644 --- a/git/util.py +++ b/git/util.py @@ -583,7 +583,7 @@ def _from_string(cls, string): # END handle name/email matching @classmethod - def _main_actor(cls, env_name, env_email, config_reader=None): + def _main_actor(cls, env_name, env_email, config_reader=None, isolated=False): actor = Actor('', '') default_email = get_user_id() default_name = default_email.split('@')[0] @@ -591,11 +591,14 @@ def _main_actor(cls, env_name, env_email, config_reader=None): for attr, evar, cvar, default in (('name', env_name, cls.conf_name, default_name), ('email', env_email, cls.conf_email, default_email)): try: + if isolated: + raise KeyError("Isolated. No env access.") val = os.environ[evar] if not PY3: val = val.decode(defenc) # end assure we don't get 'invalid strings' setattr(actor, attr, val) + continue except KeyError: if config_reader is not None: setattr(actor, attr, config_reader.get_value('user', cvar, default)) @@ -607,7 +610,7 @@ def _main_actor(cls, env_name, env_email, config_reader=None): return actor @classmethod - def committer(cls, config_reader=None): + def committer(cls, config_reader=None, isolated=False): """ :return: Actor instance corresponding to the configured committer. It behaves similar to the git implementation, such that the environment will override @@ -615,13 +618,13 @@ def committer(cls, config_reader=None): generated :param config_reader: ConfigReader to use to retrieve the values from in case they are not set in the environment""" - return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader) + return cls._main_actor(cls.env_committer_name, cls.env_committer_email, config_reader, isolated) @classmethod - def author(cls, config_reader=None): + def author(cls, config_reader=None, isolated=False): """Same as committer(), but defines the main author. It may be specified in the environment, but defaults to the committer""" - return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader) + return cls._main_actor(cls.env_author_name, cls.env_author_email, config_reader, isolated) class Stats(object):