Skip to content
Open
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
60 changes: 60 additions & 0 deletions atlassian/bitbucket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2102,6 +2102,66 @@ def add_pull_request_comment(
body["parent"] = {"id": parent_id}
return self.post(url, data=body)

def add_pull_request_inline_comment(
self,
project_key,
repository_slug,
pull_request_id,
text,
path,
from_hash,
to_hash,
src_path=None,
line=None,
line_type="CONTEXT",
diff_type="RANGE",
file_type=None,
parent_id=None,
):
"""
Add an inline comment on a specific line or file within a pull request.
Supported on Bitbucket Server / Data Center REST API.
See: https://developer.atlassian.com/server/bitbucket/rest/v900/api-group-pull-requests/

:param project_key: The project key (e.g. "PRJ")
:param repository_slug: The repository slug (e.g. "my-repo")
:param pull_request_id: The pull request ID (int)
:param text: The comment text
:param path: The file path relative to the repo root (e.g. "src/main.py")
:param from_hash: The source commit hash (the branch HEAD before the PR)
:param to_hash: The target commit hash (the branch HEAD after the PR)
:param src_path: The source file path (defaults to path if not provided)
:param line: The line number to comment on (required for diffType=COMMIT)
:param line_type: The line type for COMMIT diffs. One of: ADDED, REMOVED, CONTEXT
:param diff_type: "RANGE" for whole-file comment, "COMMIT" for line-level comment
:param file_type: "FROM" or "TO" — which side of the diff to comment on (COMMIT only)
:param parent_id: Optional parent comment ID for threaded replies
:return: The created comment object
"""
url = self._url_pull_request_comments(project_key, repository_slug, pull_request_id)
body = {"text": text}

anchor = {
"diffType": diff_type,
"fromHash": from_hash,
"toHash": to_hash,
"path": path,
"srcPath": src_path or path,
}

if diff_type == "COMMIT" and line is not None:
anchor["line"] = line
anchor["lineType"] = line_type
if file_type:
anchor["fileType"] = file_type

body["anchor"] = anchor

if parent_id:
body["parent"] = {"id": parent_id}

return self.post(url, data=body)

def _url_pull_request_comment(self, project_key, repository_slug, pull_request_id, comment_id):
url = f"{self._url_pull_request_comments(project_key, repository_slug, pull_request_id)}/{comment_id}"
return url
Expand Down
86 changes: 86 additions & 0 deletions tests/test_bitbucket_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,92 @@ def test_configure_repo_hook_script(self, mock_put):
)


class TestPullRequestInlineComments(TestCase):
def setUp(self):
self.bitbucket = Bitbucket(f"{mockup_server()}/bitbucket/server", username="username", password="password")
self.project_key = "PRJ"
self.repository_slug = "my-repo1-slug"
self.pull_request_id = 1

@patch.object(Bitbucket, "post")
def test_add_inline_comment_range(self, mock_post):
self.bitbucket.add_pull_request_inline_comment(
project_key=self.project_key,
repository_slug=self.repository_slug,
pull_request_id=self.pull_request_id,
text="This whole file looks great!",
path="src/main.py",
from_hash="abc123",
to_hash="def456",
diff_type="RANGE",
)
mock_post.assert_called_once()
call_args = mock_post.call_args
body = call_args[1]["data"]
self.assertEqual(body["text"], "This whole file looks great!")
self.assertIn("anchor", body)
self.assertEqual(body["anchor"]["diffType"], "RANGE")
self.assertEqual(body["anchor"]["path"], "src/main.py")
self.assertEqual(body["anchor"]["srcPath"], "src/main.py")
self.assertEqual(body["anchor"]["fromHash"], "abc123")
self.assertEqual(body["anchor"]["toHash"], "def456")

@patch.object(Bitbucket, "post")
def test_add_inline_comment_commit_line(self, mock_post):
self.bitbucket.add_pull_request_inline_comment(
project_key=self.project_key,
repository_slug=self.repository_slug,
pull_request_id=self.pull_request_id,
text="This line has a bug",
path="src/utils.py",
from_hash="aaa111",
to_hash="bbb222",
line=42,
line_type="ADDED",
diff_type="COMMIT",
file_type="TO",
)
mock_post.assert_called_once()
body = mock_post.call_args[1]["data"]
anchor = body["anchor"]
self.assertEqual(anchor["diffType"], "COMMIT")
self.assertEqual(anchor["line"], 42)
self.assertEqual(anchor["lineType"], "ADDED")
self.assertEqual(anchor["fileType"], "TO")

@patch.object(Bitbucket, "post")
def test_add_inline_comment_with_parent(self, mock_post):
self.bitbucket.add_pull_request_inline_comment(
project_key=self.project_key,
repository_slug=self.repository_slug,
pull_request_id=self.pull_request_id,
text="I agree with this comment",
path="README.md",
from_hash="ccc333",
to_hash="ddd444",
parent_id=99,
)
body = mock_post.call_args[1]["data"]
self.assertEqual(body["parent"]["id"], 99)
self.assertIn("anchor", body)

@patch.object(Bitbucket, "post")
def test_add_inline_comment_custom_src_path(self, mock_post):
self.bitbucket.add_pull_request_inline_comment(
project_key=self.project_key,
repository_slug=self.repository_slug,
pull_request_id=self.pull_request_id,
text="Renamed file comment",
path="src/new_name.py",
src_path="src/old_name.py",
from_hash="eee555",
to_hash="fff666",
)
body = mock_post.call_args[1]["data"]
self.assertEqual(body["anchor"]["path"], "src/new_name.py")
self.assertEqual(body["anchor"]["srcPath"], "src/old_name.py")


class TestPersonalRepositories(TestCase):
def setUp(self):
self.bitbucket = Bitbucket("https://bitbucket.example.com", username="admin", password="password")
Expand Down
Loading