Skip to content

Commit 4639c59

Browse files
committed
Add cherry-pick script
1 parent b3a34bb commit 4639c59

File tree

1 file changed

+162
-0
lines changed

1 file changed

+162
-0
lines changed

tools/python/cherry_pick.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
#
4+
# Usage:
5+
# python create_cherry_pick.py --label "release:1.24.2" --output cherry_pick.cmd --branch "origin/rel-1.24.2"
6+
#
7+
# Arguments:
8+
# --label: Label to filter PRs (required)
9+
# --output: Output cmd file path (required)
10+
# --repo: Repository (default: microsoft/onnxruntime)
11+
# --branch: Target branch to compare against for dependency checks (default: HEAD)
12+
#
13+
# This script fetches merged PRs with the specified label from the onnxruntime repository,
14+
# sorts them by merge date, and generates:
15+
# 1. A batch file (specified by --output) containing git cherry-pick commands.
16+
# 2. A markdown file (cherry_pick_pr_description.md) summarizing the cherry-picked PRs for pull request description.
17+
#
18+
# It also checks for potential missing dependencies (conflicts) by verifying if files modified
19+
# by the cherry-picked commits have any other modifications in the target branch history
20+
# that are not included in the cherry-pick list.
21+
import argparse
22+
import subprocess
23+
import json
24+
import sys
25+
from collections import defaultdict
26+
27+
def main():
28+
parser = argparse.ArgumentParser(description="Generate cherry-pick script from PRs with a specific label.")
29+
parser.add_argument("--label", required=True, help="Label to filter PRs")
30+
parser.add_argument("--output", required=True, help="Output cmd file path")
31+
parser.add_argument("--repo", default="microsoft/onnxruntime", help="Repository (default: microsoft/onnxruntime)")
32+
parser.add_argument("--branch", default="HEAD", help="Target branch to compare against for dependency checks (default: HEAD)")
33+
args = parser.parse_args()
34+
35+
# Fetch merged PRs with the specified label using gh CLI
36+
print(f"Fetching merged PRs with label '{args.label}' from {args.repo}...")
37+
cmd = [
38+
"gh", "pr", "list",
39+
"--repo", args.repo,
40+
"--label", args.label,
41+
"--state", "merged",
42+
"--json", "number,title,mergeCommit,mergedAt",
43+
"-L", "200"
44+
]
45+
46+
try:
47+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
48+
prs = json.loads(result.stdout)
49+
except subprocess.CalledProcessError as e:
50+
print(f"Error running gh command: {e}", file=sys.stderr)
51+
print(e.stderr, file=sys.stderr)
52+
sys.exit(1)
53+
except json.JSONDecodeError as e:
54+
print(f"Error parsing gh output: {e}", file=sys.stderr)
55+
print(f"Output was: {result.stdout}", file=sys.stderr)
56+
sys.exit(1)
57+
58+
if not prs:
59+
print(f"No PRs found with label '{args.label}'.")
60+
return
61+
62+
# Sort by mergedAt (ISO 8601 strings sort correctly in chronological order)
63+
prs.sort(key=lambda x: x['mergedAt'])
64+
65+
# Write to output cmd file
66+
commit_count = 0
67+
with open(args.output, "w", encoding="utf-8") as f:
68+
f.write("@echo off\n")
69+
f.write(f"rem Cherry-pick {args.label} commits\n")
70+
f.write("rem Sorted by merge time (oldest first)\n\n")
71+
72+
for pr in prs:
73+
number = pr['number']
74+
title = pr['title']
75+
safe_title = title.replace('\n', ' ')
76+
77+
if not pr.get('mergeCommit'):
78+
print(f"Warning: PR #{number} has no merge commit OID. Skipping.", file=sys.stderr)
79+
continue
80+
81+
oid = pr['mergeCommit']['oid']
82+
f.write(f"rem PR {number}: {safe_title}\n")
83+
f.write(f"git cherry-pick {oid}\n\n")
84+
commit_count += 1
85+
86+
print(f"Generated {args.output} with {commit_count} commits.")
87+
88+
# Write to markdown file. You can use it as the pull request description.
89+
md_output = "cherry_pick_pr_description.md"
90+
with open(md_output, "w", encoding="utf-8") as f:
91+
f.write(f"This cherry-picks the following commits for the release:\n")
92+
for pr in prs:
93+
if not pr.get('mergeCommit'):
94+
continue
95+
number = pr['number']
96+
f.write(f"- #{number}\n")
97+
98+
print(f"Generated {md_output} with {commit_count} commits.")
99+
100+
# Check for potential missing dependencies
101+
print("\nChecking for potential missing dependencies (conflicts)...")
102+
103+
# Collect OIDs being cherry-picked
104+
cherry_pick_oids = set()
105+
for pr in prs:
106+
if pr.get('mergeCommit'):
107+
cherry_pick_oids.add(pr['mergeCommit']['oid'])
108+
109+
for pr in prs:
110+
if not pr.get('mergeCommit'):
111+
continue
112+
113+
oid = pr['mergeCommit']['oid']
114+
number = pr['number']
115+
116+
# Get files changed by this commit
117+
try:
118+
res = subprocess.run(
119+
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", oid],
120+
capture_output=True, text=True, check=True
121+
)
122+
files = res.stdout.strip().splitlines()
123+
except subprocess.CalledProcessError as e:
124+
print(f"Error getting changed files for {oid}: {e}", file=sys.stderr)
125+
continue
126+
127+
# For each file, find commits that modified it between the target branch and the cherry-picked commit.
128+
# Deduplicate warnings: group affected files by missing commit.
129+
# missing_commits maps: missing_commit_oid -> (title, [list of affected files])
130+
missing_commits = defaultdict(lambda: ("", []))
131+
for filepath in files:
132+
try:
133+
res = subprocess.run(
134+
["git", "log", oid, "--not", args.branch, "--format=%H %s", "--", filepath],
135+
capture_output=True, text=True, check=True
136+
)
137+
for line in res.stdout.strip().splitlines():
138+
parts = line.split(' ', 1)
139+
c = parts[0]
140+
title = parts[1] if len(parts) > 1 else ""
141+
142+
if c == oid:
143+
continue
144+
if c not in cherry_pick_oids:
145+
existing_title, existing_files = missing_commits[c]
146+
if not existing_title:
147+
existing_title = title
148+
existing_files.append(filepath)
149+
missing_commits[c] = (existing_title, existing_files)
150+
151+
except subprocess.CalledProcessError as e:
152+
print(f"Error checking history for {filepath}: {e}", file=sys.stderr)
153+
continue
154+
155+
# Print deduplicated warnings
156+
for missing_oid, (title, affected_files) in missing_commits.items():
157+
files_str = ", ".join(affected_files)
158+
print(f"WARNING: PR #{number} ({oid}) depends on commit {missing_oid} ({title}) "
159+
f"which is not in the cherry-pick list. Affected files: {files_str}")
160+
161+
if __name__ == "__main__":
162+
main()

0 commit comments

Comments
 (0)