From 87953b6f1536a9079294e3fa079f0678ff1977cf Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 31 May 2026 07:44:32 -0500 Subject: [PATCH 1/4] sync/git(fix[GitSync]): Honor git_shallow and tls_verify constructor args why: Passing git_shallow=True or tls_verify=True never set the attribute, so the next obtain() raised AttributeError. The implicit **kwargs-to-__dict__ assignment that once populated them was removed in v0.4.4, leaving the `if "x" not in kwargs` blocks to set only the default-False case. what: - Accept git_shallow and tls_verify as explicit keyword-only params on GitSync.__init__ and assign them directly - Type create_project's **kwargs as t.Any (was dict[Any, Any], which mis-typed every forwarded keyword) so the now-typed params type-check - Document git_shallow in the GitSync docstring - Add a regression test: attributes are set, and git_shallow drives a shallow (depth-1) clone - CHANGES: Fixes entry --- CHANGES | 6 +++++ src/libvcs/_internal/shortcuts.py | 10 ++++----- src/libvcs/sync/git.py | 12 ++++++---- tests/sync/test_git.py | 37 +++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index 677cc75f1..8a35df495 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,12 @@ $ uv add libvcs --prerelease allow _Notes on the upcoming release will go here._ +### Fixes + +#### GitSync honors `git_shallow` and `tls_verify` constructor arguments (#531) + +Passing `git_shallow=True` or `tls_verify=True` to {class}`~libvcs.sync.git.GitSync` left the matching attribute unset, so the next {meth}`~libvcs.sync.git.GitSync.obtain` raised `AttributeError`. Both are now accepted as keyword-only constructor arguments and applied when cloning. + ## libvcs 0.41.0 (2026-05-10) libvcs 0.41.0 is a pytest-plugin compatibility release. It renames libvcs's Git and Mercurial config fixtures so the plugin no longer occupies fixture names used by third-party pytest plugins, and it keeps the docs stack aligned with the current gp-sphinx theme pipeline. diff --git a/src/libvcs/_internal/shortcuts.py b/src/libvcs/_internal/shortcuts.py index a3e401d33..9cd8a94e1 100644 --- a/src/libvcs/_internal/shortcuts.py +++ b/src/libvcs/_internal/shortcuts.py @@ -38,7 +38,7 @@ def create_project( path: StrPath, vcs: t.Literal["git"], progress_callback: ProgressCallbackProtocol | None = None, - **kwargs: dict[t.Any, t.Any], + **kwargs: t.Any, ) -> GitSync: ... @@ -49,7 +49,7 @@ def create_project( path: StrPath, vcs: t.Literal["svn"], progress_callback: ProgressCallbackProtocol | None = None, - **kwargs: dict[t.Any, t.Any], + **kwargs: t.Any, ) -> SvnSync: ... @@ -60,7 +60,7 @@ def create_project( path: StrPath, vcs: t.Literal["hg"], progress_callback: ProgressCallbackProtocol | None = ..., - **kwargs: dict[t.Any, t.Any], + **kwargs: t.Any, ) -> HgSync: ... @@ -71,7 +71,7 @@ def create_project( path: StrPath, vcs: None = None, progress_callback: ProgressCallbackProtocol | None = None, - **kwargs: dict[t.Any, t.Any], + **kwargs: t.Any, ) -> GitSync | HgSync | SvnSync: ... @@ -81,7 +81,7 @@ def create_project( path: StrPath, vcs: VCSLiteral | None = None, progress_callback: ProgressCallbackProtocol | None = None, - **kwargs: dict[t.Any, t.Any], + **kwargs: t.Any, ) -> GitSync | HgSync | SvnSync: r"""Return an object representation of a VCS repository. diff --git a/src/libvcs/sync/git.py b/src/libvcs/sync/git.py index 42d3f98ff..8ee94a555 100644 --- a/src/libvcs/sync/git.py +++ b/src/libvcs/sync/git.py @@ -211,6 +211,8 @@ def __init__( url: str, path: StrPath, remotes: GitRemotesArgs = None, + git_shallow: bool = False, + tls_verify: bool = False, **kwargs: t.Any, ) -> None: """Local git repository. @@ -220,6 +222,10 @@ def __init__( url : str URL of repo + git_shallow : bool + Clone with history truncated to the latest commit (``--depth 1``, + default False) + tls_verify : bool Should certificate for https be checked (default False) @@ -258,10 +264,8 @@ def __init__( } ) """ - if "git_shallow" not in kwargs: - self.git_shallow = False - if "tls_verify" not in kwargs: - self.tls_verify = False + self.git_shallow = git_shallow + self.tls_verify = tls_verify self._remotes: GitSyncRemoteDict diff --git a/tests/sync/test_git.py b/tests/sync/test_git.py index ecb253ce0..3a2092b56 100644 --- a/tests/sync/test_git.py +++ b/tests/sync/test_git.py @@ -120,6 +120,43 @@ def test_repo_git_obtain_full( assert (tmp_path / "myrepo").exists() +def test_git_shallow_and_tls_verify_kwargs_are_honored( + tmp_path: pathlib.Path, + git_remote_repo: pathlib.Path, +) -> None: + """``git_shallow`` and ``tls_verify`` populate their attributes. + + Regression: each kwarg previously left its attribute unset, so the next + ``obtain()`` raised ``AttributeError``. + """ + # tls_verify reaches the attribute. Its clone-time ``config`` wiring is + # broken independently of this fix and tracked separately, so we don't + # drive a clone with it here. + assert ( + GitSync( + url=git_remote_repo.as_uri(), + path=tmp_path / "tls", + tls_verify=True, + ).tls_verify + is True + ) + + # git_shallow drives a depth-1 (shallow) clone in obtain(). + git_repo = GitSync( + url=git_remote_repo.as_uri(), + path=tmp_path / "myrepo", + git_shallow=True, + ) + assert git_repo.git_shallow is True + git_repo.obtain() + + is_shallow = run( + ["git", "rev-parse", "--is-shallow-repository"], + cwd=tmp_path / "myrepo", + ) + assert is_shallow == "true" + + @pytest.mark.parametrize( # Postpone evaluation of options so fixture variables can interpolate ("constructor", "lazy_constructor_options"), From a43112512db5f0ae139f0d935bca7cc850688343 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 31 May 2026 08:33:25 -0500 Subject: [PATCH 2/4] sync/git(feat[GitSync]): Support for clone depth why: GitSync could express shallow-vs-full but not a numeric clone depth. The lower-level Git.clone() already accepted any `--depth N`, but obtain() hardcoded `depth=1 if self.git_shallow else None`, so downstream tools (e.g. vcspull) could only persist a boolean shallow flag rather than an arbitrary depth. Implements proposal points 1-2 of issue #531. what: - Add a `depth: int | None = None` keyword-only parameter to GitSync.__init__, stored as `self.depth`, alongside the existing git_shallow/tls_verify controls. - Resolve the clone depth in obtain() through an explicit precedence chain instead of the hardcoded ternary: * an explicit `depth` wins -> `git clone --depth `, * otherwise `git_shallow=True` keeps the prior depth-1 behavior, * otherwise the clone is full (`depth=None`). An unset `depth` reproduces the previous behavior exactly, so existing callers and the git_shallow path are unaffected. - Document `depth` in the GitSync docstring (takes precedence over git_shallow; default None means a full clone). - Record the deferred update-time deepen/unshallow (proposal point 3) as a `.. todo::` in update_repo(), pointing at follow-up issue #532 and naming the two git edges a future implementation must handle: `git fetch --depth N` truncates a full checkout to shallow, and `git fetch --unshallow` against a complete repo is a fatal error (guard with `git rev-parse --is-shallow-repository`). Refs #531 --- src/libvcs/sync/git.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/libvcs/sync/git.py b/src/libvcs/sync/git.py index 8ee94a555..416ec8f7f 100644 --- a/src/libvcs/sync/git.py +++ b/src/libvcs/sync/git.py @@ -213,6 +213,7 @@ def __init__( remotes: GitRemotesArgs = None, git_shallow: bool = False, tls_verify: bool = False, + depth: int | None = None, **kwargs: t.Any, ) -> None: """Local git repository. @@ -226,6 +227,11 @@ def __init__( Clone with history truncated to the latest commit (``--depth 1``, default False) + depth : int, optional + Clone with history truncated to ``depth`` commits + (``git clone --depth N``). Takes precedence over ``git_shallow``. + Default None (full clone). + tls_verify : bool Should certificate for https be checked (default False) @@ -266,6 +272,7 @@ def __init__( """ self.git_shallow = git_shallow self.tls_verify = tls_verify + self.depth = depth self._remotes: GitSyncRemoteDict @@ -368,10 +375,19 @@ def obtain(self, *args: t.Any, **kwargs: t.Any) -> None: url = self.url self.log.info("Cloning.") + # An explicit depth wins; otherwise git_shallow keeps the depth-1 + # behavior, and neither means a full clone. + clone_depth: int | None + if self.depth is not None: + clone_depth = self.depth + elif self.git_shallow: + clone_depth = 1 + else: + clone_depth = None self.cmd.clone( url=url, progress=True, - depth=1 if self.git_shallow else None, + depth=clone_depth, config={"http.sslVerify": False} if self.tls_verify else None, log_in_real_time=True, ) @@ -396,6 +412,15 @@ def update_repo( ) -> SyncResult: """Pull latest changes from git remote. + .. todo:: + + Honor ``depth`` on update by deepening or unshallowing the existing + checkout when the requested depth differs from what is on disk. + Tracked in https://github.com/vcs-python/libvcs/issues/532. Edges to + handle: ``git fetch --depth N`` against a full checkout truncates it + to shallow, and ``git fetch --unshallow`` against a complete repo is + a fatal error (guard with ``git rev-parse --is-shallow-repository``). + Parameters ---------- set_remotes : bool From efc4a149ef3795143ab0db18e2b9a546054fe072 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 31 May 2026 08:33:46 -0500 Subject: [PATCH 3/4] tests(sync/git[GitSync]): Cover obtain() clone depth why: Lock in the depth-selection behavior of GitSync.obtain(): an explicit `depth` must win over `git_shallow`, `git_shallow` must still clone at depth 1, and the default must remain a full clone. Without coverage the precedence chain and the resulting on-disk shallow state could regress silently. what: - Add `test_obtain_honors_clone_depth`, parametrized with a `typing.NamedTuple` (DepthFixture) plus a `test_id` for ids, over four cases: * full-clone (no kwargs) -> 6 commits, not shallow, * git_shallow=True -> 1 commit, shallow, * depth=3 -> 3 commits, shallow, * depth=2 + git_shallow=True -> 2 commits, shallow (depth overrides git_shallow). - Build a 6-commit remote with create_git_remote_repo() and git_commit_envvars, run obtain(), then assert `git rev-list --count HEAD` and `git rev-parse --is-shallow-repository` against each case's expectations. - Clone over the remote's file:// URI (remote_repo.as_uri()): git ignores `--depth` for local-path clones, so the file transport is required for the depth assertions to be meaningful. This invariant is documented in the test docstring. - Add the `os` import (for the commit env) and the GitCommitEnvVars type import the new test relies on. Refs #531 --- tests/sync/test_git.py | 76 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/sync/test_git.py b/tests/sync/test_git.py index 3a2092b56..7eb965029 100644 --- a/tests/sync/test_git.py +++ b/tests/sync/test_git.py @@ -3,6 +3,7 @@ from __future__ import annotations import datetime +import os import pathlib import random import shutil @@ -28,7 +29,7 @@ if t.TYPE_CHECKING: from pytest_mock import MockerFixture - from libvcs.pytest_plugin import CreateRepoFn + from libvcs.pytest_plugin import CreateRepoFn, GitCommitEnvVars if not shutil.which("git"): pytestmark = pytest.mark.skip(reason="git is not available") @@ -157,6 +158,79 @@ def test_git_shallow_and_tls_verify_kwargs_are_honored( assert is_shallow == "true" +class DepthFixture(t.NamedTuple): + """Parameters for :func:`test_obtain_honors_clone_depth`.""" + + test_id: str + sync_kwargs: dict[str, t.Any] + expected_count: int + expected_shallow: bool + + +DEPTH_FIXTURES: list[DepthFixture] = [ + DepthFixture( + test_id="full-clone", + sync_kwargs={}, + expected_count=6, + expected_shallow=False, + ), + DepthFixture( + test_id="git_shallow-depth-1", + sync_kwargs={"git_shallow": True}, + expected_count=1, + expected_shallow=True, + ), + DepthFixture( + test_id="depth-3", + sync_kwargs={"depth": 3}, + expected_count=3, + expected_shallow=True, + ), + DepthFixture( + test_id="depth-overrides-git_shallow", + sync_kwargs={"git_shallow": True, "depth": 2}, + expected_count=2, + expected_shallow=True, + ), +] + + +@pytest.mark.parametrize( + list(DepthFixture._fields), + DEPTH_FIXTURES, + ids=[test.test_id for test in DEPTH_FIXTURES], +) +def test_obtain_honors_clone_depth( + tmp_path: pathlib.Path, + create_git_remote_repo: CreateRepoFn, + git_commit_envvars: GitCommitEnvVars, + test_id: str, + sync_kwargs: dict[str, t.Any], + expected_count: int, + expected_shallow: bool, +) -> None: + """obtain() clones at the requested depth; an explicit depth wins. + + The ``file://`` URL matters: git ignores ``--depth`` for local-path clones. + """ + remote_repo = create_git_remote_repo() + env = os.environ.copy() + env.update(git_commit_envvars) + for i in range(1, 7): + (remote_repo / "f.txt").write_text(str(i)) + run(["git", "add", "f.txt"], cwd=remote_repo, env=env) + run(["git", "commit", "-m", f"c{i}"], cwd=remote_repo, env=env) + + checkout = tmp_path / "checkout" + git_repo = GitSync(url=remote_repo.as_uri(), path=checkout, **sync_kwargs) + git_repo.obtain() + + commit_count = run(["git", "rev-list", "--count", "HEAD"], cwd=checkout) + is_shallow = run(["git", "rev-parse", "--is-shallow-repository"], cwd=checkout) + assert int(commit_count) == expected_count + assert is_shallow == ("true" if expected_shallow else "false") + + @pytest.mark.parametrize( # Postpone evaluation of options so fixture variables can interpolate ("constructor", "lazy_constructor_options"), From 48ec1a533183865f60db539cd9c7b2887156dc7f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sun, 31 May 2026 08:34:05 -0500 Subject: [PATCH 4/4] docs(CHANGES) note GitSync arbitrary clone depth why: Surface the new GitSync `depth` capability in the unreleased 0.42.x changelog so users discover they can request `--depth N` and persist a numeric depth, not just a boolean shallow flag. what: - Add a `### What's new` deliverable under the 0.42.x (unreleased) block: "#### GitSync honors an arbitrary clone depth (#531)". - Describe the new `depth` keyword argument, that obtain() forwards it to `git clone --depth N`, that an unset `depth` preserves the prior behavior (git_shallow -> depth 1, else full), and the downstream benefit of persisting/applying a numeric depth. - Link the affected API with MyST roles ({class} GitSync, {meth} obtain) per the changelog conventions. Refs #531 --- CHANGES | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES b/CHANGES index 8a35df495..fbf7a191c 100644 --- a/CHANGES +++ b/CHANGES @@ -20,6 +20,12 @@ $ uv add libvcs --prerelease allow _Notes on the upcoming release will go here._ +### What's new + +#### GitSync honors an arbitrary clone depth (#531) + +{class}`~libvcs.sync.git.GitSync` accepts a `depth` keyword argument that {meth}`~libvcs.sync.git.GitSync.obtain` forwards to `git clone --depth N`. When `depth` is unset the prior behavior is preserved: `git_shallow=True` clones at depth 1, and otherwise the clone is full. Downstream tools can now persist and apply a numeric shallow depth instead of only a boolean shallow flag. + ### Fixes #### GitSync honors `git_shallow` and `tls_verify` constructor arguments (#531)