Skip to content

Commit 1047d7e

Browse files
committed
Partially switch to atomic multi-branch push
Pull Request: #29 (main)
1 parent f4b9d48 commit 1047d7e

File tree

1 file changed

+97
-34
lines changed

1 file changed

+97
-34
lines changed

git-grok

Lines changed: 97 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)