Skip to content

feat: add pr-update command to edit a PR title and/or description#43

Merged
daniel-pittman merged 2 commits into
developfrom
feat/pr-update-command
Jul 7, 2026
Merged

feat: add pr-update command to edit a PR title and/or description#43
daniel-pittman merged 2 commits into
developfrom
feat/pr-update-command

Conversation

@daniel-pittman

Copy link
Copy Markdown
Owner

Summary

Adds bb pr-update (bash) + bb_ops.pr_update (Python) + the pr_update MCP tool, closing a parity gap: the CLI could create, view, approve, merge, decline, diff, and comment on PRs but had no way to edit an open PR's title or body. Editing metadata previously meant the Bitbucket web UI.

API contract

Bitbucket Cloud's PR mutation is a PUT to the same path pr_show GETs (/repositories/{ws}/{slug}/pullrequests/{id}) — there is no PATCH for the pullrequests resource. Only the supplied fields go in the body, so the PUT merges them into the existing PR, preserving the source/destination branches and reviewers. This mirrors repo_update, which PUTs the same path repo_show GETs for the analogous repo mutation.

Symmetric across the bash and Python surfaces:

  • title: omit = leave unchanged; a non-empty/non-whitespace string sets it. An empty/whitespace title is rejected at the boundary (a PR must have a title; Bitbucket rejects a blank one).
  • description: three-way like repo_update — omit = leave unchanged, "" = clear the body, any other string = set it. A deliberate clear is not collapsed into a no-op.
  • At least one of title/description must be supplied; an empty-body PUT is rejected before any network IO.

Bash surface

bb pr-update [repo] <id> --title T [--description D | --description-file F]

--description-file handles large multiline bodies; --description and --description-file are mutually exclusive. Positionals resolve [repo] <pr-id> via the shared _resolve_pr_args heuristic, safe under set -u on bash 3.2. Aliases: pr-update, pr-edit.

Tests

15 new cases across bb_ops (PUT method + URL + body shape, the three-way description contract, boundary rejections asserting opener.calls == []) and the MCP wrapper (field forwarding, None-vs-"" preservation, error pr_id threading). pr_update added to the every-PR-op bad-id matrix and the MCP tool-registry drift guard (count 41 → 42). Full suite: 582 passing; bash -n bb clean.

Docs

Inline bb help, README PR section, and agents/bitbucket.md tool surface updated together per CONTRIBUTING.

🤖 Generated with Claude Code

https://claude.ai/code/session_011tybd7wdRdZaaQvQRJEV3d

Adds `bb pr-update` (bash) + `bb_ops.pr_update` (Python) + the `pr_update`
MCP tool, closing a parity gap: the CLI could create, view, approve,
merge, decline, diff, and comment on PRs but had no way to edit an open
PR's title or body. Editing metadata previously required the Bitbucket
web UI.

The Bitbucket Cloud contract is a PUT to the same path pr_show GETs
(/repositories/{ws}/{slug}/pullrequests/{id}); there is no PATCH for the
pullrequests resource. Only the supplied fields go in the body, so the
PUT merges them into the existing PR, preserving the source/destination
branches and reviewers. This mirrors repo_update, which PUTs the same
path repo_show GETs for the analogous repo mutation.

Contract details, symmetric across both surfaces:
- title: omit = leave unchanged; a non-empty/non-whitespace string sets
  it. An empty/whitespace title is rejected at the boundary (a PR must
  have a title; Bitbucket rejects a blank one).
- description: three-way like repo_update -- omit = leave unchanged,
  "" = clear the body, any other string = set it. A deliberate clear is
  not collapsed into a no-op.
- At least one of title/description must be supplied; an empty-body PUT
  is rejected before any network IO.

Bash surface adds `--title`, `--description`, and `--description-file`
(the last for large multiline bodies); `--description` and
`--description-file` are mutually exclusive. Positionals resolve
[repo] <pr-id> via the shared _resolve_pr_args heuristic, safe under
`set -u` on bash 3.2. Aliases: pr-update, pr-edit.

Tests: 15 new cases across bb_ops (PUT method + URL + body shape, the
three-way description contract, boundary rejections asserting no network
IO) and the MCP wrapper (field forwarding, None-vs-"" preservation, error
pr_id threading). pr_update added to the every-PR-op bad-id matrix and the
MCP tool-registry drift guard (count 41 -> 42). Full suite: 582 passing.

Docs: inline `bb help`, README PR section, and agents/bitbucket.md tool
surface updated together per CONTRIBUTING.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011tybd7wdRdZaaQvQRJEV3d
@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Claude finished @daniel-pittman's task in 2m 3s —— View job


Review: bb pr-update / bb_ops.pr_update / pr_update MCP tool

  • Read the diff (git diff origin/develop...HEAD)
  • Review bash surface (bb pr-update)
  • Review Python surface (bb_ops.pr_update + MCP tool)
  • Assess test coverage
  • Factor in Semgrep + CI Tests comments
  • Post review

Solid, well-reasoned parity implementation. It closes a real gap (editing an open PR's metadata) and models itself faithfully on the existing repo_update PUT-to-the-GET-path pattern. Boundary validation, the None/""/string three-way description contract, and the "no network IO on rejection" guarantee are all handled correctly and mirrored across both surfaces. No blocking findings.

What's done well

  • Correct API contract. PUT (not PATCH) to /pullrequests/{id} with field-merge semantics; only supplied fields go in the body, so source/destination branches and reviewers are preserved. Matches Bitbucket Cloud's actual behavior and the repo_update precedent.
  • Three-way description contract is genuinely correct. None = leave unchanged, "" = clear, other = set — and critically the MCP wrapper forwards "" verbatim (bb_ops.py:*, mcp_server.py) instead of collapsing it to None via _opt_str. Tests pin this (test_pr_update_empty_description_not_collapsed_to_none).
  • Validation order. _validate_pr_id runs before the empty-payload check, so the caller gets the most specific error; the bad-id matrix and a dedicated test both cover it. All rejection-path tests assert opener.calls == [] — no wasted API call.
  • Bash parity holds. --description/--description-file mutual exclusion, set -u-safe positional expansion ("${positionals[@]+...}"), jq --arg escaping for both fields, and the rc=0; out=$(...) || rc=$? capture pattern that the CLAUDE.md conventions call out. --description "" correctly survives _require_flag_value (empty string ≠ --*) so a deliberate clear works from the CLI too.

Minor (LOW — non-blocking)

  • Usage string presents --title as required when it isn't. The help/usage text and README render the signature as bb pr-update [repo] <id> --title T [--description D | --description-file F], which reads as "title mandatory, description optional." But the command intentionally accepts description-only updates (have_title empty is fine as long as have_description is set). The bracketing is slightly misleading versus the actual "at least one of" contract that the prose below it correctly states. Consider --title T also being bracketed, e.g. [--title T] [--description D | --description-file F]. Cosmetic only — behavior is correct. Appears in bb help (cmd_pr_update usage + cmd_help) and README.md.
    Fix this →
  • --description-file strips a single trailing newline via $(cat). Already acknowledged in an inline comment and reasonable for a PR body — noting only for completeness, not a defect.

Test coverage

Tests were added and meaningfully cover the change. The CI Tests comment reports the pytest suite green across Python 3.10/3.11/3.12 with bash syntax checks passing on 359fee2, and Semgrep (OSS) is clean.

  • bb_ops.pr_update: PUT method + exact URL + body shape; title-only / description-only omission; ""-clears-body; multiline verbatim round-trip; empty-body rejection; empty/whitespace/non-string title rejection; non-string description rejection; pr_id-validated-before-payload — each rejection asserts zero network IO.
  • MCP wrapper: field forwarding, None-passthrough, ""-not-collapsed, and pr_id threading on the error envelope.
  • Registry drift guards updated (pr_update added to the every-PR-op bad-id matrix and the tool count 41 → 42).
  • The bash cmd_pr_update path isn't unit-tested (the repo smoke-tests bash manually, per CLAUDE.md), so its behavior rests on parity with the well-covered Python side — consistent with how the rest of the CLI is validated. No additional tests needed.
    · branch feat/pr-update-command

Reviewer noted the user-facing signature 'bb pr-update [repo] <id>
--title T [--description D | --description-file F]' presents --title
as required, but description-only updates are valid (the prose below
correctly says 'at least one of'). Bracket --title too so the shape
matches the actual contract.

Four sites updated:
- cmd_pr_update usage error when neither --title nor --description
  supplied (bb:1175)
- cmd_pr_update usage error when pr_id missing (bb:1203)
- cmd_help NOTES section (bb:2860)
- README.md pr-update section

The internal cmd_pr_update comment (bb:1111) already had the correct
bracketing; user-facing strings now match it.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Semgrep (OSS): no findings

Clean scan. Posted before the Claude review.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

CI Tests

Tests passed: the pytest suite across Python 3.10/3.11/3.12 (and bash syntax checks) is green on 6ea30fbff.

Posted before the Claude review so the test outcome is folded into the review.

@claude

claude Bot commented Jul 7, 2026

Copy link
Copy Markdown

Claude finished @daniel-pittman's task in 1m 12s —— View job


Review: bb pr-update / bb_ops.pr_update / pr_update MCP tool

  • Read the diff (git diff origin/develop...HEAD)
  • Review bash surface (bb pr-update)
  • Review Python surface (bb_ops.pr_update + MCP tool)
  • Assess test coverage
  • Factor in Semgrep + CI Tests comments
  • Post review

Re-reviewed after the latest push. This is a clean, well-reasoned parity change that closes a real gap (editing an open PR's title/body without the web UI), modeled faithfully on the existing repo_update PUT-to-the-GET-path precedent. The earlier LOW finding — --title presented as required in the usage strings — is resolved in 6ea30fb: bb's inline usage, cmd_help, and README.md now all bracket it as [--title T], matching the actual "at least one of" contract. No blocking findings.

What holds up under review

  • Correct API contract. bb_ops.pr_update PUTs to f"{_prs_root(...)}/{pr_id}"/repositories/{ws}/{slug}/pullrequests/{id}, the same path pr_show GETs, and BBClient.put exists (bb_api.py:461). Field-merge semantics: only supplied fields go in the body, so source/destination branches and reviewers are preserved. PUT (not PATCH) is right for Bitbucket Cloud's pullrequests resource.
  • Three-way description contract is genuinely correct. None = leave unchanged, "" = clear, other = set — and the MCP wrapper forwards "" verbatim (no _opt_str collapse), pinned by test_pr_update_empty_description_not_collapsed_to_none. title is correctly two-way (omit vs. set), with empty/whitespace/non-string rejected at the boundary.
  • Validation order & no wasted IO. _validate_pr_id runs before the empty-payload check (bb_ops.py:427 rejects bool and non-positive), so a bad id yields the most specific error; every rejection-path test asserts opener.calls == [].
  • Bash parity holds. --description/--description-file mutual exclusion, set -u-safe positional expansion ("${positionals[@]+...}"), jq --arg escaping, the rc=0; out=$(...) || rc=$? capture pattern, and --description "" surviving _require_flag_value so a deliberate clear works from the CLI. Missing --description-file fails locally before any API call.

Minor (LOW — non-blocking)

  • --description-file strips a single trailing newline via $(cat). Already acknowledged in an inline comment and reasonable for a PR body — noted only for completeness, not a defect.

Test coverage

Tests were added and meaningfully cover the change — no HIGH test-gap finding.

  • bb_ops.pr_update (tests/test_bb_ops_prs.py): PUT method + exact URL + body shape; title-only / description-only omission; ""-clears-body; multiline verbatim round-trip; empty-body rejection; empty/whitespace/non-string title rejection; non-string description rejection; pr_id-validated-before-payload — each rejection asserts zero network IO.
  • MCP wrapper (tests/test_mcp_server.py): field forwarding, None-passthrough, ""-not-collapsed, and pr_id threading on the error envelope.
  • Registry drift guards updated (pr_update added to the every-PR-op bad-id matrix and the tool count 41 → 42).
  • The bash cmd_pr_update path isn't unit-tested (bash is smoke-tested manually per CLAUDE.md), so its behavior rests on parity with the well-covered Python side — consistent with the rest of the CLI.

The CI Tests comment reports the pytest suite green on 6ea30fbff across Python 3.10/3.11/3.12 with bash syntax checks passing, and Semgrep (OSS) is clean. (I can't run the suite here, so I'm relying on those comments.)

Verdict: ready to merge. The one prior actionable finding is fixed; remaining items are cosmetic.
· branch feat/pr-update-command

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Gate

Gate: PASS — Review reported no HIGH/CRITICAL findings.

@daniel-pittman daniel-pittman merged commit e91fb7b into develop Jul 7, 2026
13 checks passed
@daniel-pittman daniel-pittman deleted the feat/pr-update-command branch July 7, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant