@@ -86,6 +86,16 @@ class Pr:
86
86
labels : list [str ]
87
87
88
88
89
+ #
90
+ # A pair of commit hash and branch name.
91
+ #
92
+ @dataclass
93
+ class Branch :
94
+ hash : str
95
+ branch : str
96
+ push_result : BranchPushResult | None = None
97
+
98
+
89
99
#
90
100
# Some data passed from the main process to each individual child process call
91
101
# within "git rebase -i", for each commit in the stack.
@@ -127,6 +137,7 @@ class Main:
127
137
login : str
128
138
remote : str
129
139
remote_base_branch : str = "master"
140
+ in_rebase_interactive : InRebaseInteractiveData | None = None
130
141
131
142
#
132
143
# Main entry point.
@@ -151,14 +162,14 @@ class Main:
151
162
)
152
163
args = parser .parse_args ()
153
164
154
- in_rebase_interactive = InRebaseInteractiveData .parse (
165
+ self . in_rebase_interactive = InRebaseInteractiveData .parse (
155
166
os .environ .get (INTERNAL_IN_REBASE_INTERACTIVE_VAR , "" )
156
167
)
157
168
158
169
self .debug = args .debug
159
170
self .debug_force_push_branches = args .debug_force_push_branches
160
171
161
- if not in_rebase_interactive :
172
+ if not self . in_rebase_interactive :
162
173
self .gh_verify_version ()
163
174
self .git_verify_version ()
164
175
self .self_update ()
@@ -179,10 +190,10 @@ class Main:
179
190
"git_get_current_remote_base_branch" ,
180
191
self .git_get_current_remote_base_branch ,
181
192
)
182
- if not in_rebase_interactive :
193
+ if not self . in_rebase_interactive :
183
194
self .run_all ()
184
195
else :
185
- self .run_in_rebase_interactive (data = in_rebase_interactive )
196
+ self .run_in_rebase_interactive (data = self . in_rebase_interactive )
186
197
187
198
#
188
199
# Assuming all PRs in the stack already have PR URLs in their description,
@@ -227,6 +238,8 @@ class Main:
227
238
commit_with_no_url = commit
228
239
break
229
240
else :
241
+ # Push this branch and see whether GitHub says whether it
242
+ # was up to date or not.
230
243
commit_hashes_to_push_branch .append (commit .hash )
231
244
232
245
# Some commits have no related PRs (no GitHub URLs in the message)?
@@ -270,23 +283,36 @@ class Main:
270
283
271
284
self .print_header (f"Processing commit: { self .clean_title (commit .title )} " )
272
285
286
+ to_push : list [Branch ] = []
287
+
273
288
if prev_commit .hash != remote_commit .hash :
274
- prev_commit , result = self .process_commit_push_branch (commit = prev_commit )
289
+ prev_commit .branch = self .process_commit_infer_branch (commit = prev_commit )
290
+ to_push .append (Branch (hash = prev_commit .hash , branch = prev_commit .branch ))
291
+ else :
292
+ prev_commit .branch = None
293
+
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 :
275
300
self .print_branch_result (
276
301
type = "base" ,
277
- branch = str ( prev_commit .branch ) ,
278
- result = result ,
302
+ branch = prev_commit .branch ,
303
+ result = push_results [ prev_commit . branch ] ,
279
304
)
280
305
else :
281
306
self .print_branch_result (
282
307
type = "base" ,
283
308
branch = self .remote_base_branch ,
284
309
result = "up-to-date" ,
285
310
)
286
- prev_commit .branch = None
287
-
288
- commit , result = self .process_commit_push_branch (commit = commit )
289
- self .print_branch_result (type = "head" , branch = str (commit .branch ), result = result )
311
+ self .print_branch_result (
312
+ type = "head" ,
313
+ branch = commit .branch ,
314
+ result = push_results [commit .branch ],
315
+ )
290
316
291
317
new_pr_title = commit .title
292
318
new_pr_body = None
@@ -342,11 +368,11 @@ class Main:
342
368
commits : list [Commit ],
343
369
prs_by_url : dict [str , Pr ],
344
370
):
345
- commits_chronological = list (reversed (commits ))
346
- for i , commit in list (enumerate (commits_chronological ))[1 :]:
371
+ commits_old_to_new = list (reversed (commits ))
372
+ for i , commit in list (enumerate (commits_old_to_new ))[1 :]:
347
373
pr = prs_by_url .get (commit .url , None ) if commit .url else None
348
374
if pr and pr .auto_merge_status == "ENABLED" :
349
- prev_commit = commits_chronological [i - 1 ]
375
+ prev_commit = commits_old_to_new [i - 1 ]
350
376
prev_pr = (
351
377
prs_by_url .get (prev_commit .url , None ) if prev_commit .url else None
352
378
)
@@ -426,25 +452,35 @@ class Main:
426
452
):
427
453
# We must iterate from the oldest commit to the newest one, because
428
454
# previous commit PR's branch becomes the next commit PR's base branch.
429
- commits_chronological = list (reversed (commits ))
430
- for i , commit in enumerate (commits_chronological ):
455
+ commits_old_to_new = list (reversed (commits ))
456
+
457
+ # Push all branches atomically, in bulk. This is meant to prevent
458
+ # useless CODEOWNERS reviewers addition in case the commits were
459
+ # reordered or rebased, and is faster in general.
460
+ to_push : list [Branch ] = []
461
+ for commit in commits_old_to_new :
462
+ if (
463
+ self .debug_force_push_branches
464
+ or commit .hash in commit_hashes_to_push_branch
465
+ ):
466
+ commit .branch = self .process_commit_infer_branch (commit = commit )
467
+ to_push .append (Branch (hash = commit .hash , branch = commit .branch ))
468
+ push_results = self .git_push_branches (branches = to_push )
469
+
470
+ for i , commit in enumerate (commits_old_to_new ):
431
471
self .print_header (f"Updating PR: { self .clean_title (commit .title )} " )
432
472
433
- if commit .hash in commit_hashes_to_push_branch :
434
- commit , result = self . process_commit_push_branch ( commit = commit )
473
+ if commit .branch in push_results :
474
+ result = push_results [ commit . branch ]
435
475
if result == "pushed" :
436
476
self .print_branch_result (
437
477
type = "head" ,
438
- branch = str ( commit .branch ) ,
478
+ branch = commit .branch ,
439
479
result = result ,
440
480
)
441
- commits_chronological [i ] = commit
442
481
443
- assert (
444
- commit .url is not None
445
- ), f"commit { commit .hash } PR url is expected to be in the message at this point"
446
482
pr , result = self .process_update_pr (
447
- prev_commit = commits_chronological [i - 1 ] if i > 0 else None ,
483
+ prev_commit = commits_old_to_new [i - 1 ] if i > 0 else None ,
448
484
commit = commit ,
449
485
commits = commits ,
450
486
)
@@ -453,27 +489,26 @@ class Main:
453
489
result = result ,
454
490
review_decision = pr .review_decision ,
455
491
)
456
- commits_chronological [i ].branch = pr .head_branch
492
+ commits_old_to_new [i ].branch = pr .head_branch
457
493
458
494
#
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.
495
+ # For a commit, infers its corresponding remote branch name by either
496
+ # querying it from the PR (when commit.url is set), or by building it from
497
+ # the commit title and hash.
461
498
#
462
- def process_commit_push_branch (
499
+ def process_commit_infer_branch (
463
500
self ,
464
501
* ,
465
502
commit : Commit ,
466
- ) -> tuple [ Commit , BranchPushResult ] :
503
+ ) -> str :
467
504
if commit .url :
468
- pr = self .gh_get_pr (url = commit .url )
469
- commit . branch = pr .head_branch
505
+ pr = self .gh_get_pr (url = commit .url ) # likely a cache hit
506
+ return pr .head_branch
470
507
else :
471
- commit . branch = self .build_branch_name (
508
+ return self .build_branch_name (
472
509
title = commit .title ,
473
510
commit_hash = commit .hash ,
474
511
)
475
- pushed = self .git_push_branch (branch = commit .branch , hash = commit .hash )
476
- return commit , pushed
477
512
478
513
#
479
514
# Updates PR fields:
@@ -864,9 +899,16 @@ class Main:
864
899
return commits
865
900
866
901
#
867
- # Pushes a branch to remote GitHub.
902
+ # Pushes multiple branches atomically to remote GitHub. Returns a dict
903
+ # mapping branch names to their push results.
868
904
#
869
- def git_push_branch (self , * , branch : str , hash : str ) -> BranchPushResult :
905
+ def git_push_branches (
906
+ self ,
907
+ * ,
908
+ branches : list [Branch ],
909
+ ) -> dict [str , BranchPushResult ]:
910
+ if not branches :
911
+ return {}
870
912
# Git push is a quick no-op on GitHub end if the branch isn't changed
871
913
# (it prints "Everything up-to-date"), so we always push and then verify
872
914
# the output for the status (instead of fetching from the remote and
@@ -877,15 +919,26 @@ class Main:
877
919
"push" ,
878
920
"-f" ,
879
921
self .remote ,
880
- f"{ hash } :refs/heads/{ branch } " ,
922
+ * [ f"{ branch . hash } :refs/heads/{ branch . branch } " for branch in branches ] ,
881
923
],
882
924
stderr_to_stdout = True ,
883
925
)
884
- return (
885
- "up-to-date"
886
- if re .match (r"^[^\n]+up-to-date" , out , flags = re .S )
887
- else "pushed"
888
- )
926
+ # If the hash is NOT mentioned in the output, it's either a short
927
+ # "Everything up-to-date" message (which means that ALL branches are
928
+ # unchanged), or THIS particular branch is up-to-date. I.e. if a branch
929
+ # is changed, git always prints its hash in the output, on one of the
930
+ # following formats:
931
+ # 1. * [new branch] 10dc4f6 -> grok/...
932
+ # 2. + 10dc4f6...b28d03e 10dc4f6 -> grok/... (forced update)
933
+ results : dict [str , BranchPushResult ] = {
934
+ branch .branch : "up-to-date" for branch in branches
935
+ }
936
+ for branch in branches :
937
+ for line in out .splitlines ():
938
+ if branch .hash in line :
939
+ results [branch .branch ] = "pushed"
940
+ break
941
+ return results
889
942
890
943
#
891
944
# Runs an interactive rebase with the provided shell command.
@@ -1147,7 +1200,7 @@ class Main:
1147
1200
# Prints a status after a commit message was updated locally.
1148
1201
#
1149
1202
def print_commit_message_updated (self ):
1150
- print (f" ── updated commit message" )
1203
+ print (f" ── added PR URL to commit's message to bind them " )
1151
1204
sys .stdout .flush ()
1152
1205
1153
1206
#
@@ -1282,7 +1335,15 @@ class Main:
1282
1335
def debug_log_text (self , * , text : str ):
1283
1336
try :
1284
1337
with open (DEBUG_FILE , "a+" ) as file :
1285
- file .write (f"=== { datetime .now ()} \n " )
1338
+ file .write (
1339
+ f"=== { datetime .now ()} "
1340
+ + (
1341
+ f" (in rebase interactive, { self .in_rebase_interactive .commit_index_one_based } /{ self .in_rebase_interactive .total_commits_in_stack } )"
1342
+ if self .in_rebase_interactive
1343
+ else ""
1344
+ )
1345
+ + "\n "
1346
+ )
1286
1347
file .write (f"{ text .rstrip ()} \n " )
1287
1348
file .write ("\n " )
1288
1349
except :
0 commit comments