Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions tools/bitbucket/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Implemented read-only commands:
- `magpie-bitbucket pr request-changes <id>` (Cloud-only write)
- `magpie-bitbucket pr remove-request-changes <id>` (Cloud-only write)
- `magpie-bitbucket pr decline <id>` (Cloud-only write)
- `magpie-bitbucket pr reject <id> --reason-file <path>` (Cloud-only write)
- `magpie-bitbucket pr merge <id> --strategy {merge,squash,rebase} --expected-source-commit <sha>` (Cloud-only write)
- `magpie-bitbucket pr merge-task-status <id> <task-id>` (Cloud-only read)
- `magpie-bitbucket pr tasks <id>`
Expand All @@ -99,7 +100,7 @@ activity where exposed by the configured Bitbucket backend.

Write coverage is intentionally narrow. The bridge supports confirmed
Bitbucket Cloud issue-comment creation, top-level pull-request comment creation,
and pull-request approve/unapprove, request-changes/remove-request-changes, decline, and merge actions after the calling skill has obtained
and pull-request approve/unapprove, request-changes/remove-request-changes, decline, backend-neutral reject, and merge actions after the calling skill has obtained
explicit user confirmation. Other writes, such as editing/deleting comments,
creating/updating issues, changing branches, or triggering builds, remain out
of scope and should be added separately with narrow command surfaces and
Expand Down Expand Up @@ -163,7 +164,7 @@ surface:
| Change requests | `post_review` / `pr review <id> --verdict {comment,approve,request-changes} --body-file <path>` | Partial write, Cloud only | Implements the backend-neutral review contract for Bitbucket Cloud after explicit caller-side confirmation. `comment` posts the review body only. `approve` and `request-changes` post the body first and then apply the corresponding Bitbucket verdict. If the second mutation fails after the body was posted, the command fails with an explicit partial-outcome warning so callers inspect the PR before retrying. Data Center post-review writes remain unsupported. |
| Change requests | `land` / `pr merge <id> --strategy {merge,squash,rebase} --expected-source-commit <sha>` | Partial write, Cloud only | Submits a Bitbucket Cloud pull-request merge after explicit caller-side confirmation. The caller must run and inspect `pr merge-checks <id>` before invoking this command; `pr merge` does not independently enforce approval, build-status, or merge-check gates. The expected source commit (7–40 hexadecimal characters, rejected before any request otherwise) is checked immediately before the merge POST so a changed PR head fails closed. The requested strategy is mapped to Bitbucket's merge strategy (reported as `backend_strategy`) and the resulting merge commit is returned as `landed_ref` when available. An asynchronous merge may be accepted before a `landed_ref` is available; `merge_status` is then `submitted`, with Bitbucket's own task state in `task_status` and the task URL in `task_url`. A timeout on the merge POST is reported as "outcome unknown" — check `pr get <id>` before retrying, since the merge may already be running. Data Center merge writes remain unsupported. |
| Change requests | `pr merge-task-status <id> <task-id>` | Partial read-only, Cloud only | Fetches the status of an asynchronous Bitbucket Cloud pull-request merge task. Pending tasks normalize to `merge_status=submitted`; successful tasks expose `merge_result.merge_commit.hash` as `landed_ref`; failed tasks normalize to `merge_status=failed`. Data Center merge task-status reads remain unsupported. |
| Change requests | `reject` | Not implemented | Follow-up work for #606. |
| Change requests | `reject` / `pr reject <id> --reason-file <path>` | Partial write, Cloud only | Implements the backend-neutral reject contract for Bitbucket Cloud after explicit caller-side confirmation. Posts the confirmed rejection reason as a pull-request comment, then declines the pull request. If the decline fails after the reason was posted, the command reports the partial outcome and directs the caller to inspect the PR before retrying to avoid duplicating the rejection comment. Data Center reject writes remain unsupported. |
| Tracker | `issue list-open` / `issue get <id>` / `issue comments <id>` / `issue attachments <id>` | Partial read-only, Cloud only | Lists and fetches Bitbucket Cloud issues, issue comments, and issue attachment metadata/links where the repository issue tracker is enabled. Bitbucket Data Center native issue reads/comments/attachments are unsupported; linked Jira handoff remains separate follow-up work. |
| Tracker | `issue comment <id> --body-file <path>` | Partial write, Cloud only | Creates one Bitbucket Cloud issue comment from a caller-supplied body file. The calling skill must obtain explicit user confirmation before invoking this mutation. Bitbucket Data Center native issue comment writes are unsupported; linked Jira coverage remains separate. |
| CI | `pr status <id>` | Partial read-only | Fetches build/status checks for a pull request. This does not trigger, retry, or mutate Pipelines/builds. |
Expand Down
26 changes: 26 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,20 @@ def _build_parser() -> argparse.ArgumentParser:
help="Pull request ID whose change request to remove.",
)

pr_reject = pr_subparsers.add_parser(
"reject",
help="Reject a pull request with a reason after caller-side confirmation.",
)
pr_reject.add_argument(
"pull_request_id",
help="Pull request ID to reject.",
)
pr_reject.add_argument(
"--reason-file",
required=True,
help="Path to the confirmed rejection reason.",
)

pr_decline = pr_subparsers.add_parser(
"decline",
help="Decline a pull request after caller-side confirmation.",
Expand Down Expand Up @@ -376,6 +390,18 @@ def _dispatch(args: argparse.Namespace, config: BitbucketConfig) -> dict[str, An
requested=False,
)

if args.subcommand == "pr" and args.pr_action == "reject":
reason = _read_body_file(args.reason_file)
raw = backend.reject_pull_request(
config,
args.pull_request_id,
reason,
)
return normalize.rejected_pull_request(
config.kind,
raw,
)

if args.subcommand == "pr" and args.pr_action == "decline":
raw = backend.decline_pull_request(
config,
Expand Down
37 changes: 37 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,43 @@ def post_pull_request_review(
}


def reject_pull_request(
config: BitbucketConfig,
pull_request_id: str,
reason: str,
) -> dict[str, Any]:
"""Reject one Bitbucket Cloud pull request with a confirmed reason."""
if not reason.strip():
raise BitbucketError("Pull request rejection reason must not be empty")

comment_result = create_pull_request_comment(
config,
pull_request_id,
reason,
)
comment = comment_result.get("comment")

try:
decline_result = decline_pull_request(
config,
pull_request_id,
)
except BitbucketError as exc:
raise BitbucketError(
"Bitbucket pull request rejection reason was posted, but the "
f"decline failed. Inspect `pr discussion {pull_request_id}` and "
f"`pr get {pull_request_id}` before retrying to avoid duplicating "
"the rejection comment."
) from exc

return {
"pull_request_id": pull_request_id,
"reason": reason,
"comment": comment,
"pull_request": decline_result.get("pull_request"),
}


def get_pull_request_discussion(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch pull request comments from Bitbucket Cloud."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
Expand Down
12 changes: 12 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/datacenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,18 @@ def post_pull_request_review(
)


def reject_pull_request(
config: BitbucketConfig,
pull_request_id: str,
reason: str,
) -> dict[str, Any]:
"""Reject pull-request reject writes for Bitbucket Data Center for now."""
_ = (config, pull_request_id, reason)
raise BitbucketError(
"Bitbucket Data Center pull request reject writes are not supported by this command yet"
)


def get_pull_request_discussion(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch pull request activities from Bitbucket Data Center."""
project_key = quote_path(require(config.project_key, "BITBUCKET_PROJECT_KEY"))
Expand Down
22 changes: 22 additions & 0 deletions tools/bitbucket/src/magpie_bitbucket/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,28 @@ def posted_pull_request_review(
}


def rejected_pull_request(
kind: str,
raw: dict[str, Any],
) -> dict[str, Any]:
"""Normalize a backend-neutral pull-request rejection."""
comment = raw.get("comment")
normalized_comment = _cloud_comment(comment) if kind == "cloud" and isinstance(comment, dict) else {}

pull_request = raw.get("pull_request")

return {
"ok": bool(normalized_comment) and isinstance(pull_request, dict),
"backend": "bitbucket-cloud" if kind == "cloud" else "bitbucket-datacenter",
"operation": "pull-request-reject",
"pull_request_id": _string(raw.get("pull_request_id")),
"reason": _string(raw.get("reason")),
"comment": normalized_comment,
"pull_request": pull_request if isinstance(pull_request, dict) else None,
"raw": raw,
}


def pull_request_discussion(kind: str, raw: dict[str, Any]) -> dict[str, Any]:
"""Normalize pull request discussion/comments from Bitbucket."""
values = raw.get("values")
Expand Down
Loading