Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hook up new PR creation flow #15

Merged
merged 4 commits into from
Oct 1, 2024
Merged
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
12 changes: 10 additions & 2 deletions lazy_github/lib/github/branches.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -8,13 +9,20 @@ 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()]


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
18 changes: 18 additions & 0 deletions lazy_github/lib/github/pull_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ 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
response = await LazyGithubContext.client.post(url, headers=github_headers(), json=request_body)
response.raise_for_status()
return FullPullRequest(**response.json(), repo=repo)


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}"
Expand Down
8 changes: 7 additions & 1 deletion lazy_github/lib/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
137 changes: 117 additions & 20 deletions lazy_github/ui/screens/new_pull_request.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
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, FullPullRequest


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):
Expand All @@ -15,21 +35,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):
Expand Down Expand Up @@ -64,9 +128,8 @@ class NewPullRequestContainer(VerticalScroll):
margin-bottom: 1;
}

#pr_diff {
height: auto;
width: 100%;
#pr_title {
margin-bottom: 1;
}
"""

Expand All @@ -75,23 +138,53 @@ 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


class NewPullRequestModal(ModalScreen):
@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:
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[FullPullRequest | None]):
DEFAULT_CSS = """
NewPullRequestModal {
border: ascii green;
Expand All @@ -114,4 +207,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)
19 changes: 16 additions & 3 deletions lazy_github/ui/screens/primary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,7 +15,12 @@
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,
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
Expand All @@ -28,6 +34,7 @@
PrDiffTabPane,
PrOverviewTabPane,
PullRequestsContainer,
pull_request_to_cell,
)
from lazy_github.ui.widgets.repositories import ReposContainer

Expand Down Expand Up @@ -125,12 +132,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:
Expand Down
Loading