Skip to content

Commit 4900be1

Browse files
committed
sync/_async(feat[AsyncGitSync]): Add async repository synchronization
why: Phase 4 of asyncio support - AsyncGitSync enables non-blocking repository clone and update operations. what: - Add AsyncBaseSync base class with async run() and ensure_dir() - Add AsyncGitSync with full sync API: - obtain(): Clone repository and init submodules - update_repo(): Fetch, checkout, rebase with stash handling - set_remotes(), remote(), remotes_get(): Remote management - get_revision(), status(), get_git_version(): Status queries - Reuse GitRemote, GitStatus, exceptions from sync.git module - Add comprehensive tests (15 tests) including concurrency tests - 659 total tests pass (17 new)
1 parent 27f8564 commit 4900be1

File tree

5 files changed

+1085
-0
lines changed

5 files changed

+1085
-0
lines changed

src/libvcs/sync/_async/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Async repository synchronization classes.
2+
3+
This module provides async equivalents of the sync classes
4+
in :mod:`libvcs.sync`.
5+
6+
Note
7+
----
8+
This is an internal API not covered by versioning policy.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from libvcs.sync._async.git import AsyncGitSync
14+
15+
__all__ = [
16+
"AsyncGitSync",
17+
]

src/libvcs/sync/_async/base.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
"""Foundational tools for async VCS managers.
2+
3+
Async equivalent of :mod:`libvcs.sync.base`.
4+
5+
Note
6+
----
7+
This is an internal API not covered by versioning policy.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import logging
13+
import pathlib
14+
import typing as t
15+
from urllib import parse as urlparse
16+
17+
from libvcs._internal.async_run import (
18+
AsyncProgressCallbackProtocol,
19+
async_run,
20+
)
21+
from libvcs._internal.run import CmdLoggingAdapter
22+
from libvcs._internal.types import StrPath
23+
from libvcs.sync.base import convert_pip_url
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
class AsyncBaseSync:
29+
"""Base class for async repository synchronization.
30+
31+
Async equivalent of :class:`~libvcs.sync.base.BaseSync`.
32+
"""
33+
34+
log_in_real_time: bool | None = None
35+
"""Log command output to buffer"""
36+
37+
bin_name: str = ""
38+
"""VCS app name, e.g. 'git'"""
39+
40+
schemes: tuple[str, ...] = ()
41+
"""List of supported schemes to register in urlparse.uses_netloc"""
42+
43+
def __init__(
44+
self,
45+
*,
46+
url: str,
47+
path: StrPath,
48+
progress_callback: AsyncProgressCallbackProtocol | None = None,
49+
**kwargs: t.Any,
50+
) -> None:
51+
"""Initialize async VCS synchronization object.
52+
53+
Parameters
54+
----------
55+
url : str
56+
URL of the repository
57+
path : str | Path
58+
Local path for the repository
59+
progress_callback : AsyncProgressCallbackProtocol, optional
60+
Async callback for progress updates
61+
62+
Examples
63+
--------
64+
>>> import asyncio
65+
>>> class MyRepo(AsyncBaseSync):
66+
... bin_name = 'git'
67+
... async def obtain(self):
68+
... await self.run(['clone', self.url, str(self.path)])
69+
"""
70+
self.url = url
71+
72+
#: Async callback for run updates
73+
self.progress_callback = progress_callback
74+
75+
#: Directory to check out
76+
self.path: pathlib.Path
77+
if isinstance(path, pathlib.Path):
78+
self.path = path
79+
else:
80+
self.path = pathlib.Path(path)
81+
82+
if "rev" in kwargs:
83+
self.rev = kwargs["rev"]
84+
85+
# Register schemes with urlparse
86+
if hasattr(self, "schemes"):
87+
urlparse.uses_netloc.extend(self.schemes)
88+
if getattr(urlparse, "uses_fragment", None):
89+
urlparse.uses_fragment.extend(self.schemes)
90+
91+
#: Logging attribute
92+
self.log: CmdLoggingAdapter = CmdLoggingAdapter(
93+
bin_name=self.bin_name,
94+
keyword=self.repo_name,
95+
logger=logger,
96+
extra={},
97+
)
98+
99+
@property
100+
def repo_name(self) -> str:
101+
"""Return the short name of a repo checkout."""
102+
return self.path.stem
103+
104+
@classmethod
105+
def from_pip_url(cls, pip_url: str, **kwargs: t.Any) -> AsyncBaseSync:
106+
"""Create async synchronization object from pip-style URL."""
107+
url, rev = convert_pip_url(pip_url)
108+
return cls(url=url, rev=rev, **kwargs)
109+
110+
async def run(
111+
self,
112+
cmd: StrPath | list[StrPath],
113+
cwd: StrPath | None = None,
114+
check_returncode: bool = True,
115+
log_in_real_time: bool | None = None,
116+
timeout: float | None = None,
117+
**kwargs: t.Any,
118+
) -> str:
119+
"""Run a command asynchronously.
120+
121+
This method will also prefix the VCS command bin_name. By default runs
122+
using the cwd of the repo.
123+
124+
Parameters
125+
----------
126+
cmd : str | list[str]
127+
Command and arguments to run
128+
cwd : str | Path, optional
129+
Working directory, defaults to self.path
130+
check_returncode : bool, default True
131+
Raise on non-zero exit code
132+
log_in_real_time : bool, optional
133+
Stream output to callback
134+
timeout : float, optional
135+
Timeout in seconds
136+
137+
Returns
138+
-------
139+
str
140+
Combined stdout/stderr output
141+
"""
142+
if cwd is None:
143+
cwd = getattr(self, "path", None)
144+
145+
if isinstance(cmd, list):
146+
full_cmd = [self.bin_name, *[str(c) for c in cmd]]
147+
else:
148+
full_cmd = [self.bin_name, str(cmd)]
149+
150+
should_log = log_in_real_time or self.log_in_real_time or False
151+
152+
return await async_run(
153+
full_cmd,
154+
callback=self.progress_callback if should_log else None,
155+
check_returncode=check_returncode,
156+
cwd=cwd,
157+
timeout=timeout,
158+
**kwargs,
159+
)
160+
161+
def ensure_dir(self, *args: t.Any, **kwargs: t.Any) -> bool:
162+
"""Assure destination path exists. If not, create directories.
163+
164+
Note: This is synchronous as it's just filesystem operations.
165+
"""
166+
if self.path.exists():
167+
return True
168+
169+
if not self.path.parent.exists():
170+
self.path.parent.mkdir(parents=True)
171+
172+
if not self.path.exists():
173+
self.log.debug(
174+
f"Project directory for {self.repo_name} does not exist @ {self.path}",
175+
)
176+
self.path.mkdir(parents=True)
177+
178+
return True
179+
180+
async def update_repo(self, *args: t.Any, **kwargs: t.Any) -> None:
181+
"""Pull latest changes from remote repository."""
182+
raise NotImplementedError
183+
184+
async def obtain(self, *args: t.Any, **kwargs: t.Any) -> None:
185+
"""Checkout initial VCS repository from remote repository."""
186+
raise NotImplementedError
187+
188+
def __repr__(self) -> str:
189+
"""Representation of async VCS management object."""
190+
return f"<{self.__class__.__name__} {self.repo_name}>"

0 commit comments

Comments
 (0)