Skip to content

Commit 84a9dff

Browse files
committed
feat: Code References Added GitLab as a VCS provider
LiveReview Pre-Commit Check: skipped
1 parent 1641e45 commit 84a9dff

File tree

7 files changed

+181
-1
lines changed

7 files changed

+181
-1
lines changed
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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

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

1212
class JSONCodeReference(TypedDict):
1313
feature_name: str
@@ -37,5 +37,6 @@ class FeatureFlagCodeReferencesRepositorySummary:
3737
class CodeReferencesRepositoryCount:
3838
repository_url: str
3939
count: int
40+
vcs_provider: str
4041
last_successful_repository_scanned_at: datetime
4142
last_feature_found_at: datetime | None

api/tests/unit/features/test_unit_features_views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3616,6 +3616,7 @@ def test_list_features__with_code_references__returns_counts(
36163616
"feature_name": feature.name,
36173617
"file_path": "path/to/file.js",
36183618
"line_number": 23,
3619+
"vcs_provider": "gitlab",
36193620
},
36203621
],
36213622
)
@@ -3641,6 +3642,7 @@ def test_list_features__with_code_references__returns_counts(
36413642
"feature_name": feature.name,
36423643
"file_path": "path/to/file.js",
36433644
"line_number": 23,
3645+
"vcs_provider": "gitlab",
36443646
},
36453647
{
36463648
"feature_name": feature.name,
@@ -3658,12 +3660,15 @@ 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",
3667+
36643668
},
36653669
{
36663670
"repository_url": "https://gitlab.flagsmith.com/frontend/",
3671+
"vcs_provider": "gitlab",
36673672
"count": 2,
36683673
"last_successful_repository_scanned_at": "2099-01-02T14:00:00+00:00",
36693674
"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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,52 @@ 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+
@pytest.mark.parametrize(
57+
"repository_url",
58+
[
59+
"https://gitlab.com/Flagsmith/flagsmith",
60+
"https://gitlab.com/Flagsmith/flagsmith/", # with trailing slash
61+
],
62+
)
63+
def test_get_permalink__public_gitlab_repository__returns_valid_url(
64+
repository_url: str,
65+
) -> None:
66+
# Given / When
67+
result = _get_permalink(
68+
provider=VCSProvider.GITLAB,
69+
repository_url=repository_url,
70+
revision="revision-hash",
71+
file_path="path/to/file.py",
72+
line_number=10,
73+
)
74+
75+
# Then
76+
assert result == (
77+
"https://gitlab.com/Flagsmith/flagsmith/-/blob/revision-hash/path/to/file.py#L10"
78+
)
79+
80+
81+
@pytest.mark.parametrize(
82+
"repository_url",
83+
[
84+
"https://gitlab.internal.flagsmith.com/flagsmith/backend",
85+
"https://gitlab.internal.flagsmith.com/flagsmith/backend/", # with trailing slash
86+
],
87+
)
88+
def test_get_permalink__private_gitlab_repository__returns_valid_url(
89+
repository_url: str,
90+
) -> None:
91+
# Given / When
92+
result = _get_permalink(
93+
provider=VCSProvider.GITLAB,
94+
repository_url=repository_url,
95+
revision="revision-hash",
96+
file_path="path/to/file.py",
97+
line_number=10,
98+
)
99+
100+
# Then
101+
assert result == (
102+
"https://gitlab.internal.flagsmith.com/flagsmith/backend/-/blob/revision-hash/path/to/file.py#L10"
103+
)

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

Lines changed: 95 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,96 @@ 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+
def test_create_code_reference__with_gitlab_provider__returns_201(
388+
admin_client_new: APIClient,
389+
project: Project,
390+
) -> None:
391+
# Given / When
392+
response = admin_client_new.post(
393+
f"/api/v1/projects/{project.pk}/code-references/",
394+
data={
395+
"repository_url": "https://gitlab.com/flagsmith",
396+
"revision": "revision-hash",
397+
"vcs_provider": "gitlab", # Explicitly testing GitLab
398+
"code_references": [
399+
{
400+
"feature_name": "feature-1",
401+
"file_path": "path/to/file1.py",
402+
"line_number": 10,
403+
},
404+
],
405+
},
406+
format="json",
407+
)
408+
409+
# Then
410+
assert response.status_code == 201
411+
assert response.data["vcs_provider"] == "gitlab"
412+
assert FeatureFlagCodeReferencesScan.objects.get().vcs_provider == "gitlab"
413+
414+
415+
def test_create_code_reference__with_invalid_provider__returns_400(
416+
admin_client_new: APIClient,
417+
project: Project,
418+
) -> None:
419+
# Given / When
420+
response = admin_client_new.post(
421+
f"/api/v1/projects/{project.pk}/code-references/",
422+
data={
423+
"repository_url": "https://bitbucket.org/flagsmith",
424+
"revision": "revision-hash",
425+
"vcs_provider": "bitbucket", # Invalid provider
426+
"code_references": [
427+
{
428+
"feature_name": "feature-1",
429+
"file_path": "path/to/file1.py",
430+
"line_number": 10,
431+
},
432+
],
433+
},
434+
format="json",
435+
)
436+
437+
# Then
438+
assert response.status_code == 400
439+
assert "vcs_provider" in response.data
440+
assert not FeatureFlagCodeReferencesScan.objects.exists()
441+
442+
443+
def test_get_feature_code_references__gitlab_scan__returns_expected_permalinks(
444+
admin_client_new: APIClient,
445+
feature: Feature,
446+
project: Project,
447+
) -> None:
448+
# Given
449+
with freezegun.freeze_time("2099-01-01T10:00:00-0300"):
450+
FeatureFlagCodeReferencesScan.objects.create(
451+
project=project,
452+
repository_url="https://gitlab.com/flagsmith/backend",
453+
revision="backend-1",
454+
vcs_provider="gitlab",
455+
code_references=[
456+
{
457+
"feature_name": feature.name,
458+
"file_path": "backend/file1.py",
459+
"line_number": 20,
460+
},
461+
],
462+
)
463+
464+
# When
465+
response = admin_client_new.get(
466+
f"/api/v1/projects/{project.pk}/features/{feature.pk}/code-references/",
467+
)
468+
469+
# Then
470+
assert response.status_code == 200
471+
472+
response_data = response.json()
473+
assert len(response_data) == 1
474+
assert response_data[0]["vcs_provider"] == "gitlab"
475+
476+
# Assert the permalink uses the GitLab /-/blob/ format
477+
permalink = response_data[0]["code_references"][0]["permalink"]
478+
assert permalink == "https://gitlab.com/flagsmith/backend/-/blob/backend-1/backend/file1.py#L20"

0 commit comments

Comments
 (0)