Skip to content

Commit 8c6eb7a

Browse files
committed
cmd/_async(feat[AsyncSvn]): Add async Subversion command class
why: Enable async SVN operations for Phase 7 of asyncio support what: - Add AsyncSvn class with async run(), checkout(), update(), info() - Include SVN-specific auth: username, password, trust_server_cert - Default non_interactive=True for automation
1 parent 85ee7d2 commit 8c6eb7a

1 file changed

Lines changed: 382 additions & 0 deletions

File tree

src/libvcs/cmd/_async/svn.py

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
"""Async svn (subversion) commands directly against SVN working copy.
2+
3+
Async equivalent of :mod:`libvcs.cmd.svn`.
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 pathlib
13+
import typing as t
14+
from collections.abc import Sequence
15+
16+
from libvcs._internal.async_run import (
17+
AsyncProgressCallbackProtocol,
18+
async_run,
19+
)
20+
from libvcs._internal.types import StrOrBytesPath, StrPath
21+
22+
_CMD = StrOrBytesPath | Sequence[StrOrBytesPath]
23+
24+
DepthLiteral = t.Literal["infinity", "empty", "files", "immediates"] | None
25+
RevisionLiteral = t.Literal["HEAD", "BASE", "COMMITTED", "PREV"] | None
26+
27+
28+
class AsyncSvn:
29+
"""Run commands directly on a Subversion working copy asynchronously.
30+
31+
Async equivalent of :class:`~libvcs.cmd.svn.Svn`.
32+
33+
Parameters
34+
----------
35+
path : str | Path
36+
Path to the SVN working copy
37+
progress_callback : AsyncProgressCallbackProtocol, optional
38+
Async callback for progress reporting
39+
40+
Examples
41+
--------
42+
>>> import asyncio
43+
>>> async def example():
44+
... svn = AsyncSvn(path="/path/to/working-copy")
45+
... info = await svn.info()
46+
... return info
47+
>>> # asyncio.run(example())
48+
"""
49+
50+
progress_callback: AsyncProgressCallbackProtocol | None = None
51+
52+
def __init__(
53+
self,
54+
*,
55+
path: StrPath,
56+
progress_callback: AsyncProgressCallbackProtocol | None = None,
57+
) -> None:
58+
"""Initialize AsyncSvn command wrapper.
59+
60+
Parameters
61+
----------
62+
path : str | Path
63+
Path to the SVN working copy
64+
progress_callback : AsyncProgressCallbackProtocol, optional
65+
Async callback for progress reporting
66+
"""
67+
self.path: pathlib.Path
68+
if isinstance(path, pathlib.Path):
69+
self.path = path
70+
else:
71+
self.path = pathlib.Path(path)
72+
73+
self.progress_callback = progress_callback
74+
75+
def __repr__(self) -> str:
76+
"""Representation of AsyncSvn command object."""
77+
return f"<AsyncSvn path={self.path}>"
78+
79+
async def run(
80+
self,
81+
args: _CMD,
82+
*,
83+
quiet: bool | None = None,
84+
username: str | None = None,
85+
password: str | None = None,
86+
no_auth_cache: bool | None = None,
87+
non_interactive: bool | None = True,
88+
trust_server_cert: bool | None = None,
89+
config_dir: pathlib.Path | None = None,
90+
config_option: pathlib.Path | None = None,
91+
# Pass-through to async_run()
92+
cwd: StrOrBytesPath | None = None,
93+
log_in_real_time: bool = False,
94+
check_returncode: bool = True,
95+
timeout: float | None = None,
96+
**kwargs: t.Any,
97+
) -> str:
98+
"""Run a command for this SVN working copy asynchronously.
99+
100+
Async equivalent of :meth:`~libvcs.cmd.svn.Svn.run`.
101+
102+
Parameters
103+
----------
104+
args : list[str] | str
105+
SVN subcommand and arguments
106+
quiet : bool, optional
107+
-q / --quiet
108+
username : str, optional
109+
--username
110+
password : str, optional
111+
--password
112+
no_auth_cache : bool, optional
113+
--no-auth-cache
114+
non_interactive : bool, default True
115+
--non-interactive
116+
trust_server_cert : bool, optional
117+
--trust-server-cert
118+
config_dir : Path, optional
119+
--config-dir
120+
cwd : str | Path, optional
121+
Working directory. Defaults to self.path.
122+
check_returncode : bool, default True
123+
Raise on non-zero exit code
124+
timeout : float, optional
125+
Timeout in seconds
126+
127+
Returns
128+
-------
129+
str
130+
Command output
131+
"""
132+
cli_args: list[str]
133+
if isinstance(args, Sequence) and not isinstance(args, (str, bytes)):
134+
cli_args = ["svn", *[str(a) for a in args]]
135+
else:
136+
cli_args = ["svn", str(args)]
137+
138+
run_cwd = cwd if cwd is not None else self.path
139+
140+
# Build flags
141+
if no_auth_cache is True:
142+
cli_args.append("--no-auth-cache")
143+
if non_interactive is True:
144+
cli_args.append("--non-interactive")
145+
if username is not None:
146+
cli_args.extend(["--username", username])
147+
if password is not None:
148+
cli_args.extend(["--password", password])
149+
if trust_server_cert is True:
150+
cli_args.append("--trust-server-cert")
151+
if config_dir is not None:
152+
cli_args.extend(["--config-dir", str(config_dir)])
153+
if config_option is not None:
154+
cli_args.extend(["--config-option", str(config_option)])
155+
156+
return await async_run(
157+
cli_args,
158+
cwd=run_cwd,
159+
callback=self.progress_callback if log_in_real_time else None,
160+
check_returncode=check_returncode,
161+
timeout=timeout,
162+
**kwargs,
163+
)
164+
165+
async def checkout(
166+
self,
167+
*,
168+
url: str,
169+
revision: RevisionLiteral | str = None,
170+
force: bool | None = None,
171+
ignore_externals: bool | None = None,
172+
depth: DepthLiteral = None,
173+
quiet: bool | None = None,
174+
username: str | None = None,
175+
password: str | None = None,
176+
no_auth_cache: bool | None = None,
177+
non_interactive: bool | None = True,
178+
trust_server_cert: bool | None = None,
179+
# Special behavior
180+
make_parents: bool | None = True,
181+
# Pass-through
182+
log_in_real_time: bool = False,
183+
check_returncode: bool = True,
184+
timeout: float | None = None,
185+
) -> str:
186+
"""Check out a working copy from an SVN repo asynchronously.
187+
188+
Async equivalent of :meth:`~libvcs.cmd.svn.Svn.checkout`.
189+
190+
Parameters
191+
----------
192+
url : str
193+
Repository URL to checkout
194+
revision : str, optional
195+
Number, '{ DATE }', 'HEAD', 'BASE', 'COMMITTED', 'PREV'
196+
force : bool, optional
197+
Force operation to run
198+
ignore_externals : bool, optional
199+
Ignore externals definitions
200+
depth : str, optional
201+
Sparse checkout depth
202+
quiet : bool, optional
203+
Suppress output
204+
username : str, optional
205+
SVN username
206+
password : str, optional
207+
SVN password
208+
make_parents : bool, default True
209+
Create checkout directory if it doesn't exist
210+
check_returncode : bool, default True
211+
Raise on non-zero exit code
212+
timeout : float, optional
213+
Timeout in seconds
214+
215+
Returns
216+
-------
217+
str
218+
Command output
219+
"""
220+
# URL and PATH come first, matching sync Svn.checkout pattern
221+
local_flags: list[str] = [url, str(self.path)]
222+
223+
if revision is not None:
224+
local_flags.extend(["--revision", str(revision)])
225+
if depth is not None:
226+
local_flags.extend(["--depth", depth])
227+
if force is True:
228+
local_flags.append("--force")
229+
if ignore_externals is True:
230+
local_flags.append("--ignore-externals")
231+
if quiet is True:
232+
local_flags.append("--quiet")
233+
234+
# libvcs special behavior
235+
if make_parents and not self.path.exists():
236+
self.path.mkdir(parents=True)
237+
238+
return await self.run(
239+
["checkout", *local_flags],
240+
username=username,
241+
password=password,
242+
no_auth_cache=no_auth_cache,
243+
non_interactive=non_interactive,
244+
trust_server_cert=trust_server_cert,
245+
log_in_real_time=log_in_real_time,
246+
check_returncode=check_returncode,
247+
timeout=timeout,
248+
)
249+
250+
async def update(
251+
self,
252+
accept: str | None = None,
253+
force: bool | None = None,
254+
ignore_externals: bool | None = None,
255+
parents: bool | None = None,
256+
quiet: bool | None = None,
257+
revision: str | None = None,
258+
set_depth: str | None = None,
259+
# Pass-through
260+
log_in_real_time: bool = False,
261+
check_returncode: bool = True,
262+
timeout: float | None = None,
263+
) -> str:
264+
"""Fetch latest changes to working copy asynchronously.
265+
266+
Async equivalent of :meth:`~libvcs.cmd.svn.Svn.update`.
267+
268+
Parameters
269+
----------
270+
accept : str, optional
271+
Conflict resolution action
272+
force : bool, optional
273+
Force operation
274+
ignore_externals : bool, optional
275+
Ignore externals definitions
276+
parents : bool, optional
277+
Make intermediate directories
278+
quiet : bool, optional
279+
Suppress output
280+
revision : str, optional
281+
Update to specific revision
282+
set_depth : str, optional
283+
Set new working copy depth
284+
check_returncode : bool, default True
285+
Raise on non-zero exit code
286+
timeout : float, optional
287+
Timeout in seconds
288+
289+
Returns
290+
-------
291+
str
292+
Command output
293+
"""
294+
local_flags: list[str] = []
295+
296+
if revision is not None:
297+
local_flags.extend(["--revision", revision])
298+
if set_depth is not None:
299+
local_flags.extend(["--set-depth", set_depth])
300+
if accept is not None:
301+
local_flags.extend(["--accept", accept])
302+
if force is True:
303+
local_flags.append("--force")
304+
if ignore_externals is True:
305+
local_flags.append("--ignore-externals")
306+
if parents is True:
307+
local_flags.append("--parents")
308+
if quiet is True:
309+
local_flags.append("--quiet")
310+
311+
return await self.run(
312+
["update", *local_flags],
313+
log_in_real_time=log_in_real_time,
314+
check_returncode=check_returncode,
315+
timeout=timeout,
316+
)
317+
318+
async def info(
319+
self,
320+
target: StrPath | None = None,
321+
revision: str | None = None,
322+
depth: DepthLiteral = None,
323+
incremental: bool | None = None,
324+
recursive: bool | None = None,
325+
xml: bool | None = None,
326+
# Pass-through
327+
log_in_real_time: bool = False,
328+
check_returncode: bool = True,
329+
timeout: float | None = None,
330+
) -> str:
331+
"""Return info about this SVN repository asynchronously.
332+
333+
Async equivalent of :meth:`~libvcs.cmd.svn.Svn.info`.
334+
335+
Parameters
336+
----------
337+
target : str | Path, optional
338+
Target path or URL
339+
revision : str, optional
340+
Revision to get info for
341+
depth : str, optional
342+
Limit operation depth
343+
incremental : bool, optional
344+
Give output suitable for concatenation
345+
recursive : bool, optional
346+
Descend recursively
347+
xml : bool, optional
348+
Output in XML format
349+
check_returncode : bool, default True
350+
Raise on non-zero exit code
351+
timeout : float, optional
352+
Timeout in seconds
353+
354+
Returns
355+
-------
356+
str
357+
Command output (optionally XML)
358+
"""
359+
local_flags: list[str] = []
360+
361+
if isinstance(target, pathlib.Path):
362+
local_flags.append(str(target.absolute()))
363+
elif isinstance(target, str):
364+
local_flags.append(target)
365+
366+
if revision is not None:
367+
local_flags.extend(["--revision", revision])
368+
if depth is not None:
369+
local_flags.extend(["--depth", depth])
370+
if incremental is True:
371+
local_flags.append("--incremental")
372+
if recursive is True:
373+
local_flags.append("--recursive")
374+
if xml is True:
375+
local_flags.append("--xml")
376+
377+
return await self.run(
378+
["info", *local_flags],
379+
log_in_real_time=log_in_real_time,
380+
check_returncode=check_returncode,
381+
timeout=timeout,
382+
)

0 commit comments

Comments
 (0)