ghstack 0.14.0__tar.gz → 0.16.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 (41) hide show
  1. {ghstack-0.14.0 → ghstack-0.16.0}/PKG-INFO +30 -8
  2. {ghstack-0.14.0 → ghstack-0.16.0}/README.md +29 -7
  3. {ghstack-0.14.0 → ghstack-0.16.0}/pyproject.toml +1 -1
  4. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/action.py +4 -4
  5. ghstack-0.16.0/src/ghstack/async_script.py +57 -0
  6. ghstack-0.16.0/src/ghstack/checkout.py +94 -0
  7. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/cherry_pick.py +11 -9
  8. ghstack-0.16.0/src/ghstack/cli.py +602 -0
  9. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/config.py +41 -7
  10. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/forensics.py +2 -2
  11. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/github.py +11 -26
  12. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/github_fake.py +59 -26
  13. ghstack-0.16.0/src/ghstack/github_real.py +295 -0
  14. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/github_utils.py +44 -12
  15. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/gpg_sign.py +2 -2
  16. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/land.py +53 -24
  17. ghstack-0.16.0/src/ghstack/log.py +117 -0
  18. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/logs.py +154 -4
  19. ghstack-0.16.0/src/ghstack/pull.py +270 -0
  20. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/shell.py +52 -30
  21. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/status.py +2 -2
  22. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/submit.py +868 -307
  23. ghstack-0.16.0/src/ghstack/sync.py +141 -0
  24. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/test_prelude.py +125 -49
  25. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/unlink.py +10 -9
  26. ghstack-0.14.0/src/ghstack/checkout.py +0 -64
  27. ghstack-0.14.0/src/ghstack/cli.py +0 -342
  28. ghstack-0.14.0/src/ghstack/github_real.py +0 -243
  29. {ghstack-0.14.0 → ghstack-0.16.0}/LICENSE +0 -0
  30. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/__init__.py +0 -0
  31. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/__main__.py +0 -0
  32. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/cache.py +0 -0
  33. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/circleci.py +0 -0
  34. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/circleci_real.py +0 -0
  35. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/diff.py +0 -0
  36. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/git.py +0 -0
  37. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/github_schema.graphql +0 -0
  38. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/py.typed +0 -0
  39. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/rage.py +0 -0
  40. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/trailers.py +0 -0
  41. {ghstack-0.14.0 → ghstack-0.16.0}/src/ghstack/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghstack
3
- Version: 0.14.0
3
+ Version: 0.16.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>
@@ -28,7 +28,7 @@ uv tool install ghstack
28
28
  ```
29
29
 
30
30
  ghstack is tested with
31
- [several different Python versions](https://github.com/ezyang/ghstack/blob/master/.github/workflows/test.yml#L13).
31
+ [several different Python versions](https://github.com/ezyang/ghstack/blob/main/.github/workflows/test.yml#L13).
32
32
  It requires at least Python 3.9.1.
33
33
 
34
34
  ## How to setup
@@ -43,13 +43,30 @@ github_url = github.com
43
43
  github_oauth = [your_own_token]
44
44
  github_username = [your_username]
45
45
  remote_name = upstream [if remote is called upstream and not origin]
46
+ automsg = claude [optional; also supports codex]
46
47
  ```
47
48
 
49
+ You can toggle generated change descriptions with:
50
+
51
+ ```
52
+ ghstack config automsg claude
53
+ ghstack config automsg codex
54
+ ghstack config automsg codex --model gpt-5.4
55
+ ghstack config --unset automsg
56
+ ```
57
+
58
+ If `automsg` is set to `claude` or `codex`, ghstack writes
59
+ PR context (the local commit message and current PR description) to a temporary
60
+ file and invokes the agent with a prompt to summarize the specific update. The
61
+ patch update itself is included directly in the prompt. For existing PRs the
62
+ patch update is an interdiff against the previously submitted PR patch, not the
63
+ whole PR patch. Explicit `ghstack -m MESSAGE` still overrides this.
64
+
48
65
  ## How to use
49
66
 
50
67
  Make sure you have write permission to the repo you're opening PR with.
51
68
 
52
- Prepare a series of commits on top of master, then run `ghstack`. This
69
+ Prepare a series of commits on top of main, then run `ghstack`. This
53
70
  tool will push and create pull requests for each commit on the stack.
54
71
 
55
72
  **How do I stack another PR on top of an existing one?** Assuming
@@ -61,17 +78,22 @@ run `ghstack` again. If the commit is at the top of your stack,
61
78
  you can edit it with `git commit --amend`; otherwise, you'll have
62
79
  to use `git rebase -i` to edit the commit directly.
63
80
 
64
- **How do I rebase?** The obvious way: `git rebase origin/master`.
81
+ **How do I rebase?** The obvious way: `git rebase origin/main`.
65
82
  Don't do a `git merge`; `ghstack` will throw a hissy fit if you
66
83
  do that. (There's also a more fundamental reason why this
67
84
  won't work: since each commit is a separate PR, you have to
68
85
  resolve conflicts in *each* PR, not just for the entire stack.)
69
86
 
70
- **How do I start a new feature?** Just checkout master on a new
87
+ **What if the repository default branch changed?** ghstack caches
88
+ repository metadata in `.git/ghstack-repo-info.json` for the local
89
+ checkout. If ghstack is still using an old default branch name,
90
+ delete that file and rerun ghstack; it will query GitHub again.
91
+
92
+ **How do I start a new feature?** Just checkout main on a new
71
93
  branch, and start working on a fresh branch.
72
94
 
73
95
  **How do I merge my changes?** Warning: You will NOT be able to merge these
74
- commits using the normal GitHub UI, as their branch bases won't be master.
96
+ commits using the normal GitHub UI, as their branch bases won't be main.
75
97
  Use `ghstack land $PR_URL` (or alternatively `ghstack land #PR_NUM`) to land
76
98
  a ghstack'ed pull request.
77
99
 
@@ -84,10 +106,10 @@ is an end-to-end example of how to do this.
84
106
  Every commit in your local commit stack gets submitted into a separate
85
107
  pull request and pushes commits onto three branches:
86
108
 
87
- * `gh/username/1/base` - think of this like "master": it's the base
109
+ * `gh/username/1/base` - think of this like "main": it's the base
88
110
  branch that your commit was based upon. It is never force pushed;
89
111
  whenever you rebase your local stack, we add merge commits on top of
90
- base from the true upstream master.
112
+ base from the true upstream main.
91
113
 
92
114
  * `gh/username/1/head` - this branch is your change, on top of the base
93
115
  branch. Like base, it is never force pushed. We open a pull request
@@ -7,7 +7,7 @@ uv tool install ghstack
7
7
  ```
8
8
 
9
9
  ghstack is tested with
10
- [several different Python versions](https://github.com/ezyang/ghstack/blob/master/.github/workflows/test.yml#L13).
10
+ [several different Python versions](https://github.com/ezyang/ghstack/blob/main/.github/workflows/test.yml#L13).
11
11
  It requires at least Python 3.9.1.
12
12
 
13
13
  ## How to setup
@@ -22,13 +22,30 @@ github_url = github.com
22
22
  github_oauth = [your_own_token]
23
23
  github_username = [your_username]
24
24
  remote_name = upstream [if remote is called upstream and not origin]
25
+ automsg = claude [optional; also supports codex]
25
26
  ```
26
27
 
28
+ You can toggle generated change descriptions with:
29
+
30
+ ```
31
+ ghstack config automsg claude
32
+ ghstack config automsg codex
33
+ ghstack config automsg codex --model gpt-5.4
34
+ ghstack config --unset automsg
35
+ ```
36
+
37
+ If `automsg` is set to `claude` or `codex`, ghstack writes
38
+ PR context (the local commit message and current PR description) to a temporary
39
+ file and invokes the agent with a prompt to summarize the specific update. The
40
+ patch update itself is included directly in the prompt. For existing PRs the
41
+ patch update is an interdiff against the previously submitted PR patch, not the
42
+ whole PR patch. Explicit `ghstack -m MESSAGE` still overrides this.
43
+
27
44
  ## How to use
28
45
 
29
46
  Make sure you have write permission to the repo you're opening PR with.
30
47
 
31
- Prepare a series of commits on top of master, then run `ghstack`. This
48
+ Prepare a series of commits on top of main, then run `ghstack`. This
32
49
  tool will push and create pull requests for each commit on the stack.
33
50
 
34
51
  **How do I stack another PR on top of an existing one?** Assuming
@@ -40,17 +57,22 @@ run `ghstack` again. If the commit is at the top of your stack,
40
57
  you can edit it with `git commit --amend`; otherwise, you'll have
41
58
  to use `git rebase -i` to edit the commit directly.
42
59
 
43
- **How do I rebase?** The obvious way: `git rebase origin/master`.
60
+ **How do I rebase?** The obvious way: `git rebase origin/main`.
44
61
  Don't do a `git merge`; `ghstack` will throw a hissy fit if you
45
62
  do that. (There's also a more fundamental reason why this
46
63
  won't work: since each commit is a separate PR, you have to
47
64
  resolve conflicts in *each* PR, not just for the entire stack.)
48
65
 
49
- **How do I start a new feature?** Just checkout master on a new
66
+ **What if the repository default branch changed?** ghstack caches
67
+ repository metadata in `.git/ghstack-repo-info.json` for the local
68
+ checkout. If ghstack is still using an old default branch name,
69
+ delete that file and rerun ghstack; it will query GitHub again.
70
+
71
+ **How do I start a new feature?** Just checkout main on a new
50
72
  branch, and start working on a fresh branch.
51
73
 
52
74
  **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.
75
+ commits using the normal GitHub UI, as their branch bases won't be main.
54
76
  Use `ghstack land $PR_URL` (or alternatively `ghstack land #PR_NUM`) to land
55
77
  a ghstack'ed pull request.
56
78
 
@@ -63,10 +85,10 @@ is an end-to-end example of how to do this.
63
85
  Every commit in your local commit stack gets submitted into a separate
64
86
  pull request and pushes commits onto three branches:
65
87
 
66
- * `gh/username/1/base` - think of this like "master": it's the base
88
+ * `gh/username/1/base` - think of this like "main": it's the base
67
89
  branch that your commit was based upon. It is never force pushed;
68
90
  whenever you rebase your local stack, we add merge commits on top of
69
- base from the true upstream master.
91
+ base from the true upstream main.
70
92
 
71
93
  * `gh/username/1/head` - this branch is your change, on top of the base
72
94
  branch. Like base, it is never force pushed. We open a pull request
@@ -16,7 +16,7 @@ dependencies = [
16
16
  "flake8<8.0.0,>=7.0.0",
17
17
  ]
18
18
  name = "ghstack"
19
- version = "0.14.0"
19
+ version = "0.16.0"
20
20
  description = "Stack diff support for GitHub"
21
21
  readme = "README.md"
22
22
  classifiers = [
@@ -8,15 +8,15 @@ import ghstack.github_utils
8
8
  import ghstack.shell
9
9
 
10
10
 
11
- def main(
11
+ async def main(
12
12
  pull_request: str,
13
13
  github: ghstack.github.GitHubEndpoint,
14
14
  sh: Optional[ghstack.shell.Shell] = None,
15
15
  close: bool = False,
16
16
  ) -> None:
17
17
 
18
- params = ghstack.github_utils.parse_pull_request(pull_request)
19
- pr_result = github.graphql(
18
+ params = await ghstack.github_utils.parse_pull_request(pull_request)
19
+ pr_result = await github.graphql(
20
20
  """
21
21
  query ($owner: String!, $name: String!, $number: Int!) {
22
22
  repository(name: $name, owner: $owner) {
@@ -32,7 +32,7 @@ def main(
32
32
 
33
33
  if close:
34
34
  logging.info("Closing {owner}/{name}#{number}".format(**params))
35
- github.graphql(
35
+ await github.graphql(
36
36
  """
37
37
  mutation ($input: ClosePullRequestInput!) {
38
38
  closePullRequest(input: $input) {
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import ast
5
+ import asyncio
6
+ import inspect
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Dict, Optional, Sequence
10
+
11
+
12
+ async def run_path(
13
+ path: str,
14
+ *,
15
+ argv: Optional[Sequence[str]] = None,
16
+ globals_: Optional[Dict[str, Any]] = None,
17
+ ) -> Dict[str, Any]:
18
+ script_path = str(Path(path))
19
+ script_argv = [script_path, *(argv or ())]
20
+ old_argv = sys.argv
21
+ sys.argv = script_argv
22
+ try:
23
+ source = Path(script_path).read_text()
24
+ namespace: Dict[str, Any] = {
25
+ "__file__": script_path,
26
+ "__name__": "__main__",
27
+ "__package__": None,
28
+ "__builtins__": __builtins__,
29
+ }
30
+ if globals_ is not None:
31
+ namespace.update(globals_)
32
+ code = compile(
33
+ source,
34
+ script_path,
35
+ "exec",
36
+ flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT,
37
+ )
38
+ result = eval(code, namespace)
39
+ if inspect.isawaitable(result):
40
+ await result
41
+ return namespace
42
+ finally:
43
+ sys.argv = old_argv
44
+
45
+
46
+ def main(argv: Optional[Sequence[str]] = None) -> None:
47
+ parser = argparse.ArgumentParser(
48
+ description="Run a Python script with top-level await support."
49
+ )
50
+ parser.add_argument("script")
51
+ parser.add_argument("args", nargs=argparse.REMAINDER)
52
+ ns = parser.parse_args(argv)
53
+ asyncio.run(run_path(ns.script, argv=ns.args))
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import asyncio
4
+ import logging
5
+ import re
6
+ from typing import Iterable
7
+
8
+ import ghstack.github
9
+ import ghstack.github_utils
10
+ import ghstack.shell
11
+
12
+
13
+ async def _fetch_refs(
14
+ sh: ghstack.shell.Shell, *, remote_name: str, refs: Iterable[str]
15
+ ) -> None:
16
+ refspecs = [
17
+ f"+refs/heads/{ref}:refs/remotes/{remote_name}/{ref}"
18
+ for ref in sorted(set(refs))
19
+ ]
20
+ await sh.agit("fetch", "--prune", remote_name, *refspecs)
21
+
22
+
23
+ async def main(
24
+ pull_request: str,
25
+ github: ghstack.github.GitHubEndpoint,
26
+ sh: ghstack.shell.Shell,
27
+ remote_name: str,
28
+ same_base: bool = False,
29
+ ) -> None:
30
+
31
+ params = await ghstack.github_utils.parse_pull_request(
32
+ pull_request, sh=sh, remote_name=remote_name
33
+ )
34
+ head_ref_task = asyncio.ensure_future(github.get_head_ref(**params))
35
+
36
+ if same_base:
37
+ repo_info_task = asyncio.ensure_future(
38
+ ghstack.github_utils.get_github_repo_info(
39
+ github=github,
40
+ sh=sh,
41
+ repo_owner=params["owner"],
42
+ repo_name=params["name"],
43
+ github_url=params["github_url"],
44
+ remote_name=remote_name,
45
+ )
46
+ )
47
+ head_ref, repo_info = await asyncio.gather(head_ref_task, repo_info_task)
48
+ else:
49
+ head_ref = await head_ref_task
50
+ repo_info = None
51
+
52
+ orig_ref = re.sub(r"/head$", "/orig", head_ref)
53
+ if orig_ref == head_ref:
54
+ logging.warning(
55
+ "The ref {} doesn't look like a ghstack reference; "
56
+ "delegating to gh pr checkout".format(head_ref)
57
+ )
58
+ await sh.ash("gh", "pr", "checkout", pull_request.lstrip("#"))
59
+ return
60
+
61
+ # TODO: Handle remotes correctly too (so this subsumes hub)
62
+
63
+ # If --same-base is specified, check if checkout would change the merge-base
64
+ if same_base:
65
+ assert repo_info is not None
66
+ default_branch = repo_info["default_branch"]
67
+ default_branch_ref = f"{remote_name}/{default_branch}"
68
+
69
+ # Get current merge-base with default branch
70
+ current_base = await sh.agit("merge-base", default_branch_ref, "HEAD")
71
+ else:
72
+ current_base = None
73
+ default_branch_ref = None
74
+
75
+ refs_to_fetch = [orig_ref]
76
+ if same_base:
77
+ assert repo_info is not None
78
+ refs_to_fetch.append(repo_info["default_branch"])
79
+ await _fetch_refs(sh, remote_name=remote_name, refs=refs_to_fetch)
80
+
81
+ # If --same-base is specified, check what the new merge-base would be
82
+ if same_base:
83
+ assert default_branch_ref is not None
84
+ assert current_base is not None
85
+ target_ref = remote_name + "/" + orig_ref
86
+ new_base = await sh.agit("merge-base", default_branch_ref, target_ref)
87
+
88
+ if current_base != new_base:
89
+ raise RuntimeError(
90
+ f"Checkout would change merge-base from {current_base[:8]} to {new_base[:8]}, "
91
+ f"aborting due to --same-base flag"
92
+ )
93
+
94
+ await sh.agit("checkout", remote_name + "/" + orig_ref)
@@ -8,32 +8,34 @@ import ghstack.github_utils
8
8
  import ghstack.shell
9
9
 
10
10
 
11
- def main(
11
+ async def main(
12
12
  pull_request: str,
13
13
  github: ghstack.github.GitHubEndpoint,
14
14
  sh: ghstack.shell.Shell,
15
15
  remote_name: str,
16
16
  stack: bool = False,
17
+ no_fetch: bool = False,
17
18
  ) -> None:
18
19
 
19
- params = ghstack.github_utils.parse_pull_request(
20
+ params = await ghstack.github_utils.parse_pull_request(
20
21
  pull_request, sh=sh, remote_name=remote_name
21
22
  )
22
- head_ref = github.get_head_ref(**params)
23
+ head_ref = await github.get_head_ref(**params)
23
24
  orig_ref = re.sub(r"/head$", "/orig", head_ref)
24
25
  if orig_ref == head_ref:
25
26
  logging.warning(
26
27
  "The ref {} doesn't look like a ghstack reference".format(head_ref)
27
28
  )
28
29
 
29
- sh.git("fetch", "--prune", remote_name)
30
+ if not no_fetch:
31
+ await sh.agit("fetch", "--prune", remote_name)
30
32
 
31
33
  if stack:
32
34
  # Cherry-pick the entire stack from merge-base to the commit
33
35
  remote_orig_ref = remote_name + "/" + orig_ref
34
36
 
35
37
  # Find the merge-base with the main branch
36
- repo_info = ghstack.github_utils.get_github_repo_info(
38
+ repo_info = await ghstack.github_utils.get_github_repo_info(
37
39
  github=github,
38
40
  sh=sh,
39
41
  github_url=params["github_url"],
@@ -42,11 +44,11 @@ def main(
42
44
  main_branch = f"{remote_name}/{repo_info['default_branch']}"
43
45
 
44
46
  # Get merge-base between the commit and main branch
45
- merge_base = sh.git("merge-base", main_branch, remote_orig_ref).strip()
47
+ merge_base = (await sh.agit("merge-base", main_branch, remote_orig_ref)).strip()
46
48
 
47
49
  # Get all commits from merge-base to the target commit
48
50
  commit_list = (
49
- sh.git("rev-list", "--reverse", f"{merge_base}..{remote_orig_ref}")
51
+ (await sh.agit("rev-list", "--reverse", f"{merge_base}..{remote_orig_ref}"))
50
52
  .strip()
51
53
  .split("\n")
52
54
  )
@@ -56,10 +58,10 @@ def main(
56
58
 
57
59
  logging.info(f"Cherry-picking {len(commit_list)} commits from stack")
58
60
  for commit in commit_list:
59
- sh.git("cherry-pick", commit)
61
+ await sh.agit("cherry-pick", commit)
60
62
  logging.info(f"Cherry-picked {commit}")
61
63
  else:
62
64
  # Cherry-pick just the single commit
63
65
  remote_orig_ref = remote_name + "/" + orig_ref
64
- sh.git("cherry-pick", remote_orig_ref)
66
+ await sh.agit("cherry-pick", remote_orig_ref)
65
67
  logging.info(f"Cherry-picked {orig_ref}")