#!/usr/bin/env python3 # pylint: disable=invalid-name """Backup a Git repositories namespace to my GitLab instace. Which means creating a namespace in GitLab, creating a project for every repository in the namespace, adding a remote to the repository and pushing everything to that project. In this context, a namespace is a directory under ~/Repositories. """ import argparse import pathlib import sys import os import os.path import re import gitlab # pylint: disable=import-error import sh # pylint: disable=import-error from sh.contrib import git # pylint: disable=import-error def namespace_path(namespace): """Returns the full path to the namespace. Raises an exception if the path doesn't exits or isn't a directory. """ path = pathlib.Path(os.path.expanduser("~/Repositories/" + namespace)) if not path.exists(): raise argparse.ArgumentTypeError("Path {} does not exit.".format(path)) if not path.is_dir(): raise argparse.ArgumentTypeError( "Path {} is not a directory.".format(path) ) return path def name_to_path(name): """Converts a name to valid path in GitLab.""" return re.sub("[^a-zA-Z0-9_-]", "-", name).lower() def get_group(lab, name): """Returns a GitLab group object. If a group with that name already exists, return that. Otherwise create a new group and return that instead. """ for i in lab.groups.list(): if i.name == name: print( "Using existing group id: {}, name: {}, path: {}.".format( i.id, i.name, i.path ), file=sys.stderr, ) return i new_group = lab.groups.create( { "name": name, "path": name_to_path(name), "visivility": "internal", } ) print( "Created new group id: {}, name: {}, path: {}.".format( new_group.id, new_group.name, new_group.path ), file=sys.stderr, ) return group def get_project(group, name): # pylint: disable=redefined-outer-name """Returns a GitLab project. If a project with that name already exists, return that. Otherwise create a new project and return that instead. """ for i in group.projects.list(): if i.name == name: print( "Using existing project id: {}, name: {}, path: {}.".format( i.id, i.name, i.path ), file=sys.stderr, ) return i new_project = group.projects.gitlab.projects.create( {"name": name, "namespace_id": group.id} ) print( "Created new project id: {}, name: {}, path: {}.".format( new_project.id, new_project.name, new_project.path ), file=sys.stderr, ) return project def is_git_repo(path): """Returns a boolean if the path is a Git repo.""" return path.is_dir() and pathlib.Path(path, ".git").is_dir() def list_repositories(namespace): """Returns a list of paths under the namespace that are Git repositories.""" return [x for x in namespace.iterdir() if is_git_repo(x)] TOKEN = os.environ["GITLAB_PRIVATE_TOKEN"] URL = os.environ["GITLAB_BASE_URL"].removesuffix("api/v4") if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("namespace", type=namespace_path) args = parser.parse_args() with gitlab.Gitlab(url=URL, private_token=TOKEN) as Lab: group = get_group(Lab, args.namespace.name) for repo in list_repositories(args.namespace): project = get_project(group, repo.name) with sh.pushd(repo): if "shore.co.il" in git.remote().splitlines(): print( "Setting the remote URL in {}.".format(repo.name), file=sys.stderr, ) git.remote( "set-url", "shore.co.il", project.ssh_url_to_repo ) else: print( "Adding remote in {}.".format(repo.name), file=sys.stderr, ) git.remote("add", "shore.co.il", project.ssh_url_to_repo) print( "Pushing to {}.".format(project.ssh_url_to_repo), file=sys.stderr, ) git.push("--all", "shore.co.il")