gitlabform 4.0.0__tar.gz → 4.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. {gitlabform-4.0.0 → gitlabform-4.0.2}/PKG-INFO +2 -2
  2. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/__init__.py +0 -4
  3. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/multiple_entities_processor.py +16 -1
  4. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/__init__.py +7 -1
  5. gitlabform-4.0.2/gitlabform/processors/project/branches_processor.py +151 -0
  6. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/files_processor.py +118 -81
  7. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform.egg-info/PKG-INFO +2 -2
  8. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform.egg-info/SOURCES.txt +0 -4
  9. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform.egg-info/requires.txt +1 -1
  10. {gitlabform-4.0.0 → gitlabform-4.0.2}/setup.py +1 -1
  11. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/__init__.py +66 -5
  12. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_branches.py +10 -0
  13. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_files.py +23 -1
  14. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_transfer_project.py +8 -3
  15. gitlabform-4.0.0/gitlabform/gitlab/branches.py +0 -110
  16. gitlabform-4.0.0/gitlabform/gitlab/repositories.py +0 -64
  17. gitlabform-4.0.0/gitlabform/processors/project/branches_processor.py +0 -15
  18. gitlabform-4.0.0/gitlabform/processors/util/branch_protector.py +0 -358
  19. gitlabform-4.0.0/tests/unit/processors/test_branch_protector.py +0 -27
  20. {gitlabform-4.0.0 → gitlabform-4.0.2}/LICENSE +0 -0
  21. {gitlabform-4.0.0 → gitlabform-4.0.2}/README.md +0 -0
  22. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/__init__.py +0 -0
  23. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/configuration/__init__.py +0 -0
  24. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/configuration/common.py +0 -0
  25. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/configuration/core.py +0 -0
  26. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/configuration/groups.py +0 -0
  27. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/configuration/projects.py +0 -0
  28. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/configuration/transform.py +0 -0
  29. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/constants.py +0 -0
  30. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/commits.py +0 -0
  31. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/core.py +0 -0
  32. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/group_badges.py +0 -0
  33. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/group_ldap_links.py +0 -0
  34. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/group_variables.py +0 -0
  35. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/groups.py +0 -0
  36. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/merge_requests.py +0 -0
  37. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/pipelines.py +0 -0
  38. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/project_badges.py +0 -0
  39. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/project_deploy_keys.py +0 -0
  40. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/project_merge_requests_approvals.py +0 -0
  41. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/project_protected_environments.py +0 -0
  42. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/projects.py +0 -0
  43. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/python_gitlab.py +0 -0
  44. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/users.py +0 -0
  45. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/gitlab/variables.py +0 -0
  46. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/lists/__init__.py +0 -0
  47. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/lists/filter.py +0 -0
  48. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/lists/groups.py +0 -0
  49. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/lists/projects.py +0 -0
  50. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/output.py +0 -0
  51. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/__init__.py +0 -0
  52. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/abstract_processor.py +0 -0
  53. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/application/__init__.py +0 -0
  54. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/application/application_settings_processor.py +0 -0
  55. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/defining_keys.py +0 -0
  56. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/__init__.py +0 -0
  57. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/group_badges_processor.py +0 -0
  58. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/group_labels_processor.py +0 -0
  59. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/group_ldap_links_processor.py +0 -0
  60. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/group_members_processor.py +0 -0
  61. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/group_saml_links_processor.py +0 -0
  62. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/group_settings_processor.py +0 -0
  63. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/group/group_variables_processor.py +0 -0
  64. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/badges_processor.py +0 -0
  65. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/deploy_keys_processor.py +0 -0
  66. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/hooks_processor.py +0 -0
  67. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/integrations_processor.py +0 -0
  68. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/job_token_scope_processor.py +0 -0
  69. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/members_processor.py +0 -0
  70. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/merge_requests_approval_rules.py +0 -0
  71. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/merge_requests_approvals.py +0 -0
  72. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/project_labels_processor.py +0 -0
  73. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/project_processor.py +0 -0
  74. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/project_push_rules_processor.py +0 -0
  75. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/project_settings_processor.py +0 -0
  76. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/resource_groups_processor.py +0 -0
  77. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/schedules_processor.py +0 -0
  78. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/tags_processor.py +0 -0
  79. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/project/variables_processor.py +0 -0
  80. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/shared/__init__.py +0 -0
  81. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/shared/protected_environments_processor.py +0 -0
  82. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/single_entity_processor.py +0 -0
  83. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/util/__init__.py +0 -0
  84. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/util/decorators.py +0 -0
  85. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/util/difference_logger.py +0 -0
  86. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/processors/util/labels_processor.py +0 -0
  87. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/run.py +0 -0
  88. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform/util.py +0 -0
  89. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform.egg-info/dependency_links.txt +0 -0
  90. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform.egg-info/entry_points.txt +0 -0
  91. {gitlabform-4.0.0 → gitlabform-4.0.2}/gitlabform.egg-info/top_level.txt +0 -0
  92. {gitlabform-4.0.0 → gitlabform-4.0.2}/setup.cfg +0 -0
  93. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/__init__.py +0 -0
  94. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/conftest.py +0 -0
  95. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/__init__.py +0 -0
  96. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_application_settings.py +0 -0
  97. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_archive_project.py +0 -0
  98. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_badges.py +0 -0
  99. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_deploy_keys.py +0 -0
  100. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_deploy_keys_all_projects.py +0 -0
  101. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_files_templates.py +0 -0
  102. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_group_badges.py +0 -0
  103. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_group_labels.py +0 -0
  104. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_group_members_case_insensitive.py +0 -0
  105. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_group_members_groups.py +0 -0
  106. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_group_members_users.py +0 -0
  107. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_group_settings.py +0 -0
  108. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_group_variables.py +0 -0
  109. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_hooks.py +0 -0
  110. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_integrations.py +0 -0
  111. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_job_token_scope.py +0 -0
  112. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_members.py +0 -0
  113. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_members_add_group.py +0 -0
  114. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_members_enforce.py +0 -0
  115. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_project_group_members_case_insensitive.py +0 -0
  116. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_project_labels.py +0 -0
  117. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_project_members_case_insensitve.py +0 -0
  118. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_project_settings.py +0 -0
  119. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_resource_groups.py +0 -0
  120. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_running.py +0 -0
  121. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_schedules.py +0 -0
  122. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_tags.py +0 -0
  123. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_token_from_config.py +0 -0
  124. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/acceptance/standard/test_variables.py +0 -0
  125. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/__init__.py +0 -0
  126. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/__init__.py +0 -0
  127. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_case_sensitivity.py +0 -0
  128. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_inheritance_break_projects_and_groups.py +0 -0
  129. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_inheritance_break_subgroups.py +0 -0
  130. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_inheritance_break_validation.py +0 -0
  131. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_projects_and_groups.py +0 -0
  132. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_skip_groups_skip_projects.py +0 -0
  133. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_subgroups.py +0 -0
  134. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/configuration/test_yaml_version.py +0 -0
  135. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/processors/__init__.py +0 -0
  136. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/processors/test_abstract_processor.py +0 -0
  137. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/processors/test_difference_logger.py +0 -0
  138. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/processors/test_schedules_processor_extended_cron_pattern.py +0 -0
  139. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/test_access_levels.py +0 -0
  140. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/test_non_empty_configs_provider.py +0 -0
  141. {gitlabform-4.0.0 → gitlabform-4.0.2}/tests/unit/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gitlabform
3
- Version: 4.0.0
3
+ Version: 4.0.2
4
4
  Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
5
5
  Home-page: https://gitlabform.github.io/gitlabform
6
6
  Author: Greg Dubicki and Contributors
@@ -24,7 +24,7 @@ Requires-Dist: Jinja2==3.1.4
24
24
  Requires-Dist: luddite==1.0.4
25
25
  Requires-Dist: MarkupSafe==3.0.2
26
26
  Requires-Dist: mergedeep==1.3.4
27
- Requires-Dist: packaging==24.1
27
+ Requires-Dist: packaging==24.2
28
28
  Requires-Dist: python-gitlab==5.0.0
29
29
  Requires-Dist: requests==2.32.3
30
30
  Requires-Dist: ruamel.yaml==0.17.21
@@ -2,7 +2,6 @@ import enum
2
2
 
3
3
  from typing import List
4
4
 
5
- from gitlabform.gitlab.branches import GitLabBranches
6
5
  from gitlabform.gitlab.commits import GitLabCommits
7
6
  from gitlabform.gitlab.group_badges import GitLabGroupBadges
8
7
  from gitlabform.gitlab.group_ldap_links import GitLabGroupLDAPLinks
@@ -19,7 +18,6 @@ from gitlabform.gitlab.project_merge_requests_approvals import (
19
18
  )
20
19
  from gitlabform.gitlab.python_gitlab import PythonGitlab
21
20
  from gitlabform.gitlab.variables import GitLabVariables
22
- from gitlabform.gitlab.repositories import GitLabRepositories
23
21
  from gitlabform.gitlab.users import GitLabUsers
24
22
 
25
23
 
@@ -49,10 +47,8 @@ class AccessLevel(enum.IntEnum):
49
47
 
50
48
 
51
49
  class GitLab(
52
- GitLabBranches,
53
50
  GitLabCommits,
54
51
  GitLabMergeRequests,
55
- GitLabRepositories,
56
52
  GitLabGroupLDAPLinks,
57
53
  GitLabGroupBadges,
58
54
  GitLabGroupVariables,
@@ -4,7 +4,7 @@ from cli_ui import fatal
4
4
 
5
5
  import abc
6
6
  from typing import Callable, Union, Any
7
-
7
+ import time
8
8
  from gitlabform.constants import EXIT_INVALID_INPUT
9
9
  from gitlabform.gitlab import GitLab
10
10
  from gitlabform.processors.abstract_processor import AbstractProcessor
@@ -70,6 +70,21 @@ class MultipleEntitiesProcessor(AbstractProcessor, metaclass=abc.ABCMeta):
70
70
  else:
71
71
  enforce = False
72
72
 
73
+ if (
74
+ "members" in configuration
75
+ and ("protected_environments" or "merge_requests_approval_rules")
76
+ in configuration
77
+ ):
78
+ # When gitlabform needs to update project membership and also configure
79
+ # settings that are dependent on the members (i.e. environment protection
80
+ # or MR approval rule), there seems to be an issue in GitLab. Automated
81
+ # acceptance tests in gitlabform creates new user and adds to the project
82
+ # followed by configuring other setting. In that scenario need to wait a little
83
+ # before calling GitLab's REST API for processing other settings. Otherwise
84
+ # the users are not available to be configured in other settings and it
85
+ # does not return any error either"
86
+ time.sleep(2)
87
+
73
88
  # TODO: move/convert this to a configuration validation phase
74
89
  self._find_duplicates(project_or_group, entities_in_configuration)
75
90
 
@@ -55,8 +55,15 @@ class ProjectProcessors(AbstractProcessors):
55
55
  def __init__(self, gitlab: GitLab, config: Configuration, strict: bool):
56
56
  super().__init__(gitlab, config, strict)
57
57
  self.processors: List[AbstractProcessor] = [
58
+ # Order of processors matter. GitLabForm will process config sections
59
+ # in the order listed below. Settings that are related to each other,
60
+ # should be ordered accordingly. For example, branch protection or MR
61
+ # approvals can configure specific users, but the user must be a
62
+ # member of the project. So, project membership must be processed
63
+ # before those processors.
58
64
  ProjectProcessor(gitlab),
59
65
  ProjectSettingsProcessor(gitlab),
66
+ MembersProcessor(gitlab),
60
67
  ProjectPushRulesProcessor(gitlab),
61
68
  ProjectLabelsProcessor(gitlab),
62
69
  JobTokenScopeProcessor(gitlab),
@@ -67,7 +74,6 @@ class ProjectProcessors(AbstractProcessors):
67
74
  IntegrationsProcessor(gitlab),
68
75
  FilesProcessor(gitlab, config, strict),
69
76
  HooksProcessor(gitlab),
70
- MembersProcessor(gitlab),
71
77
  SchedulesProcessor(gitlab),
72
78
  BadgesProcessor(gitlab),
73
79
  ResourceGroupsProcessor(gitlab),
@@ -0,0 +1,151 @@
1
+ from typing import Optional
2
+ from logging import debug, info
3
+ from cli_ui import warning, fatal
4
+ from gitlab.v4.objects import Project, ProjectBranch, ProjectProtectedBranch
5
+ from gitlab import (
6
+ GitlabGetError,
7
+ GitlabDeleteError,
8
+ GitlabCreateError,
9
+ )
10
+ from gitlabform.constants import EXIT_INVALID_INPUT, EXIT_PROCESSING_ERROR
11
+ from gitlabform.gitlab import GitLab
12
+ from gitlabform.processors.abstract_processor import AbstractProcessor
13
+ import time
14
+
15
+
16
+ class BranchesProcessor(AbstractProcessor):
17
+ def __init__(self, gitlab: GitLab, strict: bool):
18
+ super().__init__("branches", gitlab)
19
+ self.strict = strict
20
+
21
+ def _process_configuration(self, project_and_group: str, configuration: dict):
22
+ if "members" in configuration:
23
+ # When gitlabform needs to update project membership and also
24
+ # configure branch protection, there seems to be a race condition
25
+ # or delay in GitLab. Automated acceptance tests in gitlabform
26
+ # creates new user and adds to the project followed by configuring
27
+ # branch protection setting. In that scenario need to wait a little
28
+ # before calling GitLab's REST API for branch protection. Otherwise
29
+ # the API returns error code 422 with an message like "Push access
30
+ # levels user is not a member of the project"
31
+
32
+ time.sleep(2)
33
+
34
+ project: Project = self.gl.get_project_by_path_cached(project_and_group)
35
+
36
+ for branch in sorted(configuration["branches"]):
37
+ branch_configuration: dict = self.transform_branch_config(
38
+ configuration["branches"][branch]
39
+ )
40
+
41
+ self.process_branch_protection(project, branch, branch_configuration)
42
+
43
+ def process_branch_protection(
44
+ self, project: Project, branch_name: str, branch_config: dict
45
+ ):
46
+ """
47
+ Process branch protection according to gitlabform config.
48
+ """
49
+ branch: Optional[ProjectBranch] = None
50
+ protected_branch: Optional[ProjectProtectedBranch] = None
51
+
52
+ try:
53
+ branch = project.branches.get(branch_name)
54
+ except GitlabGetError:
55
+ message = f"Branch '{branch_name}' not found when trying to processing it to be protected/unprotected!"
56
+ if self.strict:
57
+ fatal(
58
+ message,
59
+ exit_code=EXIT_INVALID_INPUT,
60
+ )
61
+ else:
62
+ warning(message)
63
+
64
+ try:
65
+ protected_branch = project.protectedbranches.get(branch_name)
66
+ except GitlabGetError:
67
+ message = f"The branch '{branch_name}' is not protected!"
68
+ debug(message)
69
+
70
+ if branch and branch_config.get("protected"):
71
+ if protected_branch and self._needs_update(
72
+ protected_branch.attributes, branch_config
73
+ ):
74
+ # Need to unprotect the branch and then re-protect again using current config
75
+ # Another option would be to "update" protected branch. But, GitLab's API for
76
+ # updating protected branch requires retrieving id of the existing rule/config
77
+ # such as 'allowed_to_push'. See: https://docs.gitlab.com/ee/api/protected_branches.html#example-update-a-push_access_level-record
78
+ # This could involve more data processing. The data for updating vs creating a
79
+ # protected branch is not the same. So, removing existing branch protection and
80
+ # reconfiguring branch protection seems simpler.
81
+ self.unprotect_branch(protected_branch)
82
+ self.protect_branch(project, branch, branch_config)
83
+ elif not protected_branch:
84
+ self.protect_branch(project, branch, branch_config)
85
+ elif protected_branch and not branch_config.get("protected"):
86
+ self.unprotect_branch(protected_branch)
87
+
88
+ def protect_branch(
89
+ self, project: Project, branch: ProjectBranch, branch_config: dict
90
+ ):
91
+ """
92
+ Protect a branch using given config.
93
+ Raise exception if running in strict mode.
94
+ """
95
+
96
+ debug("Setting branch '%s' as protected", branch.name)
97
+
98
+ try:
99
+ project.protectedbranches.create({"name": branch.name, **branch_config})
100
+ except GitlabCreateError as e:
101
+ message = (
102
+ f"Protecting branch '{branch.name}' failed! Error '{e.error_message}"
103
+ )
104
+
105
+ if self.strict:
106
+ fatal(
107
+ message,
108
+ exit_code=EXIT_PROCESSING_ERROR,
109
+ )
110
+ else:
111
+ warning(message)
112
+
113
+ def unprotect_branch(self, protected_branch: ProjectProtectedBranch):
114
+ """
115
+ Unprotect a branch.
116
+ Raise exception if running in strict mode.
117
+ """
118
+
119
+ debug("Setting branch '%s' as unprotected", protected_branch.name)
120
+
121
+ try:
122
+ # The delete method doesn't delete the actual branch.
123
+ # It only unprotects the branch.
124
+ protected_branch.delete()
125
+ except GitlabDeleteError as e:
126
+ message = f"Branch '{protected_branch.name}' could not be unprotected! Error '{e.error_message}"
127
+ if self.strict:
128
+ fatal(
129
+ message,
130
+ exit_code=EXIT_PROCESSING_ERROR,
131
+ )
132
+ else:
133
+ warning(message)
134
+
135
+ def transform_branch_config(self, branch_config: dict):
136
+ """
137
+ The branch configuration in gitlabform supports passing users or group using username
138
+ or group name but GitLab only supports their id. This method will transform the
139
+ config by replacing them with ids.
140
+ """
141
+
142
+ for key in branch_config:
143
+ if isinstance(branch_config[key], list):
144
+ for item in branch_config[key]:
145
+ if isinstance(item, dict):
146
+ if "user" in item:
147
+ item["user_id"] = self.gl.get_user_id(item.pop("user"))
148
+ elif "group" in item:
149
+ item["group_id"] = self.gl.get_group_id(item.pop("group"))
150
+
151
+ return branch_config
@@ -5,15 +5,18 @@ from pathlib import Path
5
5
  from logging import debug
6
6
  from cli_ui import debug as verbose
7
7
  from cli_ui import warning, fatal
8
+ from typing import List
8
9
 
9
10
  from jinja2 import Environment, FileSystemLoader
11
+ from gitlab import GitlabGetError, GitlabUpdateError
12
+ from gitlab.v4.objects import Project, ProjectFile, ProjectBranch
13
+ from gitlab.base import RESTObject
10
14
 
11
15
  from gitlabform.constants import EXIT_INVALID_INPUT
12
16
  from gitlabform.configuration import Configuration
13
17
  from gitlabform.gitlab import GitLab
14
- from gitlabform.gitlab.core import NotFoundException, UnexpectedResponseException
15
18
  from gitlabform.processors.abstract_processor import AbstractProcessor
16
- from gitlabform.processors.util.branch_protector import BranchProtector
19
+ from gitlabform.processors.project.branches_processor import BranchesProcessor
17
20
 
18
21
 
19
22
  class FilesProcessor(AbstractProcessor):
@@ -21,32 +24,38 @@ class FilesProcessor(AbstractProcessor):
21
24
  super().__init__("files", gitlab)
22
25
  self.config = config
23
26
  self.strict = strict
24
- self.branch_protector = BranchProtector(gitlab, strict)
27
+ self.branch_processor = BranchesProcessor(gitlab, strict)
25
28
 
26
29
  def _process_configuration(self, project_and_group: str, configuration: dict):
27
30
  for file in sorted(configuration["files"]):
31
+ project: Project = self.gl.get_project_by_path_cached(project_and_group)
28
32
  debug("Processing file '%s'...", file)
29
33
 
30
34
  if configuration.get("files|" + file + "|skip"):
31
35
  debug("Skipping file '%s'", file)
32
36
  continue
33
37
 
34
- if configuration["files"][file]["branches"] == "all":
35
- all_branches = self.gitlab.get_branches(project_and_group)
36
- branches = sorted(all_branches)
37
- elif configuration["files"][file]["branches"] == "protected":
38
- protected_branches = self.gitlab.get_protected_branches(
39
- project_and_group
40
- )
41
- branches = sorted(protected_branches)
42
- else:
43
- all_branches = self.gitlab.get_branches(project_and_group)
44
- branches = []
45
- for branch in configuration["files"][file]["branches"]:
46
- if branch in all_branches:
47
- branches.append(branch)
48
- else:
49
- message = f"! Branch '{branch}' not found, not processing file '{file}' in it"
38
+ config_target_ref = configuration["files"][file]["branches"]
39
+ branches_to_update: List[RESTObject] = []
40
+
41
+ if isinstance(config_target_ref, str):
42
+ # Target ref could be either 'all' or 'protected'.
43
+ # Get a list of branches that should be updated.
44
+ if config_target_ref == "all":
45
+ branches_to_update.extend(
46
+ project.branches.list(get_all=True, lazy=True)
47
+ )
48
+ elif config_target_ref == "protected":
49
+ branches_to_update.extend(
50
+ project.protectedbranches.list(get_all=True, lazy=True)
51
+ )
52
+ elif isinstance(config_target_ref, list):
53
+ # Get a list of branches from the config that should be updated.
54
+ for branch_name in config_target_ref:
55
+ try:
56
+ branches_to_update.append(project.branches.get(branch_name))
57
+ except GitlabGetError:
58
+ message = f"! Branch '{branch_name}' not found, not processing file '{file}' in it"
50
59
  if self.strict:
51
60
  fatal(
52
61
  message,
@@ -55,8 +64,14 @@ class FilesProcessor(AbstractProcessor):
55
64
  else:
56
65
  warning(message)
57
66
 
58
- for branch in branches:
59
- verbose(f"Processing file '{file}' in branch '{branch}'")
67
+ debug(
68
+ "File '%s' to be updated in '%s' branche(s)",
69
+ file,
70
+ len(branches_to_update),
71
+ )
72
+
73
+ for branch in branches_to_update:
74
+ verbose(f"Processing file '{file}' in branch '{branch.name}'")
60
75
 
61
76
  if configuration.get(
62
77
  "files|" + file + "|content"
@@ -69,20 +84,22 @@ class FilesProcessor(AbstractProcessor):
69
84
 
70
85
  if configuration.get("files|" + file + "|delete"):
71
86
  try:
72
- self.gitlab.get_file(project_and_group, branch, file)
73
- debug("Deleting file '%s' in branch '%s'", file, branch)
87
+ file_to_delete: ProjectFile = project.files.get(
88
+ file_path=file, ref=branch.name
89
+ )
90
+ debug("Deleting file '%s' in branch '%s'", file, branch.name)
74
91
  self.modify_file_dealing_with_branch_protection(
75
- project_and_group,
92
+ project,
76
93
  branch,
77
- file,
94
+ file_to_delete,
78
95
  "delete",
79
96
  configuration,
80
97
  )
81
- except NotFoundException:
98
+ except GitlabGetError:
82
99
  debug(
83
100
  "Not deleting file '%s' in branch '%s' (already doesn't exist)",
84
101
  file,
85
- branch,
102
+ branch.name,
86
103
  )
87
104
  else:
88
105
  # change or create file
@@ -118,16 +135,22 @@ class FilesProcessor(AbstractProcessor):
118
135
  )
119
136
 
120
137
  try:
121
- current_content = self.gitlab.get_file(
122
- project_and_group, branch, file
138
+ repo_file: ProjectFile = project.files.get(
139
+ file_path=file, ref=branch.name
123
140
  )
141
+ current_content = repo_file.content
142
+
124
143
  if current_content != new_content:
125
144
  if configuration.get("files|" + file + "|overwrite"):
126
- debug("Changing file '%s' in branch '%s'", file, branch)
145
+ debug(
146
+ "Changing file '%s' in branch '%s'",
147
+ file,
148
+ branch.name,
149
+ )
127
150
  self.modify_file_dealing_with_branch_protection(
128
- project_and_group,
151
+ project,
129
152
  branch,
130
- file,
153
+ repo_file,
131
154
  "modify",
132
155
  configuration,
133
156
  new_content,
@@ -136,19 +159,19 @@ class FilesProcessor(AbstractProcessor):
136
159
  debug(
137
160
  "Not changing file '%s' in branch '%s' - overwrite flag not set.",
138
161
  file,
139
- branch,
162
+ branch.name,
140
163
  )
141
164
  else:
142
165
  debug(
143
166
  "Not changing file '%s' in branch '%s' - it's content is already"
144
167
  " as provided)",
145
168
  file,
146
- branch,
169
+ branch.name,
147
170
  )
148
- except NotFoundException:
149
- debug("Creating file '%s' in branch '%s'", file, branch)
171
+ except GitlabGetError:
172
+ debug("Creating file '%s' in branch '%s'", file, branch.name)
150
173
  self.modify_file_dealing_with_branch_protection(
151
- project_and_group,
174
+ project,
152
175
  branch,
153
176
  file,
154
177
  "add",
@@ -162,11 +185,11 @@ class FilesProcessor(AbstractProcessor):
162
185
 
163
186
  def modify_file_dealing_with_branch_protection(
164
187
  self,
165
- project_and_group,
166
- branch,
167
- file,
168
- operation,
169
- configuration,
188
+ project: Project,
189
+ branch: RESTObject,
190
+ file_to_operate_on: str | ProjectFile,
191
+ operation: str,
192
+ configuration: dict,
170
193
  new_content=None,
171
194
  ):
172
195
  # perhaps your user permissions are ok to just perform this operation regardless
@@ -174,36 +197,43 @@ class FilesProcessor(AbstractProcessor):
174
197
 
175
198
  try:
176
199
  self.just_modify_file(
177
- project_and_group, branch, file, operation, configuration, new_content
200
+ project,
201
+ branch,
202
+ file_to_operate_on,
203
+ operation,
204
+ configuration,
205
+ new_content,
178
206
  )
179
207
 
180
- except UnexpectedResponseException as e:
208
+ except GitlabUpdateError as e:
181
209
  if (
182
- e.response_status_code == 400
183
- and "You are not allowed to push into this branch" in e.response_text
210
+ e.response_code == 400
211
+ and "You are not allowed to push into this branch" in e.error_message
184
212
  ):
185
213
  # ...but if not, then we can unprotect the branch, but only if we know how to
186
214
  # protect it again...
187
215
 
188
- if configuration.get("branches|" + branch + "|protected"):
216
+ if configuration.get("branches|" + branch.name + "|protected"):
189
217
  debug(
190
- f"> Temporarily unprotecting the branch to {operation} a file in it..."
218
+ f"> Temporarily unprotecting the branch to '{operation}' a file in it..."
191
219
  )
192
- self.branch_protector.unprotect_branch(project_and_group, branch)
220
+ # Delete operation on protected branch removes the protection only
221
+ project.protectedbranches.delete(branch.name)
193
222
  else:
194
223
  fatal(
195
- f"Operation {operation} on file {file} in branch {branch} not permitted,"
196
- f" but we don't have a branch protection configuration provided for this"
224
+ f"Operation '{operation}' on file in branch {branch.name} not permitted."
225
+ f" We don't have a branch protection configuration provided for this"
197
226
  f" branch. Breaking as we cannot unprotect the branch as we would not know"
198
227
  f" how to protect it again.",
199
228
  EXIT_INVALID_INPUT,
200
229
  )
201
230
 
202
231
  try:
232
+ debug("> Attempt updating file again")
203
233
  self.just_modify_file(
204
- project_and_group,
234
+ project,
205
235
  branch,
206
- file,
236
+ file_to_operate_on,
207
237
  operation,
208
238
  configuration,
209
239
  new_content,
@@ -211,10 +241,14 @@ class FilesProcessor(AbstractProcessor):
211
241
 
212
242
  finally:
213
243
  # ...and protect the branch again after the operation
214
- if configuration.get("branches|" + branch + "|protected"):
244
+ if configuration.get("branches|" + branch.name + "|protected"):
215
245
  debug("> Protecting the branch again.")
216
- self.branch_protector.protect_branch(
217
- project_and_group, configuration, branch
246
+ project_branch: ProjectBranch = project.branches.get(
247
+ branch.name
248
+ )
249
+ branch_config: dict = configuration["branches"][branch.name]
250
+ self.branch_processor.protect_branch(
251
+ project, project_branch, branch_config
218
252
  )
219
253
 
220
254
  else:
@@ -222,35 +256,38 @@ class FilesProcessor(AbstractProcessor):
222
256
 
223
257
  def just_modify_file(
224
258
  self,
225
- project_and_group,
226
- branch,
227
- file,
228
- operation,
229
- configuration,
259
+ project: Project,
260
+ branch: RESTObject,
261
+ file_to_operate_on: str | ProjectFile,
262
+ operation: str,
263
+ configuration: dict,
230
264
  new_content=None,
231
265
  ):
232
- if operation == "modify":
233
- self.gitlab.set_file(
234
- project_and_group,
235
- branch,
236
- file,
237
- new_content,
238
- self.get_commit_message_for_file_change("change", file, configuration),
266
+ if operation == "modify" and isinstance(file_to_operate_on, ProjectFile):
267
+ file_to_operate_on.content = new_content
268
+ file_to_operate_on.save(
269
+ commit_message=self.get_commit_message_for_file_change(
270
+ "change", file_to_operate_on.file_path, configuration
271
+ ),
272
+ branch=branch.name,
239
273
  )
240
- elif operation == "add":
241
- self.gitlab.add_file(
242
- project_and_group,
243
- branch,
244
- file,
245
- new_content,
246
- self.get_commit_message_for_file_change("add", file, configuration),
274
+ elif operation == "add" and isinstance(file_to_operate_on, str):
275
+ project.files.create(
276
+ {
277
+ "file_path": file_to_operate_on,
278
+ "branch": branch.name,
279
+ "content": new_content,
280
+ "commit_message": self.get_commit_message_for_file_change(
281
+ "delete", file_to_operate_on, configuration
282
+ ),
283
+ }
247
284
  )
248
- elif operation == "delete":
249
- self.gitlab.delete_file(
250
- project_and_group,
251
- branch,
252
- file,
253
- self.get_commit_message_for_file_change("delete", file, configuration),
285
+ elif operation == "delete" and isinstance(file_to_operate_on, ProjectFile):
286
+ file_to_operate_on.delete(
287
+ commit_message=self.get_commit_message_for_file_change(
288
+ "delete", file_to_operate_on.file_path, configuration
289
+ ),
290
+ branch=branch.name,
254
291
  )
255
292
 
256
293
  def get_file_content_as_template(self, template, project_and_group, **kwargs):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gitlabform
3
- Version: 4.0.0
3
+ Version: 4.0.2
4
4
  Summary: 🏗 Specialized configuration as a code tool for GitLab projects, groups and more using hierarchical configuration written in YAML
5
5
  Home-page: https://gitlabform.github.io/gitlabform
6
6
  Author: Greg Dubicki and Contributors
@@ -24,7 +24,7 @@ Requires-Dist: Jinja2==3.1.4
24
24
  Requires-Dist: luddite==1.0.4
25
25
  Requires-Dist: MarkupSafe==3.0.2
26
26
  Requires-Dist: mergedeep==1.3.4
27
- Requires-Dist: packaging==24.1
27
+ Requires-Dist: packaging==24.2
28
28
  Requires-Dist: python-gitlab==5.0.0
29
29
  Requires-Dist: requests==2.32.3
30
30
  Requires-Dist: ruamel.yaml==0.17.21
@@ -20,7 +20,6 @@ gitlabform/configuration/groups.py
20
20
  gitlabform/configuration/projects.py
21
21
  gitlabform/configuration/transform.py
22
22
  gitlabform/gitlab/__init__.py
23
- gitlabform/gitlab/branches.py
24
23
  gitlabform/gitlab/commits.py
25
24
  gitlabform/gitlab/core.py
26
25
  gitlabform/gitlab/group_badges.py
@@ -35,7 +34,6 @@ gitlabform/gitlab/project_merge_requests_approvals.py
35
34
  gitlabform/gitlab/project_protected_environments.py
36
35
  gitlabform/gitlab/projects.py
37
36
  gitlabform/gitlab/python_gitlab.py
38
- gitlabform/gitlab/repositories.py
39
37
  gitlabform/gitlab/users.py
40
38
  gitlabform/gitlab/variables.py
41
39
  gitlabform/lists/__init__.py
@@ -79,7 +77,6 @@ gitlabform/processors/project/variables_processor.py
79
77
  gitlabform/processors/shared/__init__.py
80
78
  gitlabform/processors/shared/protected_environments_processor.py
81
79
  gitlabform/processors/util/__init__.py
82
- gitlabform/processors/util/branch_protector.py
83
80
  gitlabform/processors/util/decorators.py
84
81
  gitlabform/processors/util/difference_logger.py
85
82
  gitlabform/processors/util/labels_processor.py
@@ -134,6 +131,5 @@ tests/unit/configuration/test_subgroups.py
134
131
  tests/unit/configuration/test_yaml_version.py
135
132
  tests/unit/processors/__init__.py
136
133
  tests/unit/processors/test_abstract_processor.py
137
- tests/unit/processors/test_branch_protector.py
138
134
  tests/unit/processors/test_difference_logger.py
139
135
  tests/unit/processors/test_schedules_processor_extended_cron_pattern.py
@@ -5,7 +5,7 @@ Jinja2==3.1.4
5
5
  luddite==1.0.4
6
6
  MarkupSafe==3.0.2
7
7
  mergedeep==1.3.4
8
- packaging==24.1
8
+ packaging==24.2
9
9
  python-gitlab==5.0.0
10
10
  requests==2.32.3
11
11
  ruamel.yaml==0.17.21
@@ -47,7 +47,7 @@ setup(
47
47
  "luddite==1.0.4",
48
48
  "MarkupSafe==3.0.2",
49
49
  "mergedeep==1.3.4",
50
- "packaging==24.1",
50
+ "packaging==24.2",
51
51
  "python-gitlab==5.0.0",
52
52
  "requests==2.32.3",
53
53
  "ruamel.yaml==0.17.21",