Skip to content

Commit 9543770

Browse files
committed
Add run_interdiff
This is a wrapper for running interdiff on pr commits that are backports of upstream kernel commits. The calling convention is similar to check_kernel_commits, where you pass the kernel-src-tree repo, the pr branch, and base branch as arguments. The script then looks though each pr commit looking for backported upstream commits, and calls interdiff to check for differences between the referenced upstream commit and the pr commit itself. As with check_kernel_commits the --markdown argument adds a little flair for embedding the output in a github comment The --interdiff argument allows for passing an alternative path for which interdiff executable is used. This will likely be used by the calling github action to use a custom built interdiff until mainline interdiff can do the fuzzy matching we like.
1 parent 862ed3a commit 9543770

File tree

1 file changed

+240
-0
lines changed

1 file changed

+240
-0
lines changed

run_interdiff.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
#!/usr/bin/env python3
2+
3+
import argparse
4+
import subprocess
5+
import re
6+
import sys
7+
import os
8+
import tempfile
9+
10+
def run_git(repo, args):
11+
"""Run a git command in the given repository and return its output as a string."""
12+
result = subprocess.run(['git', '-C', repo] + args, text=True, capture_output=True, check=False)
13+
if result.returncode != 0:
14+
raise RuntimeError(f"Git command failed: {' '.join(args)}\n{result.stderr}")
15+
return result.stdout
16+
17+
def ref_exists(repo, ref):
18+
"""Return True if the given ref exists in the repository, False otherwise."""
19+
try:
20+
run_git(repo, ['rev-parse', '--verify', '--quiet', ref])
21+
return True
22+
except RuntimeError:
23+
return False
24+
25+
def get_pr_commits(repo, pr_branch, base_branch):
26+
"""Get a list of commit SHAs that are in the PR branch but not in the base branch."""
27+
try:
28+
output = run_git(repo, ['rev-list', f'{base_branch}..{pr_branch}'])
29+
return output.strip().splitlines()
30+
except RuntimeError as e:
31+
raise RuntimeError(f"Failed to get commits from {base_branch}..{pr_branch}: {e}")
32+
33+
def get_commit_message(repo, sha):
34+
"""Get the commit message for a given commit SHA."""
35+
try:
36+
return run_git(repo, ['log', '-n', '1', '--format=%B', sha])
37+
except RuntimeError as e:
38+
raise RuntimeError(f"Failed to get commit message for {sha}: {e}")
39+
40+
def get_short_hash_and_subject(repo, sha):
41+
"""Get the abbreviated commit hash and subject for a given commit SHA."""
42+
try:
43+
output = run_git(repo, ['log', '-n', '1', '--format=%h%x00%s', sha]).strip()
44+
short_hash, subject = output.split('\x00', 1)
45+
return short_hash, subject
46+
except RuntimeError as e:
47+
raise RuntimeError(f"Failed to get short hash and subject for {sha}: {e}")
48+
49+
def extract_upstream_hash(msg):
50+
"""Extract the upstream commit hash from a commit message.
51+
Looks for lines like 'commit <hash>' in the commit message."""
52+
match = re.search(r'^commit\s+([0-9a-fA-F]{12,40})', msg, re.MULTILINE)
53+
if match:
54+
return match.group(1)
55+
return None
56+
57+
def run_interdiff(repo, backport_sha, upstream_sha, interdiff_path):
58+
"""Run interdiff comparing the backport commit with the upstream commit.
59+
Returns (success, output) tuple."""
60+
# Generate format-patch for backport commit
61+
try:
62+
backport_patch = run_git(repo, ['format-patch', '-1', '--stdout', backport_sha])
63+
except RuntimeError as e:
64+
return False, f"Failed to generate patch for backport commit: {e}"
65+
66+
# Generate format-patch for upstream commit
67+
try:
68+
upstream_patch = run_git(repo, ['format-patch', '-1', '--stdout', upstream_sha])
69+
except RuntimeError as e:
70+
return False, f"Failed to generate patch for upstream commit: {e}"
71+
72+
# Write patches to temp files
73+
bp_path = None
74+
up_path = None
75+
try:
76+
with tempfile.NamedTemporaryFile(mode='w', suffix='.patch', delete=False) as bp:
77+
bp.write(backport_patch)
78+
bp_path = bp.name
79+
80+
with tempfile.NamedTemporaryFile(mode='w', suffix='.patch', delete=False) as up:
81+
up.write(upstream_patch)
82+
up_path = up.name
83+
84+
interdiff_result = subprocess.run(
85+
[interdiff_path, '--fuzzy', bp_path, up_path],
86+
text=True,
87+
capture_output=True,
88+
check=False
89+
)
90+
91+
# Check for interdiff errors (non-zero return code other than 1)
92+
# Note: interdiff returns 0 if no differences, 1 if differences found
93+
if interdiff_result.returncode not in (0, 1):
94+
if interdiff_result.stderr:
95+
error_msg = interdiff_result.stderr.strip()
96+
else:
97+
error_msg = f"Exit code {interdiff_result.returncode}"
98+
return False, f"interdiff failed: {error_msg}"
99+
100+
return True, interdiff_result.stdout.strip()
101+
except Exception as e:
102+
return False, f"Failed to run interdiff: {e}"
103+
finally:
104+
# Clean up temp files if they were created
105+
if bp_path and os.path.exists(bp_path):
106+
os.unlink(bp_path)
107+
if up_path and os.path.exists(up_path):
108+
os.unlink(up_path)
109+
110+
def find_interdiff():
111+
"""Find interdiff in system PATH. Returns path if found, None otherwise."""
112+
result = subprocess.run(['which', 'interdiff'], capture_output=True, text=True, check=False)
113+
if result.returncode == 0:
114+
return result.stdout.strip()
115+
return None
116+
117+
def main():
118+
parser = argparse.ArgumentParser(
119+
description="Run interdiff on backported kernel commits to compare with upstream."
120+
)
121+
parser.add_argument("--repo", help="Path to the Linux kernel git repo", required=True)
122+
parser.add_argument("--pr_branch", help="Git reference to the feature branch", required=True)
123+
parser.add_argument("--base_branch", help="Branch the feature branch is based off of", required=True)
124+
parser.add_argument("--markdown", action='store_true', help="Format output with markdown")
125+
parser.add_argument("--interdiff", help="Path to interdiff executable (default: system interdiff)", default=None)
126+
args = parser.parse_args()
127+
128+
# Determine interdiff path
129+
if args.interdiff:
130+
# User specified a path
131+
interdiff_path = args.interdiff
132+
if not os.path.exists(interdiff_path):
133+
print(f"ERROR: interdiff not found at specified path: {interdiff_path}")
134+
sys.exit(1)
135+
if not os.access(interdiff_path, os.X_OK):
136+
print(f"ERROR: interdiff at {interdiff_path} is not executable")
137+
sys.exit(1)
138+
else:
139+
# Try to find system interdiff
140+
interdiff_path = find_interdiff()
141+
if not interdiff_path:
142+
print("ERROR: interdiff not found in system PATH")
143+
print("Please install patchutils or specify path with --interdiff")
144+
sys.exit(1)
145+
146+
# Validate that all required refs exist
147+
missing_refs = []
148+
for refname, refval in [('PR branch', args.pr_branch),
149+
('base branch', args.base_branch)]:
150+
if not ref_exists(args.repo, refval):
151+
missing_refs.append((refname, refval))
152+
153+
if missing_refs:
154+
for refname, refval in missing_refs:
155+
print(f"ERROR: The {refname} '{refval}' does not exist in the given repo.")
156+
print("Please fetch or create the required references before running this script.")
157+
sys.exit(1)
158+
159+
# Get all PR commits
160+
pr_commits = get_pr_commits(args.repo, args.pr_branch, args.base_branch)
161+
if not pr_commits:
162+
if args.markdown:
163+
print("> ℹ️ **No commits found in PR branch that are not in base branch.**")
164+
else:
165+
print("No commits found in PR branch that are not in base branch.")
166+
sys.exit(0)
167+
168+
any_differences = False
169+
out_lines = []
170+
171+
# Process commits in chronological order (oldest first)
172+
for sha in reversed(pr_commits):
173+
try:
174+
short_hash, subject = get_short_hash_and_subject(args.repo, sha)
175+
pr_commit_desc = f"{short_hash} ({subject})"
176+
177+
msg = get_commit_message(args.repo, sha)
178+
upstream_hash = extract_upstream_hash(msg)
179+
except RuntimeError as e:
180+
# Handle errors getting commit information
181+
any_differences = True
182+
if args.markdown:
183+
out_lines.append(f"- ❌ PR commit `{sha[:12]}` → Error getting commit info")
184+
out_lines.append(f" **Error:** {e}\n")
185+
else:
186+
out_lines.append(f"[ERROR] PR commit {sha[:12]} → Error getting commit info")
187+
out_lines.append(f" {e}")
188+
out_lines.append("")
189+
continue
190+
191+
# Only process commits that have an upstream reference
192+
if not upstream_hash:
193+
continue
194+
195+
# Run interdiff
196+
success, output = run_interdiff(args.repo, sha, upstream_hash, interdiff_path)
197+
198+
if not success:
199+
# Error running interdiff
200+
any_differences = True
201+
if args.markdown:
202+
out_lines.append(f"- ❌ PR commit `{pr_commit_desc}` → `{upstream_hash[:12]}`")
203+
out_lines.append(f" **Error:** {output}\n")
204+
else:
205+
out_lines.append(f"[ERROR] PR commit {pr_commit_desc}{upstream_hash[:12]}")
206+
out_lines.append(f" {output}")
207+
out_lines.append("")
208+
elif output:
209+
# There are differences
210+
any_differences = True
211+
if args.markdown:
212+
out_lines.append(f"- ⚠️ PR commit `{pr_commit_desc}` → upstream `{upstream_hash[:12]}`")
213+
out_lines.append(f" **Differences found:**\n")
214+
out_lines.append("```diff")
215+
out_lines.append(output)
216+
out_lines.append("```\n")
217+
else:
218+
out_lines.append(f"[DIFF] PR commit {pr_commit_desc} → upstream {upstream_hash[:12]}")
219+
out_lines.append("Differences found:")
220+
out_lines.append("")
221+
for line in output.splitlines():
222+
out_lines.append(" " + line)
223+
out_lines.append("")
224+
225+
# Print results
226+
if any_differences:
227+
if args.markdown:
228+
print("## :mag: Interdiff Analysis\n")
229+
print('\n'.join(out_lines))
230+
print("*This is an automated interdiff check for backported commits.*")
231+
else:
232+
print('\n'.join(out_lines))
233+
else:
234+
if args.markdown:
235+
print("> ✅ **All backported commits match their upstream counterparts.**")
236+
else:
237+
print("All backported commits match their upstream counterparts.")
238+
239+
if __name__ == "__main__":
240+
main()

0 commit comments

Comments
 (0)