8
from typing import Any, Generator
10
from github_utils import gh_post_pr_comment as gh_post_comment
11
from gitutils import get_git_remote_name, get_git_repo_dir, GitRepo
12
from trymerge import GitHubPR
15
"\n```\nAborting rebase because rebasing the branch resulted in the same sha as the target branch.\n"
16
+ "This usually happens because the PR has already been merged. Please rebase locally and push.\n```"
20
def parse_args() -> Any:
21
from argparse import ArgumentParser
23
parser = ArgumentParser("Rebase PR into branch")
24
parser.add_argument("--dry-run", action="store_true")
25
parser.add_argument("--branch", type=str)
26
parser.add_argument("pr_num", type=int)
27
return parser.parse_args()
30
def post_already_uptodate(
31
pr: GitHubPR, repo: GitRepo, onto_branch: str, dry_run: bool
33
msg = f"Tried to rebase and push PR #{pr.pr_num}, but it was already up to date."
34
def_branch = pr.default_branch()
35
def_branch_fcn = f"refs/remotes/{repo.remote}/{def_branch}"
36
if onto_branch != def_branch_fcn and repo.rev_parse(
38
) != repo.rev_parse(onto_branch):
39
def_branch_url = f"https://github.com/{pr.org}/{pr.project}/tree/{def_branch}"
40
msg += f" Try rebasing against [{def_branch}]({def_branch_url}) by issuing:"
41
msg += f"\n`@pytorchbot rebase -b {def_branch}`"
53
pr: GitHubPR, repo: GitRepo, onto_branch: str, dry_run: bool = False
55
branch = f"pull/{pr.pr_num}/head"
56
remote_url = f"https://github.com/{pr.info['headRepository']['nameWithOwner']}.git"
57
refspec = f"{branch}:{pr.head_ref()}"
59
repo.fetch(branch, branch)
60
repo._run_git("rebase", onto_branch, branch)
62
if repo.rev_parse(branch) == repo.rev_parse(onto_branch):
63
raise Exception(SAME_SHA_ERROR)
66
push_result = repo._run_git("push", "--dry-run", "-f", remote_url, refspec)
68
push_result = repo._run_git("push", "-f", remote_url, refspec)
69
if "Everything up-to-date" in push_result:
70
post_already_uptodate(pr, repo, onto_branch, dry_run)
77
f"Successfully rebased `{pr.head_ref()}` onto `{onto_branch}`, please pull locally "
78
+ f"before adding more changes (for example, via `git checkout {pr.head_ref()} && "
79
+ "git pull --rebase`)",
85
def rebase_ghstack_onto(
86
pr: GitHubPR, repo: GitRepo, onto_branch: str, dry_run: bool = False
90
[sys.executable, "-m", "ghstack", "--help"],
96
subprocess.run([sys.executable, "-m", "pip", "install", "ghstack"], check=True)
97
orig_ref = f"{re.sub(r'/head$', '/orig', pr.head_ref())}"
99
repo.fetch(orig_ref, orig_ref)
100
repo._run_git("rebase", onto_branch, orig_ref)
102
if repo.rev_parse(orig_ref) == repo.rev_parse(onto_branch):
103
raise Exception(SAME_SHA_ERROR)
106
email = repo._run_git("log", orig_ref, "--pretty=format:%ae", "-1")
107
name = repo._run_git("log", orig_ref, "--pretty=format:%an", "-1")
108
repo._run_git("config", "--global", "user.email", email)
109
repo._run_git("config", "--global", "user.name", name)
111
os.environ["OAUTH_TOKEN"] = os.environ["GITHUB_TOKEN"]
112
with open(".ghstackrc", "w+") as f:
115
+ "github_url=github.com\n"
116
+ "github_username=pytorchmergebot\n"
117
+ "remote_name=origin"
121
print("Don't know how to dry-run ghstack")
124
ghstack_result = subprocess.run(["ghstack"], capture_output=True, check=True)
125
push_result = ghstack_result.stdout.decode("utf-8")
127
if ghstack_result.returncode != 0:
128
print(ghstack_result.stderr.decode("utf-8"))
129
raise Exception(f"\n```{push_result}```")
144
org, project = repo.gh_owner_and_name()
145
for line in push_result.splitlines():
146
if "Updated" in line:
147
pr_num = int(line.split("/")[-1])
148
if pr_num != pr.pr_num:
153
f"Rebased `{orig_ref}` onto `{onto_branch}` because #{pr.pr_num} was rebased, "
154
"please pull locally before adding more changes (for example, via `ghstack "
155
+ f"checkout https://github.com/{org}/{project}/pull/{pr_num}`)",
163
f"Successfully rebased `{orig_ref}` onto `{onto_branch}`, please pull locally "
164
+ "before adding more changes (for example, via `ghstack "
165
+ f"checkout https://github.com/{org}/{project}/pull/{pr.pr_num}`)",
170
f"Skipped https://github.com/{org}/{project}/pull/{pr.pr_num}"
173
post_already_uptodate(pr, repo, onto_branch, dry_run)
178
def additional_rebase_failure_info(e: Exception) -> str:
180
r"remote: Permission to .* denied to .*\.\nfatal: unable to access", str(e)
183
"\nThis is likely because the author did not allow edits from maintainers on the PR or because the "
184
"repo has additional permissions settings that mergebot does not qualify."
189
@contextlib.contextmanager
190
def git_config_guard(repo: GitRepo) -> Generator[None, None, None]:
191
"""Restores user.name and user.email global properties after context is finished"""
192
user_email = repo._run_git("config", "user.email")
193
user_name = repo._run_git("config", "user.name")
198
repo._run_git("config", "--global", "user.email", user_email)
200
repo._run_git("config", "--global", "user.name", user_name)
205
repo = GitRepo(get_git_repo_dir(), get_git_remote_name(), debug=True)
206
org, project = repo.gh_owner_and_name()
208
pr = GitHubPR(org, project, args.pr_num)
209
onto_branch = args.branch if args.branch else pr.default_branch()
210
onto_branch = f"refs/remotes/{repo.remote}/{onto_branch}"
212
f"https://github.com/{org}/{project}/commit/{repo.rev_parse(onto_branch)}"
215
msg = f"@pytorchbot started a rebase job onto [{onto_branch}]({onto_branch_url})."
216
msg += f" Check the current status [here]({os.getenv('GH_RUN_URL')})"
217
gh_post_comment(org, project, args.pr_num, msg, dry_run=args.dry_run)
224
f"PR #{args.pr_num} is closed, won't rebase",
225
dry_run=args.dry_run,
230
if pr.is_ghstack_pr():
231
with git_config_guard(repo):
232
rc = rebase_ghstack_onto(pr, repo, onto_branch, dry_run=args.dry_run)
234
rc = rebase_onto(pr, repo, onto_branch, dry_run=args.dry_run)
235
sys.exit(0 if rc else 1)
237
except Exception as e:
238
msg = f"Rebase failed due to {e}"
239
msg += additional_rebase_failure_info(e)
240
run_url = os.getenv("GH_RUN_URL")
241
if run_url is not None:
242
msg += f"\nRaised by {run_url}"
243
gh_post_comment(org, project, args.pr_num, msg, dry_run=args.dry_run)
246
if __name__ == "__main__":