Skip to content

Commit 6a44998

Browse files
committed
address copilot feedback
1 parent dd0d487 commit 6a44998

File tree

1 file changed

+177
-110
lines changed

1 file changed

+177
-110
lines changed

tools/python/cherry_pick.py

Lines changed: 177 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,198 @@
11
# Copyright (c) Microsoft Corporation. All rights reserved.
22
# 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.
3+
4+
"""
5+
Cherry-Pick Helper Script
6+
-------------------------
7+
Description:
8+
This script automates the process of cherry-picking commits for a release branch.
9+
It fetches merged PRs with a specific label, sorts them by merge date, and generates:
10+
1. A batch file (.cmd) with git cherry-pick commands.
11+
2. A markdown file (.md) for the PR description.
12+
It also checks for potential missing dependencies (conflicts) by verifying if files modified
13+
by the cherry-picked commits have any other modifications in commits that are not in the
14+
specified target branch and are not included in the cherry-pick list.
15+
16+
Usage:
17+
python cherry_pick.py --label "release:1.24.2" --output cherry_pick.cmd --branch "origin/rel-1.24.2"
18+
19+
Requirements:
20+
- Python 3.7+
21+
- GitHub CLI (gh) logged in.
22+
- Git available in PATH.
23+
"""
24+
2125
import argparse
2226
import json
2327
import subprocess
2428
import sys
2529
from collections import defaultdict
2630

2731

28-
def main():
29-
parser = argparse.ArgumentParser(description="Generate cherry-pick script from PRs with a specific label.")
30-
parser.add_argument("--label", required=True, help="Label to filter PRs")
31-
parser.add_argument("--output", required=True, help="Output cmd file path")
32-
parser.add_argument("--repo", default="microsoft/onnxruntime", help="Repository (default: microsoft/onnxruntime)")
33-
parser.add_argument(
34-
"--branch", default="HEAD", help="Target branch to compare against for dependency checks (default: HEAD)"
35-
)
36-
args = parser.parse_args()
37-
38-
# Fetch merged PRs with the specified label using gh CLI
39-
print(f"Fetching merged PRs with label '{args.label}' from {args.repo}...")
32+
def run_command(command_list, cwd=None, silent=False):
33+
"""Run a command using a list of arguments for security (no shell=True)."""
34+
try:
35+
result = subprocess.run(command_list, check=False, capture_output=True, text=True, cwd=cwd, encoding="utf-8")
36+
if result.returncode != 0:
37+
if not silent:
38+
log_str = " ".join(command_list)
39+
print(f"Error running command: {log_str}", file=sys.stderr)
40+
if result.stderr:
41+
print(f"Stderr: {result.stderr.strip()}", file=sys.stderr)
42+
return None
43+
return result.stdout
44+
except FileNotFoundError:
45+
if not silent:
46+
cmd = command_list[0]
47+
print(f"Error: '{cmd}' command not found.", file=sys.stderr)
48+
if cmd == "gh":
49+
print(
50+
"Please install GitHub CLI (https://cli.github.com/) and ensure 'gh' is available on your PATH.",
51+
file=sys.stderr,
52+
)
53+
return None
54+
except Exception as e:
55+
if not silent:
56+
print(f"Exception running command {' '.join(command_list)}: {e}", file=sys.stderr)
57+
return None
58+
59+
60+
def check_preflight():
61+
"""Verify gh CLI and git repository early."""
62+
# Check git
63+
git_check = run_command(["git", "rev-parse", "--is-inside-work-tree"], silent=True)
64+
if not git_check:
65+
print("Error: This script must be run inside a git repository.", file=sys.stderr)
66+
return False
67+
68+
# Check gh
69+
gh_check = run_command(["gh", "--version"], silent=True)
70+
if not gh_check:
71+
print("Error: GitHub CLI (gh) not found or not in PATH.", file=sys.stderr)
72+
print(
73+
"Please install GitHub CLI (https://cli.github.com/) and ensure 'gh' is available on your PATH.",
74+
file=sys.stderr,
75+
)
76+
return False
77+
78+
gh_auth = run_command(["gh", "auth", "status"], silent=True)
79+
if not gh_auth:
80+
print("Error: GitHub CLI not authenticated. Please run 'gh auth login'.", file=sys.stderr)
81+
return False
82+
83+
return True
84+
85+
86+
def get_merged_prs(repo, label, limit=200):
87+
"""Fetch merged PRs with the specific label."""
88+
print(f"Fetching merged PRs with label '{label}' from {repo}...")
4089
cmd = [
4190
"gh",
4291
"pr",
4392
"list",
4493
"--repo",
45-
args.repo,
94+
repo,
4695
"--label",
47-
args.label,
96+
label,
4897
"--state",
4998
"merged",
5099
"--json",
51100
"number,title,mergeCommit,mergedAt",
52101
"-L",
53-
"200",
102+
str(limit),
54103
]
104+
output = run_command(cmd)
105+
if not output:
106+
return []
55107

56108
try:
57-
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
58-
prs = json.loads(result.stdout)
59-
except subprocess.CalledProcessError as e:
60-
print(f"Error running gh command: {e}", file=sys.stderr)
61-
print(e.stderr, file=sys.stderr)
62-
sys.exit(1)
109+
return json.loads(output)
63110
except json.JSONDecodeError as e:
64111
print(f"Error parsing gh output: {e}", file=sys.stderr)
65-
print(f"Output was: {result.stdout}", file=sys.stderr)
66-
sys.exit(1)
112+
return []
113+
114+
115+
def get_changed_files(oid):
116+
"""Get list of files changed in a commit."""
117+
output = run_command(["git", "diff-tree", "--no-commit-id", "--name-only", "-r", oid], silent=True)
118+
if output:
119+
return output.strip().splitlines()
120+
return []
121+
122+
123+
def check_missing_dependencies(prs, branch):
124+
"""Check for potential missing dependencies (conflicts)."""
125+
print("\nChecking for potential missing dependencies (conflicts)...")
126+
127+
# Collect OIDs being cherry-picked
128+
cherry_pick_oids = set()
129+
for pr in prs:
130+
if pr.get("mergeCommit"):
131+
cherry_pick_oids.add(pr["mergeCommit"]["oid"])
132+
133+
for pr in prs:
134+
if not pr.get("mergeCommit"):
135+
continue
136+
137+
oid = pr["mergeCommit"]["oid"]
138+
number = pr["number"]
139+
140+
files = get_changed_files(oid)
141+
if not files:
142+
continue
143+
144+
# For each file, find commits that modified it between the target branch and the cherry-picked commit.
145+
# Deduplicate warnings: group affected files by missing commit.
146+
# missing_commits maps: missing_commit_oid -> (title, [list of affected files])
147+
missing_commits = defaultdict(lambda: ("", []))
148+
149+
for filepath in files:
150+
# git log <cherry-pick-commit> --not <target-branch> -- <file>
151+
output = run_command(["git", "log", oid, "--not", branch, "--format=%H %s", "--", filepath], silent=True)
152+
153+
if not output:
154+
continue
155+
156+
for line in output.strip().splitlines():
157+
parts = line.split(" ", 1)
158+
c = parts[0]
159+
title = parts[1] if len(parts) > 1 else ""
160+
161+
if c == oid:
162+
continue
163+
if c not in cherry_pick_oids:
164+
existing_title, existing_files = missing_commits[c]
165+
if not existing_title:
166+
existing_title = title
167+
existing_files.append(filepath)
168+
missing_commits[c] = (existing_title, existing_files)
169+
170+
# Print deduplicated warnings
171+
for missing_oid, (title, affected_files) in missing_commits.items():
172+
files_str = ", ".join(affected_files)
173+
print(
174+
f"WARNING: PR #{number} ({oid}) modifies files that were also changed by commit {missing_oid} ({title}), "
175+
f"which is not in the cherry-pick list. This may indicate missing related changes. Affected files: {files_str}"
176+
)
177+
178+
179+
def main():
180+
parser = argparse.ArgumentParser(description="Generate cherry-pick script from PRs with a specific label.")
181+
parser.add_argument("--label", required=True, help="Label to filter PRs")
182+
parser.add_argument("--output", required=True, help="Output cmd file path")
183+
parser.add_argument("--repo", default="microsoft/onnxruntime", help="Repository (default: microsoft/onnxruntime)")
184+
parser.add_argument(
185+
"--branch", default="HEAD", help="Target branch to compare against for dependency checks (default: HEAD)"
186+
)
187+
parser.add_argument("--limit", type=int, default=200, help="Wait limitation for PR fetching (default: 200)")
188+
args = parser.parse_args()
189+
190+
# Preflight Check
191+
if not check_preflight():
192+
return
193+
194+
# 1. Fetch Merged PRs
195+
prs = get_merged_prs(args.repo, args.label, args.limit)
67196

68197
if not prs:
69198
print(f"No PRs found with label '{args.label}'.")
@@ -72,7 +201,7 @@ def main():
72201
# Sort by mergedAt (ISO 8601 strings sort correctly in chronological order)
73202
prs.sort(key=lambda x: x["mergedAt"])
74203

75-
# Write to output cmd file
204+
# 2. Write Output Script
76205
commit_count = 0
77206
with open(args.output, "w", encoding="utf-8") as f:
78207
f.write("@echo off\n")
@@ -95,84 +224,22 @@ def main():
95224

96225
print(f"Generated {args.output} with {commit_count} commits.")
97226

98-
# Write to markdown file. You can use it as the pull request description.
227+
# 3. Write PR Description Markdown
99228
md_output = "cherry_pick_pr_description.md"
100229
with open(md_output, "w", encoding="utf-8") as f:
101230
f.write("This cherry-picks the following commits for the release:\n")
102231
for pr in prs:
103232
if not pr.get("mergeCommit"):
104233
continue
105234
number = pr["number"]
106-
f.write(f"- #{number}\n")
235+
title = pr["title"]
236+
# Markdown link format: - #123 Title
237+
f.write(f"- #{number} {title}\n")
107238

108239
print(f"Generated {md_output} with {commit_count} commits.")
109240

110-
# Check for potential missing dependencies
111-
print("\nChecking for potential missing dependencies (conflicts)...")
112-
113-
# Collect OIDs being cherry-picked
114-
cherry_pick_oids = set()
115-
for pr in prs:
116-
if pr.get("mergeCommit"):
117-
cherry_pick_oids.add(pr["mergeCommit"]["oid"])
118-
119-
for pr in prs:
120-
if not pr.get("mergeCommit"):
121-
continue
122-
123-
oid = pr["mergeCommit"]["oid"]
124-
number = pr["number"]
125-
126-
# Get files changed by this commit
127-
try:
128-
res = subprocess.run(
129-
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", oid],
130-
capture_output=True,
131-
text=True,
132-
check=True,
133-
)
134-
files = res.stdout.strip().splitlines()
135-
except subprocess.CalledProcessError as e:
136-
print(f"Error getting changed files for {oid}: {e}", file=sys.stderr)
137-
continue
138-
139-
# For each file, find commits that modified it between the target branch and the cherry-picked commit.
140-
# Deduplicate warnings: group affected files by missing commit.
141-
# missing_commits maps: missing_commit_oid -> (title, [list of affected files])
142-
missing_commits = defaultdict(lambda: ("", []))
143-
for filepath in files:
144-
try:
145-
res = subprocess.run(
146-
["git", "log", oid, "--not", args.branch, "--format=%H %s", "--", filepath],
147-
capture_output=True,
148-
text=True,
149-
check=True,
150-
)
151-
for line in res.stdout.strip().splitlines():
152-
parts = line.split(" ", 1)
153-
c = parts[0]
154-
title = parts[1] if len(parts) > 1 else ""
155-
156-
if c == oid:
157-
continue
158-
if c not in cherry_pick_oids:
159-
existing_title, existing_files = missing_commits[c]
160-
if not existing_title:
161-
existing_title = title
162-
existing_files.append(filepath)
163-
missing_commits[c] = (existing_title, existing_files)
164-
165-
except subprocess.CalledProcessError as e:
166-
print(f"Error checking history for {filepath}: {e}", file=sys.stderr)
167-
continue
168-
169-
# Print deduplicated warnings
170-
for missing_oid, (title, affected_files) in missing_commits.items():
171-
files_str = ", ".join(affected_files)
172-
print(
173-
f"WARNING: PR #{number} ({oid}) depends on commit {missing_oid} ({title}) "
174-
f"which is not in the cherry-pick list. Affected files: {files_str}"
175-
)
241+
# 4. Dependency Check
242+
check_missing_dependencies(prs, args.branch)
176243

177244

178245
if __name__ == "__main__":

0 commit comments

Comments
 (0)