|
18 | 18 | from revup.types import (
|
19 | 19 | GitCommitHash,
|
20 | 20 | GitConflictException,
|
| 21 | + GitTreeHash, |
21 | 22 | RevupConflictException,
|
22 | 23 | RevupUsageException,
|
23 | 24 | )
|
@@ -256,6 +257,9 @@ class TopicStack:
|
256 | 257 | # Whether populate() was successfully called
|
257 | 258 | populated: bool = False
|
258 | 259 |
|
| 260 | + # Whether to work around github issues with reordering by pushing a dummy commit |
| 261 | + use_reordering_workaround = False |
| 262 | + |
259 | 263 | def all_reviews_iter(self) -> Iterator[Tuple[str, Topic, str, Review]]:
|
260 | 264 | """
|
261 | 265 | One liner for common iteration pattern to reduce indentation a bit.
|
@@ -699,6 +703,7 @@ async def mark_rebases(self, skip_rebase: bool) -> None:
|
699 | 703 | changes that are already merged, or where push can be skipped due to being rebases or
|
700 | 704 | being identical.
|
701 | 705 | """
|
| 706 | + num_reordered_changes = 0 |
702 | 707 | for _, topic, base_branch, review in self.all_reviews_iter():
|
703 | 708 | # If the relative branch already merged, reset the remote base directly to the base
|
704 | 709 | # branch.
|
@@ -744,6 +749,19 @@ async def mark_rebases(self, skip_rebase: bool) -> None:
|
744 | 749 | # recreate the pr (but warn anyway).
|
745 | 750 | review.status = PrStatus.NEW
|
746 | 751 |
|
| 752 | + if review.pr_info is not None and review.remote_base != review.pr_info.baseRef: |
| 753 | + logging.debug( |
| 754 | + f"Retargeting pr {review.remote_head} from {review.pr_info.baseRef}" |
| 755 | + f" to {review.remote_base}" |
| 756 | + ) |
| 757 | + num_reordered_changes += 1 |
| 758 | + if num_reordered_changes > 1: |
| 759 | + # The logic for correctly detecting whether changes have been reordered can be |
| 760 | + # complex. To simplify, we'll apply the workaround if at least 2 PRs had a base |
| 761 | + # change. This is relatively rare, and the only cost is another 1s spent on an |
| 762 | + # extra git push operation. |
| 763 | + self.use_reordering_workaround = True |
| 764 | + |
747 | 765 | if review.pr_info is None:
|
748 | 766 | # This is a new pr, no need to check patch ids
|
749 | 767 | review.is_pure_rebase = False
|
@@ -1003,7 +1021,23 @@ async def push_git_refs(self, uploader: str, create_local_branches: bool) -> Non
|
1003 | 1021 | if review.push_status != PushStatus.PUSHED or review.status == PrStatus.MERGED:
|
1004 | 1022 | continue
|
1005 | 1023 |
|
1006 |
| - push_targets.append(f"{review.new_commits[-1]}:refs/heads/{review.remote_head}") |
| 1024 | + commit_to_push = review.new_commits[-1] |
| 1025 | + if self.use_reordering_workaround: |
| 1026 | + # When reordering a relative series of PRs, github isn't able to handle the push, |
| 1027 | + # which happens via git, atomically with the api update, which happens through |
| 1028 | + # http. As a result github always sees the push happen first with the old relative |
| 1029 | + # structure. When reordering, a PR that is being moved forward might briefly look |
| 1030 | + # like it contains no new commits, causing github to either mark it merged or |
| 1031 | + # closed. To prevent this, we add an empty dummy commit onto the branch before |
| 1032 | + # pushing, do the update, then push once more to remove the dummy commit. This |
| 1033 | + # only happens when we detect that it is needed, so does not add much overhead. |
| 1034 | + dummy_commit = git.CommitHeader( |
| 1035 | + GitTreeHash(f"{commit_to_push}^{{tree}}"), [commit_to_push] |
| 1036 | + ) |
| 1037 | + dummy_commit.commit_msg = "Revup dummy commit to work around reordering issues" |
| 1038 | + commit_to_push = await self.git_ctx.commit_tree(dummy_commit) |
| 1039 | + |
| 1040 | + push_targets.append(f"{commit_to_push}:refs/heads/{review.remote_head}") |
1007 | 1041 |
|
1008 | 1042 | if create_local_branches:
|
1009 | 1043 | await self.git_ctx.git(
|
|
0 commit comments