@@ -86,6 +86,15 @@ class Pr:
8686 labels : list [str ]
8787
8888
89+ #
90+ # A pair of commit hash and branch name.
91+ #
92+ @dataclass
93+ class Branch :
94+ hash : str
95+ branch : str
96+
97+
8998#
9099# Some data passed from the main process to each individual child process call
91100# within "git rebase -i", for each commit in the stack.
@@ -228,8 +237,8 @@ class Main:
228237 commit_with_no_url = commit
229238 break
230239 else :
231- # Push this branch and see whether GitHub says whether it
232- # was up to date or not.
240+ # Push this branch and see whether GitHub says that it was
241+ # up to date or not.
233242 commit_hashes_to_push_branch .append (commit .hash )
234243
235244 # Some commits have no related PRs (no GitHub URLs in the message)?
@@ -261,6 +270,11 @@ class Main:
261270 # Runs the sync for only the top commit. This is an internal command which
262271 # is used during interactive rebase.
263272 #
273+ # This workflow runs only when the tool finds a commit that has no PR URL in
274+ # its description yet. If all commits have existing PR URLs (including the
275+ # case when they were reordered), the interactive rebase workflow is not
276+ # run; instead, the branches are just pushed, and the PRs are updated.
277+ #
264278 def run_in_rebase_interactive (self , * , data : InRebaseInteractiveData ):
265279 remote_commit = self .git_get_commits (
266280 latest_ref = f"remotes/{ self .remote } /{ self .remote_base_branch } " ,
@@ -273,23 +287,33 @@ class Main:
273287
274288 self .print_header (f"Processing commit: { self .clean_title (commit .title )} " )
275289
290+ to_push : list [Branch ] = []
276291 if prev_commit .hash != remote_commit .hash :
277- prev_commit , result = self .process_commit_push_branch (commit = prev_commit )
292+ prev_commit .branch = self .process_commit_infer_branch (commit = prev_commit )
293+ to_push .append (Branch (hash = prev_commit .hash , branch = prev_commit .branch ))
294+ commit .branch = self .process_commit_infer_branch (commit = commit )
295+ to_push .append (Branch (hash = commit .hash , branch = commit .branch ))
296+
297+ push_results = self .git_push_branches (branches = to_push )
298+
299+ if prev_commit .branch in push_results :
278300 self .print_branch_result (
279301 type = "base" ,
280- branch = str ( prev_commit .branch ) ,
281- result = result ,
302+ branch = prev_commit .branch ,
303+ result = push_results [ prev_commit . branch ] ,
282304 )
283305 else :
284306 self .print_branch_result (
285307 type = "base" ,
286308 branch = self .remote_base_branch ,
287309 result = "up-to-date" ,
288310 )
289- prev_commit .branch = None
290311
291- commit , result = self .process_commit_push_branch (commit = commit )
292- self .print_branch_result (type = "head" , branch = str (commit .branch ), result = result )
312+ self .print_branch_result (
313+ type = "head" ,
314+ branch = commit .branch ,
315+ result = push_results [commit .branch ],
316+ )
293317
294318 new_pr_title = commit .title
295319 new_pr_body = None
@@ -430,18 +454,38 @@ class Main:
430454 # We must iterate from the oldest commit to the newest one, because
431455 # previous commit PR's branch becomes the next commit PR's base branch.
432456 commits_old_to_new = list (reversed (commits ))
457+
458+ # Collect commits/branches that need to be pushed.
459+ to_push : dict [str , Branch ] = {}
460+ for commit in commits_old_to_new :
461+ if (
462+ self .debug_force_push_branches
463+ or commit .hash in commit_hashes_to_push_branch
464+ ):
465+ commit .branch = self .process_commit_infer_branch (commit = commit )
466+ to_push [commit .branch ] = Branch (hash = commit .hash , branch = commit .branch )
467+
468+ push_results : dict [str , BranchPushResult ] = {}
469+
433470 for i , commit in enumerate (commits_old_to_new ):
434471 self .print_header (f"Updating PR: { self .clean_title (commit .title )} " )
435472
436- if commit .hash in commit_hashes_to_push_branch :
437- commit , result = self .process_commit_push_branch (commit = commit )
438- if result == "pushed" :
439- self .print_branch_result (
440- type = "head" ,
441- branch = str (commit .branch ),
442- result = result ,
443- )
444- commits_old_to_new [i ] = commit
473+ # Push branches sequentially, one by one, not in bulk. This is
474+ # because when bulk-pushing after local commits reordering, GitHub
475+ # may mark some PRs as merged and close them (this behavior is
476+ # covered with a unit test).
477+ if commit .branch in to_push :
478+ push_results .update (
479+ self .git_push_branches (branches = [to_push [commit .branch ]])
480+ )
481+
482+ result = push_results .get (str (commit .branch ), None )
483+ if result == "pushed" :
484+ self .print_branch_result (
485+ type = "head" ,
486+ branch = str (commit .branch ),
487+ result = result ,
488+ )
445489
446490 pr , result = self .process_update_pr (
447491 prev_commit = commits_old_to_new [i - 1 ] if i > 0 else None ,
@@ -456,24 +500,23 @@ class Main:
456500 commits_old_to_new [i ].branch = pr .head_branch
457501
458502 #
459- # Pushes an existing branch (it we know this commit's PR URL by querying
460- # GitHub), or creates a new branch based on commit title and pushes it.
503+ # For a commit, infers its corresponding remote branch name by either
504+ # querying it from the PR (when commit.url is set), or by building it from
505+ # the commit title and hash.
461506 #
462- def process_commit_push_branch (
507+ def process_commit_infer_branch (
463508 self ,
464509 * ,
465510 commit : Commit ,
466- ) -> tuple [ Commit , BranchPushResult ] :
511+ ) -> str :
467512 if commit .url :
468- pr = self .gh_get_pr (url = commit .url )
469- commit . branch = pr .head_branch
513+ pr = self .gh_get_pr (url = commit .url ) # likely a cache hit
514+ return pr .head_branch
470515 else :
471- commit . branch = self .build_branch_name (
516+ return self .build_branch_name (
472517 title = commit .title ,
473518 commit_hash = commit .hash ,
474519 )
475- pushed = self .git_push_branch (branch = commit .branch , hash = commit .hash )
476- return commit , pushed
477520
478521 #
479522 # Updates PR fields:
@@ -796,6 +839,8 @@ class Main:
796839 # Returns parsed commits between two refs in reverse-chronological order
797840 # (newest commits first, oldest last, as they're shown in "git log").
798841 #
842+ # The returned commits will have branch = None, since it's unknown yet.
843+ #
799844 def git_get_commits (
800845 self ,
801846 * ,
@@ -860,9 +905,16 @@ class Main:
860905 return commits
861906
862907 #
863- # Pushes a branch to remote GitHub.
908+ # Pushes multiple branches atomically to remote GitHub. Returns a dict
909+ # mapping branch names to their push results.
864910 #
865- def git_push_branch (self , * , branch : str , hash : str ) -> BranchPushResult :
911+ def git_push_branches (
912+ self ,
913+ * ,
914+ branches : list [Branch ],
915+ ) -> dict [str , BranchPushResult ]:
916+ if not branches :
917+ return {}
866918 # Git push is a quick no-op on GitHub end if the branch isn't changed
867919 # (it prints "Everything up-to-date"), so we always push and then verify
868920 # the output for the status (instead of fetching from the remote and
@@ -873,15 +925,26 @@ class Main:
873925 "push" ,
874926 "-f" ,
875927 self .remote ,
876- f"{ hash } :refs/heads/{ branch } " ,
928+ * [ f"{ branch . hash } :refs/heads/{ branch . branch } " for branch in branches ] ,
877929 ],
878930 stderr_to_stdout = True ,
879931 )
880- return (
881- "up-to-date"
882- if re .match (r"^[^\n]+up-to-date" , out , flags = re .S )
883- else "pushed"
884- )
932+ # If the hash is NOT mentioned in the output, it's either a short
933+ # "Everything up-to-date" message (which means that ALL branches are
934+ # unchanged), or THIS particular branch is up-to-date. I.e. if a branch
935+ # is changed, git always prints its hash in the output, on one of the
936+ # following formats:
937+ # 1. * [new branch] 10dc4f6 -> grok/...
938+ # 2. + 10dc4f6...b28d03e 10dc4f6 -> grok/... (forced update)
939+ results : dict [str , BranchPushResult ] = {
940+ branch .branch : "up-to-date" for branch in branches
941+ }
942+ for branch in branches :
943+ for line in out .splitlines ():
944+ if branch .hash in line :
945+ results [branch .branch ] = "pushed"
946+ break
947+ return results
885948
886949 #
887950 # Runs an interactive rebase with the provided shell command.
0 commit comments