Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 5.2.13 on 2026-04-10 08:34

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("code_references", "0002_add_project_repo_created_index"),
]

operations = [
migrations.AlterField(
model_name="featureflagcodereferencesscan",
name="vcs_provider",
field=models.CharField(
choices=[("github", "GitHub"), ("gitlab", "GitLab")],
default="github",
max_length=50,
),
),
]
2 changes: 2 additions & 0 deletions api/projects/code_references/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class Meta:
"repository_url",
"project",
"revision",
"vcs_provider",
"code_references",
]
read_only_fields = [
Expand All @@ -67,3 +68,4 @@ class FeatureFlagCodeReferencesRepositoryCountSerializer(
count = serializers.IntegerField()
last_successful_repository_scanned_at = serializers.DateTimeField()
last_feature_found_at = serializers.DateTimeField(allow_null=True)
vcs_provider = serializers.ChoiceField(choices=VCSProvider.choices)
6 changes: 6 additions & 0 deletions api/projects/code_references/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def annotate_feature_queryset_with_code_references_summary(
count=F("count"),
last_successful_repository_scanned_at=F("created_at"),
last_feature_found_at=F("last_feature_found_at"),
vcs_provider=F("vcs_provider"),
),
)
)
Expand Down Expand Up @@ -162,6 +163,11 @@ def _get_permalink(
f"{repository_url}/",
f"blob/{revision}/{file_path}#L{line_number}",
)
case VCSProvider.GITLAB:
return urljoin(
f"{repository_url}/",
f"-/blob/{revision}/{file_path}#L{line_number}",
)
raise NotImplementedError( # pragma: no cover
f"Permalink generation for {provider} is not implemented."
)
2 changes: 2 additions & 0 deletions api/projects/code_references/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

class VCSProvider(TextChoices):
GITHUB = "github", "GitHub"
GITLAB = "gitlab", "GitLab"


class JSONCodeReference(TypedDict):
Expand Down Expand Up @@ -37,5 +38,6 @@ class FeatureFlagCodeReferencesRepositorySummary:
class CodeReferencesRepositoryCount:
repository_url: str
count: int
vcs_provider: str
last_successful_repository_scanned_at: datetime
last_feature_found_at: datetime | None
4 changes: 4 additions & 0 deletions api/tests/unit/features/test_unit_features_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3611,6 +3611,7 @@ def test_list_features__with_code_references__returns_counts(
project=project,
repository_url="https://gitlab.flagsmith.com/frontend/",
revision="frontend-1",
vcs_provider="gitlab",
code_references=[
{
"feature_name": feature.name,
Expand All @@ -3636,6 +3637,7 @@ def test_list_features__with_code_references__returns_counts(
project=project,
repository_url="https://gitlab.flagsmith.com/frontend/",
revision="frontend-2",
vcs_provider="gitlab",
code_references=[
{
"feature_name": feature.name,
Expand All @@ -3658,12 +3660,14 @@ def test_list_features__with_code_references__returns_counts(
assert response.json()["results"][0]["code_references_counts"] == [
{
"repository_url": "https://github.flagsmith.com/backend/",
"vcs_provider": "github",
"count": 0,
"last_successful_repository_scanned_at": "2099-01-02T14:00:00+00:00",
"last_feature_found_at": "2099-01-01T13:00:00+00:00",
},
{
"repository_url": "https://gitlab.flagsmith.com/frontend/",
"vcs_provider": "gitlab",
"count": 2,
"last_successful_repository_scanned_at": "2099-01-02T14:00:00+00:00",
"last_feature_found_at": "2099-01-02T14:00:00+00:00",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,53 @@ def test_get_permalink__private_github_repository__returns_valid_url(
assert result == (
"https://github.flagsmith.com/flagsmith/backend/blob/revision-hash/path/to/file.py#L10"
)


@pytest.mark.parametrize(
"repository_url",
[
"https://gitlab.com/Flagsmith/flagsmith",
"https://gitlab.com/Flagsmith/flagsmith/", # with trailing slash
],
)
def test_get_permalink__public_gitlab_repository__returns_valid_url(
repository_url: str,
) -> None:
# Given / When
result = _get_permalink(
provider=VCSProvider.GITLAB,
repository_url=repository_url,
revision="revision-hash",
file_path="path/to/file.py",
line_number=10,
)

# Then
assert result == (
"https://gitlab.com/Flagsmith/flagsmith/-/blob/revision-hash/path/to/file.py#L10"
)


@pytest.mark.parametrize(
"repository_url",
[
"https://gitlab.internal.flagsmith.com/flagsmith/backend",
"https://gitlab.internal.flagsmith.com/flagsmith/backend/", # with trailing slash
],
)
def test_get_permalink__private_gitlab_repository__returns_valid_url(
repository_url: str,
) -> None:
# Given / When
result = _get_permalink(
provider=VCSProvider.GITLAB,
repository_url=repository_url,
revision="revision-hash",
file_path="path/to/file.py",
line_number=10,
)

# Then
assert result == (
"https://gitlab.internal.flagsmith.com/flagsmith/backend/-/blob/revision-hash/path/to/file.py#L10"
)
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def test_create_code_reference__valid_payload__returns_201_with_accepted_referen
assert len(response.data["code_references"]) == 3
assert response.data["project"] == project.pk
assert response.data["created_at"] == "2025-04-14T12:30:00Z"
assert response.data["vcs_provider"] == "github"
assert FeatureFlagCodeReferencesScan.objects.get().vcs_provider == "github"
assert FeatureFlagCodeReferencesScan.objects.get().code_references == [
{
"feature_name": "feature-1",
Expand Down Expand Up @@ -381,3 +383,100 @@ def test_get_feature_code_references__feature_not_found__returns_404(
# Then
assert response.status_code == 404
assert response.data["detail"] == "No Feature matches the given query."


def test_create_code_reference__with_gitlab_provider__returns_201(
admin_client_new: APIClient,
project: Project,
) -> None:
# Given / When
response = admin_client_new.post(
f"/api/v1/projects/{project.pk}/code-references/",
data={
"repository_url": "https://gitlab.com/flagsmith",
"revision": "revision-hash",
"vcs_provider": "gitlab", # Explicitly testing GitLab
"code_references": [
{
"feature_name": "feature-1",
"file_path": "path/to/file1.py",
"line_number": 10,
},
],
},
format="json",
)

# Then
assert response.status_code == 201
assert response.data["vcs_provider"] == "gitlab"
assert FeatureFlagCodeReferencesScan.objects.get().vcs_provider == "gitlab"


def test_create_code_reference__with_invalid_provider__returns_400(
admin_client_new: APIClient,
project: Project,
) -> None:
# Given / When
response = admin_client_new.post(
f"/api/v1/projects/{project.pk}/code-references/",
data={
"repository_url": "https://bitbucket.org/flagsmith",
"revision": "revision-hash",
"vcs_provider": "bitbucket", # Invalid provider
"code_references": [
{
"feature_name": "feature-1",
"file_path": "path/to/file1.py",
"line_number": 10,
},
],
},
format="json",
)

# Then
assert response.status_code == 400
assert "vcs_provider" in response.data
assert not FeatureFlagCodeReferencesScan.objects.exists()


def test_get_feature_code_references__gitlab_scan__returns_expected_permalinks(
admin_client_new: APIClient,
feature: Feature,
project: Project,
) -> None:
# Given
with freezegun.freeze_time("2099-01-01T10:00:00-0300"):
FeatureFlagCodeReferencesScan.objects.create(
project=project,
repository_url="https://gitlab.com/flagsmith/backend",
revision="backend-1",
vcs_provider="gitlab",
code_references=[
{
"feature_name": feature.name,
"file_path": "backend/file1.py",
"line_number": 20,
},
],
)

# When
response = admin_client_new.get(
f"/api/v1/projects/{project.pk}/features/{feature.pk}/code-references/",
)

# Then
assert response.status_code == 200

response_data = response.json()
assert len(response_data) == 1
assert response_data[0]["vcs_provider"] == "gitlab"

# Assert the permalink uses the GitLab /-/blob/ format
permalink = response_data[0]["code_references"][0]["permalink"]
assert (
permalink
== "https://gitlab.com/flagsmith/backend/-/blob/backend-1/backend/file1.py#L20"
)
Loading