diff --git a/common.py b/common.py index 8f77c6d737613d2ec5ffd925d39a073123ae0281..f93f5e1e95f661db8ab81417b0113236b75b2280 100755 --- a/common.py +++ b/common.py @@ -239,6 +239,7 @@ def clone_project(project: Project, into, branch=None): def get_repository_file_raw(project: Project, filename, ref=None): # TODO tree objects are not supported fileobj = get_repository_file_obj(project, filename, ref) + logging.debug("Read file '%s' from '%s' at ref %s", filename, project.name, ref) return project.repository_raw_blob( fileobj["id"], retry_transient_errors=True ).decode() @@ -253,7 +254,7 @@ def get_repository_file_obj(project: Project, filename, ref=None): repository_tree = project.repository_tree( ref=ref, all=True, retry_transient_errors=True ) - logging.debug(repository_tree) + # logging.debug(repository_tree) fileobj = [f for f in repository_tree if f["name"] == filename] if len(fileobj) == 0: diff --git a/deploy_gitlab_ci.py b/deploy_gitlab_ci.py index 19000e9bae4a42d501561a4f4ea710827da3fc41..75938a540326d2ffd2f320e53e58b57398a505c4 100755 --- a/deploy_gitlab_ci.py +++ b/deploy_gitlab_ci.py @@ -40,7 +40,13 @@ def read_keys_from_gitlab_ci_yml(gitlab_ci_yml): def integrate_submodule_into( - gitlab, project_name, submodule_name, new_revision, branch, commit_and_push=True + gitlab, + project_name, + submodule_name, + new_revision, + branch, + commit_and_push=True, + force_clone=False, ): gitlab_project = common.get_project(gitlab, project_name) @@ -56,9 +62,8 @@ def integrate_submodule_into( new_revision, branch, commit_and_push=commit_and_push, + force_clone=force_clone, ) - if integration_branch_name is None: - return None # ====================================== # Store the references for creating the integration # commit in the manifest later @@ -185,7 +190,9 @@ def main(): res = integrate_submodule_into( gitlab, p, args.submodule, args.revision, args.branch ) - if res is not None: + # Store in the list if commit is set (meaning there was an update or + # an exising integration branch) + if res["commit"] is not None: project_integration[p] = res # Update submodule in manifest project @@ -196,22 +203,19 @@ def main(): args.revision, args.branch, commit_and_push=False, + force_clone=True, ) - if manifest_project is not None: - project_integration[args.project] = manifest_project - - # If submodule is already at specified revision in all projects, exit successfully - if len(project_integration) == 0: - print("No integration done, changes are already included in all projects.") - sys.exit(0) + branch = args.branch + if branch is None: + branch = manifest_project["project"].default_branch # ======================================================= # Create and merge merge_requests if needed # ======================================================= if args.merge: # Get source merge request ( the one in the gitlab-ci repo) submodule_project_path, _ = get_submodule_project_path_and_revision( - manifest_project["project"], args.submodule, args.branch + manifest_project["project"], args.submodule, branch ) submodule_project = common.get_project(gitlab, submodule_project_path) mrs = get_merge_requests( @@ -292,12 +296,11 @@ def main(): continue # get BB_RECIPE_NAME from the projects .gitlab-ci.yml - gitlab_ci_yml_file = os.path.join( - integration["repo"].working_tree_dir, ".gitlab-ci.yml" + # Use direct read from gitlab as we have not checked out + # the repo if the branch is already up to date + gitlab_ci_yml = common.get_repository_file_raw( + integration["project"], ".gitlab-ci.yml", ref=integration["branch"] ) - logging.debug("Read recipe name from %s", gitlab_ci_yml_file) - with open(gitlab_ci_yml_file, "r", encoding="utf8") as fp: - gitlab_ci_yml = fp.read() project_keys = read_keys_from_gitlab_ci_yml(gitlab_ci_yml) new_srcrev = update_srcrev( @@ -319,6 +322,13 @@ def main(): manifest_project["repo"].git.add(args.srcrev_file) logging.debug(srcrev) + # ======================================================== + # Squash all commits on the integration branch to one + # ======================================================== + manifest_project["repo"].remotes.origin.fetch(branch) + manifest_master = manifest_project["project"].branches.get(branch) + manifest_project["repo"].git.reset("--soft", manifest_master.commit["id"]) + # ======================================================== # Now commit and push the changes to the manifest repo # ======================================================== diff --git a/update_submodule.py b/update_submodule.py index f74518d00536771cab5a82164b8f74e1073aab4d..4f18263944fec9a38177fbe33c92653bd5040465 100755 --- a/update_submodule.py +++ b/update_submodule.py @@ -9,7 +9,7 @@ import tempfile from configparser import ConfigParser from furl import furl from git import GitCommandError, Repo -from gitlab import Gitlab +from gitlab import Gitlab, GitlabGetError from gitlab.v4.objects import Project from ruamel.yaml import YAML @@ -21,7 +21,7 @@ def get_submodule_project_path_and_revision(project: Project, submodule, branch= logging.error("Submodule %s not found in %s.", submodule, project.name) return None, None - logging.debug("Gitmodules: %s", gitmodules) + # logging.debug("Gitmodules: %s", gitmodules) cfgparse = ConfigParser() cfgparse.read_string(gitmodules) @@ -121,7 +121,12 @@ def clone_project_and_submodule(project: Project, submodule_name, branch=None): submodule_relative_url = submodule.url with submodule.config_writer() as writer: writer.set("url", submodule_clone_url.url) - submodule.update(init=True) + try: + submodule.update(init=True) + except GitCommandError: + # This seems to happen when a not existing commit is referenced + logging.error("Failed to initialize submodule %s", submodule_name) + with submodule.config_writer() as writer: writer.set("url", submodule_relative_url) @@ -181,6 +186,7 @@ def update_submodule_and_include_ref( new_revision, branch=None, commit_and_push=True, + force_clone=False, ): """Update the submodule and include refs to the submodule in the given project. Create mergerequest if needed. @@ -191,6 +197,7 @@ def update_submodule_and_include_ref( new_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 commit_and_push: Set to false if no commit should be created. Changes are left in staging. + force_clone: Checkout repo and setup integration branch even if no update is needed Returns: tuple of: project_repo (Repo): GitPython repo with the cloned project @@ -198,23 +205,23 @@ def update_submodule_and_include_ref( integration_commit (hexsha): Hash of the newly created commit message: Commit message based on the integrated changes. """ + gitlab = project.manager.gitlab + + submodule_update_needed = True + project_repo = None + integration_commit = None if branch is None: branch = project.default_branch logging.debug("Branch: %s", branch) - _, submodule_current_rev = get_submodule_project_path_and_revision( - project, submodule_name, branch - ) - - # Check if revisions are different - if submodule_current_rev == new_revision: - print("Submodule is already at %s" % new_revision) - return (None, None, None, None) + ( + submodule_project_path, + submodule_current_rev, + ) = get_submodule_project_path_and_revision(project, submodule_name, branch) - project_repo, submodule_project = clone_project_and_submodule( - project, submodule_name, branch - ) + # Get submodule project + submodule_project = common.get_project(gitlab, submodule_project_path) # Get commits between current and new revision revision_range = submodule_current_rev + ".." + new_revision @@ -235,64 +242,134 @@ def update_submodule_and_include_ref( break logging.debug("Integration branch suffix: %s", integration_branch_suffix) - # Setup integration branch + # Construct integration branch name integration_branch_name = common.integration_branch_name( submodule_project.name, integration_branch_suffix ) - # Create integration branch - print("Creating/replacing integration branch %s" % integration_branch_name) - project_repo.head.set_reference(project_repo.create_head(integration_branch_name)) - - # Update submodule to new revision - submodule_repo = common.get_submodule(project_repo, submodule_name) - update_submodule_in_repo(project_repo, submodule_repo, new_revision) - - # Update the gitlab-ci.yml file to the new revision - # Now also update the project '.gitlab-ci.yml' file - gitlab_ci_yml_filename = os.path.join( - project_repo.working_tree_dir, ".gitlab-ci.yml" - ) - with open(gitlab_ci_yml_filename, "r", encoding="utf8") as fp: - gitlab_ci_yml = fp.read() - logging.debug(gitlab_ci_yml) - - new_gitlab_ci_yml = update_gitlab_ci_include( - gitlab_ci_yml, - submodule_project.web_url.split("//")[1].split("/", 1)[1], - new_revision, - ) - if new_gitlab_ci_yml is None: - print("Failed to update the include revision in '.gitlab-ci.yml'") - else: - logging.debug(new_gitlab_ci_yml) - with open(gitlab_ci_yml_filename, "w", encoding="utf8") as fp: - fp.write(new_gitlab_ci_yml) - project_repo.git.add(os.path.basename(gitlab_ci_yml_filename)) - - # Construct commit message and commit the change + + # Construct commit message 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), ) - # Commit the changes - if commit_and_push: - - # Make an API request to create the gitlab.user object - gitlab = project.manager.gitlab - gitlab.auth() - integration_commit = common.commit_and_push( - project, - project_repo, - integration_branch_name, - message, - gitlab.user.username, - gitlab.user.email, - less_verbose=True, + + # Check if revisions are different + if submodule_current_rev == new_revision: + print("Submodule is already at %s" % new_revision) + submodule_update_needed = False + + # Check if we already have an integration branch (before we actually do the checkout) + # Check if integration branch already exists and if it is up to date + # This is needed for one use case: + # It is possible to amend changes to the integration branch manually + # outside the pipeline. + # When the submodule revision has not changed and the pipeline is run + # again (due to merge or manually triggered) the manual change persists + # in the final commit + # For example rename a file in gitlab-ci repo that is included in a + # subproject, requires an adapted include statement here. + # To get this change 'atomic' the updated include statement should be + # in the same commit as the update of the submodule + + if submodule_update_needed or force_clone: + existing_branch = None + try: + existing_branch = project.branches.get(integration_branch_name) + except GitlabGetError: + # Branch not found + pass + + if existing_branch: + ( + _, + integration_branch_submodule_rev, + ) = get_submodule_project_path_and_revision( + project, submodule_name, integration_branch_name + ) + logging.debug( + "Revision in integration branch '%s', new_revision '%s'", + integration_branch_submodule_rev, + new_revision, + ) + + if integration_branch_submodule_rev == new_revision: + print( + "Submodule is already at %s on branch %s" + % (new_revision, integration_branch_name) + ) + integration_commit = existing_branch.commit["id"] + submodule_update_needed = False + + # Clone the project, we need to do changes + if submodule_update_needed or force_clone: + clone_branch = branch + if existing_branch: + clone_branch = integration_branch_name + + # Actually clone + project_repo, submodule_project = clone_project_and_submodule( + project, submodule_name, clone_branch + ) + + if existing_branch: + print("Using existing integration branch %s" % integration_branch_name) + else: + # Create branch + print("Creating integration branch %s" % integration_branch_name) + project_repo.head.set_reference( + project_repo.create_head(integration_branch_name) + ) + + if submodule_update_needed: + # Update submodule to new revision + submodule_repo = common.get_submodule(project_repo, submodule_name) + update_submodule_in_repo(project_repo, submodule_repo, new_revision) + + # Update the gitlab-ci.yml file to the new revision + # Now also update the project '.gitlab-ci.yml' file + gitlab_ci_yml_filename = os.path.join( + project_repo.working_tree_dir, ".gitlab-ci.yml" + ) + with open(gitlab_ci_yml_filename, "r", encoding="utf8") as fp: + gitlab_ci_yml = fp.read() + logging.debug(gitlab_ci_yml) + + new_gitlab_ci_yml = update_gitlab_ci_include( + gitlab_ci_yml, + submodule_project.web_url.split("//")[1].split("/", 1)[1], + new_revision, ) - else: - integration_commit = None + if new_gitlab_ci_yml is None: + print("Failed to update the include revision in '.gitlab-ci.yml'") + else: + logging.debug(new_gitlab_ci_yml) + with open(gitlab_ci_yml_filename, "w", encoding="utf8") as fp: + fp.write(new_gitlab_ci_yml) + project_repo.git.add(os.path.basename(gitlab_ci_yml_filename)) + + # Commit the changes + if commit_and_push: + # ======================================================== + # Squash all commits on the integration branch to one + # ======================================================== + project_repo.remotes.origin.fetch(branch) + gitlab_branch = project.branches.get(branch) + project_repo.git.reset("--soft", gitlab_branch.commit["id"]) + + # Make an API request to create the gitlab.user object + gitlab.auth() + # Push the changes + integration_commit = common.commit_and_push( + project, + project_repo, + integration_branch_name, + message, + gitlab.user.username, + gitlab.user.email, + less_verbose=True, + ) return project_repo, integration_branch_name, integration_commit, message