lazy-github 0.1.7__tar.gz → 0.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.1.7 → lazy_github-0.2}/PKG-INFO +2 -2
- {lazy_github-0.1.7 → lazy_github-0.2}/README.md +1 -1
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/cli.py +18 -1
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/config.py +21 -5
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/git_cli.py +5 -5
- lazy_github-0.2/lazy_github/lib/github/branches.py +28 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/issues.py +9 -3
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/pull_requests.py +18 -0
- lazy_github-0.2/lazy_github/lib/github/workflows.py +41 -0
- lazy_github-0.2/lazy_github/lib/logging.py +27 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/messages.py +25 -1
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/models/github.py +47 -2
- lazy_github-0.2/lazy_github/ui/app.py +54 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/new_comment.py +14 -9
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/new_issue.py +19 -14
- lazy_github-0.2/lazy_github/ui/screens/new_pull_request.py +214 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/primary.py +84 -27
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/settings.py +24 -6
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/command_log.py +14 -10
- lazy_github-0.2/lazy_github/ui/widgets/common.py +187 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/conversations.py +9 -1
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/issues.py +58 -24
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/pull_requests.py +42 -14
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/repositories.py +23 -15
- lazy_github-0.2/lazy_github/ui/widgets/workflows.py +137 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/PKG-INFO +2 -2
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/SOURCES.txt +6 -2
- {lazy_github-0.1.7 → lazy_github-0.2}/pyproject.toml +1 -1
- lazy_github-0.1.7/lazy_github/ui/app.py +0 -26
- lazy_github-0.1.7/lazy_github/ui/widgets/actions.py +0 -14
- lazy_github-0.1.7/lazy_github/ui/widgets/common.py +0 -110
- {lazy_github-0.1.7 → lazy_github-0.2}/LICENSE +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/__main__.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/constants.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/context.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/auth.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/client.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/github/repositories.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/lib/utils.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/auth.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/screens/edit_issue.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github/ui/widgets/info.py +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/dependency_links.txt +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/entry_points.txt +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/requires.txt +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/lazy_github.egg-info/top_level.txt +0 -0
- {lazy_github-0.1.7 → lazy_github-0.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: lazy-github
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2
|
|
4
4
|
Summary: A terminal UI for interacting with Github
|
|
5
5
|
Author-email: Chris Chapline <gizmo385@users.noreply.github.com>
|
|
6
6
|
Maintainer-email: Chris Chapline <gizmo385@users.noreply.github.com>
|
|
@@ -47,7 +47,7 @@ inspiration from the [lazygit](https://github.com/jesseduffield/lazygit) project
|
|
|
47
47
|
[Textual](https://textual.textualize.io/) to drive the terminal UI interactions.
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-

|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
Currently, it supports the following:
|
|
@@ -5,7 +5,7 @@ inspiration from the [lazygit](https://github.com/jesseduffield/lazygit) project
|
|
|
5
5
|
[Textual](https://textual.textualize.io/) to drive the terminal UI interactions.
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
Currently, it supports the following:
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import click
|
|
2
|
+
import rich
|
|
3
|
+
import shutil
|
|
2
4
|
|
|
3
5
|
from lazy_github.lib.config import _CONFIG_FILE_LOCATION, Config
|
|
6
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
4
7
|
from lazy_github.ui.app import app
|
|
5
8
|
|
|
6
9
|
|
|
7
10
|
@click.group(invoke_without_command=True)
|
|
8
11
|
@click.pass_context
|
|
9
12
|
def cli(ctx: click.Context) -> None:
|
|
13
|
+
"""A Terminal UI for interacting with Github"""
|
|
10
14
|
if ctx.invoked_subcommand is None:
|
|
11
15
|
run()
|
|
12
16
|
|
|
@@ -21,7 +25,7 @@ def run():
|
|
|
21
25
|
def dump_config():
|
|
22
26
|
"""Dump the current configuration, as it would be loaded by LazyGithub"""
|
|
23
27
|
print(f"Config file location: {_CONFIG_FILE_LOCATION} (exists => {_CONFIG_FILE_LOCATION.exists()})")
|
|
24
|
-
|
|
28
|
+
rich.print_json(Config.load_config().model_dump_json())
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
@cli.command
|
|
@@ -37,3 +41,16 @@ def clear_config():
|
|
|
37
41
|
"""Reset the user's settings"""
|
|
38
42
|
_CONFIG_FILE_LOCATION.unlink(missing_ok=True)
|
|
39
43
|
print("Your settings have been cleared")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@cli.command
|
|
47
|
+
@click.option("--no-confirm", is_flag=True, default=False, help="Don't ask for confirmation")
|
|
48
|
+
def clear_cache(no_confirm: bool):
|
|
49
|
+
"""Reset the lazy-github cache"""
|
|
50
|
+
cache_directory = LazyGithubContext.config.cache.cache_directory
|
|
51
|
+
if no_confirm or click.confirm(f"Confirm deletion of everything in {cache_directory}"):
|
|
52
|
+
if cache_directory.exists():
|
|
53
|
+
shutil.rmtree(cache_directory)
|
|
54
|
+
print("Cache cleared")
|
|
55
|
+
else:
|
|
56
|
+
print("Canceling cache deletion")
|
|
@@ -2,9 +2,10 @@ import json
|
|
|
2
2
|
from contextlib import contextmanager
|
|
3
3
|
from datetime import timedelta
|
|
4
4
|
from pathlib import Path
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Any, Generator, Literal, Optional
|
|
6
|
+
from textual.theme import BUILTIN_THEMES, Theme
|
|
6
7
|
|
|
7
|
-
from pydantic import BaseModel
|
|
8
|
+
from pydantic import BaseModel, field_serializer, field_validator
|
|
8
9
|
|
|
9
10
|
from lazy_github.lib.constants import CONFIG_FOLDER, IssueOwnerFilter, IssueStateFilter
|
|
10
11
|
|
|
@@ -33,7 +34,7 @@ class IssueSettings(BaseModel):
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class RepositorySettings(BaseModel):
|
|
36
|
-
favorites:
|
|
37
|
+
favorites: list[str] = []
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class CacheSettings(BaseModel):
|
|
@@ -44,13 +45,27 @@ class CacheSettings(BaseModel):
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
class AppearanceSettings(BaseModel):
|
|
47
|
-
|
|
48
|
+
theme: Theme = BUILTIN_THEMES["textual-dark"]
|
|
48
49
|
# Settings to configure which UI elements to display by default
|
|
49
50
|
show_command_log: bool = True
|
|
50
|
-
|
|
51
|
+
show_workflows: bool = True
|
|
51
52
|
show_issues: bool = True
|
|
52
53
|
show_pull_requests: bool = True
|
|
53
54
|
|
|
55
|
+
@field_serializer("theme")
|
|
56
|
+
@classmethod
|
|
57
|
+
def serialize_theme(cls, theme: Theme | str) -> str:
|
|
58
|
+
return theme.name if isinstance(theme, Theme) else theme
|
|
59
|
+
|
|
60
|
+
@field_validator("theme", mode="before")
|
|
61
|
+
@classmethod
|
|
62
|
+
def validate_theme(cls, theme_name: Any) -> Theme:
|
|
63
|
+
return BUILTIN_THEMES.get(theme_name, BUILTIN_THEMES["textual-dark"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CoreConfig(BaseModel):
|
|
67
|
+
logfile_path: Path = CONFIG_FOLDER / "lazy_github.log"
|
|
68
|
+
|
|
54
69
|
|
|
55
70
|
_CONFIG_INSTANCE: Optional["Config"] = None
|
|
56
71
|
|
|
@@ -63,6 +78,7 @@ class Config(BaseModel):
|
|
|
63
78
|
issues: IssueSettings = IssueSettings()
|
|
64
79
|
cache: CacheSettings = CacheSettings()
|
|
65
80
|
api: ApiConfig = ApiConfig()
|
|
81
|
+
core: CoreConfig = CoreConfig()
|
|
66
82
|
|
|
67
83
|
@classmethod
|
|
68
84
|
def load_config(cls) -> "Config":
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import subprocess
|
|
2
1
|
import re
|
|
2
|
+
from subprocess import DEVNULL, SubprocessError, check_output
|
|
3
3
|
|
|
4
4
|
# Regex designed to match git@github.com:gizmo385/lazy-github.git:
|
|
5
5
|
# ".+:" Match everything to the first colon
|
|
@@ -13,8 +13,8 @@ _GIT_REMOTE_REGEX = re.compile(r".+:([^\/]+)\/([^.]+).git")
|
|
|
13
13
|
def current_local_repo_full_name(remote: str = "origin") -> str | None:
|
|
14
14
|
"""Returns the owner/name associated with the remote of the git repo in the current working directory."""
|
|
15
15
|
try:
|
|
16
|
-
output =
|
|
17
|
-
except
|
|
16
|
+
output = check_output(["git", "remote", "get-url", remote], stderr=DEVNULL).decode().strip()
|
|
17
|
+
except SubprocessError:
|
|
18
18
|
return None
|
|
19
19
|
|
|
20
20
|
if matches := re.match(_GIT_REMOTE_REGEX, output):
|
|
@@ -25,6 +25,6 @@ def current_local_repo_full_name(remote: str = "origin") -> str | None:
|
|
|
25
25
|
def current_local_branch_name() -> str | None:
|
|
26
26
|
"""Returns the name of the current branch for the git repo in the current working directory."""
|
|
27
27
|
try:
|
|
28
|
-
return
|
|
29
|
-
except
|
|
28
|
+
return check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=DEVNULL).decode().strip()
|
|
29
|
+
except SubprocessError:
|
|
30
30
|
return None
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from lazy_github.lib.constants import DIFF_CONTENT_ACCEPT_TYPE
|
|
2
|
+
from lazy_github.models.github import Branch, Repository
|
|
3
|
+
from lazy_github.lib.context import LazyGithubContext, github_headers
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def list_branches(repo: Repository, per_page: int = 30, page: int = 1) -> list[Branch]:
|
|
7
|
+
"""List branches on the specified repo"""
|
|
8
|
+
query_params = {"page": page, "per_page": per_page}
|
|
9
|
+
response = await LazyGithubContext.client.get(
|
|
10
|
+
f"/repos/{repo.owner.login}/{repo.name}/branches",
|
|
11
|
+
headers=github_headers(),
|
|
12
|
+
params=query_params,
|
|
13
|
+
)
|
|
14
|
+
response.raise_for_status()
|
|
15
|
+
return [Branch(**branch) for branch in response.json()]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def get_branch(repo: Repository, branch_name: str) -> Branch | None:
|
|
19
|
+
url = f"/repos/{repo.owner.login}/{repo.name}/branches/{branch_name}"
|
|
20
|
+
response = await LazyGithubContext.client.get(url, headers=github_headers())
|
|
21
|
+
return Branch(**response.json())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def compare_branches(repo: Repository, base_branch: Branch, head_branch: Branch) -> str:
|
|
25
|
+
url = f"/repos/{repo.owner.login}/{repo.name}/compare/{base_branch.name}..{head_branch.name}"
|
|
26
|
+
response = await LazyGithubContext.client.get(url, headers=github_headers(accept=DIFF_CONTENT_ACCEPT_TYPE))
|
|
27
|
+
response.raise_for_status()
|
|
28
|
+
return response.text
|
|
@@ -4,6 +4,8 @@ from lazy_github.lib.constants import IssueOwnerFilter, IssueStateFilter
|
|
|
4
4
|
from lazy_github.lib.context import LazyGithubContext, github_headers
|
|
5
5
|
from lazy_github.models.github import Issue, IssueComment, PartialPullRequest, Repository
|
|
6
6
|
|
|
7
|
+
DEFAULT_PAGE_SIZE = 30
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
class UpdateIssuePayload(TypedDict):
|
|
9
11
|
title: str | None
|
|
@@ -11,15 +13,19 @@ class UpdateIssuePayload(TypedDict):
|
|
|
11
13
|
state: str | None
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
async def list_issues(
|
|
16
|
+
async def list_issues(
|
|
17
|
+
repo: Repository, state: IssueStateFilter, owner: IssueOwnerFilter, page: int = 1, per_page: int = DEFAULT_PAGE_SIZE
|
|
18
|
+
) -> list[Issue]:
|
|
15
19
|
"""Fetch issues (included pull requests) from the repo matching the state/owner filters"""
|
|
16
|
-
query_params = {"state": str(state).lower()}
|
|
20
|
+
query_params = {"state": str(state).lower(), "page": page, "per_page": per_page}
|
|
17
21
|
if owner == IssueOwnerFilter.MINE:
|
|
18
22
|
user = await LazyGithubContext.client.user()
|
|
19
23
|
query_params["creator"] = user.login
|
|
20
24
|
|
|
21
25
|
response = await LazyGithubContext.client.get(
|
|
22
|
-
f"/repos/{repo.owner.login}/{repo.name}/issues",
|
|
26
|
+
f"/repos/{repo.owner.login}/{repo.name}/issues",
|
|
27
|
+
headers=github_headers(),
|
|
28
|
+
params=query_params,
|
|
23
29
|
)
|
|
24
30
|
response.raise_for_status()
|
|
25
31
|
result: list[Issue] = []
|
|
@@ -19,6 +19,24 @@ async def list_for_repo(repo: Repository) -> list[PartialPullRequest]:
|
|
|
19
19
|
return [i for i in issues if isinstance(i, PartialPullRequest)]
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
async def create_pull_request(
|
|
23
|
+
repo: Repository, title: str, body: str, base_ref: str, head_ref: str, draft: bool = False
|
|
24
|
+
) -> FullPullRequest:
|
|
25
|
+
user = await LazyGithubContext.client.user()
|
|
26
|
+
url = f"/repos/{repo.owner.login}/{repo.name}/pulls"
|
|
27
|
+
request_body = {
|
|
28
|
+
"title": title,
|
|
29
|
+
"draft": draft,
|
|
30
|
+
"base": base_ref,
|
|
31
|
+
"head": f"{user.login}:{head_ref}",
|
|
32
|
+
}
|
|
33
|
+
if body:
|
|
34
|
+
request_body["body"] = body
|
|
35
|
+
response = await LazyGithubContext.client.post(url, headers=github_headers(), json=request_body)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
return FullPullRequest(**response.json(), repo=repo)
|
|
38
|
+
|
|
39
|
+
|
|
22
40
|
async def get_full_pull_request(partial_pr: PartialPullRequest) -> FullPullRequest:
|
|
23
41
|
"""Converts a partial pull request into a full pull request"""
|
|
24
42
|
url = f"/repos/{partial_pr.repo.owner.login}/{partial_pr.repo.name}/pulls/{partial_pr.number}"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from httpx import HTTPError
|
|
2
|
+
from lazy_github.lib.logging import lg
|
|
3
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
4
|
+
from lazy_github.models.github import Repository, Workflow, WorkflowRun
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def list_workflows(repository: Repository, page: int = 1, per_page: int = 30) -> list[Workflow]:
|
|
8
|
+
"""Lists available Github action workflows on the specified repo"""
|
|
9
|
+
query_params = {"page": page, "per_page": per_page}
|
|
10
|
+
url = f"/repos/{repository.owner.login}/{repository.name}/actions/workflows"
|
|
11
|
+
try:
|
|
12
|
+
response = await LazyGithubContext.client.get(url, params=query_params)
|
|
13
|
+
response.raise_for_status()
|
|
14
|
+
raw_json = response.json()
|
|
15
|
+
except HTTPError:
|
|
16
|
+
lg.exception("Error retrieving actions from the Github API")
|
|
17
|
+
return []
|
|
18
|
+
else:
|
|
19
|
+
if workflows := raw_json.get("workflows"):
|
|
20
|
+
return [Workflow(**w) for w in workflows]
|
|
21
|
+
else:
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def list_workflow_runs(repository: Repository, page: int = 1, per_page: int = 30) -> list[WorkflowRun]:
|
|
26
|
+
"""Lists github workflows runs on the specified repo"""
|
|
27
|
+
query_params = {"page": page, "per_page": per_page}
|
|
28
|
+
url = f"/repos/{repository.owner.login}/{repository.name}/actions/runs"
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
response = await LazyGithubContext.client.get(url, params=query_params)
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
raw_json = response.json()
|
|
34
|
+
except HTTPError:
|
|
35
|
+
lg.exception("Error retrieving action runs from the Github API")
|
|
36
|
+
return []
|
|
37
|
+
else:
|
|
38
|
+
if workflows := raw_json.get("workflow_runs"):
|
|
39
|
+
return [WorkflowRun(**w) for w in workflows]
|
|
40
|
+
else:
|
|
41
|
+
return []
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# A universal logging format that we can use
|
|
6
|
+
class LazyGithubLogFormatter(logging.Formatter):
|
|
7
|
+
def __init__(self, include_exceptions: bool = True) -> None:
|
|
8
|
+
super().__init__("%(levelname)s %(asctime)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
|
9
|
+
self.include_exceptions = include_exceptions
|
|
10
|
+
|
|
11
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
12
|
+
if not self.include_exceptions:
|
|
13
|
+
record.exc_info = None
|
|
14
|
+
record.exc_text = None
|
|
15
|
+
return super().format(record)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
lg = logging.Logger("lazy_github", level=logging.DEBUG)
|
|
19
|
+
_lg_file_handler = logging.FileHandler(filename=LazyGithubContext.config.core.logfile_path)
|
|
20
|
+
_lg_file_handler.setFormatter(LazyGithubLogFormatter())
|
|
21
|
+
lg.addHandler(_lg_file_handler)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Override the logging level for a bunch of noisy library loggers
|
|
25
|
+
_LOGGERS_TO_MUTE = ["hishel", "httpx", "httpcore", "markdown_it", "asyncio"]
|
|
26
|
+
for other_logger_name in _LOGGERS_TO_MUTE:
|
|
27
|
+
logging.getLogger(other_logger_name).setLevel(logging.WARNING)
|
|
@@ -2,7 +2,7 @@ from functools import cached_property
|
|
|
2
2
|
|
|
3
3
|
from textual.message import Message
|
|
4
4
|
|
|
5
|
-
from lazy_github.models.github import Issue, PartialPullRequest, Repository
|
|
5
|
+
from lazy_github.models.github import FullPullRequest, Issue, IssueComment, PartialPullRequest, Repository
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class RepoSelected(Message):
|
|
@@ -59,3 +59,27 @@ class IssuesAndPullRequestsFetched(Message):
|
|
|
59
59
|
for issue in self.issues_and_pull_requests
|
|
60
60
|
if isinstance(issue, Issue) and not isinstance(issue, PartialPullRequest)
|
|
61
61
|
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PullRequestCreated(Message):
|
|
65
|
+
def __init__(self, pull_request: FullPullRequest) -> None:
|
|
66
|
+
super().__init__()
|
|
67
|
+
self.pull_request = pull_request
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class IssueCreated(Message):
|
|
71
|
+
def __init__(self, issue: Issue) -> None:
|
|
72
|
+
super().__init__()
|
|
73
|
+
self.issue = issue
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class NewCommentCreated(Message):
|
|
77
|
+
def __init__(self, comment: IssueComment) -> None:
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.comment = comment
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SettingsModalDismissed(Message):
|
|
83
|
+
def __init__(self, changed: bool) -> None:
|
|
84
|
+
super().__init__()
|
|
85
|
+
self.changed = changed
|
|
@@ -22,9 +22,9 @@ class RepositoryPermission(BaseModel):
|
|
|
22
22
|
class Repository(BaseModel):
|
|
23
23
|
name: str
|
|
24
24
|
full_name: str
|
|
25
|
-
default_branch: str
|
|
25
|
+
default_branch: str | None = None
|
|
26
26
|
private: bool
|
|
27
|
-
archived: bool
|
|
27
|
+
archived: bool | None = None
|
|
28
28
|
owner: User
|
|
29
29
|
description: str | None = None
|
|
30
30
|
permissions: RepositoryPermission | None = None
|
|
@@ -108,6 +108,7 @@ class ReviewState(StrEnum):
|
|
|
108
108
|
APPROVED = "APPROVED"
|
|
109
109
|
CHANGES_REQUESTED = "CHANGES_REQUESTED"
|
|
110
110
|
COMMENTED = "COMMENTED"
|
|
111
|
+
DISMISSED = "DISMISSED"
|
|
111
112
|
|
|
112
113
|
|
|
113
114
|
class ReviewComment(IssueComment):
|
|
@@ -126,3 +127,47 @@ class Review(BaseModel):
|
|
|
126
127
|
state: ReviewState
|
|
127
128
|
comments: list[ReviewComment] = []
|
|
128
129
|
submitted_at: datetime
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Commit(BaseModel):
|
|
133
|
+
sha: str
|
|
134
|
+
url: str
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Branch(BaseModel):
|
|
138
|
+
name: str
|
|
139
|
+
commit: Commit
|
|
140
|
+
protected: bool
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class WorkflowState(StrEnum):
|
|
144
|
+
ACTIVE = "active"
|
|
145
|
+
DELETED = "deleted"
|
|
146
|
+
DISABLED_FORK = "disabled_fork"
|
|
147
|
+
DISABLED_INACTIVITY = "disabled_inactivity"
|
|
148
|
+
DISABLED_MANUALLY = "disabled_manually "
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class Workflow(BaseModel):
|
|
152
|
+
name: str
|
|
153
|
+
state: WorkflowState
|
|
154
|
+
path: str
|
|
155
|
+
created_at: datetime
|
|
156
|
+
updated_at: datetime
|
|
157
|
+
url: str
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class WorkflowRun(BaseModel):
|
|
161
|
+
name: str
|
|
162
|
+
display_title: str
|
|
163
|
+
path: str
|
|
164
|
+
run_number: int
|
|
165
|
+
head_branch: str
|
|
166
|
+
status: str
|
|
167
|
+
conclusion: str | None = None
|
|
168
|
+
event: str
|
|
169
|
+
actor: User
|
|
170
|
+
triggering_actor: User
|
|
171
|
+
repository: Repository
|
|
172
|
+
created_at: datetime
|
|
173
|
+
updated_at: datetime
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from textual import log
|
|
2
|
+
from textual.app import App
|
|
3
|
+
from textual.theme import Theme
|
|
4
|
+
|
|
5
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
6
|
+
from lazy_github.lib.github.auth import GithubAuthenticationRequired
|
|
7
|
+
from lazy_github.lib.messages import SettingsModalDismissed
|
|
8
|
+
from lazy_github.ui.screens.auth import AuthenticationModal
|
|
9
|
+
from lazy_github.ui.screens.primary import LazyGithubMainScreen
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LazyGithub(App):
|
|
13
|
+
BINDINGS = [("q", "quit", "Quit"), ("ctrl+p", "command_palette"), ("ctrl+m", "maximize", "Maximize")]
|
|
14
|
+
|
|
15
|
+
has_shown_maximize_toast: bool = False
|
|
16
|
+
|
|
17
|
+
async def authenticate_with_github(self):
|
|
18
|
+
try:
|
|
19
|
+
# We pull the user here to validate auth
|
|
20
|
+
_ = await LazyGithubContext.client.user()
|
|
21
|
+
self.push_screen(LazyGithubMainScreen(id="main-screen"))
|
|
22
|
+
except GithubAuthenticationRequired:
|
|
23
|
+
log("Triggering auth with github")
|
|
24
|
+
self.push_screen(AuthenticationModal(id="auth-modal"))
|
|
25
|
+
|
|
26
|
+
async def on_ready(self):
|
|
27
|
+
await self.authenticate_with_github()
|
|
28
|
+
|
|
29
|
+
def on_settings_modal_dismissed(self, message: SettingsModalDismissed) -> None:
|
|
30
|
+
if not message.changed:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
self.notify("Settings updated")
|
|
34
|
+
if isinstance(LazyGithubContext.config.appearance.theme, Theme):
|
|
35
|
+
self.theme = LazyGithubContext.config.appearance.theme.name
|
|
36
|
+
else:
|
|
37
|
+
self.theme = LazyGithubContext.config.appearance.theme
|
|
38
|
+
|
|
39
|
+
self.query_one("#main-screen", LazyGithubMainScreen).handle_settings_update()
|
|
40
|
+
|
|
41
|
+
def on_mount(self) -> None:
|
|
42
|
+
self.theme = LazyGithubContext.config.appearance.theme.name
|
|
43
|
+
|
|
44
|
+
def action_maximize(self) -> None:
|
|
45
|
+
if self.screen.is_maximized:
|
|
46
|
+
return
|
|
47
|
+
if self.screen.focused is not None:
|
|
48
|
+
# We don't need to repeatedly show this to the user
|
|
49
|
+
if self.screen.maximize(self.screen.focused) and not self.has_shown_maximize_toast:
|
|
50
|
+
self.notify("Current view maximized. Press [b]escape[/b] to return.", title="View Maximized")
|
|
51
|
+
self.has_shown_maximize_toast = True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
app = LazyGithub()
|
|
@@ -6,8 +6,9 @@ from textual.screen import ModalScreen
|
|
|
6
6
|
from textual.widgets import Button, Label, Markdown, Rule, TextArea
|
|
7
7
|
|
|
8
8
|
from lazy_github.lib.github import issues, pull_requests
|
|
9
|
+
from lazy_github.lib.messages import NewCommentCreated
|
|
9
10
|
from lazy_github.models.github import Issue, IssueComment, Repository, Review, ReviewComment
|
|
10
|
-
from lazy_github.
|
|
11
|
+
from lazy_github.lib.logging import lg
|
|
11
12
|
|
|
12
13
|
CommmentReplyTarget = ReviewComment | Review | IssueComment
|
|
13
14
|
|
|
@@ -83,23 +84,23 @@ class NewCommentContainer(Container):
|
|
|
83
84
|
body = self.query_one("#new_comment_body", TextArea).text
|
|
84
85
|
try:
|
|
85
86
|
if isinstance(self.reply_to, ReviewComment):
|
|
86
|
-
await pull_requests.reply_to_review_comment(self.repo, self.issue, self.reply_to, body)
|
|
87
|
+
new_comment = await pull_requests.reply_to_review_comment(self.repo, self.issue, self.reply_to, body)
|
|
87
88
|
else:
|
|
88
|
-
await issues.create_comment(self.issue, body)
|
|
89
|
-
except HTTPStatusError
|
|
89
|
+
new_comment = await issues.create_comment(self.issue, body)
|
|
90
|
+
except HTTPStatusError:
|
|
90
91
|
# TODO: We should handle the error case better here
|
|
91
|
-
|
|
92
|
+
lg.exception(f"Error while posting comment for issue #{self.issue.number}")
|
|
92
93
|
self.app.pop_screen()
|
|
93
94
|
else:
|
|
94
|
-
|
|
95
|
-
self.
|
|
95
|
+
lg.info(f"Successfully posted new comment for issue #{self.issue.number}")
|
|
96
|
+
self.post_message(NewCommentCreated(new_comment))
|
|
96
97
|
|
|
97
98
|
@on(Button.Pressed, "#cancel_comment")
|
|
98
99
|
def cancel_comment(self, _: Button.Pressed) -> None:
|
|
99
100
|
self.app.pop_screen()
|
|
100
101
|
|
|
101
102
|
|
|
102
|
-
class NewCommentModal(ModalScreen):
|
|
103
|
+
class NewCommentModal(ModalScreen[IssueComment | None]):
|
|
103
104
|
DEFAULT_CSS = """
|
|
104
105
|
NewCommentModal {
|
|
105
106
|
border: ascii green;
|
|
@@ -134,5 +135,9 @@ class NewCommentModal(ModalScreen):
|
|
|
134
135
|
def compose(self) -> ComposeResult:
|
|
135
136
|
yield NewCommentContainer(self.repo, self.issue, self.reply_to)
|
|
136
137
|
|
|
138
|
+
@on(NewCommentCreated)
|
|
139
|
+
def on_comment_created(self, message: NewCommentCreated) -> None:
|
|
140
|
+
self.dismiss(message.comment)
|
|
141
|
+
|
|
137
142
|
def action_cancel(self) -> None:
|
|
138
|
-
self.
|
|
143
|
+
self.dismiss(None)
|
|
@@ -4,8 +4,10 @@ from textual.containers import Container, Horizontal, ScrollableContainer
|
|
|
4
4
|
from textual.screen import ModalScreen
|
|
5
5
|
from textual.widgets import Button, Input, Label, Rule, TextArea
|
|
6
6
|
|
|
7
|
+
from lazy_github.lib.context import LazyGithubContext
|
|
7
8
|
from lazy_github.lib.github import issues
|
|
8
|
-
from lazy_github.
|
|
9
|
+
from lazy_github.lib.messages import IssueCreated
|
|
10
|
+
from lazy_github.models.github import Issue
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class NewIssueContainer(Container):
|
|
@@ -23,11 +25,8 @@ class NewIssueContainer(Container):
|
|
|
23
25
|
}
|
|
24
26
|
"""
|
|
25
27
|
|
|
26
|
-
def __init__(self, repo: Repository, *args, **kwargs) -> None:
|
|
27
|
-
super().__init__(*args, **kwargs)
|
|
28
|
-
self.repo = repo
|
|
29
|
-
|
|
30
28
|
def compose(self) -> ComposeResult:
|
|
29
|
+
assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo in new PR modal"
|
|
31
30
|
with ScrollableContainer():
|
|
32
31
|
yield Label("[bold]Title[/bold]")
|
|
33
32
|
yield Input(placeholder="Title", id="new_issue_title")
|
|
@@ -47,6 +46,8 @@ class NewIssueContainer(Container):
|
|
|
47
46
|
|
|
48
47
|
@on(Button.Pressed, "#save_new_issue")
|
|
49
48
|
async def submit_new_issue(self, _: Button) -> None:
|
|
49
|
+
assert LazyGithubContext.current_repo is not None, "Unexpectedly missing current repo from application context!"
|
|
50
|
+
|
|
50
51
|
title = self.query_one("#new_issue_title", Input).value
|
|
51
52
|
body = self.query_one("#new_issue_body", TextArea).text
|
|
52
53
|
if not str(title):
|
|
@@ -57,12 +58,13 @@ class NewIssueContainer(Container):
|
|
|
57
58
|
return
|
|
58
59
|
|
|
59
60
|
self.notify("Creating new issue...")
|
|
60
|
-
new_issue = await issues.create_issue(
|
|
61
|
-
self.notify(f"Successfully
|
|
62
|
-
self.
|
|
61
|
+
new_issue = await issues.create_issue(LazyGithubContext.current_repo, title, body)
|
|
62
|
+
self.notify(f"Successfully created issue #{new_issue.number}")
|
|
63
|
+
self.post_message(IssueCreated(new_issue))
|
|
63
64
|
|
|
64
65
|
|
|
65
|
-
class NewIssueModal(ModalScreen):
|
|
66
|
+
class NewIssueModal(ModalScreen[Issue | None]):
|
|
67
|
+
BINDINGS = [("ESC, q", "cancel", "Cancel")]
|
|
66
68
|
DEFAULT_CSS = """
|
|
67
69
|
NewIssueModal {
|
|
68
70
|
align: center middle;
|
|
@@ -77,9 +79,12 @@ class NewIssueModal(ModalScreen):
|
|
|
77
79
|
}
|
|
78
80
|
"""
|
|
79
81
|
|
|
80
|
-
def __init__(self, repo: Repository, *args, **kwargs) -> None:
|
|
81
|
-
super().__init__(*args, **kwargs)
|
|
82
|
-
self.repo = repo
|
|
83
|
-
|
|
84
82
|
def compose(self) -> ComposeResult:
|
|
85
|
-
yield NewIssueContainer(
|
|
83
|
+
yield NewIssueContainer()
|
|
84
|
+
|
|
85
|
+
def action_cancel(self) -> None:
|
|
86
|
+
self.dismiss()
|
|
87
|
+
|
|
88
|
+
@on(IssueCreated)
|
|
89
|
+
def on_issue_created(self, message: IssueCreated) -> None:
|
|
90
|
+
self.dismiss(message.issue)
|