From 78a47e833b22d6be225b04125766d3fb65565eb1 Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Wed, 18 Dec 2024 17:09:24 +0000 Subject: [PATCH 1/8] Add automerge script This introduces a script to automatically perform the merge of incoming changes from upstream LLVM which live in the `main` branch into the downstream `arm-software` branch. It also includes a GitHub Workflow to execute it after every sync from upstream. --- .github/workflows/automerge.yml | 26 +++++ arm-software/ci/automerge.py | 196 ++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 .github/workflows/automerge.yml create mode 100755 arm-software/ci/automerge.py diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml new file mode 100644 index 000000000000..17528a834a5f --- /dev/null +++ b/.github/workflows/automerge.yml @@ -0,0 +1,26 @@ +# This workflow executes the automerge python script to automatically merge +# changes from the `main` branch of upstream LLVM into the `arm-software` +# branch of the arm/arm-toolchain repository. +name: Automerge +on: + workflow_run: + workflows: [Sync from Upstream LLVM] + types: + - completed +jobs: + Run-Automerge: + runs-on: ubuntu-latest + env: + FROM_BRANCH: main + TO_BRANCH: arm-software + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Configure Git Identity + run: | + git config --local user.name "github-actions[bot]" + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + - name: Run automerge + run: python3 arm-software/ci/automerge.py --project-name ${{ env.GITHUB_REPOSITORY }} --from-branch ${{ env.FROM_BRANCH }} --to-branch ${{ env.TO_BRANCH }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py new file mode 100755 index 000000000000..4dc2cb164ca2 --- /dev/null +++ b/arm-software/ci/automerge.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 + +""" +A script to automatically perform the merge of incoming changes from a branch +in upstream LLVM into a downstream branch. +""" + +import argparse +import json +import logging +import subprocess +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") +logger = logging.getLogger(__name__) + + +MERGE_CONFLICT_LABEL = "automerge_conflict" +AUTOMERGE_BRANCH = "automerge" +REMOTE_NAME = "origin" + + +class MergeConflictError(Exception): + """ + An exception representing a failed merge from upstream due to a conflict. + """ + def __init__(self, commit_hash: str) -> None: + super().__init__() + self.commit_hash = commit_hash + + +class Git(): + """ + A helper class for running Git commands on a repository that lives in a + specific path. + """ + def __init__(self, repo_path: Path) -> None: + self.repo_path = repo_path + + def run_cmd(self, args: list[str]) -> str: + git_cmd = ["git", "-C", str(self.repo_path)] + args + git_process = subprocess.run(git_cmd, check=True, capture_output=True, text=True) + return git_process.stdout + + +def prefix_current_commit_message(git_repo: Git) -> None: + log_output = git_repo.run_cmd(["log", "HEAD", "--max-count=1", "--pretty=format:%B"]) + commit_msg = f"Automerge: {log_output}" + git_repo.run_cmd(["commit", "--amend", "--message=" + commit_msg]) + + +def merge_commit(git_repo: Git, to_branch: str, commit_hash: str, dry_run: bool) -> None: + logger.info(f"Merging commit {commit_hash} into {to_branch}") + git_repo.run_cmd(["switch", to_branch]) + try: + git_repo.run_cmd(["merge", commit_hash, "--no-edit"]) + except subprocess.CalledProcessError: + logger.info("Merge failed") + git_repo.run_cmf(["merge", "--abort"]) + raise MergeConflictError(commit_hash) + git_repo.run_cmd(["commit", "--amend", "--reuse-message", commit_hash]) + prefix_current_commit_message(git_repo) + if dry_run: + logger.info("Dry run. Skipping push into remote repository.") + else: + git_repo.run_cmd(["push", REMOTE_NAME, to_branch]) + logger.info("Merge successful") + + +def create_pull_request(git_repo: Git) -> None: + logger.info("Creating Pull Request") + log_output = git_repo.run_cmd(["log", "HEAD", "--max-count=1", "--pretty=format:%s"]) + pr_title = f"Automerge conflict: {log_output}" + subprocess.run(["gh", "pr", "create", "--fill", "--title", pr_title], check=True) + + +def process_conflict(git_repo: Git, commit_hash: str, project_name: str, to_branch: str, dry_run: bool) -> None: + logger.info(f"Processing conflict for {commit_hash}") + git_repo.run_cmd(["switch", "--force-create", AUTOMERGE_BRANCH, commit_hash]) + if dry_run: + logger.info("Dry run, skipping push and creation of PR.") + return + git_repo.run_cmd(["push", REMOTE_NAME, AUTOMERGE_BRANCH]) + logger.info("Publishing Pull Request for conflict") + create_pull_request(git_repo, project_name, to_branch) + + +def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> None: + logger.info( + f"Calculating list of commits to be merged from {from_branch} to {to_branch}" + ) + merge_base_output = git_repo.run_cmd(["merge-base", from_branch, to_branch]) + merge_base_commit = merge_base_output.strip() + log_output = git_repo.run_cmd(["log", f"{merge_base_commit}..{from_branch}", "--pretty=format:%H"]) + commit_list = log_output.strip() + if not commit_list: + logger.info(f"No commits to be merged") + return [] + commit_list = commit_list.split("\n") + commit_list.reverse() + logger.info(f"Found {len(commit_list)} commits to be merged") + return commit_list + + +def ensure_branch_exists(git_repo: Git, branch_name: str) -> None: + try: + git_repo.run_cmd(["rev-parse", "--verify", branch_name]) + except subprocess.CalledProcessError: + git_repo.run_cmd(["remote", "set-branches", "--add", REMOTE_NAME, branch_name]) + + +def fetch_branch(git_repo: Git, branch_name: str) -> None: + logger.info(f"Fetching '{branch_name}' branch from remote.") + ensure_branch_exists(git_repo, branch_name) + git_repo.run_cmd(["fetch", REMOTE_NAME, f"{branch_name}:{branch_name}"]) + + +def get_prs_for_label(project_name: str, label: str) -> dict: + logger.info(f"Fetching list of open PRs for label '{label}'.") + gh_process = subprocess.run(["gh", "pr", "list", "--label", label, "--repo", project_name, "--json", "id"], check=True, capture_output=True, text=True) + return json.loads(gh_process.stdout) + + +def main(): + arg_parser = argparse.ArgumentParser( + prog="automerge", + description="A script that automatically merges individual commits from one branch into another.", + ) + arg_parser.add_argument( + "--project-name", + required=True, + metavar="OWNER/REPO", + help="The name of the project in GitHub.", + ) + arg_parser.add_argument( + "--from-branch", + required=True, + metavar="BRANCH_NAME", + help="The branch where the incoming commits are found.", + ) + arg_parser.add_argument( + "--to-branch", + required=True, + metavar="BRANCH_NAME", + help="The target branch for merging incoming commits", + ) + arg_parser.add_argument( + "--repo-path", + metavar="PATH", + default=Path.cwd(), + help="The path to the existing local checkout of the repository (default: working directory)", + ) + arg_parser.add_argument( + "--dry-run", + action="store_true", + help="Process changes locally, but don't merge them into the remote repository and don't create PRs", + ) + + args = arg_parser.parse_args() + + try: + pending_automerge_prs = get_prs_for_label( + args.project_name, MERGE_CONFLICT_LABEL + ) + if pending_automerge_prs: + logger.error("There are pending automerge PRs. Cannot continue.") + exit(1) + logger.info("No pending merge conflicts. Proceeding with automerge.") + + git_repo = Git(args.repo_path) + + fetch_branch(git_repo, args.from_branch) + fetch_branch(git_repo, args.to_branch) + + merge_commits = get_merge_commit_list( + git_repo, args.from_branch, args.to_branch + ) + for commit_hash in merge_commits: + merge_commit(git_repo, args.to_branch, commit_hash, args.dry_run) + except MergeConflictError as conflict: + process_conflict( + git_repo, + conflict.commit_hash, + args.project_name, + args.to_branch, + args.dry_run, + ) + except subprocess.CalledProcessError as error: + logger.error( + f'Failed to run command: "{' '.join(str(error.cmd))}"\nstdout:\n{error.stdout}\nstderr:\n{error.stderr}' + ) + exit(1) + + +if __name__ == "__main__": + main() From c4b1c25c8a65d19afa9ea3b35a8ae14ed8cf6b27 Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Thu, 19 Dec 2024 16:04:06 +0000 Subject: [PATCH 2/8] Allow automerge to ignore changes to specific paths --- arm-software/ci/.automerge_ignore | 5 +++++ arm-software/ci/automerge.py | 20 ++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 arm-software/ci/.automerge_ignore diff --git a/arm-software/ci/.automerge_ignore b/arm-software/ci/.automerge_ignore new file mode 100644 index 000000000000..c13bb72d3dd4 --- /dev/null +++ b/arm-software/ci/.automerge_ignore @@ -0,0 +1,5 @@ +.github +arm-software +CONTRIBUTING.md +README.md +LICENSE.TXT \ No newline at end of file diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py index 4dc2cb164ca2..b150f1cb92b2 100755 --- a/arm-software/ci/automerge.py +++ b/arm-software/ci/automerge.py @@ -18,6 +18,7 @@ MERGE_CONFLICT_LABEL = "automerge_conflict" AUTOMERGE_BRANCH = "automerge" REMOTE_NAME = "origin" +MERGE_IGNORE_PATHSPEC_FILE = Path(__file__).parent / ".automerge_ignore" class MergeConflictError(Exception): @@ -37,12 +38,18 @@ class Git(): def __init__(self, repo_path: Path) -> None: self.repo_path = repo_path - def run_cmd(self, args: list[str]) -> str: + def run_cmd(self, args: list[str], check: bool = True) -> str: git_cmd = ["git", "-C", str(self.repo_path)] + args - git_process = subprocess.run(git_cmd, check=True, capture_output=True, text=True) + git_process = subprocess.run(git_cmd, check=check, capture_output=True, text=True) return git_process.stdout +def has_unresolved_conflicts(git_repo: Git) -> bool: + diff_output = git_repo.run_cmd(["diff", "--name-only", "--diff-filter=U"]) + diff_output = diff_output.strip() + return bool(diff_output) + + def prefix_current_commit_message(git_repo: Git) -> None: log_output = git_repo.run_cmd(["log", "HEAD", "--max-count=1", "--pretty=format:%B"]) commit_msg = f"Automerge: {log_output}" @@ -52,13 +59,14 @@ def prefix_current_commit_message(git_repo: Git) -> None: def merge_commit(git_repo: Git, to_branch: str, commit_hash: str, dry_run: bool) -> None: logger.info(f"Merging commit {commit_hash} into {to_branch}") git_repo.run_cmd(["switch", to_branch]) - try: - git_repo.run_cmd(["merge", commit_hash, "--no-edit"]) - except subprocess.CalledProcessError: + git_repo.run_cmd(["merge", commit_hash, "--no-commit", "--no-ff"], check=False) + # Ensure all paths that should be ignored stay unchanged + git_repo.run_cmd(["restore", "--ours", "--staged", "--worktree", f"--pathspec-from-file={MERGE_IGNORE_PATHSPEC_FILE}"]) + if has_unresolved_conflicts(git_repo): logger.info("Merge failed") git_repo.run_cmf(["merge", "--abort"]) raise MergeConflictError(commit_hash) - git_repo.run_cmd(["commit", "--amend", "--reuse-message", commit_hash]) + git_repo.run_cmd(["commit", "--reuse-message", commit_hash]) prefix_current_commit_message(git_repo) if dry_run: logger.info("Dry run. Skipping push into remote repository.") From 8bc27da07fca9a47aa7d57719e4c5584e9466630 Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Fri, 20 Dec 2024 09:49:22 +0000 Subject: [PATCH 3/8] fixup! Allow automerge to ignore changes to specific paths --- arm-software/ci/automerge.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py index b150f1cb92b2..eae1f2a885de 100755 --- a/arm-software/ci/automerge.py +++ b/arm-software/ci/automerge.py @@ -9,6 +9,7 @@ import json import logging import subprocess +import sys from pathlib import Path logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") @@ -75,14 +76,14 @@ def merge_commit(git_repo: Git, to_branch: str, commit_hash: str, dry_run: bool) logger.info("Merge successful") -def create_pull_request(git_repo: Git) -> None: +def create_pull_request(git_repo: Git, to_branch: str) -> None: logger.info("Creating Pull Request") log_output = git_repo.run_cmd(["log", "HEAD", "--max-count=1", "--pretty=format:%s"]) pr_title = f"Automerge conflict: {log_output}" - subprocess.run(["gh", "pr", "create", "--fill", "--title", pr_title], check=True) + subprocess.run(["gh", "pr", "create", "--head", AUTOMERGE_BRANCH, "--base", to_branch, "--fill", "--title", pr_title], check=True) -def process_conflict(git_repo: Git, commit_hash: str, project_name: str, to_branch: str, dry_run: bool) -> None: +def process_conflict(git_repo: Git, commit_hash: str, to_branch: str, dry_run: bool) -> None: logger.info(f"Processing conflict for {commit_hash}") git_repo.run_cmd(["switch", "--force-create", AUTOMERGE_BRANCH, commit_hash]) if dry_run: @@ -90,7 +91,7 @@ def process_conflict(git_repo: Git, commit_hash: str, project_name: str, to_bran return git_repo.run_cmd(["push", REMOTE_NAME, AUTOMERGE_BRANCH]) logger.info("Publishing Pull Request for conflict") - create_pull_request(git_repo, project_name, to_branch) + create_pull_request(git_repo, to_branch) def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> None: @@ -102,7 +103,7 @@ def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> No log_output = git_repo.run_cmd(["log", f"{merge_base_commit}..{from_branch}", "--pretty=format:%H"]) commit_list = log_output.strip() if not commit_list: - logger.info(f"No commits to be merged") + logger.info("No commits to be merged") return [] commit_list = commit_list.split("\n") commit_list.reverse() @@ -172,7 +173,7 @@ def main(): ) if pending_automerge_prs: logger.error("There are pending automerge PRs. Cannot continue.") - exit(1) + sys.exit(1) logger.info("No pending merge conflicts. Proceeding with automerge.") git_repo = Git(args.repo_path) @@ -189,7 +190,6 @@ def main(): process_conflict( git_repo, conflict.commit_hash, - args.project_name, args.to_branch, args.dry_run, ) @@ -197,7 +197,7 @@ def main(): logger.error( f'Failed to run command: "{' '.join(str(error.cmd))}"\nstdout:\n{error.stdout}\nstderr:\n{error.stderr}' ) - exit(1) + sys.exit(1) if __name__ == "__main__": From 6faabaa212fcebd32360e3fda0e895114136d850 Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Fri, 20 Dec 2024 10:31:35 +0000 Subject: [PATCH 4/8] Formatting automerge.py script --- arm-software/ci/automerge.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py index eae1f2a885de..4d2b9c1b6b46 100755 --- a/arm-software/ci/automerge.py +++ b/arm-software/ci/automerge.py @@ -26,16 +26,18 @@ class MergeConflictError(Exception): """ An exception representing a failed merge from upstream due to a conflict. """ + def __init__(self, commit_hash: str) -> None: super().__init__() self.commit_hash = commit_hash -class Git(): +class Git: """ A helper class for running Git commands on a repository that lives in a specific path. """ + def __init__(self, repo_path: Path) -> None: self.repo_path = repo_path @@ -62,7 +64,9 @@ def merge_commit(git_repo: Git, to_branch: str, commit_hash: str, dry_run: bool) git_repo.run_cmd(["switch", to_branch]) git_repo.run_cmd(["merge", commit_hash, "--no-commit", "--no-ff"], check=False) # Ensure all paths that should be ignored stay unchanged - git_repo.run_cmd(["restore", "--ours", "--staged", "--worktree", f"--pathspec-from-file={MERGE_IGNORE_PATHSPEC_FILE}"]) + git_repo.run_cmd( + ["restore", "--ours", "--staged", "--worktree", f"--pathspec-from-file={MERGE_IGNORE_PATHSPEC_FILE}"] + ) if has_unresolved_conflicts(git_repo): logger.info("Merge failed") git_repo.run_cmf(["merge", "--abort"]) @@ -80,7 +84,10 @@ def create_pull_request(git_repo: Git, to_branch: str) -> None: logger.info("Creating Pull Request") log_output = git_repo.run_cmd(["log", "HEAD", "--max-count=1", "--pretty=format:%s"]) pr_title = f"Automerge conflict: {log_output}" - subprocess.run(["gh", "pr", "create", "--head", AUTOMERGE_BRANCH, "--base", to_branch, "--fill", "--title", pr_title], check=True) + subprocess.run( + ["gh", "pr", "create", "--head", AUTOMERGE_BRANCH, "--base", to_branch, "--fill", "--title", pr_title], + check=True, + ) def process_conflict(git_repo: Git, commit_hash: str, to_branch: str, dry_run: bool) -> None: @@ -95,9 +102,7 @@ def process_conflict(git_repo: Git, commit_hash: str, to_branch: str, dry_run: b def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> None: - logger.info( - f"Calculating list of commits to be merged from {from_branch} to {to_branch}" - ) + logger.info(f"Calculating list of commits to be merged from {from_branch} to {to_branch}") merge_base_output = git_repo.run_cmd(["merge-base", from_branch, to_branch]) merge_base_commit = merge_base_output.strip() log_output = git_repo.run_cmd(["log", f"{merge_base_commit}..{from_branch}", "--pretty=format:%H"]) @@ -126,7 +131,12 @@ def fetch_branch(git_repo: Git, branch_name: str) -> None: def get_prs_for_label(project_name: str, label: str) -> dict: logger.info(f"Fetching list of open PRs for label '{label}'.") - gh_process = subprocess.run(["gh", "pr", "list", "--label", label, "--repo", project_name, "--json", "id"], check=True, capture_output=True, text=True) + gh_process = subprocess.run( + ["gh", "pr", "list", "--label", label, "--repo", project_name, "--json", "id"], + check=True, + capture_output=True, + text=True, + ) return json.loads(gh_process.stdout) @@ -168,9 +178,7 @@ def main(): args = arg_parser.parse_args() try: - pending_automerge_prs = get_prs_for_label( - args.project_name, MERGE_CONFLICT_LABEL - ) + pending_automerge_prs = get_prs_for_label(args.project_name, MERGE_CONFLICT_LABEL) if pending_automerge_prs: logger.error("There are pending automerge PRs. Cannot continue.") sys.exit(1) @@ -181,9 +189,7 @@ def main(): fetch_branch(git_repo, args.from_branch) fetch_branch(git_repo, args.to_branch) - merge_commits = get_merge_commit_list( - git_repo, args.from_branch, args.to_branch - ) + merge_commits = get_merge_commit_list(git_repo, args.from_branch, args.to_branch) for commit_hash in merge_commits: merge_commit(git_repo, args.to_branch, commit_hash, args.dry_run) except MergeConflictError as conflict: From d2dd4534fbd26db4d8a647f6c7b878fee6a03f40 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 20 Dec 2024 13:11:00 +0000 Subject: [PATCH 5/8] Update arm-software/ci/automerge.py --- arm-software/ci/automerge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py index 4d2b9c1b6b46..4c1558907b96 100755 --- a/arm-software/ci/automerge.py +++ b/arm-software/ci/automerge.py @@ -69,7 +69,7 @@ def merge_commit(git_repo: Git, to_branch: str, commit_hash: str, dry_run: bool) ) if has_unresolved_conflicts(git_repo): logger.info("Merge failed") - git_repo.run_cmf(["merge", "--abort"]) + git_repo.run_cmd(["merge", "--abort"]) raise MergeConflictError(commit_hash) git_repo.run_cmd(["commit", "--reuse-message", commit_hash]) prefix_current_commit_message(git_repo) From 54c8b60eb4d58ab46dd75293bfe61cb4dae93857 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 20 Dec 2024 13:11:14 +0000 Subject: [PATCH 6/8] Update arm-software/ci/automerge.py --- arm-software/ci/automerge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py index 4c1558907b96..24cda3d65ff3 100755 --- a/arm-software/ci/automerge.py +++ b/arm-software/ci/automerge.py @@ -101,7 +101,7 @@ def process_conflict(git_repo: Git, commit_hash: str, to_branch: str, dry_run: b create_pull_request(git_repo, to_branch) -def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> None: +def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> list[str]: logger.info(f"Calculating list of commits to be merged from {from_branch} to {to_branch}") merge_base_output = git_repo.run_cmd(["merge-base", from_branch, to_branch]) merge_base_commit = merge_base_output.strip() From 5d039d03f8785d908012a946c9ed952f809b3ef2 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 20 Dec 2024 13:11:37 +0000 Subject: [PATCH 7/8] Update arm-software/ci/automerge.py --- arm-software/ci/automerge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py index 24cda3d65ff3..32ff8ca5b331 100755 --- a/arm-software/ci/automerge.py +++ b/arm-software/ci/automerge.py @@ -178,8 +178,7 @@ def main(): args = arg_parser.parse_args() try: - pending_automerge_prs = get_prs_for_label(args.project_name, MERGE_CONFLICT_LABEL) - if pending_automerge_prs: + if get_prs_for_label(args.project_name, MERGE_CONFLICT_LABEL): logger.error("There are pending automerge PRs. Cannot continue.") sys.exit(1) logger.info("No pending merge conflicts. Proceeding with automerge.") From 100e59aa81d14cf354e81f641b2283d940c1a6ee Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Fri, 20 Dec 2024 14:20:35 +0000 Subject: [PATCH 8/8] Minor adjustments to automerge script --- arm-software/ci/automerge.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/arm-software/ci/automerge.py b/arm-software/ci/automerge.py index 32ff8ca5b331..4465f3e34575 100755 --- a/arm-software/ci/automerge.py +++ b/arm-software/ci/automerge.py @@ -60,7 +60,7 @@ def prefix_current_commit_message(git_repo: Git) -> None: def merge_commit(git_repo: Git, to_branch: str, commit_hash: str, dry_run: bool) -> None: - logger.info(f"Merging commit {commit_hash} into {to_branch}") + logger.info("Merging commit %s into %s", commit_hash, to_branch) git_repo.run_cmd(["switch", to_branch]) git_repo.run_cmd(["merge", commit_hash, "--no-commit", "--no-ff"], check=False) # Ensure all paths that should be ignored stay unchanged @@ -91,7 +91,7 @@ def create_pull_request(git_repo: Git, to_branch: str) -> None: def process_conflict(git_repo: Git, commit_hash: str, to_branch: str, dry_run: bool) -> None: - logger.info(f"Processing conflict for {commit_hash}") + logger.info("Processing conflict for %s", commit_hash) git_repo.run_cmd(["switch", "--force-create", AUTOMERGE_BRANCH, commit_hash]) if dry_run: logger.info("Dry run, skipping push and creation of PR.") @@ -102,7 +102,7 @@ def process_conflict(git_repo: Git, commit_hash: str, to_branch: str, dry_run: b def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> list[str]: - logger.info(f"Calculating list of commits to be merged from {from_branch} to {to_branch}") + logger.info("Calculating list of commits to be merged from %s to %s", from_branch, to_branch) merge_base_output = git_repo.run_cmd(["merge-base", from_branch, to_branch]) merge_base_commit = merge_base_output.strip() log_output = git_repo.run_cmd(["log", f"{merge_base_commit}..{from_branch}", "--pretty=format:%H"]) @@ -112,7 +112,7 @@ def get_merge_commit_list(git_repo: Git, from_branch: str, to_branch: str) -> li return [] commit_list = commit_list.split("\n") commit_list.reverse() - logger.info(f"Found {len(commit_list)} commits to be merged") + logger.info("Found %d commits to be merged", len(commit_list)) return commit_list @@ -124,20 +124,20 @@ def ensure_branch_exists(git_repo: Git, branch_name: str) -> None: def fetch_branch(git_repo: Git, branch_name: str) -> None: - logger.info(f"Fetching '{branch_name}' branch from remote.") + logger.info("Fetching '%s' branch from remote.", branch_name) ensure_branch_exists(git_repo, branch_name) git_repo.run_cmd(["fetch", REMOTE_NAME, f"{branch_name}:{branch_name}"]) -def get_prs_for_label(project_name: str, label: str) -> dict: - logger.info(f"Fetching list of open PRs for label '{label}'.") +def pr_exist_for_label(project_name: str, label: str) -> bool: + logger.info("Fetching list of open PRs for label '%s'.", label) gh_process = subprocess.run( ["gh", "pr", "list", "--label", label, "--repo", project_name, "--json", "id"], check=True, capture_output=True, text=True, ) - return json.loads(gh_process.stdout) + return len(json.loads(gh_process.stdout)) > 0 def main(): @@ -178,7 +178,7 @@ def main(): args = arg_parser.parse_args() try: - if get_prs_for_label(args.project_name, MERGE_CONFLICT_LABEL): + if pr_exist_for_label(args.project_name, MERGE_CONFLICT_LABEL): logger.error("There are pending automerge PRs. Cannot continue.") sys.exit(1) logger.info("No pending merge conflicts. Proceeding with automerge.") @@ -200,7 +200,10 @@ def main(): ) except subprocess.CalledProcessError as error: logger.error( - f'Failed to run command: "{' '.join(str(error.cmd))}"\nstdout:\n{error.stdout}\nstderr:\n{error.stderr}' + 'Failed to run command: "%s"\nstdout:\n%s\nstderr:\n%s', + " ".join(str(error.cmd)), + error.stdout, + error.stderr, ) sys.exit(1)