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

import argparse
Tim Jaacks's avatar
Tim Jaacks committed
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, pre_commit_hook=None
):
    """Update submodule of gitlab project to given revision

    Parameters:
        project (gitlab project): The project which's submodule should be updated
        submodule_name (string): The name of the submodule to pull
        submodule_revision (hex string): The sha hash of the commit to update the submodule to
        branch (string): branch to update, if None, the projects default branch is used
        pre_commit_hook: Function to be called before the actual commit is done, to add additional changes.
                         Arguments passed: ( repo, submodule_project, submodule_revision)
    Returns: tuple of:
        branch (string): Name of the newly created integration branch
        revision (string): hexsha of the new commit
        submodule_project ( gitlab project): The submodule as gitlab priject instance

    """
Tim Jaacks's avatar
Tim Jaacks committed

    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)

        if pre_commit_hook is not None:
            pre_commit_hook(repo, submodule_project, submodule_revision)

Tim Jaacks's avatar
Tim Jaacks committed
        # 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,
    )
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help="""Increase verbosity.""",
    )
Tim Jaacks's avatar
Tim Jaacks committed

    args, _ = parser.parse_known_args()
    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)
Tim Jaacks's avatar
Tim Jaacks committed

    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()