Skip to content

Commit 90133a2

Browse files
committed
feat: Code References Added GitLab as a VCS provider
LiveReview Pre-Commit Check: skipped LiveReview Pre-Commit Check: skipped
1 parent c1aef00 commit 90133a2

7 files changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.2.13 on 2026-04-10 08:34
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("code_references", "0002_add_project_repo_created_index"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="featureflagcodereferencesscan",
15+
name="vcs_provider",
16+
field=models.CharField(
17+
choices=[("github", "GitHub"), ("gitlab", "GitLab")],
18+
default="github",
19+
max_length=50,
20+
),
21+
),
22+
]

api/projects/code_references/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Meta:
4141
"repository_url",
4242
"project",
4343
"revision",
44+
"vcs_provider",
4445
"code_references",
4546
]
4647
read_only_fields = [
@@ -67,3 +68,4 @@ class FeatureFlagCodeReferencesRepositoryCountSerializer(
6768
count = serializers.IntegerField()
6869
last_successful_repository_scanned_at = serializers.DateTimeField()
6970
last_feature_found_at = serializers.DateTimeField(allow_null=True)
71+
vcs_provider = serializers.ChoiceField(choices=VCSProvider.choices)

api/projects/code_references/services.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def annotate_feature_queryset_with_code_references_summary(
8484
count=F("count"),
8585
last_successful_repository_scanned_at=F("created_at"),
8686
last_feature_found_at=F("last_feature_found_at"),
87+
vcs_provider=F("vcs_provider"),
8788
),
8889
)
8990
)
@@ -162,6 +163,11 @@ def _get_permalink(
162163
f"{repository_url}/",
163164
f"blob/{revision}/{file_path}#L{line_number}",
164165
)
166+
case VCSProvider.GITLAB:
167+
return urljoin(
168+
f"{repository_url}/",
169+
f"-/blob/{revision}/{file_path}#L{line_number}",
170+
)
165171
raise NotImplementedError( # pragma: no cover
166172
f"Permalink generation for {provider} is not implemented."
167173
)

api/projects/code_references/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
class VCSProvider(TextChoices):
99
GITHUB = "github", "GitHub"
10+
GITLAB = "gitlab", "GitLab"
1011

1112

1213
class JSONCodeReference(TypedDict):
@@ -37,5 +38,6 @@ class FeatureFlagCodeReferencesRepositorySummary:
3738
class CodeReferencesRepositoryCount:
3839
repository_url: str
3940
count: int
41+
vcs_provider: str
4042
last_successful_repository_scanned_at: datetime
4143
last_feature_found_at: datetime | None

api/tests/unit/features/test_unit_features_views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3611,6 +3611,7 @@ def test_list_features__with_code_references__returns_counts(
36113611
project=project,
36123612
repository_url="https://gitlab.flagsmith.com/frontend/",
36133613
revision="frontend-1",
3614+
vcs_provider="gitlab",
36143615
code_references=[
36153616
{
36163617
"feature_name": feature.name,
@@ -3636,6 +3637,7 @@ def test_list_features__with_code_references__returns_counts(
36363637
project=project,
36373638
repository_url="https://gitlab.flagsmith.com/frontend/",
36383639
revision="frontend-2",
3640+
vcs_provider="gitlab",
36393641
code_references=[
36403642
{
36413643
"feature_name": feature.name,
@@ -3658,12 +3660,14 @@ def test_list_features__with_code_references__returns_counts(
36583660
assert response.json()["results"][0]["code_references_counts"] == [
36593661
{
36603662
"repository_url": "https://github.flagsmith.com/backend/",
3663+
"vcs_provider": "github",
36613664
"count": 0,
36623665
"last_successful_repository_scanned_at": "2099-01-02T14:00:00+00:00",
36633666
"last_feature_found_at": "2099-01-01T13:00:00+00:00",
36643667
},
36653668
{
36663669
"repository_url": "https://gitlab.flagsmith.com/frontend/",
3670+
"vcs_provider": "gitlab",
36673671
"count": 2,
36683672
"last_successful_repository_scanned_at": "2099-01-02T14:00:00+00:00",
36693673
"last_feature_found_at": "2099-01-02T14:00:00+00:00",

api/tests/unit/projects/code_references/test_unit_projects_code_references_services.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,53 @@ def test_get_permalink__private_github_repository__returns_valid_url(
5252
assert result == (
5353
"https://github.flagsmith.com/flagsmith/backend/blob/revision-hash/path/to/file.py#L10"
5454
)
55+
56+
57+
@pytest.mark.parametrize(
58+
"repository_url",
59+
[
60+
"https://gitlab.com/Flagsmith/flagsmith",
61+
"https://gitlab.com/Flagsmith/flagsmith/", # with trailing slash
62+
],
63+
)
64+
def test_get_permalink__public_gitlab_repository__returns_valid_url(
65+
repository_url: str,
66+
) -> None:
67+
# Given / When
68+
result = _get_permalink(
69+
provider=VCSProvider.GITLAB,
70+
repository_url=repository_url,
71+
revision="revision-hash",
72+
file_path="path/to/file.py",
73+
line_number=10,
74+
)
75+
76+
# Then
77+
assert result == (
78+
"https://gitlab.com/Flagsmith/flagsmith/-/blob/revision-hash/path/to/file.py#L10"
79+
)
80+
81+
82+
@pytest.mark.parametrize(
83+
"repository_url",
84+
[
85+
"https://gitlab.internal.flagsmith.com/flagsmith/backend",
86+
"https://gitlab.internal.flagsmith.com/flagsmith/backend/", # with trailing slash
87+
],
88+
)
89+
def test_get_permalink__private_gitlab_repository__returns_valid_url(
90+
repository_url: str,
91+
) -> None:
92+
# Given / When
93+
result = _get_permalink(
94+
provider=VCSProvider.GITLAB,
95+
repository_url=repository_url,
96+
revision="revision-hash",
97+
file_path="path/to/file.py",
98+
line_number=10,
99+
)
100+
101+
# Then
102+
assert result == (
103+
"https://gitlab.internal.flagsmith.com/flagsmith/backend/-/blob/revision-hash/path/to/file.py#L10"
104+
)

api/tests/unit/projects/code_references/test_unit_projects_code_references_views.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def test_create_code_reference__valid_payload__returns_201_with_accepted_referen
4545
assert len(response.data["code_references"]) == 3
4646
assert response.data["project"] == project.pk
4747
assert response.data["created_at"] == "2025-04-14T12:30:00Z"
48+
assert response.data["vcs_provider"] == "github"
49+
assert FeatureFlagCodeReferencesScan.objects.get().vcs_provider == "github"
4850
assert FeatureFlagCodeReferencesScan.objects.get().code_references == [
4951
{
5052
"feature_name": "feature-1",
@@ -381,3 +383,100 @@ def test_get_feature_code_references__feature_not_found__returns_404(
381383
# Then
382384
assert response.status_code == 404
383385
assert response.data["detail"] == "No Feature matches the given query."
386+
387+
388+
def test_create_code_reference__with_gitlab_provider__returns_201(
389+
admin_client_new: APIClient,
390+
project: Project,
391+
) -> None:
392+
# Given / When
393+
response = admin_client_new.post(
394+
f"/api/v1/projects/{project.pk}/code-references/",
395+
data={
396+
"repository_url": "https://gitlab.com/flagsmith",
397+
"revision": "revision-hash",
398+
"vcs_provider": "gitlab", # Explicitly testing GitLab
399+
"code_references": [
400+
{
401+
"feature_name": "feature-1",
402+
"file_path": "path/to/file1.py",
403+
"line_number": 10,
404+
},
405+
],
406+
},
407+
format="json",
408+
)
409+
410+
# Then
411+
assert response.status_code == 201
412+
assert response.data["vcs_provider"] == "gitlab"
413+
assert FeatureFlagCodeReferencesScan.objects.get().vcs_provider == "gitlab"
414+
415+
416+
def test_create_code_reference__with_invalid_provider__returns_400(
417+
admin_client_new: APIClient,
418+
project: Project,
419+
) -> None:
420+
# Given / When
421+
response = admin_client_new.post(
422+
f"/api/v1/projects/{project.pk}/code-references/",
423+
data={
424+
"repository_url": "https://bitbucket.org/flagsmith",
425+
"revision": "revision-hash",
426+
"vcs_provider": "bitbucket", # Invalid provider
427+
"code_references": [
428+
{
429+
"feature_name": "feature-1",
430+
"file_path": "path/to/file1.py",
431+
"line_number": 10,
432+
},
433+
],
434+
},
435+
format="json",
436+
)
437+
438+
# Then
439+
assert response.status_code == 400
440+
assert "vcs_provider" in response.data
441+
assert not FeatureFlagCodeReferencesScan.objects.exists()
442+
443+
444+
def test_get_feature_code_references__gitlab_scan__returns_expected_permalinks(
445+
admin_client_new: APIClient,
446+
feature: Feature,
447+
project: Project,
448+
) -> None:
449+
# Given
450+
with freezegun.freeze_time("2099-01-01T10:00:00-0300"):
451+
FeatureFlagCodeReferencesScan.objects.create(
452+
project=project,
453+
repository_url="https://gitlab.com/flagsmith/backend",
454+
revision="backend-1",
455+
vcs_provider="gitlab",
456+
code_references=[
457+
{
458+
"feature_name": feature.name,
459+
"file_path": "backend/file1.py",
460+
"line_number": 20,
461+
},
462+
],
463+
)
464+
465+
# When
466+
response = admin_client_new.get(
467+
f"/api/v1/projects/{project.pk}/features/{feature.pk}/code-references/",
468+
)
469+
470+
# Then
471+
assert response.status_code == 200
472+
473+
response_data = response.json()
474+
assert len(response_data) == 1
475+
assert response_data[0]["vcs_provider"] == "gitlab"
476+
477+
# Assert the permalink uses the GitLab /-/blob/ format
478+
permalink = response_data[0]["code_references"][0]["permalink"]
479+
assert (
480+
permalink
481+
== "https://gitlab.com/flagsmith/backend/-/blob/backend-1/backend/file1.py#L20"
482+
)

0 commit comments

Comments
 (0)