From 68d510a8ebdbb9a101df8c430bd11160fa4c5c39 Mon Sep 17 00:00:00 2001
From: Adar Nimrod <nimrod@shore.co.il>
Date: Sat, 12 Mar 2022 19:02:34 +0200
Subject: [PATCH] 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.
---
 Documents/bin/git-manage     | 105 ++++++++++++++++++++++++++---------
 Documents/bin/rcfiles/git.py |  25 ++++++---
 2 files changed, 94 insertions(+), 36 deletions(-)

diff --git a/Documents/bin/git-manage b/Documents/bin/git-manage
index 4b99db8..8f8ee5d 100755
--- a/Documents/bin/git-manage
+++ b/Documents/bin/git-manage
@@ -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
 
 
diff --git a/Documents/bin/rcfiles/git.py b/Documents/bin/rcfiles/git.py
index 45cc5f4..74f3fe8 100644
--- a/Documents/bin/rcfiles/git.py
+++ b/Documents/bin/rcfiles/git.py
@@ -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('"'): {
-- 
GitLab