Skip to content
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
42 changes: 33 additions & 9 deletions src/AllGitStatus/MainApp.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,8 @@ async def _ResetAllRepositories(self) -> None:
self._additional_info_data.clear()
self._state_data.clear()
self._data_table.clear()
await self._OnSelectionChanged(repopulate_changes=False)

await self._OnSelectionChanged()

# Get the repositories

Expand Down Expand Up @@ -287,6 +288,7 @@ async def _ResetRepository(self, repository: Repository, repository_index: int)
)

if repository_index == self._data_table.cursor_coordinate.row:
await self._OnSelectionChanged()
self._RefreshBindings()

# Create the repo name
Expand Down Expand Up @@ -314,22 +316,43 @@ async def _ResetRepository(self, repository: Repository, repository_index: int)
async def LoadCells() -> None:
assert self._github_session is not None

for source in [
sources = [
LocalGitSource(),
GitHubSource(self._github_session),
]:
]

# Set all of the column values to pending
for source in sources:
if not source.Applies(repository):
continue

for column_key, column in COLUMN_MAP.items():
if not column_key[0] and not column_key[1]:
continue

if column_key[0] != source.__class__.__name__:
continue

self._data_table.update_cell_at(
Coordinate(repository_index, column.value),
Text("⏳", justify=column.justify), # ty: ignore[invalid-argument-type]
update_width=True,
)

Comment thread
davidbrownell marked this conversation as resolved.
# Get the actual values
for source in sources:
if not source.Applies(repository):
continue

async for info in source.Query(repository):
self._PopulateCell(repository_index, info)
await self._PopulateCell(repository_index, info)

# ----------------------------------------------------------------------

self.run_worker(LoadCells())

# ----------------------------------------------------------------------
def _PopulateCell(self, repository_index: int, info: ResultInfo | ErrorInfo) -> None:
async def _PopulateCell(self, repository_index: int, info: ResultInfo | ErrorInfo) -> None:
column = COLUMN_MAP[info.key]

if isinstance(info, ErrorInfo):
Expand Down Expand Up @@ -361,21 +384,22 @@ def _PopulateCell(self, repository_index: int, info: ResultInfo | ErrorInfo) ->

self._additional_info_data.setdefault(repository_index, {})[column.value] = additional_info

if self._data_table.cursor_row == repository_index and self._data_table.cursor_column == column.value:
await self._OnSelectionChanged()

# ----------------------------------------------------------------------
async def _OnSelectionChanged(self, *, repopulate_changes: bool = True) -> None:
async def _OnSelectionChanged(self) -> None:
self._additional_info.clear()
self._RefreshBindings()

if not repopulate_changes:
return

row_index = self._data_table.cursor_coordinate.row
col_index = self._data_table.cursor_coordinate.column

additional_info = self._additional_info_data.get(row_index, {}).get(col_index)

if additional_info:
self._additional_info.write(additional_info)
self._additional_info.scroll_home()

# ----------------------------------------------------------------------
def _RefreshBindings(self) -> None:
Expand Down
78 changes: 40 additions & 38 deletions src/AllGitStatus/Sources/GitHubSource.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import textwrap

from collections.abc import AsyncGenerator
from datetime import datetime, timedelta, UTC

import aiohttp

Expand Down Expand Up @@ -131,6 +132,8 @@ async def _GenerateIssueInfo(
repo: Repository,
github_url: str,
) -> AsyncGenerator[ResultInfo | ErrorInfo]:
key = (self.__class__.__name__, "issues")

try:
label_counts: dict[str, int] = {}
total_count = 0
Expand Down Expand Up @@ -160,7 +163,7 @@ async def _GenerateIssueInfo(

label_str = f" [{', '.join(issue_labels)}]" if issue_labels else ""

issue_data.append(f" #{issue_number}{label_str} {issue_title} (by {issue_author})")
issue_data.append(f"- #{issue_number}{label_str} {issue_title} (by {issue_author})")

# Build additional info with issue details
additional_info_lines = [
Expand All @@ -182,24 +185,22 @@ async def _GenerateIssueInfo(

yield ResultInfo(
repo,
(self.__class__.__name__, "issues"),
key,
f"{total_count:5} 🐛",
"\n".join(additional_info_lines),
)

except Exception as ex:
yield ErrorInfo(
repo,
(self.__class__.__name__, "issues"),
ex,
)
yield ErrorInfo(repo, key, ex)

# ----------------------------------------------------------------------
async def _GeneratePullRequestInfo(
self,
repo: Repository,
github_url: str,
) -> AsyncGenerator[ResultInfo | ErrorInfo]:
key = (self.__class__.__name__, "pull_requests")

try:
total_count = 0
pr_data: list[str] = []
Expand All @@ -216,7 +217,7 @@ async def _GeneratePullRequestInfo(

draft_indicator = "[DRAFT] " if pr_draft else ""

pr_data.append(f" #{pr_number} {draft_indicator}{pr_title} (by {pr_author})")
pr_data.append(f"- #{pr_number} {draft_indicator}{pr_title} (by {pr_author})")

additional_info_lines = [
f"Pull Requests: {github_url}/pulls",
Expand All @@ -229,24 +230,22 @@ async def _GeneratePullRequestInfo(

yield ResultInfo(
repo,
(self.__class__.__name__, "pull_requests"),
key,
f"{total_count:5} 🔀",
"\n".join(additional_info_lines),
)

except Exception as ex:
yield ErrorInfo(
repo,
(self.__class__.__name__, "pull_requests"),
ex,
)
yield ErrorInfo(repo, key, ex)

# ----------------------------------------------------------------------
async def _GenerateSecurityAlertInfo(
self,
repo: Repository,
github_url: str,
) -> AsyncGenerator[ResultInfo | ErrorInfo]:
key = (self.__class__.__name__, "security_alerts")

try:
severity_counts: dict[str, int] = {
"critical": 0,
Expand All @@ -271,7 +270,7 @@ async def _GenerateSecurityAlertInfo(
package = alert.get("security_vulnerability", {}).get("package", {})

alert_data.append(
f" [{advisory.get('severity', 'unknown').upper()}] {package.get('name', 'unknown')}: {advisory.get('summary', 'No summary')}"
f"- [{advisory.get('severity', 'unknown').upper()}] {package.get('name', 'unknown')}: {advisory.get('summary', 'No summary')}"
)

# Build display value with icon based on severity
Expand Down Expand Up @@ -303,17 +302,13 @@ async def _GenerateSecurityAlertInfo(

yield ResultInfo(
repo,
(self.__class__.__name__, "security_alerts"),
key,
display_value,
"\n".join(additional_info_lines),
)

except Exception as ex:
yield ErrorInfo(
repo,
(self.__class__.__name__, "security_alerts"),
ex,
)
yield ErrorInfo(repo, key, ex)

# ----------------------------------------------------------------------
async def _GenerateCICDInfo(
Expand All @@ -322,12 +317,20 @@ async def _GenerateCICDInfo(
github_url: str,
default_branch: str,
) -> AsyncGenerator[ResultInfo | ErrorInfo]:
info_key = (self.__class__.__name__, "cicd_status")
key = (self.__class__.__name__, "cicd_status")

try:
url = f"https://api.github.com/repos/{repo.github_owner}/{repo.github_repo}/actions/runs?branch={default_branch}&per_page=100"
prev_month = datetime.now(tz=UTC) - timedelta(days=30)

url = f"https://api.github.com/repos/{repo.github_owner}/{repo.github_repo}/actions/runs"

params = {
"branch": default_branch,
"per_page": 100,
"created": f">={prev_month.date()}",
}

async with self._session.get(url) as response:
async with self._session.get(url, params=params) as response:
response.raise_for_status()
result = await response.json()

Expand All @@ -336,17 +339,18 @@ async def _GenerateCICDInfo(
if not workflow_runs:
yield ResultInfo(
repo,
info_key,
"🔘",
key,
"-",
textwrap.dedent(
"""\
CI/CD Status: {github_url}/actions

No workflow runs found for branch '{default_branch}'
No workflow runs found for branch '{default_branch}' since '{prev_month}'.
""",
).format(
github_url=github_url,
default_branch=default_branch,
prev_month=prev_month.date(),
),
)
return
Expand Down Expand Up @@ -386,7 +390,7 @@ async def _GenerateCICDInfo(
else:
status_label = conclusion or status or "UNKNOWN"

run_details.append(f" [{status_label}] {run['created_at']} {run['path']}: {run['name']}")
run_details.append(f"- [{status_label}] {run['created_at']} {run['path']}: {run['name']}")

# Determine display icon based on priority: failure > in_progress > success
if status_counts["failure"] > 0:
Expand Down Expand Up @@ -415,33 +419,31 @@ async def _GenerateCICDInfo(

yield ResultInfo(
repo,
info_key,
key,
display_icon,
"\n".join(additional_info_lines),
)

except Exception as ex:
yield ErrorInfo(
repo,
info_key,
ex,
)
yield ErrorInfo(repo, key, ex)

# ----------------------------------------------------------------------
async def _GeneratePaginatedResults(self, raw_url: str) -> AsyncGenerator[dict]:
url: str | None = f"{raw_url}?state=open&per_page=100"
async def _GeneratePaginatedResults(self, url: str) -> AsyncGenerator[dict]:
params = {"state": "open", "per_page": 100}
next_page_regex = re.compile(r"<([^>]+)>")

while url:
async with self._session.get(url) as response:
async with self._session.get(url, params=params) as response:
response.raise_for_status()
Comment thread
davidbrownell marked this conversation as resolved.

results = await response.json()

for result in results:
yield result

url = None
url: str | None = None
params = None

link_header = response.headers.get("Link", "")

for link in link_header.split(","):
Expand Down
Loading
Loading