From 9a362a4133abd540699b96f53a1699b0350cebd2 Mon Sep 17 00:00:00 2001 From: gizmo385 Date: Sat, 28 Sep 2024 16:21:00 -0600 Subject: [PATCH 1/4] Fix incorrect keyword argument --- lazy_github/lib/github/branches.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lazy_github/lib/github/branches.py b/lazy_github/lib/github/branches.py index f19754a..b652b95 100644 --- a/lazy_github/lib/github/branches.py +++ b/lazy_github/lib/github/branches.py @@ -8,7 +8,7 @@ async def list_branches(repo: Repository, per_page: int = 30, page: int = 1) -> response = await LazyGithubContext.client.get( f"/repos/{repo.owner.login}/{repo.name}/branches", headers=github_headers(), - query_params=query_params, + params=query_params, ) response.raise_for_status() return [Branch(**branch) for branch in response.json()] From 8cbca2f89636a14b69fae77941bb818d053be5bb Mon Sep 17 00:00:00 2001 From: gizmo385 Date: Mon, 30 Sep 2024 22:20:58 -0600 Subject: [PATCH 2/4] WIP: hooking up create PR flow --- lazy_github/lib/github/branches.py | 10 +- lazy_github/lib/github/pull_requests.py | 21 ++++ lazy_github/lib/messages.py | 8 +- lazy_github/ui/screens/new_pull_request.py | 123 ++++++++++++++++++--- 4 files changed, 144 insertions(+), 18 deletions(-) diff --git a/lazy_github/lib/github/branches.py b/lazy_github/lib/github/branches.py index b652b95..0789475 100644 --- a/lazy_github/lib/github/branches.py +++ b/lazy_github/lib/github/branches.py @@ -1,3 +1,4 @@ +from lazy_github.lib.constants import DIFF_CONTENT_ACCEPT_TYPE from lazy_github.models.github import Branch, Repository from lazy_github.lib.context import LazyGithubContext, github_headers @@ -15,6 +16,13 @@ async def list_branches(repo: Repository, per_page: int = 30, page: int = 1) -> async def get_branch(repo: Repository, branch_name: str) -> Branch | None: - url = (f"/repos/{repo.owner.login}/{repo.name}/branches/{branch_name}",) + url = f"/repos/{repo.owner.login}/{repo.name}/branches/{branch_name}" response = await LazyGithubContext.client.get(url, headers=github_headers()) return Branch(**response.json()) + + +async def compare_branches(repo: Repository, base_branch: Branch, head_branch: Branch) -> str: + url = f"/repos/{repo.owner.login}/{repo.name}/compare/{base_branch.name}..{head_branch.name}" + response = await LazyGithubContext.client.get(url, headers=github_headers(accept=DIFF_CONTENT_ACCEPT_TYPE)) + response.raise_for_status() + return response.text diff --git a/lazy_github/lib/github/pull_requests.py b/lazy_github/lib/github/pull_requests.py index a2c3488..9b3157c 100644 --- a/lazy_github/lib/github/pull_requests.py +++ b/lazy_github/lib/github/pull_requests.py @@ -19,6 +19,27 @@ async def list_for_repo(repo: Repository) -> list[PartialPullRequest]: return [i for i in issues if isinstance(i, PartialPullRequest)] +async def create_pull_request( + repo: Repository, title: str, body: str, base_ref: str, head_ref: str, draft: bool = False +) -> FullPullRequest: + user = await LazyGithubContext.client.user() + url = f"/repos/{repo.owner.login}/{repo.name}/pulls" + request_body = { + "title": title, + "draft": draft, + "base": base_ref, + "head": f"{user.login}:{head_ref}", + } + if body: + request_body["body"] = body + from lazy_github.ui.widgets.command_log import log_event + + log_event(f"PR Body: {request_body}") + response = await LazyGithubContext.client.post(url, headers=github_headers(), json=request_body) + response.raise_for_status() + return FullPullRequest(**response.json()) + + async def get_full_pull_request(partial_pr: PartialPullRequest) -> FullPullRequest: """Converts a partial pull request into a full pull request""" url = f"/repos/{partial_pr.repo.owner.login}/{partial_pr.repo.name}/pulls/{partial_pr.number}" diff --git a/lazy_github/lib/messages.py b/lazy_github/lib/messages.py index 9b4429c..f35ebb9 100644 --- a/lazy_github/lib/messages.py +++ b/lazy_github/lib/messages.py @@ -2,7 +2,7 @@ from textual.message import Message -from lazy_github.models.github import Issue, PartialPullRequest, Repository +from lazy_github.models.github import FullPullRequest, Issue, PartialPullRequest, Repository class RepoSelected(Message): @@ -59,3 +59,9 @@ def issues(self) -> list[Issue]: for issue in self.issues_and_pull_requests if isinstance(issue, Issue) and not isinstance(issue, PartialPullRequest) ] + + +class PullRequestCreated(Message): + def __init__(self, pull_request: FullPullRequest) -> None: + super().__init__() + self.pull_request = pull_request diff --git a/lazy_github/ui/screens/new_pull_request.py b/lazy_github/ui/screens/new_pull_request.py index a3a26ba..d09591b 100644 --- a/lazy_github/ui/screens/new_pull_request.py +++ b/lazy_github/ui/screens/new_pull_request.py @@ -1,8 +1,29 @@ -from textual import on +from textual import on, suggester, validation, work from textual.app import ComposeResult +from textual.message import Message from textual.screen import ModalScreen from textual.containers import Horizontal, VerticalScroll -from textual.widgets import Button, Input, Label, Markdown, Rule, Select, TextArea +from textual.widgets import Button, Input, Label, Markdown, Rule, Switch, TextArea + +from lazy_github.lib.context import LazyGithubContext +from lazy_github.lib.github.branches import list_branches +from lazy_github.lib.github.pull_requests import create_pull_request +from lazy_github.lib.messages import PullRequestCreated +from lazy_github.models.github import Branch +from lazy_github.ui.widgets.command_log import log_event + + +class BranchesLoaded(Message): + def __init__(self, branches: list[Branch]) -> None: + super().__init__() + self.branches = branches + + +class BranchesSelected(Message): + def __init__(self, head_ref: str, base_ref: str) -> None: + super().__init__() + self.head_ref = head_ref + self.base_ref = base_ref class BranchSelection(Horizontal): @@ -15,21 +36,65 @@ class BranchSelection(Horizontal): Label { padding-top: 1; } + + Input { + width: 30%; + } """ + def __init__(self) -> None: + super().__init__() + self.branches: dict[str, Branch] = {} + def compose(self) -> ComposeResult: + assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo in new PR modal" + non_empty_validator = validation.Length(minimum=1) yield Label("[bold]Base[/bold]") - yield Select(id="base_ref", prompt="Choose a base ref", options=[("main", "main")]) + yield Input( + id="base_ref", + placeholder="Choose a base ref", + value=LazyGithubContext.current_repo.default_branch, + validators=[non_empty_validator], + ) yield Label(":left_arrow: [bold]Compare[/bold]") - yield Select(id="head_ref", prompt="Choose a head ref", options=[("main", "main")]) + yield Input(id="head_ref", placeholder="Choose a head ref", validators=[non_empty_validator]) + yield Label("Draft") + yield Switch(id="pr_is_draft", value=False) + + @property + def _head_ref_input(self) -> Input: + return self.query_one("#head_ref", Input) + + @property + def _base_ref_input(self) -> Input: + return self.query_one("#base_ref", Input) @property def head_ref(self) -> str: - return str(self.query_one("#head_ref", Select).value) + return self._head_ref_input.value @property def base_ref(self) -> str: - return str(self.query_one("#base_ref", Select).value) + return self._base_ref_input.value + + async def on_mount(self) -> None: + self.fetch_branches() + + @on(BranchesLoaded) + def handle_loaded_branches(self, message: BranchesLoaded) -> None: + self.branches = {b.name: b for b in message.branches} + branch_suggester = suggester.SuggestFromList(self.branches.keys()) + self._head_ref_input.suggester = branch_suggester + self._base_ref_input.suggester = branch_suggester + + @work + async def fetch_branches(self) -> None: + # This shouldn't happen since the current repo needs to be set to open this modal, but we'll validate it to + # make sure + assert LazyGithubContext.current_repo is not None, "Current repo unexpectedly missing in new PR modal" + + branches = await list_branches(LazyGithubContext.current_repo) + self.post_message(BranchesLoaded(branches)) class NewPullRequestButtons(Horizontal): @@ -64,9 +129,8 @@ class NewPullRequestContainer(VerticalScroll): margin-bottom: 1; } - #pr_diff { - height: auto; - width: 100%; + #pr_title { + margin-bottom: 1; } """ @@ -75,20 +139,47 @@ def compose(self) -> ComposeResult: yield BranchSelection() yield Rule() yield Label("[bold]Pull Request Title[/bold]") - yield Input(id="pr_title", placeholder="Title") + yield Input(id="pr_title", placeholder="Title", validators=[validation.Length(minimum=1)]) yield Label("[bold]Pull Request Description[/bold]") - yield TextArea(id="pr_description") + yield TextArea.code_editor(id="pr_description") yield NewPullRequestButtons() - yield Label("Changes:") - yield TextArea(id="pr_diff", disabled=True) @on(Button.Pressed, "#cancel_new_pr") def cancel_pull_request(self, _: Button.Pressed): self.app.pop_screen() - def fetch_branches(self) -> None: - # TODO: Fetch the branches from the remote repo that can be loaded - pass + @on(Button.Pressed, "#submit_new_pr") + async def submit_pull_request(self, _: Button.Pressed): + assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo in new PR modal" + title_field = self.query_one("#pr_title", Input) + title_field.validate(title_field.value) + description_field = self.query_one("#pr_description", TextArea) + head_ref_field = self.query_one("#head_ref", Input) + head_ref_field.validate(head_ref_field.value) + base_ref_field = self.query_one("#base_ref", Input) + base_ref_field.validate(base_ref_field.value) + draft_field = self.query_one("#pr_is_draft", Switch) + + if not (title_field.is_valid and head_ref_field.is_valid and base_ref_field.is_valid): + self.notify("Missing required fields!", title="Invalid PR!", severity="error") + return + + self.notify("Creating new pull request...") + try: + created_pr = await create_pull_request( + LazyGithubContext.current_repo, + title_field.value, + description_field.text, + base_ref_field.value, + head_ref_field.value, + draft=draft_field.value, + ) + except Exception as e: + self.notify("Error while creating new pull request!", title="Error creating pull request", severity="error") + log_event(f"Error creating pull request: {e}") + else: + self.notify("Successfully created PR!") + self.post_message(PullRequestCreated(created_pr)) class NewPullRequestModal(ModalScreen): From c61982532ca6bca133cee58825926797442e5b83 Mon Sep 17 00:00:00 2001 From: gizmo385 Date: Tue, 1 Oct 2024 01:53:05 -0600 Subject: [PATCH 3/4] Smooth out the experience of submitting a PR --- lazy_github/lib/github/pull_requests.py | 5 +---- lazy_github/ui/screens/new_pull_request.py | 19 +++++++++++++------ lazy_github/ui/screens/primary.py | 22 ++++++++++++++++++---- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/lazy_github/lib/github/pull_requests.py b/lazy_github/lib/github/pull_requests.py index 9b3157c..4651fdd 100644 --- a/lazy_github/lib/github/pull_requests.py +++ b/lazy_github/lib/github/pull_requests.py @@ -32,12 +32,9 @@ async def create_pull_request( } if body: request_body["body"] = body - from lazy_github.ui.widgets.command_log import log_event - - log_event(f"PR Body: {request_body}") response = await LazyGithubContext.client.post(url, headers=github_headers(), json=request_body) response.raise_for_status() - return FullPullRequest(**response.json()) + return FullPullRequest(**response.json(), repo=repo) async def get_full_pull_request(partial_pr: PartialPullRequest) -> FullPullRequest: diff --git a/lazy_github/ui/screens/new_pull_request.py b/lazy_github/ui/screens/new_pull_request.py index d09591b..dc8c49f 100644 --- a/lazy_github/ui/screens/new_pull_request.py +++ b/lazy_github/ui/screens/new_pull_request.py @@ -9,7 +9,7 @@ from lazy_github.lib.github.branches import list_branches from lazy_github.lib.github.pull_requests import create_pull_request from lazy_github.lib.messages import PullRequestCreated -from lazy_github.models.github import Branch +from lazy_github.models.github import Branch, FullPullRequest from lazy_github.ui.widgets.command_log import log_event @@ -174,15 +174,18 @@ async def submit_pull_request(self, _: Button.Pressed): head_ref_field.value, draft=draft_field.value, ) - except Exception as e: - self.notify("Error while creating new pull request!", title="Error creating pull request", severity="error") - log_event(f"Error creating pull request: {e}") + except Exception: + self.notify( + "Check that your branches are valid and that a PR does not already exist", + title="Error creating pull request", + severity="error", + ) else: self.notify("Successfully created PR!") self.post_message(PullRequestCreated(created_pr)) -class NewPullRequestModal(ModalScreen): +class NewPullRequestModal(ModalScreen[FullPullRequest | None]): DEFAULT_CSS = """ NewPullRequestModal { border: ascii green; @@ -205,4 +208,8 @@ def compose(self) -> ComposeResult: yield NewPullRequestContainer() def action_cancel(self) -> None: - self.dismiss() + self.dismiss(None) + + @on(PullRequestCreated) + def on_pull_request_created(self, message: PullRequestCreated) -> None: + self.dismiss(message.pull_request) diff --git a/lazy_github/ui/screens/primary.py b/lazy_github/ui/screens/primary.py index f8281d9..8d52be0 100644 --- a/lazy_github/ui/screens/primary.py +++ b/lazy_github/ui/screens/primary.py @@ -2,6 +2,7 @@ from typing import NamedTuple from httpx import HTTPStatusError +from textual import work from textual.app import ComposeResult from textual.command import Hit, Hits, Provider from textual.containers import Container @@ -14,12 +15,18 @@ from lazy_github.lib.context import LazyGithubContext from lazy_github.lib.github.issues import list_issues from lazy_github.lib.github.pull_requests import get_full_pull_request -from lazy_github.lib.messages import IssuesAndPullRequestsFetched, IssueSelected, PullRequestSelected, RepoSelected +from lazy_github.lib.messages import ( + IssuesAndPullRequestsFetched, + IssueSelected, + PullRequestCreated, + PullRequestSelected, + RepoSelected, +) from lazy_github.ui.screens.new_issue import NewIssueModal from lazy_github.ui.screens.new_pull_request import NewPullRequestModal from lazy_github.ui.screens.settings import SettingsModal from lazy_github.ui.widgets.actions import ActionsContainer -from lazy_github.ui.widgets.command_log import CommandLogSection +from lazy_github.ui.widgets.command_log import CommandLogSection, log_event from lazy_github.ui.widgets.common import LazyGithubContainer from lazy_github.ui.widgets.info import LazyGithubInfoTabPane from lazy_github.ui.widgets.issues import IssueConversationTabPane, IssueOverviewTabPane, IssuesContainer @@ -28,6 +35,7 @@ PrDiffTabPane, PrOverviewTabPane, PullRequestsContainer, + pull_request_to_cell, ) from lazy_github.ui.widgets.repositories import ReposContainer @@ -125,12 +133,18 @@ def action_open_issue(self) -> None: self.app.push_screen(NewIssueModal(LazyGithubContext.current_repo)) - def action_open_pull_request(self) -> None: + async def action_open_pull_request(self) -> None: + self.trigger_pr_creation_flow() + + @work + async def trigger_pr_creation_flow(self) -> None: if LazyGithubContext.current_repo is None: self.notify("Please select a repository first!", title="Cannot open new pull request", severity="error") return - self.app.push_screen(NewPullRequestModal()) + if new_pr := await self.app.push_screen_wait(NewPullRequestModal()): + self.pull_requests.searchable_table.append_rows([pull_request_to_cell(new_pr)]) + self.pull_requests.pull_requests[new_pr.number] = new_pr @property def pull_requests(self) -> PullRequestsContainer: From 0004a06a9dcdb4a445322558175056d00d0c65aa Mon Sep 17 00:00:00 2001 From: gizmo385 Date: Tue, 1 Oct 2024 01:54:50 -0600 Subject: [PATCH 4/4] Fix linter issues --- lazy_github/ui/screens/new_pull_request.py | 1 - lazy_github/ui/screens/primary.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lazy_github/ui/screens/new_pull_request.py b/lazy_github/ui/screens/new_pull_request.py index dc8c49f..f1613c8 100644 --- a/lazy_github/ui/screens/new_pull_request.py +++ b/lazy_github/ui/screens/new_pull_request.py @@ -10,7 +10,6 @@ from lazy_github.lib.github.pull_requests import create_pull_request from lazy_github.lib.messages import PullRequestCreated from lazy_github.models.github import Branch, FullPullRequest -from lazy_github.ui.widgets.command_log import log_event class BranchesLoaded(Message): diff --git a/lazy_github/ui/screens/primary.py b/lazy_github/ui/screens/primary.py index 8d52be0..be76edf 100644 --- a/lazy_github/ui/screens/primary.py +++ b/lazy_github/ui/screens/primary.py @@ -18,7 +18,6 @@ from lazy_github.lib.messages import ( IssuesAndPullRequestsFetched, IssueSelected, - PullRequestCreated, PullRequestSelected, RepoSelected, ) @@ -26,7 +25,7 @@ from lazy_github.ui.screens.new_pull_request import NewPullRequestModal from lazy_github.ui.screens.settings import SettingsModal from lazy_github.ui.widgets.actions import ActionsContainer -from lazy_github.ui.widgets.command_log import CommandLogSection, log_event +from lazy_github.ui.widgets.command_log import CommandLogSection from lazy_github.ui.widgets.common import LazyGithubContainer from lazy_github.ui.widgets.info import LazyGithubInfoTabPane from lazy_github.ui.widgets.issues import IssueConversationTabPane, IssueOverviewTabPane, IssuesContainer