Skip to content

Commit 2d1d3d3

Browse files
Highlight PRs that overlap on the same file in pr_file_map.py (#15321)
Add a 'files touched by more than one open PR' section (sorted with the most-contested files first) so overlapping PRs -- the likely merge-conflict hot spots -- are visible at a glance when deciding what to land. Also report two distinct file totals: 'file touches' (every PR x file pair) and 'distinct files touched'. Only the distinct total equals existing + missing, which fixes the earlier single count that double-counted files edited by multiple PRs.
1 parent 8745154 commit 2d1d3d3

1 file changed

Lines changed: 47 additions & 15 deletions

File tree

scripts/pr_file_map.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@
66
and, for each file touched by any open PR, which PR number(s) touch it.
77
88
Output is GitHub-flavored Markdown that includes this script's path, the
9-
current UTC datetime, and summary counts for open PRs, file entries, and
10-
existing/missing files. It then renders a sorted list of files that currently
11-
exist in the working directory, each with its modifying PR numbers, followed
12-
by a separate section for files referenced by open PRs but that do not exist
13-
in the working directory (e.g. deleted, renamed, or on a branch not checked
14-
out locally).
9+
current UTC datetime, and summary counts for open PRs, file touches, distinct
10+
files, and existing/missing files. It highlights files touched by more than one
11+
open PR first (the likely merge-conflict hot spots when landing PRs), then
12+
renders a sorted list of files that currently exist in the working directory,
13+
each with its modifying PR numbers, followed by a separate section for files
14+
referenced by open PRs but that do not exist in the working directory (e.g.
15+
deleted, renamed, or on a branch not checked out locally).
16+
17+
Two file totals are reported because they answer different questions:
18+
- "file touches" counts every (PR, file) pair, so a file edited by three open
19+
PRs contributes three touches; and
20+
- "distinct files" counts each touched path once.
21+
Only the distinct total equals `existing + missing`, since those are deduped.
1522
1623
Run status is also written to stderr with:
1724
- Number of PRs from `get_open_prs()`
18-
- Number of files from `get_pr_files()`
25+
- Number of file touches from `get_pr_files()` and distinct files touched
1926
- Number of existing and missing files
2027
2128
Requirements: gh (GitHub CLI), authenticated (`gh auth login`)
@@ -85,26 +92,36 @@ def main() -> None:
8592
print(f"PR count from get_open_prs(): {pr_count}", file=sys.stderr)
8693

8794
file_to_prs: dict[str, list[int]] = defaultdict(list)
88-
file_count = 0
95+
touch_count = 0 # every (PR, file) pair; a file may be touched by many PRs
8996

9097
for pr in prs:
9198
pr_number = pr["number"]
9299
pr_files = get_pr_files(pr_number)
93-
file_count += len(pr_files)
100+
touch_count += len(pr_files)
94101
for path in pr_files:
95102
file_to_prs[path].append(pr_number)
96-
print(f"File count from get_pr_files(): {file_count}", file=sys.stderr)
103+
distinct_count = len(file_to_prs)
104+
print(
105+
f"File touches from get_pr_files(): {touch_count} "
106+
f"across {distinct_count} distinct files",
107+
file=sys.stderr,
108+
)
97109

98110
existing: dict[str, list[int]] = {}
99111
missing: dict[str, list[int]] = {}
112+
contested: dict[str, list[int]] = {}
100113

101114
for path, pr_numbers in file_to_prs.items():
115+
deduped = sorted(set(pr_numbers))
102116
target = existing if Path(path).exists() else missing
103-
target[path] = sorted(set(pr_numbers))
117+
target[path] = deduped
118+
if len(deduped) > 1:
119+
contested[path] = deduped
104120
existing_count = len(existing)
105121
missing_count = len(missing)
106122
print(
107-
f"Existing files: {existing_count}, Missing files: {missing_count}",
123+
f"Existing files: {existing_count}, Missing files: {missing_count}, "
124+
f"Contested files: {len(contested)}",
108125
file=sys.stderr,
109126
)
110127

@@ -113,12 +130,27 @@ def main() -> None:
113130
print(f"- Script: `{Path(__file__).resolve()}`")
114131
print(f"- Generated (UTC): `{datetime.now(UTC).isoformat()}`")
115132
print(f"- Number of PRs: `{pr_count}`")
116-
print(f"- Number of files: `{file_count}`")
133+
print(f"- File touches (PR x file): `{touch_count}`")
134+
print(f"- Distinct files touched: `{distinct_count}`")
135+
print(f"- Files touched by more than one PR: `{len(contested)}`")
117136
if pr_count == 0:
118-
print("No open pull requests found.")
137+
print("\nNo open pull requests found.")
119138
return
120139

121-
print(f"## `{existing_count}` existing files\n")
140+
print(
141+
f"\n## `{len(contested)}` files touched by more than one open PR "
142+
"(possible merge conflicts)\n"
143+
)
144+
if contested:
145+
print("Coordinate, rebase, or land these together to avoid conflicts.\n")
146+
# Hot spots first: most-contested files, then alphabetical.
147+
for path in sorted(contested, key=lambda p: (-len(contested[p]), p)):
148+
pr_list = " ".join(f"#{n}" for n in contested[path])
149+
print(f"- `{path}` ({len(contested[path])} PRs): {pr_list}")
150+
else:
151+
print("_None -- no open PRs overlap on the same file._")
152+
153+
print(f"\n## `{existing_count}` existing files\n")
122154
if existing:
123155
for path in sorted(existing):
124156
pr_list = " ".join(f"#{n}" for n in existing[path])

0 commit comments

Comments
 (0)