diff --git a/docs/repository.rst b/docs/repository.rst index 546c75a3..0e22178d 100644 --- a/docs/repository.rst +++ b/docs/repository.rst @@ -29,6 +29,7 @@ Functions >>> repo_path = '/path/to/create/repository' >>> repo = clone_repository(repo_url, repo_path) # Clones a non-bare repository >>> repo = clone_repository(repo_url, repo_path, bare=True) # Clones a bare repository + >>> repo = clone_repository(repo_url, repo_path, proxy=True) # Enable automatic proxy detection .. autofunction:: pygit2.discover_repository diff --git a/pygit2/__init__.py b/pygit2/__init__.py index c431f7d4..01119bd7 100644 --- a/pygit2/__init__.py +++ b/pygit2/__init__.py @@ -32,6 +32,7 @@ # Low level API from ._pygit2 import * +from ._pygit2 import _cache_enums # High level API from . import enums @@ -39,7 +40,12 @@ from .blame import Blame, BlameHunk from .blob import BlobIO from .callbacks import Payload, RemoteCallbacks, CheckoutCallbacks, StashApplyCallbacks -from .callbacks import git_clone_options, git_fetch_options, get_credentials +from .callbacks import ( + git_clone_options, + git_fetch_options, + git_proxy_options, + get_credentials, +) from .config import Config from .credentials import * from .errors import check_error, Passthrough @@ -145,14 +151,15 @@ def init_repository( def clone_repository( - url, - path, - bare=False, - repository=None, - remote=None, - checkout_branch=None, - callbacks=None, - depth=0, + url: str, + path: str, + bare: bool = False, + repository: typing.Callable | None = None, + remote: typing.Callable | None = None, + checkout_branch: str | None = None, + callbacks: RemoteCallbacks | None = None, + depth: int = 0, + proxy: None | bool | str = None, ): """ Clones a new Git repository from *url* in the given *path*. @@ -193,6 +200,12 @@ def clone_repository( If greater than 0, creates a shallow clone with a history truncated to the specified number of commits. The default is 0 (full commit history). + proxy : None or True or str + Proxy configuration. Can be one of: + + * `None` (the default) to disable proxy usage + * `True` to enable automatic proxy detection + * an url to a proxy (`http://proxy.example.org:3128/`) """ if callbacks is None: @@ -213,9 +226,10 @@ def clone_repository( opts.checkout_branch = checkout_branch_ref with git_fetch_options(payload, opts=opts.fetch_opts): - crepo = ffi.new('git_repository **') - err = C.git_clone(crepo, to_bytes(url), to_bytes(path), opts) - payload.check_error(err) + with git_proxy_options(payload, opts.fetch_opts.proxy_opts, proxy): + crepo = ffi.new('git_repository **') + err = C.git_clone(crepo, to_bytes(url), to_bytes(path), opts) + payload.check_error(err) # Ok return Repository._from_c(crepo[0], owned=True) diff --git a/pygit2/callbacks.py b/pygit2/callbacks.py index 57e3d773..cd7d1c50 100644 --- a/pygit2/callbacks.py +++ b/pygit2/callbacks.py @@ -370,6 +370,29 @@ def git_fetch_options(payload, opts=None): yield payload +@contextmanager +def git_proxy_options( + payload: object, + opts: object | None = None, + proxy: None | bool | str = None, +): + if opts is None: + opts = ffi.new('git_proxy_options *') + C.git_proxy_options_init(opts, C.GIT_PROXY_OPTIONS_VERSION) + if proxy is None: + opts.type = C.GIT_PROXY_NONE + elif proxy is True: + opts.type = C.GIT_PROXY_AUTO + elif type(proxy) is str: + opts.type = C.GIT_PROXY_SPECIFIED + # Keep url in memory, otherwise memory is freed and bad things happen + payload.__proxy_url = ffi.new('char[]', to_bytes(proxy)) + opts.url = payload.__proxy_url + else: + raise TypeError('Proxy must be None, True, or a string') + yield opts + + @contextmanager def git_push_options(payload, opts=None): if payload is None: diff --git a/pygit2/remotes.py b/pygit2/remotes.py index 2322e7f7..1451dbf6 100644 --- a/pygit2/remotes.py +++ b/pygit2/remotes.py @@ -28,7 +28,12 @@ # Import from pygit2 from ._pygit2 import Oid -from .callbacks import git_fetch_options, git_push_options, git_remote_callbacks +from .callbacks import ( + git_fetch_options, + git_push_options, + git_proxy_options, + git_remote_callbacks, +) from .enums import FetchPrune from .errors import check_error from .ffi import ffi, C @@ -107,14 +112,16 @@ def connect(self, callbacks=None, direction=C.GIT_DIRECTION_FETCH, proxy=None): * `True` to enable automatic proxy detection * an url to a proxy (`http://proxy.example.org:3128/`) """ - proxy_opts = ffi.new('git_proxy_options *') - C.git_proxy_options_init(proxy_opts, C.GIT_PROXY_OPTIONS_VERSION) - self.__set_proxy(proxy_opts, proxy) - with git_remote_callbacks(callbacks) as payload: - err = C.git_remote_connect( - self._remote, direction, payload.remote_callbacks, proxy_opts, ffi.NULL - ) - payload.check_error(err) + with git_proxy_options(self, proxy=proxy) as proxy_opts: + with git_remote_callbacks(callbacks) as payload: + err = C.git_remote_connect( + self._remote, + direction, + payload.remote_callbacks, + proxy_opts, + ffi.NULL, + ) + payload.check_error(err) def fetch( self, @@ -154,10 +161,12 @@ def fetch( opts = payload.fetch_options opts.prune = prune opts.depth = depth - self.__set_proxy(opts.proxy_opts, proxy) - with StrArray(refspecs) as arr: - err = C.git_remote_fetch(self._remote, arr.ptr, opts, to_bytes(message)) - payload.check_error(err) + with git_proxy_options(self, payload.fetch_options.proxy_opts, proxy): + with StrArray(refspecs) as arr: + err = C.git_remote_fetch( + self._remote, arr.ptr, opts, to_bytes(message) + ) + payload.check_error(err) return TransferProgress(C.git_remote_stats(self._remote)) @@ -276,24 +285,11 @@ def push(self, specs, callbacks=None, proxy=None, push_options=None, threads=1): with git_push_options(callbacks) as payload: opts = payload.push_options opts.pb_parallelism = threads - self.__set_proxy(opts.proxy_opts, proxy) - with StrArray(specs) as refspecs, StrArray(push_options) as pushopts: - pushopts.assign_to(opts.remote_push_options) - err = C.git_remote_push(self._remote, refspecs.ptr, opts) - payload.check_error(err) - - def __set_proxy(self, proxy_opts, proxy): - if proxy is None: - proxy_opts.type = C.GIT_PROXY_NONE - elif proxy is True: - proxy_opts.type = C.GIT_PROXY_AUTO - elif type(proxy) is str: - proxy_opts.type = C.GIT_PROXY_SPECIFIED - # Keep url in memory, otherwise memory is freed and bad things happen - self.__url = ffi.new('char[]', to_bytes(proxy)) - proxy_opts.url = self.__url - else: - raise TypeError('Proxy must be None, True, or a string') + with git_proxy_options(self, payload.push_options.proxy_opts, proxy): + with StrArray(specs) as refspecs, StrArray(push_options) as pushopts: + pushopts.assign_to(opts.remote_push_options) + err = C.git_remote_push(self._remote, refspecs.ptr, opts) + payload.check_error(err) class RemoteCollection: diff --git a/test/test_repository.py b/test/test_repository.py index adf339e4..d8c8efa1 100644 --- a/test/test_repository.py +++ b/test/test_repository.py @@ -736,6 +736,18 @@ def test_clone_with_checkout_branch(barerepo, tmp_path): assert repo.lookup_reference('HEAD').target == 'refs/heads/test' +@utils.requires_proxy +@utils.requires_network +def test_clone_with_proxy(tmp_path): + url = 'https://github.com/libgit2/TestGitRepository' + repo = clone_repository( + url, + tmp_path / 'testrepo-orig.git', + proxy=True, + ) + assert not repo.is_empty + + # FIXME The tests below are commented because they are broken: # # - test_clone_push_url: Passes, but does nothing useful.