-
Notifications
You must be signed in to change notification settings - Fork 1
Add run_interdiff #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,240 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| import argparse | ||
| import subprocess | ||
| import re | ||
| import sys | ||
| import os | ||
| import tempfile | ||
|
|
||
| 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 | ||
|
|
||
| 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]) | ||
| return True | ||
| except RuntimeError: | ||
| return False | ||
|
|
||
| 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.""" | ||
| try: | ||
| output = run_git(repo, ['rev-list', f'{base_branch}..{pr_branch}']) | ||
| return output.strip().splitlines() | ||
| except RuntimeError as e: | ||
| raise RuntimeError(f"Failed to get commits from {base_branch}..{pr_branch}: {e}") | ||
|
|
||
| def get_commit_message(repo, sha): | ||
| """Get the commit message for a given commit SHA.""" | ||
| try: | ||
| return run_git(repo, ['log', '-n', '1', '--format=%B', sha]) | ||
| except RuntimeError as e: | ||
| raise RuntimeError(f"Failed to get commit message for {sha}: {e}") | ||
|
|
||
| def get_short_hash_and_subject(repo, sha): | ||
| """Get the abbreviated commit hash and subject for a given commit SHA.""" | ||
| try: | ||
| output = run_git(repo, ['log', '-n', '1', '--format=%h%x00%s', sha]).strip() | ||
| short_hash, subject = output.split('\x00', 1) | ||
| return short_hash, subject | ||
| except RuntimeError as e: | ||
| raise RuntimeError(f"Failed to get short hash and subject for {sha}: {e}") | ||
|
|
||
| def extract_upstream_hash(msg): | ||
| """Extract the upstream commit hash from a commit message. | ||
| Looks for lines like 'commit <hash>' in the commit message.""" | ||
| match = re.search(r'^commit\s+([0-9a-fA-F]{12,40})', msg, re.MULTILINE) | ||
| if match: | ||
| return match.group(1) | ||
| return None | ||
|
|
||
| def run_interdiff(repo, backport_sha, upstream_sha, interdiff_path): | ||
| """Run interdiff comparing the backport commit with the upstream commit. | ||
| Returns (success, output) tuple.""" | ||
| # Generate format-patch for backport commit | ||
| try: | ||
| backport_patch = run_git(repo, ['format-patch', '-1', '--stdout', backport_sha]) | ||
| except RuntimeError as e: | ||
| return False, f"Failed to generate patch for backport commit: {e}" | ||
|
|
||
| # Generate format-patch for upstream commit | ||
| try: | ||
| upstream_patch = run_git(repo, ['format-patch', '-1', '--stdout', upstream_sha]) | ||
| except RuntimeError as e: | ||
| return False, f"Failed to generate patch for upstream commit: {e}" | ||
|
|
||
| # Write patches to temp files | ||
| bp_path = None | ||
| up_path = None | ||
| try: | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.patch', delete=False) as bp: | ||
| bp.write(backport_patch) | ||
| bp_path = bp.name | ||
|
|
||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.patch', delete=False) as up: | ||
| up.write(upstream_patch) | ||
| up_path = up.name | ||
|
|
||
| interdiff_result = subprocess.run( | ||
PlaidCat marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [interdiff_path, '--fuzzy', bp_path, up_path], | ||
| text=True, | ||
| capture_output=True, | ||
| check=False | ||
| ) | ||
|
|
||
| # Check for interdiff errors (non-zero return code other than 1) | ||
| # Note: interdiff returns 0 if no differences, 1 if differences found | ||
| if interdiff_result.returncode not in (0, 1): | ||
| if interdiff_result.stderr: | ||
| error_msg = interdiff_result.stderr.strip() | ||
| else: | ||
| error_msg = f"Exit code {interdiff_result.returncode}" | ||
| return False, f"interdiff failed: {error_msg}" | ||
|
|
||
| return True, interdiff_result.stdout.strip() | ||
| except Exception as e: | ||
| return False, f"Failed to run interdiff: {e}" | ||
| finally: | ||
| # Clean up temp files if they were created | ||
| if bp_path and os.path.exists(bp_path): | ||
| os.unlink(bp_path) | ||
| if up_path and os.path.exists(up_path): | ||
| os.unlink(up_path) | ||
|
|
||
| def find_interdiff(): | ||
| """Find interdiff in system PATH. Returns path if found, None otherwise.""" | ||
| result = subprocess.run(['which', 'interdiff'], capture_output=True, text=True, check=False) | ||
| if result.returncode == 0: | ||
| return result.stdout.strip() | ||
| return None | ||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser( | ||
| description="Run interdiff on backported kernel commits to compare with upstream." | ||
| ) | ||
| parser.add_argument("--repo", help="Path to the Linux kernel git repo", required=True) | ||
| parser.add_argument("--pr_branch", help="Git reference to the feature branch", required=True) | ||
| parser.add_argument("--base_branch", help="Branch the feature branch is based off of", required=True) | ||
| parser.add_argument("--markdown", action='store_true', help="Format output with markdown") | ||
| parser.add_argument("--interdiff", help="Path to interdiff executable (default: system interdiff)", default=None) | ||
| args = parser.parse_args() | ||
|
|
||
| # Determine interdiff path | ||
| if args.interdiff: | ||
| # User specified a path | ||
| interdiff_path = args.interdiff | ||
| if not os.path.exists(interdiff_path): | ||
| print(f"ERROR: interdiff not found at specified path: {interdiff_path}") | ||
| sys.exit(1) | ||
| if not os.access(interdiff_path, os.X_OK): | ||
| print(f"ERROR: interdiff at {interdiff_path} is not executable") | ||
| sys.exit(1) | ||
| else: | ||
| # Try to find system interdiff | ||
| interdiff_path = find_interdiff() | ||
| if not interdiff_path: | ||
| print("ERROR: interdiff not found in system PATH") | ||
| print("Please install patchutils or specify path with --interdiff") | ||
| sys.exit(1) | ||
|
|
||
| # Validate that all required refs exist | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be interesting to see if this works with remote fork PRs
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-Blocking |
||
| missing_refs = [] | ||
| for refname, refval in [('PR branch', args.pr_branch), | ||
| ('base branch', args.base_branch)]: | ||
| if not ref_exists(args.repo, refval): | ||
| missing_refs.append((refname, refval)) | ||
|
|
||
| if missing_refs: | ||
| for refname, refval in missing_refs: | ||
| print(f"ERROR: The {refname} '{refval}' does not exist in the given repo.") | ||
| print("Please fetch or create the required references before running this script.") | ||
| sys.exit(1) | ||
|
|
||
| # Get all PR commits | ||
| pr_commits = get_pr_commits(args.repo, args.pr_branch, args.base_branch) | ||
| if not pr_commits: | ||
| if args.markdown: | ||
| print("> ℹ️ **No commits found in PR branch that are not in base branch.**") | ||
| else: | ||
| print("No commits found in PR branch that are not in base branch.") | ||
| sys.exit(0) | ||
|
|
||
| any_differences = False | ||
| out_lines = [] | ||
|
|
||
| # Process commits in chronological order (oldest first) | ||
| for sha in reversed(pr_commits): | ||
| try: | ||
| 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) | ||
| upstream_hash = extract_upstream_hash(msg) | ||
| except RuntimeError as e: | ||
| # Handle errors getting commit information | ||
| any_differences = True | ||
| if args.markdown: | ||
| out_lines.append(f"- ❌ PR commit `{sha[:12]}` → Error getting commit info") | ||
| out_lines.append(f" **Error:** {e}\n") | ||
| else: | ||
| out_lines.append(f"[ERROR] PR commit {sha[:12]} → Error getting commit info") | ||
| out_lines.append(f" {e}") | ||
| out_lines.append("") | ||
| continue | ||
|
|
||
| # Only process commits that have an upstream reference | ||
| if not upstream_hash: | ||
| continue | ||
|
|
||
| # Run interdiff | ||
| success, output = run_interdiff(args.repo, sha, upstream_hash, interdiff_path) | ||
|
|
||
| if not success: | ||
| # Error running interdiff | ||
| any_differences = True | ||
| if args.markdown: | ||
| out_lines.append(f"- ❌ PR commit `{pr_commit_desc}` → `{upstream_hash[:12]}`") | ||
| out_lines.append(f" **Error:** {output}\n") | ||
| else: | ||
| out_lines.append(f"[ERROR] PR commit {pr_commit_desc} → {upstream_hash[:12]}") | ||
| out_lines.append(f" {output}") | ||
| out_lines.append("") | ||
| elif output: | ||
| # There are differences | ||
| any_differences = True | ||
| if args.markdown: | ||
| out_lines.append(f"- ⚠️ PR commit `{pr_commit_desc}` → upstream `{upstream_hash[:12]}`") | ||
| out_lines.append(f" **Differences found:**\n") | ||
| out_lines.append("```diff") | ||
| out_lines.append(output) | ||
| out_lines.append("```\n") | ||
| else: | ||
| out_lines.append(f"[DIFF] PR commit {pr_commit_desc} → upstream {upstream_hash[:12]}") | ||
| out_lines.append("Differences found:") | ||
| out_lines.append("") | ||
| for line in output.splitlines(): | ||
| out_lines.append(" " + line) | ||
| out_lines.append("") | ||
|
|
||
| # Print results | ||
| if any_differences: | ||
| if args.markdown: | ||
| print("## :mag: Interdiff Analysis\n") | ||
| print('\n'.join(out_lines)) | ||
| print("*This is an automated interdiff check for backported commits.*") | ||
| else: | ||
| print('\n'.join(out_lines)) | ||
| else: | ||
| if args.markdown: | ||
| print("> ✅ **All backported commits match their upstream counterparts.**") | ||
| else: | ||
| print("All backported commits match their upstream counterparts.") | ||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.