diff --git a/Documents/bin/git-namespace-backup b/Documents/bin/git-namespace-backup new file mode 100755 index 0000000000000000000000000000000000000000..0653a980330e809c34157b2ac0109aec15aee6da --- /dev/null +++ b/Documents/bin/git-namespace-backup @@ -0,0 +1,143 @@ +#!/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")