From 4ad0f4713e81dedca935b9a8a831e14b8d383d5e Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 10 Nov 2025 10:10:23 +0100 Subject: [PATCH 1/7] ciq-cherry-pick.py: Cherry pick commit only if the Fixes: references are committed If the commit that needs to be cherry picked has "Fixes:" references in the commit body, there is now a check in place that verifies if those commits are present in the current branch. At the moment, the script returns an Exception because the developer must check why the commit has to be cherry picked for a bug fix or cve fix if the actual commit that introduced the bug/cve was not commited. If the commit does not reference any Fixes:, an warning is shown to make the developer aware that they have to double check if it makes sense to cherry pick this commit. The script continues as this can be reviewed after. This is common in the linux kernel community. Not all fixes have a Fixes: reference. Checking if a commit is part of the branch has now improved. It checks if either the commit was backported by our team, or if the commit came from upstream. Note: The implementation reuses some of the logic in the check_kernel_commits.py. Those have been moved to ciq_helper.py. This commit address the small refactor in check_kernel_commits.py as well. Signed-off-by: Roxana Nicolescu --- check_kernel_commits.py | 96 ++++++++++++++++++----------------------- ciq-cherry-pick.py | 35 +++++++++++++-- ciq_helpers.py | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 56 deletions(-) diff --git a/check_kernel_commits.py b/check_kernel_commits.py index 4cd8198..22bf12a 100644 --- a/check_kernel_commits.py +++ b/check_kernel_commits.py @@ -8,19 +8,19 @@ import textwrap from typing import Optional - -def run_git(repo, args): - """Run a git command in the given repository and return its output as a string.""" - result = subprocess.run(["git", "-C", repo] + args, text=True, capture_output=True, check=False) - if result.returncode != 0: - raise RuntimeError(f"Git command failed: {' '.join(args)}\n{result.stderr}") - return result.stdout +from ciq_helpers import ( + CIQ_commit_exists_in_branch, + CIQ_extract_fixes_references_from_commit_body_lines, + CIQ_get_commit_body, + CIQ_hash_exists_in_ref, + CIQ_run_git, +) def ref_exists(repo, ref): """Return True if the given ref exists in the repository, False otherwise.""" try: - run_git(repo, ["rev-parse", "--verify", "--quiet", ref]) + CIQ_run_git(repo, ["rev-parse", "--verify", "--quiet", ref]) return True except RuntimeError: return False @@ -28,18 +28,13 @@ def ref_exists(repo, ref): def get_pr_commits(repo, pr_branch, base_branch): """Get a list of commit SHAs that are in the PR branch but not in the base branch.""" - output = run_git(repo, ["rev-list", f"{base_branch}..{pr_branch}"]) + output = CIQ_run_git(repo, ["rev-list", f"{base_branch}..{pr_branch}"]) return output.strip().splitlines() -def get_commit_message(repo, sha): - """Get the commit message for a given commit SHA.""" - return run_git(repo, ["log", "-n", "1", "--format=%B", sha]) - - def get_short_hash_and_subject(repo, sha): """Get the abbreviated commit hash and subject for a given commit SHA.""" - output = run_git(repo, ["log", "-n", "1", "--format=%h%x00%s", sha]).strip() + output = CIQ_run_git(repo, ["log", "-n", "1", "--format=%h%x00%s", sha]).strip() short_hash, subject = output.split("\x00", 1) return short_hash, subject @@ -48,61 +43,56 @@ def hash_exists_in_mainline(repo, upstream_ref, hash_): """ Return True if hash_ is reachable from upstream_ref (i.e., is an ancestor of it). """ - try: - run_git(repo, ["merge-base", "--is-ancestor", hash_, upstream_ref]) - return True - except RuntimeError: - return False + + return CIQ_hash_exists_in_ref(repo, upstream_ref, hash_) def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_): """ - Return unique commits in upstream_ref that have Fixes: in their message, case-insensitive. + Return unique commits in upstream_ref that have Fixes: in their message, case-insensitive, + if they have not been committed in the pr_branch. Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length. Returns a list of tuples: (full_hash, display_string) """ results = [] + + # Prepare hash prefixes from 12 down to 6 + hash_prefixes = [hash_[:index] for index in range(12, 5, -1)] + # Get all commits with 'Fixes:' in the message - output = run_git(repo, ["log", upstream_ref, "--grep", "Fixes:", "-i", "--format=%H %h %s (%an)%x0a%B%x00"]).strip() + output = CIQ_run_git( + repo, + [ + "log", + upstream_ref, + "--grep", + "Fixes:", + "-i", + "--format=%H %h %s (%an)%x0a%B%x00", + ], + ).strip() if not output: return [] + # Each commit is separated by a NUL character and a newline commits = output.split("\x00\x0a") - # Prepare hash prefixes from 12 down to 6 - hash_prefixes = [hash_[:index] for index in range(12, 5, -1)] for commit in commits: if not commit.strip(): continue - # The first line is the summary, the rest is the body + lines = commit.splitlines() - if not lines: - continue + # The first line is the summary, the rest is the body header = lines[0] - full_hash = header.split()[0] - # Search for Fixes: lines in the commit message - for line in lines[1:]: - m = re.match(r"^\s*Fixes:\s*([0-9a-fA-F]{6,40})", line, re.IGNORECASE) - if m: - for prefix in hash_prefixes: - if m.group(1).lower().startswith(prefix.lower()): - if not commit_exists_in_branch(repo, pr_branch, full_hash): - results.append((full_hash, " ".join(header.split()[1:]))) - break - else: - continue - return results + full_hash, display_string = (lambda h: (h[0], " ".join(h[1:])))(header.split()) + fixes = CIQ_extract_fixes_references_from_commit_body_lines(lines=lines[1:]) + for fix in fixes: + for prefix in hash_prefixes: + if fix.lower().startswith(prefix.lower()): + if not CIQ_commit_exists_in_branch(repo, pr_branch, full_hash): + results.append((full_hash, display_string)) + break - -def commit_exists_in_branch(repo, pr_branch, upstream_hash_): - """ - Return True if upstream_hash_ has been backported and it exists in the - pr branch - """ - output = run_git(repo, ["log", pr_branch, "--grep", "commit " + upstream_hash_]) - if not output: - return False - - return True + return results def wrap_paragraph(text, width=80, initial_indent="", subsequent_indent=""): @@ -176,7 +166,7 @@ def main(): if os.path.exists(vulns_repo): # Repository exists, update it with git pull try: - run_git(vulns_repo, ["pull"]) + CIQ_run_git(vulns_repo, ["pull"]) except RuntimeError as e: print(f"WARNING: Failed to update vulns repo: {e}") print("Continuing with existing repository...") @@ -222,7 +212,7 @@ def main(): for sha in reversed(pr_commits): # oldest first short_hash, subject = get_short_hash_and_subject(args.repo, sha) pr_commit_desc = f"{short_hash} ({subject})" - msg = get_commit_message(args.repo, sha) + msg = CIQ_get_commit_body(args.repo, sha) upstream_hashes = re.findall(r"^commit\s+([0-9a-fA-F]{40})", msg, re.MULTILINE) for uhash in upstream_hashes: short_uhash = uhash[:12] diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index 9dded99..22515b1 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -1,15 +1,36 @@ import argparse +import logging import os import subprocess import git -from ciq_helpers import CIQ_cherry_pick_commit_standardization, CIQ_original_commit_author_to_tag_string - -# from ciq_helpers import * +from ciq_helpers import ( + CIQ_cherry_pick_commit_standardization, + CIQ_commit_exists_in_current_branch, + CIQ_fixes_references, + CIQ_original_commit_author_to_tag_string, +) MERGE_MSG = git.Repo(os.getcwd()).git_dir + "/MERGE_MSG" + +def check_fixes(sha): + """ + Checks if commit has "Fixes:" references and if so, it checks if the + commit(s) that it tries to fix are part of the current branch + """ + + fixes = CIQ_fixes_references(repo_path=os.getcwd(), sha=sha) + if len(fixes) == 0: + logging.warning("The commit you try to cherry pick has no Fixes: reference; review it carefully") + return + + for fix in fixes: + if not CIQ_commit_exists_in_current_branch(os.getcwd(), fix): + raise RuntimeError(f"The commit you want to cherry pick references a Fixes: {fix} but this is not here") + + if __name__ == "__main__": print("CIQ custom cherry picker") parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) @@ -25,6 +46,8 @@ ) args = parser.parse_args() + logging.basicConfig(level=logging.INFO) + # Expand the provided SHA1 to the full SHA1 in case it's either abbreviated or an expression git_sha_res = subprocess.run(["git", "show", "--pretty=%H", "-s", args.sha], stdout=subprocess.PIPE) if git_sha_res.returncode != 0: @@ -39,6 +62,12 @@ if args.ciq_tag is not None: tags = args.ciq_tag.split(",") + try: + check_fixes(args.sha) + except Exception as e: + print(e) + exit(1) + author = CIQ_original_commit_author_to_tag_string(repo_path=os.getcwd(), sha=args.sha) if author is None: exit(1) diff --git a/ciq_helpers.py b/ciq_helpers.py index e97f11d..c32a945 100644 --- a/ciq_helpers.py +++ b/ciq_helpers.py @@ -169,6 +169,88 @@ def CIQ_original_commit_author_to_tag_string(repo_path, sha): return "commit-author " + git_auth_res.stdout.decode("utf-8").replace('"', "").strip() +def CIQ_run_git(repo_path, args): + """ + Run a git command in the given repository and return its output as a string. + """ + result = subprocess.run(["git", "-C", repo_path] + args, text=True, capture_output=True, check=False) + if result.returncode != 0: + raise RuntimeError(f"Git command failed: {' '.join(args)}\n{result.stderr}") + + return result.stdout + + +def CIQ_get_commit_body(repo_path, sha): + return CIQ_run_git(repo_path, ["show", "-s", sha, "--format=%B"]) + + +def CIQ_extract_fixes_references_from_commit_body_lines(lines): + fixes = [] + for line in lines: + m = re.match(r"^\s*Fixes:\s*([0-9a-fA-F]{6,40})", line, re.IGNORECASE) + if not m: + continue + + fixes.append(m.group(1)) + + return fixes + + +def CIQ_fixes_references(repo_path, sha): + """ + If commit message of sha contains lines like + Fixes: , this returns a list of , otherwise an empty list + """ + + commit_body = CIQ_get_commit_body(repo_path, sha) + return CIQ_extract_fixes_references_from_commit_body_lines(lines=commit_body.splitlines()) + + +def CIQ_get_full_hash(repo, short_hash): + return CIQ_run_git(repo, ["show", "-s", "--pretty=%H", short_hash]).strip() + + +def CIQ_get_current_branch(repo): + return CIQ_run_git(repo, ["branch", "--show-current"]).strip() + + +def CIQ_hash_exists_in_ref(repo, pr_ref, hash_): + """ + Return True if hash_ is reachable from pr_ref + """ + + try: + CIQ_run_git(repo, ["merge-base", "--is-ancestor", hash_, pr_ref]) + return True + except RuntimeError: + return False + + +def CIQ_commit_exists_in_branch(repo, pr_branch, upstream_hash_): + """ + Return True if upstream_hash_ has been backported and it exists in the pr branch + """ + + # First check if the commit has been backported by CIQ + output = CIQ_run_git(repo, ["log", pr_branch, "--grep", "^commit " + upstream_hash_]) + if output: + return True + + # If it was not backported by CIQ, maybe it came from upstream as it is + return CIQ_hash_exists_in_ref(repo, pr_branch, upstream_hash_) + + +def CIQ_commit_exists_in_current_branch(repo, upstream_hash_): + """ + Return True if upstream_hash_ has been backported and it exists in the current branch + """ + + current_branch = CIQ_get_current_branch(repo) + full_upstream_hash = CIQ_get_full_hash(repo, upstream_hash_) + + return CIQ_commit_exists_in_branch(repo, current_branch, full_upstream_hash) + + def repo_init(repo): """Initialize a git repo object. From bca8c65924a917df8dd156211130c7f13eade336 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 10 Nov 2025 18:54:58 +0100 Subject: [PATCH 2/7] ciq-cherry-pick.py: Automatically cherry pick cve-bf commits It now automatically cherry picks the Fixes: dependencies. To accomodate this, CIQ_find_mainline_fixes was moved to ciq_helpers. And an extra argument for upstream-ref was introduced, the default being origin/kernel-mainline, as the dependencies are looked up there. To simplify things and keep main cleaner, separate functions were used. If one of the commits (the original and its dependencies) cannot be applied, the return code will be 1. This is useful when ciq-cherry-pick.py is called from other script. This also removed redundant prints. Signed-off-by: Roxana Nicolescu --- check_kernel_commits.py | 53 +------------- ciq-cherry-pick.py | 150 ++++++++++++++++++++++++++++------------ ciq_helpers.py | 54 +++++++++++++++ 3 files changed, 162 insertions(+), 95 deletions(-) diff --git a/check_kernel_commits.py b/check_kernel_commits.py index 22bf12a..2240dc9 100644 --- a/check_kernel_commits.py +++ b/check_kernel_commits.py @@ -9,8 +9,7 @@ from typing import Optional from ciq_helpers import ( - CIQ_commit_exists_in_branch, - CIQ_extract_fixes_references_from_commit_body_lines, + CIQ_find_fixes_in_mainline, CIQ_get_commit_body, CIQ_hash_exists_in_ref, CIQ_run_git, @@ -47,54 +46,6 @@ def hash_exists_in_mainline(repo, upstream_ref, hash_): return CIQ_hash_exists_in_ref(repo, upstream_ref, hash_) -def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_): - """ - Return unique commits in upstream_ref that have Fixes: in their message, case-insensitive, - if they have not been committed in the pr_branch. - Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length. - Returns a list of tuples: (full_hash, display_string) - """ - results = [] - - # Prepare hash prefixes from 12 down to 6 - hash_prefixes = [hash_[:index] for index in range(12, 5, -1)] - - # Get all commits with 'Fixes:' in the message - output = CIQ_run_git( - repo, - [ - "log", - upstream_ref, - "--grep", - "Fixes:", - "-i", - "--format=%H %h %s (%an)%x0a%B%x00", - ], - ).strip() - if not output: - return [] - - # Each commit is separated by a NUL character and a newline - commits = output.split("\x00\x0a") - for commit in commits: - if not commit.strip(): - continue - - lines = commit.splitlines() - # The first line is the summary, the rest is the body - header = lines[0] - full_hash, display_string = (lambda h: (h[0], " ".join(h[1:])))(header.split()) - fixes = CIQ_extract_fixes_references_from_commit_body_lines(lines=lines[1:]) - for fix in fixes: - for prefix in hash_prefixes: - if fix.lower().startswith(prefix.lower()): - if not CIQ_commit_exists_in_branch(repo, pr_branch, full_hash): - results.append((full_hash, display_string)) - break - - return results - - def wrap_paragraph(text, width=80, initial_indent="", subsequent_indent=""): """Wrap a paragraph of text to the specified width and indentation.""" wrapper = textwrap.TextWrapper( @@ -238,7 +189,7 @@ def main(): ) out_lines.append("") # blank line continue - fixes = find_fixes_in_mainline(args.repo, args.pr_branch, upstream_ref, uhash) + fixes = CIQ_find_fixes_in_mainline(args.repo, args.pr_branch, upstream_ref, uhash) if fixes: any_findings = True diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index 22515b1..b791151 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -1,6 +1,7 @@ import argparse import logging import os +import re import subprocess import git @@ -8,11 +9,15 @@ from ciq_helpers import ( CIQ_cherry_pick_commit_standardization, CIQ_commit_exists_in_current_branch, + CIQ_find_fixes_in_mainline_current_branch, CIQ_fixes_references, + CIQ_get_full_hash, CIQ_original_commit_author_to_tag_string, + CIQ_run_git, ) MERGE_MSG = git.Repo(os.getcwd()).git_dir + "/MERGE_MSG" +MERGE_MSG_BAK = f"{MERGE_MSG}.bak" def check_fixes(sha): @@ -31,6 +36,100 @@ def check_fixes(sha): raise RuntimeError(f"The commit you want to cherry pick references a Fixes: {fix} but this is not here") +def manage_commit_message(full_sha, ciq_tags, jira_ticket): + """ + It standardize the commit message by including the ciq_tags, original + author and the original commit full sha. + + Original message location: MERGE_MSG + Makes a copy of the original message in MERGE_MSG_BAK + + The new standardized commit message is written to MERGE_MSG + """ + + subprocess.run(["cp", MERGE_MSG, MERGE_MSG_BAK], check=True) + + # Make sure it's a deep copy because ciq_tags may be used for other cherry-picks + new_tags = [tag for tag in ciq_tags] + + author = CIQ_original_commit_author_to_tag_string(repo_path=os.getcwd(), sha=full_sha) + if author is None: + raise RuntimeError(f"Could not find author of commit {full_sha}") + + new_tags.append(author) + with open(MERGE_MSG, "r") as file: + original_msg = file.readlines() + + new_msg = CIQ_cherry_pick_commit_standardization(original_msg, full_sha, jira=jira_ticket, tags=ciq_tags) + + print(f"Cherry Pick New Message for {full_sha}") + print(f"\n Original Message located here: {MERGE_MSG_BAK}") + + with open(MERGE_MSG, "w") as file: + file.writelines(new_msg) + + +def cherry_pick(sha, ciq_tags, jira_ticket): + """ + Cherry picks a commit and it adds the ciq standardized format + In case of error (cherry pick conflict): + - MERGE_MSG.bak contains the original commit message + - MERGE_MSG contains the standardized commit message + - Conflict has to be solved manualy + + In case of success: + - the commit is cherry picked + - MERGE_MSG.bak is deleted + - You can still see MERGE_MSG for the original message + """ + + # Expand the provided SHA1 to the full SHA1 in case it's either abbreviated or an expression + full_sha = CIQ_get_full_hash(repo=os.getcwd(), short_hash=sha) + + check_fixes(sha=full_sha) + + # Commit message is in MERGE_MSG + git_res = subprocess.run(["git", "cherry-pick", "-nsx", full_sha]) + manage_commit_message(full_sha=full_sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket) + + if git_res.returncode != 0: + error_str = ( + f"[FAILED] git cherry-pick -nsx {full_sha}\n" + "Manually resolve conflict and include `upstream-diff` tag in commit message\n" + f"Subprocess Call: {git_res}" + ) + raise RuntimeError(error_str) + + CIQ_run_git(repo_path=os.getcwd(), args=["commit", "-F", MERGE_MSG]) + + +def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref): + """ + It checks upstream_ref for commits that have this reference: + Fixes: . If any, these will also be cherry picked with the ciq + tag = cve-bf. If the tag was cve-pre, it stays the same. + """ + fixes_in_mainline = CIQ_find_fixes_in_mainline_current_branch(os.getcwd(), upstream_ref, sha) + + # Replace cve with cve-bf + # Leave cve-pre and cve-bf as they are + bf_ciq_tags = [re.sub(r"^cve ", "cve-bf ", s) for s in ciq_tags] + for full_hash, display_str in fixes_in_mainline: + print(f"Extra cherry picking {display_str}") + full_cherry_pick(sha=full_hash, ciq_tags=bf_ciq_tags, jira_ticket=jira_ticket, upstream_ref=upstream_ref) + + +def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): + """ + It cherry picks a commit from upstream-ref along with its Fixes: references. + """ + # Cherry pick the commit + cherry_pick(sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket) + + # Cherry pick the fixed-by dependencies + cherry_pick_fixes(sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, upstream_ref=upstream_ref) + + if __name__ == "__main__": print("CIQ custom cherry picker") parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) @@ -44,59 +143,22 @@ def check_fixes(sha): " cve-pre CVE-1974-0001 - A pre-condition or dependency needed for the CVE\n" "Multiple tags are separated with a comma. ex: cve CVE-1974-0001, cve CVE-1974-0002\n", ) + parser.add_argument( + "--upstream-ref", + default="origin/kernel-mainline", + help="Reference to upstream mainline branch (default: origin/kernel-mainline)", + ) + args = parser.parse_args() logging.basicConfig(level=logging.INFO) - # Expand the provided SHA1 to the full SHA1 in case it's either abbreviated or an expression - git_sha_res = subprocess.run(["git", "show", "--pretty=%H", "-s", args.sha], stdout=subprocess.PIPE) - if git_sha_res.returncode != 0: - print(f"[FAILED] git show --pretty=%H -s {args.sha}") - print("Subprocess Call:") - print(git_sha_res) - print("") - else: - args.sha = git_sha_res.stdout.decode("utf-8").strip() - tags = [] if args.ciq_tag is not None: tags = args.ciq_tag.split(",") try: - check_fixes(args.sha) + full_cherry_pick(sha=args.sha, ciq_tags=tags, jira_ticket=args.ticket, upstream_ref=args.upstream_ref) except Exception as e: print(e) exit(1) - - author = CIQ_original_commit_author_to_tag_string(repo_path=os.getcwd(), sha=args.sha) - if author is None: - exit(1) - - git_res = subprocess.run(["git", "cherry-pick", "-nsx", args.sha]) - if git_res.returncode != 0: - print(f"[FAILED] git cherry-pick -nsx {args.sha}") - print(" Manually resolve conflict and include `upstream-diff` tag in commit message") - print("Subprocess Call:") - print(git_res) - print("") - - print(os.getcwd()) - subprocess.run(["cp", MERGE_MSG, f"{MERGE_MSG}.bak"]) - - tags.append(author) - - with open(MERGE_MSG, "r") as file: - original_msg = file.readlines() - - new_msg = CIQ_cherry_pick_commit_standardization(original_msg, args.sha, jira=args.ticket, tags=tags) - - print(f"Cherry Pick New Message for {args.sha}") - for line in new_msg: - print(line.strip("\n")) - print(f"\n Original Message located here: {MERGE_MSG}.bak") - - with open(MERGE_MSG, "w") as file: - file.writelines(new_msg) - - if git_res.returncode == 0: - subprocess.run(["git", "commit", "-F", MERGE_MSG]) diff --git a/ciq_helpers.py b/ciq_helpers.py index c32a945..fc8a815 100644 --- a/ciq_helpers.py +++ b/ciq_helpers.py @@ -251,6 +251,60 @@ def CIQ_commit_exists_in_current_branch(repo, upstream_hash_): return CIQ_commit_exists_in_branch(repo, current_branch, full_upstream_hash) +def CIQ_find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_): + """ + Return unique commits in upstream_ref that have Fixes: in their message, case-insensitive, + if they have not been committed in the pr_branch. + Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length. + Returns a list of tuples: (full_hash, display_string) + """ + results = [] + + # Prepare hash prefixes from 12 down to 6 + hash_prefixes = [hash_[:index] for index in range(12, 5, -1)] + + # Get all commits with 'Fixes:' in the message + output = CIQ_run_git( + repo, + [ + "log", + upstream_ref, + "--grep", + "Fixes:", + "-i", + "--format=%H %h %s (%an)%x0a%B%x00", + ], + ).strip() + if not output: + return [] + + # Each commit is separated by a NUL character and a newline + commits = output.split("\x00\x0a") + for commit in commits: + if not commit.strip(): + continue + + lines = commit.splitlines() + # The first line is the summary, the rest is the body + header = lines[0] + full_hash, display_string = (lambda h: (h[0], " ".join(h[1:])))(header.split()) + fixes = CIQ_extract_fixes_references_from_commit_body_lines(lines=lines[1:]) + for fix in fixes: + for prefix in hash_prefixes: + if fix.lower().startswith(prefix.lower()): + if not CIQ_commit_exists_in_branch(repo, pr_branch, full_hash): + results.append((full_hash, display_string)) + break + + return results + + +def CIQ_find_fixes_in_mainline_current_branch(repo, upstream_ref, hash_): + current_branch = CIQ_get_current_branch(repo) + + return CIQ_find_fixes_in_mainline(repo, current_branch, upstream_ref, hash_) + + def repo_init(repo): """Initialize a git repo object. From f4a2b421f2d851f006f306a40c0b30531fcfd7f2 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Tue, 18 Nov 2025 10:57:32 +0100 Subject: [PATCH 3/7] ciq-cherry-pick.py: Add upstream-diff in the commit message in case of conflict If the conflict is solved manually, then developer use ``` git commit ``` which will use the MERGE_MSG file for the commit message by default. Equivalent of ``` git commit -F MERGE_MSG ``` Therefore it makes sense to include "upstream-diff |" in the MERGE_MSG. In case the conflict is solved by cherry picking other commits, ciq-cherry-pick.py will be called again and MERGE_MSG will be rewritten, including the "upstream-diff" part. Signed-off-by: Roxana Nicolescu --- ciq-cherry-pick.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index b791151..faba887 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -36,7 +36,7 @@ def check_fixes(sha): raise RuntimeError(f"The commit you want to cherry pick references a Fixes: {fix} but this is not here") -def manage_commit_message(full_sha, ciq_tags, jira_ticket): +def manage_commit_message(full_sha, ciq_tags, jira_ticket, commit_successful): """ It standardize the commit message by including the ciq_tags, original author and the original commit full sha. @@ -60,7 +60,10 @@ def manage_commit_message(full_sha, ciq_tags, jira_ticket): with open(MERGE_MSG, "r") as file: original_msg = file.readlines() - new_msg = CIQ_cherry_pick_commit_standardization(original_msg, full_sha, jira=jira_ticket, tags=ciq_tags) + optional_msg = "" if commit_successful else "upstream-diff |" + new_msg = CIQ_cherry_pick_commit_standardization( + original_msg, full_sha, jira=jira_ticket, tags=new_tags, optional_msg=optional_msg + ) print(f"Cherry Pick New Message for {full_sha}") print(f"\n Original Message located here: {MERGE_MSG_BAK}") @@ -75,7 +78,7 @@ def cherry_pick(sha, ciq_tags, jira_ticket): In case of error (cherry pick conflict): - MERGE_MSG.bak contains the original commit message - MERGE_MSG contains the standardized commit message - - Conflict has to be solved manualy + - Conflict has to be solved manually In case of success: - the commit is cherry picked @@ -90,12 +93,15 @@ def cherry_pick(sha, ciq_tags, jira_ticket): # Commit message is in MERGE_MSG git_res = subprocess.run(["git", "cherry-pick", "-nsx", full_sha]) - manage_commit_message(full_sha=full_sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket) + commit_successful = git_res.returncode == 0 + manage_commit_message( + full_sha=full_sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, commit_successful=commit_successful + ) - if git_res.returncode != 0: + if not commit_successful: error_str = ( f"[FAILED] git cherry-pick -nsx {full_sha}\n" - "Manually resolve conflict and include `upstream-diff` tag in commit message\n" + "Manually resolve conflict and add explanation under `upstream-diff` tag in commit message\n" f"Subprocess Call: {git_res}" ) raise RuntimeError(error_str) From 42b21f6b32eee4fc13e410c2f07916558ee479d7 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 24 Nov 2025 15:43:13 +0100 Subject: [PATCH 4/7] ciq-cherry-pick.py: Use CIQ_run_git for cherry picking the commit Signed-off-by: Roxana Nicolescu --- ciq-cherry-pick.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index faba887..c26d487 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -92,8 +92,12 @@ def cherry_pick(sha, ciq_tags, jira_ticket): check_fixes(sha=full_sha) # Commit message is in MERGE_MSG - git_res = subprocess.run(["git", "cherry-pick", "-nsx", full_sha]) - commit_successful = git_res.returncode == 0 + commit_successful = True + try: + CIQ_run_git(repo_path=os.getcwd(), args=["cherry-pick", "-nsx", full_sha]) + except RuntimeError: + commit_successful = False + manage_commit_message( full_sha=full_sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, commit_successful=commit_successful ) @@ -102,7 +106,6 @@ def cherry_pick(sha, ciq_tags, jira_ticket): error_str = ( f"[FAILED] git cherry-pick -nsx {full_sha}\n" "Manually resolve conflict and add explanation under `upstream-diff` tag in commit message\n" - f"Subprocess Call: {git_res}" ) raise RuntimeError(error_str) From 6431a455140186b2d0f698462f73f419ae6d3cc9 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 24 Nov 2025 17:12:46 +0100 Subject: [PATCH 5/7] ciq-cherry-pick.py: Make the --sha argument mandatory Signed-off-by: Roxana Nicolescu --- ciq-cherry-pick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index c26d487..59131e9 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -142,7 +142,7 @@ def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): if __name__ == "__main__": print("CIQ custom cherry picker") parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument("--sha", help="Target SHA1 to cherry-pick") + parser.add_argument("--sha", help="Target SHA1 to cherry-pick", required=True) parser.add_argument("--ticket", help="Ticket associated to cherry-pick work, comma separated list is supported.") parser.add_argument( "--ciq-tag", From 62f096edf4f2bc92b4f6d5cdc125b054b0a8505d Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 24 Nov 2025 17:13:27 +0100 Subject: [PATCH 6/7] ciq-cherry-pick.py: Improve error handling All exceptions are propapagated to main and handled there. Also added some comments to full_cherry_pick function to explain that if one of the cherry pick fails, the previous successful ones are left intact. This can be improved in the future by making it interactive. Signed-off-by: Roxana Nicolescu --- ciq-cherry-pick.py | 39 ++++++++++++++++++++++++++++++--------- ciq_helpers.py | 4 ++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index 59131e9..a66e9d0 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -3,6 +3,7 @@ import os import re import subprocess +import traceback import git @@ -13,6 +14,7 @@ CIQ_fixes_references, CIQ_get_full_hash, CIQ_original_commit_author_to_tag_string, + CIQ_reset_HEAD, CIQ_run_git, ) @@ -57,8 +59,11 @@ def manage_commit_message(full_sha, ciq_tags, jira_ticket, commit_successful): raise RuntimeError(f"Could not find author of commit {full_sha}") new_tags.append(author) - with open(MERGE_MSG, "r") as file: - original_msg = file.readlines() + try: + with open(MERGE_MSG, "r") as file: + original_msg = file.readlines() + except IOError as e: + raise RuntimeError(f"Failed to read commit message from {MERGE_MSG}: {e}") from e optional_msg = "" if commit_successful else "upstream-diff |" new_msg = CIQ_cherry_pick_commit_standardization( @@ -68,8 +73,11 @@ def manage_commit_message(full_sha, ciq_tags, jira_ticket, commit_successful): print(f"Cherry Pick New Message for {full_sha}") print(f"\n Original Message located here: {MERGE_MSG_BAK}") - with open(MERGE_MSG, "w") as file: - file.writelines(new_msg) + try: + with open(MERGE_MSG, "w") as file: + file.writelines(new_msg) + except IOError as e: + raise RuntimeError(f"Failed to write commit message to {MERGE_MSG}: {e}") from e def cherry_pick(sha, ciq_tags, jira_ticket): @@ -79,6 +87,8 @@ def cherry_pick(sha, ciq_tags, jira_ticket): - MERGE_MSG.bak contains the original commit message - MERGE_MSG contains the standardized commit message - Conflict has to be solved manually + In case runtime errors that are not cherry pick conflicts, the cherry + pick changes are reverted. (git reset --hard HEAD) In case of success: - the commit is cherry picked @@ -87,7 +97,10 @@ def cherry_pick(sha, ciq_tags, jira_ticket): """ # Expand the provided SHA1 to the full SHA1 in case it's either abbreviated or an expression - full_sha = CIQ_get_full_hash(repo=os.getcwd(), short_hash=sha) + try: + full_sha = CIQ_get_full_hash(repo=os.getcwd(), short_hash=sha) + except RuntimeError as e: + raise RuntimeError(f"Invalid commit SHA {sha}: {e}") from e check_fixes(sha=full_sha) @@ -98,9 +111,13 @@ def cherry_pick(sha, ciq_tags, jira_ticket): except RuntimeError: commit_successful = False - manage_commit_message( - full_sha=full_sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, commit_successful=commit_successful - ) + try: + manage_commit_message( + full_sha=full_sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, commit_successful=commit_successful + ) + except RuntimeError as e: + CIQ_reset_HEAD(repo=os.getcwd()) + raise RuntimeError(f"Could not create proper commit message: {e}") from e if not commit_successful: error_str = ( @@ -131,6 +148,9 @@ def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref): def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): """ It cherry picks a commit from upstream-ref along with its Fixes: references. + If cherry-pick or cherry_pick_fixes fail, the exception is propagated + If one of the cherry picks fails, an exception is returned and the previous + successful cherry picks are left as they are. """ # Cherry pick the commit cherry_pick(sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket) @@ -169,5 +189,6 @@ def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): try: full_cherry_pick(sha=args.sha, ciq_tags=tags, jira_ticket=args.ticket, upstream_ref=args.upstream_ref) except Exception as e: - print(e) + print(f"full_cherry_pick failed {e}") + traceback.print_exc() exit(1) diff --git a/ciq_helpers.py b/ciq_helpers.py index fc8a815..4d663e9 100644 --- a/ciq_helpers.py +++ b/ciq_helpers.py @@ -305,6 +305,10 @@ def CIQ_find_fixes_in_mainline_current_branch(repo, upstream_ref, hash_): return CIQ_find_fixes_in_mainline(repo, current_branch, upstream_ref, hash_) +def CIQ_reset_HEAD(repo): + return CIQ_run_git(repo=repo, args=["reset", "--hard", "HEAD"]) + + def repo_init(repo): """Initialize a git repo object. From 7a8f7f8bca7c818b30bd34677d03d2493584c8fa Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 24 Nov 2025 17:49:28 +0100 Subject: [PATCH 7/7] ciq-cherry-pick.py: Add option to ignore if one of the 'Fixes' commits are not in the tree This can be useful in cases where there are multiple commits this one is trying to fix, and only one if of interest for us. Signed-off-by: Roxana Nicolescu --- ciq-cherry-pick.py | 48 ++++++++++++++++++++++++++++++++++++---------- ciq_helpers.py | 8 ++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index a66e9d0..03b582a 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -14,6 +14,7 @@ CIQ_fixes_references, CIQ_get_full_hash, CIQ_original_commit_author_to_tag_string, + CIQ_raise_or_warn, CIQ_reset_HEAD, CIQ_run_git, ) @@ -22,7 +23,7 @@ MERGE_MSG_BAK = f"{MERGE_MSG}.bak" -def check_fixes(sha): +def check_fixes(sha, ignore_fixes_check): """ Checks if commit has "Fixes:" references and if so, it checks if the commit(s) that it tries to fix are part of the current branch @@ -33,9 +34,13 @@ def check_fixes(sha): logging.warning("The commit you try to cherry pick has no Fixes: reference; review it carefully") return + not_present_fixes = [] for fix in fixes: if not CIQ_commit_exists_in_current_branch(os.getcwd(), fix): - raise RuntimeError(f"The commit you want to cherry pick references a Fixes: {fix} but this is not here") + not_present_fixes.append(fix) + + err = f"The commit you want to cherry pick has the following Fixes: references that are not part of the tree {not_present_fixes}" + CIQ_raise_or_warn(cond=not not_present_fixes, error_msg=err, warn=ignore_fixes_check) def manage_commit_message(full_sha, ciq_tags, jira_ticket, commit_successful): @@ -80,7 +85,7 @@ def manage_commit_message(full_sha, ciq_tags, jira_ticket, commit_successful): raise RuntimeError(f"Failed to write commit message to {MERGE_MSG}: {e}") from e -def cherry_pick(sha, ciq_tags, jira_ticket): +def cherry_pick(sha, ciq_tags, jira_ticket, ignore_fixes_check): """ Cherry picks a commit and it adds the ciq standardized format In case of error (cherry pick conflict): @@ -102,7 +107,7 @@ def cherry_pick(sha, ciq_tags, jira_ticket): except RuntimeError as e: raise RuntimeError(f"Invalid commit SHA {sha}: {e}") from e - check_fixes(sha=full_sha) + check_fixes(sha=full_sha, ignore_fixes_check=ignore_fixes_check) # Commit message is in MERGE_MSG commit_successful = True @@ -129,7 +134,7 @@ def cherry_pick(sha, ciq_tags, jira_ticket): CIQ_run_git(repo_path=os.getcwd(), args=["commit", "-F", MERGE_MSG]) -def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref): +def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref, ignore_fixes_check): """ It checks upstream_ref for commits that have this reference: Fixes: . If any, these will also be cherry picked with the ciq @@ -142,10 +147,16 @@ def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref): bf_ciq_tags = [re.sub(r"^cve ", "cve-bf ", s) for s in ciq_tags] for full_hash, display_str in fixes_in_mainline: print(f"Extra cherry picking {display_str}") - full_cherry_pick(sha=full_hash, ciq_tags=bf_ciq_tags, jira_ticket=jira_ticket, upstream_ref=upstream_ref) + full_cherry_pick( + sha=full_hash, + ciq_tags=bf_ciq_tags, + jira_ticket=jira_ticket, + upstream_ref=upstream_ref, + ignore_fixes_check=ignore_fixes_check, + ) -def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): +def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref, ignore_fixes_check): """ It cherry picks a commit from upstream-ref along with its Fixes: references. If cherry-pick or cherry_pick_fixes fail, the exception is propagated @@ -153,10 +164,16 @@ def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): successful cherry picks are left as they are. """ # Cherry pick the commit - cherry_pick(sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket) + cherry_pick(sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, ignore_fixes_check=ignore_fixes_check) # Cherry pick the fixed-by dependencies - cherry_pick_fixes(sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, upstream_ref=upstream_ref) + cherry_pick_fixes( + sha=sha, + ciq_tags=ciq_tags, + jira_ticket=jira_ticket, + upstream_ref=upstream_ref, + ignore_fixes_check=ignore_fixes_check, + ) if __name__ == "__main__": @@ -177,6 +194,11 @@ def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): default="origin/kernel-mainline", help="Reference to upstream mainline branch (default: origin/kernel-mainline)", ) + parser.add_argument( + "--ignore-fixes-check", + action="store_true", + help="If the commit(s) this commit is trying to fix are not part of the tree, do not exit", + ) args = parser.parse_args() @@ -187,7 +209,13 @@ def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): tags = args.ciq_tag.split(",") try: - full_cherry_pick(sha=args.sha, ciq_tags=tags, jira_ticket=args.ticket, upstream_ref=args.upstream_ref) + full_cherry_pick( + sha=args.sha, + ciq_tags=tags, + jira_ticket=args.ticket, + upstream_ref=args.upstream_ref, + ignore_fixes_check=args.ignore_fixes_check, + ) except Exception as e: print(f"full_cherry_pick failed {e}") traceback.print_exc() diff --git a/ciq_helpers.py b/ciq_helpers.py index 4d663e9..4b58eb6 100644 --- a/ciq_helpers.py +++ b/ciq_helpers.py @@ -3,6 +3,7 @@ # CIQ Kernel Tools function library +import logging import os import re import subprocess @@ -309,6 +310,13 @@ def CIQ_reset_HEAD(repo): return CIQ_run_git(repo=repo, args=["reset", "--hard", "HEAD"]) +def CIQ_raise_or_warn(cond, error_msg, warn): + if not warn: + raise RuntimeError(error_msg) + + logging.warning(error_msg) + + def repo_init(repo): """Initialize a git repo object.