Skip to content

Commit a309afa

Browse files
[MINOR] Support multiple self hosted urls (#33)
* Support multiple self-managed gitlab hosts (#27) - tested by @dyitzchaki-roku * Side repo improvements
1 parent 5192c3b commit a309afa

File tree

8 files changed

+167
-88
lines changed

8 files changed

+167
-88
lines changed

.github/workflows/_code_checks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
run: |
4040
python -m pip install --upgrade pip
4141
python -m pip install -r requirements_dev.txt
42-
pip install -r requirements.txt
42+
python -m pip install -r requirements.txt
4343
4444
- name: lint
4545
run: make lint

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
.idea/
2-
*/__pycache__/
2+
**/__pycache__/
33
*.egg-info/
44
dist/

gitlab_submodule/gitlab_submodule.py

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,23 @@
1-
from typing import Generator, List, Optional, Union
1+
from typing import Generator, List, Optional
22

3-
from gitlab import Gitlab
4-
from gitlab.v4.objects import Project, ProjectManager
3+
from gitlab.v4.objects import Project
54

65
from gitlab_submodule.objects import Submodule, Subproject
6+
from gitlab_submodule.project_manager_utils import OneOrManyClients
77
from gitlab_submodule.read_gitmodules import \
88
iterate_project_submodules as iterate_submodules
99
from gitlab_submodule.submodule_commit import get_submodule_commit
1010
from gitlab_submodule.submodule_to_project import submodule_to_project
1111

1212

13-
def _get_project_manager(
14-
gitlab_object: Union[Gitlab, ProjectManager]) -> ProjectManager:
15-
if isinstance(gitlab_object, ProjectManager):
16-
return gitlab_object
17-
elif isinstance(gitlab_object, Gitlab):
18-
return gitlab_object.projects
19-
else:
20-
raise TypeError('Needs a Gitlab instance or its ProjectManager')
21-
22-
2313
def submodule_to_subproject(
2414
gitmodules_submodule: Submodule,
25-
gl: Union[Gitlab, ProjectManager],
26-
self_managed_gitlab_host: Optional[str] = None
15+
gls: OneOrManyClients,
2716
) -> Subproject:
2817
try:
2918
submodule_project = submodule_to_project(
3019
gitmodules_submodule,
31-
_get_project_manager(gl),
32-
self_managed_gitlab_host
20+
gls,
3321
)
3422
submodule_commit = get_submodule_commit(
3523
gitmodules_submodule,
@@ -46,17 +34,15 @@ def submodule_to_subproject(
4634

4735
def iterate_subprojects(
4836
project: Project,
49-
gl: Union[Gitlab, ProjectManager],
37+
gls: OneOrManyClients,
5038
ref: Optional[str] = None,
5139
only_gitlab_subprojects: bool = False,
52-
self_managed_gitlab_host: Optional[str] = None
5340
) -> Generator[Subproject, None, None]:
5441
for gitmodules_submodule in iterate_submodules(project, ref):
5542
try:
5643
subproject: Subproject = submodule_to_subproject(
5744
gitmodules_submodule,
58-
_get_project_manager(gl),
59-
self_managed_gitlab_host,
45+
gls,
6046
)
6147
if not (only_gitlab_subprojects and not subproject.project):
6248
yield subproject
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from typing import Dict, List, Union
2+
3+
from gitlab import Gitlab
4+
from gitlab.v4.objects import ProjectManager
5+
6+
# Some typing
7+
Client = Union[Gitlab, ProjectManager]
8+
OneOrManyClients = Union[Client, List[Client]]
9+
ProjectManagerDicts = Dict[str, ProjectManager]
10+
11+
12+
def as_project_manager(gl: Client) -> ProjectManager:
13+
if isinstance(gl, ProjectManager):
14+
return gl
15+
elif isinstance(gl, Gitlab):
16+
return gl.projects
17+
else:
18+
raise TypeError('Needs a Gitlab instance or its ProjectManager')
19+
20+
21+
def get_host_url(gl: Client) -> str:
22+
if isinstance(gl, Gitlab):
23+
return gl._base_url
24+
elif isinstance(gl, ProjectManager):
25+
return gl.gitlab._base_url
26+
else:
27+
raise TypeError(gl)
28+
29+
30+
def map_domain_to_clients(gls: OneOrManyClients) -> ProjectManagerDicts:
31+
if not isinstance(gls, list):
32+
gls = [gls]
33+
return {get_host_url(gl): as_project_manager(gl) for gl in gls}

gitlab_submodule/read_gitmodules.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def iterate_project_submodules(
1818
ref: Optional[str] = None) -> Iterable[Submodule]:
1919
gitmodules_file_content = _get_gitmodules_file_content(project, ref)
2020
if not gitmodules_file_content:
21-
return []
21+
raise StopIteration
2222
for kwargs in _read_gitmodules_file_content(
2323
gitmodules_file_content):
2424
yield Submodule(
Lines changed: 58 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,96 @@
11
import logging
22
import re
33
from posixpath import join, normpath
4-
from typing import List, Optional, Union
4+
from typing import Optional, Tuple
55

66
from gitlab.exceptions import GitlabGetError, GitlabHttpError
77
from gitlab.v4.objects import Project, ProjectManager
88
from giturlparse import GitUrlParsed, parse
99

1010
from gitlab_submodule.objects import Submodule
11+
from gitlab_submodule.project_manager_utils import (OneOrManyClients,
12+
map_domain_to_clients)
1113
from gitlab_submodule.string_utils import lstrip, rstrip
1214

1315
logger = logging.getLogger(__name__)
1416

1517

16-
def submodule_to_project(
17-
submodule: Submodule,
18-
project_manager: ProjectManager,
19-
self_managed_gitlab_host: Optional[Union[str, List[str]]] = None
20-
) -> Optional[Project]:
21-
submodule_project_path_with_namespace = \
22-
_submodule_url_to_path_with_namespace(submodule.url,
23-
submodule.parent_project,
24-
self_managed_gitlab_host)
25-
if not submodule_project_path_with_namespace:
26-
return None
27-
try:
28-
submodule_project = project_manager.get(
29-
submodule_project_path_with_namespace)
30-
except (GitlabGetError, GitlabHttpError):
31-
# Repo doesn't actually exist (possible because you can modify
32-
# .gitmodules without using `git submodule add`)
33-
raise FileNotFoundError(
34-
'No repo found at url "{}" for submodule at path "{}" - Check if '
35-
'the repo was deleted.'.format(submodule.url, submodule.path))
36-
return submodule_project
18+
def host_url_to_domain(url: str) -> str:
19+
return url.split("//")[1].rstrip("/")
20+
3721

22+
def match_submodule_to_client_and_format_project_path(
23+
submodule: Submodule,
24+
gls: OneOrManyClients
25+
) -> Optional[Tuple[ProjectManager, str]]:
26+
url = submodule.url
3827

39-
def _submodule_url_to_path_with_namespace(
40-
url: str,
41-
parent_project: Project,
42-
self_managed_gitlab_host: Optional[Union[str, List[str]]] = None
43-
) -> Optional[str]:
44-
"""Returns a path pointing to a Gitlab project, or None if the submodule
45-
is hosted elsewhere
46-
"""
4728
# check if the submodule url is a relative path to the project path
4829
if url.startswith('./') or url.startswith('../'):
4930
# we build the path of the submodule project using the path of
5031
# the current project
5132
url = rstrip(url, '.git')
52-
path_with_namespace = normpath(
53-
join(parent_project.path_with_namespace, url))
54-
return path_with_namespace
33+
path_with_namespace = normpath(join(
34+
submodule.parent_project.path_with_namespace,
35+
url
36+
))
37+
client: ProjectManager = submodule.parent_project.manager
38+
return client, path_with_namespace
5539

40+
# If URL is not relative: try parsing it
5641
parsed: GitUrlParsed = parse(url)
5742
if not parsed.valid:
5843
logger.warning(f'submodule git url does not seem to be valid: {url}')
5944
return None
6045

61-
# even if the parent project is hosted on a self-managed gitlab host,
62-
# it can still use submodules hosted on gitlab.com
63-
gitlab_hosts = ['gitlab']
64-
if self_managed_gitlab_host:
65-
if isinstance(self_managed_gitlab_host, str):
66-
gitlab_hosts.append(self_managed_gitlab_host)
67-
else:
68-
gitlab_hosts.extend(self_managed_gitlab_host)
46+
url_to_client = map_domain_to_clients(gls)
47+
domain_to_client = {
48+
host_url_to_domain(_url): client
49+
for _url, client in url_to_client.items()
50+
}
6951

70-
# giturlparse.GitUrlParsed.platform is too permissive and will be set to
71-
# 'gitlab' for some non-gitlab urls, for instance:
72-
# https://opensource.ncsa.illinois.edu/bitbucket/scm/u3d/3dutilities.git
73-
if (parsed.platform not in ('gitlab', 'base')
74-
or not any([re.match(fr'^{host}(\.\w+)?$', parsed.host)
75-
for host in gitlab_hosts])):
52+
matched_domain = [
53+
domain for domain in domain_to_client
54+
if re.search("(^|[/@])" + domain, url)
55+
]
56+
if len(matched_domain) == 0:
7657
logger.warning(f'submodule git url is not hosted on gitlab: {url}')
7758
return None
59+
elif len(matched_domain) > 1:
60+
raise ValueError(f"More than one of the provided Gitlab host domains "
61+
f"matches submodule url {url}")
62+
else:
63+
matched_domain = matched_domain[0]
64+
client = domain_to_client[matched_domain]
7865

7966
# Format to python-gitlab path_with_namespace:
8067
# rewrite to https format then split by host and keep & cut the right part.
8168
# I find it more robust than trying to rebuild the path from the different
8269
# attributes of giturlparse.GitUrlParsed objects
8370
https_url = parsed.url2https
84-
path_with_namespace = https_url.split(parsed.host)[1]
71+
path_with_namespace = https_url.split(matched_domain)[1]
8572
path_with_namespace = lstrip(path_with_namespace, '/')
8673
path_with_namespace = rstrip(path_with_namespace, '.git')
87-
return path_with_namespace
74+
return client, path_with_namespace
75+
76+
77+
def submodule_to_project(
78+
submodule: Submodule,
79+
gls: OneOrManyClients,
80+
) -> Optional[Project]:
81+
match = match_submodule_to_client_and_format_project_path(
82+
submodule=submodule,
83+
gls=gls
84+
)
85+
if not match:
86+
return None
87+
try:
88+
client, submodule_project_path_with_namespace = match
89+
submodule_project = client.get(submodule_project_path_with_namespace)
90+
except (GitlabGetError, GitlabHttpError):
91+
# Repo doesn't actually exist (possible because you can modify
92+
# .gitmodules without using `git submodule add`)
93+
raise FileNotFoundError(
94+
'No repo found at url "{}" for submodule at path "{}" - Check if '
95+
'the repo was deleted.'.format(submodule.url, submodule.path))
96+
return submodule_project
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from gitlab import Gitlab
2+
3+
from gitlab_submodule.project_manager_utils import get_host_url
4+
from gitlab_submodule.project_manager_utils import map_domain_to_clients
5+
6+
7+
def test_get_host_url():
8+
gl = Gitlab()
9+
assert get_host_url(gl.projects) == "https://gitlab.com"
10+
11+
12+
def test_map_domain_to_clients():
13+
gl1 = Gitlab()
14+
gl2 = Gitlab("myhost.com").projects
15+
mapped = map_domain_to_clients([gl1, gl2])
16+
assert mapped == {
17+
"https://gitlab.com": gl1.projects,
18+
"myhost.com": gl2
19+
}

tests/test_submodule_to_project.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,59 @@
11
from unittest import TestCase
2-
from unittest.mock import Mock
2+
from unittest.mock import MagicMock
33

4-
from gitlab_submodule.submodule_to_project import \
5-
_submodule_url_to_path_with_namespace
4+
from gitlab import Gitlab
5+
from gitlab.v4.objects import ProjectManager
6+
7+
from gitlab_submodule import Submodule
8+
from gitlab_submodule.submodule_to_project import host_url_to_domain
9+
from gitlab_submodule.submodule_to_project import (
10+
match_submodule_to_client_and_format_project_path)
11+
12+
13+
def test_host_url_to_domain():
14+
assert host_url_to_domain("https://myhost.com/") == "myhost.com"
615

716

817
class TestSubmoduleToProject(TestCase):
18+
19+
def mock_submodule(self, url: str) -> MagicMock:
20+
submodule = MagicMock(Submodule)
21+
submodule.url = url
22+
return submodule
23+
924
def test__submodule_url_to_path_with_namespace(self):
1025
# Normal gitlab host
11-
path_with_namespace = _submodule_url_to_path_with_namespace(
12-
'https://gitlab.com/namespace/repo.git',
13-
Mock())
26+
_, path_with_namespace = \
27+
match_submodule_to_client_and_format_project_path(
28+
self.mock_submodule('https://gitlab.com/namespace/repo.git'),
29+
gls=Gitlab()
30+
)
1431
self.assertEqual(path_with_namespace, 'namespace/repo')
1532

16-
# Self-managed gitlab URL without self_managed_gitlab_host
17-
path_with_namespace = _submodule_url_to_path_with_namespace(
18-
'https://custom-gitlab/namespace/repo.git',
19-
Mock())
20-
self.assertEqual(path_with_namespace, None)
33+
# Self-managed gitlab URL, wrong client
34+
match = match_submodule_to_client_and_format_project_path(
35+
self.mock_submodule('https://custom-gitlab/namespace/repo.git'),
36+
gls=Gitlab())
37+
self.assertEqual(match, None)
38+
39+
# Self-managed gitlab URL that includes the URL of the wrong client
40+
match = \
41+
match_submodule_to_client_and_format_project_path(
42+
self.mock_submodule(
43+
'https://custom-gitlab.com/namespace/repo.git'),
44+
gls=Gitlab()
45+
)
46+
self.assertEqual(match, None)
2147

2248
# Self-managed gitlab URL with self_managed_gitlab_host
23-
path_with_namespace = _submodule_url_to_path_with_namespace(
24-
'https://custom-gitlab/namespace/repo.git',
25-
Mock(),
26-
self_managed_gitlab_host='custom-gitlab')
49+
self_hosted_client = MagicMock(ProjectManager)
50+
self_hosted_client.gitlab = MagicMock(Gitlab)
51+
self_hosted_client.gitlab._base_url = "https://custom-gitlab.com"
52+
client, path_with_namespace = \
53+
match_submodule_to_client_and_format_project_path(
54+
self.mock_submodule(
55+
'https://custom-gitlab.com/namespace/repo.git'),
56+
gls=[Gitlab(), self_hosted_client],
57+
)
2758
self.assertEqual(path_with_namespace, 'namespace/repo')
59+
self.assertEqual(client, self_hosted_client)

0 commit comments

Comments
 (0)