Skip to content
Snippets Groups Projects
update_submodule.py 6.81 KiB
Newer Older
Tim Jaacks's avatar
Tim Jaacks committed
#!/usr/bin/env python3
import common

import argparse
import os
import sys
import tempfile
from furl import furl
from git import GitCommandError, Repo
from gitlab import Gitlab


def update_submodule(project, submodule_name, submodule_revision, branch=None):
    """Update submodule of gitlab project to given revision"""

    gitlab = project.manager.gitlab

    # If no branch is given, use project's default branch
    if branch is None:
        branch = project.default_branch

    with tempfile.TemporaryDirectory() as project_dir:

        # Construct clone url containing access token
        clone_url = furl(project.http_url_to_repo)
        clone_url.username = "gitlab-ci"
        clone_url.password = gitlab.private_token

        # Checkout project
        try:
            repo = Repo.clone_from(clone_url.url, project_dir, branch=branch)
        except GitCommandError as e:
            sys.exit("ERROR: could not clone repository\n" + str(e))
        except IndexError:
Tim Jaacks's avatar
Tim Jaacks committed
            sys.exit("ERROR: branch '%s' not found" % branch)

        # Find submodule
        submodule = common.get_submodule(repo, submodule_name)

        # Check if revisions are different
        if submodule.hexsha == submodule_revision:
            print("Submodule is already at %s" % submodule_revision)
Tim Jaacks's avatar
Tim Jaacks committed

        # Check for relative path
        if not submodule.url.startswith(".."):
            sys.exit(
                "ERROR: absolute submodule paths are not supported (%s)" % submodule.url
            )

        # Get absolute project path
        # This cannot be done with gitpython directly due to issue:
        # https://github.com/gitpython-developers/GitPython/issues/730
        relative_path = os.path.splitext(submodule.url)[0]  # remove .git
        project_path = project.path_with_namespace
        while relative_path.startswith(".."):
            relative_path = relative_path[3:]  # strip off '../'
            project_path, _ = os.path.split(project_path)  # remove last part
        submodule_project_path = os.path.join(project_path, relative_path)

        # Get submodule project
        submodule_project = common.get_project(gitlab, submodule_project_path)

        # Get commits between current and new revision
        revision_range = submodule.hexsha + ".." + submodule_revision
        commits = submodule_project.commits.list(ref_name=revision_range)
        if not commits:
            sys.exit("ERROR: no commits found in range %s" % revision_range)

        # Find out if top commit is part of a merge request
        # If so, use source branch of this MR as integration branch name
        # Else use commit sha instead
        integration_branch_suffix = submodule_revision
        for mr in commits[0].merge_requests():
            if mr["target_branch"] == submodule_project.default_branch:
                integration_branch_suffix = mr["source_branch"]
                break

        # Initialize submodule
        # Hack due to issue above: change to absolute path and switch back afterwards
        submodule_clone_url = furl(submodule_project.http_url_to_repo)
        submodule_clone_url.username = "gitlab-ci"
        submodule_clone_url.password = gitlab.private_token
        submodule_relative_url = submodule.url
        with submodule.config_writer() as writer:
            writer.set("url", submodule_clone_url.url)
        submodule.update(init=True)
        with submodule.config_writer() as writer:
            writer.set("url", submodule_relative_url)

        # Check if integration branch already exists and if it is up to date
        integration_branch = common.integration_branch_name(
            submodule_project.name, integration_branch_suffix
        )
        existing_branch = None
        for ref in repo.references:
            if "origin/" + integration_branch == ref.name:
                existing_branch = ref
        if existing_branch:
            repo.head.set_reference(existing_branch)
Tim Jaacks's avatar
Tim Jaacks committed
            submodule = common.get_submodule(repo, submodule_name)
            if submodule.hexsha == submodule_revision:
                print(
                    "Submodule is already at %s on branch %s"
                    % (submodule_revision, integration_branch)
                )
                return (integration_branch, existing_branch.commit, submodule_project)
            else:
                print("Replacing outdated integration branch %s" % integration_branch)
                repo.head.set_reference(branch)
                submodule = common.get_submodule(repo, submodule_name)
Tim Jaacks's avatar
Tim Jaacks committed
        else:
            print("Creating integration branch %s" % integration_branch)

        # Create integration branch
        repo.head.set_reference(repo.create_head(integration_branch))
Tim Jaacks's avatar
Tim Jaacks committed

        # Update submodule
        try:
            submodule.module().git.checkout(submodule_revision)
        except GitCommandError as e:
            sys.exit("ERROR: could not checkout commit\n" + str(e))
        repo.git.add(submodule.path)

        # Make an API request to create the gitlab.user object
        gitlab.auth()

        # Construct commit message and commit the change
        message = "Integrate %s/%s%s\n%s" % (
            submodule_project.name,
            integration_branch_suffix,
            " and %d more" % (len(commits) - 1) if len(commits) > 1 else "",
            common.list_commits(commits),
        )
        project_revision = common.commit_and_push(
            project,
            repo,
            integration_branch,
            message,
            gitlab.user.username,
            gitlab.user.email,
        )

        return (integration_branch, project_revision, submodule_project)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--gitlab-url",
        help="""URL to the GitLab instance""",
        dest="gitlab_url",
        required=True,
    )
    parser.add_argument(
        "--token",
        help="""GitLab REST API private access token""",
        dest="token",
        required=True,
    )
    parser.add_argument(
        "--project",
        help="""name of the GitLab project""",
        dest="project",
        required=True,
    )
    parser.add_argument(
        "--submodule",
        help="""submodule to update""",
        dest="submodule",
        required=True,
    )
    parser.add_argument(
        "--revision",
        help="""new revision for submodule""",
        dest="revision",
        required=True,
    )
    parser.add_argument(
        "--branch",
        help="""project branch (if not default branch)""",
        dest="branch",
        required=False,
        default=None,
    )

    args, _ = parser.parse_known_args()

    gitlab = Gitlab(args.gitlab_url, private_token=args.token)
    project = common.get_project(gitlab, args.project)

    update_submodule(project, args.submodule, args.revision, args.branch)


if __name__ == "__main__":
    main()