ghstack 0.16.0__tar.gz → 0.18.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.18.0}/PKG-INFO +1 -1
- {ghstack-0.16.0 → ghstack-0.18.0}/pyproject.toml +1 -1
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/cli.py +2 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/config.py +17 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/github_fake.py +31 -7
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/github_real.py +27 -24
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/github_utils.py +7 -2
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/submit.py +38 -6
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/test_prelude.py +10 -7
- {ghstack-0.16.0 → ghstack-0.18.0}/LICENSE +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/README.md +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/__init__.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/__main__.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/action.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/async_script.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/cache.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/checkout.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/cherry_pick.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/circleci.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/circleci_real.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/diff.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/forensics.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/git.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/github.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/github_schema.graphql +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/gpg_sign.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/land.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/log.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/logs.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/pull.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/py.typed +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/rage.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/shell.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/status.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/sync.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/trailers.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.0}/src/ghstack/types.py +0 -0
- {ghstack-0.16.0 → ghstack-0.18.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}")
|
|
@@ -125,13 +125,32 @@ class GitHubState:
|
|
|
125
125
|
return r
|
|
126
126
|
|
|
127
127
|
def push_hook(self, refs: Sequence[str]) -> None:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
128
|
+
self._refs_dirty = True
|
|
129
|
+
|
|
130
|
+
async def detect_merged(self) -> None:
|
|
131
|
+
"""
|
|
132
|
+
GitHub closes a pull request as merged once its head becomes
|
|
133
|
+
reachable from its base, which in direct mode can happen through an
|
|
134
|
+
ordinary push to some other pull request's head branch. Only scan
|
|
135
|
+
after something moved; a scan is a git call per open pull request.
|
|
136
|
+
"""
|
|
137
|
+
if not self._refs_dirty or self.upstream_sh is None:
|
|
138
|
+
self._refs_dirty = False
|
|
139
|
+
return
|
|
140
|
+
self._refs_dirty = False
|
|
141
|
+
for pr in self.pull_requests.values():
|
|
142
|
+
if pr.closed:
|
|
143
|
+
continue
|
|
144
|
+
reachable = await self.upstream_sh.agit(
|
|
145
|
+
"merge-base",
|
|
146
|
+
"--is-ancestor",
|
|
147
|
+
pr.headRefName,
|
|
148
|
+
pr.baseRefName,
|
|
149
|
+
exitcode=True,
|
|
150
|
+
)
|
|
151
|
+
if reachable:
|
|
152
|
+
pr.closed = True
|
|
153
|
+
pr.merged = True
|
|
135
154
|
|
|
136
155
|
def notify_merged(self, pr_resolved: ghstack.diff.PullRequestResolved) -> None:
|
|
137
156
|
repo = self.repository(pr_resolved.owner, pr_resolved.repo)
|
|
@@ -140,6 +159,7 @@ class GitHubState:
|
|
|
140
159
|
# TODO: model merged too
|
|
141
160
|
|
|
142
161
|
def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None:
|
|
162
|
+
self._refs_dirty = False
|
|
143
163
|
self.repositories = {}
|
|
144
164
|
self.pull_requests = {}
|
|
145
165
|
self.issue_comments = {}
|
|
@@ -275,6 +295,7 @@ class PullRequest(Node):
|
|
|
275
295
|
# state: PullRequestState
|
|
276
296
|
title: str
|
|
277
297
|
url: str
|
|
298
|
+
merged: bool = False
|
|
278
299
|
reviewers: List[str] = dataclasses.field(default_factory=list)
|
|
279
300
|
labels: List[str] = dataclasses.field(default_factory=list)
|
|
280
301
|
|
|
@@ -342,6 +363,7 @@ class FakeGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
342
363
|
self.state = GitHubState(upstream_sh)
|
|
343
364
|
|
|
344
365
|
async def graphql(self, query: str, **kwargs: Any) -> Any:
|
|
366
|
+
await self.state.detect_merged()
|
|
345
367
|
r = await graphql.graphql(
|
|
346
368
|
schema=GITHUB_SCHEMA,
|
|
347
369
|
source=query,
|
|
@@ -419,6 +441,7 @@ class FakeGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
419
441
|
if "base" in input and input["base"] is not None:
|
|
420
442
|
pr.baseRefName = input["base"]
|
|
421
443
|
pr.baseRef = await repo._make_ref_async(state, pr.baseRefName)
|
|
444
|
+
state._refs_dirty = True
|
|
422
445
|
if "body" in input and input["body"] is not None:
|
|
423
446
|
pr.body = input["body"]
|
|
424
447
|
|
|
@@ -463,6 +486,7 @@ class FakeGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
463
486
|
)
|
|
464
487
|
|
|
465
488
|
async def arest(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
489
|
+
await self.state.detect_merged()
|
|
466
490
|
return await self._arest_impl(method, path, **kwargs)
|
|
467
491
|
|
|
468
492
|
async def _arest_impl(self, method: str, path: str, **kwargs: Any) -> Any:
|
|
@@ -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)
|
|
@@ -516,8 +516,10 @@ class Submitter:
|
|
|
516
516
|
|
|
517
517
|
object.__setattr__(self, "base", default_branch)
|
|
518
518
|
|
|
519
|
-
|
|
520
|
-
|
|
519
|
+
# ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
520
|
+
# The main algorithm
|
|
521
|
+
|
|
522
|
+
async def _initialize_direct(self, pr_info_cache: Dict[GitHubNumber, Any]) -> None:
|
|
521
523
|
direct = self.direct_opt
|
|
522
524
|
if direct is None:
|
|
523
525
|
direct_r = await self.sh.agit(
|
|
@@ -525,12 +527,22 @@ class Submitter:
|
|
|
525
527
|
)
|
|
526
528
|
assert isinstance(direct_r, bool)
|
|
527
529
|
direct = direct_r
|
|
528
|
-
|
|
530
|
+
if self.direct_opt is None and not direct:
|
|
531
|
+
styles = {
|
|
532
|
+
re.fullmatch(r"gh/[^/]+/[0-9]+/base", base_ref) is None
|
|
533
|
+
for pr_info in pr_info_cache.values()
|
|
534
|
+
if (base_ref := self._pr_ref_name(pr_info, "base")) is not None
|
|
535
|
+
}
|
|
536
|
+
if len(styles) > 1:
|
|
537
|
+
raise RuntimeError(
|
|
538
|
+
"Cannot infer ghstack submission style: the stack contains "
|
|
539
|
+
"both direct and non-direct pull requests. Pass --direct or "
|
|
540
|
+
"--no-direct explicitly."
|
|
541
|
+
)
|
|
542
|
+
if styles:
|
|
543
|
+
direct = styles.pop()
|
|
529
544
|
object.__setattr__(self, "direct", direct)
|
|
530
545
|
|
|
531
|
-
# ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
532
|
-
# The main algorithm
|
|
533
|
-
|
|
534
546
|
async def run(self) -> List[DiffMeta]:
|
|
535
547
|
timer = _Timer() if _TIMING_ENABLED else None
|
|
536
548
|
|
|
@@ -601,6 +613,7 @@ class Submitter:
|
|
|
601
613
|
)
|
|
602
614
|
|
|
603
615
|
pr_info_cache = await self._prefetch_pr_info(commits_to_rebase)
|
|
616
|
+
await self._initialize_direct(pr_info_cache)
|
|
604
617
|
if not self.no_fetch:
|
|
605
618
|
await self._fetch_foreign_pr_refs(pr_info_cache.values())
|
|
606
619
|
|
|
@@ -1992,6 +2005,25 @@ Current PR description:
|
|
|
1992
2005
|
# otherwise GitHub can spuriously think that the user pushed a number
|
|
1993
2006
|
# of patches as part of the PR, when actually they were just from the
|
|
1994
2007
|
# new upstream branch.
|
|
2008
|
+
# In direct mode a pull request's base is another pull request's head
|
|
2009
|
+
# branch, so a reorder can leave a pull request's head reachable from
|
|
2010
|
+
# the base GitHub still has on file, and GitHub closes any pull request
|
|
2011
|
+
# in that state as merged. Park the ones whose base is moving on the
|
|
2012
|
+
# default branch, which no head branch is ever reachable from, until
|
|
2013
|
+
# their real base has been pushed.
|
|
2014
|
+
if self.direct:
|
|
2015
|
+
await _gather_ordered(
|
|
2016
|
+
self.github.arest(
|
|
2017
|
+
"patch",
|
|
2018
|
+
"repos/{}/{}/pulls/{}".format(
|
|
2019
|
+
self.repo_owner, self.repo_name, s.number
|
|
2020
|
+
),
|
|
2021
|
+
base=self.base,
|
|
2022
|
+
)
|
|
2023
|
+
for s in diffs_to_submit
|
|
2024
|
+
if not s.closed and s.base != s.elab_diff.base_ref
|
|
2025
|
+
)
|
|
2026
|
+
|
|
1995
2027
|
all_push_specs: List[str] = []
|
|
1996
2028
|
|
|
1997
2029
|
for s in reversed(diffs_to_submit):
|
|
@@ -122,9 +122,9 @@ class Context:
|
|
|
122
122
|
github: ghstack.github.GitHubEndpoint
|
|
123
123
|
upstream_sh: ghstack.shell.Shell
|
|
124
124
|
sh: ghstack.shell.Shell
|
|
125
|
-
direct: bool
|
|
125
|
+
direct: Optional[bool]
|
|
126
126
|
|
|
127
|
-
def __init__(self, direct: bool) -> None:
|
|
127
|
+
def __init__(self, direct: Optional[bool]) -> None:
|
|
128
128
|
# Set up a "parent" repository with an empty initial commit that we'll operate on
|
|
129
129
|
upstream_dir = tempfile.mkdtemp()
|
|
130
130
|
self.upstream_sh = ghstack.shell.Shell(cwd=upstream_dir, testing=True)
|
|
@@ -154,7 +154,7 @@ class Context:
|
|
|
154
154
|
onerror=handle_remove_read_only,
|
|
155
155
|
)
|
|
156
156
|
|
|
157
|
-
async def check_global_github_invariants(self, direct: bool) -> None:
|
|
157
|
+
async def check_global_github_invariants(self, direct: Optional[bool]) -> None:
|
|
158
158
|
r = await self.github.graphql(
|
|
159
159
|
"""
|
|
160
160
|
query {
|
|
@@ -177,7 +177,7 @@ class Context:
|
|
|
177
177
|
continue
|
|
178
178
|
# In direct mode, only head refs may not be reused;
|
|
179
179
|
# base refs can be reused in octopus situations
|
|
180
|
-
if
|
|
180
|
+
if direct is False:
|
|
181
181
|
assert pr["baseRefName"] not in seen_refs
|
|
182
182
|
seen_refs.add(pr["baseRefName"])
|
|
183
183
|
assert pr["headRefName"] not in seen_refs
|
|
@@ -200,7 +200,7 @@ async def init_test() -> Context:
|
|
|
200
200
|
|
|
201
201
|
|
|
202
202
|
@contextlib.asynccontextmanager
|
|
203
|
-
async def scoped_test(direct: bool) -> AsyncIterator[None]:
|
|
203
|
+
async def scoped_test(direct: Optional[bool]) -> AsyncIterator[None]:
|
|
204
204
|
global CTX
|
|
205
205
|
assert CTX is None
|
|
206
206
|
try:
|
|
@@ -224,8 +224,10 @@ async def gh_submit(
|
|
|
224
224
|
reviewer: Optional[str] = None,
|
|
225
225
|
label: Optional[str] = None,
|
|
226
226
|
automsg: Optional[str] = None,
|
|
227
|
+
**submit_kwargs: Any,
|
|
227
228
|
) -> List[ghstack.submit.DiffMeta]:
|
|
228
229
|
self = CTX
|
|
230
|
+
direct_opt = submit_kwargs.pop("direct_opt", self.direct)
|
|
229
231
|
r = await ghstack.submit.main(
|
|
230
232
|
msg=msg,
|
|
231
233
|
username="ezyang",
|
|
@@ -236,7 +238,7 @@ async def gh_submit(
|
|
|
236
238
|
repo_owner_opt="pytorch",
|
|
237
239
|
repo_name_opt="pytorch",
|
|
238
240
|
short=short,
|
|
239
|
-
direct_opt=
|
|
241
|
+
direct_opt=direct_opt,
|
|
240
242
|
no_skip=no_skip,
|
|
241
243
|
github_url="github.com",
|
|
242
244
|
remote_name="origin",
|
|
@@ -247,6 +249,7 @@ async def gh_submit(
|
|
|
247
249
|
reviewer=reviewer,
|
|
248
250
|
label=label,
|
|
249
251
|
automsg=automsg,
|
|
252
|
+
**submit_kwargs,
|
|
250
253
|
)
|
|
251
254
|
await self.check_global_github_invariants(self.direct)
|
|
252
255
|
return r
|
|
@@ -451,7 +454,7 @@ async def assert_github_state(expect: str, *, skip: int = 0) -> None:
|
|
|
451
454
|
assert_expected_inline(await dump_github(), expect, skip=skip + 1)
|
|
452
455
|
|
|
453
456
|
|
|
454
|
-
def is_direct() -> bool:
|
|
457
|
+
def is_direct() -> Optional[bool]:
|
|
455
458
|
return CTX.direct
|
|
456
459
|
|
|
457
460
|
|
|
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
|