Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ $ uv add libvcs --prerelease allow
_Notes on the upcoming release will go here._
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### 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)

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.
Expand Down
10 changes: 5 additions & 5 deletions src/libvcs/_internal/shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...


Expand All @@ -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: ...


Expand All @@ -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: ...


Expand All @@ -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: ...


Expand All @@ -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.

Expand Down
39 changes: 34 additions & 5 deletions src/libvcs/sync/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ def __init__(
url: str,
path: StrPath,
remotes: GitRemotesArgs = None,
git_shallow: bool = False,
tls_verify: bool = False,
depth: int | None = None,
**kwargs: t.Any,
) -> None:
"""Local git repository.
Expand All @@ -220,6 +223,15 @@ def __init__(
url : str
URL of repo

git_shallow : bool
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)

Expand Down Expand Up @@ -258,10 +270,9 @@ 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.depth = depth

self._remotes: GitSyncRemoteDict

Expand Down Expand Up @@ -364,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,
)
Expand All @@ -392,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
Expand Down
113 changes: 112 additions & 1 deletion tests/sync/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import datetime
import os
import pathlib
import random
import shutil
Expand All @@ -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")
Expand Down Expand Up @@ -120,6 +121,116 @@ 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"


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"),
Expand Down