Skip to content

Commit

Permalink
Auto-populate PR description from commit message (#1370)
Browse files Browse the repository at this point in the history
Extends the behavior that auto-populates title when there is a single commit to also copy the entire commit comment to PR description. Resolves #695.

Co-authored-by: Aidan Ryan <[email protected]>
  • Loading branch information
ajryan and aidanryan-msft authored Oct 23, 2023
1 parent bd34a6f commit 4c3ef23
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 14 deletions.
24 changes: 20 additions & 4 deletions azure-devops/azext_devops/dev/repos/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,20 +187,34 @@ def create_pull_request(project=None, repository=None, source_branch=None, targe
pr.work_item_refs = resolved_work_items
pr = client.create_pull_request(git_pull_request_to_create=pr, project=project,
repository_id=repository)
# if title or description are not provided and there is a single commit, we will
# use the its comment to populate the not-provided field(s).
# the first line of the commit comment is used for title and all lines of the
# comment are used for description.
title_from_commit = None
if title is None:
# if title wasn't specified and only one commit, we will set the PR title to the comment of that commit
description_from_commit = None
if (title is None) or (description is None):
commits = client.get_pull_request_commits(repository_id=repository, pull_request_id=pr.pull_request_id,
project=project)
if len(commits) == 1:
title_from_commit = commits[0].comment
commit_details = client.get_commit(commit_id=commits[0].commit_id, repository_id=repository,
project=project, change_count=0)
first_commit_comment = commit_details.comment

if (first_commit_comment):
# when title is not specified, use the first line of first commit comment as PR title
if title is None:
title_from_commit = first_commit_comment.split("\n")[0]
# when description is not specified, use the entire commit comment as PR description
if description is None:
description_from_commit = first_commit_comment
set_completion_options = (bypass_policy or
bypass_policy_reason is not None or
squash or
merge_commit_message is not None or
delete_source_branch or
transition_work_items)
if auto_complete or set_completion_options or title_from_commit is not None:
if auto_complete or set_completion_options or title_from_commit is not None or description_from_commit is not None:
pr_for_update = GitPullRequest()
if auto_complete:
# auto-complete will not get set on create, so a subsequent update is required.
Expand All @@ -216,6 +230,8 @@ def create_pull_request(project=None, repository=None, source_branch=None, targe
pr_for_update.completion_options = completion_options
if title_from_commit is not None:
pr_for_update.title = title_from_commit
if description_from_commit is not None:
pr_for_update.description = description_from_commit
pr = client.update_pull_request(git_pull_request_to_update=pr_for_update,
project=pr.repository.project.id,
repository_id=pr.repository.id,
Expand Down
60 changes: 50 additions & 10 deletions azure-devops/azext_devops/test/repos/test_pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# Attempt to load mock (works on Python version below 3.3)
from mock import patch

from azext_devops.devops_sdk.v5_0.git.models import GitPullRequest, GitRepository, TeamProjectReference
from azext_devops.devops_sdk.v5_0.git.models import GitCommit, GitCommitRef, GitPullRequest, GitRepository, TeamProjectReference
from azext_devops.devops_sdk.v5_0.git.git_client import GitClient
from azext_devops.dev.repos.pull_request import (create_pull_request,
show_pull_request,
Expand All @@ -28,7 +28,7 @@
list_pr_policies,
queue_pr_policy)
from azext_devops.dev.common.git import get_current_branch_name, resolve_git_ref_heads

from azext_devops.dev.common.services import clear_connection_cache
from azext_devops.test.utils.authentication import AuthenticatedTests
from azext_devops.test.utils.helper import get_client_mock_helper, TEST_DEVOPS_ORG_URL
Expand All @@ -43,6 +43,7 @@ class TestPullRequestMethods(AuthenticatedTests):
_TEST_SOURCE_BRANCH = 'sample_source_branch'
_TEST_TARGET_BRANCH = 'sample_target_branch'
_TEST_PR_TITLE = 'sample_pr_title'
_TEST_GIT_COMMIT_ID = 5
_TEST_PR_DESCRIPTION = 'sample_pr_description'

def setUp(self):
Expand All @@ -59,7 +60,8 @@ def setUp(self):
self.delete_PR_reviewers_patcher = patch('azext_devops.devops_sdk.v5_0.git.git_client.GitClient.delete_pull_request_reviewer')
self.get_PR_reviewers_patcher = patch('azext_devops.devops_sdk.v5_0.git.git_client.GitClient.get_pull_request_reviewers')
self.get_PR_WIs_patcher = patch('azext_devops.devops_sdk.v5_0.git.git_client.GitClient.get_pull_request_work_item_refs')

self.get_PR_commits_patcher = patch('azext_devops.devops_sdk.v5_0.git.git_client.GitClient.get_pull_request_commits')
self.get_commit_byId_patcher = patch('azext_devops.devops_sdk.v5_0.git.git_client.GitClient.get_commit')
# patch get client so no network call is made
self.get_client = patch('azext_devops.devops_sdk.connection.Connection.get_client', new=get_client_mock_helper)

Expand All @@ -86,6 +88,8 @@ def setUp(self):
self.mock_delete_PR_reviewer = self.delete_PR_reviewers_patcher.start()
self.mock_get_PR_reviewer = self.get_PR_reviewers_patcher.start()
self.mock_get_PR_WIs = self.get_PR_WIs_patcher.start()
self.mock_get_PR_commits = self.get_PR_commits_patcher.start()
self.mock_get_commit_byId = self.get_commit_byId_patcher.start()
self.mock_open_browser = self.open_in_browser_patcher.start()
self.mock_resolve_reviewers_as_refs = self.resolve_reviewers_as_refs_patcher.start()
self.mock_resolve_reviewers_as_ids = self.resolve_reviewers_as_ids.start()
Expand All @@ -96,7 +100,7 @@ def setUp(self):

# Setup mocks for clients
self.mock_get_client = self.get_client.start()

#clear connection cache before running each test
clear_connection_cache()

Expand Down Expand Up @@ -148,7 +152,7 @@ def test_create_pull_request_with_duplicate_reviwer(self):
source_branch = self._TEST_SOURCE_BRANCH,
target_branch = self._TEST_TARGET_BRANCH,
title = self._TEST_PR_TITLE,
description = self._TEST_PR_DESCRIPTION,
description = self._TEST_PR_DESCRIPTION,
reviewers = ['[email protected]','[email protected]'],
organization = self._TEST_DEVOPS_ORGANIZATION)

Expand Down Expand Up @@ -203,6 +207,42 @@ def test_create_pull_request_with_auto_complete(self):
update_object_from_update_call = self.mock_update_PR.call_args_list[0][1]['git_pull_request_to_update']
assert update_object_from_update_call.completion_options.merge_commit_message == merge_complete_message

def test_create_pull_request_comment_description_auto_populate(self):
test_pr_id = 1

#big setup because this object is passed around in create with auto complete flow
pr_to_return = GitPullRequest()
pr_to_return.pull_request_id = test_pr_id
pr_to_return.repository = GitRepository()
pr_to_return.repository.project = TeamProjectReference()
self.mock_create_PR.return_value = pr_to_return

pr_commits_to_return = [GitCommitRef(commit_id=self._TEST_GIT_COMMIT_ID)]
self.mock_get_PR_commits.return_value = pr_commits_to_return

commit_details_to_return = GitCommit(commit_id=self._TEST_GIT_COMMIT_ID, comment='comment line 1\ncomment line 2')
self.mock_get_commit_byId.return_value = commit_details_to_return

self.mock_resolve_identity.return_value = 'resolved identity'

# empty title and description so they are auto-populated from the commit
response = create_pull_request(project = self._TEST_PROJECT_NAME,
repository = self._TEST_REPOSITORY_NAME,
source_branch = self._TEST_SOURCE_BRANCH,
target_branch = self._TEST_TARGET_BRANCH,
organization = self._TEST_DEVOPS_ORGANIZATION,
auto_complete = True)

# assert
self.mock_create_PR.assert_called_once()
self.mock_update_PR.assert_called_once()

pr_id_from_udpate_call = self.mock_update_PR.call_args_list[0][1]['pull_request_id']
assert pr_id_from_udpate_call == test_pr_id
update_object_from_update_call = self.mock_update_PR.call_args_list[0][1]['git_pull_request_to_update']
assert update_object_from_update_call.title == 'comment line 1'
assert update_object_from_update_call.description == 'comment line 1\ncomment line 2'

def test_show_pull_request(self):
test_pr_id = 1
test_project_id = 20
Expand All @@ -224,9 +264,9 @@ def test_show_pull_request(self):
#assert
self.mock_get_PR_byId.assert_called_once_with(test_pr_id)
self.mock_get_PR.assert_called_once_with(project = test_project_id,
repository_id = test_repository_id,
pull_request_id = test_pr_id,
include_commits= False,
repository_id = test_repository_id,
pull_request_id = test_pr_id,
include_commits= False,
include_work_item_refs= True)

def test_list_pull_request(self):
Expand Down Expand Up @@ -360,7 +400,7 @@ def test_delete_pull_request_reviewers_multiple_users(self):
#assert
assert self.mock_delete_PR_reviewer.call_count == 3
self.mock_get_PR_reviewer.assert_called_once()

def test_list_pull_request_reviewers(self):
#setup
test_pr_id = 1
Expand Down Expand Up @@ -394,7 +434,7 @@ def test_list_pull_request_work_items(self):

def test_vote_pull_request(self):
response = vote_pull_request(id = 1,
vote = 'approve',
vote = 'approve',
organization = self._TEST_DEVOPS_ORGANIZATION)

#assert
Expand Down

0 comments on commit 4c3ef23

Please sign in to comment.