Skip to content

Commit 40d41b0

Browse files
committed
Update (base update)
[ghstack-poisoned]
1 parent 7510f8c commit 40d41b0

File tree

2 files changed

+157
-0
lines changed

2 files changed

+157
-0
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import argparse
8+
import os
9+
import re
10+
11+
from typing import List
12+
13+
# Provided by the PyGithub pip package.
14+
from github import Auth, Github
15+
from github.Repository import Repository
16+
17+
18+
def parse_args():
19+
parser = argparse.ArgumentParser(
20+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
21+
)
22+
parser.add_argument(
23+
"--repo",
24+
type=str,
25+
help='The github repo to modify: e.g. "pytorch/executorch".',
26+
required=True,
27+
)
28+
parser.add_argument(
29+
"--pr",
30+
type=int,
31+
help="Number of the PR in the stack to check and create corresponding PR",
32+
required=True,
33+
)
34+
return parser.parse_args()
35+
36+
37+
def extract_stack_from_body(pr_body: str) -> List[int]:
38+
"""Extracts a list of PR numbers from a ghexport-generated PR body.
39+
40+
The base of the stack is in index 0.
41+
"""
42+
43+
# Expected format. The `__->__` could appear on any line. Stop parsing
44+
# after the blank line. This would return [1, 2, 3].
45+
"""
46+
Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at bottom):
47+
* #3
48+
* __->__ #2
49+
* #1
50+
51+
<PR description details>
52+
"""
53+
54+
prs = []
55+
ghstack_begin = "Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at bottom):"
56+
ghstack_begin_seen = False
57+
for line in pr_body.splitlines():
58+
if ghstack_begin in line:
59+
ghstack_begin_seen = True
60+
if not ghstack_begin_seen:
61+
continue
62+
match = re.match(r"\*(?:.*?)? #(\d+)", line)
63+
if match:
64+
# It's a bullet followed by an integer.
65+
prs.append(int(match.group(1)))
66+
return list(reversed(prs))
67+
68+
69+
def get_pr_stack_from_number(pr_number: int, repo: Repository) -> List[int]:
70+
pr_stack = extract_stack_from_body(repo.get_pull(pr_number).body)
71+
72+
if not pr_stack:
73+
raise Exception(
74+
f"Could not find PR stack in body of #{pr_number}. "
75+
+ "Please make sure that the PR was created with ghstack."
76+
)
77+
78+
return pr_stack
79+
80+
81+
def create_prs_for_orig_branch(pr_stack: List[int], repo: Repository):
82+
# For the first PR, we want to merge to `main` branch, and we will update
83+
# as we go through the stack
84+
orig_branch_merge_base = "main"
85+
for i in range(len(pr_stack)):
86+
pr = repo.get_pull(pr_stack[i])
87+
if not pr.is_merged():
88+
print("The PR (and stack above) is not merged yet, skipping")
89+
# TODO: Must return
90+
# Check for invariant: For the current PR, it must be gh/user/x/base <- gh/user/x/head
91+
assert pr.base.ref.replace("base", "head") == pr.head.ref
92+
# The PR we want to create is then "branch_to_merge" <- gh/user/x/orig
93+
# gh/user/x/orig is the clean diff between gh/user/x/base <- gh/user/x/head
94+
orig_branch_merge_head = pr.base.ref.replace("base", "orig")
95+
bot_metadata = f"""This PR was created by the merge bot to help merge the original PR into the main branch.
96+
ghstack PR number: https://github.com/pytorch/executorch/pull/{pr.number}
97+
^ Please use this as the source of truth for the PR number to reference in comments
98+
ghstack PR base: https://github.com/pytorch/executorch/tree/{pr.base.ref}
99+
ghstack PR head: https://github.com/pytorch/executorch/tree/{pr.head.ref}
100+
Merge bot PR base: https://github.com/pytorch/executorch/tree/{orig_branch_merge_base}
101+
Merge bot PR head: https://github.com/pytorch/executorch/tree/{orig_branch_merge_head}
102+
\nOriginal PR body:\n
103+
"""
104+
105+
existing_orig_pr = repo.get_pulls(head="pytorch:" + orig_branch_merge_head, base=orig_branch_merge_base, state="open")
106+
if existing_orig_pr.totalCount > 0:
107+
print(f"PR for {orig_branch_merge_head} already exists {existing_orig_pr[0]}")
108+
# We don't need to create/edit because the head PR is merged and orig is finalized.
109+
else:
110+
repo.create_pull(
111+
base=orig_branch_merge_base,
112+
head=orig_branch_merge_head,
113+
title=pr.title,
114+
body=bot_metadata + pr.body,
115+
)
116+
# Advance the base for the next PR
117+
orig_branch_merge_base = orig_branch_merge_head
118+
119+
120+
def main():
121+
args = parse_args()
122+
123+
with Github(auth=Auth.Token(os.environ["GITHUB_TOKEN"])) as gh:
124+
repo = gh.get_repo(args.repo)
125+
create_prs_for_orig_branch(get_pr_stack_from_number(args.pr, repo), repo)
126+
127+
128+
if __name__ == "__main__":
129+
main()

.github/workflows/ghstack_land.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Propose to merge ghstack orig PRs to main
2+
on:
3+
pull_request:
4+
types: [opened, synchronize, closed]
5+
branches:
6+
- 'gh/*/[0-9]+/base'
7+
jobs:
8+
ghstack_merge_to_main:
9+
name: Try to create a PR with ghstack /orig branch
10+
runs-on: ubuntu-22.04
11+
environment: cherry-pick-bot
12+
steps:
13+
- uses: actions/checkout@v3
14+
with:
15+
fetch-depth: '0'
16+
- uses: actions/setup-python@v4
17+
with:
18+
python-version: '3.10'
19+
- name: Try to merge PR to main
20+
run: |
21+
pip install pygithub
22+
23+
PR_NUMBER=$(echo "$GITHUB_REF" | grep -oE '[0-9]+')
24+
25+
python .github/scripts/propose_ghstack_orig_pr.py --pr $PR_NUMBER --repo pytorch/executorch
26+
env:
27+
GITHUB_TOKEN: ${{ secrets.GH_PYTORCHBOT_CHERRY_PICK_TOKEN }}
28+
GITHUB_REF: ${{ github.ref }}

0 commit comments

Comments
 (0)