lazy-github 0.6.0__tar.gz → 0.6.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lazy_github-0.6.0 → lazy_github-0.6.2}/PKG-INFO +1 -1
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/cli.py +15 -3
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/bindings.py +7 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/backends/cli.py +1 -1
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/issues.py +8 -0
- lazy_github-0.6.2/lazy_github/lib/github/reactions.py +41 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/messages.py +11 -3
- lazy_github-0.6.2/lazy_github/lib/pr_drafts.py +65 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/models/github.py +42 -1
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/create_or_edit_pull_request.py +91 -0
- lazy_github-0.6.2/lazy_github/ui/screens/lookup_issue.py +89 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/primary.py +17 -7
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/conversations.py +89 -10
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/issues.py +13 -1
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/pull_requests.py +109 -29
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/repositories.py +3 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/workflows.py +9 -2
- lazy_github-0.6.2/lazy_github/version.py +1 -0
- lazy_github-0.6.2/lint.sh +6 -0
- lazy_github-0.6.0/lazy_github/version.py +0 -1
- lazy_github-0.6.0/lint.sh +0 -6
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.devcontainer/devcontainer.json +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.github/workflows/code-checks.yaml +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.github/workflows/publish.yaml +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.gitignore +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.pre-commit-config.yaml +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.quicklinks/README.md +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.quicklinks/config.lua +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/.quicklinks/queries/python/patterns/textual_imports.scm +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/LICENSE +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/README.md +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/flake.lock +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/flake.nix +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/gh-lazy +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/images/lazy-github-conversation-ui.svg +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/images/lazy-github-settings-ui.png +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/__main__.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/cache.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/config.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/constants.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/context.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/debug.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/diff_parser.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/git_cli.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/auth.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/backends/hishel.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/backends/protocol.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/branches.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/checks.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/client.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/notifications.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/pull_requests.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/repositories.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/users.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/github/workflows.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/lib/logging.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/app.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/auth.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/confirm.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/debug.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/edit_issue.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/lookup_pull_request.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/lookup_repository.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/new_comment.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/new_issue.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/notifications.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/settings.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/trigger_workflow.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/command_log.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/common.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/diff_viewer.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/info.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/split_diff_viewer.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/widgets/workflow_run_details.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/pyproject.toml +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/start.sh +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/tests/__init__.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/tests/test_cache.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/tests/test_config.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/tests/test_settings_ui.py +0 -0
- {lazy_github-0.6.0 → lazy_github-0.6.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lazy-github
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: A terminal UI for interacting with Github
|
|
5
5
|
Author-email: "Chris (Gizmo)" <gizmo385@users.noreply.github.com>
|
|
6
6
|
Maintainer-email: "Chris (Gizmo)" <gizmo385@users.noreply.github.com>
|
|
@@ -55,10 +55,14 @@ def clear_auth():
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
@cli.command
|
|
58
|
-
|
|
58
|
+
@click.option("--no-confirm", is_flag=True, default=False, help="Don't ask for confirmation")
|
|
59
|
+
def clear_config(no_confirm: bool):
|
|
59
60
|
"""Reset the user's settings"""
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
if no_confirm or click.confirm("Confirm deletion your LazyGithub settings"):
|
|
62
|
+
_CONFIG_FILE_LOCATION.unlink(missing_ok=True)
|
|
63
|
+
print("Your settings have been cleared")
|
|
64
|
+
else:
|
|
65
|
+
print("Canceling")
|
|
62
66
|
|
|
63
67
|
|
|
64
68
|
@cli.command
|
|
@@ -80,3 +84,11 @@ def clear_cache(no_confirm: bool):
|
|
|
80
84
|
def debug():
|
|
81
85
|
"""Outputs LazyGithub debug info"""
|
|
82
86
|
print(collect_debug_info())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@cli.command
|
|
90
|
+
def version():
|
|
91
|
+
"""Prints the current version"""
|
|
92
|
+
from lazy_github.version import VERSION
|
|
93
|
+
|
|
94
|
+
print(f"LazyGithub version {VERSION}")
|
|
@@ -38,6 +38,13 @@ class LazyGithubBindings:
|
|
|
38
38
|
"I", "open_issue", "New Issue", id="issue.new", tooltip="Open a new issue in the current repository"
|
|
39
39
|
)
|
|
40
40
|
EDIT_ISSUE = Binding("E", "edit_issue", "Edit Issue", id="issue.edit", tooltip="Edit the selected issue")
|
|
41
|
+
LOOKUP_ISSUE = Binding(
|
|
42
|
+
"O",
|
|
43
|
+
"lookup_issue",
|
|
44
|
+
"Lookup Issue",
|
|
45
|
+
id="issue.lookup",
|
|
46
|
+
tooltip="Lookup an issue in the current repository",
|
|
47
|
+
)
|
|
41
48
|
|
|
42
49
|
# Pull request actions
|
|
43
50
|
OPEN_PULL_REQUEST = Binding(
|
|
@@ -101,7 +101,7 @@ def _create_request_body_tempfile(body: bytes) -> tempfile._TemporaryFileWrapper
|
|
|
101
101
|
_TEMPORARY_JSON_BODY_DIRECTORY.mkdir(parents=True, exist_ok=True)
|
|
102
102
|
if sys.version_info.minor > 11:
|
|
103
103
|
# delete_on_close parameter added in Python 3.12
|
|
104
|
-
temp = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False, dir=_TEMPORARY_JSON_BODY_DIRECTORY)
|
|
104
|
+
temp = tempfile.NamedTemporaryFile(delete=False, delete_on_close=False, dir=_TEMPORARY_JSON_BODY_DIRECTORY)
|
|
105
105
|
else:
|
|
106
106
|
temp = tempfile.NamedTemporaryFile(delete=False, dir=_TEMPORARY_JSON_BODY_DIRECTORY)
|
|
107
107
|
temp.write(body)
|
|
@@ -7,6 +7,14 @@ from lazy_github.models.github import Issue, IssueComment, PartialPullRequest, R
|
|
|
7
7
|
DEFAULT_PAGE_SIZE = 30
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
async def get_issue_by_number(repo: Repository, issue_number: int) -> Issue:
|
|
11
|
+
"""Looks up a single issue by number in the given repository"""
|
|
12
|
+
url = f"/repos/{repo.owner.login}/{repo.name}/issues/{issue_number}"
|
|
13
|
+
response = await LazyGithubContext.client.get(url, headers=github_headers())
|
|
14
|
+
response.raise_for_status()
|
|
15
|
+
return Issue(**response.json(), repo=repo)
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
class UpdateIssuePayload(TypedDict):
|
|
11
19
|
title: str | None
|
|
12
20
|
body: str | None
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from lazy_github.lib.context import LazyGithubContext, github_headers
|
|
4
|
+
from lazy_github.models.github import Issue, IssueComment, Reaction, ReactionSet, ReactionType, Repository, User
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _build_reaction_set(response: Any) -> ReactionSet:
|
|
8
|
+
response.raise_for_status()
|
|
9
|
+
reaction_users: dict[ReactionType, list[User]] = {}
|
|
10
|
+
reaction_counts: dict[ReactionType, int] = {}
|
|
11
|
+
|
|
12
|
+
for raw_reaction in response.json():
|
|
13
|
+
reaction_type = ReactionType.from_github(raw_reaction["content"])
|
|
14
|
+
reaction_users.setdefault(reaction_type, [])
|
|
15
|
+
reaction_counts.setdefault(reaction_type, 0)
|
|
16
|
+
|
|
17
|
+
user = User(**raw_reaction["user"])
|
|
18
|
+
reaction_users[reaction_type].append(user)
|
|
19
|
+
reaction_counts[reaction_type] += 1
|
|
20
|
+
|
|
21
|
+
return ReactionSet(reaction_users=reaction_users, reaction_counts=reaction_counts)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def list_reactions_on_comment(repo: Repository, comment: IssueComment) -> ReactionSet:
|
|
25
|
+
url = f"/repos/{repo.full_name}/issues/comments/{comment.id}/reactions"
|
|
26
|
+
response = await LazyGithubContext.client.get(url, headers=github_headers())
|
|
27
|
+
return _build_reaction_set(response)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def add_reaction_on_comment(repo: Repository, comment: IssueComment, reaction: ReactionType) -> Reaction:
|
|
31
|
+
url = f"/repos/{repo.full_name}/issues/comments/{comment.id}/reactions"
|
|
32
|
+
body = {"content": reaction.name.lower()}
|
|
33
|
+
response = await LazyGithubContext.client.post(url, headers=github_headers(), json=body)
|
|
34
|
+
response.raise_for_status()
|
|
35
|
+
return Reaction(**response.json())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def list_reactions_on_issue(repo: Repository, issue: Issue) -> ReactionSet:
|
|
39
|
+
url = f"/repos/{repo.full_name}/issues/{issue.number}/reactions"
|
|
40
|
+
response = await LazyGithubContext.client.get(url, headers=github_headers())
|
|
41
|
+
return _build_reaction_set(response)
|
|
@@ -9,18 +9,26 @@ from lazy_github.models.github import (
|
|
|
9
9
|
IssueComment,
|
|
10
10
|
Notification,
|
|
11
11
|
PartialPullRequest,
|
|
12
|
+
ReactionSet,
|
|
12
13
|
Repository,
|
|
13
14
|
Review,
|
|
14
15
|
WorkflowRun,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
class
|
|
19
|
-
"""A message sent when reviews for a pull request have been loaded"""
|
|
19
|
+
class ReviewsAndCommentsLoaded(Message):
|
|
20
|
+
"""A message sent when reviews and comments for a pull request have been loaded"""
|
|
20
21
|
|
|
21
|
-
def __init__(self, reviews: list[Review]) -> None:
|
|
22
|
+
def __init__(self, reviews: list[Review], comments: list[IssueComment]) -> None:
|
|
22
23
|
super().__init__()
|
|
23
24
|
self.reviews = reviews
|
|
25
|
+
self.comments = comments
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CommentReactionsLoaded(Message):
|
|
29
|
+
def __init__(self, reactions: dict[int, ReactionSet]) -> None:
|
|
30
|
+
super().__init__()
|
|
31
|
+
self.reactions = reactions
|
|
24
32
|
|
|
25
33
|
|
|
26
34
|
class RepoSelected(Message):
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict, dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
6
|
+
from lazy_github.lib.logging import lg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class PullRequestDraft:
|
|
11
|
+
repo_full_name: str
|
|
12
|
+
title: str
|
|
13
|
+
description: str
|
|
14
|
+
base_ref: str
|
|
15
|
+
head_ref: str
|
|
16
|
+
is_draft: bool
|
|
17
|
+
reviewers: list[str]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_draft_path(repo_full_name: str) -> Path:
|
|
21
|
+
"""Returns the path to the draft file for a given repository."""
|
|
22
|
+
safe_name = repo_full_name.replace("/", "_")
|
|
23
|
+
return LazyGithubContext.config.cache.cache_directory / "pr_drafts" / f"{safe_name}.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_pr_draft(repo_full_name: str) -> PullRequestDraft | None:
|
|
27
|
+
"""Load a PR draft for the given repository, returning None if not found or corrupt."""
|
|
28
|
+
draft_path = get_draft_path(repo_full_name)
|
|
29
|
+
if not draft_path.exists():
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
data = json.loads(draft_path.read_text())
|
|
34
|
+
return PullRequestDraft(**data)
|
|
35
|
+
except json.JSONDecodeError as e:
|
|
36
|
+
lg.warning(f"Failed to parse PR draft file '{draft_path}' as valid JSON: {e}. Ignoring draft.")
|
|
37
|
+
return None
|
|
38
|
+
except (TypeError, KeyError) as e:
|
|
39
|
+
lg.warning(f"PR draft file '{draft_path}' has invalid structure: {e}. Ignoring draft.")
|
|
40
|
+
return None
|
|
41
|
+
except Exception as e:
|
|
42
|
+
lg.warning(f"Unexpected error loading PR draft from '{draft_path}': {e}. Ignoring draft.")
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def save_pr_draft(draft: PullRequestDraft) -> None:
|
|
47
|
+
"""Save a PR draft to disk."""
|
|
48
|
+
draft_path = get_draft_path(draft.repo_full_name)
|
|
49
|
+
try:
|
|
50
|
+
draft_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
draft_path.write_text(json.dumps(asdict(draft), indent=2))
|
|
52
|
+
lg.debug(f"Saved PR draft to {draft_path}")
|
|
53
|
+
except Exception as e:
|
|
54
|
+
lg.warning(f"Failed to save PR draft to '{draft_path}': {e}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def clear_pr_draft(repo_full_name: str) -> None:
|
|
58
|
+
"""Delete the draft file for the given repository."""
|
|
59
|
+
draft_path = get_draft_path(repo_full_name)
|
|
60
|
+
try:
|
|
61
|
+
if draft_path.exists():
|
|
62
|
+
draft_path.unlink()
|
|
63
|
+
lg.debug(f"Cleared PR draft at {draft_path}")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
lg.warning(f"Failed to clear PR draft at '{draft_path}': {e}")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from enum import StrEnum
|
|
2
|
+
from enum import Enum, StrEnum
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
5
|
|
|
@@ -302,3 +302,44 @@ class CheckStatus(BaseModel):
|
|
|
302
302
|
class CombinedCheckStatus(BaseModel):
|
|
303
303
|
state: CheckStatusState
|
|
304
304
|
statuses: list[CheckStatus]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class ReactionType(Enum):
|
|
308
|
+
def __init__(self, value: str, emoji: str) -> None:
|
|
309
|
+
super().__init__()
|
|
310
|
+
self._value = value
|
|
311
|
+
self.emoji = emoji
|
|
312
|
+
|
|
313
|
+
@classmethod
|
|
314
|
+
def from_github(cls, github_content: str) -> "ReactionType":
|
|
315
|
+
try:
|
|
316
|
+
return cls[github_content.upper()]
|
|
317
|
+
except KeyError:
|
|
318
|
+
if github_content == "+1":
|
|
319
|
+
return ReactionType.THUMBS_UP
|
|
320
|
+
elif github_content == "-1":
|
|
321
|
+
return ReactionType.THUMBS_DOWN
|
|
322
|
+
else:
|
|
323
|
+
raise ValueError(f"Invalid reaction string: {github_content}")
|
|
324
|
+
|
|
325
|
+
THUMBS_UP = ("+1", "👍")
|
|
326
|
+
THUMBS_DOWN = ("-1", "👎")
|
|
327
|
+
LAUGH = ("laugh", "😄")
|
|
328
|
+
CONFUSED = ("confused", "😕")
|
|
329
|
+
HEART = ("heart", "❤️")
|
|
330
|
+
HOORAY = ("hooray", "🎉")
|
|
331
|
+
ROCKET = ("rocket", "🚀")
|
|
332
|
+
EYES = ("eyes", "👀")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class Reaction(BaseModel):
|
|
336
|
+
reaction_type: ReactionType
|
|
337
|
+
user: User
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class ReactionSet(BaseModel):
|
|
341
|
+
reaction_users: dict[ReactionType, list[User]]
|
|
342
|
+
reaction_counts: dict[ReactionType, int]
|
|
343
|
+
|
|
344
|
+
def __bool__(self) -> bool:
|
|
345
|
+
return bool(self.reaction_counts) or bool(self.reaction_users)
|
{lazy_github-0.6.0 → lazy_github-0.6.2}/lazy_github/ui/screens/create_or_edit_pull_request.py
RENAMED
|
@@ -5,6 +5,7 @@ from textual.app import ComposeResult
|
|
|
5
5
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
6
6
|
from textual.content import Content
|
|
7
7
|
from textual.screen import ModalScreen
|
|
8
|
+
from textual.timer import Timer
|
|
8
9
|
from textual.types import DuplicateID
|
|
9
10
|
from textual.widgets import Button, Input, Label, Markdown, Rule, SelectionList, Switch, TextArea
|
|
10
11
|
from textual.widgets.selection_list import Selection
|
|
@@ -27,6 +28,7 @@ from lazy_github.lib.github.repositories import get_collaborators
|
|
|
27
28
|
from lazy_github.lib.github.users import get_user_by_username
|
|
28
29
|
from lazy_github.lib.logging import lg
|
|
29
30
|
from lazy_github.lib.messages import BranchesLoaded, PullRequestCreatedOrUpdated
|
|
31
|
+
from lazy_github.lib.pr_drafts import PullRequestDraft, clear_pr_draft, load_pr_draft, save_pr_draft
|
|
30
32
|
from lazy_github.models.github import Branch, FullPullRequest, PartialPullRequest
|
|
31
33
|
from lazy_github.ui.screens.confirm import ConfirmDialog
|
|
32
34
|
|
|
@@ -289,6 +291,7 @@ class CreateOrEditPullRequestContainer(VerticalScroll):
|
|
|
289
291
|
"[yellow]Warning:[/yellow] Current local branch has no configured upstream", id="branch_missing"
|
|
290
292
|
)
|
|
291
293
|
self.branch_missing_label.display = False
|
|
294
|
+
self._draft_save_timer: Timer | None = None
|
|
292
295
|
|
|
293
296
|
def compose(self) -> ComposeResult:
|
|
294
297
|
pr_template = LazyGithubContext.config.pull_requests.pull_request_template
|
|
@@ -335,12 +338,96 @@ class CreateOrEditPullRequestContainer(VerticalScroll):
|
|
|
335
338
|
if not does_branch_have_configured_upstream(LazyGithubContext.current_directory_branch):
|
|
336
339
|
self.branch_missing_label.display = True
|
|
337
340
|
|
|
341
|
+
def _save_draft_state(self) -> None:
|
|
342
|
+
"""Save the current form state as a draft, or clear if empty."""
|
|
343
|
+
if self.existing_pull_request or not LazyGithubContext.current_repo:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
title = self.query_one("#pr_title", Input).value.strip()
|
|
347
|
+
description = self.query_one("#pr_description", TextArea).text.strip()
|
|
348
|
+
base_ref = self.query_one("#base_ref", Input).value
|
|
349
|
+
head_ref = self.query_one("#head_ref", Input).value
|
|
350
|
+
is_draft = self.query_one("#pr_is_draft", Switch).value
|
|
351
|
+
reviewers = list(self.query_one(ReviewerSelectionContainer).reviewers)
|
|
352
|
+
|
|
353
|
+
# If there's no meaningful content, clear any existing draft
|
|
354
|
+
if not title and not description and not reviewers:
|
|
355
|
+
clear_pr_draft(LazyGithubContext.current_repo.full_name)
|
|
356
|
+
return
|
|
357
|
+
|
|
358
|
+
draft = PullRequestDraft(
|
|
359
|
+
repo_full_name=LazyGithubContext.current_repo.full_name,
|
|
360
|
+
title=title,
|
|
361
|
+
description=description,
|
|
362
|
+
base_ref=base_ref,
|
|
363
|
+
head_ref=head_ref,
|
|
364
|
+
is_draft=is_draft,
|
|
365
|
+
reviewers=reviewers,
|
|
366
|
+
)
|
|
367
|
+
save_pr_draft(draft)
|
|
368
|
+
|
|
369
|
+
def _restore_draft_state(self) -> None:
|
|
370
|
+
"""Restore form state from a saved draft if one exists."""
|
|
371
|
+
if self.existing_pull_request or not LazyGithubContext.current_repo:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
draft = load_pr_draft(LazyGithubContext.current_repo.full_name)
|
|
375
|
+
if not draft:
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# Check if draft has meaningful content
|
|
379
|
+
has_content = draft.title.strip() or draft.description.strip() or draft.reviewers
|
|
380
|
+
|
|
381
|
+
self.query_one("#pr_title", Input).value = draft.title
|
|
382
|
+
self.query_one("#pr_description", TextArea).text = draft.description
|
|
383
|
+
self.query_one("#base_ref", Input).value = draft.base_ref
|
|
384
|
+
self.query_one("#head_ref", Input).value = draft.head_ref
|
|
385
|
+
self.query_one("#pr_is_draft", Switch).value = draft.is_draft
|
|
386
|
+
|
|
387
|
+
reviewer_container = self.query_one(ReviewerSelectionContainer)
|
|
388
|
+
for reviewer in draft.reviewers:
|
|
389
|
+
reviewer_container.reviewers.add(reviewer)
|
|
390
|
+
try:
|
|
391
|
+
reviewer_container.reviewers_selection_list.add_option(
|
|
392
|
+
Selection(reviewer, reviewer, id=reviewer, initial_state=True)
|
|
393
|
+
)
|
|
394
|
+
except DuplicateID:
|
|
395
|
+
pass
|
|
396
|
+
if draft.reviewers:
|
|
397
|
+
reviewer_container.current_reviewers_label.display = True
|
|
398
|
+
reviewer_container.reviewers_selection_list.display = True
|
|
399
|
+
|
|
400
|
+
if has_content:
|
|
401
|
+
self.notify("Restored saved PR draft")
|
|
402
|
+
|
|
338
403
|
async def on_mount(self) -> None:
|
|
339
404
|
self.query_one("#pr_title", Input).focus()
|
|
340
405
|
self.ensure_directory_branch_has_configured_upstream()
|
|
406
|
+
if not self.existing_pull_request:
|
|
407
|
+
self._restore_draft_state()
|
|
408
|
+
|
|
409
|
+
def _schedule_draft_save(self) -> None:
|
|
410
|
+
"""Schedule a debounced draft save after 2 seconds of inactivity."""
|
|
411
|
+
if self.existing_pull_request:
|
|
412
|
+
return
|
|
413
|
+
if self._draft_save_timer is not None:
|
|
414
|
+
self._draft_save_timer.stop()
|
|
415
|
+
self._draft_save_timer = self.set_timer(2, self._save_draft_state)
|
|
416
|
+
|
|
417
|
+
@on(Input.Changed, "#pr_title")
|
|
418
|
+
@on(Input.Changed, "#base_ref")
|
|
419
|
+
@on(Input.Changed, "#head_ref")
|
|
420
|
+
def _on_input_changed(self, _: Input.Changed) -> None:
|
|
421
|
+
self._schedule_draft_save()
|
|
422
|
+
|
|
423
|
+
@on(TextArea.Changed, "#pr_description")
|
|
424
|
+
def _on_description_changed(self, _: TextArea.Changed) -> None:
|
|
425
|
+
self._schedule_draft_save()
|
|
341
426
|
|
|
342
427
|
@on(Button.Pressed, "#cancel_new_pr")
|
|
343
428
|
def cancel_pull_request(self, _: Button.Pressed):
|
|
429
|
+
if not self.existing_pull_request:
|
|
430
|
+
self._save_draft_state()
|
|
344
431
|
self.app.pop_screen()
|
|
345
432
|
|
|
346
433
|
async def _edit_pr(self) -> None:
|
|
@@ -418,6 +505,7 @@ class CreateOrEditPullRequestContainer(VerticalScroll):
|
|
|
418
505
|
lg.info(f"Requesting PR reviews from: {', '.join(reviewers)}")
|
|
419
506
|
await request_reviews(created_pr, reviewers)
|
|
420
507
|
|
|
508
|
+
clear_pr_draft(LazyGithubContext.current_repo.full_name)
|
|
421
509
|
self.notify("Successfully created PR!")
|
|
422
510
|
self.post_message(PullRequestCreatedOrUpdated(created_pr))
|
|
423
511
|
|
|
@@ -461,6 +549,9 @@ class CreateOrEditPullRequestModal(ModalScreen[FullPullRequest | None]):
|
|
|
461
549
|
yield CreateOrEditPullRequestContainer(self.existing_pull_request)
|
|
462
550
|
|
|
463
551
|
def action_close(self) -> None:
|
|
552
|
+
if not self.existing_pull_request:
|
|
553
|
+
container = self.query_one(CreateOrEditPullRequestContainer)
|
|
554
|
+
container._save_draft_state()
|
|
464
555
|
self.dismiss(None)
|
|
465
556
|
|
|
466
557
|
@on(PullRequestCreatedOrUpdated)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from textual import on
|
|
2
|
+
from textual.app import ComposeResult
|
|
3
|
+
from textual.containers import Container, Horizontal
|
|
4
|
+
from textual.content import Content
|
|
5
|
+
from textual.screen import ModalScreen
|
|
6
|
+
from textual.widgets import Button, Input, Label, Markdown, Rule
|
|
7
|
+
|
|
8
|
+
from lazy_github.lib.bindings import LazyGithubBindings
|
|
9
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
10
|
+
from lazy_github.lib.github.backends.protocol import GithubApiRequestFailed
|
|
11
|
+
from lazy_github.lib.github.issues import get_issue_by_number
|
|
12
|
+
from lazy_github.models.github import Issue
|
|
13
|
+
from lazy_github.ui.widgets.common import LazyGithubFooter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LookupIssueButtons(Horizontal):
|
|
17
|
+
DEFAULT_CSS = """
|
|
18
|
+
LookupIssueButtons {
|
|
19
|
+
align: center middle;
|
|
20
|
+
height: auto;
|
|
21
|
+
width: 100%;
|
|
22
|
+
}
|
|
23
|
+
Button {
|
|
24
|
+
margin: 1;
|
|
25
|
+
}
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def compose(self) -> ComposeResult:
|
|
29
|
+
yield Button("Open", id="lookup", variant="success")
|
|
30
|
+
yield Button("Cancel", id="cancel", variant="error")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LookupIssueContainer(Container):
|
|
34
|
+
DEFAULT_CSS = """
|
|
35
|
+
LookupIssueContainer {
|
|
36
|
+
align: center middle;
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def compose(self) -> ComposeResult:
|
|
41
|
+
yield Markdown("# Search for an issue by number:")
|
|
42
|
+
yield Label(Content.from_markup("[bold]Issue Number:[/bold]"))
|
|
43
|
+
yield Input(
|
|
44
|
+
id="issue_number",
|
|
45
|
+
placeholder="Issue number",
|
|
46
|
+
type="number",
|
|
47
|
+
)
|
|
48
|
+
yield Rule()
|
|
49
|
+
yield LookupIssueButtons()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LookupIssueModal(ModalScreen[Issue | None]):
|
|
53
|
+
DEFAULT_CSS = """
|
|
54
|
+
LookupIssueModal {
|
|
55
|
+
align: center middle;
|
|
56
|
+
content-align: center middle;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
LookupIssueContainer {
|
|
60
|
+
width: 60;
|
|
61
|
+
max-height: 25;
|
|
62
|
+
border: thick $background 80%;
|
|
63
|
+
background: $surface-lighten-3;
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
BINDINGS = [LazyGithubBindings.SUBMIT_DIALOG, LazyGithubBindings.CLOSE_DIALOG]
|
|
68
|
+
|
|
69
|
+
def compose(self) -> ComposeResult:
|
|
70
|
+
yield LookupIssueContainer()
|
|
71
|
+
yield LazyGithubFooter()
|
|
72
|
+
|
|
73
|
+
@on(Button.Pressed, "#lookup")
|
|
74
|
+
async def action_submit(self) -> None:
|
|
75
|
+
assert LazyGithubContext.current_repo is not None, "Current repo is missing!"
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
issue_number = int(self.query_one("#issue_number", Input).value)
|
|
79
|
+
issue = await get_issue_by_number(LazyGithubContext.current_repo, issue_number)
|
|
80
|
+
except ValueError:
|
|
81
|
+
self.notify("Must enter a valid issue number!", title="Invalid Issue Number", severity="error")
|
|
82
|
+
except GithubApiRequestFailed:
|
|
83
|
+
self.notify("Could not find issue!", title="Unknown Issue", severity="error")
|
|
84
|
+
else:
|
|
85
|
+
self.dismiss(issue)
|
|
86
|
+
|
|
87
|
+
@on(Button.Pressed, "#cancel")
|
|
88
|
+
async def action_close(self) -> None:
|
|
89
|
+
self.dismiss(None)
|
|
@@ -30,7 +30,7 @@ from lazy_github.lib.messages import (
|
|
|
30
30
|
IssueSelected,
|
|
31
31
|
PullRequestSelected,
|
|
32
32
|
RepoSelected,
|
|
33
|
-
|
|
33
|
+
ReviewsAndCommentsLoaded,
|
|
34
34
|
WorkflowRunSelected,
|
|
35
35
|
)
|
|
36
36
|
from lazy_github.models.github import Issue, PartialPullRequest, Repository, WorkflowRun
|
|
@@ -333,11 +333,11 @@ class MainViewPane(Container):
|
|
|
333
333
|
if focus_run_details:
|
|
334
334
|
tabbed_content.children[0].focus()
|
|
335
335
|
|
|
336
|
-
@on(
|
|
337
|
-
async def handle_reviews_loaded(self, message:
|
|
336
|
+
@on(ReviewsAndCommentsLoaded)
|
|
337
|
+
async def handle_reviews_loaded(self, message: ReviewsAndCommentsLoaded) -> None:
|
|
338
338
|
try:
|
|
339
339
|
overview_pane = self.query_one(PrOverviewTabPane)
|
|
340
|
-
except WrongType
|
|
340
|
+
except (WrongType, NoMatches):
|
|
341
341
|
pass
|
|
342
342
|
else:
|
|
343
343
|
overview_pane.add_reviews(message.reviews)
|
|
@@ -480,9 +480,19 @@ class LazyGithubMainScreen(Screen):
|
|
|
480
480
|
|
|
481
481
|
async def action_toggle_ui(self, ui_to_hide: str):
|
|
482
482
|
widget = self.query_one(f"#{ui_to_hide}", Widget)
|
|
483
|
-
new_value = not widget.
|
|
484
|
-
widget.display =
|
|
485
|
-
widget.visible =
|
|
483
|
+
new_value = not widget.display
|
|
484
|
+
widget.display = new_value
|
|
485
|
+
widget.visible = new_value
|
|
486
|
+
|
|
487
|
+
# If showing a section and we have a repo loaded, refresh its data
|
|
488
|
+
if new_value and LazyGithubContext.current_repo:
|
|
489
|
+
repo = LazyGithubContext.current_repo
|
|
490
|
+
selections = self.main_view_pane.selections
|
|
491
|
+
if ui_to_hide == "workflows":
|
|
492
|
+
selections.workflows.initialize_tables_from_cache(repo)
|
|
493
|
+
selections.workflows.load_repo(repo)
|
|
494
|
+
elif ui_to_hide in ("issues", "pull_requests"):
|
|
495
|
+
selections.fetch_issues_and_pull_requests(repo)
|
|
486
496
|
|
|
487
497
|
async def action_show_settings_modal(self) -> None:
|
|
488
498
|
self.app.push_screen(SettingsModal())
|