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
5 changes: 4 additions & 1 deletion src/scmrepo/git/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,14 @@ def clone(
url: str,
to_path: str,
rev: Optional[str] = None,
bare: bool = False,
mirror: bool = False,
**kwargs,
):
assert not (rev and bare), "rev checkout is unsupported in bare repos"
for _, backend in GitBackends.DEFAULT.items():
try:
backend.clone(url, to_path, **kwargs)
backend.clone(url, to_path, bare=bare, mirror=mirror, **kwargs)
repo = cls(to_path)
if rev:
repo.checkout(rev)
Expand Down
2 changes: 2 additions & 0 deletions src/scmrepo/git/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def clone(
to_path: str,
shallow_branch: Optional[str] = None,
progress: Callable[["GitProgressEvent"], None] = None,
bare: bool = False,
mirror: bool = False,
):
pass

Expand Down
31 changes: 30 additions & 1 deletion src/scmrepo/git/backend/dulwich/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,16 @@ def clone(
to_path: str,
shallow_branch: Optional[str] = None,
progress: Callable[["GitProgressEvent"], None] = None,
bare: bool = False,
mirror: bool = False,
):
from urllib.parse import urlparse

from dulwich.porcelain import NoneStream
from dulwich.porcelain import clone as git_clone

if mirror:
bare = True
parsed = urlparse(url)
try:
clone_from = partial(
Expand All @@ -207,6 +211,7 @@ def clone(
errstream=(
DulwichProgressReporter(progress) if progress else NoneStream()
),
bare=bare,
)
if shallow_branch:
# NOTE: dulwich only supports shallow/depth for non-local
Expand All @@ -221,7 +226,10 @@ def clone(
repo = clone_from()

with closing(repo):
cls._set_default_tracking_branch(repo)
if mirror:
cls._set_mirror(repo, progress=progress)
else:
cls._set_default_tracking_branch(repo)
except Exception as exc:
raise CloneError(url, to_path) from exc

Expand All @@ -240,6 +248,27 @@ def _set_default_tracking_branch(repo: "Repo"):
config.set(section, b"remote", b"origin")
config.set(section, b"merge", ref)

@staticmethod
def _set_mirror(
repo: "Repo", progress: Callable[["GitProgressEvent"], None] = None
):
from dulwich.porcelain import NoneStream, fetch

config = repo.get_config()
section = config[(b"remote", b"origin")]
try:
del section[b"fetch"]
except KeyError:
pass
section[b"fetch"] = b"+refs/*:refs/*"
section[b"mirror"] = b"true"
config.write_to_path()
fetch(
repo,
remote_location=b"origin",
errstream=(DulwichProgressReporter(progress) if progress else NoneStream()),
)

@staticmethod
def init(path: str, bare: bool = False) -> None:
from dulwich.porcelain import init
Expand Down
4 changes: 4 additions & 0 deletions src/scmrepo/git/backend/gitpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ def clone(
to_path: str,
shallow_branch: Optional[str] = None,
progress: Callable[["GitProgressEvent"], None] = None,
bare: bool = False,
mirror: bool = False,
):
from git import Repo
from git.exc import GitCommandError
Expand Down Expand Up @@ -178,6 +180,8 @@ def clone(
env=env, # needed before we can fix it in __init__
no_single_branch=True,
progress=GitProgressReporter.wrap_fn(progress) if progress else None,
bare=bare,
mirror=mirror,
)
if shallow_branch is None:
tmp_repo = clone_from()
Expand Down
31 changes: 29 additions & 2 deletions src/scmrepo/git/backend/pygit2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

if TYPE_CHECKING:
from pygit2.remote import Remote # type: ignore
from pygit2.repository import Repository

from scmrepo.progress import GitProgressEvent

Expand Down Expand Up @@ -154,25 +155,51 @@ def release_odb_handles(self):
# can be reacquired later as needed.
self.repo.free()

@staticmethod
@classmethod
def clone(
cls,
url: str,
to_path: str,
shallow_branch: Optional[str] = None,
progress: Callable[["GitProgressEvent"], None] = None,
bare: bool = False,
mirror: bool = False,
):
from pygit2 import GitError, clone_repository

from .callbacks import RemoteCallbacks

if shallow_branch:
raise NotImplementedError
if mirror:
bare = True
try:
with RemoteCallbacks(progress=progress) as cb:
clone_repository(url, to_path, callbacks=cb)
repo = clone_repository(url, to_path, callbacks=cb, bare=bare)
if mirror:
cls._set_mirror(repo, progress=progress)
except GitError as exc:
raise CloneError(url, to_path) from exc

@staticmethod
def _set_mirror(
repo: "Repository",
progress: Callable[["GitProgressEvent"], None] = None,
):
from .callbacks import RemoteCallbacks

url = repo.remotes["origin"].url
repo.remotes.delete("origin")
# NOTE: Pygit2 remotes.create("origin", url, fetch_refspec) creates a
# duplicate config section for each remote config entry. We just edit
# the config directly so that it creates a single section to be
# consistent with CLI Git
repo.config["remote.origin.url"] = url
repo.config["remote.origin.fetch"] = "+refs/*:refs/*"
repo.config["remote.origin.mirror"] = True
with RemoteCallbacks(progress=progress) as cb:
repo.remotes["origin"].fetch(callbacks=cb)

@staticmethod
def init(path: str, bare: bool = False) -> None:
from pygit2 import init_repository
Expand Down
18 changes: 16 additions & 2 deletions tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -907,23 +907,37 @@ async def test_git_ssh(

@pytest.mark.parametrize("scheme", ["", "file://"])
@pytest.mark.parametrize("shallow_branch", [None, "master"])
@pytest.mark.parametrize("bare", [True, False])
def test_clone(
tmp_dir: TmpDir,
scm: Git,
git: Git,
tmp_dir_factory: TempDirFactory,
scheme: str,
shallow_branch: Optional[str],
bare: bool,
):
tmp_dir.gen("foo", "foo")
scm.add_commit("foo", message="init")
rev = scm.get_rev()

target_dir = tmp_dir_factory.mktemp("git-clone")
git.clone(f"{scheme}{tmp_dir}", str(target_dir), shallow_branch=shallow_branch)
git.clone(
f"{scheme}{tmp_dir}",
str(target_dir),
shallow_branch=shallow_branch,
bare=bare,
)
target = Git(str(target_dir))
assert target.get_rev() == rev
assert (target_dir / "foo").read_text() == "foo"
if bare:
assert not (target_dir / "foo").exists()
else:
assert (target_dir / "foo").read_text() == "foo"
assert (target_dir / "foo").read_text() == "foo"
fs = target.get_fs(rev)
with fs.open("foo", mode="r") as fobj:
assert fobj.read().strip() == "foo"


@pytest.mark.skip_git_backend("pygit2")
Expand Down