Skip to content

Commit 23c8986

Browse files
authored
feat(git): Improve remotes support (#314)
Fixes #311 for use in vcs-python/vcspull#362
2 parents 3c0f536 + 91f5055 commit 23c8986

File tree

4 files changed

+241
-7
lines changed

4 files changed

+241
-7
lines changed

CHANGES

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,35 @@ $ pip install --user --upgrade --pre libvcs
1111

1212
## current - unrelased
1313

14+
### What's new
15+
16+
- {class}`libvcs.git.GitRepo` now accepts remotes in `__init__`
17+
18+
```python
19+
repo = GitRepo(
20+
url="https://github.com/vcs-python/libvcs",
21+
repo_dir=checkout,
22+
remotes={
23+
'gitlab': 'https://gitlab.com/vcs-python/libvcs',
24+
}
25+
)
26+
```
27+
28+
```python
29+
repo = GitRepo(
30+
url="https://github.com/vcs-python/libvcs",
31+
repo_dir=checkout,
32+
remotes={
33+
'gitlab': {
34+
'fetch': 'https://gitlab.com/vcs-python/libvcs',
35+
'push': 'https://gitlab.com/vcs-python/libvcs',
36+
},
37+
}
38+
)
39+
```
40+
41+
- {meth}`libvcs.git.GitRepo.update_repo` now accepts `set_remotes=True`
42+
1443
### Compatibility
1544

1645
- Python 3.7 and 3.8 dropped (#308)

docs/conf.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def setup(app):
8787
]
8888
}
8989

90+
# sphinx.ext.autodoc
91+
autoclass_content = "both"
92+
autodoc_member_order = "bysource"
93+
9094
# sphinxext.opengraph
9195
ogp_site_url = about["__docs__"]
9296
ogp_image = "_static/img/icons/icon-192x192.png"

libvcs/git.py

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import logging
1818
import os
1919
import re
20-
from typing import Dict, NamedTuple, Optional
20+
from typing import Dict, NamedTuple, Optional, TypedDict, Union
2121
from urllib import parse as urlparse
2222

2323
from . import exc
@@ -125,11 +125,20 @@ def convert_pip_url(pip_url: str) -> VCSLocation:
125125
return VCSLocation(url=url, rev=rev)
126126

127127

128+
class RemoteDict(TypedDict):
129+
fetch: str
130+
push: str
131+
132+
133+
FullRemoteDict = Dict[str, RemoteDict]
134+
RemotesArgs = Union[None, FullRemoteDict, Dict[str, str]]
135+
136+
128137
class GitRepo(BaseRepo):
129138
bin_name = "git"
130139
schemes = ("git", "git+http", "git+https", "git+ssh", "git+git", "git+file")
131140

132-
def __init__(self, url, repo_dir, **kwargs):
141+
def __init__(self, url: str, repo_dir: str, remotes: RemotesArgs = None, **kwargs):
133142
"""A git repository.
134143
135144
Parameters
@@ -139,12 +148,61 @@ def __init__(self, url, repo_dir, **kwargs):
139148
140149
tls_verify : bool
141150
Should certificate for https be checked (default False)
151+
152+
Examples
153+
--------
154+
155+
.. code-block:: python
156+
157+
import os
158+
from libvcs.git import GitRepo
159+
160+
checkout = os.path.dirname(os.path.abspath(__name__)) + '/' + 'my_libvcs'
161+
162+
repo = GitRepo(
163+
url="https://github.com/vcs-python/libvcs",
164+
repo_dir=checkout,
165+
remotes={
166+
'gitlab': 'https://gitlab.com/vcs-python/libvcs'
167+
}
168+
)
169+
170+
.. code-block:: python
171+
172+
import os
173+
from libvcs.git import GitRepo
174+
175+
checkout = os.path.dirname(os.path.abspath(__name__)) + '/' + 'my_libvcs'
176+
177+
repo = GitRepo(
178+
url="https://github.com/vcs-python/libvcs",
179+
repo_dir=checkout,
180+
remotes={
181+
'gitlab': {
182+
'fetch': 'https://gitlab.com/vcs-python/libvcs',
183+
'push': 'https://gitlab.com/vcs-python/libvcs',
184+
},
185+
}
186+
)
142187
"""
143188
if "git_shallow" not in kwargs:
144189
self.git_shallow = False
145190
if "tls_verify" not in kwargs:
146191
self.tls_verify = False
147192

193+
self._remotes: Union[FullRemoteDict, None]
194+
195+
if remotes is None:
196+
self._remotes: FullRemoteDict = {"origin": url}
197+
elif isinstance(remotes, dict):
198+
self._remotes: FullRemoteDict = remotes
199+
for remote_name, url in remotes.items():
200+
if isinstance(str, dict):
201+
remotes[remote_name] = {
202+
"fetch": url,
203+
"push": url,
204+
}
205+
148206
BaseRepo.__init__(self, url, repo_dir, **kwargs)
149207

150208
@classmethod
@@ -161,6 +219,28 @@ def get_revision(self):
161219
except exc.CommandError:
162220
return "initial"
163221

222+
def set_remotes(self, overwrite: bool = False):
223+
remotes = self._remotes
224+
if isinstance(remotes, dict):
225+
for remote_name, url in remotes.items():
226+
existing_remote = self.remote(remote_name)
227+
if isinstance(url, dict) and "fetch" in url:
228+
if not existing_remote or existing_remote.fetch_url != url:
229+
self.set_remote(
230+
name=remote_name, url=url["fetch"], overwrite=overwrite
231+
)
232+
if "push" in url:
233+
if not existing_remote or existing_remote.push_url != url:
234+
self.set_remote(
235+
name=remote_name,
236+
url=url["push"],
237+
push=True,
238+
overwrite=overwrite,
239+
)
240+
else:
241+
if not existing_remote or existing_remote.fetch_url != url:
242+
self.set_remote(name=remote_name, url=url, overwrite=overwrite)
243+
164244
def obtain(self):
165245
"""Retrieve the repository, clone if doesn't exist."""
166246
self.ensure_dir()
@@ -182,14 +262,19 @@ def obtain(self):
182262
cmd = ["submodule", "update", "--recursive", "--init"]
183263
self.run(cmd, log_in_real_time=True)
184264

185-
def update_repo(self):
265+
self.set_remotes()
266+
267+
def update_repo(self, set_remotes: bool = False):
186268
self.ensure_dir()
187269

188270
if not os.path.isdir(os.path.join(self.path, ".git")):
189271
self.obtain()
190272
self.update_repo()
191273
return
192274

275+
if set_remotes:
276+
self.set_remotes(overwrite=True)
277+
193278
# Get requested revision or tag
194279
url, git_tag = self.url, getattr(self, "rev", None)
195280

@@ -388,7 +473,7 @@ def remote(self, name, **kwargs) -> GitRemote:
388473
except exc.LibVCSException:
389474
return None
390475

391-
def set_remote(self, name, url, overwrite=False):
476+
def set_remote(self, name, url, push: bool = False, overwrite=False):
392477
"""Set remote with name and URL like git remote add.
393478
394479
Parameters

tests/test_git.py

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from libvcs import exc
1414
from libvcs.git import (
15+
FullRemoteDict,
1516
GitRemote,
1617
GitRepo,
1718
convert_pip_url as git_convert_pip_url,
@@ -26,6 +27,7 @@
2627

2728
RepoTestFactory = Callable[..., GitRepo]
2829
RepoTestFactoryLazyKwargs = Callable[..., dict]
30+
RepoTestFactoryRemotesLazyExpected = Callable[..., FullRemoteDict]
2931

3032

3133
@pytest.fixture(autouse=True, scope="module")
@@ -215,21 +217,61 @@ def progress_callback_spy(output, timestamp):
215217

216218
@pytest.mark.parametrize(
217219
# Postpone evaluation of options so fixture variables can interpolate
218-
"constructor,lazy_constructor_options",
220+
"constructor,lazy_constructor_options,lazy_remote_expected",
219221
[
220222
[
221223
GitRepo,
222224
lambda git_remote, repos_path, repo_name, **kwargs: {
223225
"url": f"file://{git_remote}",
224226
"repo_dir": repos_path / repo_name,
225227
},
228+
lambda git_remote, **kwargs: {"origin": f"file://{git_remote}"},
229+
],
230+
[
231+
GitRepo,
232+
lambda git_remote, repos_path, repo_name, **kwargs: {
233+
"url": f"file://{git_remote}",
234+
"repo_dir": repos_path / repo_name,
235+
"remotes": {"origin": f"file://{git_remote}"},
236+
},
237+
lambda git_remote, **kwargs: {"origin": f"file://{git_remote}"},
238+
],
239+
[
240+
GitRepo,
241+
lambda git_remote, repos_path, repo_name, **kwargs: {
242+
"url": f"file://{git_remote}",
243+
"repo_dir": repos_path / repo_name,
244+
"remotes": {
245+
"origin": f"file://{git_remote}",
246+
"second_remote": f"file://{git_remote}",
247+
},
248+
},
249+
lambda git_remote, **kwargs: {
250+
"origin": f"file://{git_remote}",
251+
"second_remote": f"file://{git_remote}",
252+
},
253+
],
254+
[
255+
GitRepo,
256+
lambda git_remote, repos_path, repo_name, **kwargs: {
257+
"url": f"file://{git_remote}",
258+
"repo_dir": repos_path / repo_name,
259+
"remotes": {
260+
"second_remote": f"file://{git_remote}",
261+
},
262+
},
263+
lambda git_remote, **kwargs: {
264+
"origin": f"file://{git_remote}",
265+
"second_remote": f"file://{git_remote}",
266+
},
226267
],
227268
[
228269
create_repo_from_pip_url,
229270
lambda git_remote, repos_path, repo_name, **kwargs: {
230271
"pip_url": f"git+file://{git_remote}",
231272
"repo_dir": repos_path / repo_name,
232273
},
274+
lambda git_remote, **kwargs: {"origin": f"file://{git_remote}"},
233275
],
234276
],
235277
)
@@ -238,16 +280,90 @@ def test_remotes(
238280
git_remote: pathlib.Path,
239281
constructor: RepoTestFactory,
240282
lazy_constructor_options: RepoTestFactoryLazyKwargs,
283+
lazy_remote_expected: RepoTestFactoryRemotesLazyExpected,
284+
):
285+
repo_name = "myrepo"
286+
remote_name = "myremote"
287+
remote_url = "https://localhost/my/git/repo.git"
288+
289+
git_repo: GitRepo = constructor(**lazy_constructor_options(**locals()))
290+
git_repo.obtain()
291+
292+
expected = lazy_remote_expected(**locals())
293+
assert len(expected.keys()) > 0
294+
for expected_remote_name, expected_remote_url in expected.items():
295+
assert (
296+
expected_remote_name,
297+
expected_remote_url,
298+
expected_remote_url,
299+
) == git_repo.remote(expected_remote_name)
300+
301+
302+
@pytest.mark.parametrize(
303+
# Postpone evaluation of options so fixture variables can interpolate
304+
"constructor,lazy_constructor_options,lazy_remote_dict,lazy_remote_expected",
305+
[
306+
[
307+
GitRepo,
308+
lambda git_remote, repos_path, repo_name, **kwargs: {
309+
"url": f"file://{git_remote}",
310+
"repo_dir": repos_path / repo_name,
311+
"remotes": {
312+
"origin": f"file://{git_remote}",
313+
},
314+
},
315+
lambda git_remote, **kwargs: {
316+
"second_remote": f"file://{git_remote}",
317+
},
318+
lambda git_remote, **kwargs: {
319+
"origin": f"file://{git_remote}",
320+
"second_remote": f"file://{git_remote}",
321+
},
322+
],
323+
[
324+
GitRepo,
325+
lambda git_remote, repos_path, repo_name, **kwargs: {
326+
"url": f"file://{git_remote}",
327+
"repo_dir": repos_path / repo_name,
328+
"remotes": {
329+
"origin": f"file://{git_remote}",
330+
},
331+
},
332+
lambda git_remote, **kwargs: {
333+
"origin": "https://github.com/vcs-python/libvcs",
334+
},
335+
lambda git_remote, **kwargs: {
336+
"origin": "https://github.com/vcs-python/libvcs",
337+
},
338+
],
339+
],
340+
)
341+
def test_remotes_update_repo(
342+
repos_path: pathlib.Path,
343+
git_remote: pathlib.Path,
344+
constructor: RepoTestFactory,
345+
lazy_constructor_options: RepoTestFactoryLazyKwargs,
346+
lazy_remote_dict: RepoTestFactoryRemotesLazyExpected,
347+
lazy_remote_expected: RepoTestFactoryRemotesLazyExpected,
241348
):
242349
repo_name = "myrepo"
243350
remote_name = "myremote"
244351
remote_url = "https://localhost/my/git/repo.git"
245352

246353
git_repo: GitRepo = constructor(**lazy_constructor_options(**locals()))
247354
git_repo.obtain()
248-
git_repo.set_remote(name=remote_name, url=remote_url)
249355

250-
assert (remote_name, remote_url, remote_url) == git_repo.remote(remote_name)
356+
git_repo._remotes = lazy_remote_dict(**locals())
357+
git_repo.update_repo(set_remotes=True)
358+
359+
expected = lazy_remote_expected(**locals())
360+
assert len(expected.keys()) > 0
361+
for expected_remote_name, expected_remote_url in expected.items():
362+
assert (
363+
expected_remote_name,
364+
expected_remote_url,
365+
expected_remote_url,
366+
) == git_repo.remote(expected_remote_name)
251367

252368

253369
def test_git_get_url_and_rev_from_pip_url():

0 commit comments

Comments
 (0)