Skip to content

Commit

Permalink
Merge pull request #25 from Nr18/feat/update-existing-pr
Browse files Browse the repository at this point in the history
feat: update the existing pull request
  • Loading branch information
Joris Conijn authored Feb 7, 2022
2 parents a03d02c + add3293 commit 797394e
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 12 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ If the merge is successful, it will:

From this point you are ready for the next change.

### Update existing pull request

When a pull requests exists a proposal is made to update the existing pull request.

## Testing locally

```bash
Expand Down
59 changes: 59 additions & 0 deletions pull_request_codecommit/aws/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,46 @@ def __execute(self, parameters: List[str]) -> str:

return response.stdout.decode("utf-8").strip("\n")

def get_open_pull_request(
self, repository: str, source: str, destination: str
) -> dict:
response = self.__execute(
[
"codecommit",
"list-pull-requests",
"--repository-name",
repository,
"--pull-request-status",
"OPEN",
]
)
data = json.loads(response)
open_pull_requests = data.get("pullRequestIds")

for pull_request_id in open_pull_requests:
pr = self.__get_pull_request(pull_request_id)
target = pr.get("pullRequestTargets", [{}])[0]

if (
target.get("sourceReference") == f"refs/heads/{source}"
and target.get("destinationReference") == f"refs/heads/{destination}"
):
return pr

return {}

def __get_pull_request(self, pull_request_id: int) -> dict:
response = self.__execute(
[
"codecommit",
"get-pull-request",
"--pull-request-id",
str(pull_request_id),
]
)
data = json.loads(response)
return data.get("pullRequest", {})

def create_pull_request(
self,
title: str,
Expand All @@ -60,6 +100,25 @@ def create_pull_request(

return data.get("pullRequest")

def update_pull_request(
self,
pull_request_id: int,
description: str,
) -> dict:
response = self.__execute(
[
"codecommit",
"update-pull-request-description",
"--pull-request-id",
str(pull_request_id),
"--description",
description,
]
)
data = json.loads(response)

return data.get("pullRequest")

def merge_pull_request(self, repository: str, pull_request_id: int) -> dict:
response = self.__execute(
[
Expand Down
35 changes: 31 additions & 4 deletions pull_request_codecommit/pull_request_codecommit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ class PullRequestCodeCommit:
def __init__(self, repository: Repository) -> None:
self.__aws_client: Optional[AwsClient] = None
self.__repo = repository
self.__data: dict = {}
self.__raw_data: dict = {}

@property
def __data(self) -> dict:
if not self.__raw_data:
self.__raw_data = self.__client.get_open_pull_request(
repository=self.__repo.remote.name,
source=self.__repo.branch,
destination=self.__repo.destination,
)

return self.__raw_data

@property
def __client(self) -> AwsClient:
Expand All @@ -27,6 +38,14 @@ def __client(self) -> AwsClient:
def pull_request_id(self) -> int:
return self.__data.get("pullRequestId", 0)

@property
def author(self) -> str:
return self.__data.get("authorArn", "").split("/")[-1]

@property
def title(self) -> str:
return self.__data.get("title", "")

@property
def description(self) -> str:
return self.__data.get("description", "").replace("\n\n", "\n")
Expand All @@ -36,7 +55,9 @@ def link(self) -> str:
return self.__repo.remote.pull_request_url(self.pull_request_id)

def save(self, title: str, description: str):
self.__data = self.__create(
self.__update(
description=self.__markdown_conversion(description)
) if self.pull_request_id != 0 else self.__create(
title=title, description=self.__markdown_conversion(description)
)

Expand All @@ -56,11 +77,17 @@ def merge(self) -> str:
def __markdown_conversion(description: str) -> str:
return description.replace("\n", "\n\n")

def __create(self, title: str, description: str) -> dict:
return self.__client.create_pull_request(
def __create(self, title: str, description: str) -> None:
self.__raw_data = self.__client.create_pull_request(
title=title,
description=description,
repository=self.__repo.remote.name,
source=self.__repo.branch,
destination=self.__repo.destination,
)

def __update(self, description: str) -> None:
self.__client.update_pull_request(
pull_request_id=self.pull_request_id,
description=description,
)
29 changes: 28 additions & 1 deletion pull_request_codecommit/pull_request_proposal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
from .git import Commits
from .pull_request_codecommit import PullRequestCodeCommit

SPLITTER = """
#=
#= Existing pull request found: #{{PULL_REQUEST_ID}}
#=
#= Author: {{AUTHOR}}
#=
#= Above you will see the title and description of the existing pull request.
#= Below you will find the newly proposed description.
#=
"""


class PullRequestProposal:
def __init__(self, pull_request: PullRequestCodeCommit, commits: Commits):
Expand All @@ -14,6 +25,9 @@ def title(self) -> str:
if self.__title:
return self.__title

if self.__pull_request.title:
return self.__pull_request.title

return self.__generate_title()

def __generate_title(self) -> str:
Expand All @@ -39,7 +53,10 @@ def description(self) -> str:
):
return self.append_issues(self.__commits.first.message.body)

return self.__generate_description()
return self.__merge_descriptions(
existing_description=self.__pull_request.description,
description=self.__generate_description(),
)

def append_issues(self, description: str) -> str:
if not self.__commits.issues:
Expand All @@ -55,6 +72,16 @@ def __generate_description(self):
"\n".join(list(map(lambda commit: commit.message.subject, self.__commits)))
)

def __merge_descriptions(self, existing_description: str, description: str) -> str:
if not existing_description:
return description

splitter = SPLITTER.replace(
"{{PULL_REQUEST_ID}}", str(self.__pull_request.pull_request_id)
)
splitter = splitter.replace("{{AUTHOR}}", self.__pull_request.author)
return existing_description + splitter + description

def update(self, title: str, description: str):
self.__title = title
self.__description = self.__remove_comments(description)
Expand Down
14 changes: 14 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,18 @@
b"[default]\nbranch: my-main\n[profile my-profile]\nbranch: my-master",
COMMITS,
),
(
"codecommit://my-repository-open-pr",
None,
None,
b"[default]\nbranch: my-main\n[profile my-profile]\nbranch: my-master",
COMMITS,
),
(
"codecommit://my-repository-other-open-pr",
None,
None,
b"[default]\nbranch: my-main\n[profile my-profile]\nbranch: my-master",
COMMITS,
),
]
62 changes: 57 additions & 5 deletions tests/test_command.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from io import TextIOWrapper, BytesIO
from typing import List, Tuple, Any, Optional
from typing import List, Tuple, Optional, Dict, Any
from unittest.mock import patch, MagicMock

import pytest
Expand All @@ -15,17 +15,63 @@ def edit_message(message: str) -> str:
return message


def aws_client_execute_side_effect(parameters, stdout):
def aws_client_execute_side_effect(parameters, stdout) -> MagicMock:
data: Dict[str, Any] = {}
assert -1 == stdout
mock_stdout = MagicMock()

if "create-pull-request" in parameters:
data = {"pullRequest": {"pullRequestId": 1}}
data = {"pullRequest": {"pullRequestId": "1"}}

elif "merge-pull-request-by-fast-forward" in parameters:
status = "OPEN" if parameters[-1] == "my-repository-pr-failure" else "CLOSED"
status = "OPEN" if "my-repository-pr-failure" in parameters else "CLOSED"
data = {"pullRequest": {"pullRequestStatus": status}}

elif "list-pull-requests" in parameters:
repository_name = next(
filter(lambda item: item.startswith("my-repository"), parameters)
)
pull_request_ids: List[int] = {
"my-repository-open-pr": [1],
"my-repository-other-open-pr": [2],
}.get(repository_name, [])

data = {"pullRequestIds": list(map(str, pull_request_ids))}

elif "get-pull-request" in parameters:
pull_request_id = int(parameters[parameters.index("--pull-request-id") + 1])

source = "feat/my-feature" if pull_request_id == 1 else "feat/my-other-feature"
destination = "my-main" if pull_request_id == 1 else "my-development"

data = {
"pullRequest": {
"pullRequestId": str(pull_request_id),
"authorArn": "arn:aws:sts::111122223333:assumed-role/Role/john.doe@acme.com",
"title": "feat: update existing pull request support",
"description": "Update the existing pull request when a pull request already exists",
"pullRequestTargets": [
{
"sourceCommit": "ca45e279EXAMPLE",
"sourceReference": f"refs/heads/{source}",
"mergeBase": "a99f5ddbEXAMPLE",
"destinationReference": f"refs/heads/{destination}",
"mergeMetadata": {"isMerged": "false"},
"destinationCommit": "2abfc6beEXAMPLE",
"repositoryName": "my-repository-open-pr",
}
],
}
}
elif "update-pull-request-description" in parameters:
pull_request_id = int(parameters[parameters.index("--pull-request-id") + 1])
data = {
"pullRequest": {
"pullRequestId": str(pull_request_id),
"authorArn": "arn:aws:sts::111122223333:assumed-role/Role/john.doe@acme.com",
}
}

mock_stdout.stdout = bytes(json.dumps(data), "utf-8")
return mock_stdout

Expand Down Expand Up @@ -67,7 +113,8 @@ def test_invoke(
mock_git_client.return_value.get_commit_messages.return_value = Commits(commits)

mock_git_client.return_value.remote.return_value = remote
mock_git_client.return_value.current_branch.return_value = "feat/my-feature"
mock_git_client.return_value.current_branch = "feat/my-feature"

configparser.open = MagicMock(return_value=TextIOWrapper(BytesIO(config))) # type: ignore
mock_aws_client.side_effect = aws_client_execute_side_effect

Expand Down Expand Up @@ -159,11 +206,13 @@ def test_invoke_github(mock_git_client: MagicMock) -> None:
assert result.exit_code == 1


@patch("pull_request_codecommit.aws.client.subprocess.run")
@patch("pull_request_codecommit.repository.GitClient")
@patch("pull_request_codecommit.click.edit")
def test_invoke_quit_edit(
mock_edit: MagicMock,
mock_git_client: MagicMock,
mock_aws_client: MagicMock,
) -> None:
mock_edit.return_value = None
mock_git_client.return_value.remote.return_value = (
Expand All @@ -172,9 +221,12 @@ def test_invoke_quit_edit(
mock_git_client.return_value.current_branch.return_value = "feat/my-feature"
config = b"[default]\nbranch: my-main\n[profile my-profile]\nbranch: my-master"
configparser.open = MagicMock(return_value=TextIOWrapper(BytesIO(config))) # type: ignore
mock_aws_client.side_effect = aws_client_execute_side_effect

runner = CliRunner()
result = runner.invoke(main)
assert type(result.exception) == SystemExit
assert "Pull request was not created" in result.output
assert result.exit_code == 1


Expand Down
35 changes: 33 additions & 2 deletions tests/test_pull_request.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

import pytest

Expand Down Expand Up @@ -65,12 +65,43 @@
),
],
)
@patch("pull_request_codecommit.pull_request_codecommit.AwsClient")
def test_git_client(
commits: Commits, expected_title: str, expected_description: str
mock_aws_client: MagicMock,
commits: Commits,
expected_title: str,
expected_description: str,
) -> None:
mock_aws_client.return_value.get_open_pull_request.return_value = {}

mock_repo = MagicMock()
mock_repo.commits.return_value = commits

pull_request = PullRequest(mock_repo)
assert pull_request.title == expected_title
assert pull_request.description == expected_description


@patch("pull_request_codecommit.pull_request_codecommit.AwsClient")
def test_update_pull_request(mock_aws_client: MagicMock) -> None:
mock_aws_client.return_value.get_open_pull_request.return_value = {
"pullRequestId": "1",
"title": "feat: test update pull request",
"authorArn": "arn:aws:sts::111122223333:assumed-role/Role/john.doe@acme.com",
"description": "my committed description",
}

mock_repo = MagicMock()
mock_repo.commits.return_value = Commits(COMMIT_DETAILED_MESSAGE_ISSUE)
mock_repo.remote.name = "my-repository"
mock_repo.remote.region = "eu-west-1"
pull_request = PullRequest(mock_repo)

assert pull_request.title == "feat: test update pull request"
assert "my committed description" in pull_request.description
assert "Existing pull request found: #1" in pull_request.description
assert "Author: john.doe@acme.com" in pull_request.description
assert "Issues: #1" in pull_request.description

pull_request.proposal.update(pull_request.title, pull_request.description)
pull_request.save()

0 comments on commit 797394e

Please sign in to comment.