66and, for each file touched by any open PR, which PR number(s) touch it.
77
88Output 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
1623Run 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
2128Requirements: 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 ("\n No 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