Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/scm/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

type ErrorCode = Literal[
"malformed_external_id",
"missing_repository_path",
"path_is_directory",
"path_is_not_directory",
"provider_not_found",
Expand Down Expand Up @@ -38,6 +39,7 @@

ERROR_CODES: dict[ErrorCode, str] = {
"malformed_external_id": "The repository's external ID was malformed.",
"missing_repository_path": "No path was given for the repository.",
"path_is_directory": "The requested path is a directory, not a file.",
"path_is_not_directory": "The requested path is not a directory.",
"provider_not_found": "An unsupported integration provider was found.",
Expand Down Expand Up @@ -127,6 +129,10 @@ class MalformedExternalId(SCMCodedError):
code = "malformed_external_id"


class MissingRepositoryPath(SCMCodedError):
code = "missing_repository_path"


class PathIsDirectory(SCMCodedError):
code = "path_is_directory"

Expand Down
12 changes: 8 additions & 4 deletions src/scm/providers/gitlab/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from scm.errors import (
InvalidCheckRunStateTransition,
MalformedExternalId,
MissingRepositoryPath,
PathIsNotDirectory,
ReadmeNotFound,
ResourceBadRequest,
Expand Down Expand Up @@ -243,11 +244,14 @@ def __init__(self, client: ApiClient, organization_id: int, repository: Reposito

@property
def web_repository_path(self) -> str:
"""The repository's path on the instance (e.g. "gitlab-org/gitlab-foss"), for web URLs.

Sentry's name for a GitLab repository is its display name ("GitLab.org / GitLab FOSS"), which differs from
the path in more than spaces, so the path has to come from the repository.
"""
Normalize Sentry's GitLab full repo name (e.g., "my-group / my-team / my-subteam / my-project"
--> "my-group/my-team/my-subteam/my-project") for use in web URLs.
"""
return self.repository["name"].replace(" ", "")
if not (path := self.repository.get("path")):
raise MissingRepositoryPath(detail="The GitLab repository has no path.")
return path

def request(
self,
Expand Down
2 changes: 2 additions & 0 deletions src/scm/rpc/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def deserialize_repository(content: bytes) -> Repository:
"provider_name": repository.attributes.provider_name,
"web_base_url": repository.attributes.web_base_url,
"installation_id": repository.attributes.installation_id,
"path": repository.attributes.path,
}
return result

Expand All @@ -69,6 +70,7 @@ def serialize_repository(repository: Repository) -> bytes:
provider_name=repository["provider_name"],
web_base_url=repository["web_base_url"],
installation_id=repository["installation_id"],
path=repository.get("path"),
),
)
)
Expand Down
1 change: 1 addition & 0 deletions src/scm/rpc/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class RepositoryAttributes(msgspec.Struct):
provider_name: ProviderName
web_base_url: str | None = None
installation_id: str | None = None
path: str | None = None


class JsonApiData[Type, Attributes](msgspec.Struct):
Expand Down
3 changes: 3 additions & 0 deletions src/scm/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ class Repository(TypedDict):
web_base_url: str | None
# The SCM app's external id.
installation_id: str | None
# The repository's path on its provider's host, e.g. "gitlab-org/gitlab-foss". Only Sentry's GitLab
# repositories set it today, because their name is a display name ("GitLab.org / GitLab FOSS").
path: NotRequired[str | None]


class GitRepository(TypedDict):
Expand Down
33 changes: 18 additions & 15 deletions tests/unit/provider/test_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest

from scm.errors import (
MissingRepositoryPath,
RateLimitExceeded,
ResourceBadGateway,
ResourceBadRequest,
Expand Down Expand Up @@ -47,14 +48,17 @@ def client() -> ApiClient:
return unittest.mock.MagicMock(_name="client")


def _make_gitlab_provider(client: ApiClient, *, name: str = "test-repo") -> GitLabProvider:
def _make_gitlab_provider(
client: ApiClient, *, name: str = "Test Repo", path: str | None = "test-repo"
) -> GitLabProvider:
return GitLabProvider(
client=client,
organization_id=1,
repository=Repository(
id=1,
integration_id=1,
name=name,
path=path,
organization_id=1,
is_active=True,
external_id="gitlab.com:79787061",
Expand Down Expand Up @@ -13939,24 +13943,23 @@ def test_download_archive_zip_uses_zip_extension(client, provider: GitLabProvide
assert call.kwargs["path"] == "/projects/79787061/repository/archive.zip"


@pytest.mark.parametrize(
("repository_name", "expected_path"),
[
("test-org/test-repo", "test-org/test-repo"),
("my-group / my-project", "my-group/my-project"),
("my-group / my-subteam / my-project", "my-group/my-subteam/my-project"),
],
)
def test_web_repository_path_normalizes_namespaced_name(
client: ApiClient, repository_name: str, expected_path: str
) -> None:
provider = _make_gitlab_provider(client, name=repository_name)
def test_web_repository_path_is_the_repository_path_not_its_name(client: ApiClient) -> None:
# Sentry's name for a GitLab repository is its display name, which differs from the path in more than spaces.
provider = _make_gitlab_provider(client, name="GitLab.org / GitLab FOSS", path="gitlab-org/gitlab-foss")

assert provider.web_repository_path == "gitlab-org/gitlab-foss"


@pytest.mark.parametrize("path", [None, ""])
def test_web_repository_path_needs_the_repository_path(client: ApiClient, path: str | None) -> None:
provider = _make_gitlab_provider(client, name="GitLab.org / GitLab FOSS", path=path)

assert provider.web_repository_path == expected_path
with pytest.raises(MissingRepositoryPath):
provider.get_commit_url("abc123")


def test_web_repository_path_used_in_commit_and_mr_urls(client: ApiClient) -> None:
provider = _make_gitlab_provider(client, name="my-group / my-project")
provider = _make_gitlab_provider(client, name="My Group / My Project", path="my-group/my-project")

assert provider.get_commit_url("abc123") == "https://gitlab.com/my-group/my-project/-/commit/abc123"
assert provider.get_commits_url("abc123") == "https://gitlab.com/my-group/my-project/-/commits/abc123"
Expand Down
11 changes: 11 additions & 0 deletions tests/unit/test_rpc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@ def test_web_base_url_none_when_not_set(self):
decoded = deserialize_repository(serialize_repository(make_repository()))
assert decoded["web_base_url"] is None

def test_path_round_trips(self):
repo = make_repository(provider_name="gitlab", path="gitlab-org/gitlab-foss")
decoded = deserialize_repository(serialize_repository(repo))
assert decoded["path"] == "gitlab-org/gitlab-foss"

def test_path_none_when_the_server_omits_it(self):
payload = json.loads(serialize_repository(make_repository()))
del payload["data"]["attributes"]["path"]
decoded = deserialize_repository(json.dumps(payload).encode())
assert decoded["path"] is None

def test_installation_id_round_trips(self):
repo = make_repository(provider_name="cursor_origin", installation_id="inst_01example")
decoded = deserialize_repository(serialize_repository(repo))
Expand Down
Loading