Commit 68d510a8 authored by nimrod's avatar nimrod
Browse files

git manage tfinit and a little bit more.

- Add a new git manage command, tfinit, that initializes a Terraform
  backend managed by GitLab for this repository (based on the GitLab
remote).
- Better handling of cases when running in a subdirectory of the repo in
  the git module. Use git to check if a directory is a repository and to
find the top level of a repository.
- Use the top level functions' docstring for the CLI help message.
- Use the sub-parser to output better error messages.
parent 60c51e9f
Loading
Loading
Loading
Loading
+78 −27
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ import github3.exceptions # pylint: disable=import-error
import gitlab.exceptions  # pylint: disable=import-error
import passhole.passhole  # pylint: disable=import-error
import sh  # pylint: disable=import-error
from sh import terraform  # pylint: disable=import-error

sys.path.append(os.path.expanduser("~/Documents/bin"))

@@ -21,6 +22,13 @@ import rcfiles.gitlab # noqa: E402 pylint: disable=wrong-import-position
GH_MIRROR_PREFIX = "https://*****@github.com/"


def error(message):
    """Print an error message with the current subparser's usage and exit."""
    # pylint: disable=protected-access
    sub_parser = arg_parser._subparsers._actions[1].choices[_args.command]
    sub_parser.error(message)


def github_mirrors(project, gh_conn):
    """Return a list of GitHub repositories that are mirrored from the GitLab
    project."""
@@ -139,9 +147,9 @@ def guess_remote(remote_type="gitlab", gl_conn=None):

def guess_name(args, gh_conn=None, gl_conn=None):
    """Try to guess the name from the arguments and the repository remotes."""
    if args.name is None:
    if "name" not in args or args.name is None:
        if not rcfiles.git.in_repo():
            arg_parser.error("Name not provided and not in a Git repo.")
            error("Name not provided and not in a Git repo.")

        remote = guess_remote(
            remote_type="github"
@@ -151,13 +159,9 @@ def guess_name(args, gh_conn=None, gl_conn=None):
        )
        if remote is None:
            if "github" in args and args.github:
                arg_parser.error(
                    "Name not provided and could not find a GitHub remote."
                )
                error("Name not provided and could not find a GitHub remote.")
            else:
                arg_parser.error(
                    "Name not provided and could not find a GitLab remote."
                )
                error("Name not provided and could not find a GitLab remote.")
        else:
            if "github" in args and args.github:
                name = rcfiles.github.url_to_name(remote["url"])
@@ -198,15 +202,13 @@ def create_github_repo(args):
    - Commits an initial empty commit.
    """
    if "/" in args.name:
        arg_parser.error("Can't specify an organization.")
        error("Can't specify an organization.")
    if args.internal or args.private:
        arg_parser.error(
            "Can't create internal or private GitHub repositories."
        )
        error("Can't create internal or private GitHub repositories.")
    try:
        conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        arg_parser.error(f"Failed to connect to GitHub: {e}")
        error(f"Failed to connect to GitHub: {e}")

    repo = conn.create_repository(
        args.name,
@@ -238,11 +240,11 @@ def create_gitlab_repo(args):
    - Adds the mirror remote.
    """
    if args.private and args.internal:
        arg_parser.error("Repository can be internal or private, not both.")
        error("Repository can be internal or private, not both.")
    try:
        conn = rcfiles.gitlab.connect()
    except Exception as e:  # pylint: disable=broad-except
        arg_parser.error(f"Failed to connect to GitLab: {e}")
        error(f"Failed to connect to GitLab: {e}")

    if args.private:
        visibility = "private"
@@ -296,7 +298,7 @@ def create_gitlab_repo(args):
def create_repo(args):
    """Create a new repository."""
    if args.mirror and args.github:
        arg_parser.error("Can't mirror from GitHub to GitLab.")
        error("Can't mirror from GitHub to GitLab.")
    if args.github:
        create_github_repo(args)
    else:
@@ -313,18 +315,18 @@ def mirror_repo(args):
    try:
        gh_conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        arg_parser.error(f"Failed to connect to GitHub: {e}")
        error(f"Failed to connect to GitHub: {e}")
    try:
        gl_conn = rcfiles.gitlab.connect()
    except Exception as e:  # pylint: disable=broad-except
        arg_parser.error(f"Failed to connect to GitLab: {e}")
        error(f"Failed to connect to GitLab: {e}")

    name = guess_name(args, gh_conn, gl_conn)

    try:
        project = gl_conn.projects.get(name)
    except gitlab.exceptions.GitlabGetError:
        arg_parser.error(f"Could not find GitLab project {name}.")
        error(f"Could not find GitLab project {name}.")

    gh_repo = mirror_project(project, gh_conn, get_mirror_token())
    print(
@@ -349,12 +351,12 @@ def archive_repo(args):
    try:
        gh_conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        arg_parser.error(f"Failed to connect to GitHub: {e}")
        error(f"Failed to connect to GitHub: {e}")
    if not args.github:
        try:
            gl_conn = rcfiles.gitlab.connect()
        except Exception as e:  # pylint: disable=broad-except
            arg_parser.error(f"Failed to connect to GitLab: {e}")
            error(f"Failed to connect to GitLab: {e}")

    if args.github:
        owner, name = guess_name(args, gh_conn=gh_conn, gl_conn=None).split(
@@ -379,7 +381,7 @@ def archive_repo(args):


def fork_repo(args):
    """Forks a GitHub repository.
    """Fork a GitHub repository.

    Does the following:
    - Forks the GitHub repository.
@@ -389,10 +391,10 @@ def fork_repo(args):
    try:
        conn = rcfiles.github.connect()
    except Exception as e:  # pylint: disable=broad-except
        arg_parser.error(f"Failed to connect to GitHub: {e}")
        error(f"Failed to connect to GitHub: {e}")

    if "/" not in args.name:
        arg_parser.error("Must provide a full repository name.")
        error("Must provide a full repository name.")

    org, name = args.name.split("/")
    upstream = conn.repository(org, name)
@@ -410,6 +412,43 @@ def fork_repo(args):
        print("Added an upstream remote.", file=sys.stderr)


def terraform_init(args):
    """Initialize a GitLab-managed Terraform state."""
    if not rcfiles.git.in_repo():
        error("This command must be run from inside a Git repository.")
    try:
        conn = rcfiles.gitlab.connect()
    except Exception as e:  # pylint: disable=broad-except
        error(f"Failed to connect to GitLab: {e}")
    remotes = rcfiles.git.get_all_remotes()
    for remote in remotes.values():
        if rcfiles.gitlab.is_gitlab_url(conn, remote["url"]):
            break
    else:
        error(
            "This command must be run from inside a Git repository with a GitLab remote."  # noqa: E501
        )
    username = rcfiles.gitlab.me(conn)
    url = rcfiles.gitlab.http_url(conn)
    name = guess_name(args, gl_conn=conn)
    project = conn.projects.get(name)
    address = (
        f"{url}/api/v4/projects/{project.id}/terraform/state/{args.state}"
    )
    # fmt: off
    terraform.init(
        "-backend-config", f"address={address}",
        "-backend-config", f"lock_address={address}/lock",
        "-backend-config", f"unlock_address={address}/lock",
        "-backend-config", f"username={username}",
        "-backend-config", f"password={conn.private_token}",
        "-backend-config", "lock_method=POST",
        "-backend-config", "unlock_method=DELETE",
        "-reconfigure",
    )
    # fmt: on


def build_arg_parser():
    """Builds the argument parser."""
    parser = argparse.ArgumentParser(description=__doc__)
@@ -418,7 +457,7 @@ def build_arg_parser():
    )

    parser_create = subparsers.add_parser(
        "new", help="Create a new repository."
        "new", help=create_repo.__doc__.splitlines()[0]
    )
    parser_create.set_defaults(func=create_repo)
    parser_create.add_argument("name", help="Name of the repository.")
@@ -446,7 +485,7 @@ def build_arg_parser():
    )

    parser_mirror = subparsers.add_parser(
        "mirror", help="Mirror a GitLab repository to GitHub."
        "mirror", help=mirror_repo.__doc__.splitlines()[0]
    )
    parser_mirror.set_defaults(func=mirror_repo)
    parser_mirror.add_argument(
@@ -454,7 +493,7 @@ def build_arg_parser():
    )

    parser_archive = subparsers.add_parser(
        "archive", help="Archive a repository."
        "archive", help=archive_repo.__doc__.splitlines()[0]
    )
    parser_archive.set_defaults(func=archive_repo)
    parser_archive.add_argument(
@@ -469,6 +508,18 @@ def build_arg_parser():
    )
    parser_fork.set_defaults(func=fork_repo)
    parser_fork.add_argument("name", help="Name of the repository.")

    parser_tfinit = subparsers.add_parser(
        "tfinit", help=terraform_init.__doc__.splitlines()[0]
    )
    parser_tfinit.set_defaults(func=terraform_init)
    parser_tfinit.add_argument(
        "state",
        help="The Terraform state name",
        default="default",
        nargs="?",
    )

    return parser


+16 −9
Original line number Diff line number Diff line
@@ -10,20 +10,27 @@ from sh.contrib import git # pylint: disable=import-error

def is_repo(path):
    """Returns a boolean if the path is a Git repo."""
    return os.path.isdir(path) and pathlib.Path(path, ".git").is_dir()
    try:
        git("-C", path, "rev-parse", "--is-inside-work-tree")
    except sh.ErrorReturnCode:
        return False
    return True


def in_repo():
    """Is the current working directory a git repo?

    Because we invoke the command as a Git command (git foo) it is run from
    the root of the repository if inside a repository so there's no need to
    traverse up the directory hierarchy to find if we're in a Git repository,
    it's enough to just check if the .git directory exists where we are.
    """
    """Is the current working directory a git repo?"""
    return is_repo(".")


def find_repo_toplevel(path):
    """Return the repository's top level directory (the root of the repo)."""
    if not is_repo(path):
        return None
    return pathlib.Path(
        git("-C", path, "rev-parse", "--show-toplevel").strip()
    )


def get_all_remotes():
    """Return a dictionary of remotes and their URL.

@@ -33,7 +40,7 @@ def get_all_remotes():
        return None

    config = configparser.ConfigParser()
    config.read(".git/config")
    config.read(find_repo_toplevel(".") / ".git/config")

    remotes = {
        x.removeprefix('remote "').removesuffix('"'): {