diff --git a/azure-devops/azext_devops/dev/repos/pull_request.py b/azure-devops/azext_devops/dev/repos/pull_request.py index e75549eb..048e4776 100644 --- a/azure-devops/azext_devops/dev/repos/pull_request.py +++ b/azure-devops/azext_devops/dev/repos/pull_request.py @@ -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. @@ -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, diff --git a/azure-devops/azext_devops/test/repos/test_pull_request.py b/azure-devops/azext_devops/test/repos/test_pull_request.py index a7ea6a2c..63599c49 100644 --- a/azure-devops/azext_devops/test/repos/test_pull_request.py +++ b/azure-devops/azext_devops/test/repos/test_pull_request.py @@ -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, @@ -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 @@ -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): @@ -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) @@ -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() @@ -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() @@ -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 = ['a@b.com','A@b.com'], organization = self._TEST_DEVOPS_ORGANIZATION) @@ -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 @@ -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): @@ -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 @@ -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