Coverage for src/cli/main.py: 93.16%
117 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 21:33 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 21:33 +0000
1#!/usr/bin/env python3
3# Standard libraries
4from argparse import (
5 _ArgumentGroup,
6 _MutuallyExclusiveGroup,
7 ArgumentParser,
8 Namespace,
9 RawTextHelpFormatter,
10 SUPPRESS,
11)
12from os import environ
13from pathlib import Path
14from shutil import get_terminal_size
15from sys import exit as sys_exit
17# Components
18from ..package.bundle import Bundle
19from ..package.settings import Settings
20from ..package.updates import Updates
21from ..package.version import Version
22from ..prints.colors import Colors
23from ..system.platform import Platform
24from ..types.environments import Environments
25from ..types.gitlab import MigrationEntities
26from ..types.paths import Paths
27from .entrypoint import Entrypoint
29# Constants
30HELP_POSITION: int = 29
32# Main, pylint: disable=too-many-branches,too-many-statements
33def main() -> None:
35 # Variables
36 environments: Environments
37 group: _ArgumentGroup
38 result: Entrypoint.Result = Entrypoint.Result.ERROR
39 subgroup: _MutuallyExclusiveGroup
41 # Configure environment variables
42 environments = Environments()
43 environments.group = 'environment variables'
44 environments.add(
45 'gitlab_input_token',
46 Bundle.ENV_GITLAB_INPUT_TOKEN,
47 'Input GitLab API token environment variable',
48 Bundle.ENV_GITLAB_TOKEN,
49 )
50 environments.add(
51 'gitlab_output_token',
52 Bundle.ENV_GITLAB_OUTPUT_TOKEN,
53 'Output GitLab API token environment variable',
54 Bundle.ENV_GITLAB_TOKEN,
55 )
56 environments.add(
57 'ci_job_token',
58 Bundle.ENV_CI_JOB_TOKEN,
59 'GitLab CI job token environment variable (CI only)',
60 )
62 # Arguments creation
63 parser: ArgumentParser = ArgumentParser(
64 prog=Bundle.NAME,
65 description=f'{Bundle.NAME}: {Bundle.DESCRIPTION}',
66 epilog=environments.help(HELP_POSITION),
67 add_help=False,
68 formatter_class=lambda prog: RawTextHelpFormatter(
69 prog,
70 max_help_position=HELP_POSITION,
71 width=min(
72 120,
73 get_terminal_size().columns - 2,
74 ),
75 ),
76 )
78 # Arguments internal definitions
79 group = parser.add_argument_group('internal arguments')
80 group.add_argument(
81 '-h',
82 '--help',
83 dest='help',
84 action='store_true',
85 help='Show this help message',
86 )
87 group.add_argument(
88 '--version',
89 dest='version',
90 action='store_true',
91 help='Show the current version',
92 )
93 group.add_argument(
94 '--no-color',
95 dest='no_color',
96 action='store_true',
97 help=f'Disable colors outputs with \'{Bundle.ENV_NO_COLOR}=1\'\n'
98 '(or default settings: [themes] > no_color)',
99 )
100 group.add_argument(
101 '--update-check',
102 dest='update_check',
103 action='store_true',
104 help='Check for newer package updates',
105 )
106 group.add_argument(
107 '--settings',
108 dest='settings',
109 action='store_true',
110 help='Show the current settings path and contents',
111 )
112 group.add_argument(
113 '--set',
114 dest='set',
115 action='store',
116 metavar=('GROUP', 'KEY', 'VAL'),
117 nargs=3,
118 help='Set settings specific \'VAL\' value to [GROUP] > KEY\n' \
119 'or unset by using \'UNSET\' as \'VAL\'',
120 )
122 # Arguments credentials definitions
123 group = parser.add_argument_group('credentials arguments')
124 group.add_argument(
125 '-c',
126 '--config',
127 dest='configs',
128 action='append',
129 metavar='FILES',
130 help=f'Python GitLab configuration files'
131 f' (default: {Bundle.ENV_PYTHON_GITLAB_CFG} environment)',
132 )
134 # Arguments migration definitions
135 group = parser.add_argument_group('migration arguments')
136 group.add_argument(
137 '--archive-exports',
138 dest='archive_exports',
139 action='store',
140 metavar='FOLDER',
141 help='Store exported projects and groups to a folder',
142 )
143 subgroup = group.add_mutually_exclusive_group()
144 subgroup.add_argument(
145 '--archive-sources',
146 dest='archive_sources',
147 action='store_true',
148 help='Archive sources after successful migration',
149 )
150 subgroup.add_argument(
151 '--delete-sources',
152 dest='delete_sources',
153 action='store_true',
154 help='Delete sources after successful migration',
155 )
156 group.add_argument(
157 '--dry-run',
158 dest='dry_run',
159 action='store_true',
160 help='Enable dry run mode to check without saving',
161 )
162 group.add_argument(
163 '--exclude-group',
164 dest='exclude_group',
165 action='store_true',
166 help='Exclude parent group migration',
167 )
168 group.add_argument(
169 '--exclude-subgroups',
170 dest='exclude_subgroups',
171 action='store_true',
172 help='Exclude children subgroups migration',
173 )
174 group.add_argument(
175 '--exclude-projects',
176 dest='exclude_projects',
177 action='store_true',
178 help='Exclude children projects migration',
179 )
180 group.add_argument(
181 '--flatten-group',
182 dest='flatten_group',
183 action='store_true',
184 help='Flatten group projects upon migration',
185 )
186 group.add_argument(
187 '--migrate-packages',
188 dest='migrate_packages',
189 action='store_true',
190 help='Migrate input GitLab Packages to output GitLab projects',
191 )
192 group.add_argument(
193 '--confirm',
194 dest='confirm',
195 action='store_true',
196 help='Automatically confirm all removal and contents warnings',
197 )
198 group.add_argument(
199 '--overwrite',
200 dest='overwrite',
201 action='store_true',
202 help='Overwrite existing projects on output GitLab',
203 )
204 group.add_argument(
205 '--rename-project',
206 dest='rename_project',
207 action='store',
208 metavar='NAME',
209 help='Rename GitLab output project (only for single input project)',
210 )
211 group.add_argument(
212 '--normalize-names',
213 dest='normalize_names',
214 action='store_true',
215 help='Normalize GitLab output group and project names',
216 )
217 group.add_argument(
218 '--available-entities',
219 dest='available_entities',
220 action='store_true',
221 help='List the available GitLab export/import entities known by the tool',
222 )
223 group.add_argument(
224 '--reset-entities',
225 dest='reset_entities',
226 default=','.join(MigrationEntities.defaults()),
227 metavar='ENTITIES',
228 help='List of GitLab export/import entities to reset separated by ","'
229 ' (default: %(default)s)',
230 )
232 # Arguments general settings definitions
233 group = parser.add_argument_group('general settings arguments')
234 group.add_argument(
235 '--set-avatar',
236 dest='set_avatar',
237 action='store',
238 metavar='FILE',
239 help='Set avatar of GitLab output projects and groups',
240 )
241 group.add_argument(
242 '--update-descriptions',
243 dest='update_description',
244 action='store_true',
245 help='Update description of GitLab output projects and groups automatically',
246 )
248 # Arguments hidden definitions
249 group = parser.add_argument_group('hidden arguments')
250 group.add_argument(
251 '--archive-exports-dir',
252 dest='archive_exports_dir',
253 action='store',
254 default=None,
255 help=SUPPRESS,
256 )
258 # Arguments positional definitions
259 group = parser.add_argument_group('positional arguments')
260 group.add_argument(
261 '--',
262 dest='double_dash',
263 action='store_true',
264 help='Positional arguments separator (recommended)',
265 )
266 group.add_argument(
267 dest='input_url_path',
268 action='store',
269 nargs='?',
270 help='Input GitLab group or project path URL',
271 )
272 group.add_argument(
273 dest='output_url_namespace',
274 action='store',
275 nargs='?',
276 help='Output GitLab group or user namespace URL',
277 )
278 group.add_argument(
279 dest='rename_project_positional',
280 action='store',
281 nargs='?',
282 metavar='rename_single_project',
283 help='Rename GitLab output project (only for single input project)',
284 )
286 # Arguments parser
287 options: Namespace = parser.parse_args()
289 # Help informations
290 if options.help:
291 print(' ')
292 parser.print_help()
293 print(' ')
294 Platform.flush()
295 sys_exit(0)
297 # Instantiate settings
298 settings: Settings = Settings(name=Bundle.NAME)
300 # Prepare no_color
301 if not options.no_color:
302 if settings.has('themes', 'no_color'):
303 options.no_color = settings.get_bool('themes', 'no_color')
304 else:
305 options.no_color = False
306 settings.set_bool('themes', 'no_color', options.no_color)
308 # Configure no_color
309 if options.no_color:
310 environ[Bundle.ENV_FORCE_COLOR] = '0'
311 environ[Bundle.ENV_NO_COLOR] = '1'
313 # Prepare colors
314 Colors.prepare()
316 # Settings setter
317 if options.set:
318 settings.set(options.set[0], options.set[1], options.set[2])
319 settings.show()
320 sys_exit(0)
322 # Settings informations
323 if options.settings:
324 settings.show()
325 sys_exit(0)
327 # Instantiate updates
328 updates: Updates = Updates(
329 name=Bundle.PACKAGE,
330 settings=settings,
331 )
333 # Version informations
334 if options.version:
335 print(
336 f'{Bundle.NAME} {Version.get()} from {Version.path()} (python {Version.python()})'
337 )
338 Platform.flush()
339 sys_exit(0)
341 # Check for current updates
342 if options.update_check:
343 if not updates.check():
344 updates.check(older=True)
345 sys_exit(0)
347 # Adapt rename project arguments
348 if options.rename_project_positional and not options.rename_project:
349 options.rename_project = options.rename_project_positional
351 # Arguments validation
352 if options.input_url_path and not options.output_url_namespace and (
353 options.archive_exports # Export input to archives
354 or options.archive_sources # Archive input sources
355 or options.delete_sources # Delete input sources
356 ):
357 pass
358 elif not options.input_url_path or not options.output_url_namespace:
359 result = Entrypoint.Result.CRITICAL
361 # Prepare archive exports
362 if options.archive_exports:
363 options.archive_exports_dir = Paths.resolve(Path('.') / options.archive_exports)
364 Path(options.archive_exports_dir).mkdir(parents=True, exist_ok=True)
366 # Header
367 print(' ')
368 Platform.flush()
370 # Tool identifier
371 if result != Entrypoint.Result.CRITICAL:
372 print(f'{Colors.BOLD} {Bundle.NAME}'
373 f'{Colors.YELLOW_LIGHT} ({Version.get()})'
374 f'{Colors.RESET}')
375 Platform.flush()
377 # CLI entrypoint
378 if result != Entrypoint.Result.CRITICAL:
379 result = Entrypoint.cli(
380 options,
381 environments,
382 )
384 # CLI helper
385 else:
386 parser.print_help()
388 # Footer
389 print(' ')
390 Platform.flush()
392 # Check for daily updates
393 if updates.enabled and updates.daily:
394 updates.check()
396 # Result
397 if result in [
398 Entrypoint.Result.SUCCESS,
399 Entrypoint.Result.FINALIZE,
400 ]:
401 sys_exit(0)
402 else:
403 sys_exit(1)
405# Entrypoint
406if __name__ == '__main__': # pragma: no cover
407 main()