Add --health option to verify Gitea connectivity and display version. Add Description column (truncated at 40 chars) with --no-desc to hide it. Add get_version() method to GiteaClient. fixes #14 fixes #15 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
223 lines
6.8 KiB
Python
223 lines
6.8 KiB
Python
"""Formatage et affichage Rich du dashboard Gitea."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
|
|
from gitea_dashboard.collector import RepoData
|
|
|
|
|
|
def _format_repo_name(repo: RepoData) -> str:
|
|
"""Formate le nom du repo avec les indicateurs visuels."""
|
|
indicators = []
|
|
if repo.is_fork:
|
|
indicators.append("[F]")
|
|
if repo.is_archived:
|
|
indicators.append("[A]")
|
|
if repo.is_mirror:
|
|
indicators.append("[M]")
|
|
|
|
if indicators:
|
|
return f"{repo.name} {' '.join(indicators)}"
|
|
return repo.name
|
|
|
|
|
|
def _format_relative_date(iso_date: str) -> str:
|
|
"""Convertit une date ISO en date relative lisible."""
|
|
try:
|
|
dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
|
|
except (ValueError, AttributeError):
|
|
return ""
|
|
|
|
now = datetime.now(timezone.utc)
|
|
delta = now - dt
|
|
|
|
days = delta.days
|
|
if days < 0:
|
|
return "dans le futur"
|
|
if days == 0:
|
|
return "aujourd'hui"
|
|
if days == 1:
|
|
return "il y a 1j"
|
|
if days < 30:
|
|
return f"il y a {days}j"
|
|
months = days // 30
|
|
if months < 12:
|
|
return f"il y a {months}m"
|
|
years = days // 365
|
|
return f"il y a {years}a"
|
|
|
|
|
|
def _format_release(release: dict | None) -> str:
|
|
"""Formate la release pour l'affichage."""
|
|
if release is None:
|
|
return "\u2014"
|
|
|
|
tag = release.get("tag_name", "")
|
|
published = release.get("published_at", "")
|
|
|
|
if published:
|
|
relative = _format_relative_date(published)
|
|
if relative:
|
|
return f"{tag} ({relative})"
|
|
|
|
return tag
|
|
|
|
|
|
def _colorize_milestone_due(due_on: str | None) -> str:
|
|
"""Retourne le style Rich selon la proximite de l'echeance.
|
|
|
|
- Rouge : echeance depassee
|
|
- Jaune : echeance dans les 7 prochains jours
|
|
- Vert : echeance dans plus de 7 jours
|
|
- Chaine vide : pas d'echeance definie
|
|
"""
|
|
if not due_on:
|
|
return ""
|
|
try:
|
|
dt = datetime.fromisoformat(due_on.replace("Z", "+00:00"))
|
|
except (ValueError, AttributeError):
|
|
return ""
|
|
|
|
now = datetime.now(timezone.utc)
|
|
delta = dt - now
|
|
days = delta.days
|
|
|
|
if days < 0:
|
|
return "red"
|
|
if days < 7:
|
|
return "yellow"
|
|
return "green"
|
|
|
|
|
|
def _truncate(text: str, max_length: int = 40) -> str:
|
|
"""Tronque le texte a max_length caracteres avec '...' si necessaire."""
|
|
if len(text) <= max_length:
|
|
return text
|
|
return text[:max_length] + "..."
|
|
|
|
|
|
def sort_repos(repos: list[RepoData], sort_key: str) -> list[RepoData]:
|
|
"""Trie la liste des repos selon le critere donne.
|
|
|
|
Args:
|
|
repos: Liste des repos a trier.
|
|
sort_key: Critere de tri parmi :
|
|
- "name" : alphabetique par nom (defaut)
|
|
- "issues" : par nombre d'issues ouvertes (decroissant)
|
|
- "release" : par date de derniere release (plus recent d'abord)
|
|
- "activity" : par date du dernier commit (plus recent d'abord)
|
|
"""
|
|
if sort_key == "name":
|
|
return sorted(repos, key=lambda r: r.name.lower())
|
|
if sort_key == "issues":
|
|
return sorted(repos, key=lambda r: r.open_issues, reverse=True)
|
|
if sort_key == "release":
|
|
# Repos sans release en dernier (date vide = epoch 0)
|
|
def release_date(r: RepoData) -> str:
|
|
if r.latest_release and r.latest_release.get("published_at"):
|
|
return r.latest_release["published_at"]
|
|
return ""
|
|
|
|
return sorted(repos, key=release_date, reverse=True)
|
|
if sort_key == "activity":
|
|
# Repos sans commit en dernier (date vide = epoch 0)
|
|
return sorted(repos, key=lambda r: r.last_commit_date or "", reverse=True)
|
|
return repos
|
|
|
|
|
|
def render_dashboard(
|
|
repos: list[RepoData],
|
|
console: Console | None = None,
|
|
sort_key: str = "name",
|
|
show_description: bool = True,
|
|
) -> None:
|
|
"""Affiche le dashboard complet dans le terminal.
|
|
|
|
- Tableau principal : nom repo, description (optionnelle, tronquee a 40 chars),
|
|
indicateurs (fork/archive/mirror), issues ouvertes, derniere release
|
|
- Section milestones : par repo ayant des milestones,
|
|
nom, progression (closed/total), date echeance
|
|
|
|
Le parametre console permet l'injection pour les tests.
|
|
Si show_description est True, ajoute une colonne "Description"
|
|
entre "Repo" et "Issues", tronquee a 40 caracteres.
|
|
"""
|
|
if console is None:
|
|
console = Console()
|
|
|
|
if not repos:
|
|
console.print("Aucun repo trouve.")
|
|
return
|
|
|
|
# Tri des repos
|
|
sorted_repos = sort_repos(repos, sort_key)
|
|
|
|
# Tableau principal
|
|
table = Table(title="Gitea Dashboard")
|
|
table.add_column("Repo", style="bold")
|
|
if show_description:
|
|
table.add_column("Description")
|
|
table.add_column("Issues", justify="right")
|
|
table.add_column("Release")
|
|
table.add_column("Dernier commit")
|
|
|
|
for repo in sorted_repos:
|
|
name = _format_repo_name(repo)
|
|
issues_str = str(repo.open_issues)
|
|
issues_style = "red" if repo.open_issues > 0 else "green"
|
|
release_str = _format_release(repo.latest_release)
|
|
commit_str = (
|
|
_format_relative_date(repo.last_commit_date) if repo.last_commit_date else "\u2014"
|
|
)
|
|
|
|
row = [name]
|
|
if show_description:
|
|
row.append(_truncate(repo.description or ""))
|
|
row.extend(
|
|
[
|
|
f"[{issues_style}]{issues_str}[/{issues_style}]",
|
|
release_str,
|
|
commit_str,
|
|
]
|
|
)
|
|
|
|
table.add_row(*row)
|
|
|
|
console.print(table)
|
|
|
|
# Section milestones — uniquement si au moins un repo en a
|
|
repos_with_milestones = [r for r in sorted_repos if r.milestones]
|
|
|
|
if repos_with_milestones:
|
|
console.print()
|
|
console.print("[bold]Milestones[/bold]")
|
|
|
|
for repo in repos_with_milestones:
|
|
for ms in repo.milestones:
|
|
title = ms["title"]
|
|
closed = ms["closed_issues"]
|
|
total = ms["open_issues"] + ms["closed_issues"]
|
|
pct = round(closed / total * 100) if total > 0 else 0
|
|
|
|
line = f" {repo.name} / {title} : {closed}/{total} ({pct}%)"
|
|
|
|
due_on = ms.get("due_on")
|
|
if due_on:
|
|
# Extraire juste la date (YYYY-MM-DD)
|
|
try:
|
|
dt = datetime.fromisoformat(due_on.replace("Z", "+00:00"))
|
|
line += f" \u2014 echeance {dt.strftime('%Y-%m-%d')}"
|
|
except (ValueError, AttributeError):
|
|
pass
|
|
|
|
# Coloration selon la proximite de l'echeance
|
|
style = _colorize_milestone_due(due_on)
|
|
if style:
|
|
console.print(f"[{style}]{line}[/{style}]")
|
|
else:
|
|
console.print(line)
|