Skip to content

Commit 89d132a

Browse files
authored
[stack-pr] Convert stack-pr.py into an installable python package (#6)
Refactor the bare scripts into a proper python package with a pyproject.toml. Add support for a build backend so stack-pr can submitted to the Python Package Index and be easily installed with pipx. Fix some minor import issues and lint warnings using ruff.
1 parent de4eef4 commit 89d132a

File tree

10 files changed

+190
-69
lines changed

10 files changed

+190
-69
lines changed

Diff for: .gitattributes

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# GitHub syntax highlighting
2+
pixi.lock linguist-language=YAML linguist-generated=true

Diff for: .gitignore

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
6+
7+
# Distribution / packaging
8+
.Python
9+
build/
10+
develop-eggs/
11+
dist/
12+
downloads/
13+
eggs/
14+
.eggs/
15+
lib/
16+
lib64/
17+
parts/
18+
sdist/
19+
var/
20+
wheels/
21+
share/python-wheels/
22+
*.egg-info/
23+
.installed.cfg
24+
*.egg
25+
MANIFEST
26+
27+
# PyInstaller
28+
# Usually these files are written by a python script from a template
29+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
30+
*.manifest
31+
*.spec
32+
33+
# Installer logs
34+
pip-log.txt
35+
pip-delete-this-directory.txt
36+
37+
# Unit test / coverage reports
38+
htmlcov/
39+
.tox/
40+
.nox/
41+
.coverage
42+
.coverage.*
43+
.cache
44+
nosetests.xml
45+
coverage.xml
46+
*.cover
47+
*.py,cover
48+
.hypothesis/
49+
.pytest_cache/
50+
cover/
51+
52+
# IPython
53+
profile_default/
54+
ipython_config.py
55+
56+
57+
# pdm
58+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
59+
pdm.lock
60+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
61+
# in version control.
62+
# https://pdm-project.org/#use-with-ide
63+
.pdm.toml
64+
.pdm-python
65+
.pdm-build/
66+
67+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
68+
__pypackages__/
69+
70+
# Environments
71+
.env
72+
.venv
73+
env/
74+
venv/
75+
ENV/
76+
env.bak/
77+
venv.bak/
78+
79+
.pdm-python
80+
81+
# pixi environments
82+
.pixi
83+
*.egg-info
84+
pixi.lock

Diff for: README.md

+28-14
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,26 @@ This is a non-comprehensive list of dependencies required by `stack-pr.py`:
2121
- Install `gh`, e.g., `brew install gh` on MacOS.
2222
- Run `gh auth login` with SSH
2323

24+
## Installation
25+
26+
To install via [pipx](https://pipx.pypa.io/stable/) run:
27+
28+
```bash
29+
pipx install stack-pr
30+
```
31+
32+
Manually, you can clone the repository and run the following command:
33+
34+
```bash
35+
pipx install .
36+
```
37+
2438
## Workflow
2539

26-
`stack-pr.py` is a script allowing you to work with stacked PRs: submit,
40+
`stack-pr` is a tool allowing you to work with stacked PRs: submit,
2741
view, and land them.
2842

29-
`stack-pr.py` tool has four commands:
43+
The `stack-pr` tool has four commands:
3044

3145
- `submit` (or `export`) - create a new stack of PRs from the given set of
3246
commits. One can think of this as “push my local changes to the corresponding
@@ -104,7 +118,7 @@ We can double-check that by running the script with `view` command - it is
104118
always a safe command to run:
105119
106120
```bash
107-
# stack-pr.py view
121+
# stack-pr view
108122
...
109123
VIEW
110124
**Stack:**
@@ -119,7 +133,7 @@ corresponding PRs and cross-link them. To do that, we run the tool with
119133
`submit` command:
120134
121135
```bash
122-
# stack-pr.py submit
136+
# stack-pr submit
123137
...
124138
SUCCESS!
125139
```
@@ -140,7 +154,7 @@ If the command succeeded, we should see “SUCCESS!” in the end, and we can no
140154
run `view` again to look at the new stack:
141155
142156
```python
143-
# stack-pr.py view
157+
# stack-pr view
144158
...
145159
VIEW
146160
**Stack:**
@@ -161,7 +175,7 @@ and run `submit` again. If needed, we can rearrange commits or add new ones.
161175
When we are ready to merge our changes, we use `land` command.
162176
163177
```python
164-
# stack-pr.py land
178+
# stack-pr land
165179
LAND
166180
Stack:
167181
* cc932b71 (#439, 'ZolotukhinM/stack/103' -> 'ZolotukhinM/stack/102'): Optimized navigation algorithms for deep space travel
@@ -181,7 +195,7 @@ This command lands the first PR of the stack and rebases the rest. If we run
181195
there:
182196
183197
```python
184-
# stack-pr.py view
198+
# stack-pr view
185199
VIEW
186200
**Stack:**
187201
* **8177f347** (#439, 'ZolotukhinM/stack/103' -> 'ZolotukhinM/stack/102'): Optimized navigation algorithms for deep space travel
@@ -198,13 +212,13 @@ the script:
198212
199213
```bash
200214
# Submit a stack of last 5 commits
201-
stack-pr.py submit -B HEAD~5
215+
stack-pr submit -B HEAD~5
202216
203217
# Use 'origin/main' instead of 'main' as the base for the stack
204-
stack-pr.py submit -B origin/main
218+
stack-pr submit -B origin/main
205219
206220
# Do not include last two commits to the stack
207-
stack-pr.py submit -H HEAD~2
221+
stack-pr submit -H HEAD~2
208222
```
209223
210224
These options work for all script commands (and it’s recommended to first use
@@ -214,12 +228,12 @@ land first three of them:
214228
215229
```bash
216230
# Inspect what commits will be included HEAD~5..HEAD
217-
stack-pr.py view -B HEAD~5
231+
stack-pr view -B HEAD~5
218232
# Create a stack from last five commits
219-
stack-pr.py submit -B HEAD~5
233+
stack-pr submit -B HEAD~5
220234
221235
# Inspect what commits will be included into the range HEAD~5..HEAD~2
222-
stack-pr.py view -B HEAD~5 -H HEAD~2
236+
stack-pr view -B HEAD~5 -H HEAD~2
223237
# Land first three PRs from the stack
224-
stack-pr.py land -B HEAD~5 -H HEAD~2
238+
stack-pr land -B HEAD~5 -H HEAD~2
225239
```

Diff for: pyproject.toml

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
[build-system]
2+
requires = ["pdm-backend"]
3+
build-backend = "pdm.backend"
4+
5+
[project]
6+
name = "stack-pr"
7+
authors = [
8+
{name = "Modular Inc", email = "[email protected]"},
9+
]
10+
maintainers = [
11+
{name = "Modular Inc", email = "[email protected]"}
12+
]
13+
description = "Stacked PRs for GitHub"
14+
version = "1.0"
15+
readme = "README.md"
16+
license = { file = "LICENSE" }
17+
requires-python = ">=3.8"
18+
keywords = ["stacked-prs", "github", "pull-requests", "stack-pr", "git", "version-control"]
19+
classifiers = [
20+
"Development Status :: 5 - Production/Stable",
21+
"Intended Audience :: Developers",
22+
"Topic :: Software Development :: Version Control :: Git",
23+
"License :: OSI Approved :: Apache Software License",
24+
"Programming Language :: Python",
25+
]
26+
dependencies = []
27+
28+
[project.urls]
29+
Homepage = "https://github.com/modularml/stack-pr"
30+
Repository = "https://github.com/modularml/stack-pr"
31+
"Bug Tracker" = "https://github.com/modularml/stack-pr/issues"
32+
33+
[project.scripts]
34+
stack-pr = "stack_pr.cli:main"
35+
36+
[tool.pdm]
37+
distribution = true
38+
39+
[tool.pixi.project]
40+
channels = ["conda-forge"]
41+
platforms = ["osx-arm64", "osx-64", "linux-64", "linux-aarch64"]
42+
43+
[tool.pixi.pypi-dependencies]
44+
stack-pr = { path = ".", editable = true }
45+
46+
[tool.pixi.tasks]
47+
48+
[tool.pixi.dependencies]
49+
python = ">=3.8"

Diff for: src/stack_pr/__init__.py

Whitespace-only changes.

Diff for: src/stack_pr/__main__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .stack_pr import main
2+
3+
if __name__ == "__main__":
4+
main()

Diff for: stack-pr.py renamed to src/stack_pr/cli.py

+14-37
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@
5555
import re
5656
from subprocess import SubprocessError
5757

58-
from git import (
58+
from .git import (
5959
branch_exists,
6060
check_gh_installed,
6161
get_current_branch_name,
6262
get_gh_username,
6363
get_uncommitted_changes,
6464
)
65-
from shell_commands import get_command_output, run_shell_command
65+
from .shell_commands import get_command_output, run_shell_command
6666
from typing import List, NamedTuple, Optional, Pattern
6767

6868
# A bunch of regexps for parsing commit messages and PR descriptions
@@ -199,9 +199,7 @@ def commit_id(self) -> str:
199199
return self._search_group(RE_RAW_COMMIT_ID, "commit")
200200

201201
def parents(self) -> List[str]:
202-
return [
203-
m.group("commit") for m in RE_RAW_PARENT.finditer(self.raw_header)
204-
]
202+
return [m.group("commit") for m in RE_RAW_PARENT.finditer(self.raw_header)]
205203

206204
def author(self) -> str:
207205
return self._search_group(RE_RAW_AUTHOR, "author")
@@ -214,8 +212,7 @@ def author_email(self) -> str:
214212

215213
def commit_msg(self) -> str:
216214
return "\n".join(
217-
m.group("line")
218-
for m in RE_RAW_COMMIT_MSG_LINE.finditer(self.raw_header)
215+
m.group("line") for m in RE_RAW_COMMIT_MSG_LINE.finditer(self.raw_header)
219216
)
220217

221218

@@ -408,9 +405,7 @@ def get_stack(base: str, head: str) -> List[StackEntry]:
408405
st: List[StackEntry] = []
409406
stack = (
410407
split_header(
411-
get_command_output(
412-
["git", "rev-list", "--header", "^" + base, head]
413-
)
408+
get_command_output(["git", "rev-list", "--header", "^" + base, head])
414409
)
415410
)[::-1]
416411

@@ -561,9 +556,7 @@ def init_local_branches(st: List[StackEntry], remote: str):
561556
log(h("Initializing local branches"), level=1)
562557
set_head_branches(st, remote)
563558
for e in st:
564-
run_shell_command(
565-
["git", "checkout", e.commit.commit_id(), "-B", e.head]
566-
)
559+
run_shell_command(["git", "checkout", e.commit.commit_id(), "-B", e.head])
567560

568561

569562
def push_branches(st: List[StackEntry], remote):
@@ -574,12 +567,8 @@ def push_branches(st: List[StackEntry], remote):
574567

575568

576569
def print_cmd_failure_details(exc: SubprocessError):
577-
cmd_stdout = (
578-
exc.stdout.decode("utf-8").replace("\\n", "\n").replace("\\t", "\t")
579-
)
580-
cmd_stderr = (
581-
exc.stderr.decode("utf-8").replace("\\n", "\n").replace("\\t", "\t")
582-
)
570+
cmd_stdout = exc.stdout.decode("utf-8").replace("\\n", "\n").replace("\\t", "\t")
571+
cmd_stderr = exc.stderr.decode("utf-8").replace("\\n", "\n").replace("\\t", "\t")
583572
print(f"Exitcode: {exc.returncode}")
584573
print(f"Stdout: {cmd_stdout}")
585574
print(f"Stderr: {cmd_stderr}")
@@ -656,9 +645,7 @@ def add_cross_links(st: List[StackEntry], keep_body: bool):
656645
if keep_body:
657646
# Keep current body of the PR after the cross links component
658647
current_pr_body = get_current_pr_body(e)
659-
pr_body.append(
660-
current_pr_body.split(CROSS_LINKS_DELIMETER, 1)[-1].lstrip()
661-
)
648+
pr_body.append(current_pr_body.split(CROSS_LINKS_DELIMETER, 1)[-1].lstrip())
662649
else:
663650
pr_body.extend(
664651
[
@@ -827,9 +814,7 @@ def command_submit(
827814
# Now we have all the branches, so we can create the corresponding PRs
828815
log(h("Submitting PRs"), level=1)
829816
for e_idx, e in enumerate(st):
830-
is_pr_draft = draft or (
831-
(draft_bitmask is not None) and draft_bitmask[e_idx]
832-
)
817+
is_pr_draft = draft or ((draft_bitmask is not None) and draft_bitmask[e_idx])
833818
create_pr(e, is_pr_draft, reviewer)
834819

835820
# Verify consistency in everything we have so far
@@ -998,9 +983,7 @@ def command_land(args: CommonArgs):
998983
for e in prs_to_rebase:
999984
rebase_pr(e, args.remote, args.target)
1000985
# Change the target of the new bottom-most PR in the stack to 'target'
1001-
run_shell_command(
1002-
["gh", "pr", "edit", prs_to_rebase[0].pr, "-B", args.target]
1003-
)
986+
run_shell_command(["gh", "pr", "edit", prs_to_rebase[0].pr, "-B", args.target])
1004987

1005988
# Delete local and remote stack branches
1006989
run_shell_command(["git", "checkout", current_branch])
@@ -1013,9 +996,7 @@ def command_land(args: CommonArgs):
1013996
run_shell_command(
1014997
["git", "rebase", f"{args.remote}/{args.target}", args.target]
1015998
)
1016-
run_shell_command(
1017-
["git", "rebase", f"{args.remote}/{args.target}", current_branch]
1018-
)
999+
run_shell_command(["git", "rebase", f"{args.remote}/{args.target}", current_branch])
10191000

10201001
log(h(blue("SUCCESS!")), level=1)
10211002

@@ -1143,13 +1124,9 @@ def create_argparser() -> argparse.ArgumentParser:
11431124
subparsers = parser.add_subparsers(help="sub-command help", dest="command")
11441125

11451126
common_parser = argparse.ArgumentParser(add_help=False)
1146-
common_parser.add_argument(
1147-
"-R", "--remote", default="origin", help="Remote name"
1148-
)
1127+
common_parser.add_argument("-R", "--remote", default="origin", help="Remote name")
11491128
common_parser.add_argument("-B", "--base", help="Local base branch")
1150-
common_parser.add_argument(
1151-
"-H", "--head", default="HEAD", help="Local head branch"
1152-
)
1129+
common_parser.add_argument("-H", "--head", default="HEAD", help="Local head branch")
11531130
common_parser.add_argument(
11541131
"-T", "--target", default="main", help="Remote target branch"
11551132
)

0 commit comments

Comments
 (0)