Loading README.md +3 −3 Changes for README.md: 3 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -49,7 +49,7 @@ gitlab-projects-migrate 'https://old.gitlab.com' 'group/subgroup' 'https://new.g usage: gitlab-projects-migrate [-h] [--version] [-I INPUT_TOKEN] [-O OUTPUT_TOKEN] [--delete-sources] [--dry-run] [--exclude-group] [--exclude-subgroups] [--exclude-projects] [--keep-members] [--overwrite] [--set-avatar FILE] [--update-description] [input_gitlab] [input_path] [output_gitlab] [output_group] [input_gitlab] [input_path] [output_gitlab] [output_namespace] gitlab-projects-migrate: Migrate GitLab projects from a GitLab group to another GitLab's group Loading @@ -76,9 +76,9 @@ general settings arguments: positional arguments: input_gitlab # Input GitLab URL (default: https://gitlab.com) input_path # Input GitLab group or project path input_path # Input GitLab group, user namespace or project path output_gitlab # Output GitLab URL (default: https://gitlab.com) output_group # Output GitLab group output_namespace # Output GitLab group or user namespace ``` <!-- readme-help-stop --> Loading src/cli/entrypoint.py +114 −36 Changes for src/cli/entrypoint.py: 114 added lines, 36 removed lines. Original line number Diff line number Diff line Loading @@ -6,7 +6,12 @@ from tempfile import NamedTemporaryFile # Modules libraries from gitlab.exceptions import GitlabGetError from gitlab.v4.objects import Group, Project from gitlab.v4.objects import ( Group as GitLabGroup, Namespace as GitLabNamespace, Project as GitLabProject, User as GitLabUser, ) # Components from ..features.gitlab import GitLabFeature Loading @@ -22,10 +27,11 @@ class Entrypoint: def cli(options: Namespace) -> bool: # Variables input_group: Group = None input_project: Project = None input_group: GitLabGroup = None input_project: GitLabProject = None input_user: GitLabUser = None output_exists: bool = False output_group: Group = None output_namespace: GitLabNamespace = None # Header print(' ') Loading Loading @@ -58,7 +64,19 @@ class Entrypoint: f'{Colors.CYAN} ({Namespaces.text(input_group.description)})' f'{Colors.RESET}') Platform.flush() except GitlabGetError: except GitlabGetError as exception: try: if '/' in options.input_path: raise TypeError from exception input_user = input_gitlab.user(options.input_path) input_namespace = input_gitlab.namespace(options.input_path) print(f'{Colors.BOLD} - GitLab input user namespace: ' f'{Colors.GREEN}{input_namespace.full_path}' f'{Colors.CYAN} ({input_namespace.name})' f'{Colors.RESET}') print(' ') Platform.flush() except (GitlabGetError, TypeError): input_project = input_gitlab.project(options.input_path) print(f'{Colors.BOLD} - GitLab input project: ' f'{Colors.GREEN}{input_project.path_with_namespace}' Loading @@ -69,7 +87,8 @@ class Entrypoint: # Output group try: output_exists = False output_group = output_gitlab.group(options.output_group) output_group = output_gitlab.group(options.output_namespace) output_namespace = output_gitlab.namespace(options.output_namespace) output_exists = True print(f'{Colors.BOLD} - GitLab output group: ' f'{Colors.GREEN}{output_group.full_path}' Loading @@ -78,16 +97,30 @@ class Entrypoint: print(' ') Platform.flush() # Output parent group # Output namespace except GitlabGetError as exception: try: if '/' in options.output_namespace: raise TypeError from exception _output_user = output_gitlab.user(options.output_namespace) output_namespace = output_gitlab.namespace(options.output_namespace) print(f'{Colors.BOLD} - GitLab output user namespace: ' f'{Colors.GREEN}{output_namespace.full_path}' f'{Colors.CYAN} ({output_namespace.name})' f'{Colors.RESET}') print(' ') Platform.flush() # Output parent group except (GitlabGetError, TypeError): # pylint: disable=raise-missing-from # Validate options if not input_group or options.exclude_group: if (not input_group and not input_user) or options.exclude_group: raise exception # Missing output group print(f'{Colors.BOLD} - GitLab output group: ' f'{Colors.GREEN}{options.output_group}' f'{Colors.GREEN}{options.output_namespace}' f'{Colors.RED} (Non-existent output group)' f'{Colors.RESET}') print(' ') Loading @@ -103,7 +136,7 @@ class Entrypoint: output_gitlab, input_project.path_with_namespace, input_project.namespace['id'], output_group.full_path, output_namespace.full_path, ) # Delete input project Loading @@ -130,13 +163,16 @@ class Entrypoint: input_gitlab, output_gitlab, input_group.full_path, options.output_group, options.output_namespace, migration=not output_exists, ): return False # Acquire output group output_group = output_gitlab.group(options.output_group, optional=True) # Acquire output namespace output_namespace = output_gitlab.namespace( options.output_namespace, optional=True, ) # Iterate through subgroups if not options.exclude_subgroups or not output_exists: Loading @@ -155,7 +191,7 @@ class Entrypoint: output_gitlab, input_group.full_path, input_subgroup.full_path, output_group.full_path, output_namespace.full_path, migration=output_exists, ): return False Loading @@ -177,7 +213,7 @@ class Entrypoint: output_gitlab, input_project.path_with_namespace, input_group.full_path, output_group.full_path, output_namespace.full_path, ): return False Loading @@ -195,6 +231,44 @@ class Entrypoint: print(' ') Platform.flush() # Handle user elif input_user: # Iterate through projects if not options.exclude_projects: for input_project in sorted( input_user.projects.list( get_all=True, order_by='path', sort='asc', ), key=lambda item: item.path_with_namespace, ): if not Entrypoint.project( options, input_gitlab, output_gitlab, input_project.path_with_namespace, input_namespace.full_path, output_namespace.full_path, ): return False # Delete input project if options.delete_sources: print( f'{Colors.BOLD} - GitLab input project: ' f'{Colors.GREEN}{input_project.path_with_namespace}' f'{Colors.CYAN} ({Namespaces.text(input_project.description)})' f'{Colors.RESET}') Platform.flush() input_gitlab.project_delete(input_project.path_with_namespace) print(f'{Colors.BOLD} - Delete sources project: ' f'{Colors.GREEN}Success' f'{Colors.RESET}') print(' ') Platform.flush() # Result return True Loading Loading @@ -314,18 +388,21 @@ class Entrypoint: input_gitlab: GitLabFeature, output_gitlab: GitLabFeature, criteria_project: str, criteria_input_group: str, criteria_output_group: str, criteria_input_namespace: str, criteria_output_namespace: str, ) -> bool: # Acquire input project input_project = input_gitlab.project(criteria_project) # Acquire input group input_group = input_gitlab.group(criteria_input_group) # Acquire input namespace input_namespace = input_gitlab.namespace(criteria_input_namespace) # Acquire output group output_group = output_gitlab.group(criteria_output_group, optional=True) # Acquire output namespace output_namespace = output_gitlab.namespace( criteria_output_namespace, optional=True, ) # Show project details print(f'{Colors.BOLD} - GitLab input project: ' Loading @@ -336,20 +413,18 @@ class Entrypoint: # Ignore existing projects input_subpath = Namespaces.subpath( input_group.full_path, input_namespace.full_path, input_project.path_with_namespace, ) if not options.overwrite and input_subpath in [ Namespaces.subpath( output_group.full_path, output_namespace.full_path, output_project.path_with_namespace, ) for output_project in output_group.projects.list( get_all=True, include_subgroups=True, ) ) for output_project in output_gitlab.namespace_projects( criteria_output_namespace) ]: output_project = output_gitlab.project( f'{output_group.full_path}/{input_subpath}') f'{output_namespace.full_path}/{input_subpath}') print(f'{Colors.BOLD} - Already exists in GitLab output: ' f'{Colors.GREEN}{output_project.path_with_namespace}' f'{Colors.CYAN} ({Namespaces.text(output_project.description)})' Loading @@ -373,21 +448,22 @@ class Entrypoint: # Existing project removal if options.overwrite: output_gitlab.project_delete(f'{output_group.full_path}/{input_subpath}') output_gitlab.project_delete( f'{output_namespace.full_path}/{input_subpath}') # Import project print(f'{Colors.BOLD} - Importing to: ' f'{Colors.GREEN}{options.output_group}' f'{Colors.GREEN}{options.output_namespace}' f'{Colors.CYAN} / {input_subpath}' f'{Colors.RESET}') Platform.flush() output_namespace, output_path = Namespaces.split_namespace( output_subnamespace, output_path = Namespaces.split_namespace( input_subpath, relative=True, ) output_project = output_gitlab.project_import( file_export.name, f'{options.output_group}{output_namespace}', f'{options.output_namespace}{output_subnamespace}', output_path, input_project.name, options.overwrite, Loading @@ -395,12 +471,14 @@ class Entrypoint: # Acquire subgroup description output_subgroup_description: str if not options.dry_run: if options.dry_run: output_subgroup_description = input_project.description elif output_namespace.kind == 'user': output_subgroup_description = output_namespace.name else: output_subgroup = output_gitlab.group( f'{options.output_group}{output_namespace}') f'{options.output_namespace}{output_subnamespace}') output_subgroup_description = output_subgroup.description else: output_subgroup_description = input_project.description # Update project description if options.update_description: Loading src/cli/main.py +4 −4 Changes for src/cli/main.py: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -156,7 +156,7 @@ def main() -> None: dest='input_path', action='store', nargs='?', help='Input GitLab group or project path', help='Input GitLab group, user namespace or project path', ) group.add_argument( dest='output_gitlab', Loading @@ -166,11 +166,11 @@ def main() -> None: help='Output GitLab URL (default: https://gitlab.com)', ) group.add_argument( dest='output_group', dest='output_namespace', action='store', nargs='?', default='', help='Output GitLab group', help='Output GitLab group or user namespace', ) # Arguments parser Loading @@ -197,7 +197,7 @@ def main() -> None: # Arguments validation if not options.input_token or not options.input_gitlab or not options.input_path \ or not options.output_gitlab or not options.output_group: or not options.output_gitlab or not options.output_namespace: print(' ') parser.print_help() print(' ') Loading src/features/gitlab.py +54 −2 Changes for src/features/gitlab.py: 54 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ from typing import Any, List, Optional # Modules libraries from gitlab import Gitlab from gitlab.exceptions import GitlabDeleteError, GitlabGetError from gitlab.v4.objects import Group, Project from gitlab.v4.objects import Group, Namespace, Project, User # Components from ..types.namespaces import Namespaces Loading @@ -34,7 +34,7 @@ class GitLabFeature: # Group def group(self, criteria: str, optional: bool = False) -> Group: # Fake group for dry run # Optional group for dry run if self.__dry_run and optional: try: return self.group(criteria, optional=False) Loading @@ -53,6 +53,7 @@ class GitLabFeature: def list(self, **_kwargs: Optional[Any]) -> List[str]: return [] # Create fake group _, path = Namespaces.split_namespace( criteria, relative=False, Loading Loading @@ -181,6 +182,53 @@ class GitLabFeature: # Save group group.save() # Namespace def namespace(self, criteria: str, optional: bool = False) -> Namespace: # Optional namespace for dry run if self.__dry_run and optional: try: return self.namespace(criteria, optional=False) # Simulate namespace except GitlabGetError: NamespaceTuple = namedtuple('NamespaceTuple', [ 'id', 'name', 'full_path', ]) # Create fake namespace _, path = Namespaces.split_namespace( criteria, relative=False, ) return NamespaceTuple( id='dry-run', name=path, full_path=criteria, ) # Get namespace return self.__gitlab.namespaces.get(criteria) # Namespace projects def namespace_projects(self, criteria: str) -> Any: # Acquire namespace namespace = self.namespace(criteria) # Get user projects if namespace.kind == 'user': user = self.user(criteria) return user.projects.list(get_all=True) # Get group projects return self.group(criteria).projects.list( get_all=True, include_subgroups=True, ) # Project def project(self, criteria: str) -> Project: return self.__gitlab.projects.get(criteria) Loading Loading @@ -332,6 +380,10 @@ class GitLabFeature: }) sleep(5) # User def user(self, criteria: str) -> User: return self.__gitlab.users.list(username=criteria)[0] # URL @property def url(self) -> str: Loading Loading
README.md +3 −3 Changes for README.md: 3 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -49,7 +49,7 @@ gitlab-projects-migrate 'https://old.gitlab.com' 'group/subgroup' 'https://new.g usage: gitlab-projects-migrate [-h] [--version] [-I INPUT_TOKEN] [-O OUTPUT_TOKEN] [--delete-sources] [--dry-run] [--exclude-group] [--exclude-subgroups] [--exclude-projects] [--keep-members] [--overwrite] [--set-avatar FILE] [--update-description] [input_gitlab] [input_path] [output_gitlab] [output_group] [input_gitlab] [input_path] [output_gitlab] [output_namespace] gitlab-projects-migrate: Migrate GitLab projects from a GitLab group to another GitLab's group Loading @@ -76,9 +76,9 @@ general settings arguments: positional arguments: input_gitlab # Input GitLab URL (default: https://gitlab.com) input_path # Input GitLab group or project path input_path # Input GitLab group, user namespace or project path output_gitlab # Output GitLab URL (default: https://gitlab.com) output_group # Output GitLab group output_namespace # Output GitLab group or user namespace ``` <!-- readme-help-stop --> Loading
src/cli/entrypoint.py +114 −36 Changes for src/cli/entrypoint.py: 114 added lines, 36 removed lines. Original line number Diff line number Diff line Loading @@ -6,7 +6,12 @@ from tempfile import NamedTemporaryFile # Modules libraries from gitlab.exceptions import GitlabGetError from gitlab.v4.objects import Group, Project from gitlab.v4.objects import ( Group as GitLabGroup, Namespace as GitLabNamespace, Project as GitLabProject, User as GitLabUser, ) # Components from ..features.gitlab import GitLabFeature Loading @@ -22,10 +27,11 @@ class Entrypoint: def cli(options: Namespace) -> bool: # Variables input_group: Group = None input_project: Project = None input_group: GitLabGroup = None input_project: GitLabProject = None input_user: GitLabUser = None output_exists: bool = False output_group: Group = None output_namespace: GitLabNamespace = None # Header print(' ') Loading Loading @@ -58,7 +64,19 @@ class Entrypoint: f'{Colors.CYAN} ({Namespaces.text(input_group.description)})' f'{Colors.RESET}') Platform.flush() except GitlabGetError: except GitlabGetError as exception: try: if '/' in options.input_path: raise TypeError from exception input_user = input_gitlab.user(options.input_path) input_namespace = input_gitlab.namespace(options.input_path) print(f'{Colors.BOLD} - GitLab input user namespace: ' f'{Colors.GREEN}{input_namespace.full_path}' f'{Colors.CYAN} ({input_namespace.name})' f'{Colors.RESET}') print(' ') Platform.flush() except (GitlabGetError, TypeError): input_project = input_gitlab.project(options.input_path) print(f'{Colors.BOLD} - GitLab input project: ' f'{Colors.GREEN}{input_project.path_with_namespace}' Loading @@ -69,7 +87,8 @@ class Entrypoint: # Output group try: output_exists = False output_group = output_gitlab.group(options.output_group) output_group = output_gitlab.group(options.output_namespace) output_namespace = output_gitlab.namespace(options.output_namespace) output_exists = True print(f'{Colors.BOLD} - GitLab output group: ' f'{Colors.GREEN}{output_group.full_path}' Loading @@ -78,16 +97,30 @@ class Entrypoint: print(' ') Platform.flush() # Output parent group # Output namespace except GitlabGetError as exception: try: if '/' in options.output_namespace: raise TypeError from exception _output_user = output_gitlab.user(options.output_namespace) output_namespace = output_gitlab.namespace(options.output_namespace) print(f'{Colors.BOLD} - GitLab output user namespace: ' f'{Colors.GREEN}{output_namespace.full_path}' f'{Colors.CYAN} ({output_namespace.name})' f'{Colors.RESET}') print(' ') Platform.flush() # Output parent group except (GitlabGetError, TypeError): # pylint: disable=raise-missing-from # Validate options if not input_group or options.exclude_group: if (not input_group and not input_user) or options.exclude_group: raise exception # Missing output group print(f'{Colors.BOLD} - GitLab output group: ' f'{Colors.GREEN}{options.output_group}' f'{Colors.GREEN}{options.output_namespace}' f'{Colors.RED} (Non-existent output group)' f'{Colors.RESET}') print(' ') Loading @@ -103,7 +136,7 @@ class Entrypoint: output_gitlab, input_project.path_with_namespace, input_project.namespace['id'], output_group.full_path, output_namespace.full_path, ) # Delete input project Loading @@ -130,13 +163,16 @@ class Entrypoint: input_gitlab, output_gitlab, input_group.full_path, options.output_group, options.output_namespace, migration=not output_exists, ): return False # Acquire output group output_group = output_gitlab.group(options.output_group, optional=True) # Acquire output namespace output_namespace = output_gitlab.namespace( options.output_namespace, optional=True, ) # Iterate through subgroups if not options.exclude_subgroups or not output_exists: Loading @@ -155,7 +191,7 @@ class Entrypoint: output_gitlab, input_group.full_path, input_subgroup.full_path, output_group.full_path, output_namespace.full_path, migration=output_exists, ): return False Loading @@ -177,7 +213,7 @@ class Entrypoint: output_gitlab, input_project.path_with_namespace, input_group.full_path, output_group.full_path, output_namespace.full_path, ): return False Loading @@ -195,6 +231,44 @@ class Entrypoint: print(' ') Platform.flush() # Handle user elif input_user: # Iterate through projects if not options.exclude_projects: for input_project in sorted( input_user.projects.list( get_all=True, order_by='path', sort='asc', ), key=lambda item: item.path_with_namespace, ): if not Entrypoint.project( options, input_gitlab, output_gitlab, input_project.path_with_namespace, input_namespace.full_path, output_namespace.full_path, ): return False # Delete input project if options.delete_sources: print( f'{Colors.BOLD} - GitLab input project: ' f'{Colors.GREEN}{input_project.path_with_namespace}' f'{Colors.CYAN} ({Namespaces.text(input_project.description)})' f'{Colors.RESET}') Platform.flush() input_gitlab.project_delete(input_project.path_with_namespace) print(f'{Colors.BOLD} - Delete sources project: ' f'{Colors.GREEN}Success' f'{Colors.RESET}') print(' ') Platform.flush() # Result return True Loading Loading @@ -314,18 +388,21 @@ class Entrypoint: input_gitlab: GitLabFeature, output_gitlab: GitLabFeature, criteria_project: str, criteria_input_group: str, criteria_output_group: str, criteria_input_namespace: str, criteria_output_namespace: str, ) -> bool: # Acquire input project input_project = input_gitlab.project(criteria_project) # Acquire input group input_group = input_gitlab.group(criteria_input_group) # Acquire input namespace input_namespace = input_gitlab.namespace(criteria_input_namespace) # Acquire output group output_group = output_gitlab.group(criteria_output_group, optional=True) # Acquire output namespace output_namespace = output_gitlab.namespace( criteria_output_namespace, optional=True, ) # Show project details print(f'{Colors.BOLD} - GitLab input project: ' Loading @@ -336,20 +413,18 @@ class Entrypoint: # Ignore existing projects input_subpath = Namespaces.subpath( input_group.full_path, input_namespace.full_path, input_project.path_with_namespace, ) if not options.overwrite and input_subpath in [ Namespaces.subpath( output_group.full_path, output_namespace.full_path, output_project.path_with_namespace, ) for output_project in output_group.projects.list( get_all=True, include_subgroups=True, ) ) for output_project in output_gitlab.namespace_projects( criteria_output_namespace) ]: output_project = output_gitlab.project( f'{output_group.full_path}/{input_subpath}') f'{output_namespace.full_path}/{input_subpath}') print(f'{Colors.BOLD} - Already exists in GitLab output: ' f'{Colors.GREEN}{output_project.path_with_namespace}' f'{Colors.CYAN} ({Namespaces.text(output_project.description)})' Loading @@ -373,21 +448,22 @@ class Entrypoint: # Existing project removal if options.overwrite: output_gitlab.project_delete(f'{output_group.full_path}/{input_subpath}') output_gitlab.project_delete( f'{output_namespace.full_path}/{input_subpath}') # Import project print(f'{Colors.BOLD} - Importing to: ' f'{Colors.GREEN}{options.output_group}' f'{Colors.GREEN}{options.output_namespace}' f'{Colors.CYAN} / {input_subpath}' f'{Colors.RESET}') Platform.flush() output_namespace, output_path = Namespaces.split_namespace( output_subnamespace, output_path = Namespaces.split_namespace( input_subpath, relative=True, ) output_project = output_gitlab.project_import( file_export.name, f'{options.output_group}{output_namespace}', f'{options.output_namespace}{output_subnamespace}', output_path, input_project.name, options.overwrite, Loading @@ -395,12 +471,14 @@ class Entrypoint: # Acquire subgroup description output_subgroup_description: str if not options.dry_run: if options.dry_run: output_subgroup_description = input_project.description elif output_namespace.kind == 'user': output_subgroup_description = output_namespace.name else: output_subgroup = output_gitlab.group( f'{options.output_group}{output_namespace}') f'{options.output_namespace}{output_subnamespace}') output_subgroup_description = output_subgroup.description else: output_subgroup_description = input_project.description # Update project description if options.update_description: Loading
src/cli/main.py +4 −4 Changes for src/cli/main.py: 4 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -156,7 +156,7 @@ def main() -> None: dest='input_path', action='store', nargs='?', help='Input GitLab group or project path', help='Input GitLab group, user namespace or project path', ) group.add_argument( dest='output_gitlab', Loading @@ -166,11 +166,11 @@ def main() -> None: help='Output GitLab URL (default: https://gitlab.com)', ) group.add_argument( dest='output_group', dest='output_namespace', action='store', nargs='?', default='', help='Output GitLab group', help='Output GitLab group or user namespace', ) # Arguments parser Loading @@ -197,7 +197,7 @@ def main() -> None: # Arguments validation if not options.input_token or not options.input_gitlab or not options.input_path \ or not options.output_gitlab or not options.output_group: or not options.output_gitlab or not options.output_namespace: print(' ') parser.print_help() print(' ') Loading
src/features/gitlab.py +54 −2 Changes for src/features/gitlab.py: 54 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -13,7 +13,7 @@ from typing import Any, List, Optional # Modules libraries from gitlab import Gitlab from gitlab.exceptions import GitlabDeleteError, GitlabGetError from gitlab.v4.objects import Group, Project from gitlab.v4.objects import Group, Namespace, Project, User # Components from ..types.namespaces import Namespaces Loading @@ -34,7 +34,7 @@ class GitLabFeature: # Group def group(self, criteria: str, optional: bool = False) -> Group: # Fake group for dry run # Optional group for dry run if self.__dry_run and optional: try: return self.group(criteria, optional=False) Loading @@ -53,6 +53,7 @@ class GitLabFeature: def list(self, **_kwargs: Optional[Any]) -> List[str]: return [] # Create fake group _, path = Namespaces.split_namespace( criteria, relative=False, Loading Loading @@ -181,6 +182,53 @@ class GitLabFeature: # Save group group.save() # Namespace def namespace(self, criteria: str, optional: bool = False) -> Namespace: # Optional namespace for dry run if self.__dry_run and optional: try: return self.namespace(criteria, optional=False) # Simulate namespace except GitlabGetError: NamespaceTuple = namedtuple('NamespaceTuple', [ 'id', 'name', 'full_path', ]) # Create fake namespace _, path = Namespaces.split_namespace( criteria, relative=False, ) return NamespaceTuple( id='dry-run', name=path, full_path=criteria, ) # Get namespace return self.__gitlab.namespaces.get(criteria) # Namespace projects def namespace_projects(self, criteria: str) -> Any: # Acquire namespace namespace = self.namespace(criteria) # Get user projects if namespace.kind == 'user': user = self.user(criteria) return user.projects.list(get_all=True) # Get group projects return self.group(criteria).projects.list( get_all=True, include_subgroups=True, ) # Project def project(self, criteria: str) -> Project: return self.__gitlab.projects.get(criteria) Loading Loading @@ -332,6 +380,10 @@ class GitLabFeature: }) sleep(5) # User def user(self, criteria: str) -> User: return self.__gitlab.users.list(username=criteria)[0] # URL @property def url(self) -> str: Loading