Skip to content

feat(api): add project templates #3057

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 10, 2024
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
8 changes: 8 additions & 0 deletions docs/gl_objects/merge_requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ Get a single MR::
mr = project.mergerequests.get(mr_iid)

Get MR reviewer details::

mr = project.mergerequests.get(mr_iid)
reviewers = mr.reviewer_details.list()

Expand All @@ -105,6 +106,13 @@ Create a MR::
'title': 'merge cool feature',
'labels': ['label1', 'label2']})

# Use a project MR description template
mr_description_template = project.merge_request_templates.get("Default")
mr = project.mergerequests.create({'source_branch': 'cool_feature',
'target_branch': 'main',
'title': 'merge cool feature',
'description': mr_description_template.content})

Update a MR::

mr.description = 'New description'
Expand Down
72 changes: 71 additions & 1 deletion docs/gl_objects/templates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Reference
+ :class:`gitlab.v4.objects.DockerfileManager`
+ :attr:`gitlab.Gitlab.gitlabciymls`

* GitLab API: Not documented.
* GitLab API: https://docs.gitlab.com/ce/api/templates/dockerfiles.html

Examples
--------
Expand All @@ -112,3 +112,73 @@ Get a Dockerfile template::

dockerfile = gl.dockerfiles.get('Python')
print(dockerfile.content)

Project templates
=========================

These templates are project-specific versions of the templates above, as
well as issue and merge request templates.

Reference
---------

* v4 API:

+ :class:`gitlab.v4.objects.ProjectLicenseTemplate`
+ :class:`gitlab.v4.objects.ProjectLicenseTemplateManager`
+ :attr:`gitlab.v4.objects.Project.license_templates`
+ :class:`gitlab.v4.objects.ProjectGitignoreTemplate`
+ :class:`gitlab.v4.objects.ProjectGitignoreTemplateManager`
+ :attr:`gitlab.v4.objects.Project.gitignore_templates`
+ :class:`gitlab.v4.objects.ProjectGitlabciymlTemplate`
+ :class:`gitlab.v4.objects.ProjectGitlabciymlTemplateManager`
+ :attr:`gitlab.v4.objects.Project.gitlabciyml_templates`
+ :class:`gitlab.v4.objects.ProjectDockerfileTemplate`
+ :class:`gitlab.v4.objects.ProjectDockerfileTemplateManager`
+ :attr:`gitlab.v4.objects.Project.dockerfile_templates`
+ :class:`gitlab.v4.objects.ProjectIssueTemplate`
+ :class:`gitlab.v4.objects.ProjectIssueTemplateManager`
+ :attr:`gitlab.v4.objects.Project.issue_templates`
+ :class:`gitlab.v4.objects.ProjectMergeRequestTemplate`
+ :class:`gitlab.v4.objects.ProjectMergeRequestTemplateManager`
+ :attr:`gitlab.v4.objects.Project.merge_request_templates`

* GitLab API: https://docs.gitlab.com/ce/api/project_templates.html

Examples
--------

List known project templates::

license_templates = project.license_templates.list()
gitignore_templates = project.gitignore_templates.list()
gitlabciyml_templates = project.gitlabciyml_templates.list()
dockerfile_templates = project.dockerfile_templates.list()
issue_templates = project.issue_templates.list()
merge_request_templates = project.merge_request_templates.list()

Get project templates::

license_template = project.license_templates.get('apache-2.0')
gitignore_template = project.gitignore_templates.get('Python')
gitlabciyml_template = project.gitlabciyml_templates.get('Pelican')
dockerfile_template = project.dockerfile_templates.get('Python')
issue_template = project.issue_templates.get('Default')
merge_request_template = project.merge_request_templates.get('Default')

print(license_template.content)
print(gitignore_template.content)
print(gitlabciyml_template.content)
print(dockerfile_template.content)
print(issue_template.content)
print(merge_request_template.content)

Create an issue or merge request using a description template::

issue = project.issues.create({'title': 'I have a bug',
'description': issue_template.content})
mr = project.mergerequests.create({'source_branch': 'cool_feature',
'target_branch': 'main',
'title': 'merge cool feature',
'description': merge_request_template.content})

14 changes: 14 additions & 0 deletions gitlab/v4/objects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@
ProjectIssuesStatisticsManager,
)
from .tags import ProjectProtectedTagManager, ProjectTagManager # noqa: F401
from .templates import ( # noqa: F401
ProjectDockerfileTemplateManager,
ProjectGitignoreTemplateManager,
ProjectGitlabciymlTemplateManager,
ProjectIssueTemplateManager,
ProjectLicenseTemplateManager,
ProjectMergeRequestTemplateManager,
)
from .triggers import ProjectTriggerManager # noqa: F401
from .users import ProjectUserManager # noqa: F401
from .variables import ProjectVariableManager # noqa: F401
Expand Down Expand Up @@ -189,27 +197,33 @@ class Project(
customattributes: ProjectCustomAttributeManager
deployments: ProjectDeploymentManager
deploytokens: ProjectDeployTokenManager
dockerfile_templates: ProjectDockerfileTemplateManager
environments: ProjectEnvironmentManager
events: ProjectEventManager
exports: ProjectExportManager
files: ProjectFileManager
forks: "ProjectForkManager"
generic_packages: GenericPackageManager
gitignore_templates: ProjectGitignoreTemplateManager
gitlabciyml_templates: ProjectGitlabciymlTemplateManager
groups: ProjectGroupManager
hooks: ProjectHookManager
imports: ProjectImportManager
integrations: ProjectIntegrationManager
invitations: ProjectInvitationManager
issues: ProjectIssueManager
issue_templates: ProjectIssueTemplateManager
issues_statistics: ProjectIssuesStatisticsManager
iterations: ProjectIterationManager
jobs: ProjectJobManager
job_token_scope: ProjectJobTokenScopeManager
keys: ProjectKeyManager
labels: ProjectLabelManager
license_templates: ProjectLicenseTemplateManager
members: ProjectMemberManager
members_all: ProjectMemberAllManager
mergerequests: ProjectMergeRequestManager
merge_request_templates: ProjectMergeRequestTemplateManager
merge_trains: ProjectMergeTrainManager
milestones: ProjectMilestoneManager
notes: ProjectNoteManager
Expand Down
104 changes: 104 additions & 0 deletions gitlab/v4/objects/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@
"GitlabciymlManager",
"License",
"LicenseManager",
"ProjectDockerfileTemplate",
"ProjectDockerfileTemplateManager",
"ProjectGitignoreTemplate",
"ProjectGitignoreTemplateManager",
"ProjectGitlabciymlTemplate",
"ProjectGitlabciymlTemplateManager",
"ProjectIssueTemplate",
"ProjectIssueTemplateManager",
"ProjectLicenseTemplate",
"ProjectLicenseTemplateManager",
"ProjectMergeRequestTemplate",
"ProjectMergeRequestTemplateManager",
]


Expand Down Expand Up @@ -65,3 +77,95 @@ class LicenseManager(RetrieveMixin, RESTManager):

def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> License:
return cast(License, super().get(id=id, lazy=lazy, **kwargs))


class ProjectDockerfileTemplate(RESTObject):
_id_attr = "name"


class ProjectDockerfileTemplateManager(RetrieveMixin, RESTManager):
_path = "/projects/{project_id}/templates/dockerfiles"
_obj_cls = ProjectDockerfileTemplate
_from_parent_attrs = {"project_id": "id"}

def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectDockerfileTemplate:
return cast(ProjectDockerfileTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectGitignoreTemplate(RESTObject):
_id_attr = "name"


class ProjectGitignoreTemplateManager(RetrieveMixin, RESTManager):
_path = "/projects/{project_id}/templates/gitignores"
_obj_cls = ProjectGitignoreTemplate
_from_parent_attrs = {"project_id": "id"}

def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectGitignoreTemplate:
return cast(ProjectGitignoreTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectGitlabciymlTemplate(RESTObject):
_id_attr = "name"


class ProjectGitlabciymlTemplateManager(RetrieveMixin, RESTManager):
_path = "/projects/{project_id}/templates/gitlab_ci_ymls"
_obj_cls = ProjectGitlabciymlTemplate
_from_parent_attrs = {"project_id": "id"}

def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectGitlabciymlTemplate:
return cast(ProjectGitlabciymlTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectLicenseTemplate(RESTObject):
_id_attr = "key"


class ProjectLicenseTemplateManager(RetrieveMixin, RESTManager):
_path = "/projects/{project_id}/templates/licenses"
_obj_cls = ProjectLicenseTemplate
_from_parent_attrs = {"project_id": "id"}

def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectLicenseTemplate:
return cast(ProjectLicenseTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectIssueTemplate(RESTObject):
_id_attr = "name"


class ProjectIssueTemplateManager(RetrieveMixin, RESTManager):
_path = "/projects/{project_id}/templates/issues"
_obj_cls = ProjectIssueTemplate
_from_parent_attrs = {"project_id": "id"}

def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectIssueTemplate:
return cast(ProjectIssueTemplate, super().get(id=id, lazy=lazy, **kwargs))


class ProjectMergeRequestTemplate(RESTObject):
_id_attr = "name"


class ProjectMergeRequestTemplateManager(RetrieveMixin, RESTManager):
_path = "/projects/{project_id}/templates/merge_requests"
_obj_cls = ProjectMergeRequestTemplate
_from_parent_attrs = {"project_id": "id"}

def get(
self, id: Union[str, int], lazy: bool = False, **kwargs: Any
) -> ProjectMergeRequestTemplate:
return cast(
ProjectMergeRequestTemplate, super().get(id=id, lazy=lazy, **kwargs)
)
106 changes: 106 additions & 0 deletions tests/unit/objects/test_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Gitlab API:
https://docs.gitlab.com/ce/api/templates/dockerfiles.html
https://docs.gitlab.com/ce/api/templates/gitignores.html
https://docs.gitlab.com/ce/api/templates/gitlab_ci_ymls.html
https://docs.gitlab.com/ce/api/templates/licenses.html
https://docs.gitlab.com/ce/api/project_templates.html
"""

import pytest
import responses

from gitlab.v4.objects import (
Dockerfile,
Gitignore,
Gitlabciyml,
License,
ProjectDockerfileTemplate,
ProjectGitignoreTemplate,
ProjectGitlabciymlTemplate,
ProjectIssueTemplate,
ProjectLicenseTemplate,
ProjectMergeRequestTemplate,
)


@pytest.mark.parametrize(
"tmpl, tmpl_mgr, tmpl_path",
[
(Dockerfile, "dockerfiles", "dockerfiles"),
(Gitignore, "gitignores", "gitignores"),
(Gitlabciyml, "gitlabciymls", "gitlab_ci_ymls"),
(License, "licenses", "licenses"),
],
ids=[
"dockerfile",
"gitignore",
"gitlabciyml",
"license",
],
)
def test_get_template(gl, tmpl, tmpl_mgr, tmpl_path):
tmpl_id = "sample"
tmpl_content = {"name": tmpl_id, "content": "Sample template content"}

# License templates have 'key' as the id attribute, so ensure
# this is included in the response content
if tmpl == License:
tmpl_id = "smpl"
tmpl_content.update({"key": tmpl_id})

path = f"templates/{tmpl_path}/{tmpl_id}"
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.GET,
url=f"http://localhost/api/v4/{path}",
json=tmpl_content,
)

template = getattr(gl, tmpl_mgr).get(tmpl_id)

assert isinstance(template, tmpl)
assert getattr(template, template._id_attr) == tmpl_id


@pytest.mark.parametrize(
"tmpl, tmpl_mgr, tmpl_path",
[
(ProjectDockerfileTemplate, "dockerfile_templates", "dockerfiles"),
(ProjectGitignoreTemplate, "gitignore_templates", "gitignores"),
(ProjectGitlabciymlTemplate, "gitlabciyml_templates", "gitlab_ci_ymls"),
(ProjectLicenseTemplate, "license_templates", "licenses"),
(ProjectIssueTemplate, "issue_templates", "issues"),
(ProjectMergeRequestTemplate, "merge_request_templates", "merge_requests"),
],
ids=[
"dockerfile",
"gitignore",
"gitlabciyml",
"license",
"issue",
"mergerequest",
],
)
def test_get_project_template(project, tmpl, tmpl_mgr, tmpl_path):
tmpl_id = "sample"
tmpl_content = {"name": tmpl_id, "content": "Sample template content"}

# ProjectLicenseTemplate templates have 'key' as the id attribute, so ensure
# this is included in the response content
if tmpl == ProjectLicenseTemplate:
tmpl_id = "smpl"
tmpl_content.update({"key": tmpl_id})

path = f"projects/{project.id}/templates/{tmpl_path}/{tmpl_id}"
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.GET,
url=f"http://localhost/api/v4/{path}",
json=tmpl_content,
)

template = getattr(project, tmpl_mgr).get(tmpl_id)

assert isinstance(template, tmpl)
assert getattr(template, template._id_attr) == tmpl_id
Loading