gitlab-projects-settings 2.1.0__tar.gz → 3.0.0__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 (42) hide show
  1. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.gitlab-ci.yml +37 -37
  2. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/CHANGELOG.md +30 -0
  3. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/PKG-INFO +12 -4
  4. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/README.md +11 -3
  5. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/gitlab_projects_settings.egg-info/PKG-INFO +12 -4
  6. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/gitlab_projects_settings.egg-info/SOURCES.txt +3 -1
  7. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/cli/entrypoint.py +160 -49
  8. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/cli/main.py +38 -2
  9. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/features/gitlab.py +133 -37
  10. gitlab_projects_settings-3.0.0/src/types/__init__.py +0 -0
  11. gitlab_projects_settings-3.0.0/src/types/namespaces.py +33 -0
  12. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.chglog/CHANGELOG.tpl.md +0 -0
  13. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.chglog/changelog.sh +0 -0
  14. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.chglog/config.yml +0 -0
  15. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.gitignore +0 -0
  16. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.markdownlint.json +0 -0
  17. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.style.yapf +0 -0
  18. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.vscode/extensions.json +0 -0
  19. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/.vscode/settings.json +0 -0
  20. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/LICENSE +0 -0
  21. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/gitlab_projects_settings.egg-info/dependency_links.txt +0 -0
  22. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/gitlab_projects_settings.egg-info/entry_points.txt +0 -0
  23. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/gitlab_projects_settings.egg-info/requires.txt +0 -0
  24. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/gitlab_projects_settings.egg-info/top_level.txt +0 -0
  25. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/mypy.ini +0 -0
  26. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/requirements/build.txt +0 -0
  27. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/requirements/deploy.txt +0 -0
  28. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/requirements/quality.txt +0 -0
  29. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/requirements/runtime.txt +0 -0
  30. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/setup.cfg +0 -0
  31. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/setup.py +0 -0
  32. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/__init__.py +0 -0
  33. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/__main__.py +0 -0
  34. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/cli/__init__.py +0 -0
  35. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/features/__init__.py +0 -0
  36. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/package/__init__.py +0 -0
  37. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/package/bundle.py +0 -0
  38. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/package/version.py +0 -0
  39. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/prints/__init__.py +0 -0
  40. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/prints/colors.py +0 -0
  41. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/system/__init__.py +0 -0
  42. {gitlab_projects_settings-2.1.0 → gitlab_projects_settings-3.0.0}/src/system/platform.py +0 -0
@@ -53,43 +53,6 @@ changelog:
53
53
  rules:
54
54
  - if: $CI_LOCAL
55
55
 
56
- readme:
57
- stage: development
58
- image: local:silent
59
- script:
60
- - |
61
- export COLUMNS=120
62
- mkdir -p ./.tmp/
63
- PACKAGE_NAME=$(grep '^PROJECT_NAME =' ./setup.py | cut -d"'" -f2)
64
- {
65
- echo ''
66
- echo '```yaml'
67
- alignments='[ ]\{2,\}'
68
- argument='[[:alnum:]][[:alnum:]_-]\{0,\}'
69
- letter='[[:alnum:]_]\{1\}'
70
- parameter='[-]\{1,2\}[[:alnum:]][[:alnum:]_-]\{0,\}'
71
- word='[[:alnum:]_-]\{1,\}'
72
- "${PACKAGE_NAME}" --help \
73
- | sed '1d; $d' \
74
- | sed "s/^\( ${parameter} ${word}${alignments}\)/\1# /g" \
75
- | sed "s/^\( ${parameter}${alignments}\)/\1# /g" \
76
- | sed "s/^\( ${parameter}, ${parameter}${alignments}\)/\1# /g" \
77
- | sed "s/^\( ${argument}${alignments}\)/\1# /g"
78
- echo '```'
79
- echo ''
80
- } >./.tmp/README.help.tmp
81
- - |
82
- lead='^<!-- readme-help-start -->$'
83
- tail='^<!-- readme-help-stop -->$'
84
- sed -i "/${lead}/,/${tail}/{ /${lead}/{ p; r ./.tmp/README.help.tmp
85
- }; /${tail}/p; d }" ./README.md | head -n 100
86
- - rm -f ./.tmp/README.help.tmp
87
- - |
88
- git -c color.diff=always --no-pager diff ./README.md | grep --color=never '.' && echo ''
89
- echo ' > README: Updated successfully'
90
- rules:
91
- - if: $CI_LOCAL
92
-
93
56
  # =============================================================================
94
57
  # Stage: prepare
95
58
 
@@ -192,6 +155,43 @@ install:
192
155
  rules:
193
156
  - if: $CI_LOCAL
194
157
 
158
+ readme:
159
+ stage: build
160
+ image: local:silent
161
+ script:
162
+ - |
163
+ export COLUMNS=120
164
+ mkdir -p ./.tmp/
165
+ PACKAGE_NAME=$(grep '^PROJECT_NAME =' ./setup.py | cut -d"'" -f2)
166
+ {
167
+ echo ''
168
+ echo '```yaml'
169
+ alignments='[ ]\{2,\}'
170
+ argument='[[:alnum:]][[:alnum:]_-]\{0,\}'
171
+ letter='[[:alnum:]_]\{1\}'
172
+ parameter='[-]\{1,2\}[[:alnum:]][[:alnum:]_-]\{0,\}'
173
+ word='[[:alnum:]_-]\{1,\}'
174
+ "${PACKAGE_NAME}" --help \
175
+ | sed '1d; $d' \
176
+ | sed "s/^\( ${parameter} ${word}${alignments}\)/\1# /g" \
177
+ | sed "s/^\( ${parameter}${alignments}\)/\1# /g" \
178
+ | sed "s/^\( ${parameter}, ${parameter}${alignments}\)/\1# /g" \
179
+ | sed "s/^\( ${argument}${alignments}\)/\1# /g"
180
+ echo '```'
181
+ echo ''
182
+ } >./.tmp/README.help.tmp
183
+ - |
184
+ lead='^<!-- readme-help-start -->$'
185
+ tail='^<!-- readme-help-stop -->$'
186
+ sed -i "/${lead}/,/${tail}/{ /${lead}/{ p; r ./.tmp/README.help.tmp
187
+ }; /${tail}/p; d }" ./README.md | head -n 100
188
+ - rm -f ./.tmp/README.help.tmp
189
+ - |
190
+ git -c color.diff=always --no-pager diff ./README.md | grep --color=never '.' && echo ''
191
+ echo ' > README: Updated successfully'
192
+ rules:
193
+ - if: $CI_LOCAL
194
+
195
195
  # =============================================================================
196
196
  # Stage: deploy
197
197
 
@@ -1,4 +1,34 @@
1
1
 
2
+ <a name="3.0.0"></a>
3
+ ## [3.0.0](https://gitlab.com/AdrianDC/gitlab-projects-settings/compare/2.1.0...3.0.0) (2024-05-06)
4
+
5
+ ### Bug Fixes
6
+
7
+ * **entrypoint:** resolve support for private user namespaces
8
+ * **entrypoint:** detect if GitLab actions can continue
9
+ * **entrypoint:** enforce against missing '.description' values
10
+ * **gitlab:** enforce 'group_delete' usage in '--dry-run' mode
11
+ * **gitlab:** delay groups deletion by 10s and projects by 5s
12
+ * **gitlab:** get all branches and tags upon 'list()' calls
13
+ * **gitlab:** get all members in 'project_reset_members'
14
+
15
+ ### Ci
16
+
17
+ * **gitlab-ci:** move 'readme' job after 'build' and local 'install'
18
+
19
+ ### Cleanups
20
+
21
+ * **gitlab:** minor comments changes in 'project_reset_features'
22
+
23
+ ### Features
24
+
25
+ * **entrypoint:** preserve main group description if exists
26
+ * **entrypoint:** always flush progress output logs
27
+ * **gitlab:** detect multiple branches to keep 'Merge requests'
28
+ * **gitlab:** detect 'Token Access' usage for 'CI/CD' features
29
+ * **namespaces:** migrate 'Helper' class to 'Namespaces' class
30
+
31
+
2
32
  <a name="2.1.0"></a>
3
33
  ## [2.1.0](https://gitlab.com/AdrianDC/gitlab-projects-settings/compare/2.0.2...2.1.0) (2024-04-28)
4
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gitlab-projects-settings
3
- Version: 2.1.0
3
+ Version: 3.0.0
4
4
  Summary: Configure GitLab groups and projects settings automatically
5
5
  Home-page: https://gitlab.com/AdrianDC/gitlab-projects-settings
6
6
  Author: Adrian DC
@@ -64,8 +64,9 @@ The following step is required before using the tool:
64
64
  ```yaml
65
65
  usage: gitlab-projects-settings [-h] [--version] [-t TOKEN] [--dry-run] [--exclude-group] [--exclude-subgroups]
66
66
  [--exclude-projects] [--reset-features] [--reset-members] [--set-avatar FILE]
67
- [--set-description TEXT] [--update-description] [--protect-branches]
68
- [--protect-tags LEVEL]
67
+ [--set-description TEXT] [--update-description] [--run-housekeeping]
68
+ [--archive-project | --unarchive-project] [--delete-group] [--delete-project]
69
+ [--protect-branches] [--protect-tags LEVEL]
69
70
  [gitlab] [path]
70
71
 
71
72
  gitlab-projects-settings: Configure GitLab groups and projects settings automatically
@@ -90,13 +91,20 @@ general settings arguments:
90
91
  --set-description TEXT # Set description of GitLab projects and groups
91
92
  --update-description # Update description of GitLab projects and groups automatically
92
93
 
94
+ advanced settings arguments:
95
+ --run-housekeeping # Run housekeeping of project or projects GitLab in groups
96
+ --archive-project # Archive project or projects in GitLab groups
97
+ --unarchive-project # Unarchive project or projects in GitLab groups
98
+ --delete-group # Delete group or groups in GitLab groups
99
+ --delete-project # Delete project or projects in GitLab groups
100
+
93
101
  repository settings arguments:
94
102
  --protect-branches # Protect branches with default master/main, develop and staging
95
103
  --protect-tags LEVEL # Protect tags at level [no-one,admins,maintainers,developers]
96
104
 
97
105
  positional arguments:
98
106
  gitlab # GitLab URL (default: https://gitlab.com)
99
- path # GitLab group or project path
107
+ path # GitLab group, user namespace or project path
100
108
  ```
101
109
 
102
110
  <!-- readme-help-stop -->
@@ -30,8 +30,9 @@ The following step is required before using the tool:
30
30
  ```yaml
31
31
  usage: gitlab-projects-settings [-h] [--version] [-t TOKEN] [--dry-run] [--exclude-group] [--exclude-subgroups]
32
32
  [--exclude-projects] [--reset-features] [--reset-members] [--set-avatar FILE]
33
- [--set-description TEXT] [--update-description] [--protect-branches]
34
- [--protect-tags LEVEL]
33
+ [--set-description TEXT] [--update-description] [--run-housekeeping]
34
+ [--archive-project | --unarchive-project] [--delete-group] [--delete-project]
35
+ [--protect-branches] [--protect-tags LEVEL]
35
36
  [gitlab] [path]
36
37
 
37
38
  gitlab-projects-settings: Configure GitLab groups and projects settings automatically
@@ -56,13 +57,20 @@ general settings arguments:
56
57
  --set-description TEXT # Set description of GitLab projects and groups
57
58
  --update-description # Update description of GitLab projects and groups automatically
58
59
 
60
+ advanced settings arguments:
61
+ --run-housekeeping # Run housekeeping of project or projects GitLab in groups
62
+ --archive-project # Archive project or projects in GitLab groups
63
+ --unarchive-project # Unarchive project or projects in GitLab groups
64
+ --delete-group # Delete group or groups in GitLab groups
65
+ --delete-project # Delete project or projects in GitLab groups
66
+
59
67
  repository settings arguments:
60
68
  --protect-branches # Protect branches with default master/main, develop and staging
61
69
  --protect-tags LEVEL # Protect tags at level [no-one,admins,maintainers,developers]
62
70
 
63
71
  positional arguments:
64
72
  gitlab # GitLab URL (default: https://gitlab.com)
65
- path # GitLab group or project path
73
+ path # GitLab group, user namespace or project path
66
74
  ```
67
75
 
68
76
  <!-- readme-help-stop -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gitlab-projects-settings
3
- Version: 2.1.0
3
+ Version: 3.0.0
4
4
  Summary: Configure GitLab groups and projects settings automatically
5
5
  Home-page: https://gitlab.com/AdrianDC/gitlab-projects-settings
6
6
  Author: Adrian DC
@@ -64,8 +64,9 @@ The following step is required before using the tool:
64
64
  ```yaml
65
65
  usage: gitlab-projects-settings [-h] [--version] [-t TOKEN] [--dry-run] [--exclude-group] [--exclude-subgroups]
66
66
  [--exclude-projects] [--reset-features] [--reset-members] [--set-avatar FILE]
67
- [--set-description TEXT] [--update-description] [--protect-branches]
68
- [--protect-tags LEVEL]
67
+ [--set-description TEXT] [--update-description] [--run-housekeeping]
68
+ [--archive-project | --unarchive-project] [--delete-group] [--delete-project]
69
+ [--protect-branches] [--protect-tags LEVEL]
69
70
  [gitlab] [path]
70
71
 
71
72
  gitlab-projects-settings: Configure GitLab groups and projects settings automatically
@@ -90,13 +91,20 @@ general settings arguments:
90
91
  --set-description TEXT # Set description of GitLab projects and groups
91
92
  --update-description # Update description of GitLab projects and groups automatically
92
93
 
94
+ advanced settings arguments:
95
+ --run-housekeeping # Run housekeeping of project or projects GitLab in groups
96
+ --archive-project # Archive project or projects in GitLab groups
97
+ --unarchive-project # Unarchive project or projects in GitLab groups
98
+ --delete-group # Delete group or groups in GitLab groups
99
+ --delete-project # Delete project or projects in GitLab groups
100
+
93
101
  repository settings arguments:
94
102
  --protect-branches # Protect branches with default master/main, develop and staging
95
103
  --protect-tags LEVEL # Protect tags at level [no-one,admins,maintainers,developers]
96
104
 
97
105
  positional arguments:
98
106
  gitlab # GitLab URL (default: https://gitlab.com)
99
- path # GitLab group or project path
107
+ path # GitLab group, user namespace or project path
100
108
  ```
101
109
 
102
110
  <!-- readme-help-stop -->
@@ -35,4 +35,6 @@ src/package/version.py
35
35
  src/prints/__init__.py
36
36
  src/prints/colors.py
37
37
  src/system/__init__.py
38
- src/system/platform.py
38
+ src/system/platform.py
39
+ src/types/__init__.py
40
+ src/types/namespaces.py
@@ -5,23 +5,25 @@ from argparse import Namespace
5
5
 
6
6
  # Modules libraries
7
7
  from gitlab.exceptions import GitlabGetError
8
- from gitlab.v4.objects import Group, Project
8
+ from gitlab.v4.objects import Group as GitLabGroup, Project as GitLabProject, User as GitLabUser
9
9
 
10
10
  # Components
11
11
  from ..features.gitlab import GitLabFeature
12
12
  from ..prints.colors import Colors
13
13
  from ..system.platform import Platform
14
+ from ..types.namespaces import Namespaces
14
15
 
15
16
  # Entrypoint class, pylint: disable=too-few-public-methods
16
17
  class Entrypoint:
17
18
 
18
- # CLI
19
+ # CLI, pylint: disable=too-many-branches
19
20
  @staticmethod
20
21
  def cli(options: Namespace) -> bool:
21
22
 
22
23
  # Variables
23
- group: Group = None
24
- project: Project = None
24
+ group: GitLabGroup = None
25
+ project: GitLabProject = None
26
+ user: GitLabUser = None
25
27
 
26
28
  # Header
27
29
  print(' ')
@@ -46,14 +48,26 @@ class Entrypoint:
46
48
  f'{Colors.RESET}')
47
49
  print(' ')
48
50
  Platform.flush()
49
- except GitlabGetError:
50
- project = gitlab.project(options.path)
51
- print(f'{Colors.BOLD} - GitLab project: '
52
- f'{Colors.GREEN}{project.path_with_namespace}'
53
- f'{Colors.CYAN} ({project.description})'
54
- f'{Colors.RESET}')
55
- print(' ')
56
- Platform.flush()
51
+ except GitlabGetError as exception:
52
+ try:
53
+ if '/' in options.path:
54
+ raise TypeError from exception
55
+ user = gitlab.user(options.path)
56
+ namespace = gitlab.namespace(user.namespace_id)
57
+ print(f'{Colors.BOLD} - GitLab user namespace: '
58
+ f'{Colors.GREEN}{namespace.full_path}'
59
+ f'{Colors.CYAN} ({namespace.name})'
60
+ f'{Colors.RESET}')
61
+ print(' ')
62
+ Platform.flush()
63
+ except (GitlabGetError, TypeError):
64
+ project = gitlab.project(options.path)
65
+ print(f'{Colors.BOLD} - GitLab project: '
66
+ f'{Colors.GREEN}{project.path_with_namespace}'
67
+ f'{Colors.CYAN} ({project.description})'
68
+ f'{Colors.RESET}')
69
+ print(' ')
70
+ Platform.flush()
57
71
 
58
72
  # Handle single project
59
73
  if project:
@@ -68,11 +82,12 @@ class Entrypoint:
68
82
 
69
83
  # Handle group
70
84
  if not options.exclude_group:
71
- Entrypoint.group(
72
- options,
73
- gitlab,
74
- group.id,
75
- )
85
+ if not Entrypoint.group(
86
+ options,
87
+ gitlab,
88
+ group.id,
89
+ ):
90
+ return False
76
91
 
77
92
  # Iterate through subgroups
78
93
  if not options.exclude_subgroups:
@@ -85,12 +100,13 @@ class Entrypoint:
85
100
  ),
86
101
  key=lambda item: item.full_path,
87
102
  ):
88
- Entrypoint.group(
89
- options,
90
- gitlab,
91
- group_subgroup.id,
92
- True,
93
- )
103
+ if not Entrypoint.group(
104
+ options,
105
+ gitlab,
106
+ group_subgroup.id,
107
+ True,
108
+ ):
109
+ return False
94
110
 
95
111
  # Iterate through projects
96
112
  if not options.exclude_projects:
@@ -103,11 +119,32 @@ class Entrypoint:
103
119
  ),
104
120
  key=lambda item: item.path_with_namespace,
105
121
  ):
106
- Entrypoint.project(
107
- options,
108
- gitlab,
109
- group_project.id,
110
- )
122
+ if not Entrypoint.project(
123
+ options,
124
+ gitlab,
125
+ group_project.id,
126
+ ):
127
+ return False
128
+
129
+ # Handle user recursively
130
+ elif user:
131
+
132
+ # Iterate through projects
133
+ if not options.exclude_projects:
134
+ for user_project in sorted(
135
+ user.projects.list(
136
+ get_all=True,
137
+ order_by='path',
138
+ sort='asc',
139
+ ),
140
+ key=lambda item: item.path_with_namespace,
141
+ ):
142
+ if not Entrypoint.project(
143
+ options,
144
+ gitlab,
145
+ user_project.id,
146
+ ):
147
+ return False
111
148
 
112
149
  # Result
113
150
  return True
@@ -119,13 +156,13 @@ class Entrypoint:
119
156
  gitlab: GitLabFeature,
120
157
  criteria: str,
121
158
  subgroup: bool = False,
122
- ) -> None:
159
+ ) -> bool:
123
160
 
124
161
  # Acquire group
125
162
  group = gitlab.group(criteria)
126
163
 
127
164
  # Acquire parent group
128
- parent_group: Group = None
165
+ parent_group: GitLabGroup = None
129
166
  if group.parent_id:
130
167
  parent_group = gitlab.group(group.parent_id)
131
168
 
@@ -142,6 +179,17 @@ class Entrypoint:
142
179
  f'{Colors.YELLOW_LIGHT}{group.full_path} '
143
180
  f'{Colors.CYAN}({group.description})'
144
181
  f'{Colors.RESET}')
182
+ Platform.flush()
183
+
184
+ # Delete group
185
+ if options.delete_group:
186
+ gitlab.group_delete(criteria)
187
+ print(f'{Colors.BOLD} - Delete {group_type}: '
188
+ f'{Colors.GREEN}Success'
189
+ f'{Colors.RESET}')
190
+ print(' ')
191
+ Platform.flush()
192
+ return subgroup
145
193
 
146
194
  # Set group description
147
195
  if options.set_description:
@@ -149,27 +197,32 @@ class Entrypoint:
149
197
  print(f'{Colors.BOLD} - Set description: '
150
198
  f'{Colors.CYAN}{options.set_description}'
151
199
  f'{Colors.RESET}')
200
+ Platform.flush()
152
201
 
153
202
  # Update group description
154
203
  elif options.update_description:
155
204
  parent_description_text: str = ''
156
205
  if parent_name:
157
- parent_description_text = ' - ' + GitLabFeature.Helper.describe(
158
- parent_description,
159
- parent_name,
206
+ parent_description_text = ' - ' + Namespaces.describe(
207
+ name=parent_name,
208
+ description=parent_description,
160
209
  )
161
- if not parent_description_text or not group.description.endswith(
162
- f'{parent_description_text}'):
163
- description = f'{GitLabFeature.Helper.capitalize(group.name)}' \
164
- f'{parent_description_text}'
210
+ if not group.description or subgroup and ( \
211
+ not parent_description_text or \
212
+ not group.description.endswith(f'{parent_description_text}') \
213
+ ):
214
+ description = f'{Namespaces.describe(name=group.name)}' \
215
+ f'{parent_description_text}'
165
216
  gitlab.group_set_description(criteria, description)
166
217
  print(f'{Colors.BOLD} - Updated description: '
167
218
  f'{Colors.CYAN}{description}'
168
219
  f'{Colors.RESET}')
220
+ Platform.flush()
169
221
  else:
170
222
  print(f'{Colors.BOLD} - Kept description: '
171
223
  f'{Colors.GREEN}{group.description}'
172
224
  f'{Colors.RESET}')
225
+ Platform.flush()
173
226
 
174
227
  # Reset group members
175
228
  if options.reset_members:
@@ -177,6 +230,7 @@ class Entrypoint:
177
230
  print(f'{Colors.BOLD} - Reset members: '
178
231
  f'{Colors.GREEN}Success'
179
232
  f'{Colors.RESET}')
233
+ Platform.flush()
180
234
 
181
235
  # Set group avatar
182
236
  if options.set_avatar:
@@ -184,30 +238,42 @@ class Entrypoint:
184
238
  print(f'{Colors.BOLD} - Set avatar: '
185
239
  f'{Colors.CYAN}{options.set_avatar}'
186
240
  f'{Colors.RESET}')
241
+ Platform.flush()
187
242
 
188
243
  # Footer
189
244
  print(' ')
190
245
  Platform.flush()
191
246
 
192
- # Project, pylint: disable=too-many-branches
247
+ # Result
248
+ return True
249
+
250
+ # Project, pylint: disable=too-many-branches,too-many-statements
193
251
  @staticmethod
194
252
  def project(
195
253
  options: Namespace,
196
254
  gitlab: GitLabFeature,
197
255
  criteria: str,
198
- ) -> None:
256
+ ) -> bool:
199
257
 
200
258
  # Acquire project
201
259
  project = gitlab.project(criteria)
202
260
 
203
- # Acquire group
204
- group = gitlab.group(project.namespace['id'])
205
-
206
261
  # Show project details
207
262
  print(f'{Colors.BOLD} - GitLab project: '
208
263
  f'{Colors.YELLOW_LIGHT}{project.path_with_namespace} '
209
264
  f'{Colors.CYAN}({project.description})'
210
265
  f'{Colors.RESET}')
266
+ Platform.flush()
267
+
268
+ # Delete project
269
+ if options.delete_project:
270
+ gitlab.project_delete(criteria)
271
+ print(f'{Colors.BOLD} - Delete project: '
272
+ f'{Colors.GREEN}Success'
273
+ f'{Colors.RESET}')
274
+ print(' ')
275
+ Platform.flush()
276
+ return True
211
277
 
212
278
  # Set project description
213
279
  if options.set_description:
@@ -215,24 +281,34 @@ class Entrypoint:
215
281
  print(f'{Colors.BOLD} - Set description: '
216
282
  f'{Colors.CYAN}{options.set_description}'
217
283
  f'{Colors.RESET}')
284
+ Platform.flush()
218
285
 
219
286
  # Update project description
220
287
  elif options.update_description:
221
- group_description = GitLabFeature.Helper.describe(
222
- group.description,
223
- group.name,
224
- )
225
- if not project.description.endswith(f' - {group_description}'):
226
- description = f'{GitLabFeature.Helper.capitalize(project.name)}' \
227
- f' - {group_description}'
288
+ namespace_description: str
289
+ if project.namespace['kind'] == 'user':
290
+ namespace = gitlab.namespace(project.namespace['id'])
291
+ namespace_description = namespace.name
292
+ else:
293
+ group = gitlab.group(project.namespace['id'])
294
+ namespace_description = Namespaces.describe(
295
+ name=group.name,
296
+ description=group.description,
297
+ )
298
+ if not project.description or \
299
+ not project.description.endswith(f' - {namespace_description}'):
300
+ description = f'{Namespaces.describe(name=project.name)}' \
301
+ f' - {namespace_description}'
228
302
  gitlab.project_set_description(criteria, description)
229
303
  print(f'{Colors.BOLD} - Updated description: '
230
304
  f'{Colors.CYAN}{description}'
231
305
  f'{Colors.RESET}')
306
+ Platform.flush()
232
307
  else:
233
308
  print(f'{Colors.BOLD} - Kept description: '
234
309
  f'{Colors.GREEN}{project.description}'
235
310
  f'{Colors.RESET}')
311
+ Platform.flush()
236
312
 
237
313
  # Reset project features
238
314
  if options.reset_features:
@@ -241,10 +317,12 @@ class Entrypoint:
241
317
  print(f'{Colors.BOLD} - Reset features: '
242
318
  f'{Colors.CYAN}{features}'
243
319
  f'{Colors.RESET}')
320
+ Platform.flush()
244
321
  else:
245
322
  print(f'{Colors.BOLD} - Reset features: '
246
323
  f'{Colors.GREEN}Already done'
247
324
  f'{Colors.RESET}')
325
+ Platform.flush()
248
326
 
249
327
  # Reset project members
250
328
  if options.reset_members:
@@ -252,6 +330,7 @@ class Entrypoint:
252
330
  print(f'{Colors.BOLD} - Reset members: '
253
331
  f'{Colors.GREEN}Success'
254
332
  f'{Colors.RESET}')
333
+ Platform.flush()
255
334
 
256
335
  # Set project avatar
257
336
  if options.set_avatar:
@@ -259,6 +338,31 @@ class Entrypoint:
259
338
  print(f'{Colors.BOLD} - Set avatar: '
260
339
  f'{Colors.CYAN}{options.set_avatar}'
261
340
  f'{Colors.RESET}')
341
+ Platform.flush()
342
+
343
+ # Run project housekeeping
344
+ if options.run_housekeeping:
345
+ gitlab.project_run_housekeeping(criteria)
346
+ print(f'{Colors.BOLD} - Ran housekeeping: '
347
+ f'{Colors.GREEN}Success'
348
+ f'{Colors.RESET}')
349
+ Platform.flush()
350
+
351
+ # Archive project
352
+ if options.archive_project:
353
+ gitlab.project_set_archive(criteria, True)
354
+ print(f'{Colors.BOLD} - Archive project: '
355
+ f'{Colors.GREEN}Success'
356
+ f'{Colors.RESET}')
357
+ Platform.flush()
358
+
359
+ # Unarchive project
360
+ elif options.unarchive_project:
361
+ gitlab.project_set_archive(criteria, False)
362
+ print(f'{Colors.BOLD} - Unarchive project: '
363
+ f'{Colors.GREEN}Success'
364
+ f'{Colors.RESET}')
365
+ Platform.flush()
262
366
 
263
367
  # Protect project branches
264
368
  if options.protect_branches:
@@ -267,10 +371,12 @@ class Entrypoint:
267
371
  print(f'{Colors.BOLD} - Protecting branches: '
268
372
  f'{Colors.CYAN}{branches}'
269
373
  f'{Colors.RESET}')
374
+ Platform.flush()
270
375
  else:
271
376
  print(f'{Colors.BOLD} - Protecting branches: '
272
377
  f'{Colors.GREEN}Already done'
273
378
  f'{Colors.RESET}')
379
+ Platform.flush()
274
380
 
275
381
  # Protect project tags
276
382
  if options.protect_tags:
@@ -280,11 +386,16 @@ class Entrypoint:
280
386
  f'{Colors.CYAN}{tags}'
281
387
  f'{Colors.GREEN} (level: {options.protect_tags})'
282
388
  f'{Colors.RESET}')
389
+ Platform.flush()
283
390
  else:
284
391
  print(f'{Colors.BOLD} - Protecting tags: '
285
392
  f'{Colors.GREEN}Already done'
286
393
  f'{Colors.RESET}')
394
+ Platform.flush()
287
395
 
288
396
  # Footer
289
397
  print(' ')
290
398
  Platform.flush()
399
+
400
+ # Result
401
+ return True
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  # Standard libraries
4
- from argparse import (_ArgumentGroup, ArgumentParser, Namespace, RawTextHelpFormatter)
4
+ from argparse import (_ArgumentGroup, _MutuallyExclusiveGroup, ArgumentParser, Namespace,
5
+ RawTextHelpFormatter)
5
6
  from os import environ
6
7
  from shutil import get_terminal_size
7
8
  from sys import exit as sys_exit
@@ -19,6 +20,7 @@ def main() -> None:
19
20
  # Variables
20
21
  group: _ArgumentGroup
21
22
  result: bool = False
23
+ subgroup: _MutuallyExclusiveGroup
22
24
 
23
25
  # Arguments creation
24
26
  parser: ArgumentParser = ArgumentParser(
@@ -122,6 +124,40 @@ def main() -> None:
122
124
  help='Update description of GitLab projects and groups automatically',
123
125
  )
124
126
 
127
+ # Arguments advanced settings definitions
128
+ group = parser.add_argument_group('advanced settings arguments')
129
+ group.add_argument(
130
+ '--run-housekeeping',
131
+ dest='run_housekeeping',
132
+ action='store_true',
133
+ help='Run housekeeping of project or projects GitLab in groups',
134
+ )
135
+ subgroup = group.add_mutually_exclusive_group()
136
+ subgroup.add_argument(
137
+ '--archive-project',
138
+ dest='archive_project',
139
+ action='store_true',
140
+ help='Archive project or projects in GitLab groups',
141
+ )
142
+ subgroup.add_argument(
143
+ '--unarchive-project',
144
+ dest='unarchive_project',
145
+ action='store_true',
146
+ help='Unarchive project or projects in GitLab groups',
147
+ )
148
+ group.add_argument(
149
+ '--delete-group',
150
+ dest='delete_group',
151
+ action='store_true',
152
+ help='Delete group or groups in GitLab groups',
153
+ )
154
+ group.add_argument(
155
+ '--delete-project',
156
+ dest='delete_project',
157
+ action='store_true',
158
+ help='Delete project or projects in GitLab groups',
159
+ )
160
+
125
161
  # Arguments repository settings definitions
126
162
  group = parser.add_argument_group('repository settings arguments')
127
163
  group.add_argument(
@@ -151,7 +187,7 @@ def main() -> None:
151
187
  dest='path',
152
188
  action='store',
153
189
  nargs='?',
154
- help='GitLab group or project path',
190
+ help='GitLab group, user namespace or project path',
155
191
  )
156
192
 
157
193
  # Arguments parser
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  # Standard libraries
4
+ from time import sleep
4
5
  from typing import List
5
6
 
6
7
  # Modules libraries
7
8
  from gitlab import Gitlab
8
- from gitlab.exceptions import GitlabListError
9
- from gitlab.v4.objects import Group, Project
9
+ from gitlab.exceptions import GitlabGetError, GitlabListError
10
+ from gitlab.v4.objects import Group, Namespace, Project, User
10
11
 
11
12
  # GitLabFeature class
12
13
  class GitLabFeature:
@@ -25,12 +26,21 @@ class GitLabFeature:
25
26
  def group(self, criteria: str) -> Group:
26
27
  return self.__gitlab.groups.get(criteria)
27
28
 
29
+ # Group delete
30
+ def group_delete(self, criteria: str) -> None:
31
+
32
+ # Delete group
33
+ if not self.__dry_run:
34
+ group = self.group(criteria)
35
+ group.delete()
36
+ sleep(10)
37
+
28
38
  # Group reset members
29
39
  def group_reset_members(self, criteria: str) -> None:
30
40
 
31
41
  # Remove group members
32
42
  group = self.group(criteria)
33
- for member in group.members.list():
43
+ for member in group.members.list(get_all=True):
34
44
  if not self.__dry_run:
35
45
  group.members.delete(member.id)
36
46
 
@@ -61,10 +71,23 @@ class GitLabFeature:
61
71
  # Save group
62
72
  group.save()
63
73
 
74
+ # Namespace
75
+ def namespace(self, criteria: str) -> Namespace:
76
+ return self.__gitlab.namespaces.get(criteria)
77
+
64
78
  # Project
65
79
  def project(self, criteria: str) -> Project:
66
80
  return self.__gitlab.projects.get(criteria)
67
81
 
82
+ # Project delete
83
+ def project_delete(self, criteria: str) -> None:
84
+
85
+ # Delete project
86
+ project = self.project(criteria)
87
+ if not self.__dry_run:
88
+ project.delete()
89
+ sleep(5)
90
+
68
91
  # Project protect branches
69
92
  def project_protect_branches(self, criteria: str) -> List[str]:
70
93
 
@@ -72,14 +95,15 @@ class GitLabFeature:
72
95
  result: List[str] = []
73
96
  project = self.project(criteria)
74
97
  try:
75
- assert project.branches.list()
98
+ assert project.branches.list(get_all=True)
76
99
  except (AssertionError, GitlabListError):
77
100
  return result
78
101
 
79
102
  # Acquire project, branches and protected branches
80
- branches = [branch.name for branch in project.branches.list()]
103
+ branches = [branch.name for branch in project.branches.list(get_all=True)]
81
104
  protectedbranches = [
82
- protectedbranch.name for protectedbranch in project.protectedbranches.list()
105
+ protectedbranch.name
106
+ for protectedbranch in project.protectedbranches.list(get_all=True)
83
107
  ]
84
108
 
85
109
  # Protect main/master
@@ -132,7 +156,7 @@ class GitLabFeature:
132
156
  result: List[str] = []
133
157
  project = self.project(criteria)
134
158
  try:
135
- assert project.tags.list()
159
+ assert project.tags.list(get_all=True)
136
160
  except (AssertionError, GitlabListError):
137
161
  return result
138
162
 
@@ -151,11 +175,11 @@ class GitLabFeature:
151
175
 
152
176
  # Acquire protected tags
153
177
  protectedtags = [
154
- protectedtag.name for protectedtag in project.protectedtags.list()
178
+ protectedtag.name for protectedtag in project.protectedtags.list(get_all=True)
155
179
  ]
156
180
 
157
181
  # Update protected tags
158
- for protectedtag in project.protectedtags.list():
182
+ for protectedtag in project.protectedtags.list(get_all=True):
159
183
  protectedtag_level = protectedtag.create_access_levels[0]['access_level']
160
184
  if protectedtag_level != 0 and (access_level == 0
161
185
  or protectedtag_level < access_level):
@@ -169,7 +193,7 @@ class GitLabFeature:
169
193
  result += [name]
170
194
 
171
195
  # Protect unprotected tags
172
- for tag in project.tags.list():
196
+ for tag in project.tags.list(get_all=True):
173
197
  if tag.name not in protectedtags:
174
198
  if not self.__dry_run:
175
199
  project.protectedtags.create({
@@ -193,80 +217,145 @@ class GitLabFeature:
193
217
  result: List[str] = []
194
218
  project = self.__gitlab.projects.get(criteria, statistics=True)
195
219
 
196
- # Disable unused features
220
+ # Disable unused feature (Issues)
197
221
  if project.issues_enabled and not project.issues.list(get_all=False):
198
222
  project.issues_enabled = False
199
223
  result += ['Issues']
224
+
225
+ # Disable unused feature (Repository)
200
226
  try:
201
227
  assert project.commits.list(get_all=False)
202
228
  except (AssertionError, GitlabListError):
203
229
  if project.repository_access_level != 'disabled':
204
230
  project.repository_access_level = 'disabled'
205
231
  result += ['Repository']
206
- if project.merge_requests_enabled and not project.mergerequests.list(
207
- get_all=False):
208
- project.merge_requests_enabled = False
209
- result += ['Merge requests']
232
+
233
+ # Disable unused feature (Merge requests)
234
+ try:
235
+ if not project.mergerequests.list(get_all=False):
236
+ pass
237
+ elif len(project.branches.list(get_all=False)) > 1:
238
+ pass
239
+ else:
240
+ assert False
241
+ except (AssertionError, GitlabListError):
242
+ if project.merge_requests_enabled:
243
+ project.merge_requests_enabled = False
244
+ result += ['Merge requests']
245
+
246
+ # Disable unused feature (Forks)
210
247
  if project.forking_access_level != 'disabled' and not project.forks.list(
211
248
  get_all=False):
212
249
  project.forking_access_level = 'disabled'
213
250
  result += ['Forks']
251
+
252
+ # Disable unused feature (Git LFS)
214
253
  if project.lfs_enabled and project.statistics['lfs_objects_size'] == 0:
215
254
  project.lfs_enabled = False
216
255
  result += ['Git LFS']
217
- if project.jobs_enabled and not project.jobs.list(get_all=False):
218
- project.jobs_enabled = False
219
- result += ['CI/CD']
256
+
257
+ # Disable unused feature (CI/CD)
258
+ try:
259
+ if project.jobs.list(get_all=False):
260
+ pass
261
+ elif any('name' in item and item['name'] == '.gitlab-ci.yml'
262
+ for item in project.repository_tree(get_all=True)):
263
+ pass
264
+ elif any('path_with_namespace' in item
265
+ and item['path_with_namespace'] != project.path_with_namespace
266
+ for item in self.__gitlab.http_get(
267
+ f'/projects/{project.id}/job_token_scope/allowlist',
268
+ get_all=True,
269
+ )):
270
+ pass
271
+ else:
272
+ assert False
273
+ except (AssertionError, GitlabGetError, GitlabListError):
274
+ if project.jobs_enabled:
275
+ project.jobs_enabled = False
276
+ result += ['CI/CD']
277
+
278
+ # Disable unused feature (Container registry)
220
279
  if project.container_registry_enabled and not project.repositories.list(
221
280
  get_all=False):
222
281
  project.container_registry_enabled = False
223
282
  result += ['Container registry']
283
+
284
+ # Disable unused feature (Analytics)
224
285
  if project.analytics_access_level != 'disabled':
225
286
  project.analytics_access_level = 'disabled'
226
287
  result += ['Analytics']
288
+
289
+ # Disable unused feature (Security and Compliance)
227
290
  if project.security_and_compliance_access_level != 'disabled':
228
291
  project.security_and_compliance_access_level = 'disabled'
229
292
  result += ['Security and Compliance']
293
+
294
+ # Disable unused feature (Wiki)
230
295
  if project.wiki_enabled and not project.wikis.list(get_all=False):
231
296
  project.wiki_enabled = False
232
297
  result += ['Wiki']
298
+
299
+ # Disable unused feature (Snippets)
233
300
  if project.snippets_enabled and not project.snippets.list(get_all=False):
234
301
  project.snippets_enabled = False
235
302
  result += ['Snippets']
303
+
304
+ # Disable unused feature (Package registry)
236
305
  if project.packages_enabled and not project.packages.list(get_all=False):
237
306
  project.packages_enabled = False
238
307
  result += ['Package registry']
308
+
309
+ # Disable unused feature (Model experiments)
239
310
  if project.model_experiments_access_level != 'disabled':
240
311
  project.model_experiments_access_level = 'disabled'
241
312
  result += ['Model experiments']
313
+
314
+ # Disable unused feature (Model registry)
242
315
  if project.model_registry_access_level != 'disabled':
243
316
  project.model_registry_access_level = 'disabled'
244
317
  result += ['Model registry']
318
+
319
+ # Disable unused feature (Pages)
245
320
  if project.pages_access_level != 'disabled':
246
321
  project.pages_access_level = 'disabled'
247
322
  result += ['Pages']
323
+
324
+ # Disable unused feature (Monitor)
248
325
  if project.monitor_access_level != 'disabled':
249
326
  project.monitor_access_level = 'disabled'
250
327
  result += ['Monitor']
328
+
329
+ # Disable unused feature (Environments)
251
330
  try:
252
331
  assert project.environments.list(get_all=False)
253
332
  except (AssertionError, GitlabListError):
254
333
  if project.environments_access_level != 'disabled':
255
334
  project.environments_access_level = 'disabled'
256
335
  result += ['Environments']
336
+
337
+ # Disable unused feature (Feature flags)
257
338
  if project.feature_flags_access_level != 'disabled':
258
339
  project.feature_flags_access_level = 'disabled'
259
340
  result += ['Feature flags']
341
+
342
+ # Disable unused feature (Infrastructure)
260
343
  if project.infrastructure_access_level != 'disabled':
261
344
  project.infrastructure_access_level = 'disabled'
262
345
  result += ['Infrastructure']
346
+
347
+ # Disable unused feature (Releases)
263
348
  if project.releases_access_level != 'disabled' and not project.releases.list(
264
349
  get_all=False):
265
350
  project.releases_access_level = 'disabled'
266
351
  result += ['Releases']
352
+
353
+ # Disable unused feature (Service Desk)
267
354
  if project.service_desk_enabled:
268
355
  project.service_desk_enabled = False
269
356
  result += ['Service Desk']
357
+
358
+ # Disable unused feature (Auto DevOps)
270
359
  if project.auto_devops_enabled:
271
360
  project.auto_devops_enabled = False
272
361
  result += ['Auto DevOps']
@@ -284,12 +373,33 @@ class GitLabFeature:
284
373
  # Remove project members
285
374
  if not self.__dry_run:
286
375
  project = self.project(criteria)
287
- for member in project.members.list():
376
+ for member in project.members.list(get_all=True):
288
377
  project.members.delete(member.id)
289
378
 
290
379
  # Save project
291
380
  project.save()
292
381
 
382
+ # Project run housekeeping
383
+ def project_run_housekeeping(self, criteria: str) -> None:
384
+
385
+ # Run project housekeeping
386
+ if not self.__dry_run:
387
+ project = self.project(criteria)
388
+ project.housekeeping()
389
+
390
+ # Project set archive
391
+ def project_set_archive(self, criteria: str, enabled: bool) -> None:
392
+
393
+ # Archive project
394
+ if not self.__dry_run and enabled:
395
+ project = self.project(criteria)
396
+ project.archive()
397
+
398
+ # Unarchive project
399
+ elif not self.__dry_run:
400
+ project = self.project(criteria)
401
+ project.unarchive()
402
+
293
403
  # Project set avatar
294
404
  def project_set_avatar(self, criteria: str, file: str) -> None:
295
405
 
@@ -313,25 +423,11 @@ class GitLabFeature:
313
423
  # Save project
314
424
  project.save()
315
425
 
426
+ # User
427
+ def user(self, criteria: str) -> User:
428
+ return self.__gitlab.users.list(username=criteria)[0]
429
+
316
430
  # URL
317
431
  @property
318
432
  def url(self) -> str:
319
433
  return str(self.__gitlab.api_url)
320
-
321
- # Helper class
322
- class Helper:
323
-
324
- # Capitalize
325
- @staticmethod
326
- def capitalize(text: str) -> str:
327
- return f'{text[:1].capitalize()}{text[1:]}'
328
-
329
- # Describe
330
- @staticmethod
331
- def describe(
332
- description: str,
333
- name: str,
334
- ) -> str:
335
- if description:
336
- return description
337
- return GitLabFeature.Helper.capitalize(name)
File without changes
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Standard libraries
4
+ from re import sub
5
+
6
+ # Namespaces class
7
+ class Namespaces:
8
+
9
+ # Capitalize
10
+ @staticmethod
11
+ def capitalize(text: str, words: bool = False) -> str:
12
+
13
+ # Capitalize words
14
+ if words:
15
+ return ' '.join(
16
+ Namespaces.capitalize(word, words=False) for word in text.split())
17
+
18
+ # Capitalize text
19
+ return f'{text[:1].capitalize()}{text[1:]}'
20
+
21
+ # Describe
22
+ @staticmethod
23
+ def describe(
24
+ name: str,
25
+ description: str = '',
26
+ ) -> str:
27
+
28
+ # Use description
29
+ if description:
30
+ return description
31
+
32
+ # Adapt name
33
+ return Namespaces.capitalize(sub(r'[-_]', ' ', name), words=True)