ghstack 0.13.0__tar.gz → 0.14.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.13.0 → ghstack-0.14.0}/PKG-INFO +1 -1
- {ghstack-0.13.0 → ghstack-0.14.0}/pyproject.toml +1 -1
- ghstack-0.14.0/src/ghstack/checkout.py +64 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/cli.py +23 -1
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/config.py +101 -1
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/github_fake.py +21 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/github_utils.py +17 -3
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/submit.py +32 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/test_prelude.py +39 -4
- ghstack-0.13.0/src/ghstack/checkout.py +0 -31
- {ghstack-0.13.0 → ghstack-0.14.0}/LICENSE +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/README.md +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/__init__.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/__main__.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/action.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/cache.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/cherry_pick.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/circleci.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/circleci_real.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/diff.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/forensics.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/git.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/github.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/github_real.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/github_schema.graphql +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/gpg_sign.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/land.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/logs.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/py.typed +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/rage.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/shell.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/status.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/trailers.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/types.py +0 -0
- {ghstack-0.13.0 → ghstack-0.14.0}/src/ghstack/unlink.py +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import ghstack.github
|
|
7
|
+
import ghstack.github_utils
|
|
8
|
+
import ghstack.shell
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(
|
|
12
|
+
pull_request: str,
|
|
13
|
+
github: ghstack.github.GitHubEndpoint,
|
|
14
|
+
sh: ghstack.shell.Shell,
|
|
15
|
+
remote_name: str,
|
|
16
|
+
same_base: bool = False,
|
|
17
|
+
) -> None:
|
|
18
|
+
|
|
19
|
+
params = ghstack.github_utils.parse_pull_request(
|
|
20
|
+
pull_request, sh=sh, remote_name=remote_name
|
|
21
|
+
)
|
|
22
|
+
head_ref = github.get_head_ref(**params)
|
|
23
|
+
orig_ref = re.sub(r"/head$", "/orig", head_ref)
|
|
24
|
+
if orig_ref == head_ref:
|
|
25
|
+
logging.warning(
|
|
26
|
+
"The ref {} doesn't look like a ghstack reference".format(head_ref)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# TODO: Handle remotes correctly too (so this subsumes hub)
|
|
30
|
+
|
|
31
|
+
# If --same-base is specified, check if checkout would change the merge-base
|
|
32
|
+
if same_base:
|
|
33
|
+
# Get the default branch name from the repo
|
|
34
|
+
repo_info = ghstack.github_utils.get_github_repo_info(
|
|
35
|
+
github=github,
|
|
36
|
+
sh=sh,
|
|
37
|
+
repo_owner=params["owner"],
|
|
38
|
+
repo_name=params["name"],
|
|
39
|
+
github_url=params["github_url"],
|
|
40
|
+
remote_name=remote_name,
|
|
41
|
+
)
|
|
42
|
+
default_branch = repo_info["default_branch"]
|
|
43
|
+
default_branch_ref = f"{remote_name}/{default_branch}"
|
|
44
|
+
|
|
45
|
+
# Get current merge-base with default branch
|
|
46
|
+
current_base = sh.git("merge-base", default_branch_ref, "HEAD")
|
|
47
|
+
else:
|
|
48
|
+
current_base = None
|
|
49
|
+
default_branch_ref = None
|
|
50
|
+
|
|
51
|
+
sh.git("fetch", "--prune", remote_name)
|
|
52
|
+
|
|
53
|
+
# If --same-base is specified, check what the new merge-base would be
|
|
54
|
+
if same_base:
|
|
55
|
+
target_ref = remote_name + "/" + orig_ref
|
|
56
|
+
new_base = sh.git("merge-base", default_branch_ref, target_ref)
|
|
57
|
+
|
|
58
|
+
if current_base != new_base:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
f"Checkout would change merge-base from {current_base[:8]} to {new_base[:8]}, "
|
|
61
|
+
f"aborting due to --same-base flag"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
sh.git("checkout", remote_name + "/" + orig_ref)
|
|
@@ -120,8 +120,13 @@ def action(close: bool, pull_request: str) -> None:
|
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
@main.command("checkout")
|
|
123
|
+
@click.option(
|
|
124
|
+
"--same-base",
|
|
125
|
+
is_flag=True,
|
|
126
|
+
help="Only checkout if merge-base with main branch would remain the same",
|
|
127
|
+
)
|
|
123
128
|
@click.argument("pull_request", metavar="PR")
|
|
124
|
-
def checkout(pull_request: str) -> None:
|
|
129
|
+
def checkout(same_base: bool, pull_request: str) -> None:
|
|
125
130
|
"""
|
|
126
131
|
Checkout a PR
|
|
127
132
|
"""
|
|
@@ -131,6 +136,7 @@ def checkout(pull_request: str) -> None:
|
|
|
131
136
|
github=github,
|
|
132
137
|
sh=shell,
|
|
133
138
|
remote_name=config.remote_name,
|
|
139
|
+
same_base=same_base,
|
|
134
140
|
)
|
|
135
141
|
|
|
136
142
|
|
|
@@ -257,6 +263,18 @@ def status(pull_request: str) -> None:
|
|
|
257
263
|
"With --no-stack, we support only non-range identifiers, and will submit each commit "
|
|
258
264
|
"listed in the command line.",
|
|
259
265
|
)
|
|
266
|
+
@click.option(
|
|
267
|
+
"--reviewer",
|
|
268
|
+
default=None,
|
|
269
|
+
help="Comma-separated list of GitHub usernames to add as reviewers to new PRs "
|
|
270
|
+
"(overrides .ghstackrc setting)",
|
|
271
|
+
)
|
|
272
|
+
@click.option(
|
|
273
|
+
"--label",
|
|
274
|
+
default=None,
|
|
275
|
+
help="Comma-separated list of labels to add to new PRs "
|
|
276
|
+
"(overrides .ghstackrc setting)",
|
|
277
|
+
)
|
|
260
278
|
@click.option(
|
|
261
279
|
"--direct/--no-direct",
|
|
262
280
|
"direct_opt",
|
|
@@ -280,6 +298,8 @@ def submit(
|
|
|
280
298
|
base: Optional[str],
|
|
281
299
|
revs: Tuple[str, ...],
|
|
282
300
|
stack: bool,
|
|
301
|
+
reviewer: Optional[str],
|
|
302
|
+
label: Optional[str],
|
|
283
303
|
) -> None:
|
|
284
304
|
"""
|
|
285
305
|
Submit or update a PR stack
|
|
@@ -301,6 +321,8 @@ def submit(
|
|
|
301
321
|
revs=revs,
|
|
302
322
|
stack=stack,
|
|
303
323
|
direct_opt=direct_opt,
|
|
324
|
+
reviewer=reviewer if reviewer is not None else config.reviewer,
|
|
325
|
+
label=label if label is not None else config.label,
|
|
304
326
|
)
|
|
305
327
|
|
|
306
328
|
|
|
@@ -5,8 +5,10 @@ import getpass
|
|
|
5
5
|
import logging
|
|
6
6
|
import os
|
|
7
7
|
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from typing import NamedTuple, Optional
|
|
11
|
+
from typing import NamedTuple, Optional, Tuple
|
|
10
12
|
|
|
11
13
|
import requests
|
|
12
14
|
|
|
@@ -15,6 +17,71 @@ import ghstack.logs
|
|
|
15
17
|
DEFAULT_GHSTACKRC_PATH = Path.home() / ".ghstackrc"
|
|
16
18
|
GHSTACKRC_PATH_VAR = "GHSTACKRC_PATH"
|
|
17
19
|
|
|
20
|
+
|
|
21
|
+
def is_gh_cli_available() -> bool:
|
|
22
|
+
"""Check if the GitHub CLI (gh) is available in PATH."""
|
|
23
|
+
return shutil.which("gh") is not None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_gh_cli_credentials(
|
|
27
|
+
github_url: str = "github.com",
|
|
28
|
+
) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
|
29
|
+
"""
|
|
30
|
+
Extract credentials from the GitHub CLI if available and authenticated.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
github_url: The GitHub host to get credentials for.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
A tuple of (token, username, url) or (None, None, None) if unavailable.
|
|
37
|
+
"""
|
|
38
|
+
if not is_gh_cli_available():
|
|
39
|
+
return None, None, None
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
# Check if gh is authenticated for this host
|
|
43
|
+
auth_status = subprocess.run(
|
|
44
|
+
["gh", "auth", "status", "-h", github_url],
|
|
45
|
+
capture_output=True,
|
|
46
|
+
text=True,
|
|
47
|
+
)
|
|
48
|
+
if auth_status.returncode != 0:
|
|
49
|
+
logging.debug(f"gh CLI not authenticated for {github_url}")
|
|
50
|
+
return None, None, None
|
|
51
|
+
|
|
52
|
+
# Get the token
|
|
53
|
+
token_result = subprocess.run(
|
|
54
|
+
["gh", "auth", "token", "-h", github_url],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
text=True,
|
|
57
|
+
)
|
|
58
|
+
if token_result.returncode != 0:
|
|
59
|
+
logging.debug("Failed to get token from gh CLI")
|
|
60
|
+
return None, None, None
|
|
61
|
+
token = token_result.stdout.strip()
|
|
62
|
+
if not token:
|
|
63
|
+
return None, None, None
|
|
64
|
+
|
|
65
|
+
# Get the username using gh api
|
|
66
|
+
username_result = subprocess.run(
|
|
67
|
+
["gh", "api", "user", "-q", ".login", "--hostname", github_url],
|
|
68
|
+
capture_output=True,
|
|
69
|
+
text=True,
|
|
70
|
+
)
|
|
71
|
+
username = None
|
|
72
|
+
if username_result.returncode == 0:
|
|
73
|
+
username = username_result.stdout.strip()
|
|
74
|
+
|
|
75
|
+
logging.debug(
|
|
76
|
+
f"Successfully retrieved credentials from gh CLI for {github_url}"
|
|
77
|
+
)
|
|
78
|
+
return token, username, github_url
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logging.debug(f"Error getting credentials from gh CLI: {e}")
|
|
82
|
+
return None, None, None
|
|
83
|
+
|
|
84
|
+
|
|
18
85
|
Config = NamedTuple(
|
|
19
86
|
"Config",
|
|
20
87
|
[
|
|
@@ -40,6 +107,10 @@ Config = NamedTuple(
|
|
|
40
107
|
("github_url", str),
|
|
41
108
|
# Name of the upstream remote
|
|
42
109
|
("remote_name", str),
|
|
110
|
+
# Default reviewers to add to new pull requests (comma-separated usernames)
|
|
111
|
+
("reviewer", Optional[str]),
|
|
112
|
+
# Default labels to add to new pull requests (comma-separated labels)
|
|
113
|
+
("label", Optional[str]),
|
|
43
114
|
],
|
|
44
115
|
)
|
|
45
116
|
|
|
@@ -97,6 +168,7 @@ def read_config(
|
|
|
97
168
|
# Environment variable overrides config file
|
|
98
169
|
# This envvar is legacy from ghexport days
|
|
99
170
|
github_oauth = os.getenv("OAUTH_TOKEN")
|
|
171
|
+
gh_cli_username = None # Track username from gh CLI
|
|
100
172
|
if github_oauth is not None:
|
|
101
173
|
logging.warning(
|
|
102
174
|
"Deprecated OAUTH_TOKEN environment variable used to populate github_oauth--"
|
|
@@ -105,6 +177,17 @@ def read_config(
|
|
|
105
177
|
)
|
|
106
178
|
if github_oauth is None and config.has_option("ghstack", "github_oauth"):
|
|
107
179
|
github_oauth = config.get("ghstack", "github_oauth")
|
|
180
|
+
|
|
181
|
+
# Try GitHub CLI if available and no token found yet
|
|
182
|
+
if github_oauth is None and request_github_token:
|
|
183
|
+
gh_token, gh_username, _ = get_gh_cli_credentials(github_url)
|
|
184
|
+
if gh_token is not None:
|
|
185
|
+
print(f"Using GitHub credentials from gh CLI for {github_url}")
|
|
186
|
+
github_oauth = gh_token
|
|
187
|
+
gh_cli_username = gh_username
|
|
188
|
+
# Don't save gh CLI credentials to config - they may change/expire
|
|
189
|
+
|
|
190
|
+
# Fall back to device flow if still no token
|
|
108
191
|
if github_oauth is None and request_github_token:
|
|
109
192
|
print("Generating GitHub access token...")
|
|
110
193
|
CLIENT_ID = "89cc88ca50efbe86907a"
|
|
@@ -150,6 +233,11 @@ def read_config(
|
|
|
150
233
|
github_username = None
|
|
151
234
|
if config.has_option("ghstack", "github_username"):
|
|
152
235
|
github_username = config.get("ghstack", "github_username")
|
|
236
|
+
# Use username from gh CLI if we got it
|
|
237
|
+
if github_username is None and gh_cli_username is not None:
|
|
238
|
+
github_username = gh_cli_username
|
|
239
|
+
# Don't save gh CLI username to config - it comes from gh CLI
|
|
240
|
+
# Fall back to API lookup if we have a token but no username yet
|
|
153
241
|
if github_username is None and github_oauth is not None:
|
|
154
242
|
request_url: str
|
|
155
243
|
if github_url == "github.com":
|
|
@@ -203,6 +291,16 @@ def read_config(
|
|
|
203
291
|
else:
|
|
204
292
|
remote_name = "origin"
|
|
205
293
|
|
|
294
|
+
if config.has_option("ghstack", "reviewer"):
|
|
295
|
+
reviewer = config.get("ghstack", "reviewer")
|
|
296
|
+
else:
|
|
297
|
+
reviewer = None
|
|
298
|
+
|
|
299
|
+
if config.has_option("ghstack", "label"):
|
|
300
|
+
label = config.get("ghstack", "label")
|
|
301
|
+
else:
|
|
302
|
+
label = None
|
|
303
|
+
|
|
206
304
|
if write_back:
|
|
207
305
|
with open(config_path, "w") as f:
|
|
208
306
|
config.write(f)
|
|
@@ -218,6 +316,8 @@ def read_config(
|
|
|
218
316
|
default_project_dir=default_project_dir,
|
|
219
317
|
github_url=github_url,
|
|
220
318
|
remote_name=remote_name,
|
|
319
|
+
reviewer=reviewer,
|
|
320
|
+
label=label,
|
|
221
321
|
)
|
|
222
322
|
logging.debug(f"conf = {conf}")
|
|
223
323
|
return conf
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
+
import dataclasses
|
|
3
4
|
import os.path
|
|
4
5
|
import re
|
|
5
6
|
from dataclasses import dataclass
|
|
@@ -271,6 +272,8 @@ class PullRequest(Node):
|
|
|
271
272
|
# state: PullRequestState
|
|
272
273
|
title: str
|
|
273
274
|
url: str
|
|
275
|
+
reviewers: List[str] = dataclasses.field(default_factory=list)
|
|
276
|
+
labels: List[str] = dataclasses.field(default_factory=list)
|
|
274
277
|
|
|
275
278
|
def repository(self, info: GraphQLResolveInfo) -> Repository:
|
|
276
279
|
return github_state(info).repositories[self._repository]
|
|
@@ -473,6 +476,24 @@ class FakeGitHubEndpoint(ghstack.github.GitHubEndpoint):
|
|
|
473
476
|
GitHubNumber(int(m.group(3))),
|
|
474
477
|
cast(CreateIssueCommentInput, kwargs),
|
|
475
478
|
)
|
|
479
|
+
if m := re.match(
|
|
480
|
+
r"^repos/([^/]+)/([^/]+)/pulls/([^/]+)/requested_reviewers", path
|
|
481
|
+
):
|
|
482
|
+
# Handle adding reviewers
|
|
483
|
+
state = self.state
|
|
484
|
+
repo = state.repository(m.group(1), m.group(2))
|
|
485
|
+
pr = state.pull_request(repo, GitHubNumber(int(m.group(3))))
|
|
486
|
+
reviewers = kwargs.get("reviewers", [])
|
|
487
|
+
pr.reviewers.extend(reviewers)
|
|
488
|
+
return {}
|
|
489
|
+
if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/([^/]+)/labels", path):
|
|
490
|
+
# Handle adding labels
|
|
491
|
+
state = self.state
|
|
492
|
+
repo = state.repository(m.group(1), m.group(2))
|
|
493
|
+
pr = state.pull_request(repo, GitHubNumber(int(m.group(3))))
|
|
494
|
+
labels = kwargs.get("labels", [])
|
|
495
|
+
pr.labels.extend(labels)
|
|
496
|
+
return {}
|
|
476
497
|
elif method == "patch":
|
|
477
498
|
if m := re.match(r"^repos/([^/]+)/([^/]+)(?:/pulls/([^/]+))?$", path):
|
|
478
499
|
owner, name, number = m.groups()
|
|
@@ -25,7 +25,9 @@ def get_github_repo_name_with_owner(
|
|
|
25
25
|
remote_name: str,
|
|
26
26
|
) -> GitHubRepoNameWithOwner:
|
|
27
27
|
# Grovel in remotes to figure it out
|
|
28
|
-
|
|
28
|
+
# Use --push to get the push URL, which is what matters for determining
|
|
29
|
+
# where commits will actually be pushed to
|
|
30
|
+
remote_url = sh.git("remote", "get-url", "--push", remote_name)
|
|
29
31
|
while True:
|
|
30
32
|
match = r"^git@{github_url}:/?([^/]+)/(.+?)(?:\.git)?$".format(
|
|
31
33
|
github_url=github_url
|
|
@@ -138,20 +140,32 @@ GitHubPullRequestParams = TypedDict(
|
|
|
138
140
|
)
|
|
139
141
|
|
|
140
142
|
|
|
143
|
+
def _normalize_remote_url(remote_url: str) -> str:
|
|
144
|
+
"""Convert SSH remote URL to HTTPS format, strip .git suffix."""
|
|
145
|
+
# git@github.com:owner/repo.git -> https://github.com/owner/repo
|
|
146
|
+
m = re.match(r"^git@([^:]+):/?(.+?)(?:\.git)?$", remote_url)
|
|
147
|
+
if m:
|
|
148
|
+
return f"https://{m.group(1)}/{m.group(2)}"
|
|
149
|
+
return re.sub(r"\.git$", "", remote_url)
|
|
150
|
+
|
|
151
|
+
|
|
141
152
|
def parse_pull_request(
|
|
142
153
|
pull_request: str,
|
|
143
154
|
*,
|
|
144
155
|
sh: Optional[ghstack.shell.Shell] = None,
|
|
145
156
|
remote_name: Optional[str] = None,
|
|
146
157
|
) -> GitHubPullRequestParams:
|
|
158
|
+
pull_request = pull_request.lstrip("#")
|
|
147
159
|
m = RE_PR_URL.match(pull_request)
|
|
148
160
|
if not m:
|
|
149
161
|
# We can reconstruct the URL if just a PR number is passed
|
|
150
162
|
if sh is not None and remote_name is not None:
|
|
151
|
-
remote_url = sh.git("remote", "get-url", remote_name)
|
|
163
|
+
remote_url = sh.git("remote", "get-url", "--push", remote_name)
|
|
152
164
|
# Do not pass the shell to avoid infinite loop
|
|
153
165
|
try:
|
|
154
|
-
return parse_pull_request(
|
|
166
|
+
return parse_pull_request(
|
|
167
|
+
_normalize_remote_url(remote_url) + "/pull/" + pull_request
|
|
168
|
+
)
|
|
155
169
|
except RuntimeError:
|
|
156
170
|
# Fall back on original error message
|
|
157
171
|
pass
|
|
@@ -343,6 +343,12 @@ class Submitter:
|
|
|
343
343
|
# merged. If None, infer whether or not the PR should be direct or not.
|
|
344
344
|
direct_opt: Optional[bool] = None
|
|
345
345
|
|
|
346
|
+
# Default reviewers to add to new pull requests (comma-separated usernames)
|
|
347
|
+
reviewer: Optional[str] = None
|
|
348
|
+
|
|
349
|
+
# Default labels to add to new pull requests (comma-separated labels)
|
|
350
|
+
label: Optional[str] = None
|
|
351
|
+
|
|
346
352
|
# ~~~~~~~~~~~~~~~~~~~~~~~~
|
|
347
353
|
# Computed in post init
|
|
348
354
|
|
|
@@ -1434,6 +1440,32 @@ is closed (likely due to being merged). Please rebase to upstream and try again
|
|
|
1434
1440
|
)
|
|
1435
1441
|
comment_id = rc["id"]
|
|
1436
1442
|
|
|
1443
|
+
# Add reviewers if specified
|
|
1444
|
+
if self.reviewer:
|
|
1445
|
+
reviewers = [r.strip() for r in self.reviewer.split(",") if r.strip()]
|
|
1446
|
+
if reviewers:
|
|
1447
|
+
try:
|
|
1448
|
+
self.github.post(
|
|
1449
|
+
f"repos/{self.repo_owner}/{self.repo_name}/pulls/{number}/requested_reviewers",
|
|
1450
|
+
reviewers=reviewers,
|
|
1451
|
+
)
|
|
1452
|
+
logging.info(f"Added reviewers: {', '.join(reviewers)}")
|
|
1453
|
+
except Exception as e:
|
|
1454
|
+
logging.warning(f"Failed to add reviewers: {e}")
|
|
1455
|
+
|
|
1456
|
+
# Add labels if specified
|
|
1457
|
+
if self.label:
|
|
1458
|
+
labels = [label.strip() for label in self.label.split(",") if label.strip()]
|
|
1459
|
+
if labels:
|
|
1460
|
+
try:
|
|
1461
|
+
self.github.post(
|
|
1462
|
+
f"repos/{self.repo_owner}/{self.repo_name}/issues/{number}/labels",
|
|
1463
|
+
labels=labels,
|
|
1464
|
+
)
|
|
1465
|
+
logging.info(f"Added labels: {', '.join(labels)}")
|
|
1466
|
+
except Exception as e:
|
|
1467
|
+
logging.warning(f"Failed to add labels: {e}")
|
|
1468
|
+
|
|
1437
1469
|
logging.info("Opened PR #{}".format(number))
|
|
1438
1470
|
|
|
1439
1471
|
pull_request_resolved = ghstack.diff.PullRequestResolved(
|
|
@@ -12,6 +12,7 @@ from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple, Uni
|
|
|
12
12
|
|
|
13
13
|
from expecttest import assert_expected_inline
|
|
14
14
|
|
|
15
|
+
import ghstack.checkout
|
|
15
16
|
import ghstack.cherry_pick
|
|
16
17
|
|
|
17
18
|
import ghstack.github
|
|
@@ -32,6 +33,7 @@ __all__ = [
|
|
|
32
33
|
"gh_land",
|
|
33
34
|
"gh_unlink",
|
|
34
35
|
"gh_cherry_pick",
|
|
36
|
+
"gh_checkout",
|
|
35
37
|
"GitCommitHash",
|
|
36
38
|
"checkout",
|
|
37
39
|
"amend",
|
|
@@ -49,6 +51,8 @@ __all__ = [
|
|
|
49
51
|
"get_sh",
|
|
50
52
|
"get_upstream_sh",
|
|
51
53
|
"get_github",
|
|
54
|
+
"get_pr_reviewers",
|
|
55
|
+
"get_pr_labels",
|
|
52
56
|
"tick",
|
|
53
57
|
"captured_output",
|
|
54
58
|
]
|
|
@@ -192,6 +196,8 @@ def gh_submit(
|
|
|
192
196
|
base: Optional[str] = None,
|
|
193
197
|
revs: Sequence[str] = (),
|
|
194
198
|
stack: bool = True,
|
|
199
|
+
reviewer: Optional[str] = None,
|
|
200
|
+
label: Optional[str] = None,
|
|
195
201
|
) -> List[ghstack.submit.DiffMeta]:
|
|
196
202
|
self = CTX
|
|
197
203
|
r = ghstack.submit.main(
|
|
@@ -212,6 +218,8 @@ def gh_submit(
|
|
|
212
218
|
revs=revs,
|
|
213
219
|
stack=stack,
|
|
214
220
|
check_invariants=True,
|
|
221
|
+
reviewer=reviewer,
|
|
222
|
+
label=label,
|
|
215
223
|
)
|
|
216
224
|
self.check_global_github_invariants(self.direct)
|
|
217
225
|
return r
|
|
@@ -251,6 +259,17 @@ def gh_cherry_pick(pull_request: str, stack: bool = False) -> None:
|
|
|
251
259
|
)
|
|
252
260
|
|
|
253
261
|
|
|
262
|
+
def gh_checkout(pull_request: str, same_base: bool = False) -> None:
|
|
263
|
+
self = CTX
|
|
264
|
+
return ghstack.checkout.main(
|
|
265
|
+
pull_request=pull_request,
|
|
266
|
+
github=self.github,
|
|
267
|
+
sh=self.sh,
|
|
268
|
+
remote_name="origin",
|
|
269
|
+
same_base=same_base,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
254
273
|
def write_file_and_add(filename: str, contents: str) -> None:
|
|
255
274
|
self = CTX
|
|
256
275
|
with self.sh.open(filename, "w") as f:
|
|
@@ -373,6 +392,26 @@ def is_direct() -> bool:
|
|
|
373
392
|
return CTX.direct
|
|
374
393
|
|
|
375
394
|
|
|
395
|
+
def get_github() -> ghstack.github_fake.FakeGitHubEndpoint:
|
|
396
|
+
return CTX.github
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def get_pr_reviewers(pr_number: int) -> List[str]:
|
|
400
|
+
"""Get the reviewers for a PR number."""
|
|
401
|
+
github = get_github()
|
|
402
|
+
repo = github.state.repository("pytorch", "pytorch")
|
|
403
|
+
pr = github.state.pull_request(repo, ghstack.github_fake.GitHubNumber(pr_number))
|
|
404
|
+
return pr.reviewers
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def get_pr_labels(pr_number: int) -> List[str]:
|
|
408
|
+
"""Get the labels for a PR number."""
|
|
409
|
+
github = get_github()
|
|
410
|
+
repo = github.state.repository("pytorch", "pytorch")
|
|
411
|
+
pr = github.state.pull_request(repo, ghstack.github_fake.GitHubNumber(pr_number))
|
|
412
|
+
return pr.labels
|
|
413
|
+
|
|
414
|
+
|
|
376
415
|
def assert_eq(a: Any, b: Any) -> None:
|
|
377
416
|
assert a == b, f"{a} != {b}"
|
|
378
417
|
|
|
@@ -406,9 +445,5 @@ def get_upstream_sh() -> ghstack.shell.Shell:
|
|
|
406
445
|
return CTX.upstream_sh
|
|
407
446
|
|
|
408
447
|
|
|
409
|
-
def get_github() -> ghstack.github.GitHubEndpoint:
|
|
410
|
-
return CTX.github
|
|
411
|
-
|
|
412
|
-
|
|
413
448
|
def tick() -> None:
|
|
414
449
|
CTX.sh.test_tick()
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
import ghstack.github
|
|
7
|
-
import ghstack.github_utils
|
|
8
|
-
import ghstack.shell
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def main(
|
|
12
|
-
pull_request: str,
|
|
13
|
-
github: ghstack.github.GitHubEndpoint,
|
|
14
|
-
sh: ghstack.shell.Shell,
|
|
15
|
-
remote_name: str,
|
|
16
|
-
) -> None:
|
|
17
|
-
|
|
18
|
-
params = ghstack.github_utils.parse_pull_request(
|
|
19
|
-
pull_request, sh=sh, remote_name=remote_name
|
|
20
|
-
)
|
|
21
|
-
head_ref = github.get_head_ref(**params)
|
|
22
|
-
orig_ref = re.sub(r"/head$", "/orig", head_ref)
|
|
23
|
-
if orig_ref == head_ref:
|
|
24
|
-
logging.warning(
|
|
25
|
-
"The ref {} doesn't look like a ghstack reference".format(head_ref)
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
# TODO: Handle remotes correctly too (so this subsumes hub)
|
|
29
|
-
|
|
30
|
-
sh.git("fetch", "--prune", remote_name)
|
|
31
|
-
sh.git("checkout", remote_name + "/" + orig_ref)
|
|
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
|