ghstack 0.10.0__tar.gz → 0.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ghstack-0.10.0 → ghstack-0.12.0}/PKG-INFO +7 -7
- {ghstack-0.10.0 → ghstack-0.12.0}/README.md +5 -5
- {ghstack-0.10.0 → ghstack-0.12.0}/pyproject.toml +2 -2
- ghstack-0.12.0/src/ghstack/cherry_pick.py +65 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/cli.py +23 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/diff.py +2 -2
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/github_real.py +70 -29
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/github_utils.py +36 -13
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/land.py +18 -13
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/submit.py +5 -15
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/test_prelude.py +14 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/__init__.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/__main__.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/action.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/cache.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/checkout.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/circleci.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/circleci_real.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/config.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/forensics.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/git.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/github.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/github_fake.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/github_schema.graphql +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/gpg_sign.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/logs.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/py.typed +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/rage.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/shell.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/status.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/trailers.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/types.py +0 -0
- {ghstack-0.10.0 → ghstack-0.12.0}/src/ghstack/unlink.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ghstack
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
4
4
|
Summary: Stack diff support for GitHub
|
|
5
5
|
Author: Edward Z. Yang
|
|
6
6
|
Author-email: Edward Z. Yang <ezyang@mit.edu>
|
|
@@ -14,7 +14,7 @@ Requires-Dist: requests>=2,<3
|
|
|
14
14
|
Requires-Dist: typing-extensions>=3
|
|
15
15
|
Requires-Dist: click>=8,<9
|
|
16
16
|
Requires-Dist: flake8>=7.0.0,<8.0.0
|
|
17
|
-
Requires-Python: >=3.
|
|
17
|
+
Requires-Python: >=3.9.1, <4.0.0
|
|
18
18
|
Project-URL: repository, https://github.com/ezyang/ghstack
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
|
|
@@ -28,7 +28,7 @@ uv tool install ghstack
|
|
|
28
28
|
|
|
29
29
|
ghstack is tested with
|
|
30
30
|
[several different Python versions](https://github.com/ezyang/ghstack/blob/master/.github/workflows/test.yml#L13).
|
|
31
|
-
It requires at least Python 3.
|
|
31
|
+
It requires at least Python 3.9.1.
|
|
32
32
|
|
|
33
33
|
## How to setup
|
|
34
34
|
|
|
@@ -69,13 +69,13 @@ resolve conflicts in *each* PR, not just for the entire stack.)
|
|
|
69
69
|
**How do I start a new feature?** Just checkout master on a new
|
|
70
70
|
branch, and start working on a fresh branch.
|
|
71
71
|
|
|
72
|
-
**
|
|
73
|
-
normal GitHub UI, as their branch bases won't be master.
|
|
74
|
-
`ghstack land $PR_URL` (or alternatively `ghstack land #PR_NUM`) to land
|
|
72
|
+
**How do I merge my changes?** Warning: You will NOT be able to merge these
|
|
73
|
+
commits using the normal GitHub UI, as their branch bases won't be master.
|
|
74
|
+
Use `ghstack land $PR_URL` (or alternatively `ghstack land #PR_NUM`) to land
|
|
75
75
|
a ghstack'ed pull request.
|
|
76
76
|
|
|
77
77
|
You can also setup a GitHub action to allow a bot land.
|
|
78
|
-
[ghstack_land_example](https://github.com/Chillee/ghstack_land_example/blob/main/.github/workflows/
|
|
78
|
+
[ghstack_land_example](https://github.com/Chillee/ghstack_land_example/blob/main/.github/workflows/ghstack_land.yml)
|
|
79
79
|
is an end-to-end example of how to do this.
|
|
80
80
|
|
|
81
81
|
## Structure of submitted pull requests
|
|
@@ -8,7 +8,7 @@ uv tool install ghstack
|
|
|
8
8
|
|
|
9
9
|
ghstack is tested with
|
|
10
10
|
[several different Python versions](https://github.com/ezyang/ghstack/blob/master/.github/workflows/test.yml#L13).
|
|
11
|
-
It requires at least Python 3.
|
|
11
|
+
It requires at least Python 3.9.1.
|
|
12
12
|
|
|
13
13
|
## How to setup
|
|
14
14
|
|
|
@@ -49,13 +49,13 @@ resolve conflicts in *each* PR, not just for the entire stack.)
|
|
|
49
49
|
**How do I start a new feature?** Just checkout master on a new
|
|
50
50
|
branch, and start working on a fresh branch.
|
|
51
51
|
|
|
52
|
-
**
|
|
53
|
-
normal GitHub UI, as their branch bases won't be master.
|
|
54
|
-
`ghstack land $PR_URL` (or alternatively `ghstack land #PR_NUM`) to land
|
|
52
|
+
**How do I merge my changes?** Warning: You will NOT be able to merge these
|
|
53
|
+
commits using the normal GitHub UI, as their branch bases won't be master.
|
|
54
|
+
Use `ghstack land $PR_URL` (or alternatively `ghstack land #PR_NUM`) to land
|
|
55
55
|
a ghstack'ed pull request.
|
|
56
56
|
|
|
57
57
|
You can also setup a GitHub action to allow a bot land.
|
|
58
|
-
[ghstack_land_example](https://github.com/Chillee/ghstack_land_example/blob/main/.github/workflows/
|
|
58
|
+
[ghstack_land_example](https://github.com/Chillee/ghstack_land_example/blob/main/.github/workflows/ghstack_land.yml)
|
|
59
59
|
is an end-to-end example of how to do this.
|
|
60
60
|
|
|
61
61
|
## Structure of submitted pull requests
|
|
@@ -3,7 +3,7 @@ authors = [
|
|
|
3
3
|
{name = "Edward Z. Yang", email = "ezyang@mit.edu"},
|
|
4
4
|
]
|
|
5
5
|
license = {text = "MIT"}
|
|
6
|
-
requires-python = "<4.0.0,>=3.
|
|
6
|
+
requires-python = "<4.0.0,>=3.9.1"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"aiohttp<4,>=3",
|
|
9
9
|
"importlib-metadata>=1.4; python_version < \"3.8\"",
|
|
@@ -13,7 +13,7 @@ dependencies = [
|
|
|
13
13
|
"flake8<8.0.0,>=7.0.0",
|
|
14
14
|
]
|
|
15
15
|
name = "ghstack"
|
|
16
|
-
version = "0.
|
|
16
|
+
version = "0.12.0"
|
|
17
17
|
description = "Stack diff support for GitHub"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
classifiers = [
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import ghstack.github
|
|
7
|
+
import ghstack.github_utils
|
|
8
|
+
import ghstack.shell
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(
|
|
12
|
+
pull_request: str,
|
|
13
|
+
github: ghstack.github.GitHubEndpoint,
|
|
14
|
+
sh: ghstack.shell.Shell,
|
|
15
|
+
remote_name: str,
|
|
16
|
+
stack: bool = False,
|
|
17
|
+
) -> None:
|
|
18
|
+
|
|
19
|
+
params = ghstack.github_utils.parse_pull_request(
|
|
20
|
+
pull_request, sh=sh, remote_name=remote_name
|
|
21
|
+
)
|
|
22
|
+
head_ref = github.get_head_ref(**params)
|
|
23
|
+
orig_ref = re.sub(r"/head$", "/orig", head_ref)
|
|
24
|
+
if orig_ref == head_ref:
|
|
25
|
+
logging.warning(
|
|
26
|
+
"The ref {} doesn't look like a ghstack reference".format(head_ref)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
sh.git("fetch", "--prune", remote_name)
|
|
30
|
+
|
|
31
|
+
if stack:
|
|
32
|
+
# Cherry-pick the entire stack from merge-base to the commit
|
|
33
|
+
remote_orig_ref = remote_name + "/" + orig_ref
|
|
34
|
+
|
|
35
|
+
# Find the merge-base with the main branch
|
|
36
|
+
repo_info = ghstack.github_utils.get_github_repo_info(
|
|
37
|
+
github=github,
|
|
38
|
+
sh=sh,
|
|
39
|
+
github_url=params["github_url"],
|
|
40
|
+
remote_name=remote_name,
|
|
41
|
+
)
|
|
42
|
+
main_branch = f"{remote_name}/{repo_info['default_branch']}"
|
|
43
|
+
|
|
44
|
+
# Get merge-base between the commit and main branch
|
|
45
|
+
merge_base = sh.git("merge-base", main_branch, remote_orig_ref).strip()
|
|
46
|
+
|
|
47
|
+
# Get all commits from merge-base to the target commit
|
|
48
|
+
commit_list = (
|
|
49
|
+
sh.git("rev-list", "--reverse", f"{merge_base}..{remote_orig_ref}")
|
|
50
|
+
.strip()
|
|
51
|
+
.split("\n")
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not commit_list or commit_list == [""]:
|
|
55
|
+
raise RuntimeError("No commits found to cherry-pick in the specified range")
|
|
56
|
+
|
|
57
|
+
logging.info(f"Cherry-picking {len(commit_list)} commits from stack")
|
|
58
|
+
for commit in commit_list:
|
|
59
|
+
sh.git("cherry-pick", commit)
|
|
60
|
+
logging.info(f"Cherry-picked {commit}")
|
|
61
|
+
else:
|
|
62
|
+
# Cherry-pick just the single commit
|
|
63
|
+
remote_orig_ref = remote_name + "/" + orig_ref
|
|
64
|
+
sh.git("cherry-pick", remote_orig_ref)
|
|
65
|
+
logging.info(f"Cherry-picked {orig_ref}")
|
|
@@ -7,6 +7,7 @@ import click
|
|
|
7
7
|
import ghstack
|
|
8
8
|
import ghstack.action
|
|
9
9
|
import ghstack.checkout
|
|
10
|
+
import ghstack.cherry_pick
|
|
10
11
|
import ghstack.circleci_real
|
|
11
12
|
import ghstack.config
|
|
12
13
|
import ghstack.github_real
|
|
@@ -126,6 +127,28 @@ def checkout(pull_request: str) -> None:
|
|
|
126
127
|
)
|
|
127
128
|
|
|
128
129
|
|
|
130
|
+
@main.command("cherry-pick")
|
|
131
|
+
@click.option(
|
|
132
|
+
"--stack",
|
|
133
|
+
"-s",
|
|
134
|
+
is_flag=True,
|
|
135
|
+
help="Cherry-pick all commits from the commit to the merge-base with main branch",
|
|
136
|
+
)
|
|
137
|
+
@click.argument("pull_request", metavar="PR")
|
|
138
|
+
def cherry_pick(stack: bool, pull_request: str) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Cherry-pick a PR
|
|
141
|
+
"""
|
|
142
|
+
with cli_context(request_github_token=False) as (shell, config, github):
|
|
143
|
+
ghstack.cherry_pick.main(
|
|
144
|
+
pull_request=pull_request,
|
|
145
|
+
github=github,
|
|
146
|
+
sh=shell,
|
|
147
|
+
remote_name=config.remote_name,
|
|
148
|
+
stack=stack,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
129
152
|
@main.command("land")
|
|
130
153
|
@click.option("--force", is_flag=True, help="force land even if the PR is closed")
|
|
131
154
|
@click.argument("pull_request", metavar="PR")
|
|
@@ -14,7 +14,7 @@ RE_GH_METADATA = re.compile(
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
RAW_PULL_REQUEST_RESOLVED = (
|
|
17
|
-
r"(Pull Request resolved|Pull-Request-resolved): "
|
|
17
|
+
r"(Pull Request resolved|Pull-Request-resolved|Pull-Request): "
|
|
18
18
|
r"https://{github_url}/(?P<owner>[^/]+)/(?P<repo>[^/]+)/pull/(?P<number>[0-9]+)"
|
|
19
19
|
)
|
|
20
20
|
|
|
@@ -93,7 +93,7 @@ class Diff:
|
|
|
93
93
|
# be the same.
|
|
94
94
|
source_id: str
|
|
95
95
|
|
|
96
|
-
# The contents of 'Pull-Request
|
|
96
|
+
# The contents of 'Pull-Request'. This is None for
|
|
97
97
|
# diffs that haven't been submitted by ghstack. For BC reasons,
|
|
98
98
|
# this also accepts gh-metadata.
|
|
99
99
|
pull_request_resolved: Optional[PullRequestResolved]
|
|
@@ -3,12 +3,16 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import re
|
|
6
|
+
import time
|
|
6
7
|
from typing import Any, Dict, Optional, Sequence, Tuple, Union
|
|
7
8
|
|
|
8
9
|
import requests
|
|
9
10
|
|
|
10
11
|
import ghstack.github
|
|
11
12
|
|
|
13
|
+
MAX_RETRIES = 5
|
|
14
|
+
INITIAL_BACKOFF_SECONDS = 60
|
|
15
|
+
|
|
12
16
|
|
|
13
17
|
class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
14
18
|
"""
|
|
@@ -155,32 +159,63 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path
|
|
158
|
-
logging.debug("# {} {}".format(method, url))
|
|
159
|
-
logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1)))
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
proxies=self._proxies(),
|
|
166
|
-
verify=self.verify,
|
|
167
|
-
cert=self.cert,
|
|
168
|
-
)
|
|
163
|
+
backoff_seconds = INITIAL_BACKOFF_SECONDS
|
|
164
|
+
for attempt in range(0, MAX_RETRIES):
|
|
165
|
+
logging.debug("# {} {}".format(method, url))
|
|
166
|
+
logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1)))
|
|
169
167
|
|
|
170
|
-
|
|
168
|
+
resp: requests.Response = getattr(requests, method)(
|
|
169
|
+
url,
|
|
170
|
+
json=kwargs,
|
|
171
|
+
headers=headers,
|
|
172
|
+
proxies=self._proxies(),
|
|
173
|
+
verify=self.verify,
|
|
174
|
+
cert=self.cert,
|
|
175
|
+
)
|
|
171
176
|
|
|
172
|
-
|
|
173
|
-
r = resp.json()
|
|
174
|
-
except ValueError:
|
|
175
|
-
logging.debug("Response body:\n{}".format(r.text))
|
|
176
|
-
raise
|
|
177
|
-
else:
|
|
178
|
-
pretty_json = json.dumps(r, indent=1)
|
|
179
|
-
logging.debug("Response JSON:\n{}".format(pretty_json))
|
|
177
|
+
logging.debug("Response status: {}".format(resp.status_code))
|
|
180
178
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
try:
|
|
180
|
+
r = resp.json()
|
|
181
|
+
except ValueError:
|
|
182
|
+
logging.debug("Response body:\n{}".format(r.text))
|
|
183
|
+
raise
|
|
184
|
+
else:
|
|
185
|
+
pretty_json = json.dumps(r, indent=1)
|
|
186
|
+
logging.debug("Response JSON:\n{}".format(pretty_json))
|
|
187
|
+
|
|
188
|
+
# Per Github rate limiting: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#exceeding-the-rate-limit
|
|
189
|
+
if resp.status_code in (403, 429):
|
|
190
|
+
remaining_count = resp.headers.get("x-ratelimit-remaining")
|
|
191
|
+
reset_time = resp.headers.get("x-ratelimit-reset")
|
|
192
|
+
|
|
193
|
+
if remaining_count == "0" and reset_time:
|
|
194
|
+
sleep_time = int(reset_time) - int(time.time())
|
|
195
|
+
logging.warning(
|
|
196
|
+
f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds."
|
|
197
|
+
)
|
|
198
|
+
time.sleep(sleep_time)
|
|
199
|
+
continue
|
|
200
|
+
else:
|
|
201
|
+
retry_after_seconds = resp.headers.get("retry-after")
|
|
202
|
+
if retry_after_seconds:
|
|
203
|
+
sleep_time = int(retry_after_seconds)
|
|
204
|
+
logging.warning(
|
|
205
|
+
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
sleep_time = backoff_seconds
|
|
209
|
+
logging.warning(
|
|
210
|
+
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds (exponential backoff)."
|
|
211
|
+
)
|
|
212
|
+
backoff_seconds *= 2
|
|
213
|
+
time.sleep(sleep_time)
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
if resp.status_code == 404:
|
|
217
|
+
raise ghstack.github.NotFoundError(
|
|
218
|
+
"""\
|
|
184
219
|
GitHub raised a 404 error on the request for
|
|
185
220
|
{url}.
|
|
186
221
|
Usually, this doesn't actually mean the page doesn't exist; instead, it
|
|
@@ -189,14 +224,20 @@ permissions. Please create a new OAuth token at
|
|
|
189
224
|
https://{github_url}/settings/tokens and DOUBLE CHECK that you checked
|
|
190
225
|
"public_repo" for permissions, and update ~/.ghstackrc with your new
|
|
191
226
|
value.
|
|
227
|
+
|
|
228
|
+
Another possible reason for this error is if the repository has moved
|
|
229
|
+
to a new location or been renamed. Check that the repository URL is
|
|
230
|
+
still correct.
|
|
192
231
|
""".format(
|
|
193
|
-
|
|
232
|
+
url=url, github_url=self.github_url
|
|
233
|
+
)
|
|
194
234
|
)
|
|
195
|
-
)
|
|
196
235
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
236
|
+
try:
|
|
237
|
+
resp.raise_for_status()
|
|
238
|
+
except requests.HTTPError:
|
|
239
|
+
raise RuntimeError(pretty_json)
|
|
201
240
|
|
|
202
|
-
|
|
241
|
+
return r
|
|
242
|
+
|
|
243
|
+
raise RuntimeError("Exceeded maximum retries due to GitHub rate limiting")
|
|
@@ -77,20 +77,43 @@ def get_github_repo_info(
|
|
|
77
77
|
name_with_owner = {"owner": repo_owner, "name": repo_name}
|
|
78
78
|
|
|
79
79
|
# TODO: Cache this guy
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
try:
|
|
81
|
+
repo = github.graphql(
|
|
82
|
+
"""
|
|
83
|
+
query ($owner: String!, $name: String!) {
|
|
84
|
+
repository(name: $name, owner: $owner) {
|
|
85
|
+
id
|
|
86
|
+
isFork
|
|
87
|
+
defaultBranchRef {
|
|
88
|
+
name
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
}""",
|
|
92
|
+
owner=name_with_owner["owner"],
|
|
93
|
+
name=name_with_owner["name"],
|
|
94
|
+
)["data"]["repository"]
|
|
95
|
+
except RuntimeError as e:
|
|
96
|
+
# Check if this is a repository access error (NOT_FOUND)
|
|
97
|
+
error_msg = str(e)
|
|
98
|
+
if (
|
|
99
|
+
"Could not resolve to a Repository" in error_msg
|
|
100
|
+
and "NOT_FOUND" in error_msg
|
|
101
|
+
):
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
f"Original error: {error_msg}\n\n"
|
|
104
|
+
f"Could not access repository '{name_with_owner['owner']}/{name_with_owner['name']}'. "
|
|
105
|
+
f"This usually means:\n"
|
|
106
|
+
f"1. The repository is private and your OAuth token doesn't have access\n"
|
|
107
|
+
f"2. The repository is in a different organization that hasn't granted access\n"
|
|
108
|
+
f"3. The repository doesn't exist or has been moved\n\n"
|
|
109
|
+
f"If this is a private repository or in a separate organization, you can grant access by:\n"
|
|
110
|
+
f"1. Delete the 'github_oauth' entry from your .ghstackrc file\n"
|
|
111
|
+
f"2. Run ghstack again to redo the OAuth flow\n"
|
|
112
|
+
f"3. Make sure to grant access to the appropriate organizations when prompted"
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
# Re-raise the original error if it's not the repository access issue
|
|
116
|
+
raise
|
|
94
117
|
|
|
95
118
|
return {
|
|
96
119
|
"name_with_owner": name_with_owner,
|
|
@@ -152,11 +152,18 @@ to complain to the ghstack authors."""
|
|
|
152
152
|
sh.git("cherry-pick", "--abort")
|
|
153
153
|
raise
|
|
154
154
|
|
|
155
|
+
# All good! Push!
|
|
156
|
+
maybe_force_arg = []
|
|
157
|
+
if needs_force:
|
|
158
|
+
maybe_force_arg = ["--force-with-lease"]
|
|
159
|
+
sh.git(
|
|
160
|
+
"push", *maybe_force_arg, remote_name, f"HEAD:refs/heads/{default_branch}"
|
|
161
|
+
)
|
|
162
|
+
|
|
155
163
|
# Advance base to head to "close" the PR for all PRs.
|
|
156
|
-
# This
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
# fail
|
|
164
|
+
# This happens after the cherry-pick and push, because the cherry-picks
|
|
165
|
+
# can fail (merge conflict) and the push can also fail (race condition)
|
|
166
|
+
|
|
160
167
|
# TODO: It might be helpful to advance orig to reflect the true
|
|
161
168
|
# state of upstream at the time we are doing the land, and then
|
|
162
169
|
# directly *merge* head into base, so that the PR accurately
|
|
@@ -173,21 +180,19 @@ to complain to the ghstack authors."""
|
|
|
173
180
|
)
|
|
174
181
|
github.notify_merged(pr_resolved)
|
|
175
182
|
|
|
176
|
-
# All good! Push!
|
|
177
|
-
maybe_force_arg = []
|
|
178
|
-
if needs_force:
|
|
179
|
-
maybe_force_arg = ["--force-with-lease"]
|
|
180
|
-
sh.git(
|
|
181
|
-
"push", *maybe_force_arg, remote_name, f"HEAD:refs/heads/{default_branch}"
|
|
182
|
-
)
|
|
183
|
-
|
|
184
183
|
# Delete the branches
|
|
185
184
|
for orig_ref, _ in stack_orig_refs:
|
|
186
185
|
# TODO: regex here so janky
|
|
187
186
|
base_ref = re.sub(r"/orig$", "/base", orig_ref)
|
|
188
187
|
head_ref = re.sub(r"/orig$", "/head", orig_ref)
|
|
189
188
|
try:
|
|
190
|
-
sh.git("push", remote_name, "--delete", orig_ref, base_ref
|
|
189
|
+
sh.git("push", remote_name, "--delete", orig_ref, base_ref)
|
|
190
|
+
except RuntimeError:
|
|
191
|
+
# Whatever, keep going
|
|
192
|
+
logging.warning("Failed to delete branch, continuing", exc_info=True)
|
|
193
|
+
# Try deleting head_ref separately since often after it's merged it doesn't exist anymore
|
|
194
|
+
try:
|
|
195
|
+
sh.git("push", remote_name, "--delete", head_ref)
|
|
191
196
|
except RuntimeError:
|
|
192
197
|
# Whatever, keep going
|
|
193
198
|
logging.warning("Failed to delete branch, continuing", exc_info=True)
|
|
@@ -167,9 +167,7 @@ def strip_mentions(body: str) -> str:
|
|
|
167
167
|
return RE_MENTION.sub(r"\1", body)
|
|
168
168
|
|
|
169
169
|
|
|
170
|
-
STACK_HEADER = (
|
|
171
|
-
"Stack from [ghstack](https://github.com/ezyang/ghstack) (oldest at bottom)"
|
|
172
|
-
)
|
|
170
|
+
STACK_HEADER = f"Stack from [ghstack](https://github.com/ezyang/ghstack/tree/{ghstack.__version__}) (oldest at bottom)"
|
|
173
171
|
|
|
174
172
|
|
|
175
173
|
def starts_with_bullet(body: str) -> bool:
|
|
@@ -468,12 +466,6 @@ class Submitter:
|
|
|
468
466
|
raise RuntimeError(
|
|
469
467
|
"There appears to be no commits to process, based on the revs you passed me."
|
|
470
468
|
)
|
|
471
|
-
elif commit_count > 8 and not self.force:
|
|
472
|
-
raise RuntimeError(
|
|
473
|
-
"Cowardly refusing to handle a stack with more than eight PRs. "
|
|
474
|
-
"You are likely to get rate limited by GitHub if you try to create or "
|
|
475
|
-
"manipulate this many PRs. You can bypass this throttle using --force"
|
|
476
|
-
)
|
|
477
469
|
|
|
478
470
|
# This is not really accurate if you're doing a fancy pattern;
|
|
479
471
|
# if this is a problem file us a bug.
|
|
@@ -816,7 +808,7 @@ as it supports bidirectional syncing. However, there is no way to
|
|
|
816
808
|
convert a pre-existing PR in the old style to the new format which
|
|
817
809
|
supports bidirectional syncing. If you would like to blow away the old
|
|
818
810
|
PR and start anew, edit the Summary in the Phabricator diff to delete
|
|
819
|
-
the line 'Pull-Request
|
|
811
|
+
the line 'Pull-Request' and then run ghexport again.
|
|
820
812
|
"""
|
|
821
813
|
)
|
|
822
814
|
|
|
@@ -833,7 +825,7 @@ the Phabricator diff page? If so, please continue to use that button
|
|
|
833
825
|
to export your diff.
|
|
834
826
|
|
|
835
827
|
If you think this is in error, edit the Summary in the Phabricator diff
|
|
836
|
-
to delete the line 'Pull-Request
|
|
828
|
+
to delete the line 'Pull-Request' and then run ghexport again.
|
|
837
829
|
"""
|
|
838
830
|
)
|
|
839
831
|
else:
|
|
@@ -970,9 +962,7 @@ to disassociate the commit with the pull request, and then try again.
|
|
|
970
962
|
if self.direct:
|
|
971
963
|
trailers_to_add.append(f"ghstack-comment-id: {elab_diff.comment_id}")
|
|
972
964
|
|
|
973
|
-
trailers_to_add.append(
|
|
974
|
-
f"Pull-Request-resolved: {pull_request_resolved.url()}"
|
|
975
|
-
)
|
|
965
|
+
trailers_to_add.append(f"Pull-Request: {pull_request_resolved.url()}")
|
|
976
966
|
|
|
977
967
|
commit_msg = ghstack.trailers.interpret_trailers(
|
|
978
968
|
strip_mentions(diff.summary.rstrip()), trailers_to_add
|
|
@@ -1779,7 +1769,7 @@ is closed (likely due to being merged). Please rebase to upstream and try again
|
|
|
1779
1769
|
commit_body = RE_GHSTACK_SOURCE_ID.sub("", commit_body)
|
|
1780
1770
|
# Comment ID is not necessary; source of truth is orig commit
|
|
1781
1771
|
commit_body = RE_GHSTACK_COMMENT_ID.sub("", commit_body)
|
|
1782
|
-
# Don't store Pull request
|
|
1772
|
+
# Don't store Pull request in the PR body; it's
|
|
1783
1773
|
# unnecessary
|
|
1784
1774
|
commit_body = ghstack.diff.re_pull_request_resolved_w_sp(self.github_url).sub(
|
|
1785
1775
|
"", commit_body
|
|
@@ -12,6 +12,8 @@ from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple, Uni
|
|
|
12
12
|
|
|
13
13
|
from expecttest import assert_expected_inline
|
|
14
14
|
|
|
15
|
+
import ghstack.cherry_pick
|
|
16
|
+
|
|
15
17
|
import ghstack.github
|
|
16
18
|
import ghstack.github_fake
|
|
17
19
|
import ghstack.github_utils
|
|
@@ -29,6 +31,7 @@ __all__ = [
|
|
|
29
31
|
"gh_submit",
|
|
30
32
|
"gh_land",
|
|
31
33
|
"gh_unlink",
|
|
34
|
+
"gh_cherry_pick",
|
|
32
35
|
"GitCommitHash",
|
|
33
36
|
"checkout",
|
|
34
37
|
"amend",
|
|
@@ -237,6 +240,17 @@ def gh_unlink() -> None:
|
|
|
237
240
|
)
|
|
238
241
|
|
|
239
242
|
|
|
243
|
+
def gh_cherry_pick(pull_request: str, stack: bool = False) -> None:
|
|
244
|
+
self = CTX
|
|
245
|
+
return ghstack.cherry_pick.main(
|
|
246
|
+
pull_request=pull_request,
|
|
247
|
+
github=self.github,
|
|
248
|
+
sh=self.sh,
|
|
249
|
+
remote_name="origin",
|
|
250
|
+
stack=stack,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
240
254
|
def write_file_and_add(filename: str, contents: str) -> None:
|
|
241
255
|
self = CTX
|
|
242
256
|
with self.sh.open(filename, "w") as f:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|