Commit 64c7663e authored by nimrod's avatar nimrod
Browse files

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.
parent fc71121e
Loading
Loading
Loading
Loading
+177 −11
Original line number Diff line number Diff line
@@ -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."
+8 −5
Original line number Diff line number Diff line
@@ -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):
        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()
+0 −7
Original line number Diff line number Diff line
@@ -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
+3 −3
Original line number Diff line number Diff line
@@ -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,