Skip to content

Commit 80703a6

Browse files
committed
ciq-cherry-pick.py: Cherry pick commit only if the Fixes: references are commited
If the commit that needs to be cherry picked has "Fixes:" references in the commit body, there is now a check in pace that verify if those commits are present in the current branch. At the moment, the scrips 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 <rnicolescu@ciq.com>
1 parent a19fa7a commit 80703a6

File tree

3 files changed

+206
-56
lines changed

3 files changed

+206
-56
lines changed

check_kernel_commits.py

Lines changed: 43 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,33 @@
88
import textwrap
99
from typing import Optional
1010

11-
12-
def run_git(repo, args):
13-
"""Run a git command in the given repository and return its output as a string."""
14-
result = subprocess.run(["git", "-C", repo] + args, text=True, capture_output=True, check=False)
15-
if result.returncode != 0:
16-
raise RuntimeError(f"Git command failed: {' '.join(args)}\n{result.stderr}")
17-
return result.stdout
11+
from ciq_helpers import (
12+
CIQ_commit_exists_in_branch,
13+
CIQ_extract_fixes_references_from_commit_body_lines,
14+
CIQ_get_commit_body,
15+
CIQ_hash_exists_in_ref,
16+
CIQ_run_git,
17+
)
1818

1919

2020
def ref_exists(repo, ref):
2121
"""Return True if the given ref exists in the repository, False otherwise."""
2222
try:
23-
run_git(repo, ["rev-parse", "--verify", "--quiet", ref])
23+
CIQ_run_git(repo, ["rev-parse", "--verify", "--quiet", ref])
2424
return True
2525
except RuntimeError:
2626
return False
2727

2828

2929
def get_pr_commits(repo, pr_branch, base_branch):
3030
"""Get a list of commit SHAs that are in the PR branch but not in the base branch."""
31-
output = run_git(repo, ["rev-list", f"{base_branch}..{pr_branch}"])
31+
output = CIQ_run_git(repo, ["rev-list", f"{base_branch}..{pr_branch}"])
3232
return output.strip().splitlines()
3333

3434

35-
def get_commit_message(repo, sha):
36-
"""Get the commit message for a given commit SHA."""
37-
return run_git(repo, ["log", "-n", "1", "--format=%B", sha])
38-
39-
4035
def get_short_hash_and_subject(repo, sha):
4136
"""Get the abbreviated commit hash and subject for a given commit SHA."""
42-
output = run_git(repo, ["log", "-n", "1", "--format=%h%x00%s", sha]).strip()
37+
output = CIQ_run_git(repo, ["log", "-n", "1", "--format=%h%x00%s", sha]).strip()
4338
short_hash, subject = output.split("\x00", 1)
4439
return short_hash, subject
4540

@@ -48,61 +43,56 @@ def hash_exists_in_mainline(repo, upstream_ref, hash_):
4843
"""
4944
Return True if hash_ is reachable from upstream_ref (i.e., is an ancestor of it).
5045
"""
51-
try:
52-
run_git(repo, ["merge-base", "--is-ancestor", hash_, upstream_ref])
53-
return True
54-
except RuntimeError:
55-
return False
46+
47+
return CIQ_hash_exists_in_ref(repo, upstream_ref, hash_)
5648

5749

5850
def find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
5951
"""
60-
Return unique commits in upstream_ref that have Fixes: <N chars of hash_> in their message, case-insensitive.
52+
Return unique commits in upstream_ref that have Fixes: <N chars of hash_> in their message, case-insensitive,
53+
if they have not been commited in the pr_branch.
6154
Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length.
6255
Returns a list of tuples: (full_hash, display_string)
6356
"""
6457
results = []
58+
59+
# Prepare hash prefixes from 12 down to 6
60+
hash_prefixes = [hash_[:index] for index in range(12, 5, -1)]
61+
6562
# Get all commits with 'Fixes:' in the message
66-
output = run_git(repo, ["log", upstream_ref, "--grep", "Fixes:", "-i", "--format=%H %h %s (%an)%x0a%B%x00"]).strip()
63+
output = CIQ_run_git(
64+
repo,
65+
[
66+
"log",
67+
upstream_ref,
68+
"--grep",
69+
"Fixes:",
70+
"-i",
71+
"--format=%H %h %s (%an)%x0a%B%x00",
72+
],
73+
).strip()
6774
if not output:
6875
return []
76+
6977
# Each commit is separated by a NUL character and a newline
7078
commits = output.split("\x00\x0a")
71-
# Prepare hash prefixes from 12 down to 6
72-
hash_prefixes = [hash_[:index] for index in range(12, 5, -1)]
7379
for commit in commits:
7480
if not commit.strip():
7581
continue
76-
# The first line is the summary, the rest is the body
82+
7783
lines = commit.splitlines()
78-
if not lines:
79-
continue
84+
# The first line is the summary, the rest is the body
8085
header = lines[0]
81-
full_hash = header.split()[0]
82-
# Search for Fixes: lines in the commit message
83-
for line in lines[1:]:
84-
m = re.match(r"^\s*Fixes:\s*([0-9a-fA-F]{6,40})", line, re.IGNORECASE)
85-
if m:
86-
for prefix in hash_prefixes:
87-
if m.group(1).lower().startswith(prefix.lower()):
88-
if not commit_exists_in_branch(repo, pr_branch, full_hash):
89-
results.append((full_hash, " ".join(header.split()[1:])))
90-
break
91-
else:
92-
continue
93-
return results
86+
full_hash, display_string = (lambda h: (h[0], " ".join(h[1:])))(header.split())
87+
fixes = CIQ_extract_fixes_references_from_commit_body_lines(lines=lines[1:])
88+
for fix in fixes:
89+
for prefix in hash_prefixes:
90+
if fix.lower().startswith(prefix.lower()):
91+
if not CIQ_commit_exists_in_branch(repo, pr_branch, full_hash):
92+
results.append((full_hash, display_string))
93+
break
9494

95-
96-
def commit_exists_in_branch(repo, pr_branch, upstream_hash_):
97-
"""
98-
Return True if upstream_hash_ has been backported and it exists in the
99-
pr branch
100-
"""
101-
output = run_git(repo, ["log", pr_branch, "--grep", "commit " + upstream_hash_])
102-
if not output:
103-
return False
104-
105-
return True
95+
return results
10696

10797

10898
def wrap_paragraph(text, width=80, initial_indent="", subsequent_indent=""):
@@ -176,7 +166,7 @@ def main():
176166
if os.path.exists(vulns_repo):
177167
# Repository exists, update it with git pull
178168
try:
179-
run_git(vulns_repo, ["pull"])
169+
CIQ_run_git(vulns_repo, ["pull"])
180170
except RuntimeError as e:
181171
print(f"WARNING: Failed to update vulns repo: {e}")
182172
print("Continuing with existing repository...")
@@ -222,7 +212,7 @@ def main():
222212
for sha in reversed(pr_commits): # oldest first
223213
short_hash, subject = get_short_hash_and_subject(args.repo, sha)
224214
pr_commit_desc = f"{short_hash} ({subject})"
225-
msg = get_commit_message(args.repo, sha)
215+
msg = CIQ_get_commit_body(args.repo, sha)
226216
upstream_hashes = re.findall(r"^commit\s+([0-9a-fA-F]{40})", msg, re.MULTILINE)
227217
for uhash in upstream_hashes:
228218
short_uhash = uhash[:12]

ciq-cherry-pick.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
11
import argparse
2+
import logging
23
import os
34
import subprocess
45

56
import git
67

7-
from ciq_helpers import CIQ_cherry_pick_commit_standardization, CIQ_original_commit_author_to_tag_string
8-
9-
# from ciq_helpers import *
8+
from ciq_helpers import (
9+
CIQ_cherry_pick_commit_standardization,
10+
CIQ_commit_exists_in_current_branch,
11+
CIQ_fixes_references,
12+
CIQ_original_commit_author_to_tag_string,
13+
)
1014

1115
MERGE_MSG = git.Repo(os.getcwd()).git_dir + "/MERGE_MSG"
1216

17+
18+
def check_fixes(sha):
19+
"""
20+
Checks if commit has "Fixes:" references and if so, it checks if the
21+
commit(s) that it tries to fix are part of the current branch
22+
"""
23+
24+
fixes = CIQ_fixes_references(repo_path=".", sha=sha)
25+
if len(fixes) == 0:
26+
logging.warning("The commit you try to cherry pick has no Fixes: reference; review it carefully")
27+
return
28+
29+
for fix in fixes:
30+
if not CIQ_commit_exists_in_current_branch(".", fix):
31+
raise RuntimeError(f"The commit you want to cherry pick references a Fixes {fix}: but this is not here")
32+
33+
1334
if __name__ == "__main__":
1435
print("CIQ custom cherry picker")
1536
parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
@@ -39,6 +60,8 @@
3960
if args.ciq_tag is not None:
4061
tags = args.ciq_tag.split(",")
4162

63+
check_fixes(args.sha)
64+
4265
author = CIQ_original_commit_author_to_tag_string(repo_path=os.getcwd(), sha=args.sha)
4366
if author is None:
4467
exit(1)

ciq_helpers.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,143 @@ def CIQ_original_commit_author_to_tag_string(repo_path, sha):
169169
return "commit-author " + git_auth_res.stdout.decode("utf-8").replace('"', "").strip()
170170

171171

172+
def CIQ_run_git(repo_path, args):
173+
"""
174+
Run a git command in the given repository and return its output as a string.
175+
"""
176+
result = subprocess.run(["git", "-C", repo_path] + args, text=True, capture_output=True, check=False)
177+
if result.returncode != 0:
178+
raise RuntimeError(f"Git command failed: {' '.join(args)}\n{result.stderr}")
179+
180+
return result.stdout
181+
182+
183+
def CIQ_get_commit_body(repo_path, sha):
184+
return CIQ_run_git(repo_path, ["show", "-s", sha, "--format=%B"])
185+
186+
187+
def CIQ_extract_fixes_references_from_commit_body_lines(lines):
188+
fixes = []
189+
for line in lines:
190+
m = re.match(r"^\s*Fixes:\s*([0-9a-fA-F]{6,40})", line, re.IGNORECASE)
191+
if not m:
192+
continue
193+
194+
fixes.append(m.group(1))
195+
196+
return fixes
197+
198+
199+
def CIQ_fixes_references(repo_path, sha):
200+
"""
201+
If commit message of sha contains lines like
202+
Fixes: <short_fixed>, this returns a list of <short_fixed>, otherwise an empty list
203+
"""
204+
205+
commit_body = CIQ_get_commit_body(repo_path, sha)
206+
return CIQ_extract_fixes_references_from_commit_body_lines(lines=commit_body.splitlines())
207+
208+
209+
def CIQ_get_full_hash(repo, short_hash):
210+
return CIQ_run_git(repo, ["show", "-s", "--pretty=%H", short_hash]).strip()
211+
212+
213+
def CIQ_get_current_branch(repo):
214+
return CIQ_run_git(repo, ["branch", "--show-current"]).strip()
215+
216+
217+
def CIQ_hash_exists_in_ref(repo, pr_ref, hash_):
218+
"""
219+
Return True is hash_ is reachable from pr_branch
220+
"""
221+
222+
try:
223+
CIQ_run_git(repo, ["merge-base", "--is-ancestor", hash_, pr_ref])
224+
return True
225+
except RuntimeError:
226+
return False
227+
228+
229+
# TODO think of a better name
230+
def CIQ_commit_exists_in_branch(repo, pr_branch, upstream_hash_):
231+
"""
232+
Return True if upstream_hash_ has been backported and it exists in the pr branch
233+
"""
234+
235+
# First check if the commit has been backported by CIQ
236+
output = CIQ_run_git(repo, ["log", pr_branch, "--grep", "commit " + upstream_hash_])
237+
if output:
238+
return True
239+
240+
# If it was not backported by CIQ, maybe it came from upstream as it is
241+
return CIQ_hash_exists_in_ref(repo, pr_branch, upstream_hash_)
242+
243+
244+
def CIQ_commit_exists_in_current_branch(repo, upstream_hash_):
245+
"""
246+
Return True if upstream_hash_ has been backported and it exists in the current branch
247+
"""
248+
249+
current_branch = CIQ_get_current_branch(repo)
250+
full_upstream_hash = CIQ_get_full_hash(repo, upstream_hash_)
251+
252+
return CIQ_commit_exists_in_branch(repo, current_branch, full_upstream_hash)
253+
254+
255+
def CIQ_find_fixes_in_mainline(repo, pr_branch, upstream_ref, hash_):
256+
"""
257+
Return unique commits in upstream_ref that have Fixes: <N chars of hash_> in their message, case-insensitive,
258+
if they have not been commited in the pr_branch.
259+
Start from 12 chars and work down to 6, but do not include duplicates if already found at a longer length.
260+
Returns a list of tuples: (full_hash, display_string)
261+
"""
262+
results = []
263+
264+
# Prepare hash prefixes from 12 down to 6
265+
hash_prefixes = [hash_[:index] for index in range(12, 5, -1)]
266+
267+
# Get all commits with 'Fixes:' in the message
268+
output = CIQ_run_git(
269+
repo,
270+
[
271+
"log",
272+
upstream_ref,
273+
"--grep",
274+
"Fixes:",
275+
"-i",
276+
"--format=%H %h %s (%an)%x0a%B%x00",
277+
],
278+
).strip()
279+
if not output:
280+
return []
281+
282+
# Each commit is separated by a NUL character and a newline
283+
commits = output.split("\x00\x0a")
284+
for commit in commits:
285+
if not commit.strip():
286+
continue
287+
288+
lines = commit.splitlines()
289+
# The first line is the summary, the rest is the body
290+
header = lines[0]
291+
full_hash, display_string = (lambda h: (h[0], " ".join(h[1:])))(header.split())
292+
fixes = CIQ_extract_fixes_references_from_commit_body_lines(lines=lines[1:])
293+
for fix in fixes:
294+
for prefix in hash_prefixes:
295+
if fix.lower().startswith(prefix.lower()):
296+
if not CIQ_commit_exists_in_branch(repo, pr_branch, full_hash):
297+
results.append((full_hash, display_string))
298+
break
299+
300+
return results
301+
302+
303+
def CIQ_find_fixes_in_mainline_current_branch(repo, upstream_ref, hash_):
304+
current_branch = CIQ_get_current_branch(repo)
305+
306+
return CIQ_find_fixes_in_mainline(repo, current_branch, upstream_ref, hash_)
307+
308+
172309
def repo_init(repo):
173310
"""Initialize a git repo object.
174311

0 commit comments

Comments
 (0)