Skip to content

Commit ab602e5

Browse files
authored
feat,tests(svn, hg): Parsing updates and tests (#381)
Follow up to #374, fixes #382 Subversion, Mercurial parsing: - Fixes - doctests - Add pytest modules
2 parents b570af1 + 363000e commit ab602e5

File tree

5 files changed

+508
-28
lines changed

5 files changed

+508
-28
lines changed

libvcs/conftest.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from libvcs._internal.run import run, which
1414
from libvcs.projects.git import GitProject, GitRemote
15+
from libvcs.projects.hg import MercurialProject
16+
from libvcs.projects.svn import SubversionProject
1517

1618
skip_if_git_missing = pytest.mark.skipif(
1719
not which("git"), reason="git is not available"
@@ -333,6 +335,32 @@ def git_repo(projects_path: pathlib.Path, git_remote_repo: pathlib.Path):
333335
return git_repo
334336

335337

338+
@pytest.fixture
339+
def hg_repo(
340+
projects_path: pathlib.Path, hg_remote_repo: pathlib.Path
341+
) -> MercurialProject:
342+
"""Pre-made hg clone of remote repo checked out to user's projects dir."""
343+
hg_repo = MercurialProject(
344+
url=f"file://{hg_remote_repo}",
345+
dir=str(projects_path / "hg_repo"),
346+
)
347+
hg_repo.obtain()
348+
return hg_repo
349+
350+
351+
@pytest.fixture
352+
def svn_repo(
353+
projects_path: pathlib.Path, svn_remote_repo: pathlib.Path
354+
) -> SubversionProject:
355+
"""Pre-made svn clone of remote repo checked out to user's projects dir."""
356+
svn_repo = SubversionProject(
357+
url=f"file://{svn_remote_repo}",
358+
dir=str(projects_path / "svn_repo"),
359+
)
360+
svn_repo.obtain()
361+
return svn_repo
362+
363+
336364
@pytest.fixture(autouse=True)
337365
def add_doctest_fixtures(
338366
doctest_namespace: dict[str, Any],

libvcs/parse/hg.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
RE_PATH = r"""
2929
((?P<user>.*)@)?
3030
(?P<hostname>([^/:]+))
31-
(?P<separator>[:,/])?
31+
(:(?P<port>\d{1,4}))?
32+
(?P<separator>/)?
3233
(?P<path>
33-
(\w[^:.]*) # cut the path at . to negate .hg
34+
/?(\w[^:.]*)
3435
)?
3536
"""
3637

@@ -151,9 +152,9 @@ class HgURL(URLProtocol, SkipDefaultFieldsReprMixin):
151152
>>> HgURL(url='ssh://username@machinename/path/to/repo')
152153
HgURL(url=ssh://username@machinename/path/to/repo,
153154
scheme=ssh,
155+
user=username,
154156
hostname=machinename,
155157
path=path/to/repo,
156-
user=username,
157158
matcher=core-hg)
158159
159160
- Compatibility checking: :meth:`HgURL.is_valid()`
@@ -162,9 +163,11 @@ class HgURL(URLProtocol, SkipDefaultFieldsReprMixin):
162163

163164
url: str
164165
scheme: Optional[str] = None
165-
hostname: Optional[str] = None
166-
path: Optional[str] = None
167166
user: Optional[str] = None
167+
hostname: str = dataclasses.field(default="")
168+
port: Optional[int] = None
169+
separator: str = dataclasses.field(default="/")
170+
path: str = dataclasses.field(default="")
168171

169172
#
170173
# commit-ish: tag, branch, ref, revision
@@ -233,23 +236,63 @@ def to_url(self) -> str:
233236
>>> hg_location.to_url()
234237
'https://hg.mozilla.org/mobile-browser'
235238
236-
Switch them to hglab:
239+
Switch them to localhost:
237240
238241
>>> hg_location.hostname = 'localhost'
239242
>>> hg_location.scheme = 'http'
240243
241244
>>> hg_location.to_url()
242245
'http://localhost/mobile-browser'
243246
244-
todo
245-
----
247+
Another example, `hugin <http://hugin.hg.sourceforge.net>`_:
248+
249+
>>> hugin = HgURL(url="http://hugin.hg.sourceforge.net:8000/hgroot/hugin/hugin")
250+
251+
>>> hugin
252+
HgURL(url=http://hugin.hg.sourceforge.net:8000/hgroot/hugin/hugin,
253+
scheme=http,
254+
hostname=hugin.hg.sourceforge.net,
255+
port=8000,
256+
path=hgroot/hugin/hugin,
257+
matcher=core-hg)
258+
259+
>>> hugin.to_url()
260+
'http://hugin.hg.sourceforge.net:8000/hgroot/hugin/hugin'
261+
262+
SSH URL with a username, `graphicsmagic <http://graphicsmagick.org/Hg.html>`_:
263+
264+
>>> graphicsmagick = HgURL(
265+
... url="ssh://yourid@hg.GraphicsMagick.org//hg/GraphicsMagick"
266+
... )
267+
268+
>>> graphicsmagick
269+
HgURL(url=ssh://yourid@hg.GraphicsMagick.org//hg/GraphicsMagick,
270+
scheme=ssh,
271+
user=yourid,
272+
hostname=hg.GraphicsMagick.org,
273+
path=/hg/GraphicsMagick,
274+
matcher=core-hg)
275+
276+
>>> graphicsmagick.to_url()
277+
'ssh://yourid@hg.GraphicsMagick.org//hg/GraphicsMagick'
278+
279+
Switch the username:
280+
281+
>>> graphicsmagick.user = 'lucas'
282+
283+
>>> graphicsmagick.to_url()
284+
'ssh://lucas@hg.GraphicsMagick.org//hg/GraphicsMagick'
246285
247-
- Formats: Show an example converting a hghub url from ssh -> https format,
248-
and the other way around.
249286
"""
250-
if self.scheme is not None:
251-
parts = [self.scheme, "://", self.hostname, "/", self.path]
252-
else:
253-
parts = [self.user or "hg", "@", self.hostname, ":", self.path]
287+
parts = [self.scheme or "ssh", "://"]
288+
if self.user:
289+
parts.extend([self.user, "@"])
290+
291+
parts.append(self.hostname)
292+
293+
if self.port is not None:
294+
parts.extend([":", f"{self.port}"])
295+
296+
parts.extend([self.separator, self.path])
254297

255298
return "".join(part for part in parts if isinstance(part, str))

libvcs/parse/svn.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@
2929
RE_PATH = r"""
3030
((?P<user>.*)@)?
3131
(?P<hostname>([^/:]+))
32-
(?P<separator>[:,/])?
32+
(:(?P<port>\d{1,4}))?
33+
(?P<separator>/)?
3334
(?P<path>
3435
(\w[^:.]*)
3536
)?
3637
"""
3738

39+
# Valid schemes for svn(1).
40+
# See Table 1.1 Repository access URLs in SVN Book
41+
# https://svnbook.red-bean.com/nightly/en/svn.basic.in-action.html#svn.basic.in-action.wc.tbl-1
3842
RE_SCHEME = r"""
3943
(?P<scheme>
4044
(
41-
http|https|
42-
svn\+ssh
45+
file|http|https|svn|svn\+ssh
4346
)
4447
)
4548
"""
@@ -154,10 +157,12 @@ class SvnURL(URLProtocol, SkipDefaultFieldsReprMixin):
154157
"""
155158

156159
url: str
157-
scheme: str = dataclasses.field(init=False)
158-
hostname: str = dataclasses.field(init=False)
159-
path: str = dataclasses.field(init=False)
160+
scheme: Optional[str] = None
160161
user: Optional[str] = None
162+
hostname: str = dataclasses.field(default="")
163+
port: Optional[int] = None
164+
separator: str = dataclasses.field(default="/")
165+
path: str = dataclasses.field(default="")
161166

162167
#
163168
# commit-ish: ref
@@ -213,9 +218,9 @@ def to_url(self) -> str:
213218
>>> svn_location
214219
SvnURL(url=svn+ssh://my-username@my-server/vcs-python/libvcs,
215220
scheme=svn+ssh,
221+
user=my-username,
216222
hostname=my-server,
217223
path=vcs-python/libvcs,
218-
user=my-username,
219224
matcher=core-svn)
220225
221226
Switch repo libvcs -> vcspull:
@@ -232,12 +237,15 @@ def to_url(self) -> str:
232237
>>> svn_location.to_url()
233238
'svn+ssh://tom@my-server/vcs-python/vcspull'
234239
"""
235-
if self.scheme is not None:
236-
parts = [self.scheme, "://"]
237-
if self.user:
238-
parts.extend([self.user, "@"])
239-
parts += [self.hostname, "/", self.path]
240-
else:
241-
parts = [self.user or "svn", "@", self.hostname, ":", self.path]
240+
parts = [self.scheme or "ssh", "://"]
241+
if self.user:
242+
parts.extend([self.user, "@"])
243+
244+
parts.append(self.hostname)
245+
246+
if self.port is not None:
247+
parts.extend([":", f"{self.port}"])
248+
249+
parts.extend([self.separator, self.path])
242250

243251
return "".join(part for part in parts if isinstance(part, str))

0 commit comments

Comments
 (0)