From 64c7663efc9c44e31622cce8559ebccd15c621d5 Mon Sep 17 00:00:00 2001
From: Adar Nimrod <nimrod@shore.co.il>
Date: Tue, 7 Sep 2021 18:06:44 +0300
Subject: [PATCH] Working git-manage (I think).

- Finished implementing all of the functionality I had in mind (for
  now).
- Added a bunch of info output.
- Fixed a few small issues.
- Worked around an issue with creating an initial commit in GitHub
  (ended up doing it with the git CLI).
- More information in the docstrings for the top level functions.
---
 Documents/bin/git-manage        | 188 ++++++++++++++++++++++++++++++--
 Documents/bin/rcfiles/git.py    |  13 ++-
 Documents/bin/rcfiles/github.py |   7 --
 Documents/bin/rcfiles/gitlab.py |   6 +-
 4 files changed, 188 insertions(+), 26 deletions(-)

diff --git a/Documents/bin/git-manage b/Documents/bin/git-manage
index c7b5b38..774e9a8 100755
--- a/Documents/bin/git-manage
+++ b/Documents/bin/git-manage
@@ -9,6 +9,7 @@ import sys
 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
 
 sys.path.append(os.path.expanduser("~/Documents/bin"))
 
@@ -17,11 +18,12 @@ import rcfiles.github  # noqa: E402 pylint: disable=wrong-import-position
 import rcfiles.gitlab  # noqa: E402 pylint: disable=wrong-import-position
 
 
-GH_MIRROR_PREFIX = "https://*****@github.com"
+GH_MIRROR_PREFIX = "https://*****@github.com/"
 
 
 def github_mirrors(project, gh_conn):
-    """Return a list of GitHub repositories that are mirrored from GitLab."""
+    """Return a list of GitHub repositories that are mirrored from the GitLab
+    project."""
     repos = []
     for mirror in project.remote_mirrors.list(all=True):
         if mirror.url.startswith(GH_MIRROR_PREFIX):
@@ -39,14 +41,22 @@ def github_mirrors(project, gh_conn):
 
 
 def mirror_project(project, gh_conn, token):
-    """Mirror a project to GitHub."""
+    """Mirror a GitLab project to GitHub."""
     gh_me = rcfiles.github.me(gh_conn)
     mirror_url = f"https://{token}@github.com/{gh_me}/{project.name}.git"
 
     try:
-        gh_repo = gh_conn.repository(project.name, gh_me)
+        gh_repo = gh_conn.repository(gh_me, project.name)
+        print(
+            f"Using existing GitHub repository {gh_repo.html_url}.",
+            file=sys.stderr,
+        )
     except github3.exceptions.NotFoundError:
         gh_repo = gh_conn.create_repository(project.name)
+        print(
+            f"Created a new GitHub reposiroty {gh_repo.html_url}.",
+            file=sys.stderr,
+        )
 
     gh_repo.edit(
         project.name,
@@ -54,7 +64,6 @@ def mirror_project(project, gh_conn, token):
         description=f"Mirror of {project.web_url}",
         has_issues=False,
         has_wiki=False,
-        default_branch=project.default_branch,
         has_projects=False,
     )
 
@@ -85,7 +94,7 @@ def get_mirror_token():
     # password.
     db = passhole.passhole.open_database()
     entry = db.find_entries(path=ENTRY_PATH, first=True)
-    return passhole.passhole.get_field(entry, TOKEN_FIELD)
+    return entry.get_custom_property(TOKEN_FIELD)
 
 
 def guess_remote(remote_type="gitlab", gl_conn=None):
@@ -154,28 +163,155 @@ def guess_name(args, gh_conn=None, gl_conn=None):
                 name = rcfiles.github.url_to_name(remote["url"])
             else:
                 name = rcfiles.gitlab.url_to_name(gl_conn, remote["url"])
+        print(
+            f"""Name not provided, using {name} from the {remote["name"]} remote.""",  # noqa: E501
+            file=sys.stderr,
+        )
     else:
         if "github" in args and args.github:
             if "/" in args.name:
                 name = args.name
             else:
+                print(
+                    "Name does not include project, defaulting to the GitHub user.",  # noqa: E501
+                    file=sys.stderr,
+                )
                 name = f"{rcfiles.github.me(gh_conn)}/{args.name}"
         else:
             if "/" in args.name:
                 name = args.name
             else:
+                print(
+                    "Name does not include project, defaulting to the GitLab user.",  # noqa: E501
+                    file=sys.stderr,
+                )
                 name = f"{rcfiles.gitlab.me(gl_conn)}/{args.name}"
     return name
 
 
+def create_github_repo(args):
+    """Create a new GitHub repository.
+
+    Does the following:
+    - Creates the mirror.
+    - Clones the repository.
+    - Commits an initial empty commit.
+    """
+    if "/" in args.name:
+        parser.error("Can't specify an organization.")
+    if args.internal or args.private:
+        parser.error("Can't create internal or private GitHub repositories.")
+    try:
+        conn = rcfiles.github.connect()
+    except Exception as e:  # pylint: disable=broad-except
+        parser.error(f"Failed to connect to GitHub: {e}")
+
+    repo = conn.create_repository(
+        args.name,
+        description=args.description,
+    )
+    print(
+        f"Created a new GitHub repository {repo.html_url}.",
+        file=sys.stderr,
+    )
+
+    rcfiles.git.git.clone(repo.ssh_url)
+    print("Cloned repository.", file=sys.stderr)
+
+    with sh.pushd(repo.name):
+        rcfiles.git.git.commit(
+            "--allow-empty", "--only", "--message", "Initial empty commit."
+        )
+        rcfiles.git.git.push("origin")
+    print(
+        "Committed an initial empty commit.",
+        file=sys.stderr,
+    )
+
+
+def create_gitlab_repo(args):
+    """Create a new GitLab repository.
+
+    Does the following:
+    - Creates the repository.
+    - Creates the mirror.
+    - Commits an initial empty commit.
+    - Clones the  repository.
+    - Adds the mirror remote.
+    """
+    if args.private and args.internal:
+        parser.error("Repository can be internal or private, not both.")
+    try:
+        conn = rcfiles.gitlab.connect()
+    except Exception as e:  # pylint: disable=broad-except
+        parser.error(f"Failed to connect to GitLab: {e}")
+
+    if args.private:
+        visibility = "private"
+    elif args.internal:
+        visibility = "internal"
+    else:
+        visibility = "public"
+
+    name_with_namespace = guess_name(args, gl_conn=conn, gh_conn=None)
+    namespace, name = name_with_namespace.split("/")
+    if namespace == rcfiles.gitlab.me(conn):
+        project = conn.projects.create(
+            {
+                "name": name,
+                "description": args.description,
+                "visibility": visibility,
+            }
+        )
+    else:
+        group = conn.groups.get(namespace)
+        project = conn.projects.create(
+            {
+                "name": name,
+                "description": args.description,
+                "visibility": visibility,
+                "namespace_id": group.id,
+            }
+        )
+    print(
+        f"Created a new {visibility} GitLab repository {project.web_url}.",
+        file=sys.stderr,
+    )
+
+    if args.mirror:
+        gh_repo = mirror_repo(args)
+
+    rcfiles.gitlab.empty_commit(project)
+    print(
+        "Committed an initial empty commit.",
+        file=sys.stderr,
+    )
+
+    rcfiles.git.git.clone(project.ssh_url_to_repo)
+    print("Cloned repository.", file=sys.stderr)
+
+    if args.mirror:
+        rcfiles.git.add_remote(project.name, "github", gh_repo.ssh_url)
+        print("Added a remote for the mirror repository.", file=sys.stderr)
+
+
 def create_repo(args):
     """Create a new repository."""
     if args.mirror and args.github:
         parser.error("Can't mirror from GitHub to GitLab.")
+    if args.github:
+        create_github_repo(args)
+    else:
+        create_gitlab_repo(args)
 
 
 def mirror_repo(args):
-    """Mirror a GitLab repository to GitHub."""
+    """Mirror a GitLab repository to GitHub.
+
+    Does the following:
+    - Creates the mirror.
+    - Adds the mirror remote.
+    """
     try:
         gh_conn = rcfiles.github.connect()
     except Exception as e:  # pylint: disable=broad-except
@@ -193,12 +329,25 @@ def mirror_repo(args):
         parser.error(f"Could not find GitLab project {name}.")
 
     gh_repo = mirror_project(project, gh_conn, get_mirror_token())
+    print(
+        f"Setup mirror for {project.web_url} to {gh_repo.html_url}.",
+        file=sys.stderr,
+    )
+
     if args.name is None and rcfiles.git.in_repo():
         rcfiles.git.add_remote(".", "github", gh_repo.ssh_url)
+        print("Added a remote for the mirror repository.", file=sys.stderr)
+
+    return gh_repo
 
 
 def archive_repo(args):
-    """Archive a repository."""
+    """Archive a repository.
+
+    Does the following:
+    - Archives the repository (sets it to read-only).
+    - Archives all GitHub mirrors.
+    """
     try:
         gh_conn = rcfiles.github.connect()
     except Exception as e:  # pylint: disable=broad-except
@@ -219,11 +368,16 @@ def archive_repo(args):
     if args.github:
         repo = gh_conn.repository(owner, name)
         repo.edit(name, archived=True)
+        print(f"Archived repository {repo.html_url}.", file=sys.stderr)
     else:
         project = gl_conn.projects.get(name)
         project.archive()
+        print(f"Archived repository {project.web_url}.", file=sys.stderr)
         for mirror in github_mirrors(project, gh_conn):
             mirror.edit(mirror.name, archived=True)
+            print(
+                f"Archived GitHub mirror {mirror.html_url}.", file=sys.stderr
+            )
 
 
 if __name__ == "__main__":
@@ -236,9 +390,7 @@ if __name__ == "__main__":
         "new", help="Create a new repository."
     )
     parser_create.set_defaults(func=create_repo)
-    parser_create.add_argument(
-        "name", help="Name of the repository.", nargs="?"
-    )
+    parser_create.add_argument("name", help="Name of the repository.")
     parser_create.add_argument(
         "-m", "--mirror", help="Setup a mirror in GitHub.", action="store_true"
     )
@@ -247,6 +399,20 @@ if __name__ == "__main__":
         help="Create the repository in GitHub.",
         action="store_true",
     )
+    parser_create.add_argument(
+        "--internal",
+        help="Create an internal GitLab repository.",
+        action="store_true",
+    )
+    parser_create.add_argument(
+        "--private",
+        help="Create a private GitLab repository.",
+        action="store_true",
+    )
+    parser_create.add_argument(
+        "--description",
+        help="Repository description.",
+    )
 
     parser_mirror = subparsers.add_parser(
         "mirror", help="Mirror a GitLab repository to GitHub."
diff --git a/Documents/bin/rcfiles/git.py b/Documents/bin/rcfiles/git.py
index f7ac33b..cd7b358 100644
--- a/Documents/bin/rcfiles/git.py
+++ b/Documents/bin/rcfiles/git.py
@@ -49,18 +49,21 @@ def get_all_remotes():
 def add_remote(repo, name, url):
     """Add a remote to the Git repository."""
     with sh.pushd(repo):
-        git.remote("add", name, url)
+        try:
+            git.remote("add", name, url)
+        except sh.ErrorReturnCode_3:
+            git.remote("set-url", name, url)
 
 
 def author_name():
     """Get the author name."""
     if "GIT_AUTHOR_NAME" in os.environ:
-        return os.environ["GIT_AUTHOR_NAME"]
-    return git.config("--get", "user.name")
+        return os.environ["GIT_AUTHOR_NAME"].strip()
+    return git.config("--get", "user.name").strip()
 
 
 def author_email():
     """Get the author email."""
     if "GIT_AUTHOR_EMAil" in os.environ:
-        return os.environ["GIT_AUTHOR_EMAIL"]
-    return git.config("--get", "user.email")
+        return os.environ["GIT_AUTHOR_EMAIL"].strip()
+    return git.config("--get", "user.email").strip()
diff --git a/Documents/bin/rcfiles/github.py b/Documents/bin/rcfiles/github.py
index 72d3294..e5a137e 100644
--- a/Documents/bin/rcfiles/github.py
+++ b/Documents/bin/rcfiles/github.py
@@ -3,8 +3,6 @@
 import os
 import github3  # pylint: disable=import-error
 
-# from . import git
-
 HTTP_URL = "https://github.com/"
 SSH_URL = "git@github.com:"
 
@@ -38,8 +36,3 @@ def connect():
 def me(conn):
     """Return my GitHub account name."""
     return conn.me().login
-
-
-def empty_commit(repository):
-    """Commit an empty commit."""
-    raise NotImplementedError
diff --git a/Documents/bin/rcfiles/gitlab.py b/Documents/bin/rcfiles/gitlab.py
index 3e28713..93cf04b 100644
--- a/Documents/bin/rcfiles/gitlab.py
+++ b/Documents/bin/rcfiles/gitlab.py
@@ -9,7 +9,7 @@ from . import git
 
 def http_url(conn):
     """Return the HTTP url to the GitLab instance."""
-    return conn.get_url()
+    return conn.url
 
 
 def ssh_url(conn):
@@ -21,7 +21,7 @@ def url_to_name(conn, url):
     """Get the full name from the GitLab URL."""
     return (
         url.removeprefix(http_url(conn))
-        .removeprefix(ssh_url(conn.git))
+        .removeprefix(ssh_url(conn))
         .removesuffix("/")
         .removesuffix(".git")
     )
@@ -91,7 +91,7 @@ def me(conn):
 
 def empty_commit(project):
     """Commit an empty commit."""
-    return project.commit.create(
+    return project.commits.create(
         {
             "id": project.id,
             "branch": project.default_branch,
-- 
GitLab