bmsdna-devtools 0.45.0__tar.gz → 0.46.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.
- bmsdna_devtools-0.45.0/README.md → bmsdna_devtools-0.46.0/PKG-INFO +38 -3
- bmsdna_devtools-0.45.0/PKG-INFO → bmsdna_devtools-0.46.0/README.md +20 -21
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/cli.py +133 -9
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/gh_pr.py +5 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/github_api.py +5 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pr_build.py +19 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pr_info.py +3 -0
- bmsdna_devtools-0.46.0/bmsdna/devtools/pr_ref.py +90 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/pyproject.toml +1 -1
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/skills/bmsdna-devtools/SKILL.md +6 -1
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_info.py +335 -2
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/uv.lock +1 -1
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/.github/workflows/auto-release.yml +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/.github/workflows/python-publish.yml +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/.github/workflows/python-test.yml +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/.gitignore +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/.python-version +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/__init__.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/_api_dump.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/ado_auth.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/ado_issue.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/agent_mentions.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/api_usage.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/app_service_logs.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/azure_arm.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/azure_auth.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/bdt_config.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/cli_tools.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/commit.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/dead_code.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/env_config.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/env_files.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/find_injection.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/find_repo.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/gh_args.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/gh_issue.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/gitrepo.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/injection_frontend.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/injection_python.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/issue_do.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/issue_show.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/issue_watch.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/lint.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/lint_findings.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/lint_models.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/lint_sql.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/lint_sql_files.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/lint_tooling.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/lint_typescript.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/logs.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pr_choice.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pr_create_args.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pr_issue_link.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pr_labels.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pr_markdown.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/pull.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/testdb.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/translate.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/__init__.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/cli.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/pipeline.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/pricing.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/report.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/sql/00_schema.sql +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/sql/01_ingest_claude.sql +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/sql/01_ingest_codex.sql +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/sql/02_messages.sql +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/sql/03_markers.sql +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/sql/04_facts.sql +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/usage/sql/05_views.sql +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/bmsdna/devtools/worktree.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/justfile +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/conftest.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/fake_github.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_ado_auth.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_ado_issue.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_ado_issue_flow.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_agent_mentions.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_api_usage.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_app_service_logs.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_azure_arm.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_bdt_config.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_cleanup.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_commit.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_issue.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_pr_create.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_pr_id.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_pull.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_tools.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_cli_version.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_commit.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_env_config.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_env_files.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_find_injection.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_find_repo.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_gh_issue.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_gh_pr.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_github_api.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_gitrepo.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_issue_do.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_issue_for_me.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_issue_show.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_issue_watch.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_lint.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_lint_models.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_lint_sql.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_lint_sql_files.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_lint_tooling.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_lint_typescript.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_logs.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_build.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_build_create_options.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_build_flow.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_choice.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_create_args.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_description_not_truncated.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_issue_link.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_labels.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pr_markdown.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_pull.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_testdb.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_translate.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_usage.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_wait_timeout.py +0 -0
- {bmsdna_devtools-0.45.0 → bmsdna_devtools-0.46.0}/tests/test_worktree_clean.py +0 -0
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bmsdna-devtools
|
|
3
|
+
Version: 0.46.0
|
|
4
|
+
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
|
+
Requires-Python: >=3.14
|
|
6
|
+
Requires-Dist: azure-identity>=1.19.0
|
|
7
|
+
Requires-Dist: pgdevkit[db]>=0.7.1
|
|
8
|
+
Requires-Dist: requests>=2.32.0
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
11
|
+
Requires-Dist: typer>=0.26.7
|
|
12
|
+
Provides-Extra: lint
|
|
13
|
+
Requires-Dist: sqlglot>=27.0.0; extra == 'lint'
|
|
14
|
+
Provides-Extra: usage
|
|
15
|
+
Requires-Dist: duckdb>=1.3.0; extra == 'usage'
|
|
16
|
+
Requires-Dist: plotly>=5.24.0; extra == 'usage'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
1
19
|
# bmsdna-devtools
|
|
2
20
|
|
|
3
21
|
Shared developer tooling for BMS projects: PR build/check status and a
|
|
@@ -104,7 +122,7 @@ it watches a branch-triggered build/workflow run, not any particular PR.)
|
|
|
104
122
|
## `bdt pr info`
|
|
105
123
|
|
|
106
124
|
```bash
|
|
107
|
-
bdt pr info [--json] [--pr-id
|
|
125
|
+
bdt pr info [--json] [--pr-id PR]... [--target-branch BRANCH] [--all]
|
|
108
126
|
```
|
|
109
127
|
|
|
110
128
|
One-shot summary of the current branch's PR: link, state (open/merged/closed, draft), aggregate
|
|
@@ -115,6 +133,24 @@ the check rollup (only a failed check fails it; skipped/cancelled ones don't cou
|
|
|
115
133
|
from the latest build of each pipeline (any failed result fails it; unfinished builds are pending, or
|
|
116
134
|
`waiting` when a stage needs approval). `unknown` if Azure DevOps can't be asked.
|
|
117
135
|
|
|
136
|
+
`--all` lists every open PR from the current branch (e.g. one into `dev` and one into `main`, which plain
|
|
137
|
+
`bdt pr info` refuses to pick between): with `--json` an array of the same objects, `[]` (exit 0) when there is
|
|
138
|
+
none; `--target-branch` narrows it, `--pr-id` can't be combined with it. Only open PRs are listed, and a PR with
|
|
139
|
+
merge conflicts is listed like any other (plain `pr info` on Azure DevOps errors on it).
|
|
140
|
+
|
|
141
|
+
`--pr-id` takes several PRs, also of other repos: repeat it and/or comma-separate (`--pr-id 12,34 --pr-id owner/repo#5`).
|
|
142
|
+
Each entry is a bare number (a PR of the current repo), `owner/repo#N` (GitHub), `project/repo#N` or
|
|
143
|
+
`org/project/repo#N` (Azure DevOps; the host is the current repo's, and the org is its org unless given), or the PR's
|
|
144
|
+
URL (GitHub, `dev.azure.com` or `*.visualstudio.com`; the only form that needs no git checkout). One plain number
|
|
145
|
+
behaves as before: the single object. One `owner/repo#N` is a single object too; two or more entries print a list in the
|
|
146
|
+
order given (duplicates once), as `--json` an array like `--all`'s, as text with the repo in each header. A failing entry
|
|
147
|
+
(unknown number, no access) doesn't hide the others: it is reported as `error: <entry>: <reason>` on stderr, the rest is
|
|
148
|
+
still printed, and the exit code is 1. An unreadable entry is a usage error before anything is looked up. `--pr-id` can't
|
|
149
|
+
be combined with `--all`; `--target-branch` is ignored for it, as before. Azure DevOps entries use one PAT/`az login` for
|
|
150
|
+
every org, and a PR with merge conflicts is an error entry there.
|
|
151
|
+
|
|
152
|
+
Every JSON object has a `repo` (`owner/repo` on GitHub, `org/project/repo` on Azure DevOps).
|
|
153
|
+
|
|
118
154
|
Azure DevOps limits: the PR is found by the current branch (into `--target-branch` if given, else any target),
|
|
119
155
|
a PR with merge conflicts is reported as an error (exit 1) like `pr status` does, and the closed
|
|
120
156
|
work items are only those whose URL is in the PR description -- not ones linked via
|
|
@@ -272,8 +308,7 @@ bdt pr comment --message "Looks good" [--screenshot shot.png] [--file report.pdf
|
|
|
272
308
|
bdt pr comment --message-file plan.md # post a file's text as the comment (`-` = stdin)
|
|
273
309
|
```
|
|
274
310
|
|
|
275
|
-
`--file`
|
|
276
|
-
content. To post a markdown document (an implementation plan, review findings) use `--message-file`.
|
|
311
|
+
Use `--message-file` to post a markdown document (an implementation plan, review findings) as the comment.
|
|
277
312
|
|
|
278
313
|
## `bdt issue create` / `show` / `update` / `delete`, `bdt issue comment add` / `update` / `delete`
|
|
279
314
|
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: bmsdna-devtools
|
|
3
|
-
Version: 0.45.0
|
|
4
|
-
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
|
-
Requires-Python: >=3.14
|
|
6
|
-
Requires-Dist: azure-identity>=1.19.0
|
|
7
|
-
Requires-Dist: pgdevkit[db]>=0.7.1
|
|
8
|
-
Requires-Dist: requests>=2.32.0
|
|
9
|
-
Requires-Dist: rich>=13.0.0
|
|
10
|
-
Requires-Dist: tomli-w>=1.0.0
|
|
11
|
-
Requires-Dist: typer>=0.26.7
|
|
12
|
-
Provides-Extra: lint
|
|
13
|
-
Requires-Dist: sqlglot>=27.0.0; extra == 'lint'
|
|
14
|
-
Provides-Extra: usage
|
|
15
|
-
Requires-Dist: duckdb>=1.3.0; extra == 'usage'
|
|
16
|
-
Requires-Dist: plotly>=5.24.0; extra == 'usage'
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
1
|
# bmsdna-devtools
|
|
20
2
|
|
|
21
3
|
Shared developer tooling for BMS projects: PR build/check status and a
|
|
@@ -122,7 +104,7 @@ it watches a branch-triggered build/workflow run, not any particular PR.)
|
|
|
122
104
|
## `bdt pr info`
|
|
123
105
|
|
|
124
106
|
```bash
|
|
125
|
-
bdt pr info [--json] [--pr-id
|
|
107
|
+
bdt pr info [--json] [--pr-id PR]... [--target-branch BRANCH] [--all]
|
|
126
108
|
```
|
|
127
109
|
|
|
128
110
|
One-shot summary of the current branch's PR: link, state (open/merged/closed, draft), aggregate
|
|
@@ -133,6 +115,24 @@ the check rollup (only a failed check fails it; skipped/cancelled ones don't cou
|
|
|
133
115
|
from the latest build of each pipeline (any failed result fails it; unfinished builds are pending, or
|
|
134
116
|
`waiting` when a stage needs approval). `unknown` if Azure DevOps can't be asked.
|
|
135
117
|
|
|
118
|
+
`--all` lists every open PR from the current branch (e.g. one into `dev` and one into `main`, which plain
|
|
119
|
+
`bdt pr info` refuses to pick between): with `--json` an array of the same objects, `[]` (exit 0) when there is
|
|
120
|
+
none; `--target-branch` narrows it, `--pr-id` can't be combined with it. Only open PRs are listed, and a PR with
|
|
121
|
+
merge conflicts is listed like any other (plain `pr info` on Azure DevOps errors on it).
|
|
122
|
+
|
|
123
|
+
`--pr-id` takes several PRs, also of other repos: repeat it and/or comma-separate (`--pr-id 12,34 --pr-id owner/repo#5`).
|
|
124
|
+
Each entry is a bare number (a PR of the current repo), `owner/repo#N` (GitHub), `project/repo#N` or
|
|
125
|
+
`org/project/repo#N` (Azure DevOps; the host is the current repo's, and the org is its org unless given), or the PR's
|
|
126
|
+
URL (GitHub, `dev.azure.com` or `*.visualstudio.com`; the only form that needs no git checkout). One plain number
|
|
127
|
+
behaves as before: the single object. One `owner/repo#N` is a single object too; two or more entries print a list in the
|
|
128
|
+
order given (duplicates once), as `--json` an array like `--all`'s, as text with the repo in each header. A failing entry
|
|
129
|
+
(unknown number, no access) doesn't hide the others: it is reported as `error: <entry>: <reason>` on stderr, the rest is
|
|
130
|
+
still printed, and the exit code is 1. An unreadable entry is a usage error before anything is looked up. `--pr-id` can't
|
|
131
|
+
be combined with `--all`; `--target-branch` is ignored for it, as before. Azure DevOps entries use one PAT/`az login` for
|
|
132
|
+
every org, and a PR with merge conflicts is an error entry there.
|
|
133
|
+
|
|
134
|
+
Every JSON object has a `repo` (`owner/repo` on GitHub, `org/project/repo` on Azure DevOps).
|
|
135
|
+
|
|
136
136
|
Azure DevOps limits: the PR is found by the current branch (into `--target-branch` if given, else any target),
|
|
137
137
|
a PR with merge conflicts is reported as an error (exit 1) like `pr status` does, and the closed
|
|
138
138
|
work items are only those whose URL is in the PR description -- not ones linked via
|
|
@@ -290,8 +290,7 @@ bdt pr comment --message "Looks good" [--screenshot shot.png] [--file report.pdf
|
|
|
290
290
|
bdt pr comment --message-file plan.md # post a file's text as the comment (`-` = stdin)
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
-
`--file`
|
|
294
|
-
content. To post a markdown document (an implementation plan, review findings) use `--message-file`.
|
|
293
|
+
Use `--message-file` to post a markdown document (an implementation plan, review findings) as the comment.
|
|
295
294
|
|
|
296
295
|
## `bdt issue create` / `show` / `update` / `delete`, `bdt issue comment add` / `update` / `delete`
|
|
297
296
|
|
|
@@ -32,7 +32,7 @@ from . import find_injection as find_injection_mod
|
|
|
32
32
|
from . import lint as lint_mod
|
|
33
33
|
from . import logs as logs_mod
|
|
34
34
|
from . import translate as translate_mod
|
|
35
|
-
from . import pr_build, pr_info as pr_info_mod, pr_issue_link, pr_labels, pull as pull_mod, worktree as worktree_mod
|
|
35
|
+
from . import pr_build, pr_info as pr_info_mod, pr_issue_link, pr_ref, pr_labels, pull as pull_mod, worktree as worktree_mod
|
|
36
36
|
from .ado_auth import auth_header
|
|
37
37
|
from .bdt_config import find_pyproject, load_bdt_table
|
|
38
38
|
from .cli_tools import CLI_TIMEOUT_SECS, EXIT_WAIT_TIMEOUT, detect_agent_session, require_gh
|
|
@@ -42,6 +42,7 @@ from .usage.cli import usage_app
|
|
|
42
42
|
from .gitrepo import (
|
|
43
43
|
AdoRemote,
|
|
44
44
|
GitHubRemote,
|
|
45
|
+
NotAzureDevOpsRemoteError,
|
|
45
46
|
NotGitHubRemoteError,
|
|
46
47
|
UnknownRemoteError,
|
|
47
48
|
current_branch,
|
|
@@ -170,6 +171,69 @@ def _current_pr_info(pr_id: int | None, target_branch: str | None, pat: str | No
|
|
|
170
171
|
return pr_info_mod.ado_info(session, pr, remote)
|
|
171
172
|
|
|
172
173
|
|
|
174
|
+
def _open_prs_info(target_branch: str | None, pat: str | None) -> list[pr_info_mod.PrInfo]:
|
|
175
|
+
"""`_current_pr_info` for every open PR from the current branch, `[]` if there is none."""
|
|
176
|
+
remote = current_remote()
|
|
177
|
+
if isinstance(remote, GitHubRemote):
|
|
178
|
+
gh = require_gh()
|
|
179
|
+
return [
|
|
180
|
+
pr_info_mod.github_info(gh_pr.get_pr(gh, number, fields=gh_pr.PR_INFO_FIELDS), remote)
|
|
181
|
+
for number in gh_pr.open_pr_numbers(gh, target_branch)
|
|
182
|
+
]
|
|
183
|
+
session = requests.Session()
|
|
184
|
+
session.headers.update(auth_header(pat))
|
|
185
|
+
return [pr_info_mod.ado_info(session, pr, remote) for pr in pr_build.get_active_prs(session, remote, current_branch(), target_branch)]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
_REMOTE_ERRORS = (UnknownRemoteError, NotGitHubRemoteError, NotAzureDevOpsRemoteError)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _current_remote_or_none() -> AdoRemote | GitHubRemote | None:
|
|
192
|
+
"""The current repo's remote, or None outside a checkout -- without git's own complaint, as that is no error here."""
|
|
193
|
+
r = subprocess.run(["git", "remote", "get-url", "origin"], capture_output=True, encoding="utf-8")
|
|
194
|
+
if r.returncode != 0:
|
|
195
|
+
return None
|
|
196
|
+
try:
|
|
197
|
+
return parse_remote(r.stdout.strip())
|
|
198
|
+
except _REMOTE_ERRORS:
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _ref_pr_info(ref: pr_ref.PrRef, pat: str | None, clients: dict) -> pr_info_mod.PrInfo:
|
|
203
|
+
"""The PR `ref` names, in its own repo (the current one for a bare number). `clients` caches the GitHub client
|
|
204
|
+
and Azure DevOps session across the entries of one call, so the token and auth are looked up once."""
|
|
205
|
+
remote = ref.remote or current_remote()
|
|
206
|
+
if isinstance(remote, GitHubRemote):
|
|
207
|
+
if "gh" not in clients:
|
|
208
|
+
clients["gh"] = require_gh()
|
|
209
|
+
base = clients["gh"]
|
|
210
|
+
gh = base if ref.remote is None else base.for_repo(remote.owner, remote.repo)
|
|
211
|
+
return pr_info_mod.github_info(gh_pr.get_pr(gh, ref.pr_id, fields=gh_pr.PR_INFO_FIELDS), remote)
|
|
212
|
+
if "session" not in clients:
|
|
213
|
+
clients["session"] = requests.Session()
|
|
214
|
+
clients["session"].headers.update(auth_header(pat))
|
|
215
|
+
session = clients["session"]
|
|
216
|
+
return pr_info_mod.ado_info(session, pr_build.get_pr_by_id(session, remote, ref.pr_id), remote)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _failure_text(e: BaseException) -> str:
|
|
220
|
+
return str(e.code) if isinstance(e, SystemExit) and isinstance(e.code, str) else str(e) or type(e).__name__
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _ref_infos(refs: list[pr_ref.PrRef], pat: str | None) -> tuple[list[pr_info_mod.PrInfo], list[str]]:
|
|
224
|
+
"""`_ref_pr_info` for each of `refs`, in order: the PRs found and one message per entry that failed, so one
|
|
225
|
+
unreachable repo or unknown number doesn't hide the others."""
|
|
226
|
+
clients: dict = {}
|
|
227
|
+
infos: list[pr_info_mod.PrInfo] = []
|
|
228
|
+
errors: list[str] = []
|
|
229
|
+
for ref in refs:
|
|
230
|
+
try:
|
|
231
|
+
infos.append(_ref_pr_info(ref, pat, clients))
|
|
232
|
+
except (SystemExit, requests.RequestException, KeyError, *_REMOTE_ERRORS) as e:
|
|
233
|
+
errors.append(f"{ref}: {_failure_text(e)}")
|
|
234
|
+
return infos, errors
|
|
235
|
+
|
|
236
|
+
|
|
173
237
|
def _gh_branch_for(gh: GitHubClient, pr_id: int | None, *, needed: bool) -> str:
|
|
174
238
|
"""Branch to namespace uploaded screenshots/files under (see `gh_pr.push_assets`) --
|
|
175
239
|
the checked-out branch normally, or (when acting on a PR by `--pr-id`, which may not
|
|
@@ -510,25 +574,85 @@ def pr_status(
|
|
|
510
574
|
pr_build.run(remote, pat, target_branch, wait, pr_id=pr_id, timeout=timeout)
|
|
511
575
|
|
|
512
576
|
|
|
577
|
+
def _info_text(info: pr_info_mod.PrInfo, with_repo: bool = False) -> str:
|
|
578
|
+
flags = ", ".join([info.state, *(["draft"] if info.draft else []), f"build: {info.build}"])
|
|
579
|
+
name = f"PR {info.repo}#{info.number}" if with_repo and info.repo else f"PR #{info.number}"
|
|
580
|
+
return "\n".join([f"{name}: {info.title} ({flags})", info.url, *(f"Issue #{i.number}: {i.url}" for i in info.issues)])
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _echo_infos(infos: list[pr_info_mod.PrInfo], as_json: bool, with_repo: bool) -> None:
|
|
584
|
+
if as_json:
|
|
585
|
+
typer.echo(json.dumps([i.to_json_dict() for i in infos]))
|
|
586
|
+
elif infos:
|
|
587
|
+
typer.echo("\n\n".join(_info_text(i, with_repo) for i in infos))
|
|
588
|
+
|
|
589
|
+
|
|
513
590
|
@pr_app.command("info")
|
|
514
591
|
def pr_info(
|
|
515
592
|
as_json: bool = typer.Option(False, "--json", help="Print machine-readable JSON (for status integrations that poll it)"),
|
|
516
593
|
target_branch: str | None = typer.Option(None, "--target-branch", help=_LOOKUP_TARGET_HELP),
|
|
517
|
-
|
|
594
|
+
pr_ids: list[str] | None = typer.Option(
|
|
595
|
+
None,
|
|
596
|
+
"--pr-id",
|
|
597
|
+
help="Summarise this PR directly, instead of resolving it from the current git branch (lets you skip having its "
|
|
598
|
+
f"branch checked out locally). Repeat it (or comma-separate) for several, as a list; a PR of another repo is {pr_ref.FORMS}",
|
|
599
|
+
),
|
|
600
|
+
all_prs: bool = typer.Option(
|
|
601
|
+
False,
|
|
602
|
+
"--all",
|
|
603
|
+
help="Every open PR from the current branch (e.g. one into dev and one into main) instead of exactly one: "
|
|
604
|
+
"with --json a list of the objects, [] and exit 0 when there is none",
|
|
605
|
+
),
|
|
518
606
|
pat: str | None = typer.Option(None, "--pat", envvar=["AZURE_DEVOPS_EXT_PAT", "AZURE_DEVOPS_PAT"], help="Azure DevOps PAT (else uses DefaultAzureCredential, e.g. your `az login`)"),
|
|
519
607
|
) -> None:
|
|
520
608
|
"""One-shot summary of the PR opened from the current branch, or --pr-id directly: link, state,
|
|
521
609
|
aggregate build state, and the issue(s)/work item(s) it closes. Exits 1 if there is no such PR.
|
|
522
610
|
Build state is judged the way `pr status` does (latest build per pipeline on Azure DevOps).
|
|
611
|
+
|
|
612
|
+
--all and several --pr-id values list PRs instead (with --json an array, whatever the count).
|
|
523
613
|
"""
|
|
524
|
-
|
|
614
|
+
refs: list[pr_ref.PrRef] = []
|
|
615
|
+
values: list[str] = []
|
|
616
|
+
if pr_ids:
|
|
617
|
+
values = pr_ref.split_values(pr_ids)
|
|
618
|
+
if not values:
|
|
619
|
+
raise typer.BadParameter("no PR given", param_hint="--pr-id")
|
|
620
|
+
try:
|
|
621
|
+
parsed = [pr_ref.parse_pr_ref(v, current_remote) for v in values]
|
|
622
|
+
except pr_ref.PrRefError as e:
|
|
623
|
+
raise typer.BadParameter(str(e), param_hint="--pr-id") from None
|
|
624
|
+
here = _current_remote_or_none() if any(r.remote for r in parsed) else None
|
|
625
|
+
# `5,5` and `5,owner/repo#5` are still a list of what they name once
|
|
626
|
+
unique: dict[str, pr_ref.PrRef] = {}
|
|
627
|
+
for ref in (pr_ref.canonical(r, here) for r in parsed):
|
|
628
|
+
unique.setdefault(str(ref).casefold(), ref)
|
|
629
|
+
refs = list(unique.values())
|
|
630
|
+
if all_prs and refs:
|
|
631
|
+
raise typer.BadParameter("--all lists the PRs of the current branch, it can't be combined with --pr-id", param_hint="--all")
|
|
632
|
+
|
|
633
|
+
if all_prs:
|
|
634
|
+
_echo_infos(_open_prs_info(target_branch, pat), as_json, with_repo=False)
|
|
635
|
+
return
|
|
636
|
+
if len(values) > 1:
|
|
637
|
+
infos, errors = _ref_infos(refs, pat)
|
|
638
|
+
_echo_infos(infos, as_json, with_repo=True)
|
|
639
|
+
for error in errors:
|
|
640
|
+
typer.echo(f"error: {error}", err=True)
|
|
641
|
+
if errors:
|
|
642
|
+
raise typer.Exit(1)
|
|
643
|
+
return
|
|
644
|
+
if refs and refs[0].remote is not None:
|
|
645
|
+
infos, errors = _ref_infos(refs, pat)
|
|
646
|
+
if errors:
|
|
647
|
+
typer.echo(f"error: {errors[0]}", err=True)
|
|
648
|
+
raise typer.Exit(1)
|
|
649
|
+
info = infos[0]
|
|
650
|
+
else:
|
|
651
|
+
info = _current_pr_info(refs[0].pr_id if refs else None, target_branch, pat)
|
|
525
652
|
if as_json:
|
|
526
653
|
typer.echo(json.dumps(info.to_json_dict()))
|
|
527
654
|
return
|
|
528
|
-
|
|
529
|
-
typer.echo(f"PR #{info.number}: {info.title} ({flags})\n{info.url}")
|
|
530
|
-
for issue in info.issues:
|
|
531
|
-
typer.echo(f"Issue #{issue.number}: {issue.url}")
|
|
655
|
+
typer.echo(_info_text(info))
|
|
532
656
|
|
|
533
657
|
|
|
534
658
|
@pr_app.command("retry")
|
|
@@ -630,13 +754,13 @@ def pr_update(
|
|
|
630
754
|
def pr_comment(
|
|
631
755
|
message: str | None = typer.Option(None, "--message", help="Comment text"),
|
|
632
756
|
message_file: str | None = typer.Option(
|
|
633
|
-
None, "--message-file", help="Read the comment text from this file (`-` = stdin)
|
|
757
|
+
None, "--message-file", help="Read the comment text from this file (`-` = stdin), e.g. a markdown plan"
|
|
634
758
|
),
|
|
635
759
|
screenshot: list[str] = typer.Option(
|
|
636
760
|
[], "--screenshot", help="Path to an image to embed in the comment (repeatable)"
|
|
637
761
|
),
|
|
638
762
|
file: list[str] = typer.Option(
|
|
639
|
-
[], "--file", help="Path to
|
|
763
|
+
[], "--file", help="Path to an arbitrary file to link in the comment as an attachment (repeatable)"
|
|
640
764
|
),
|
|
641
765
|
target: str | None = typer.Option(None, "--target", help=_LOOKUP_TARGET_HELP),
|
|
642
766
|
pr_id: int | None = typer.Option(None, "--pr-id", help=_PR_ID_HELP),
|
|
@@ -170,6 +170,11 @@ def resolve_pr_id(gh: GitHubClient, pr_id: int | None, target: str | None = None
|
|
|
170
170
|
return prs[choose_pr(branch, candidates)]["number"]
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
def open_pr_numbers(gh: GitHubClient, target: str | None = None) -> list[int]:
|
|
174
|
+
"""Numbers of every open PR from the current branch (into `target` if given), newest first; `[]` if there is none."""
|
|
175
|
+
return [p["number"] for p in _branch_prs(gh, current_branch(), state="open", base=target)]
|
|
176
|
+
|
|
177
|
+
|
|
173
178
|
_CONTEXT_FIELDS = """
|
|
174
179
|
fragment ContextFields on StatusCheckRollupContext {
|
|
175
180
|
__typename
|
|
@@ -55,6 +55,7 @@ class GitHubClient:
|
|
|
55
55
|
|
|
56
56
|
def __init__(self, token: str, owner: str | None = None, repo: str | None = None, base_url: str = API_URL) -> None:
|
|
57
57
|
self.base_url = base_url.rstrip("/")
|
|
58
|
+
self._token = token
|
|
58
59
|
self._owner = owner
|
|
59
60
|
self._repo = repo
|
|
60
61
|
self.session = requests.Session()
|
|
@@ -67,6 +68,10 @@ class GitHubClient:
|
|
|
67
68
|
}
|
|
68
69
|
)
|
|
69
70
|
|
|
71
|
+
def for_repo(self, owner: str, repo: str) -> GitHubClient:
|
|
72
|
+
"""The same credentials, addressing another repo."""
|
|
73
|
+
return GitHubClient(self._token, owner, repo, self.base_url)
|
|
74
|
+
|
|
70
75
|
@property
|
|
71
76
|
def owner(self) -> str:
|
|
72
77
|
self._resolve_repo()
|
|
@@ -207,6 +207,25 @@ def get_pr(session: requests.Session, remote: AdoRemote, source_branch: str, tar
|
|
|
207
207
|
sys.exit(1)
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
def get_active_prs(session: requests.Session, remote: AdoRemote, source_branch: str, target_branch: str | None) -> list[dict]:
|
|
211
|
+
"""Every active PR from `source_branch` (into `target_branch` if given); `[]` if there is none.
|
|
212
|
+
|
|
213
|
+
Unlike `get_pr` nothing is asked about and merge conflicts aren't an error: the caller wants the whole list.
|
|
214
|
+
"""
|
|
215
|
+
r = session.get(
|
|
216
|
+
f"{_base_url(remote)}/_apis/git/repositories/{remote.repo}/pullrequests",
|
|
217
|
+
params={
|
|
218
|
+
"searchCriteria.sourceRefName": f"refs/heads/{source_branch}",
|
|
219
|
+
**({"searchCriteria.targetRefName": f"refs/heads/{target_branch}"} if target_branch else {}),
|
|
220
|
+
"searchCriteria.status": "active",
|
|
221
|
+
"api-version": "7.1",
|
|
222
|
+
},
|
|
223
|
+
timeout=HTTP_TIMEOUT_SECS,
|
|
224
|
+
)
|
|
225
|
+
r.raise_for_status()
|
|
226
|
+
return r.json().get("value", [])
|
|
227
|
+
|
|
228
|
+
|
|
210
229
|
def get_pr_by_id(session: requests.Session, remote: AdoRemote, pr_id: int) -> dict:
|
|
211
230
|
"""Fetch a PR directly by its ID -- for `--pr-id` overrides, where the caller wants to act
|
|
212
231
|
on a specific PR without needing its source branch checked out locally (unlike `get_pr`,
|
|
@@ -40,6 +40,7 @@ class PrInfo:
|
|
|
40
40
|
draft: bool
|
|
41
41
|
build: str
|
|
42
42
|
issues: list[IssueRef] = field(default_factory=list)
|
|
43
|
+
repo: str = "" # "owner/repo" (GitHub) or "org/project/repo" (Azure DevOps): which repo the PR is in
|
|
43
44
|
|
|
44
45
|
def to_json_dict(self) -> dict:
|
|
45
46
|
return asdict(self)
|
|
@@ -95,6 +96,7 @@ def github_info(pr: dict, remote: GitHubRemote) -> PrInfo:
|
|
|
95
96
|
draft=bool(pr.get("isDraft")),
|
|
96
97
|
build=build_state(pr.get("statusCheckRollup") or []),
|
|
97
98
|
issues=issues,
|
|
99
|
+
repo=f"{remote.owner}/{remote.repo}",
|
|
98
100
|
)
|
|
99
101
|
|
|
100
102
|
|
|
@@ -143,4 +145,5 @@ def ado_info(session: requests.Session, pr: dict, remote: AdoRemote) -> PrInfo:
|
|
|
143
145
|
draft=bool(pr.get("isDraft")),
|
|
144
146
|
build=_ado_build(session, remote, pr),
|
|
145
147
|
issues=issues,
|
|
148
|
+
repo=f"{remote.org}/{remote.project}/{remote.repo}",
|
|
146
149
|
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""`bdt pr info --pr-id` values: which PR, in which repo.
|
|
2
|
+
|
|
3
|
+
A bare number is a PR of the current repo. For a PR of another repo (a status integration or an agent working
|
|
4
|
+
across repos) an entry carries the repo too: `owner/repo#12` on GitHub, `project/repo#12` or `org/project/repo#12` on
|
|
5
|
+
Azure DevOps (the host being the current repo's), or the PR's full web URL, which needs no checkout at all.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from .gitrepo import AdoRemote, GitHubRemote, NotAzureDevOpsRemoteError, parse_ado_remote
|
|
15
|
+
|
|
16
|
+
FORMS = "a number, owner/repo#N (GitHub), project/repo#N or org/project/repo#N (Azure DevOps), or the PR's URL"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PrRefError(ValueError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class PrRef:
|
|
25
|
+
pr_id: int
|
|
26
|
+
remote: AdoRemote | GitHubRemote | None # None: the current repo
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
if isinstance(self.remote, GitHubRemote):
|
|
30
|
+
return f"{self.remote.owner}/{self.remote.repo}#{self.pr_id}"
|
|
31
|
+
if isinstance(self.remote, AdoRemote):
|
|
32
|
+
return f"{self.remote.org}/{self.remote.project}/{self.remote.repo}#{self.pr_id}"
|
|
33
|
+
return f"#{self.pr_id}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
_GITHUB_PR_URL = re.compile(r"^https://github\.com/([^/\s]+)/([^/\s]+)/pull/(\d+)(?:[/?#].*)?$")
|
|
37
|
+
_ADO_PR_URL = re.compile(r"^https://(?:dev\.azure\.com/|[^/\s]+\.visualstudio\.com/).*?/_git/[^/\s]+/pullrequest/(\d+)(?:[/?#].*)?$")
|
|
38
|
+
_SHORTHAND = re.compile(r"^(.+)#(\d+)$")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def split_values(values: list[str]) -> list[str]:
|
|
42
|
+
"""Entries of repeated and/or comma-separated `--pr-id` values, blanks dropped."""
|
|
43
|
+
return [part.strip() for value in values for part in value.split(",") if part.strip()]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_pr_ref(text: str, current_remote: Callable[[], AdoRemote | GitHubRemote]) -> PrRef:
|
|
47
|
+
"""`current_remote` is only called for the `repo#N` forms, which take their host from it."""
|
|
48
|
+
text = text.strip()
|
|
49
|
+
if re.fullmatch(r"#?\d+", text):
|
|
50
|
+
return PrRef(int(text.lstrip("#")), None)
|
|
51
|
+
|
|
52
|
+
if match := _GITHUB_PR_URL.match(text):
|
|
53
|
+
return PrRef(int(match.group(3)), GitHubRemote(match.group(1), match.group(2)))
|
|
54
|
+
if match := _ADO_PR_URL.match(text):
|
|
55
|
+
# the legacy `https://org.visualstudio.com/DefaultCollection/Project/_git/...` form has a collection segment
|
|
56
|
+
url = text.replace("/DefaultCollection/", "/", 1) if "visualstudio.com" in text else text
|
|
57
|
+
try:
|
|
58
|
+
return PrRef(int(match.group(1)), parse_ado_remote(url))
|
|
59
|
+
except (NotAzureDevOpsRemoteError, IndexError):
|
|
60
|
+
raise PrRefError(f"can't read the Azure DevOps repo from '{text}'") from None
|
|
61
|
+
|
|
62
|
+
if match := _SHORTHAND.match(text):
|
|
63
|
+
parts = match.group(1).split("/")
|
|
64
|
+
pr_id = int(match.group(2))
|
|
65
|
+
if all(parts) and len(parts) in (2, 3):
|
|
66
|
+
try:
|
|
67
|
+
here = current_remote()
|
|
68
|
+
except (SystemExit, Exception) as e:
|
|
69
|
+
raise PrRefError(
|
|
70
|
+
f"'{text}' needs to know whether this repo is GitHub or Azure DevOps ({e}); use the PR's URL instead"
|
|
71
|
+
) from None
|
|
72
|
+
if isinstance(here, GitHubRemote) and len(parts) == 2:
|
|
73
|
+
return PrRef(pr_id, GitHubRemote(parts[0], parts[1]))
|
|
74
|
+
if isinstance(here, AdoRemote):
|
|
75
|
+
if len(parts) == 2:
|
|
76
|
+
return PrRef(pr_id, AdoRemote(here.org, parts[0], parts[1]))
|
|
77
|
+
return PrRef(pr_id, AdoRemote(parts[0], parts[1], parts[2]))
|
|
78
|
+
raise PrRefError(f"'{text}': GitHub PRs are owner/repo#N")
|
|
79
|
+
raise PrRefError(f"can't read '{text}' as a PR: use {FORMS}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def canonical(ref: PrRef, here: AdoRemote | GitHubRemote | None) -> PrRef:
|
|
83
|
+
"""`ref` as a bare number when it names a PR of the current repo `here` (compared case-insensitively), so that
|
|
84
|
+
`5` and `owner/repo#5` are one PR; `here` None (no checkout) leaves it as it is."""
|
|
85
|
+
if ref.remote is None or here is None or type(ref.remote) is not type(here):
|
|
86
|
+
return ref
|
|
87
|
+
same = [str(getattr(ref.remote, f)).casefold() for f in ref.remote.__dataclass_fields__] == [
|
|
88
|
+
str(getattr(here, f)).casefold() for f in here.__dataclass_fields__
|
|
89
|
+
]
|
|
90
|
+
return PrRef(ref.pr_id, None) if same else ref
|
|
@@ -10,7 +10,7 @@ packages = ["bmsdna"]
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "bmsdna-devtools"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.46.0"
|
|
14
14
|
description = "Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects"
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.14" # pgdevkit>=0.7.1 requires 3.14; was >=3.11 before adding it as a dependency
|
|
@@ -141,11 +141,16 @@ the command's own timeout (or run it in the background) longer than `--timeout`.
|
|
|
141
141
|
## PR summary
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
|
-
bdt pr info [--json]
|
|
144
|
+
bdt pr info [--json] [--all] [--pr-id PR]...
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
PR link, state, aggregate build state and the issue(s) it closes, in one call -- cheaper than
|
|
148
148
|
`bdt pr status` when you only need to know where things stand. Exits 1 if the branch has no PR.
|
|
149
|
+
`--all` lists every open PR of the branch instead (a JSON array with `--json`, `[]` when there is none), for a branch
|
|
150
|
+
with several PRs, e.g. into `dev` and `main`, where plain `pr info` has to ask which one.
|
|
151
|
+
`--pr-id` (repeatable, comma-separated) names PRs directly, also of other repos: a number, `owner/repo#N` (GitHub),
|
|
152
|
+
`project/repo#N` (Azure DevOps) or the PR's URL; two or more give a list, and a failing entry is an `error:` line on
|
|
153
|
+
stderr with exit 1 while the others are still printed. Every JSON object carries its `repo`.
|
|
149
154
|
(`bdt pr status --wait` is still what to use to wait for CI.)
|
|
150
155
|
|
|
151
156
|
## Creating a worktree
|