avr-cli 0.2.0__tar.gz → 0.4.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.
- {avr_cli-0.2.0 → avr_cli-0.4.0}/PKG-INFO +3 -2
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avr_cli.egg-info/PKG-INFO +3 -2
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avr_cli.egg-info/SOURCES.txt +11 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avr_cli.egg-info/requires.txt +2 -1
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/_build_info.py +1 -1
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/api_client.py +21 -3
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/auth.py +9 -1
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/job.py +9 -3
- avr_cli-0.4.0/avrea_cli/commands/pr.py +338 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/repo.py +373 -1
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/run.py +203 -52
- avr_cli-0.4.0/avrea_cli/commands/sbom.py +757 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/display.py +5 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/helpers.py +76 -4
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/json_output.py +20 -5
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/log_display.py +9 -1
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/main.py +10 -1
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/repo_context.py +2 -2
- avr_cli-0.4.0/avrea_cli/run_diagnostics.py +651 -0
- avr_cli-0.4.0/avrea_cli/run_diagnostics_display.py +172 -0
- avr_cli-0.4.0/avrea_cli/run_refs.py +242 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/vm.py +177 -11
- {avr_cli-0.2.0 → avr_cli-0.4.0}/pyproject.toml +5 -3
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_api_client.py +43 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_auth_login.py +63 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_display.py +8 -1
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_helpers.py +125 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_job_view_logs.py +31 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_json_output.py +30 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_piped_output.py +75 -3
- avr_cli-0.4.0/tests/test_pr_list.py +297 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_repo_context.py +26 -3
- avr_cli-0.4.0/tests/test_repo_mirror.py +310 -0
- avr_cli-0.4.0/tests/test_run_command_refs.py +122 -0
- avr_cli-0.4.0/tests/test_run_diagnose.py +348 -0
- avr_cli-0.4.0/tests/test_run_refs.py +174 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_run_view.py +120 -0
- avr_cli-0.4.0/tests/test_sbom.py +903 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_vm.py +155 -4
- {avr_cli-0.2.0 → avr_cli-0.4.0}/LICENSE +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/README.md +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avr_cli.egg-info/dependency_links.txt +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avr_cli.egg-info/entry_points.txt +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avr_cli.egg-info/top_level.txt +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/__init__.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/assets/logo_mark.svg +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/audit.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/billing.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/click_ext.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/__init__.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/auth_cmd.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/cache.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/config_cmd.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/health.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/internal.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/log.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/org.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/settings.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/status.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/commands/workflow.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/config.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/docs_gen.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/firewall.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/metrics_display.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/output.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/paths.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/transport.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/avrea_cli/version.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/setup.cfg +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_auth.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_auth_sso.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_cache.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_config.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_config_cmd.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_docs_gen.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_env_conventions.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_firewall.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_global_flags.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_job_list.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_job_metrics_cmd.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_lazy_registry.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_log_display.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_metrics_display.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_org_email_domain.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_org_saml.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_output.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_public_mirrors.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_run_attempts.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_run_list_flags.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_run_logs.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_settings.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_transport.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_web_flag.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_workflow_actions.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_workflow_run.py +0 -0
- {avr_cli-0.2.0 → avr_cli-0.4.0}/tests/test_workflow_view.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: avr-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Avrea command-line client
|
|
5
5
|
Author-email: Avrea <support@avrea.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
14
14
|
Requires-Python: >=3.14
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: anyio>=4.14.2
|
|
17
18
|
Requires-Dist: click>=8.3.1
|
|
18
19
|
Requires-Dist: httpx>=0.28.1
|
|
19
20
|
Requires-Dist: platformdirs>=4.0
|
|
@@ -25,7 +26,7 @@ Provides-Extra: dev
|
|
|
25
26
|
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
26
27
|
Requires-Dist: pytest-asyncio>=1.3.0; extra == "dev"
|
|
27
28
|
Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
|
|
28
|
-
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff==0.15.13; extra == "dev"
|
|
29
30
|
Requires-Dist: ty==0.0.15; extra == "dev"
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: avr-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Avrea command-line client
|
|
5
5
|
Author-email: Avrea <support@avrea.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
14
14
|
Requires-Python: >=3.14
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: anyio>=4.14.2
|
|
17
18
|
Requires-Dist: click>=8.3.1
|
|
18
19
|
Requires-Dist: httpx>=0.28.1
|
|
19
20
|
Requires-Dist: platformdirs>=4.0
|
|
@@ -25,7 +26,7 @@ Provides-Extra: dev
|
|
|
25
26
|
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
26
27
|
Requires-Dist: pytest-asyncio>=1.3.0; extra == "dev"
|
|
27
28
|
Requires-Dist: pytest-timeout>=2.4.0; extra == "dev"
|
|
28
|
-
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff==0.15.13; extra == "dev"
|
|
29
30
|
Requires-Dist: ty==0.0.15; extra == "dev"
|
|
30
31
|
Dynamic: license-file
|
|
31
32
|
|
|
@@ -26,6 +26,9 @@ avrea_cli/metrics_display.py
|
|
|
26
26
|
avrea_cli/output.py
|
|
27
27
|
avrea_cli/paths.py
|
|
28
28
|
avrea_cli/repo_context.py
|
|
29
|
+
avrea_cli/run_diagnostics.py
|
|
30
|
+
avrea_cli/run_diagnostics_display.py
|
|
31
|
+
avrea_cli/run_refs.py
|
|
29
32
|
avrea_cli/transport.py
|
|
30
33
|
avrea_cli/version.py
|
|
31
34
|
avrea_cli/vm.py
|
|
@@ -39,8 +42,10 @@ avrea_cli/commands/internal.py
|
|
|
39
42
|
avrea_cli/commands/job.py
|
|
40
43
|
avrea_cli/commands/log.py
|
|
41
44
|
avrea_cli/commands/org.py
|
|
45
|
+
avrea_cli/commands/pr.py
|
|
42
46
|
avrea_cli/commands/repo.py
|
|
43
47
|
avrea_cli/commands/run.py
|
|
48
|
+
avrea_cli/commands/sbom.py
|
|
44
49
|
avrea_cli/commands/settings.py
|
|
45
50
|
avrea_cli/commands/status.py
|
|
46
51
|
avrea_cli/commands/workflow.py
|
|
@@ -68,12 +73,18 @@ tests/test_org_email_domain.py
|
|
|
68
73
|
tests/test_org_saml.py
|
|
69
74
|
tests/test_output.py
|
|
70
75
|
tests/test_piped_output.py
|
|
76
|
+
tests/test_pr_list.py
|
|
71
77
|
tests/test_public_mirrors.py
|
|
72
78
|
tests/test_repo_context.py
|
|
79
|
+
tests/test_repo_mirror.py
|
|
73
80
|
tests/test_run_attempts.py
|
|
81
|
+
tests/test_run_command_refs.py
|
|
82
|
+
tests/test_run_diagnose.py
|
|
74
83
|
tests/test_run_list_flags.py
|
|
75
84
|
tests/test_run_logs.py
|
|
85
|
+
tests/test_run_refs.py
|
|
76
86
|
tests/test_run_view.py
|
|
87
|
+
tests/test_sbom.py
|
|
77
88
|
tests/test_settings.py
|
|
78
89
|
tests/test_transport.py
|
|
79
90
|
tests/test_vm.py
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# Auto-generated by .github/workflows/release.yml; do not edit.
|
|
2
|
-
GIT_COMMIT = "
|
|
2
|
+
GIT_COMMIT = "5d74471e85fe9ce7a40209ef3daebaad435c38d8"
|
|
@@ -34,11 +34,17 @@ class ApiClient:
|
|
|
34
34
|
styled_status = click.style(status, fg="red")
|
|
35
35
|
click.echo(f" → {styled_method} {url} [{styled_status}]", err=True)
|
|
36
36
|
|
|
37
|
-
def public_get(
|
|
38
|
-
|
|
37
|
+
def public_get(
|
|
38
|
+
self, path: str, params: dict[str, Any] | None = None, timeout: float | None = None
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""GET request to public API. ``timeout`` overrides the client default for
|
|
41
|
+
this call, e.g. so a polling loop never outlives its own deadline."""
|
|
39
42
|
url = f"{self.config.public_api_url}{path}"
|
|
43
|
+
effective_timeout = timeout if timeout is not None else self.timeout
|
|
40
44
|
try:
|
|
41
|
-
response = self._http.get(
|
|
45
|
+
response = self._http.get(
|
|
46
|
+
url, headers=self.config.get_api_headers(), timeout=effective_timeout, params=params
|
|
47
|
+
)
|
|
42
48
|
except httpx.HTTPError as exc:
|
|
43
49
|
self._log("GET", url, type(exc).__name__)
|
|
44
50
|
raise
|
|
@@ -58,6 +64,18 @@ class ApiClient:
|
|
|
58
64
|
response.raise_for_status()
|
|
59
65
|
return response.text
|
|
60
66
|
|
|
67
|
+
def public_get_bytes(self, path: str, params: dict[str, Any] | None = None) -> bytes:
|
|
68
|
+
"""GET a binary or opaque public API response, such as an SBOM artifact."""
|
|
69
|
+
url = f"{self.config.public_api_url}{path}"
|
|
70
|
+
try:
|
|
71
|
+
response = self._http.get(url, headers=self.config.get_api_headers(), timeout=self.timeout, params=params)
|
|
72
|
+
except httpx.HTTPError as exc:
|
|
73
|
+
self._log("GET", url, type(exc).__name__)
|
|
74
|
+
raise
|
|
75
|
+
self._log("GET", str(response.request.url), response.status_code)
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
return response.content
|
|
78
|
+
|
|
61
79
|
def public_post(
|
|
62
80
|
self,
|
|
63
81
|
path: str,
|
|
@@ -515,6 +515,14 @@ def login(public_api_url: str, *, provider: str = "github", email: str | None =
|
|
|
515
515
|
pass # Best-effort; the session will expire on its own
|
|
516
516
|
|
|
517
517
|
return api_key
|
|
518
|
+
except httpx.HTTPStatusError as e:
|
|
519
|
+
# Not handle_http_error: this is the login flow itself, where a 401
|
|
520
|
+
# means the browser session expired, not "go run `avr auth login`".
|
|
521
|
+
click.echo(
|
|
522
|
+
f"Error: {public_api_url} returned HTTP {e.response.status_code} when creating an API key.",
|
|
523
|
+
err=True,
|
|
524
|
+
)
|
|
525
|
+
raise click.Abort() from None
|
|
518
526
|
except httpx.HTTPError as e:
|
|
519
|
-
click.echo(f"Error:
|
|
527
|
+
click.echo(f"Error: Could not reach {public_api_url} to create an API key: {e}", err=True)
|
|
520
528
|
raise click.Abort() from None
|
|
@@ -186,7 +186,10 @@ def job(ctx):
|
|
|
186
186
|
"--repo",
|
|
187
187
|
"repository_ids",
|
|
188
188
|
multiple=True,
|
|
189
|
-
help=
|
|
189
|
+
help=(
|
|
190
|
+
"Filter by repository (org/repo or rep-xxx). Pass --repo more than once to filter multiple repositories. "
|
|
191
|
+
"Auto-detected from git remote if omitted."
|
|
192
|
+
),
|
|
190
193
|
)
|
|
191
194
|
@click.option("--name", "job_names", multiple=True, help="Filter by job name (repeatable).")
|
|
192
195
|
@click.option(
|
|
@@ -300,7 +303,7 @@ def job_list(
|
|
|
300
303
|
link_slug = get_org_slug(client, org_id) if links_enabled else ""
|
|
301
304
|
_print_jobs_table(jobs_data, console_url=link_console_url, slug=link_slug)
|
|
302
305
|
|
|
303
|
-
next_cursor = response.get("pagination"
|
|
306
|
+
next_cursor = (response.get("pagination") or {}).get("next_cursor")
|
|
304
307
|
if next_cursor:
|
|
305
308
|
click.echo(f"\nMore results available. Next page: --cursor {next_cursor}", err=True)
|
|
306
309
|
|
|
@@ -311,7 +314,10 @@ def job_list(
|
|
|
311
314
|
"--repo",
|
|
312
315
|
"repository_ids",
|
|
313
316
|
multiple=True,
|
|
314
|
-
help=
|
|
317
|
+
help=(
|
|
318
|
+
"Filter by repository (org/repo or rep-xxx). Pass --repo more than once to filter multiple repositories. "
|
|
319
|
+
"Auto-detected from git remote if omitted."
|
|
320
|
+
),
|
|
315
321
|
)
|
|
316
322
|
@click.option("--name", "job_names", multiple=True, help="Filter by job name (repeatable).")
|
|
317
323
|
@click.option("--interval", type=int, default=5, show_default=True, help="Refresh interval in seconds.")
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""Pull request CLI commands."""
|
|
2
|
+
|
|
3
|
+
from avrea_cli.api_client import ApiClient
|
|
4
|
+
from avrea_cli.click_ext import GhGroup
|
|
5
|
+
from avrea_cli.config import CliConfig
|
|
6
|
+
from avrea_cli.display import DIM_FG
|
|
7
|
+
from avrea_cli.display import get_console_url
|
|
8
|
+
from avrea_cli.display import hyperlink
|
|
9
|
+
from avrea_cli.display import is_piped
|
|
10
|
+
from avrea_cli.display import pr_url
|
|
11
|
+
from avrea_cli.display import print_piped_header
|
|
12
|
+
from avrea_cli.display import print_piped_row
|
|
13
|
+
from avrea_cli.display import truncate
|
|
14
|
+
from avrea_cli.helpers import ensure_authenticated
|
|
15
|
+
from avrea_cli.helpers import ensure_ctx
|
|
16
|
+
from avrea_cli.helpers import get_org_id
|
|
17
|
+
from avrea_cli.helpers import get_org_slug
|
|
18
|
+
from avrea_cli.helpers import handle_http_error
|
|
19
|
+
from avrea_cli.helpers import validate_cursor
|
|
20
|
+
from avrea_cli.json_output import emit_json
|
|
21
|
+
from avrea_cli.json_output import handle_json_meta
|
|
22
|
+
from avrea_cli.json_output import make_schema
|
|
23
|
+
from avrea_cli.json_output import split_fields
|
|
24
|
+
from avrea_cli.output import format_relative_timestamp
|
|
25
|
+
from avrea_cli.repo_context import resolve_repos_or_detect
|
|
26
|
+
from typing import Any
|
|
27
|
+
import click
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
_PR_LIST_FIELDS = make_schema(
|
|
31
|
+
"number",
|
|
32
|
+
"title",
|
|
33
|
+
"state",
|
|
34
|
+
"draft",
|
|
35
|
+
"merged",
|
|
36
|
+
"author_login",
|
|
37
|
+
"base_ref",
|
|
38
|
+
"head_ref",
|
|
39
|
+
"head_sha",
|
|
40
|
+
"base_sha",
|
|
41
|
+
"created_at",
|
|
42
|
+
"updated_at",
|
|
43
|
+
"comment_count",
|
|
44
|
+
"unresolved_thread_count",
|
|
45
|
+
"check_status",
|
|
46
|
+
"mergeability",
|
|
47
|
+
"repository_id",
|
|
48
|
+
"repository_full_name",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_SCOPES = ["all", "authored", "involved"]
|
|
52
|
+
_STATES = ["open", "closed", "merged", "all"]
|
|
53
|
+
_PR_LIST_FEATURE_FLAG = "feature.org-pull-requests.enabled"
|
|
54
|
+
_PR_LIST_UNSUPPORTED_MESSAGE = "Sorry, pull request listing is not yet supported for this organization. Coming soon."
|
|
55
|
+
_PR_LIST_AVAILABILITY_UNKNOWN_MESSAGE = (
|
|
56
|
+
"Sorry, we could not find this organization or confirm pull request listing support. "
|
|
57
|
+
"Organizations without Avrea Git are not supported yet. Coming soon."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.group(cls=GhGroup)
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def pr(ctx):
|
|
64
|
+
"""View pull requests."""
|
|
65
|
+
ensure_ctx(ctx)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pr.command("list")
|
|
69
|
+
@click.option(
|
|
70
|
+
"--org", "org_id", help="Organization ID or slug. Uses default org if not specified (see: avr config set org)."
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"--repo",
|
|
74
|
+
"repository_ids",
|
|
75
|
+
multiple=True,
|
|
76
|
+
help=(
|
|
77
|
+
"Filter by repository (org/repo or rep-xxx). Pass --repo more than once to filter multiple repositories. "
|
|
78
|
+
"Auto-detected from git remote if omitted."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--scope",
|
|
83
|
+
type=click.Choice(_SCOPES, case_sensitive=False),
|
|
84
|
+
default="all",
|
|
85
|
+
show_default=True,
|
|
86
|
+
help="List every readable PR, PRs you authored, or PRs you are involved in.",
|
|
87
|
+
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"--state",
|
|
90
|
+
type=click.Choice(_STATES, case_sensitive=False),
|
|
91
|
+
default="open",
|
|
92
|
+
show_default=True,
|
|
93
|
+
help="Filter by pull request state. 'all' removes the state filter.",
|
|
94
|
+
)
|
|
95
|
+
@click.option("-L", "--limit", type=click.IntRange(1, 200), default=20, show_default=True, help="Max PRs to return.")
|
|
96
|
+
@click.option("--cursor", default=None, help="Pagination cursor from a previous response.")
|
|
97
|
+
@click.option(
|
|
98
|
+
"--json",
|
|
99
|
+
"json_fields",
|
|
100
|
+
default=None,
|
|
101
|
+
help='Output JSON. Pass comma-separated field names, "*" for all fields, or "?" to list available fields.',
|
|
102
|
+
)
|
|
103
|
+
@click.option("-q", "--jq", "jq_expr", default=None, help="Filter --json output through a jq expression.")
|
|
104
|
+
@click.pass_context
|
|
105
|
+
def pr_list(
|
|
106
|
+
ctx,
|
|
107
|
+
org_id,
|
|
108
|
+
repository_ids,
|
|
109
|
+
scope,
|
|
110
|
+
state,
|
|
111
|
+
limit,
|
|
112
|
+
cursor,
|
|
113
|
+
json_fields,
|
|
114
|
+
jq_expr,
|
|
115
|
+
):
|
|
116
|
+
"""List pull requests across repositories.
|
|
117
|
+
|
|
118
|
+
\b
|
|
119
|
+
Examples:
|
|
120
|
+
avr pr list
|
|
121
|
+
avr pr list --scope authored
|
|
122
|
+
avr pr list --repo acme/widgets --state merged
|
|
123
|
+
avr pr list --json number,title,mergeability
|
|
124
|
+
avr pr list --json '?' # list available fields
|
|
125
|
+
avr pr list --json '*' # all fields
|
|
126
|
+
|
|
127
|
+
\b
|
|
128
|
+
JSON FIELDS
|
|
129
|
+
author_login, base_ref, base_sha, check_status, comment_count, created_at,
|
|
130
|
+
draft, head_ref, head_sha, mergeability, merged, number, repository_full_name,
|
|
131
|
+
repository_id, state, title, unresolved_thread_count, updated_at
|
|
132
|
+
"""
|
|
133
|
+
if handle_json_meta(json_fields, jq_expr, _PR_LIST_FIELDS):
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
client: ApiClient = ctx.obj["client"]
|
|
137
|
+
config: CliConfig = ctx.obj["config"]
|
|
138
|
+
ensure_authenticated(config)
|
|
139
|
+
org_id = get_org_id(config, org_id, client=client)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
feature = client.public_get(f"/orgs/{org_id}/feature-flags/{_PR_LIST_FEATURE_FLAG}")
|
|
143
|
+
except httpx.HTTPStatusError as exc:
|
|
144
|
+
if exc.response.status_code == 404:
|
|
145
|
+
raise click.ClickException(_PR_LIST_AVAILABILITY_UNKNOWN_MESSAGE) from None
|
|
146
|
+
handle_http_error(exc, "check pull request listing availability")
|
|
147
|
+
except httpx.TransportError as exc:
|
|
148
|
+
raise click.ClickException(f"Could not list pull requests because the request to Avrea failed: {exc}") from None
|
|
149
|
+
|
|
150
|
+
if feature.get("enabled") is not True:
|
|
151
|
+
raise click.ClickException(_PR_LIST_UNSUPPORTED_MESSAGE)
|
|
152
|
+
|
|
153
|
+
resolved_repositories = resolve_repos_or_detect(
|
|
154
|
+
client,
|
|
155
|
+
config,
|
|
156
|
+
org_id,
|
|
157
|
+
repository_ids,
|
|
158
|
+
soft_detect=True,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
params: dict[str, Any] = {
|
|
162
|
+
"scope": scope.lower(),
|
|
163
|
+
"limit": limit,
|
|
164
|
+
}
|
|
165
|
+
if state.lower() != "all":
|
|
166
|
+
params["state"] = state.lower()
|
|
167
|
+
if resolved_repositories:
|
|
168
|
+
params["repository_ids"] = resolved_repositories
|
|
169
|
+
cursor = validate_cursor(cursor)
|
|
170
|
+
if cursor:
|
|
171
|
+
params["cursor"] = cursor
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
response = client.public_get(f"/orgs/{org_id}/pull-requests", params=params)
|
|
175
|
+
pulls = response.get("data") or []
|
|
176
|
+
except httpx.HTTPStatusError as exc:
|
|
177
|
+
handle_http_error(exc, "list pull requests")
|
|
178
|
+
except httpx.TransportError as exc:
|
|
179
|
+
raise click.ClickException(f"Could not list pull requests because the request to Avrea failed: {exc}") from None
|
|
180
|
+
|
|
181
|
+
if json_fields is not None:
|
|
182
|
+
emit_json(list(pulls), split_fields(json_fields, _PR_LIST_FIELDS), _PR_LIST_FIELDS, jq_expr)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if not pulls:
|
|
186
|
+
click.echo("No pull requests found.")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if is_piped():
|
|
190
|
+
_print_piped(pulls)
|
|
191
|
+
else:
|
|
192
|
+
_print_table(
|
|
193
|
+
ctx,
|
|
194
|
+
client,
|
|
195
|
+
config,
|
|
196
|
+
org_id,
|
|
197
|
+
pulls,
|
|
198
|
+
show_repository=len(resolved_repositories) != 1,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
next_cursor = (response.get("pagination") or {}).get("next_cursor")
|
|
202
|
+
if next_cursor:
|
|
203
|
+
click.echo(f"\nMore results available. Next page: --cursor {next_cursor}", err=True)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _display_state(pull: dict[str, Any]) -> str:
|
|
207
|
+
if pull.get("merged"):
|
|
208
|
+
return "merged"
|
|
209
|
+
if pull.get("draft") and pull.get("state") != "closed":
|
|
210
|
+
return "draft"
|
|
211
|
+
return str(pull.get("state") or "unknown")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _merge_status(pull: dict[str, Any]) -> str:
|
|
215
|
+
mergeability = pull.get("mergeability")
|
|
216
|
+
if isinstance(mergeability, dict) and mergeability.get("status"):
|
|
217
|
+
return str(mergeability["status"])
|
|
218
|
+
if pull.get("merged") or pull.get("draft") or pull.get("state") == "closed":
|
|
219
|
+
return "-"
|
|
220
|
+
return "?"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _print_piped(pulls: list[dict[str, Any]]) -> None:
|
|
224
|
+
print_piped_header(
|
|
225
|
+
[
|
|
226
|
+
"number",
|
|
227
|
+
"repository",
|
|
228
|
+
"state",
|
|
229
|
+
"title",
|
|
230
|
+
"author",
|
|
231
|
+
"head_ref",
|
|
232
|
+
"base_ref",
|
|
233
|
+
"check_status",
|
|
234
|
+
"mergeability",
|
|
235
|
+
"comment_count",
|
|
236
|
+
"unresolved_threads",
|
|
237
|
+
"head_sha",
|
|
238
|
+
"updated_at",
|
|
239
|
+
]
|
|
240
|
+
)
|
|
241
|
+
for pull in pulls:
|
|
242
|
+
print_piped_row(
|
|
243
|
+
[
|
|
244
|
+
pull.get("number"),
|
|
245
|
+
pull.get("repository_full_name") or pull.get("repository_id"),
|
|
246
|
+
_display_state(pull),
|
|
247
|
+
pull.get("title"),
|
|
248
|
+
pull.get("author_login"),
|
|
249
|
+
pull.get("head_ref"),
|
|
250
|
+
pull.get("base_ref"),
|
|
251
|
+
pull.get("check_status"),
|
|
252
|
+
(pull.get("mergeability") or {}).get("status"),
|
|
253
|
+
pull.get("comment_count"),
|
|
254
|
+
pull.get("unresolved_thread_count"),
|
|
255
|
+
pull.get("head_sha"),
|
|
256
|
+
pull.get("updated_at"),
|
|
257
|
+
]
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _print_table(
|
|
262
|
+
ctx: click.Context,
|
|
263
|
+
client: ApiClient,
|
|
264
|
+
config: CliConfig,
|
|
265
|
+
org_id: str,
|
|
266
|
+
pulls: list[dict[str, Any]],
|
|
267
|
+
*,
|
|
268
|
+
show_repository: bool,
|
|
269
|
+
) -> None:
|
|
270
|
+
widths = {
|
|
271
|
+
"repo": 30,
|
|
272
|
+
"pr": 8,
|
|
273
|
+
"state": 8,
|
|
274
|
+
"checks": 11,
|
|
275
|
+
"merge": 12,
|
|
276
|
+
"unres": 7,
|
|
277
|
+
"title": 48,
|
|
278
|
+
"author": 18,
|
|
279
|
+
"age": 10,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
def cell(value: object, width: int) -> str:
|
|
283
|
+
return f"{truncate(str(value), width - 1):{width}s}"
|
|
284
|
+
|
|
285
|
+
def header(value: str, width: int) -> str:
|
|
286
|
+
return click.style(cell(value, width), fg=DIM_FG, underline=True)
|
|
287
|
+
|
|
288
|
+
click.echo(
|
|
289
|
+
" "
|
|
290
|
+
+ header("PR", widths["pr"])
|
|
291
|
+
+ (header("REPOSITORY", widths["repo"]) if show_repository else "")
|
|
292
|
+
+ header("STATE", widths["state"])
|
|
293
|
+
+ header("CHECKS", widths["checks"])
|
|
294
|
+
+ header("MERGE", widths["merge"])
|
|
295
|
+
+ header("UNRES", widths["unres"])
|
|
296
|
+
+ header("TITLE", widths["title"])
|
|
297
|
+
+ header("AUTHOR", widths["author"])
|
|
298
|
+
+ header("UPDATED", widths["age"])
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
links_enabled = ctx.obj.get("links_enabled", False)
|
|
302
|
+
console_url = get_console_url(config.public_api_url) if links_enabled else ""
|
|
303
|
+
slug = get_org_slug(client, org_id) if links_enabled else ""
|
|
304
|
+
|
|
305
|
+
for pull in pulls:
|
|
306
|
+
repository = pull.get("repository_full_name") or pull.get("repository_id") or "?"
|
|
307
|
+
number = pull.get("number")
|
|
308
|
+
pr_label = f"#{number}" if number is not None else "?"
|
|
309
|
+
state = _display_state(pull)
|
|
310
|
+
checks = pull.get("check_status") or "-"
|
|
311
|
+
merge = _merge_status(pull)
|
|
312
|
+
unresolved = pull.get("unresolved_thread_count")
|
|
313
|
+
unresolved = "?" if unresolved is None else unresolved
|
|
314
|
+
title = pull.get("title") or ""
|
|
315
|
+
author = pull.get("author_login") or "?"
|
|
316
|
+
age = format_relative_timestamp(pull.get("updated_at"))
|
|
317
|
+
|
|
318
|
+
repository_cell = cell(repository, widths["repo"])
|
|
319
|
+
pr_cell = click.style(cell(pr_label, widths["pr"]), fg="cyan")
|
|
320
|
+
title_cell = cell(title, widths["title"])
|
|
321
|
+
repo_id = pull.get("repository_id")
|
|
322
|
+
if links_enabled and repo_id and isinstance(number, int):
|
|
323
|
+
url = pr_url(console_url, slug, str(repo_id), number)
|
|
324
|
+
pr_cell = hyperlink(pr_cell, url)
|
|
325
|
+
title_cell = hyperlink(title_cell, url)
|
|
326
|
+
|
|
327
|
+
click.echo(
|
|
328
|
+
" "
|
|
329
|
+
+ pr_cell
|
|
330
|
+
+ (repository_cell if show_repository else "")
|
|
331
|
+
+ cell(state, widths["state"])
|
|
332
|
+
+ cell(checks, widths["checks"])
|
|
333
|
+
+ cell(merge, widths["merge"])
|
|
334
|
+
+ cell(unresolved, widths["unres"])
|
|
335
|
+
+ title_cell
|
|
336
|
+
+ cell(author, widths["author"])
|
|
337
|
+
+ click.style(cell(age, widths["age"]), dim=True)
|
|
338
|
+
)
|