ghstack 0.16.0__tar.gz → 0.17.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.16.0 → ghstack-0.17.0}/PKG-INFO +1 -1
- {ghstack-0.16.0 → ghstack-0.17.0}/pyproject.toml +1 -1
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/cli.py +2 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/config.py +17 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_real.py +27 -24
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_utils.py +7 -2
- {ghstack-0.16.0 → ghstack-0.17.0}/LICENSE +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/README.md +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/__init__.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/__main__.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/action.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/async_script.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/cache.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/checkout.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/cherry_pick.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/circleci.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/circleci_real.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/diff.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/forensics.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/git.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_fake.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/github_schema.graphql +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/gpg_sign.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/land.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/log.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/logs.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/pull.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/py.typed +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/rage.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/shell.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/status.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/submit.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/sync.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/test_prelude.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/trailers.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/types.py +0 -0
- {ghstack-0.16.0 → ghstack-0.17.0}/src/ghstack/unlink.py +0 -0
|
@@ -111,6 +111,11 @@ Config = NamedTuple(
|
|
|
111
111
|
("reviewer", Optional[str]),
|
|
112
112
|
# Default labels to add to new pull requests (comma-separated labels)
|
|
113
113
|
("label", Optional[str]),
|
|
114
|
+
# Retry/backoff tuning
|
|
115
|
+
("max_retries", int),
|
|
116
|
+
# The initial backoff time to use, in seconds. We will double this
|
|
117
|
+
# time for each retry.
|
|
118
|
+
("initial_backoff_seconds", int),
|
|
114
119
|
# Command to generate a per-PR update description from diff contents
|
|
115
120
|
("automsg", Optional[str]),
|
|
116
121
|
],
|
|
@@ -334,6 +339,16 @@ def read_config(
|
|
|
334
339
|
else:
|
|
335
340
|
automsg = None
|
|
336
341
|
|
|
342
|
+
if config.has_option("ghstack", "max_retries"):
|
|
343
|
+
max_retries = config.getint("ghstack", "max_retries")
|
|
344
|
+
else:
|
|
345
|
+
max_retries = 5
|
|
346
|
+
|
|
347
|
+
if config.has_option("ghstack", "initial_backoff_seconds"):
|
|
348
|
+
initial_backoff_seconds = config.getint("ghstack", "initial_backoff_seconds")
|
|
349
|
+
else:
|
|
350
|
+
initial_backoff_seconds = 60
|
|
351
|
+
|
|
337
352
|
if write_back:
|
|
338
353
|
with open(config_path, "w") as f:
|
|
339
354
|
config.write(f)
|
|
@@ -351,6 +366,8 @@ def read_config(
|
|
|
351
366
|
remote_name=remote_name,
|
|
352
367
|
reviewer=reviewer,
|
|
353
368
|
label=label,
|
|
369
|
+
max_retries=max_retries,
|
|
370
|
+
initial_backoff_seconds=initial_backoff_seconds,
|
|
354
371
|
automsg=automsg,
|
|
355
372
|
)
|
|
356
373
|
logging.debug(f"conf = {conf}")
|
|
@@ -13,9 +13,6 @@ import aiohttp
|
|
|
13
13
|
|
|
14
14
|
import ghstack.github
|
|
15
15
|
|
|
16
|
-
MAX_RETRIES = 5
|
|
17
|
-
INITIAL_BACKOFF_SECONDS = 60
|
|
18
|
-
|
|
19
16
|
|
|
20
17
|
class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
21
18
|
"""
|
|
@@ -61,6 +58,9 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
61
58
|
cert: Optional[Union[str, Tuple[str, str]]]
|
|
62
59
|
_session: Optional[aiohttp.ClientSession]
|
|
63
60
|
|
|
61
|
+
max_retries: int
|
|
62
|
+
initial_backoff_seconds: int
|
|
63
|
+
|
|
64
64
|
def __init__(
|
|
65
65
|
self,
|
|
66
66
|
oauth_token: Optional[str],
|
|
@@ -68,12 +68,16 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
68
68
|
proxy: Optional[str] = None,
|
|
69
69
|
verify: Optional[Union[str, bool]] = None,
|
|
70
70
|
cert: Optional[Union[str, Tuple[str, str]]] = None,
|
|
71
|
+
max_retries: int = 5,
|
|
72
|
+
initial_backoff_seconds: int = 60,
|
|
71
73
|
):
|
|
72
74
|
self.oauth_token = oauth_token
|
|
73
75
|
self.proxy = proxy
|
|
74
76
|
self.github_url = github_url
|
|
75
77
|
self.verify = verify
|
|
76
78
|
self.cert = cert
|
|
79
|
+
self.max_retries = max_retries
|
|
80
|
+
self.initial_backoff_seconds = initial_backoff_seconds
|
|
77
81
|
self._rest_request_ids = itertools.count(1)
|
|
78
82
|
self._session = None
|
|
79
83
|
|
|
@@ -212,11 +216,11 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
212
216
|
if aiohttp_ssl is not None:
|
|
213
217
|
request_kwargs["ssl"] = aiohttp_ssl
|
|
214
218
|
|
|
215
|
-
backoff_seconds =
|
|
219
|
+
backoff_seconds = self.initial_backoff_seconds
|
|
216
220
|
request_id = next(self._rest_request_ids)
|
|
217
221
|
log_prefix = f"rest[{request_id}]"
|
|
218
222
|
session = self._get_session()
|
|
219
|
-
for attempt in range(0,
|
|
223
|
+
for attempt in range(0, self.max_retries):
|
|
220
224
|
logging.debug("# %s %s %s", log_prefix, method, url)
|
|
221
225
|
logging.debug(
|
|
222
226
|
"%s request body:\n%s", log_prefix, json.dumps(kwargs, indent=1)
|
|
@@ -224,12 +228,11 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
224
228
|
|
|
225
229
|
async with getattr(session, method)(url, **request_kwargs) as resp:
|
|
226
230
|
logging.debug("%s response status: %s", log_prefix, resp.status)
|
|
231
|
+
resp_text = await resp.text()
|
|
227
232
|
try:
|
|
228
|
-
r =
|
|
229
|
-
except
|
|
230
|
-
logging.debug(
|
|
231
|
-
"%s response body:\n%s", log_prefix, await resp.text()
|
|
232
|
-
)
|
|
233
|
+
r = json.loads(resp_text)
|
|
234
|
+
except ValueError:
|
|
235
|
+
logging.debug("%s response body:\n%s", log_prefix, resp_text)
|
|
233
236
|
raise
|
|
234
237
|
else:
|
|
235
238
|
pretty_json = json.dumps(r, indent=1)
|
|
@@ -240,32 +243,32 @@ class RealGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
240
243
|
if resp.status in (403, 429):
|
|
241
244
|
remaining_count = resp.headers.get("x-ratelimit-remaining")
|
|
242
245
|
reset_time = resp.headers.get("x-ratelimit-reset")
|
|
246
|
+
more_attempts = attempt < self.max_retries - 1
|
|
243
247
|
|
|
244
248
|
if remaining_count == "0" and reset_time:
|
|
245
|
-
sleep_time = int(reset_time) - int(time.time())
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
sleep_time = max(0, int(reset_time) - int(time.time()))
|
|
250
|
+
if more_attempts:
|
|
251
|
+
logging.warning(
|
|
252
|
+
f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds."
|
|
253
|
+
)
|
|
254
|
+
await asyncio.sleep(sleep_time)
|
|
255
|
+
continue
|
|
251
256
|
# GitHub doesn't document the content of these messages, but this
|
|
252
257
|
# seems to be an accurate way to find secondary rate limits. Any
|
|
253
258
|
# other reason for 403 or 429 will fall through to the error below.
|
|
254
|
-
elif
|
|
259
|
+
elif "rate limit" in resp_text.lower():
|
|
255
260
|
retry_after_seconds = resp.headers.get("retry-after")
|
|
256
261
|
if retry_after_seconds:
|
|
257
262
|
sleep_time = int(retry_after_seconds)
|
|
258
|
-
logging.warning(
|
|
259
|
-
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
|
|
260
|
-
)
|
|
261
263
|
else:
|
|
262
264
|
sleep_time = backoff_seconds
|
|
265
|
+
backoff_seconds *= 2
|
|
266
|
+
if more_attempts:
|
|
263
267
|
logging.warning(
|
|
264
|
-
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds
|
|
268
|
+
f"Secondary rate limit hit. Sleeping for {sleep_time} seconds."
|
|
265
269
|
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
continue
|
|
270
|
+
await asyncio.sleep(max(0, sleep_time))
|
|
271
|
+
continue
|
|
269
272
|
|
|
270
273
|
if resp.status == 404:
|
|
271
274
|
raise ghstack.github.NotFoundError(
|
|
@@ -32,7 +32,10 @@ async def get_github_repo_name_with_owner(
|
|
|
32
32
|
# where commits will actually be pushed to
|
|
33
33
|
remote_url = await sh.agit("remote", "get-url", "--push", remote_name)
|
|
34
34
|
while True:
|
|
35
|
-
|
|
35
|
+
# The SSH user can be "git" (e.g. git@github.com:owner/repo.git) or an
|
|
36
|
+
# arbitrary identity such as the org-scoped deploy user GitHub now hands
|
|
37
|
+
# out (e.g. org-21003710@github.com:owner/repo.git), so match any user.
|
|
38
|
+
match = r"^[^@]+@{github_url}:/?([^/]+)/(.+?)(?:\.git)?$".format(
|
|
36
39
|
github_url=github_url
|
|
37
40
|
)
|
|
38
41
|
m = re.match(match, remote_url)
|
|
@@ -175,7 +178,9 @@ GitHubPullRequestParams = TypedDict(
|
|
|
175
178
|
def _normalize_remote_url(remote_url: str) -> str:
|
|
176
179
|
"""Convert SSH remote URL to HTTPS format, strip .git suffix."""
|
|
177
180
|
# git@github.com:owner/repo.git -> https://github.com/owner/repo
|
|
178
|
-
|
|
181
|
+
# The SSH user may be "git" or an org-scoped identity such as
|
|
182
|
+
# org-21003710@github.com:owner/repo.git, so match any user.
|
|
183
|
+
m = re.match(r"^[^@]+@([^:]+):/?(.+?)(?:\.git)?$", remote_url)
|
|
179
184
|
if m:
|
|
180
185
|
return f"https://{m.group(1)}/{m.group(2)}"
|
|
181
186
|
return re.sub(r"\.git$", "", remote_url)
|
|
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
|
|
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
|