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.
Files changed (38) hide show
  1. {ghstack-0.17.0 → ghstack-0.18.0}/PKG-INFO +1 -1
  2. {ghstack-0.17.0 → ghstack-0.18.0}/pyproject.toml +1 -1
  3. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_fake.py +31 -7
  4. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/submit.py +38 -6
  5. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/test_prelude.py +10 -7
  6. {ghstack-0.17.0 → ghstack-0.18.0}/LICENSE +0 -0
  7. {ghstack-0.17.0 → ghstack-0.18.0}/README.md +0 -0
  8. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/__init__.py +0 -0
  9. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/__main__.py +0 -0
  10. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/action.py +0 -0
  11. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/async_script.py +0 -0
  12. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/cache.py +0 -0
  13. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/checkout.py +0 -0
  14. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/cherry_pick.py +0 -0
  15. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/circleci.py +0 -0
  16. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/circleci_real.py +0 -0
  17. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/cli.py +0 -0
  18. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/config.py +0 -0
  19. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/diff.py +0 -0
  20. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/forensics.py +0 -0
  21. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/git.py +0 -0
  22. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github.py +0 -0
  23. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_real.py +0 -0
  24. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_schema.graphql +0 -0
  25. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/github_utils.py +0 -0
  26. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/gpg_sign.py +0 -0
  27. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/land.py +0 -0
  28. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/log.py +0 -0
  29. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/logs.py +0 -0
  30. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/pull.py +0 -0
  31. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/py.typed +0 -0
  32. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/rage.py +0 -0
  33. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/shell.py +0 -0
  34. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/status.py +0 -0
  35. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/sync.py +0 -0
  36. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/trailers.py +0 -0
  37. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/types.py +0 -0
  38. {ghstack-0.17.0 → ghstack-0.18.0}/src/ghstack/unlink.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghstack
3
- Version: 0.17.0
3
+ Version: 0.18.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>
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "flake8<8.0.0,>=7.0.0",
17
17
  ]
18
18
  name = "ghstack"
19
- version = "0.17.0"
19
+ version = "0.18.0"
20
20
  description = "Stack diff support for GitHub"
21
21
  readme = "README.md"
22
22
  classifiers = [
@@ -125,13 +125,32 @@ class GitHubState:
125
125
  return r
126
126
 
127
127
  def push_hook(self, refs: Sequence[str]) -> None:
128
- # updated_refs = set(refs)
129
- # for pr in self.pull_requests:
130
- # # TODO: this assumes only origin repository
131
- # # if pr.headRefName in updated_refs:
132
- # # pr.headRef =
133
- # pass
134
- pass
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
- # Check if direct should be used, if the user didn't explicitly
520
- # specify an option
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 not direct:
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=self.direct,
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