Skip to content

Commit 546b831

Browse files
authored
sync/git(feat[GitSync]): Support arbitrary clone depth (#535)
GitSync could express shallow-vs-full but not a numeric clone depth, so downstream tools could only persist a boolean shallow flag. This adds a `depth` keyword argument that obtain() forwards to `git clone --depth N`, and fixes a latent constructor bug uncovered along the way. - **Add `depth`:** GitSync accepts an explicit clone depth. obtain() resolves it by precedence — an explicit `depth` wins, otherwise `git_shallow=True` clones at depth 1, otherwise a full clone. An unset `depth` preserves existing behavior, so current callers are unaffected. - **Fix `git_shallow`/`tls_verify`:** passing either to the GitSync constructor previously left the attribute unset, so the next obtain() raised AttributeError. Both are now explicit keyword-only arguments. (`tls_verify`'s clone-time `http.sslVerify` wiring has a separate pre-existing defect, tracked in #533.) This affected published releases. - **Type `create_project` kwargs:** the forwarded `**kwargs` was annotated `dict[Any, Any]`, mistyping every keyword; corrected to `Any`. The optional update-time deepen/unshallow (proposal point 3) is deferred to a follow-up, recorded as a todo in update_repo(). Addresses #531
2 parents 57b2971 + 48ec1a5 commit 546b831

4 files changed

Lines changed: 163 additions & 11 deletions

File tree

CHANGES

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ $ uv add libvcs --prerelease allow
2020
_Notes on the upcoming release will go here._
2121
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
2222

23+
### What's new
24+
25+
#### GitSync honors an arbitrary clone depth (#531)
26+
27+
{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.
28+
29+
### Fixes
30+
31+
#### GitSync honors `git_shallow` and `tls_verify` constructor arguments (#531)
32+
33+
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.
34+
2335
## libvcs 0.41.0 (2026-05-10)
2436

2537
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.

src/libvcs/_internal/shortcuts.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def create_project(
3838
path: StrPath,
3939
vcs: t.Literal["git"],
4040
progress_callback: ProgressCallbackProtocol | None = None,
41-
**kwargs: dict[t.Any, t.Any],
41+
**kwargs: t.Any,
4242
) -> GitSync: ...
4343

4444

@@ -49,7 +49,7 @@ def create_project(
4949
path: StrPath,
5050
vcs: t.Literal["svn"],
5151
progress_callback: ProgressCallbackProtocol | None = None,
52-
**kwargs: dict[t.Any, t.Any],
52+
**kwargs: t.Any,
5353
) -> SvnSync: ...
5454

5555

@@ -60,7 +60,7 @@ def create_project(
6060
path: StrPath,
6161
vcs: t.Literal["hg"],
6262
progress_callback: ProgressCallbackProtocol | None = ...,
63-
**kwargs: dict[t.Any, t.Any],
63+
**kwargs: t.Any,
6464
) -> HgSync: ...
6565

6666

@@ -71,7 +71,7 @@ def create_project(
7171
path: StrPath,
7272
vcs: None = None,
7373
progress_callback: ProgressCallbackProtocol | None = None,
74-
**kwargs: dict[t.Any, t.Any],
74+
**kwargs: t.Any,
7575
) -> GitSync | HgSync | SvnSync: ...
7676

7777

@@ -81,7 +81,7 @@ def create_project(
8181
path: StrPath,
8282
vcs: VCSLiteral | None = None,
8383
progress_callback: ProgressCallbackProtocol | None = None,
84-
**kwargs: dict[t.Any, t.Any],
84+
**kwargs: t.Any,
8585
) -> GitSync | HgSync | SvnSync:
8686
r"""Return an object representation of a VCS repository.
8787

src/libvcs/sync/git.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ def __init__(
211211
url: str,
212212
path: StrPath,
213213
remotes: GitRemotesArgs = None,
214+
git_shallow: bool = False,
215+
tls_verify: bool = False,
216+
depth: int | None = None,
214217
**kwargs: t.Any,
215218
) -> None:
216219
"""Local git repository.
@@ -220,6 +223,15 @@ def __init__(
220223
url : str
221224
URL of repo
222225
226+
git_shallow : bool
227+
Clone with history truncated to the latest commit (``--depth 1``,
228+
default False)
229+
230+
depth : int, optional
231+
Clone with history truncated to ``depth`` commits
232+
(``git clone --depth N``). Takes precedence over ``git_shallow``.
233+
Default None (full clone).
234+
223235
tls_verify : bool
224236
Should certificate for https be checked (default False)
225237
@@ -258,10 +270,9 @@ def __init__(
258270
}
259271
)
260272
"""
261-
if "git_shallow" not in kwargs:
262-
self.git_shallow = False
263-
if "tls_verify" not in kwargs:
264-
self.tls_verify = False
273+
self.git_shallow = git_shallow
274+
self.tls_verify = tls_verify
275+
self.depth = depth
265276

266277
self._remotes: GitSyncRemoteDict
267278

@@ -364,10 +375,19 @@ def obtain(self, *args: t.Any, **kwargs: t.Any) -> None:
364375
url = self.url
365376

366377
self.log.info("Cloning.")
378+
# An explicit depth wins; otherwise git_shallow keeps the depth-1
379+
# behavior, and neither means a full clone.
380+
clone_depth: int | None
381+
if self.depth is not None:
382+
clone_depth = self.depth
383+
elif self.git_shallow:
384+
clone_depth = 1
385+
else:
386+
clone_depth = None
367387
self.cmd.clone(
368388
url=url,
369389
progress=True,
370-
depth=1 if self.git_shallow else None,
390+
depth=clone_depth,
371391
config={"http.sslVerify": False} if self.tls_verify else None,
372392
log_in_real_time=True,
373393
)
@@ -392,6 +412,15 @@ def update_repo(
392412
) -> SyncResult:
393413
"""Pull latest changes from git remote.
394414
415+
.. todo::
416+
417+
Honor ``depth`` on update by deepening or unshallowing the existing
418+
checkout when the requested depth differs from what is on disk.
419+
Tracked in https://github.com/vcs-python/libvcs/issues/532. Edges to
420+
handle: ``git fetch --depth N`` against a full checkout truncates it
421+
to shallow, and ``git fetch --unshallow`` against a complete repo is
422+
a fatal error (guard with ``git rev-parse --is-shallow-repository``).
423+
395424
Parameters
396425
----------
397426
set_remotes : bool

tests/sync/test_git.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import datetime
6+
import os
67
import pathlib
78
import random
89
import shutil
@@ -28,7 +29,7 @@
2829
if t.TYPE_CHECKING:
2930
from pytest_mock import MockerFixture
3031

31-
from libvcs.pytest_plugin import CreateRepoFn
32+
from libvcs.pytest_plugin import CreateRepoFn, GitCommitEnvVars
3233

3334
if not shutil.which("git"):
3435
pytestmark = pytest.mark.skip(reason="git is not available")
@@ -120,6 +121,116 @@ def test_repo_git_obtain_full(
120121
assert (tmp_path / "myrepo").exists()
121122

122123

124+
def test_git_shallow_and_tls_verify_kwargs_are_honored(
125+
tmp_path: pathlib.Path,
126+
git_remote_repo: pathlib.Path,
127+
) -> None:
128+
"""``git_shallow`` and ``tls_verify`` populate their attributes.
129+
130+
Regression: each kwarg previously left its attribute unset, so the next
131+
``obtain()`` raised ``AttributeError``.
132+
"""
133+
# tls_verify reaches the attribute. Its clone-time ``config`` wiring is
134+
# broken independently of this fix and tracked separately, so we don't
135+
# drive a clone with it here.
136+
assert (
137+
GitSync(
138+
url=git_remote_repo.as_uri(),
139+
path=tmp_path / "tls",
140+
tls_verify=True,
141+
).tls_verify
142+
is True
143+
)
144+
145+
# git_shallow drives a depth-1 (shallow) clone in obtain().
146+
git_repo = GitSync(
147+
url=git_remote_repo.as_uri(),
148+
path=tmp_path / "myrepo",
149+
git_shallow=True,
150+
)
151+
assert git_repo.git_shallow is True
152+
git_repo.obtain()
153+
154+
is_shallow = run(
155+
["git", "rev-parse", "--is-shallow-repository"],
156+
cwd=tmp_path / "myrepo",
157+
)
158+
assert is_shallow == "true"
159+
160+
161+
class DepthFixture(t.NamedTuple):
162+
"""Parameters for :func:`test_obtain_honors_clone_depth`."""
163+
164+
test_id: str
165+
sync_kwargs: dict[str, t.Any]
166+
expected_count: int
167+
expected_shallow: bool
168+
169+
170+
DEPTH_FIXTURES: list[DepthFixture] = [
171+
DepthFixture(
172+
test_id="full-clone",
173+
sync_kwargs={},
174+
expected_count=6,
175+
expected_shallow=False,
176+
),
177+
DepthFixture(
178+
test_id="git_shallow-depth-1",
179+
sync_kwargs={"git_shallow": True},
180+
expected_count=1,
181+
expected_shallow=True,
182+
),
183+
DepthFixture(
184+
test_id="depth-3",
185+
sync_kwargs={"depth": 3},
186+
expected_count=3,
187+
expected_shallow=True,
188+
),
189+
DepthFixture(
190+
test_id="depth-overrides-git_shallow",
191+
sync_kwargs={"git_shallow": True, "depth": 2},
192+
expected_count=2,
193+
expected_shallow=True,
194+
),
195+
]
196+
197+
198+
@pytest.mark.parametrize(
199+
list(DepthFixture._fields),
200+
DEPTH_FIXTURES,
201+
ids=[test.test_id for test in DEPTH_FIXTURES],
202+
)
203+
def test_obtain_honors_clone_depth(
204+
tmp_path: pathlib.Path,
205+
create_git_remote_repo: CreateRepoFn,
206+
git_commit_envvars: GitCommitEnvVars,
207+
test_id: str,
208+
sync_kwargs: dict[str, t.Any],
209+
expected_count: int,
210+
expected_shallow: bool,
211+
) -> None:
212+
"""obtain() clones at the requested depth; an explicit depth wins.
213+
214+
The ``file://`` URL matters: git ignores ``--depth`` for local-path clones.
215+
"""
216+
remote_repo = create_git_remote_repo()
217+
env = os.environ.copy()
218+
env.update(git_commit_envvars)
219+
for i in range(1, 7):
220+
(remote_repo / "f.txt").write_text(str(i))
221+
run(["git", "add", "f.txt"], cwd=remote_repo, env=env)
222+
run(["git", "commit", "-m", f"c{i}"], cwd=remote_repo, env=env)
223+
224+
checkout = tmp_path / "checkout"
225+
git_repo = GitSync(url=remote_repo.as_uri(), path=checkout, **sync_kwargs)
226+
git_repo.obtain()
227+
228+
commit_count = run(["git", "rev-list", "--count", "HEAD"], cwd=checkout)
229+
is_shallow = run(["git", "rev-parse", "--is-shallow-repository"], cwd=checkout)
230+
assert int(commit_count) == expected_count
231+
assert is_shallow == ("true" if expected_shallow else "false")
232+
233+
123234
@pytest.mark.parametrize(
124235
# Postpone evaluation of options so fixture variables can interpolate
125236
("constructor", "lazy_constructor_options"),

0 commit comments

Comments
 (0)