Coverage for src/cli/entrypoint.py: 6.04%
563 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 Namespace
5from enum import Enum
6import re
7from tempfile import NamedTemporaryFile
8from typing import List, NamedTuple, Optional
9import urllib.parse
11# Modules libraries
12from gitlab.config import ConfigError, GitlabConfigParser
13from gitlab.exceptions import (
14 GitlabGetError,
15 GitlabListError,
16)
17from gitlab.v4.objects import (
18 Group as GitLabGroup,
19 GroupProject as GitLabGroupProject,
20 Namespace as GitLabNamespace,
21 Project as GitLabProject,
22 ProjectImport as GitLabProjectImport,
23 User as GitLabUser,
24 UserProject as GitLabUserProject,
25)
26import questionary
28# Components
29from ..features.gitlab import GitLabFeature
30from ..package.bundle import Bundle
31from ..prints.colors import Colors
32from ..prints.themes import Themes
33from ..system.platform import Platform
34from ..types.environments import Environments
35from ..types.humanize import Humanize
36from ..types.gitlab import MigrationEntities
37from ..types.namespaces import Namespaces
38from ..types.paths import Paths
40# Entrypoint class, pylint: disable=too-many-lines,too-few-public-methods,too-many-nested-blocks,too-many-statements
41class Entrypoint:
43 # Constants
44 EXPORTS_PREFIX: str = f'{Bundle.NAME}-exports-'
45 PACKAGES_PREFIX: str = f'{Bundle.NAME}-packages-'
47 # Enumerations
48 Result = Enum('Result', [
49 'SUCCESS',
50 'FINALIZE',
51 'ERROR',
52 'CRITICAL',
53 ])
55 # CLI, pylint: disable=too-many-branches,too-many-locals,too-many-return-statements
56 @staticmethod
57 def cli(
58 options: Namespace,
59 environments: Environments,
60 ) -> Result:
62 # Variables
63 changed: bool = False
64 input_gitlab: GitLabFeature
65 input_group: Optional[GitLabGroup] = None
66 input_project: Optional[GitLabProject] = None
67 input_user: Optional[GitLabUser] = None
68 output_enabled: bool = bool(options.output_url_namespace)
69 output_exists: bool = False
70 output_gitlab: Optional[GitLabFeature] = None
71 output_gitlab_id: str
72 output_gitlab_namespace: str = ''
73 output_gitlab_url: str
74 output_namespace: Optional[GitLabNamespace] = None
75 progress_count: int
76 progress_index: int
77 project: GitLabGroupProject | GitLabUserProject
78 projects: List[GitLabGroupProject] | List[GitLabUserProject]
79 result: Entrypoint.Result
81 # Header
82 print(' ')
84 # Parse input URL variables
85 input_gitlab_splits: urllib.parse.SplitResult = urllib.parse.urlsplit(
86 options.input_url_path)
87 input_gitlab_id: str = f'{input_gitlab_splits.netloc}'
88 input_gitlab_url: str = f'{input_gitlab_splits.scheme}://{input_gitlab_splits.netloc}'
89 input_gitlab_path: str = input_gitlab_splits.path.lstrip('/')
91 # Parse output URL variables
92 if output_enabled:
93 output_gitlab_splits: urllib.parse.SplitResult = urllib.parse.urlsplit(
94 options.output_url_namespace)
95 output_gitlab_id = f'{output_gitlab_splits.netloc}'
96 output_gitlab_url = f'{output_gitlab_splits.scheme}://{output_gitlab_splits.netloc}'
97 output_gitlab_namespace = output_gitlab_splits.path.lstrip('/')
99 # Prepare credentials
100 input_private_token: str = environments.value('gitlab_input_token')
101 output_private_token: str = environments.value('gitlab_output_token')
102 job_token: str = environments.value('ci_job_token')
103 input_ssl_verify: bool | str = True
104 output_ssl_verify: bool | str = True
106 # Parse configuration files
107 try:
108 input_config: GitlabConfigParser
109 if not input_private_token:
110 input_config = GitlabConfigParser(input_gitlab_id, options.configs)
111 input_private_token = str(input_config.private_token)
112 if input_ssl_verify and (not input_config.ssl_verify
113 or isinstance(input_config.ssl_verify, str)):
114 input_ssl_verify = input_config.ssl_verify
115 output_config: GitlabConfigParser
116 if not output_private_token and output_enabled:
117 output_config = GitlabConfigParser(output_gitlab_id, options.configs)
118 output_private_token = str(output_config.private_token)
119 if output_ssl_verify and (not output_config.ssl_verify
120 or isinstance(output_config.ssl_verify, str)):
121 output_ssl_verify = output_config.ssl_verify
122 except ConfigError as e:
123 print(str(e))
125 # Input client
126 input_gitlab = GitLabFeature(
127 url=input_gitlab_url,
128 private_token=input_private_token,
129 job_token=job_token,
130 ssl_verify=input_ssl_verify,
131 dry_run=options.dry_run,
132 )
133 print(f'{Colors.BOLD} - GitLab input: '
134 f'{Colors.GREEN}{input_gitlab.url}'
135 f'{Colors.CYAN} ({input_gitlab.username})'
136 f'{Colors.RESET}')
137 Platform.flush()
139 # Output client
140 if output_enabled:
141 output_gitlab = GitLabFeature(
142 url=output_gitlab_url,
143 private_token=output_private_token,
144 job_token=job_token,
145 ssl_verify=output_ssl_verify,
146 dry_run=options.dry_run,
147 )
148 print(f'{Colors.BOLD} - GitLab output: '
149 f'{Colors.GREEN}{output_gitlab.url}'
150 f'{Colors.CYAN} ({output_gitlab.username})'
151 f'{Colors.RESET}')
152 print(' ')
153 Platform.flush()
155 # Input path
156 try:
157 input_group = input_gitlab.group(input_gitlab_path)
158 print(f'{Colors.BOLD} - GitLab input group: '
159 f'{Colors.GREEN}{input_group.full_path}'
160 f'{Colors.CYAN} # {Namespaces.text(input_group.description)}'
161 f'{Colors.RESET}')
162 Platform.flush()
163 except GitlabGetError as exception:
164 try:
165 if '/' in input_gitlab_path:
166 raise TypeError from exception
167 input_user = input_gitlab.user(input_gitlab_path)
168 input_namespace = input_gitlab.namespace(input_gitlab_path)
169 print(f'{Colors.BOLD} - GitLab input user namespace: '
170 f'{Colors.GREEN}{input_namespace.full_path}'
171 f'{Colors.CYAN} # {input_namespace.name}'
172 f'{Colors.RESET}')
173 print(' ')
174 Platform.flush()
175 except (GitlabGetError, TypeError):
176 input_project = input_gitlab.project(input_gitlab_path)
177 print(f'{Colors.BOLD} - GitLab input project: '
178 f'{Colors.GREEN}{input_project.path_with_namespace}'
179 f'{Colors.CYAN} # {Namespaces.text(input_project.description)}'
180 f'{Colors.RESET}')
181 Platform.flush()
183 # Output group
184 if output_enabled:
185 assert output_gitlab
186 try:
187 output_exists = False
188 output_group = output_gitlab.group(output_gitlab_namespace)
189 output_namespace = output_gitlab.namespace(output_gitlab_namespace)
190 output_exists = True
191 print(f'{Colors.BOLD} - GitLab output group: '
192 f'{Colors.GREEN}{output_group.full_path}'
193 f'{Colors.CYAN} # {Namespaces.text(output_group.description)}'
194 f'{Colors.RESET}')
195 print(' ')
196 Platform.flush()
198 # Output namespace
199 except GitlabGetError as exception:
200 try:
201 if '/' in output_gitlab_namespace:
202 raise TypeError from exception
203 _output_user = output_gitlab.user(output_gitlab_namespace)
204 output_namespace = output_gitlab.namespace(output_gitlab_namespace)
205 output_exists = True
206 print(f'{Colors.BOLD} - GitLab output user namespace: '
207 f'{Colors.GREEN}{output_namespace.full_path}'
208 f'{Colors.CYAN} # {output_namespace.name}'
209 f'{Colors.RESET}')
210 print(' ')
211 Platform.flush()
213 # Output parent group
214 except (GitlabGetError, TypeError): # pylint: disable=raise-missing-from
216 # Validate options
217 if (not input_group and not input_user) or options.exclude_group:
218 raise exception
220 # Missing output group
221 print(f'{Colors.BOLD} - GitLab output group: '
222 f'{Colors.GREEN}{output_gitlab_namespace}'
223 f'{Colors.RED} (Non-existent output group)'
224 f'{Colors.RESET}')
225 print(' ')
226 Platform.flush()
228 # Validate options
229 if options.rename_project and not input_project:
230 raise RuntimeError(
231 'Renaming project is only allowed with a single input project')
233 # Handle available features
234 if options.available_entities:
235 print(f'{Colors.BOLD} - GitLab project:'
236 f'{Colors.RESET}')
237 print(f'{Colors.BOLD} - Available entities: '
238 f'{Colors.CYAN}\'{", ".join(MigrationEntities.entities())}\''
239 f'{Colors.RESET}')
240 print(f'{Colors.BOLD} - Available removers: '
241 f'{Colors.CYAN}\'{", ".join(MigrationEntities.removers())}\''
242 f'{Colors.RESET}')
243 print(f'{Colors.BOLD} - Available templates: '
244 f'{Colors.CYAN}\'{", ".join(MigrationEntities.templates())}\''
245 f'{Colors.RESET}')
246 Platform.flush()
247 return Entrypoint.Result.FINALIZE
249 # Handle single project
250 if input_project:
252 # Validate types
253 if output_enabled:
254 assert output_gitlab
255 assert output_namespace
257 # Handle project
258 Entrypoint.project(
259 options,
260 input_gitlab,
261 output_gitlab,
262 input_project.path_with_namespace,
263 input_project.namespace['id'],
264 output_namespace.full_path if output_namespace else '',
265 output_gitlab_namespace,
266 options.rename_project,
267 1,
268 1,
269 )
271 # Handle sources
272 if options.archive_sources or options.delete_sources:
273 print(f'{Colors.BOLD} - GitLab input project: '
274 f'{Colors.GREEN}{input_project.path_with_namespace}'
275 f'{Colors.CYAN} # {Namespaces.text(input_project.description)}'
276 f'{Colors.RESET}')
277 Platform.flush()
279 # Archive input project
280 if options.archive_sources:
281 changed = input_gitlab.project_set_archive(
282 input_project.path_with_namespace,
283 enabled=True,
284 )
285 print(f'{Colors.BOLD} - Archive sources project: '
286 f'{Colors.CYAN if changed else Colors.GREEN}'
287 f'{"Success" if changed else "Already done"}'
288 f'{Colors.RESET}')
289 print(' ')
290 Platform.flush()
292 # Delete input project
293 elif options.delete_sources:
295 # Confirm project deletion
296 if not Entrypoint.confirm(
297 'Delete project',
298 input_project.path_with_namespace,
299 not options.confirm,
300 'deletion',
301 ):
302 raise PermissionError()
304 # Delete input project
305 input_gitlab.project_delete(input_project.path_with_namespace)
306 print(f'{Colors.BOLD} - Delete sources project: '
307 f'{Colors.GREEN}Success'
308 f'{Colors.RESET}')
309 print(' ')
310 Platform.flush()
312 # Handle group recursively
313 elif input_group:
315 # Handle group if missing
316 if not options.exclude_group:
317 result = Entrypoint.group(
318 options,
319 input_gitlab,
320 output_gitlab,
321 input_group.full_path,
322 output_gitlab_namespace,
323 migration=not output_exists,
324 )
325 if result in [
326 Entrypoint.Result.FINALIZE,
327 Entrypoint.Result.ERROR,
328 ]:
329 return result
331 # Acquire output namespace
332 if output_gitlab:
333 output_namespace = output_gitlab.namespace(
334 output_gitlab_namespace,
335 optional=True,
336 )
338 # Validate types
339 if output_enabled:
340 assert output_gitlab
341 assert output_namespace
343 # Iterate through subgroups
344 if output_gitlab and not options.exclude_subgroups and not options.flatten_group \
345 or not output_exists:
346 input_subgroups = sorted(
347 input_group.descendant_groups.list(
348 get_all=True,
349 # include_subgroups=True,
350 order_by='path',
351 sort='asc',
352 ),
353 key=lambda item: item.full_path,
354 )
355 progress_count = len(input_subgroups)
356 progress_index = 0
357 for input_subgroup in input_subgroups:
358 progress_index += 1
359 result = Entrypoint.subgroup(
360 options,
361 input_gitlab,
362 output_gitlab,
363 input_group.full_path,
364 input_subgroup.full_path,
365 output_namespace.full_path if output_namespace else '',
366 migration=output_exists,
367 progress_index=progress_index,
368 progress_count=progress_count,
369 )
370 if result in [
371 Entrypoint.Result.FINALIZE,
372 Entrypoint.Result.ERROR,
373 ]:
374 return result
376 # Iterate through projects
377 if not options.exclude_projects:
378 projects = sorted(
379 input_group.projects.list(
380 get_all=True,
381 with_shared=False,
382 include_subgroups=not options.exclude_subgroups,
383 order_by='path',
384 sort='asc',
385 ),
386 key=lambda item: item.path_with_namespace,
387 )
388 progress_count = len(projects)
389 progress_index = 0
390 for project in projects:
391 progress_index += 1
392 result = Entrypoint.project(
393 options,
394 input_gitlab,
395 output_gitlab,
396 project.path_with_namespace,
397 input_group.full_path,
398 output_namespace.full_path if output_namespace else '',
399 output_gitlab_namespace,
400 progress_index=progress_index,
401 progress_count=progress_count,
402 )
403 if result in [
404 Entrypoint.Result.FINALIZE,
405 Entrypoint.Result.ERROR,
406 ]:
407 return result
409 # Archive input project
410 if options.archive_sources:
411 changed = input_gitlab.project_set_archive(
412 project.path_with_namespace,
413 enabled=True,
414 )
415 print(f'{Colors.BOLD} - GitLab input project: '
416 f'{Colors.YELLOW_LIGHT}{project.path_with_namespace}'
417 f'{Colors.CYAN} # {Namespaces.text(project.description)}'
418 f'{Colors.RESET}')
419 print(f'{Colors.BOLD} - Archive sources project: '
420 f'{Colors.CYAN if changed else Colors.GREEN}'
421 f'{"Success" if changed else "Already done"}'
422 f'{Colors.RESET}')
423 print(' ')
424 Platform.flush()
426 # Delete input group after validation
427 if options.delete_sources:
428 print(f'{Colors.BOLD} - GitLab input group: '
429 f'{Colors.GREEN}{input_group.full_path}'
430 f'{Colors.CYAN} # {Namespaces.text(input_group.description)}'
431 f'{Colors.RESET}')
432 Platform.flush()
433 if not Entrypoint.confirm(
434 'Delete group',
435 input_group.full_path,
436 not options.confirm,
437 'deletion',
438 ):
439 raise PermissionError()
441 # Delete input group
442 input_gitlab.group_delete(input_group.full_path)
443 print(f'{Colors.BOLD} - Delete sources group: '
444 f'{Colors.GREEN}Success'
445 f'{Colors.RESET}')
446 print(' ')
447 Platform.flush()
449 # Handle user
450 elif input_user:
452 # Validate types
453 if output_enabled:
454 assert output_gitlab
455 assert output_namespace
457 # Iterate through projects
458 if not options.exclude_projects:
459 projects = sorted(
460 input_user.projects.list(
461 get_all=True,
462 order_by='path',
463 sort='asc',
464 ),
465 key=lambda item: item.path_with_namespace,
466 )
467 progress_count = len(projects)
468 progress_index = 0
469 for project in projects:
470 progress_index += 1
471 result = Entrypoint.project(
472 options,
473 input_gitlab,
474 output_gitlab,
475 project.path_with_namespace,
476 input_namespace.full_path,
477 output_namespace.full_path if output_namespace else '',
478 output_gitlab_namespace,
479 progress_index=progress_index,
480 progress_count=progress_count,
481 )
482 if result in [
483 Entrypoint.Result.FINALIZE,
484 Entrypoint.Result.ERROR,
485 ]:
486 return result
488 # Handle sources
489 if options.archive_sources or options.delete_sources:
490 print(f'{Colors.BOLD} - GitLab input project: '
491 f'{Colors.GREEN}{project.path_with_namespace}'
492 f'{Colors.CYAN} # {Namespaces.text(project.description)}'
493 f'{Colors.RESET}')
494 Platform.flush()
496 # Archive input project
497 if options.archive_sources:
498 changed = input_gitlab.project_set_archive(
499 project.path_with_namespace,
500 enabled=True,
501 )
502 print(f'{Colors.BOLD} - Archive sources project: '
503 f'{Colors.CYAN if changed else Colors.GREEN}'
504 f'{"Success" if changed else "Already done"}'
505 f'{Colors.RESET}')
506 print(' ')
507 Platform.flush()
509 # Delete input project
510 elif options.delete_sources:
512 # Confirm project deletion
513 if not Entrypoint.confirm(
514 'Delete project',
515 project.path_with_namespace,
516 not options.confirm,
517 'deletion',
518 ):
519 print(' ')
520 Platform.flush()
521 return Entrypoint.Result.SUCCESS
523 # Delete input project
524 input_gitlab.project_delete(project.path_with_namespace)
525 print(f'{Colors.BOLD} - Delete sources project: '
526 f'{Colors.GREEN}Success'
527 f'{Colors.RESET}')
528 print(' ')
529 Platform.flush()
531 # Result
532 return Entrypoint.Result.SUCCESS
534 # Confirm
535 @staticmethod
536 def confirm(
537 description: str,
538 text: str = '',
539 interactive: bool = True,
540 action: str = '',
541 indent: str = ' ',
542 ) -> bool:
544 # Header
545 print(
546 f'{Colors.BOLD}{indent}- {description}{": " if description else ""}Confirm \''
547 f'{Colors.RED}{text}'
548 f'{Colors.BOLD}\' {action}:'
549 f'{Colors.RESET}', end='')
550 Platform.flush()
552 # Confirm without user interaction
553 if not interactive:
554 print(f'{Colors.RED} Confirmed by parameters'
555 f'{Colors.RESET}')
556 Platform.flush()
557 return True
559 # Confirm without user input
560 if not Platform.IS_TTY_STDIN:
561 print(f'{Colors.RED} Confirmed without user input'
562 f'{Colors.RESET}')
563 Platform.flush()
564 return True
566 # Get user configuration
567 answer: bool = questionary.confirm(
568 message='',
569 default=False,
570 qmark='',
571 style=Themes.confirmation_style(),
572 auto_enter=True,
573 ).ask()
575 # Result
576 return answer
578 # Group, pylint: disable=too-many-arguments,too-many-locals,too-many-positional-arguments
579 @staticmethod
580 def group(
581 options: Namespace,
582 input_gitlab: GitLabFeature,
583 output_gitlab: Optional[GitLabFeature],
584 criteria_input_group: str,
585 criteria_output_group: str,
586 migration: bool = True,
587 ) -> Result:
589 # Variables
590 changed: bool
591 input_subgroup_allowed: str
592 input_subgroup_parent: str
593 output_group: Optional[GitLabGroup] = None
594 output_namespace: Optional[GitLabNamespace] = None
596 # Acquire input group
597 input_group = input_gitlab.group(criteria_input_group)
598 input_group_namespace, _ = Namespaces.split_namespace(
599 criteria_input_group,
600 relative=False,
601 )
603 # Detect group or subgroup
604 output_group_namespace, output_group_path = Namespaces.split_namespace(
605 criteria_output_group,
606 relative=False,
607 )
609 # Detect identical input and output
610 same_namespace: bool = False
611 if output_gitlab and input_gitlab.url == output_gitlab.url \
612 and input_group_namespace == output_group_namespace:
613 same_namespace = True
615 # Prepare output group name
616 output_group_name = input_group.name \
617 if input_group.name != input_group.path and not same_namespace \
618 else output_group_path
620 # Show group details
621 print(f'{Colors.BOLD} - GitLab group: '
622 f'{Colors.YELLOW_LIGHT}{input_group.full_path} '
623 f'{Colors.CYAN}({Namespaces.text(input_group.description)})'
624 f'{Colors.RESET}')
625 Platform.flush()
627 # Validate subgroup paths
628 for input_subgroup in input_group.descendant_groups.list(
629 get_all=True,
630 order_by='path',
631 sort='asc',
632 # include_subgroups=True,
633 ):
634 input_subgroup_allowed = input_subgroup.path.lstrip('-_').rstrip('._')
635 input_subgroup_parent = input_subgroup.full_path[:-len(input_subgroup.path)]
636 if input_subgroup.path != input_subgroup_allowed:
637 print(f'{Colors.RED} - Incompatible GitLab subgroup path detected: '
638 f'{Colors.CYAN}{input_subgroup.full_path}'
639 f'{Colors.RED} != '
640 f'{Colors.GREEN}{input_subgroup_parent}{input_subgroup_allowed}'
641 f'{Colors.RESET}')
642 print(' ')
643 Platform.flush()
644 return Entrypoint.Result.ERROR
646 # Migration mode
647 if output_gitlab and not migration:
649 # Existing user
650 try:
651 if '/' in criteria_output_group:
652 raise TypeError
653 _output_user = output_gitlab.user(criteria_output_group)
654 output_namespace = output_gitlab.namespace(criteria_output_group)
655 print(
656 f'{Colors.BOLD} - Already existing user namespace in GitLab output: '
657 f'{Colors.GREEN}{output_namespace.full_path}'
658 f'{Colors.RESET}')
659 print(' ')
660 Platform.flush()
661 return Entrypoint.Result.SUCCESS
663 # Existing group
664 except (GitlabGetError, RuntimeError, TypeError):
665 output_group = output_gitlab.group(criteria_output_group)
666 print(f'{Colors.BOLD} - Already existing group in GitLab output: '
667 f'{Colors.GREEN}{output_group.full_path}'
668 f'{Colors.CYAN} # {Namespaces.text(output_group.description)}'
669 f'{Colors.RESET}')
670 print(' ')
671 Platform.flush()
672 return Entrypoint.Result.SUCCESS
674 # Ignore existing names
675 if output_gitlab:
676 output_group_parent = output_gitlab.group(output_group_namespace)
677 if output_group_name in [
678 output_group.name
679 for output_group in output_group_parent.subgroups.list(get_all=True)
680 ]:
681 print(f'{Colors.RED} - Already existing group name in GitLab output: '
682 f'{Colors.CYAN}{output_group_name}'
683 f'{Colors.RESET}')
684 print(' ')
685 Platform.flush()
686 raise RuntimeError()
688 # Confirm group is exportable
689 export_limitations = input_gitlab.group_export_limitations(input_group.full_path)
690 if export_limitations:
691 print(f'{Colors.BOLD} - Limited group export detected: '
692 f'{Colors.RESET}')
693 for limitation, items in export_limitations.items():
694 (level, values) = items
695 if level == GitLabFeature.LIMITATIONS_ERROR:
696 print(f'{Colors.BOLD} - With data loss: '
697 f'{Colors.RED}{limitation}'
698 + (f'{Colors.CYAN} ({", ".join(values)})' if values else '') + \
699 f'{Colors.RESET}')
700 else:
701 print(f'{Colors.BOLD} - With optional custom migration: '
702 f'{Colors.YELLOW_LIGHT}{limitation}'
703 + (f'{Colors.CYAN} ({", ".join(values)})' if values else '') + \
704 f'{Colors.RESET}')
705 if not options.dry_run and not Entrypoint.confirm(
706 '',
707 input_group.full_path,
708 not options.confirm,
709 'limited group export',
710 indent=' ',
711 ):
712 raise PermissionError()
714 # Export group
715 if output_gitlab or options.archive_exports:
716 print(f'{Colors.BOLD} - Exporting from: '
717 f'{Colors.GREEN}{input_group.full_path}'
718 f'{Colors.RESET}')
719 Platform.flush()
720 with NamedTemporaryFile(
721 prefix=Paths.slugify(f'{Entrypoint.EXPORTS_PREFIX}'
722 f'-{input_group.full_path}-'),
723 suffix='.tar.gz',
724 dir=options.archive_exports_dir,
725 delete=not options.archive_exports_dir,
726 ) as file_export:
727 input_gitlab.group_export(
728 file_export.name,
729 input_group.full_path,
730 options.reset_entities,
731 )
733 # Import group
734 if output_gitlab:
735 print(f'{Colors.BOLD} - Importing to: '
736 f'{Colors.GREEN}{criteria_output_group}'
737 f'{Colors.RESET}')
738 Platform.flush()
739 output_gitlab.group_import(
740 file_export.name,
741 output_group_namespace,
742 output_group_path,
743 output_group_name,
744 )
746 # Abort group migration
747 if not output_gitlab:
748 print(f'{Colors.BOLD} - Exported group: '
749 f'{Colors.GREEN}Success'
750 f'{Colors.RESET}')
751 Platform.flush()
753 # Abort output group
754 if not output_gitlab:
755 print(' ')
756 Platform.flush()
757 return Entrypoint.Result.SUCCESS
759 # Acquire output group
760 output_group_criteria: str = ''
761 if not options.dry_run:
762 output_group = output_gitlab.group(criteria_output_group)
763 output_group_criteria = output_group.full_path
765 # Set group description
766 description = Namespaces.describe(
767 name=output_group_name,
768 description=input_group.description,
769 )
770 changed = output_gitlab.group_set_description(
771 output_group_criteria,
772 description,
773 )
774 print(f'{Colors.BOLD} - Set description: '
775 f'{Colors.CYAN if changed else Colors.GREEN}{description}'
776 f'{Colors.RESET}')
777 Platform.flush()
779 # Set group avatar
780 if options.set_avatar:
781 output_gitlab.group_set_avatar(
782 output_group_criteria,
783 options.set_avatar,
784 )
785 print(f'{Colors.BOLD} - Set avatar: '
786 f'{Colors.CYAN}{options.set_avatar}'
787 f'{Colors.RESET}')
788 Platform.flush()
790 # Show group result
791 print(f'{Colors.BOLD} - Migrated group: '
792 f'{Colors.GREEN}Success'
793 f'{Colors.RESET}')
794 Platform.flush()
796 # Footer
797 print(' ')
798 Platform.flush()
800 # Result
801 return Entrypoint.Result.SUCCESS
803 # Project, pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-positional-arguments
804 @staticmethod
805 def project(
806 options: Namespace,
807 input_gitlab: GitLabFeature,
808 output_gitlab: Optional[GitLabFeature],
809 criteria_project: str,
810 criteria_input_namespace: str,
811 criteria_output_namespace: str,
812 output_gitlab_namespace: str,
813 rename_project: str = '',
814 progress_index: int = 1,
815 progress_count: int = 1,
816 ) -> Result:
818 # Variables
819 archived: bool
820 branch_default: str
821 changed: bool = False
822 input_namespace: GitLabNamespace
823 input_project: GitLabProject
824 output_exists: bool = False
825 output_path: str
826 output_project: GitLabProjectImport
827 output_subnamespace: str
828 output_subpath: str
829 project: GitLabProject
831 # Acquire input project
832 input_project = input_gitlab.project(
833 criteria_project,
834 statistics=True,
835 )
837 # Acquire input namespace
838 input_namespace = input_gitlab.namespace(criteria_input_namespace)
840 # Parse input subpath
841 input_subpath = Namespaces.subpath(
842 input_namespace.full_path,
843 input_project.path_with_namespace,
844 )
846 # Acquire output namespace
847 if output_gitlab:
848 output_namespace = output_gitlab.namespace(
849 criteria_output_namespace,
850 optional=True,
851 )
853 # Prepare project path
854 output_project_path: str = rename_project if rename_project else input_project.path
855 output_project_path = re.sub(r'\.+', '.', output_project_path)
856 output_project_path = re.sub(r'-+', '-', output_project_path)
857 output_project_path = re.sub(r'_+', '_', output_project_path)
859 # Normalize project path
860 output_project_path = output_project_path.lstrip('-_. ')
861 output_project_path = output_project_path.rstrip('-_. ')
862 output_project_path = re.sub(r'\.git$', '', output_project_path)
863 output_project_path = re.sub(r'\.atom$', '', output_project_path)
864 if options.normalize_names:
865 output_project_path = output_project_path.replace(' ', '-')
866 output_project_path = output_project_path.replace('_', '-')
867 output_project_path = output_project_path.lower()
869 # Parse output path
870 if output_gitlab:
871 output_subnamespace, output_path = Namespaces.split_namespace(
872 input_subpath,
873 relative=True,
874 )
875 if output_path:
876 output_path = re.sub(
877 f'{input_project.path}$',
878 output_project_path,
879 output_path,
880 )
882 # Flatten output group
883 if output_gitlab and options.flatten_group:
884 output_subnamespace = ''
886 # Parse output subpath
887 if output_gitlab:
888 output_subpath = Namespaces.subpath(
889 output_namespace.full_path,
890 f'{output_gitlab_namespace}{output_subnamespace}/{output_path}',
891 )
893 # Show project details
894 print(f'{Colors.BOLD} - GitLab input project ('
895 f'{Colors.GREEN}{progress_index}'
896 f'{Colors.RESET}/'
897 f'{Colors.CYAN}{progress_count}'
898 f'{Colors.BOLD}) : '
899 f'{Colors.YELLOW_LIGHT}{input_project.path_with_namespace} '
900 f'{Colors.CYAN}({Namespaces.text(input_project.description)})'
901 f'{Colors.RESET}')
902 Platform.flush()
904 # Ignore existing projects
905 if output_gitlab and output_subpath in [
906 Namespaces.subpath(
907 output_namespace.full_path,
908 output_project.path_with_namespace,
909 ) for output_project in output_gitlab.namespace_projects(
910 criteria_output_namespace,
911 include_subgroups=True,
912 )
913 ]:
914 output_exists = True
915 project = output_gitlab.project(
916 f'{output_namespace.full_path}/{output_subpath}')
917 print(f'{Colors.BOLD} - Already existing project in GitLab output: '
918 f'{Colors.GREEN}{project.path_with_namespace}'
919 f'{Colors.CYAN} # {Namespaces.text(project.description)}'
920 f'{Colors.RESET}')
921 Platform.flush()
923 # Ignore existing names
924 if not output_exists and output_gitlab and output_project_path in [
925 output_project.name
926 for output_project in output_gitlab.namespace_projects(
927 f'{output_gitlab_namespace}{output_subnamespace}',
928 include_subgroups=False,
929 )
930 ]:
931 print(f'{Colors.RED} - Already existing project name in GitLab output: '
932 f'{Colors.CYAN}{output_project_path}'
933 f'{Colors.RESET}')
934 print(' ')
935 Platform.flush()
936 raise RuntimeError()
938 # Migrate missing projects
939 if not output_exists or options.overwrite:
941 # Detect project export limitions
942 export_limitations = input_gitlab.project_export_limitations(
943 input_project.path_with_namespace)
945 # Accept project packages migration
946 if output_gitlab and options.migrate_packages and \
947 'Packages registry' in export_limitations:
948 del export_limitations['Packages registry']
950 # Confirm project is exportable
951 if export_limitations:
952 print(f'{Colors.BOLD} - Limited project export detected: '
953 f'{Colors.RESET}')
954 for limitation, items in export_limitations.items():
955 (level, values) = items
956 if level == GitLabFeature.LIMITATIONS_ERROR:
957 print(f'{Colors.BOLD} - With data loss: '
958 f'{Colors.RED}{limitation}'
959 + (f'{Colors.CYAN} ({", ".join(values)})' if values else '') + \
960 f'{Colors.RESET}')
961 else:
962 print(f'{Colors.BOLD} - With optional custom migration: '
963 f'{Colors.YELLOW_LIGHT}{limitation}'
964 + (f'{Colors.CYAN} ({", ".join(values)})' if values else '') + \
965 f'{Colors.RESET}')
966 if not options.dry_run and not Entrypoint.confirm(
967 '',
968 input_project.path_with_namespace,
969 not options.confirm,
970 'limited project export',
971 indent=' ',
972 ):
973 raise PermissionError()
975 # Acquire project settings
976 archived = input_project.archived
977 branch_default = input_gitlab.project_get_branch_default(
978 input_project.path_with_namespace)
980 # Export project
981 if output_gitlab or options.archive_exports:
982 print(f'{Colors.BOLD} - Exporting from: '
983 f'{Colors.GREEN}{input_namespace.full_path}'
984 f'{Colors.CYAN} / {input_subpath}'
985 f'{Colors.RESET}')
986 Platform.flush()
988 # Show project sizes
989 print(
990 f'{Colors.BOLD} - Project sizes: '
991 f'{Colors.CYAN}{Humanize.size(input_project.statistics["storage_size"])} '
992 f'{Colors.YELLOW_LIGHT}(storage), '
993 f'{Colors.CYAN}{Humanize.size(input_project.statistics["repository_size"])} '
994 f'{Colors.YELLOW_LIGHT}(repository)'
995 f'{Colors.RESET}')
996 Platform.flush()
998 # Archive project during export
999 changed = input_gitlab.project_set_archive(
1000 input_project.path_with_namespace,
1001 True,
1002 )
1003 print(f'{Colors.BOLD} - Archiving during export: '
1004 f'{Colors.CYAN if changed else Colors.GREEN}Success'
1005 f'{Colors.RESET}')
1006 Platform.flush()
1008 # Export project to file
1009 with NamedTemporaryFile(
1010 prefix=Paths.slugify(f'{Entrypoint.EXPORTS_PREFIX}'
1011 f'-{input_project.path_with_namespace}-'),
1012 suffix='.tar.gz',
1013 dir=options.archive_exports_dir,
1014 delete=not options.archive_exports_dir,
1015 ) as file_export:
1016 input_gitlab.project_export(
1017 file_export.name,
1018 input_project.path_with_namespace,
1019 options.reset_entities,
1020 )
1022 # Restore project archive
1023 if not archived:
1024 changed = input_gitlab.project_set_archive(
1025 input_project.path_with_namespace,
1026 False,
1027 )
1028 print(f'{Colors.BOLD} - Unarchived after export: '
1029 f'{Colors.CYAN if changed else Colors.GREEN}Success'
1030 f'{Colors.RESET}')
1031 Platform.flush()
1033 # Existing project removal
1034 if output_gitlab and options.overwrite:
1035 if not Entrypoint.confirm(
1036 'Delete project',
1037 f'{output_namespace.full_path}/{output_subpath}',
1038 not options.confirm,
1039 'deletion',
1040 ):
1041 raise PermissionError()
1042 output_gitlab.project_delete(
1043 f'{output_namespace.full_path}/{output_subpath}')
1045 # Import project
1046 if output_gitlab:
1047 print(f'{Colors.BOLD} - Importing to: '
1048 f'{Colors.GREEN}{output_gitlab_namespace}'
1049 f'{Colors.CYAN} / {output_subpath}'
1050 f'{Colors.RESET}')
1051 Platform.flush()
1052 output_project = output_gitlab.project_import(
1053 file_export.name,
1054 f'{output_gitlab_namespace}{output_subnamespace}',
1055 output_path,
1056 output_project_path,
1057 options.overwrite,
1058 sudo=output_namespace.full_path
1059 if output_namespace.kind == 'user'
1060 and output_namespace != output_gitlab.username else None,
1061 )
1063 # Successful project migration
1064 if not output_gitlab:
1065 print(f'{Colors.BOLD} - Exported project: '
1066 f'{Colors.GREEN}Success'
1067 f'{Colors.RESET}')
1068 Platform.flush()
1070 # Abort output project
1071 if not output_gitlab:
1072 print(' ')
1073 Platform.flush()
1074 return Entrypoint.Result.SUCCESS
1076 # Acquire subgroup description
1077 output_subgroup_description: str
1078 if options.dry_run:
1079 output_subgroup_description = input_project.description
1080 elif output_namespace.kind == 'user':
1081 output_subgroup_description = output_namespace.name
1082 else:
1083 output_subgroup = output_gitlab.group(
1084 f'{output_gitlab_namespace}{output_subnamespace}')
1085 output_subgroup_description = output_subgroup.description
1087 # Update project description
1088 if options.update_description:
1089 group_description = Namespaces.describe(
1090 name=output_project_path,
1091 description=output_subgroup_description,
1092 )
1093 if not output_project.description or \
1094 not output_project.description.endswith(f' - {group_description}'):
1095 description = f'{Namespaces.describe(name=output_project.name)}' \
1096 f' - {group_description}'
1097 changed = output_gitlab.project_set_description(
1098 output_project.path_with_namespace,
1099 description,
1100 )
1101 print(f'{Colors.BOLD} - Updated description: '
1102 f'{Colors.CYAN if changed else Colors.GREEN}{description}'
1103 f'{Colors.RESET}')
1104 Platform.flush()
1105 else:
1106 print(f'{Colors.BOLD} - Kept description: '
1107 f'{Colors.GREEN}{Namespaces.text(output_project.description)}'
1108 f'{Colors.RESET}')
1109 Platform.flush()
1111 # Reset project members
1112 if MigrationEntities.MEMBERS.name in options.reset_entities:
1113 output_gitlab.project_reset_members(output_project.path_with_namespace)
1114 print(f'{Colors.BOLD} - Reset members: '
1115 f'{Colors.GREEN}Success'
1116 f'{Colors.RESET}')
1117 Platform.flush()
1119 # Set project avatar
1120 if options.set_avatar:
1121 output_gitlab.project_set_avatar(
1122 output_project.path_with_namespace,
1123 options.set_avatar,
1124 )
1125 print(f'{Colors.BOLD} - Set avatar: '
1126 f'{Colors.CYAN}{options.set_avatar}'
1127 f'{Colors.RESET}')
1128 Platform.flush()
1130 # Configure project repository
1131 if branch_default:
1132 changed = output_gitlab.project_set_branch_default(
1133 output_project.path_with_namespace,
1134 branch_default,
1135 )
1136 print(f'{Colors.BOLD} - Set default branch: '
1137 f'{Colors.CYAN if changed else Colors.GREEN}{branch_default}'
1138 f'{Colors.RESET}')
1139 Platform.flush()
1141 # Configure project archive
1142 changed = output_gitlab.project_set_archive(
1143 output_project.path_with_namespace,
1144 archived,
1145 )
1146 print(f'{Colors.BOLD} - '
1147 f'{"Archived" if archived else "Unarchived"}'
1148 ' migrated project: '
1149 f'{Colors.CYAN if changed else Colors.GREEN}Success'
1150 f'{Colors.RESET}')
1151 Platform.flush()
1153 # Show project result
1154 print(f'{Colors.BOLD} - Migrated project: '
1155 f'{Colors.GREEN}Success'
1156 f'{Colors.RESET}')
1157 Platform.flush()
1159 # Migrate project packages
1160 if output_gitlab and options.migrate_packages and not options.dry_run:
1162 # Prepare packages migration
1163 project = output_gitlab.project(
1164 f'{output_namespace.full_path}/{output_subpath}')
1165 print(f'{Colors.BOLD} - Migrating packages from: '
1166 f'{Colors.GREEN}{input_namespace.full_path}'
1167 f'{Colors.CYAN} / {input_subpath}'
1168 f'{Colors.RESET}')
1169 Platform.flush()
1171 # Detect migrated packages
1172 GitLabPackageFile = NamedTuple('GitLabPackageFile', (
1173 ('name', str),
1174 ('version', str),
1175 ('package_type', str),
1176 ('file_name', str),
1177 ('size', int),
1178 ('file_md5', str),
1179 ('file_sha1', str),
1180 ('file_sha256', str),
1181 ))
1182 input_project_packages: List[GitLabPackageFile] = []
1183 output_project_packages: List[GitLabPackageFile] = []
1184 try:
1185 for package in project.packages.list(
1186 get_all=True,
1187 include_versionless=True,
1188 ):
1189 for package_file in package.package_files.list(get_all=True):
1190 output_project_packages += [
1191 GitLabPackageFile(
1192 name=package.name,
1193 version=package.version,
1194 package_type=package.package_type,
1195 file_name=package_file.file_name,
1196 size=package_file.size,
1197 file_md5=package_file.file_md5,
1198 file_sha1=package_file.file_sha1,
1199 file_sha256=package_file.file_sha256,
1200 )
1201 ]
1202 except GitlabListError:
1203 pass
1205 # Detect existing packages
1206 try:
1207 for package in input_project.packages.list(
1208 get_all=True,
1209 include_versionless=True,
1210 ):
1212 # Iterate through packages files
1213 print(f'{Colors.BOLD} - Importing project package:'
1214 f'{Colors.YELLOW_LIGHT} {package.name}'
1215 f'{Colors.CYAN} / {package.version}'
1216 f'{Colors.RESET}')
1217 Platform.flush()
1218 for package_file in package.package_files.list(get_all=True):
1219 with NamedTemporaryFile(
1220 prefix=Paths.slugify(f'{Entrypoint.PACKAGES_PREFIX}'
1221 f'-{package.id}-{package_file.id}-'),
1222 suffix='.tmp',
1223 delete=True,
1224 ) as package_tempfile:
1226 # Detect already migrated package file
1227 if GitLabPackageFile(
1228 name=package.name,
1229 version=package.version,
1230 package_type=package.package_type,
1231 file_name=package_file.file_name,
1232 size=package_file.size,
1233 file_md5=package_file.file_md5,
1234 file_sha1=package_file.file_sha1,
1235 file_sha256=package_file.file_sha256,
1236 ) in output_project_packages:
1237 print(
1238 f'{Colors.BOLD} - Kept already migrated package file:'
1239 f'{Colors.GREEN} {package_file.file_name}'
1240 f'{Colors.GREY} / {package_file.size} bytes'
1241 f'{Colors.RESET}')
1242 Platform.flush()
1244 # Handle input package file
1245 else:
1246 input_project_package = GitLabPackageFile(
1247 name=package.name,
1248 version=package.version,
1249 package_type=package.package_type,
1250 file_name=package_file.file_name,
1251 size=0,
1252 file_md5='',
1253 file_sha1='',
1254 file_sha256='',
1255 )
1256 if input_project_package in input_project_packages and \
1257 not Entrypoint.confirm(
1258 'Duplicated package file name detected',
1259 package_file.file_name,
1260 not options.confirm,
1261 'unsafe migration',
1262 ' ',
1263 ):
1264 continue
1265 input_project_packages += [input_project_package]
1267 # Upload generic package file
1268 if package.package_type == 'generic':
1269 package_tempfile.write(
1270 input_project.generic_packages.download(
1271 package_name=package.name,
1272 package_version=package.version,
1273 file_name=package_file.file_name,
1274 ))
1275 package_tempfile.flush()
1276 project.generic_packages.upload(
1277 package_name=package.name,
1278 package_version=package.version,
1279 file_name=package_file.file_name,
1280 path=package_tempfile.name,
1281 )
1282 print(f'{Colors.BOLD} - Uploaded package file:'
1283 f'{Colors.GREEN} {package_file.file_name}'
1284 f'{Colors.GREY} / {package_file.size} bytes'
1285 f'{Colors.RESET}')
1286 Platform.flush()
1288 # Unsupported package file
1289 else:
1290 raise RuntimeError(
1291 f'Unknown GitLab Packages type "{package.package_type}"'
1292 f' for package {package.name}')
1294 # Successful packages migration
1295 print(f'{Colors.BOLD} - Migrated project packages: '
1296 f'{Colors.GREEN}Success'
1297 f'{Colors.RESET}')
1298 Platform.flush()
1300 # Ignore missing packages
1301 except GitlabListError:
1302 print(f'{Colors.BOLD} - Ignored project packages: '
1303 f'{Colors.CYAN}No packages found'
1304 f'{Colors.RESET}')
1305 Platform.flush()
1307 # Validate project migration
1308 if output_gitlab and not options.dry_run:
1309 print(f'{Colors.BOLD} - Validating project migration:'
1310 f'{Colors.RESET}')
1311 Platform.flush()
1313 # Confirm project migration
1314 project = output_gitlab.project(
1315 f'{output_namespace.full_path}/{output_subpath}')
1316 migration_differences = GitLabFeature.migration_projects_compare(
1317 input_project,
1318 project,
1319 options.reset_entities,
1320 )
1321 if migration_differences:
1322 print(f'{Colors.BOLD} - '
1323 f'{Colors.RED}Differences detected after migration:'
1324 f'{Colors.RESET}')
1325 for migration_difference in migration_differences:
1326 difference, details = migration_difference
1327 print(f'{Colors.BOLD} - '
1328 f'{Colors.RED}{difference}:'
1329 f'{Colors.CYAN} {details}'
1330 f'{Colors.RESET}')
1331 if not Entrypoint.confirm(
1332 '',
1333 project.path_with_namespace,
1334 not options.confirm,
1335 'incomplete project migration',
1336 indent=' ',
1337 ):
1338 raise PermissionError()
1340 # Show project validation
1341 print(f'{Colors.BOLD} - Validated project migration: '
1342 f'{Colors.GREEN}Success'
1343 f'{Colors.RESET}')
1344 Platform.flush()
1346 # Separator
1347 print(' ')
1348 Platform.flush()
1350 # Result
1351 return Entrypoint.Result.SUCCESS
1353 # Subgroup, pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-statements
1354 @staticmethod
1355 def subgroup(
1356 options: Namespace,
1357 input_gitlab: GitLabFeature,
1358 output_gitlab: Optional[GitLabFeature],
1359 criteria_input_group: str,
1360 criteria_input_subgroup: str,
1361 criteria_output_group: str,
1362 migration: bool = True,
1363 progress_index: int = 1,
1364 progress_count: int = 1,
1365 ) -> Result:
1367 # Variables
1368 changed: bool
1369 input_subpath: str
1370 output_subgroup_fullpath: str
1371 output_subgroup_namespace: str
1372 output_subgroup_path: str
1374 # Acquire input group
1375 input_group = input_gitlab.group(criteria_input_group)
1377 # Acquire input subgroup
1378 input_subgroup = input_gitlab.group(criteria_input_subgroup)
1380 # Acquire output group
1381 if output_gitlab:
1382 output_group = output_gitlab.group(criteria_output_group, optional=True)
1384 # Show subgroup details
1385 print(f'{Colors.BOLD} - GitLab subgroup ('
1386 f'{Colors.GREEN}{progress_index}'
1387 f'{Colors.RESET}/'
1388 f'{Colors.CYAN}{progress_count}'
1389 f'{Colors.BOLD}) : '
1390 f'{Colors.YELLOW_LIGHT}{input_subgroup.full_path} '
1391 f'{Colors.CYAN}({Namespaces.text(input_subgroup.description)})'
1392 f'{Colors.RESET}')
1393 Platform.flush()
1395 # Parse subgroup paths
1396 input_subpath = Namespaces.subpath(
1397 input_group.full_path,
1398 input_subgroup.full_path,
1399 )
1400 output_subgroup_namespace, output_subgroup_path = Namespaces.split_namespace(
1401 input_subpath,
1402 relative=True,
1403 )
1404 output_subgroup_fullpath = f'{output_subgroup_namespace}/{output_subgroup_path}'
1406 # Migration mode
1407 if migration:
1409 # Ignore existing subgroup
1410 if output_gitlab and f'{output_subgroup_fullpath.lstrip("/")}' in [
1411 Namespaces.subpath(
1412 output_group.full_path,
1413 output_subgroup.full_path,
1414 ) for output_subgroup in output_group.descendant_groups.list(
1415 get_all=True,
1416 # include_subgroups=True,
1417 )
1418 ]:
1419 output_subgroup = output_gitlab.group(
1420 f'{output_group.full_path}{output_subgroup_fullpath}')
1421 print(f'{Colors.BOLD} - Already existing subgroup in GitLab output: '
1422 f'{Colors.GREEN}{output_subgroup.full_path}'
1423 f'{Colors.CYAN} # {Namespaces.text(output_subgroup.description)}'
1424 f'{Colors.RESET}')
1425 print(' ')
1426 Platform.flush()
1427 return Entrypoint.Result.SUCCESS
1429 # Confirm subgroup is exportable
1430 export_limitations = input_gitlab.group_export_limitations(
1431 input_subgroup.full_path)
1432 if export_limitations:
1433 print(f'{Colors.BOLD} - Limited subgroup export detected: '
1434 f'{Colors.RESET}')
1435 for limitation, items in export_limitations.items():
1436 (level, values) = items
1437 if level == GitLabFeature.LIMITATIONS_ERROR:
1438 print(f'{Colors.BOLD} - With data loss: '
1439 f'{Colors.RED}{limitation}'
1440 + (f'{Colors.CYAN} ({", ".join(values)})' if values else '') + \
1441 f'{Colors.RESET}')
1442 else:
1443 print(f'{Colors.BOLD} - With optional custom migration: '
1444 f'{Colors.YELLOW_LIGHT}{limitation}'
1445 + (f'{Colors.CYAN} ({", ".join(values)})' if values else '') + \
1446 f'{Colors.RESET}')
1447 if not options.dry_run and not Entrypoint.confirm(
1448 '',
1449 input_subgroup.full_path,
1450 not options.confirm,
1451 'limited subgroup export',
1452 indent=' ',
1453 ):
1454 raise PermissionError()
1456 # Export subgroup
1457 if output_gitlab or options.archive_exports:
1458 print(f'{Colors.BOLD} - Exporting from: '
1459 f'{Colors.GREEN}{input_subgroup.full_path}'
1460 f'{Colors.RESET}')
1461 Platform.flush()
1462 with NamedTemporaryFile(
1463 prefix=Paths.slugify(f'{Entrypoint.EXPORTS_PREFIX}'
1464 f'-{input_subgroup.full_path}-'),
1465 suffix='.tar.gz',
1466 dir=options.archive_exports_dir,
1467 delete=not options.archive_exports_dir,
1468 ) as file_export:
1469 input_gitlab.group_export(
1470 file_export.name,
1471 input_subgroup.full_path,
1472 options.reset_entities,
1473 )
1475 # Import subgroup
1476 if output_gitlab:
1477 print(f'{Colors.BOLD} - Importing to: '
1478 f'{Colors.GREEN}'
1479 f'{output_group.full_path}{output_subgroup_fullpath}'
1480 f'{Colors.RESET}')
1481 Platform.flush()
1482 output_gitlab.group_import(
1483 file_export.name,
1484 f'{output_group.full_path}{output_subgroup_namespace}',
1485 output_subgroup_path,
1486 input_subgroup.name,
1487 )
1489 # Abort subgroup migration
1490 if not output_gitlab:
1491 print(f'{Colors.BOLD} - Exported subgroup: '
1492 f'{Colors.GREEN}Success'
1493 f'{Colors.RESET}')
1494 Platform.flush()
1496 # Abort output subgroup
1497 if not output_gitlab:
1498 print(' ')
1499 Platform.flush()
1500 return Entrypoint.Result.SUCCESS
1502 # Acquire subgroups
1503 output_subgroup_child_description: str
1504 output_subgroup_child_name: str
1505 output_subgroup_parent_description: str
1506 if not options.dry_run:
1507 output_subgroup_parent = output_gitlab.group(
1508 f'{output_group.full_path}{output_subgroup_namespace}')
1509 output_subgroup_parent_description = output_subgroup_parent.description
1510 output_subgroup_child = output_gitlab.group(
1511 f'{output_group.full_path}{output_subgroup_fullpath}')
1512 output_subgroup_child_description = output_subgroup_child.description
1513 output_subgroup_child_name = output_subgroup_child.name
1514 else:
1515 output_subgroup_parent_description = input_subgroup.description
1516 output_subgroup_child_description = input_subgroup.description
1517 output_subgroup_child_name = input_subgroup.name
1519 # Update subgroup header
1520 print(f'{Colors.BOLD} - Updating subgroup: '
1521 f'{Colors.GREEN}{output_group.full_path}{output_subgroup_fullpath}'
1522 f'{Colors.RESET}')
1523 Platform.flush()
1525 # Update subgroup description
1526 if options.update_description:
1527 parent_description = Namespaces.describe(
1528 name=output_subgroup_child_name,
1529 description=output_subgroup_parent_description,
1530 )
1531 if not output_subgroup_child_description.endswith(f' - {parent_description}'):
1532 description = f'{Namespaces.describe(name=output_subgroup_child_name)}' \
1533 f' - {parent_description}'
1534 changed = output_gitlab.group_set_description(
1535 f'{output_group.full_path}{output_subgroup_fullpath}',
1536 description,
1537 )
1538 print(f'{Colors.BOLD} - Updated description: '
1539 f'{Colors.CYAN if changed else Colors.GREEN}{description}'
1540 f'{Colors.RESET}')
1541 Platform.flush()
1542 else:
1543 print(f'{Colors.BOLD} - Kept description: '
1544 f'{Colors.GREEN}{output_subgroup_child_description}'
1545 f'{Colors.RESET}')
1546 Platform.flush()
1548 # Set subgroup avatar
1549 if options.set_avatar:
1550 output_gitlab.group_set_avatar(
1551 f'{output_group.full_path}{output_subgroup_fullpath}',
1552 options.set_avatar,
1553 )
1554 print(f'{Colors.BOLD} - Set avatar: '
1555 f'{Colors.CYAN}{options.set_avatar}'
1556 f'{Colors.RESET}')
1557 Platform.flush()
1559 # Show subgroup result
1560 print(f'{Colors.BOLD} - Migrated subgroup: '
1561 f'{Colors.GREEN}Success'
1562 f'{Colors.RESET}')
1563 Platform.flush()
1565 # Footer
1566 print(' ')
1567 Platform.flush()
1569 # Result
1570 return Entrypoint.Result.SUCCESS