#!/usr/bin/env python3 import common import argparse import logging 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 """ 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: 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) return (None, None, None) # 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) 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) else: print("Creating integration branch %s" % integration_branch) # Create integration branch repo.head.set_reference(repo.create_head(integration_branch)) # 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) # 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.""", ) args, _ = parser.parse_known_args() if args.verbose: logging.basicConfig(level=logging.DEBUG) 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()