From 6fcb5366991febcc7a630f6403bf36ddb14484ef Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 24 Nov 2025 17:12:46 +0100 Subject: [PATCH 1/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 9dded99..9fab773 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -13,7 +13,7 @@ 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 76c77d21b4f970a34366456293117f60d599fbc0 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 10 Nov 2025 10:10:23 +0100 Subject: [PATCH 2/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 | 37 ++++++++++++++-- ciq_helpers.py | 82 +++++++++++++++++++++++++++++++++++ 3 files changed, 159 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 9fab773..fb638ad 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -1,17 +1,42 @@ import argparse +import logging import os import subprocess +import sys 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") + + logging.basicConfig(level=logging.INFO) + parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) 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.") @@ -39,6 +64,12 @@ if args.ciq_tag is not None: tags = args.ciq_tag.split(",") + try: + check_fixes(args.sha) + except Exception as e: + logging.error(e) + sys.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 129ee41f95d6b558758b317fbf3367c906a8ec0d Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 10 Nov 2025 18:54:58 +0100 Subject: [PATCH 3/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 | 140 ++++++++++++++++++++++++++++++---------- ciq_helpers.py | 54 ++++++++++++++++ 3 files changed, 162 insertions(+), 85 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 fb638ad..05f1694 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 sys @@ -9,11 +10,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): @@ -32,6 +37,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): + """ + 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): + """ + Check 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 ", tag.strip()) for tag 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): + """ + 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") @@ -48,6 +147,12 @@ 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() # Expand the provided SHA1 to the full SHA1 in case it's either abbreviated or an expression @@ -65,40 +170,7 @@ def check_fixes(sha): 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: logging.error(e) sys.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 7dbdd88a89f8c9fa92dda478da49970ae8813b27 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Tue, 18 Nov 2025 10:57:32 +0100 Subject: [PATCH 4/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 05f1694..3891c76 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -37,7 +37,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): """ Standardize the commit message by including the ciq_tags, original author and the original commit full sha. @@ -61,7 +61,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}") @@ -76,7 +79,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 @@ -91,12 +94,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 e2c5c4d68ccfc934d2d6210d3a205f40ef019539 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 24 Nov 2025 15:43:13 +0100 Subject: [PATCH 5/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 3891c76..ee8661a 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -93,8 +93,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 ) @@ -103,7 +107,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 6bb9129a8b0c739ac98e9fbb288562d837421a4b 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, except for CherryPickException that is raised only if cherry pick failed due to conflicts. This way user is not overwhelmed with a stacktrace when the error is obvious. For unexpected errors, the full stacktrace is shown. 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 | 48 ++++++++++++++++++++++++++++++++++++---------- ciq_helpers.py | 4 ++++ 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index ee8661a..d8eaffc 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -4,6 +4,7 @@ import re import subprocess import sys +import traceback import git @@ -14,6 +15,7 @@ CIQ_fixes_references, CIQ_get_full_hash, CIQ_original_commit_author_to_tag_string, + CIQ_reset_HEAD, CIQ_run_git, ) @@ -21,6 +23,10 @@ MERGE_MSG_BAK = f"{MERGE_MSG}.bak" +class CherryPickException(Exception): + pass + + def check_fixes(sha): """ Checks if commit has "Fixes:" references and if so, it checks if the @@ -58,8 +64,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( @@ -69,8 +78,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): @@ -80,6 +92,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 of 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 @@ -88,7 +102,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) @@ -99,16 +116,20 @@ 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 = ( f"[FAILED] git cherry-pick -nsx {full_sha}\n" "Manually resolve conflict and add explanation under `upstream-diff` tag in commit message\n" ) - raise RuntimeError(error_str) + raise CherryPickException(error_str) CIQ_run_git(repo_path=os.getcwd(), args=["commit", "-F", MERGE_MSG]) @@ -132,6 +153,9 @@ def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref): def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref): """ 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) @@ -180,6 +204,10 @@ 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: + except CherryPickException as e: logging.error(e) sys.exit(1) + except Exception as e: + logging.error(f"full_cherry_pick failed {e}") + traceback.print_exc() + sys.exit(1) diff --git a/ciq_helpers.py b/ciq_helpers.py index fc8a815..0ff9b6d 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_path=repo, args=["reset", "--hard", "HEAD"]) + + def repo_init(repo): """Initialize a git repo object. From 320c7d2b4bf5506d8f8bdc770224907a7ca9cd97 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 | 11 +++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index d8eaffc..436e498 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -15,6 +15,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, ) @@ -27,7 +28,7 @@ class CherryPickException(Exception): pass -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 @@ -38,9 +39,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_present_fixes, error_msg=err, warn=ignore_fixes_check) def manage_commit_message(full_sha, ciq_tags, jira_ticket, commit_successful): @@ -85,7 +90,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): @@ -107,7 +112,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 @@ -134,7 +139,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): """ Check upstream_ref for commits that have this reference: Fixes: . If any, these will also be cherry picked with the ciq @@ -147,10 +152,16 @@ def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref): bf_ciq_tags = [re.sub(r"^cve ", "cve-bf ", tag.strip()) for tag 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): """ Cherry picks a commit from upstream-ref along with its Fixes: references. If cherry-pick or cherry_pick_fixes fail, the exception is propagated @@ -158,10 +169,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__": @@ -185,6 +202,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="Continue even if the commit(s) referenced in Fixes: tags are not present in the current branch", + ) args = parser.parse_args() @@ -203,7 +225,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 CherryPickException as e: logging.error(e) sys.exit(1) diff --git a/ciq_helpers.py b/ciq_helpers.py index 0ff9b6d..c9463d8 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,16 @@ def CIQ_reset_HEAD(repo): return CIQ_run_git(repo_path=repo, args=["reset", "--hard", "HEAD"]) +def CIQ_raise_or_warn(cond, error_msg, warn): + if not cond: + return + + if not warn: + raise RuntimeError(error_msg) + + logging.warning(error_msg) + + def repo_init(repo): """Initialize a git repo object.