bmsdna-devtools 0.44.0__tar.gz → 0.45.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.44.0 → bmsdna_devtools-0.45.0}/PKG-INFO +14 -2
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/README.md +13 -1
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/bdt_config.py +5 -1
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/cli.py +12 -2
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_build.py +32 -5
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/worktree.py +40 -1
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/pyproject.toml +1 -1
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_bdt_config.py +21 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_pr_id.py +49 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_build.py +4 -13
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_build_flow.py +46 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_worktree_clean.py +66 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/uv.lock +1 -1
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/.github/workflows/auto-release.yml +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/.github/workflows/python-publish.yml +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/.github/workflows/python-test.yml +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/.gitignore +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/.python-version +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/__init__.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/_api_dump.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/ado_auth.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/ado_issue.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/agent_mentions.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/api_usage.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/app_service_logs.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/azure_arm.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/azure_auth.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/cli_tools.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/commit.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/dead_code.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/env_config.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/env_files.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/find_injection.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/find_repo.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/gh_args.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/gh_issue.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/gh_pr.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/github_api.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/gitrepo.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/injection_frontend.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/injection_python.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/issue_do.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/issue_show.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/issue_watch.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_findings.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_models.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_sql.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_sql_files.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_tooling.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/lint_typescript.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/logs.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_choice.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_create_args.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_info.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_issue_link.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_labels.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pr_markdown.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/pull.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/testdb.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/translate.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/__init__.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/cli.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/pipeline.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/pricing.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/report.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/00_schema.sql +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/01_ingest_claude.sql +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/01_ingest_codex.sql +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/02_messages.sql +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/03_markers.sql +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/04_facts.sql +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/05_views.sql +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/justfile +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/skills/bmsdna-devtools/SKILL.md +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/conftest.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/fake_github.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_ado_auth.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_ado_issue.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_ado_issue_flow.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_agent_mentions.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_api_usage.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_app_service_logs.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_azure_arm.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_cleanup.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_commit.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_issue.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_pr_create.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_pull.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_tools.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_cli_version.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_commit.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_env_config.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_env_files.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_find_injection.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_find_repo.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_gh_issue.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_gh_pr.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_github_api.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_gitrepo.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_issue_do.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_issue_for_me.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_issue_show.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_issue_watch.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_lint.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_lint_models.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_lint_sql.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_lint_sql_files.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_lint_tooling.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_lint_typescript.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_logs.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_build_create_options.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_choice.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_create_args.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_description_not_truncated.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_info.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_issue_link.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_labels.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_markdown.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pull.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_testdb.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_translate.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_usage.py +0 -0
- {bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_wait_timeout.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: bmsdna-devtools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.45.0
|
|
4
4
|
Summary: Shared Azure DevOps / GitHub / git / Azure Monitor developer tooling for BMS projects
|
|
5
5
|
Requires-Python: >=3.14
|
|
6
6
|
Requires-Dist: azure-identity>=1.19.0
|
|
@@ -283,6 +283,16 @@ create prints a reminder to run `bdt pr status` afterward to check whether
|
|
|
283
283
|
the CI build passes. This is a best-effort check — failures reading policy
|
|
284
284
|
config (auth, permissions) fail open and simply skip the reminder.
|
|
285
285
|
|
|
286
|
+
## `bdt pr comment`
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
bdt pr comment --message "Looks good" [--screenshot shot.png] [--file report.pdf] [--pr-id 123]
|
|
290
|
+
bdt pr comment --message-file plan.md # post a file's text as the comment (`-` = stdin)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`--file` links the file as an *attachment* (a download link in the comment); it does not post the file's
|
|
294
|
+
content. To post a markdown document (an implementation plan, review findings) use `--message-file`.
|
|
295
|
+
|
|
286
296
|
## `bdt issue create` / `show` / `update` / `delete`, `bdt issue comment add` / `update` / `delete`
|
|
287
297
|
|
|
288
298
|
```bash
|
|
@@ -559,7 +569,9 @@ scan_jinja = ["backend/print"] # "KEY" | tr in .jinja2/.j2/.html
|
|
|
559
569
|
bdt worktree my-feature [--base dev] [--env-file .local_env] [--no-submodules] [--install "just install"]
|
|
560
570
|
```
|
|
561
571
|
|
|
562
|
-
Creates `.worktrees/<name>` branched from `--base
|
|
572
|
+
Creates `.worktrees/<name>` branched from `--base`. It first fetches `origin/<base>` and, if the local
|
|
573
|
+
`<base>` is strictly behind it, branches from `origin/<base>` instead (so the PR starts on current code);
|
|
574
|
+
a local base that is ahead of or has diverged from the remote is kept as is. Then it initializes submodules
|
|
563
575
|
(unless `--no-submodules`), and copies an env file into the new worktree as
|
|
564
576
|
`.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
|
|
565
577
|
|
|
@@ -265,6 +265,16 @@ create prints a reminder to run `bdt pr status` afterward to check whether
|
|
|
265
265
|
the CI build passes. This is a best-effort check — failures reading policy
|
|
266
266
|
config (auth, permissions) fail open and simply skip the reminder.
|
|
267
267
|
|
|
268
|
+
## `bdt pr comment`
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
bdt pr comment --message "Looks good" [--screenshot shot.png] [--file report.pdf] [--pr-id 123]
|
|
272
|
+
bdt pr comment --message-file plan.md # post a file's text as the comment (`-` = stdin)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`--file` links the file as an *attachment* (a download link in the comment); it does not post the file's
|
|
276
|
+
content. To post a markdown document (an implementation plan, review findings) use `--message-file`.
|
|
277
|
+
|
|
268
278
|
## `bdt issue create` / `show` / `update` / `delete`, `bdt issue comment add` / `update` / `delete`
|
|
269
279
|
|
|
270
280
|
```bash
|
|
@@ -541,7 +551,9 @@ scan_jinja = ["backend/print"] # "KEY" | tr in .jinja2/.j2/.html
|
|
|
541
551
|
bdt worktree my-feature [--base dev] [--env-file .local_env] [--no-submodules] [--install "just install"]
|
|
542
552
|
```
|
|
543
553
|
|
|
544
|
-
Creates `.worktrees/<name>` branched from `--base
|
|
554
|
+
Creates `.worktrees/<name>` branched from `--base`. It first fetches `origin/<base>` and, if the local
|
|
555
|
+
`<base>` is strictly behind it, branches from `origin/<base>` instead (so the PR starts on current code);
|
|
556
|
+
a local base that is ahead of or has diverged from the remote is kept as is. Then it initializes submodules
|
|
545
557
|
(unless `--no-submodules`), and copies an env file into the new worktree as
|
|
546
558
|
`.env` (auto-detects `.local_env` then `.env` if `--env-file` isn't given).
|
|
547
559
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Shared lookup for `[tool.bdt.*]` configuration tables in the consuming
|
|
2
2
|
repo's pyproject.toml (walks up from a start directory the same way `git`
|
|
3
|
-
looks for `.git`, so it works from any subdirectory of the repo).
|
|
3
|
+
looks for `.git`, so it works from any subdirectory of the repo). The walk stops at the
|
|
4
|
+
repository root (the first directory holding a `.git` dir or file), so a submodule or nested
|
|
5
|
+
repo never picks up the config of the repo that contains it.
|
|
4
6
|
"""
|
|
5
7
|
|
|
6
8
|
from __future__ import annotations
|
|
@@ -15,6 +17,8 @@ def find_pyproject(start: Path | None = None) -> Path | None:
|
|
|
15
17
|
candidate = directory / "pyproject.toml"
|
|
16
18
|
if candidate.is_file():
|
|
17
19
|
return candidate
|
|
20
|
+
if (directory / ".git").exists():
|
|
21
|
+
return None
|
|
18
22
|
return None
|
|
19
23
|
|
|
20
24
|
|
|
@@ -629,11 +629,14 @@ def pr_update(
|
|
|
629
629
|
@pr_app.command("comment")
|
|
630
630
|
def pr_comment(
|
|
631
631
|
message: str | None = typer.Option(None, "--message", help="Comment text"),
|
|
632
|
+
message_file: str | None = typer.Option(
|
|
633
|
+
None, "--message-file", help="Read the comment text from this file (`-` = stdin); unlike --file, the content is posted as the comment"
|
|
634
|
+
),
|
|
632
635
|
screenshot: list[str] = typer.Option(
|
|
633
636
|
[], "--screenshot", help="Path to an image to embed in the comment (repeatable)"
|
|
634
637
|
),
|
|
635
638
|
file: list[str] = typer.Option(
|
|
636
|
-
[], "--file", help="Path to
|
|
639
|
+
[], "--file", help="Path to a file to link in the comment as an attachment (repeatable); to post a file's text as the comment use --message-file"
|
|
637
640
|
),
|
|
638
641
|
target: str | None = typer.Option(None, "--target", help=_LOOKUP_TARGET_HELP),
|
|
639
642
|
pr_id: int | None = typer.Option(None, "--pr-id", help=_PR_ID_HELP),
|
|
@@ -647,6 +650,13 @@ def pr_comment(
|
|
|
647
650
|
"""Post a comment on the PR opened from the current branch, or --pr-id directly
|
|
648
651
|
(Azure DevOps or GitHub, auto-detected).
|
|
649
652
|
"""
|
|
653
|
+
if message_file is not None:
|
|
654
|
+
if message is not None:
|
|
655
|
+
raise typer.BadParameter("pass either --message or --message-file, not both", param_hint="--message-file")
|
|
656
|
+
try:
|
|
657
|
+
message = sys.stdin.read() if message_file == "-" else Path(message_file).read_text(encoding="utf-8")
|
|
658
|
+
except OSError as e:
|
|
659
|
+
raise typer.BadParameter(f"cannot read {message_file!r}: {e}", param_hint="--message-file") from e
|
|
650
660
|
for path in screenshot:
|
|
651
661
|
if not Path(path).is_file():
|
|
652
662
|
raise typer.BadParameter(f"Screenshot not found: {path}", param_hint="--screenshot")
|
|
@@ -654,7 +664,7 @@ def pr_comment(
|
|
|
654
664
|
if not Path(path).is_file():
|
|
655
665
|
raise typer.BadParameter(f"File not found: {path}", param_hint="--file")
|
|
656
666
|
if not message and not screenshot and not file:
|
|
657
|
-
raise typer.BadParameter("Provide at least one of --message, --screenshot, --file")
|
|
667
|
+
raise typer.BadParameter("Provide at least one of --message, --message-file, --screenshot, --file")
|
|
658
668
|
|
|
659
669
|
remote = current_remote()
|
|
660
670
|
if isinstance(remote, GitHubRemote):
|
|
@@ -561,13 +561,40 @@ def update(
|
|
|
561
561
|
def link_work_item(session: requests.Session, remote: AdoRemote, pr_id: int, work_item_id: int) -> None:
|
|
562
562
|
"""Link work item `work_item_id` to PR `pr_id`, so it shows up in the PR's "Work Items" tab
|
|
563
563
|
(and the work item's own "Development" links) -- the Azure DevOps equivalent of GitHub's
|
|
564
|
-
closing-keyword PR<->issue link
|
|
565
|
-
|
|
566
|
-
|
|
564
|
+
closing-keyword PR<->issue link.
|
|
565
|
+
|
|
566
|
+
There is no REST resource that links "PR -> work item" directly; what `az repos pr work-item add`
|
|
567
|
+
does (and so do we) is add an `ArtifactLink` relation to the *work item*, pointing at the PR's
|
|
568
|
+
`vstfs:///Git/PullRequestId/{projectId}%2F{repoId}%2F{prId}` artifact URI. Idempotent -- an
|
|
569
|
+
already-linked work item is left alone (adding the relation twice would be rejected).
|
|
567
570
|
"""
|
|
568
|
-
r = session.
|
|
569
|
-
f"{_base_url(remote)}/_apis/git/repositories/{quote(remote.repo, safe='')}/
|
|
571
|
+
r = session.get(
|
|
572
|
+
f"{_base_url(remote)}/_apis/git/repositories/{quote(remote.repo, safe='')}/pullrequests/{pr_id}",
|
|
573
|
+
params={"api-version": "7.1"},
|
|
574
|
+
timeout=HTTP_TIMEOUT_SECS,
|
|
575
|
+
)
|
|
576
|
+
r.raise_for_status()
|
|
577
|
+
repository = r.json()["repository"]
|
|
578
|
+
artifact_url = f"vstfs:///Git/PullRequestId/{repository['project']['id']}%2F{repository['id']}%2F{pr_id}"
|
|
579
|
+
|
|
580
|
+
work_item_url = f"{_base_url(remote)}/_apis/wit/workitems/{work_item_id}"
|
|
581
|
+
r = session.get(work_item_url, params={"$expand": "relations", "api-version": "7.1"}, timeout=HTTP_TIMEOUT_SECS)
|
|
582
|
+
r.raise_for_status()
|
|
583
|
+
existing = r.json().get("relations") or []
|
|
584
|
+
if any(rel.get("rel") == "ArtifactLink" and str(rel.get("url", "")).lower() == artifact_url.lower() for rel in existing):
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
r = session.patch(
|
|
588
|
+
work_item_url,
|
|
570
589
|
params={"api-version": "7.1"},
|
|
590
|
+
json=[
|
|
591
|
+
{
|
|
592
|
+
"op": "add",
|
|
593
|
+
"path": "/relations/-",
|
|
594
|
+
"value": {"rel": "ArtifactLink", "url": artifact_url, "attributes": {"name": "Pull Request"}},
|
|
595
|
+
}
|
|
596
|
+
],
|
|
597
|
+
headers={"Content-Type": "application/json-patch+json"},
|
|
571
598
|
timeout=HTTP_TIMEOUT_SECS,
|
|
572
599
|
)
|
|
573
600
|
r.raise_for_status()
|
|
@@ -59,6 +59,43 @@ def _run_capture(cmd: list[str], cwd: Path | None = None) -> subprocess.Complete
|
|
|
59
59
|
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, check=False)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
def _git_ok(args: list[str], cwd: Path, timeout: float = 5.0) -> bool:
|
|
63
|
+
try:
|
|
64
|
+
return subprocess.run(["git", *args], cwd=cwd, capture_output=True, check=False, timeout=timeout).returncode == 0
|
|
65
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _start_point(root: Path, base: str) -> tuple[str, bool]:
|
|
70
|
+
"""Where a new worktree branch should start, and whether that is a remote-tracking ref.
|
|
71
|
+
|
|
72
|
+
`base` is usually a long-lived local branch (`dev`) that nobody keeps up to date, so a worktree
|
|
73
|
+
branched from it starts behind -- the PR then shows merge conflicts and the plan gets written
|
|
74
|
+
against stale code. Fetch `origin/<base>` first and start from it when the local branch is
|
|
75
|
+
strictly behind. A local branch that is ahead of / diverged from the remote is left alone (its
|
|
76
|
+
unpushed work is on purpose), as is everything when there is no `origin` or it can't be reached.
|
|
77
|
+
"""
|
|
78
|
+
local = f"refs/heads/{base}"
|
|
79
|
+
remote = f"refs/remotes/origin/{base}"
|
|
80
|
+
has_local = _git_ok(["rev-parse", "--verify", "--quiet", local], root)
|
|
81
|
+
if not _git_ok(["remote", "get-url", "origin"], root):
|
|
82
|
+
return base, False
|
|
83
|
+
if not _git_ok(["fetch", "--quiet", "origin", f"+refs/heads/{base}:{remote}"], root, timeout=pull_mod.CLI_TIMEOUT_SECS):
|
|
84
|
+
if has_local:
|
|
85
|
+
print(f"warning: could not fetch origin/{base}; branching from the local {base}, which may be behind", file=sys.stderr)
|
|
86
|
+
return base, False
|
|
87
|
+
if not has_local:
|
|
88
|
+
return f"origin/{base}", True
|
|
89
|
+
if _git_ok(["merge-base", "--is-ancestor", remote, local], root):
|
|
90
|
+
return base, False # up to date or ahead of the remote
|
|
91
|
+
if _git_ok(["merge-base", "--is-ancestor", local, remote], root):
|
|
92
|
+
behind = _run_capture(["git", "rev-list", "--count", f"{local}..{remote}"], cwd=root).stdout.strip()
|
|
93
|
+
print(f"{base} is {behind} commit(s) behind origin/{base}; branching from origin/{base}")
|
|
94
|
+
return f"origin/{base}", True
|
|
95
|
+
print(f"warning: local {base} and origin/{base} have diverged; branching from the local {base}", file=sys.stderr)
|
|
96
|
+
return base, False
|
|
97
|
+
|
|
98
|
+
|
|
62
99
|
def create(
|
|
63
100
|
name: str,
|
|
64
101
|
base: str = "dev",
|
|
@@ -72,7 +109,9 @@ def create(
|
|
|
72
109
|
if path.exists():
|
|
73
110
|
sys.exit(f"error: {path} already exists")
|
|
74
111
|
|
|
75
|
-
|
|
112
|
+
start, remote_start = _start_point(root, base)
|
|
113
|
+
# --no-track: the new branch must not push to / compare against origin/<base>
|
|
114
|
+
_run(["git", "worktree", "add", *(["--no-track"] if remote_start else []), str(path), "-b", name, start], cwd=root)
|
|
76
115
|
|
|
77
116
|
if submodules and (root / ".gitmodules").exists():
|
|
78
117
|
_run(["git", "submodule", "update", "--init"], cwd=path)
|
|
@@ -10,7 +10,7 @@ packages = ["bmsdna"]
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "bmsdna-devtools"
|
|
13
|
-
version = "0.
|
|
13
|
+
version = "0.45.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
|
|
@@ -29,3 +29,24 @@ def test_load_bdt_table_returns_empty_when_key_absent(tmp_path) -> None:
|
|
|
29
29
|
def test_load_bdt_table_reads_configured_table(tmp_path) -> None:
|
|
30
30
|
write_pyproject(tmp_path, "[tool.bdt.ado]\nboard = 'My Team'\n")
|
|
31
31
|
assert load_bdt_table("ado", tmp_path) == {"board": "My Team"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_find_pyproject_stops_at_the_repo_root(tmp_path) -> None:
|
|
35
|
+
"""A submodule (its `.git` is a file) is another repository: the parent repo's config must not apply."""
|
|
36
|
+
write_pyproject(tmp_path, "[tool.bdt.pr.required_labels]\ne2e_area = ['e2e-full']\n")
|
|
37
|
+
(tmp_path / ".git").mkdir()
|
|
38
|
+
sub = tmp_path / "database"
|
|
39
|
+
(sub / "src").mkdir(parents=True)
|
|
40
|
+
(sub / ".git").write_text("gitdir: ../.git/modules/database\n")
|
|
41
|
+
|
|
42
|
+
assert find_pyproject(sub / "src") is None
|
|
43
|
+
assert load_bdt_table("pr", sub / "src") == {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_find_pyproject_prefers_the_repo_roots_own_pyproject(tmp_path) -> None:
|
|
47
|
+
write_pyproject(tmp_path, "[project]\nname = 'x'\n")
|
|
48
|
+
(tmp_path / ".git").mkdir()
|
|
49
|
+
nested = tmp_path / "a" / "b"
|
|
50
|
+
nested.mkdir(parents=True)
|
|
51
|
+
|
|
52
|
+
assert find_pyproject(nested) == tmp_path / "pyproject.toml"
|
|
@@ -232,3 +232,52 @@ def test_pr_comment_with_pr_id_resolves_by_id_and_skips_branch_for_ado(monkeypat
|
|
|
232
232
|
|
|
233
233
|
assert result.exit_code == 0, result.output
|
|
234
234
|
assert captured == {"pr_id": 99, "message": "hi"}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_pr_comment_message_file_posts_the_file_content(monkeypatch, tmp_path) -> None:
|
|
238
|
+
monkeypatch.setattr("bmsdna.devtools.cli.current_remote", lambda: GITHUB_REMOTE)
|
|
239
|
+
monkeypatch.setattr("bmsdna.devtools.cli.require_gh", lambda: "gh")
|
|
240
|
+
captured = {}
|
|
241
|
+
monkeypatch.setattr(
|
|
242
|
+
"bmsdna.devtools.cli.gh_pr.comment_with_screenshots",
|
|
243
|
+
lambda gh, owner, repo, branch, message, screenshot, file, pr_id=None: captured.update(message=message, file=file),
|
|
244
|
+
)
|
|
245
|
+
plan = tmp_path / "plan.md"
|
|
246
|
+
plan.write_text("# Plan\n\n- step 1\n", encoding="utf-8")
|
|
247
|
+
|
|
248
|
+
result = runner.invoke(app, ["pr", "comment", "--pr-id", "99", "--message-file", str(plan)])
|
|
249
|
+
|
|
250
|
+
assert result.exit_code == 0, result.output
|
|
251
|
+
assert captured == {"message": "# Plan\n\n- step 1\n", "file": []}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_pr_comment_message_file_reads_stdin_for_dash(monkeypatch) -> None:
|
|
255
|
+
monkeypatch.setattr("bmsdna.devtools.cli.current_remote", lambda: GITHUB_REMOTE)
|
|
256
|
+
monkeypatch.setattr("bmsdna.devtools.cli.require_gh", lambda: "gh")
|
|
257
|
+
captured = {}
|
|
258
|
+
monkeypatch.setattr(
|
|
259
|
+
"bmsdna.devtools.cli.gh_pr.comment_with_screenshots",
|
|
260
|
+
lambda gh, owner, repo, branch, message, screenshot, file, pr_id=None: captured.update(message=message),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
result = runner.invoke(app, ["pr", "comment", "--pr-id", "99", "--message-file", "-"], input="from stdin")
|
|
264
|
+
|
|
265
|
+
assert result.exit_code == 0, result.output
|
|
266
|
+
assert captured == {"message": "from stdin"}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_pr_comment_message_and_message_file_are_exclusive(tmp_path) -> None:
|
|
270
|
+
plan = tmp_path / "plan.md"
|
|
271
|
+
plan.write_text("x", encoding="utf-8")
|
|
272
|
+
|
|
273
|
+
result = runner.invoke(app, ["pr", "comment", "--message", "hi", "--message-file", str(plan)])
|
|
274
|
+
|
|
275
|
+
assert result.exit_code != 0
|
|
276
|
+
assert "not both" in result.output
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_pr_comment_message_file_missing_is_a_usage_error(tmp_path) -> None:
|
|
280
|
+
result = runner.invoke(app, ["pr", "comment", "--message-file", str(tmp_path / "nope.md")])
|
|
281
|
+
|
|
282
|
+
assert result.exit_code == 2
|
|
283
|
+
assert "cannot read" in result.output
|
|
@@ -418,24 +418,15 @@ def test_get_pr_by_id_exits_on_merge_conflict() -> None:
|
|
|
418
418
|
# -- link_work_item -----------------------------------------------------------
|
|
419
419
|
|
|
420
420
|
|
|
421
|
-
def test_link_work_item_posts_to_the_pr_work_items_endpoint() -> None:
|
|
422
|
-
remote = AdoRemote("myorg", "MyProj", "myrepo")
|
|
423
|
-
session = MagicMock()
|
|
424
|
-
session.post.return_value = MagicMock(status_code=200)
|
|
425
|
-
|
|
426
|
-
link_work_item(session, remote, pr_id=456, work_item_id=99)
|
|
427
|
-
|
|
428
|
-
url = session.post.call_args.args[0]
|
|
429
|
-
assert url == "https://dev.azure.com/myorg/MyProj/_apis/git/repositories/myrepo/pullRequests/456/workitems/99"
|
|
430
|
-
session.post.return_value.raise_for_status.assert_called_once()
|
|
431
|
-
|
|
432
|
-
|
|
433
421
|
def test_link_work_item_raises_on_http_error() -> None:
|
|
434
422
|
remote = AdoRemote("myorg", "MyProj", "myrepo")
|
|
435
423
|
session = MagicMock()
|
|
424
|
+
session.get.return_value = MagicMock(
|
|
425
|
+
status_code=200, json=lambda: {"repository": {"id": "r", "project": {"id": "p"}}, "relations": []}
|
|
426
|
+
)
|
|
436
427
|
response = MagicMock(status_code=404)
|
|
437
428
|
response.raise_for_status.side_effect = requests.HTTPError("not found")
|
|
438
|
-
session.
|
|
429
|
+
session.patch.return_value = response
|
|
439
430
|
|
|
440
431
|
with pytest.raises(requests.HTTPError):
|
|
441
432
|
link_work_item(session, remote, pr_id=456, work_item_id=99)
|
|
@@ -651,3 +651,49 @@ def test_run_wait_checks_build_policy_only_once(monkeypatch) -> None:
|
|
|
651
651
|
run(REMOTE, "fake-pat", "main", wait=True, source_branch="feature-x")
|
|
652
652
|
|
|
653
653
|
assert calls == ["main"]
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# --- link_work_item -------------------------------------------------------
|
|
657
|
+
|
|
658
|
+
_PR_BODY = {"pullRequestId": 42, "repository": {"id": "repo-guid", "project": {"id": "proj-guid"}}}
|
|
659
|
+
_ARTIFACT = "vstfs:///Git/PullRequestId/proj-guid%2Frepo-guid%2F42"
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def _link_session(relations: list[dict]) -> MagicMock:
|
|
663
|
+
session = MagicMock()
|
|
664
|
+
|
|
665
|
+
def fake_get(url, params=None, **kwargs):
|
|
666
|
+
if "/pullrequests/42" in url:
|
|
667
|
+
return FakeResponse(_PR_BODY)
|
|
668
|
+
if "/_apis/wit/workitems/7" in url:
|
|
669
|
+
return FakeResponse({"id": 7, "relations": relations})
|
|
670
|
+
raise AssertionError(f"unexpected GET {url}")
|
|
671
|
+
|
|
672
|
+
session.get.side_effect = fake_get
|
|
673
|
+
session.patch.return_value = FakeResponse({})
|
|
674
|
+
return session
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def test_link_work_item_adds_an_artifact_link_relation_on_the_work_item() -> None:
|
|
678
|
+
from bmsdna.devtools.pr_build import link_work_item
|
|
679
|
+
|
|
680
|
+
session = _link_session([{"rel": "AttachedFile", "url": "https://x/attachment"}])
|
|
681
|
+
|
|
682
|
+
link_work_item(session, AdoRemote(org="myorg", project="BMS - CCMT2", repo="BMS - CCMT2"), 42, 7)
|
|
683
|
+
|
|
684
|
+
(url,), kwargs = session.patch.call_args
|
|
685
|
+
assert url == "https://dev.azure.com/myorg/BMS%20-%20CCMT2/_apis/wit/workitems/7"
|
|
686
|
+
assert kwargs["headers"] == {"Content-Type": "application/json-patch+json"}
|
|
687
|
+
assert kwargs["json"] == [
|
|
688
|
+
{"op": "add", "path": "/relations/-", "value": {"rel": "ArtifactLink", "url": _ARTIFACT, "attributes": {"name": "Pull Request"}}}
|
|
689
|
+
]
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def test_link_work_item_is_idempotent() -> None:
|
|
693
|
+
from bmsdna.devtools.pr_build import link_work_item
|
|
694
|
+
|
|
695
|
+
session = _link_session([{"rel": "ArtifactLink", "url": _ARTIFACT.upper().replace("VSTFS", "vstfs")}])
|
|
696
|
+
|
|
697
|
+
link_work_item(session, REMOTE, 42, 7)
|
|
698
|
+
|
|
699
|
+
session.patch.assert_not_called()
|
|
@@ -108,6 +108,72 @@ def test_create_does_not_hint_no_default_when_base_is_main_but_remotes_real_defa
|
|
|
108
108
|
assert "--no-default" not in out
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
# --- create: starts from a fetched origin/<base> -------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _clone_with_dev(tmp_path):
|
|
115
|
+
"""A clone whose `origin` has a `dev` branch; returns (origin, clone)."""
|
|
116
|
+
origin = init_repo(tmp_path / "origin")
|
|
117
|
+
_git(["branch", "dev"], cwd=origin)
|
|
118
|
+
clone = tmp_path / "clone"
|
|
119
|
+
_git(["clone", "-q", str(origin), str(clone)], cwd=tmp_path)
|
|
120
|
+
_git(["config", "user.email", "test@example.com"], cwd=clone)
|
|
121
|
+
_git(["config", "user.name", "Test"], cwd=clone)
|
|
122
|
+
_git(["branch", "dev", "origin/dev"], cwd=clone)
|
|
123
|
+
return origin, clone
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _commit(repo, branch, filename):
|
|
127
|
+
_git(["checkout", "-q", branch], cwd=repo)
|
|
128
|
+
(repo / filename).write_text(filename)
|
|
129
|
+
_git(["add", "."], cwd=repo)
|
|
130
|
+
_git(["commit", "-q", "-m", filename], cwd=repo)
|
|
131
|
+
_git(["checkout", "-q", "main"], cwd=repo)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_create_branches_from_fetched_origin_when_local_base_is_behind(tmp_path, capsys) -> None:
|
|
135
|
+
origin, clone = _clone_with_dev(tmp_path)
|
|
136
|
+
_commit(origin, "dev", "new-on-origin.txt")
|
|
137
|
+
|
|
138
|
+
path = create("my-feature", base="dev", submodules=False, root=clone)
|
|
139
|
+
|
|
140
|
+
assert (path / "new-on-origin.txt").exists()
|
|
141
|
+
assert "1 commit(s) behind origin/dev" in capsys.readouterr().out
|
|
142
|
+
# not tracking origin/dev: a plain `git push` must not target the base branch
|
|
143
|
+
assert _git(["config", "--get", "branch.my-feature.remote"], cwd=clone, check=False).stdout.strip() == ""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_create_keeps_local_base_when_it_is_ahead_of_origin(tmp_path) -> None:
|
|
147
|
+
_, clone = _clone_with_dev(tmp_path)
|
|
148
|
+
_commit(clone, "dev", "local-only.txt")
|
|
149
|
+
|
|
150
|
+
path = create("my-feature", base="dev", submodules=False, root=clone)
|
|
151
|
+
|
|
152
|
+
assert (path / "local-only.txt").exists()
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_create_warns_and_keeps_local_base_when_diverged(tmp_path, capsys) -> None:
|
|
156
|
+
origin, clone = _clone_with_dev(tmp_path)
|
|
157
|
+
_commit(origin, "dev", "on-origin.txt")
|
|
158
|
+
_commit(clone, "dev", "local-only.txt")
|
|
159
|
+
|
|
160
|
+
path = create("my-feature", base="dev", submodules=False, root=clone)
|
|
161
|
+
|
|
162
|
+
assert (path / "local-only.txt").exists()
|
|
163
|
+
assert not (path / "on-origin.txt").exists()
|
|
164
|
+
assert "diverged" in capsys.readouterr().err
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_create_uses_local_base_without_an_origin_remote(tmp_path, capsys) -> None:
|
|
168
|
+
repo = init_repo(tmp_path / "repo")
|
|
169
|
+
_git(["branch", "dev"], cwd=repo)
|
|
170
|
+
|
|
171
|
+
path = create("my-feature", base="dev", submodules=False, root=repo)
|
|
172
|
+
|
|
173
|
+
assert path.exists()
|
|
174
|
+
assert "warning" not in capsys.readouterr().err
|
|
175
|
+
|
|
176
|
+
|
|
111
177
|
# --- find_repos -----------------------------------------------------------
|
|
112
178
|
|
|
113
179
|
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/01_ingest_claude.sql
RENAMED
|
File without changes
|
{bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/bmsdna/devtools/usage/sql/01_ingest_codex.sql
RENAMED
|
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
|
|
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
|
{bmsdna_devtools-0.44.0 → bmsdna_devtools-0.45.0}/tests/test_pr_description_not_truncated.py
RENAMED
|
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
|