ghstack 0.17.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.17.0 → ghstack-0.18.0}/PKG-INFO +1 -1
- {ghstack-0.17.0 → ghstack-0.18.0}/pyproject.toml +1 -1
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_fake.py +31 -7
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/submit.py +38 -6
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/test_prelude.py +10 -7
- {ghstack-0.17.0 → ghstack-0.18.0}/LICENSE +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/README.md +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/__init__.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/__main__.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/action.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/async_script.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/cache.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/checkout.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/cherry_pick.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/circleci.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/circleci_real.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/cli.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/config.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/diff.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/forensics.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/git.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_real.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_schema.graphql +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_utils.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/gpg_sign.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/land.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/log.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/logs.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/pull.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/py.typed +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/rage.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/shell.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/status.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/sync.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/trailers.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/types.py +0 -0
- {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/unlink.py +0 -0
|
@@ -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:
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|