diff options
-rw-r--r-- | MANIFEST.in | 2 | ||||
-rw-r--r-- | doc/source/tutorial.rst | 9 | ||||
-rw-r--r-- | git/cmd.py | 85 | ||||
-rw-r--r-- | git/remote.py | 2 | ||||
-rwxr-xr-x | git/scripts/ssh_wrapper.sh | 2 | ||||
-rw-r--r-- | git/test/test_docs.py | 9 | ||||
-rw-r--r-- | git/test/test_git.py | 56 | ||||
-rw-r--r-- | git/test/test_remote.py | 4 |
8 files changed, 151 insertions, 18 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index 95b2e883..4c02e39a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,7 @@ include CHANGES include AUTHORS include README include requirements.txt +include scripts/ssh_wrapper.py graft git/test/fixtures graft git/test/performance - diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 4df0013c..7f57ec94 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -329,7 +329,14 @@ You can easily access configuration information for a remote by accessing option .. literalinclude:: ../../git/test/test_docs.py :language: python :start-after: # [26-test_references_and_objects] - :end-before: # ![26-test_references_and_objects] + :end-before: # ![26-test_references_and_objects] + +You can also specify an SSH key to use for any operations on the remotes + +.. literalinclude:: ../../git/test/test_docs.py + :language: python + :start-after: # [32-test_references_and_objects] + :end-before: # ![32-test_references_and_objects] Submodule Handling ****************** @@ -5,6 +5,7 @@ # the BSD License: http://www.opensource.org/licenses/bsd-license.php import os +import os.path import sys import select import logging @@ -12,6 +13,7 @@ import threading import errno import mmap +from contextlib import contextmanager from subprocess import ( call, Popen, @@ -139,7 +141,7 @@ def handle_process_output(process, stdout_handler, stderr_handler, finalizer): if hasattr(select, 'poll'): # poll is preferred, as select is limited to file handles up to 1024 ... . This could otherwise be - # an issue for us, as it matters how many handles or own process has + # an issue for us, as it matters how many handles our own process has poll = select.poll() READ_ONLY = select.POLLIN | select.POLLPRI | select.POLLHUP | select.POLLERR CLOSED = select.POLLHUP | select.POLLERR @@ -223,7 +225,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") + "_git_options", "_environment") # CONFIGURATION # The size in bytes read from stdout when copying git's output to another stream @@ -413,6 +415,9 @@ class Git(LazyMixin): self._working_dir = working_dir self._git_options = () + # Extra environment variables to pass to git commands + self._environment = {} + # cached command slots self.cat_file_header = None self.cat_file_all = None @@ -434,6 +439,10 @@ class Git(LazyMixin): super(Git, self)._set_cache_(attr) # END handle version info + def _sshkey_script_path(self): + this_dir = os.path.dirname(__file__) + return os.path.join(this_dir, 'scripts', 'ssh_wrapper.sh') + @property def working_dir(self): """:return: Git directory we are working on""" @@ -536,6 +545,8 @@ class Git(LazyMixin): # Start the process env = os.environ.copy() env["LC_MESSAGES"] = "C" + env.update(self._environment) + proc = Popen(command, env=env, cwd=cwd, @@ -608,6 +619,74 @@ class Git(LazyMixin): else: return stdout_value + def environment(self): + return self._environment + + def update_environment(self, **kwargs): + """ + Set environment variables for future git invocations. Return all changed + values in a format that can be passed back into this function to revert + the changes: + + ``Examples``:: + + old_env = self.update_environment(PWD='/tmp') + self.update_environment(**old_env) + + :param kwargs: environment variables to use for git processes + :return: dict that maps environment variables to their old values + """ + old_env = {} + for key, value in kwargs.items(): + # set value if it is None + if value is not None: + if key in self._environment: + old_env[key] = self._environment[key] + else: + old_env[key] = None + self._environment[key] = value + # remove key from environment if its value is None + elif key in self._environment: + old_env[key] = self._environment[key] + del self._environment[key] + return old_env + + @contextmanager + def custom_environment(self, **kwargs): + """ + A context manager around the above ``update_environment`` method to restore the + environment back to its previous state after operation. + + ``Examples``:: + + with self.custom_environment(GIT_SSH='/bin/ssh_wrapper'): + repo.remotes.origin.fetch() + + :param kwargs: see update_environment + """ + old_env = self.update_environment(**kwargs) + try: + yield + finally: + self.update_environment(**old_env) + + @contextmanager + def sshkey(self, sshkey_file_path): + """ + A context manager to temporarily set an SSH key for all operations that + run inside it. + + ``Examples``:: + + with self.sshkey('deployment_key'): + repo.remotes.origin.fetch() + + :param sshkey_file_path: Path to a private SSH key file + """ + ssh_wrapper = self._sshkey_script_path() + with self.custom_environment(GIT_SSH_KEY_FILE=sshkey_file_path, GIT_SSH=ssh_wrapper): + yield + def transform_kwargs(self, split_single_char_options=False, **kwargs): """Transforms Python style kwargs into git command line options.""" args = list() @@ -731,7 +810,7 @@ class Git(LazyMixin): import warnings msg = "WARNING: Automatically switched to use git.cmd as git executable" msg += ", which reduces performance by ~70%." - msg += "Its recommended to put git.exe into the PATH or to " + msg += "It is recommended to put git.exe into the PATH or to " msg += "set the %s " % self._git_exec_env_var msg += "environment variable to the executable's location" warnings.warn(msg) diff --git a/git/remote.py b/git/remote.py index 39d9dc4d..d048f87b 100644 --- a/git/remote.py +++ b/git/remote.py @@ -362,7 +362,7 @@ class Remote(LazyMixin, Iterable): # that it has the config_writer property, but instead calls __getattr__ # which will not yield the expected results. 'pinging' the members # with a dir call creates the config_writer property that we require - # ... bugs like these make me wonder wheter python really wants to be used + # ... bugs like these make me wonder whether python really wants to be used # for production. It doesn't happen on linux though. dir(self) # END windows special handling diff --git a/git/scripts/ssh_wrapper.sh b/git/scripts/ssh_wrapper.sh new file mode 100755 index 00000000..bc0ab024 --- /dev/null +++ b/git/scripts/ssh_wrapper.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +ssh -i "$GIT_SSH_KEY_FILE" $@ diff --git a/git/test/test_docs.py b/git/test/test_docs.py index 586f0ce4..965d10fb 100644 --- a/git/test/test_docs.py +++ b/git/test/test_docs.py @@ -437,6 +437,15 @@ class Tutorials(TestBase): git.for_each_ref() # '-' becomes '_' when calling it # ![31-test_references_and_objects] + # [32-test_references_and_objects] + private_key_file = os.path.join(rw_dir, 'id_rsa_deployment_key') + with repo.git.sshkey(private_key_file): + # 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 f25fa21a..990f4cd0 100644 --- a/git/test/test_git.py +++ b/git/test/test_git.py @@ -4,18 +4,24 @@ # # This module is part of GitPython and is released under # the BSD License: http://www.opensource.org/licenses/bsd-license.php - import os import mock -from git.test.lib import (TestBase, - patch, - raises, - assert_equal, - assert_true, - assert_match, - fixture_path) -from git import (Git, - GitCommandError) + +from git.test.lib import ( + TestBase, + patch, + raises, + assert_equal, + assert_true, + assert_match, + fixture_path +) +from git import ( + Git, + GitCommandError, + Repo +) +from gitdb.test.lib import with_rw_directory from git.compat import PY3 @@ -153,3 +159,33 @@ class TestGit(TestBase): editor = 'non_existant_editor' with mock.patch.dict('os.environ', {'GIT_EDITOR': editor}): assert self.git.var("GIT_EDITOR") == editor + + @with_rw_directory + def test_environment(self, rw_dir): + # sanity check + assert self.git.environment() == {} + + # make sure the context manager works and cleans up after itself + with self.git.custom_environment(PWD='/tmp'): + assert self.git.environment() == {'PWD': '/tmp'} + + assert self.git.environment() == {} + + old_env = self.git.update_environment(VARKEY='VARVALUE') + # The returned dict can be used to revert the change, hence why it has + # an entry with value 'None'. + assert old_env == {'VARKEY': None} + assert self.git.environment() == {'VARKEY': 'VARVALUE'} + + new_env = self.git.update_environment(**old_env) + assert new_env == {'VARKEY': 'VARVALUE'} + assert self.git.environment() == {} + + rw_repo = Repo.init(os.path.join(rw_dir, 'repo')) + remote = rw_repo.create_remote('ssh-origin', "ssh://git@server/foo") + + with rw_repo.git.sshkey('doesntexist.key'): + remote.fetch() + # end + + diff --git a/git/test/test_remote.py b/git/test/test_remote.py index 4fd78230..98d74d8b 100644 --- a/git/test/test_remote.py +++ b/git/test/test_remote.py @@ -164,11 +164,11 @@ class TestRemote(TestBase): def get_info(res, remote, name): return res["%s/%s" % (remote, name)] - # put remote head to master as it is garantueed to exist + # put remote head to master as it is guaranteed to exist remote_repo.head.reference = remote_repo.heads.master res = fetch_and_test(remote) - # all uptodate + # all up to date for info in res: assert info.flags & info.HEAD_UPTODATE |