-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathrun_interdiff.py
More file actions
executable file
·244 lines (206 loc) · 9.46 KB
/
run_interdiff.py
File metadata and controls
executable file
·244 lines (206 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env python3
import argparse
import os
import re
import subprocess
import sys
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(
[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
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(" **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()