blob: 312bdc0593e15fccfec02019b576475e91c84bf9 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
from dataclasses import asdict, dataclass
from typing import Callable, Iterable
from utils import Colors, colorize
from worktree import Worktree
@dataclass
class SyncPrintData:
behind: int
new: int
@dataclass
class BuildDirPrintData:
path: str
active: bool
config: str
last_build: str
last_build_sec: float | None = None
@dataclass
class WorktreePrintData:
name: str
task: str | None
sync: SyncPrintData
build_dirs: list[BuildDirPrintData]
def format_time_ago_plain(sec: float | None) -> str:
if sec is None:
return "never built"
if sec < 60:
return "just now"
minutes = int(sec) // 60
hours = minutes // 60
days = hours // 24
if days > 0:
return f"{days}d ago"
if hours > 0:
return f"{hours}h ago"
return f"{minutes}m ago"
def format_time_ago(sec: float | None) -> str:
if sec is None:
return colorize("never built", Colors.RED)
return format_time_ago_plain(sec)
class WorktreePrinter:
"""Formats and displays Worktree information in terminal and JSON outputs.
Provides helper methods to render worktree status, lease details, build
timestamps, and sync health across list and pool subcommands.
"""
@staticmethod
def print_worktrees(
worktrees: Iterable[Worktree],
title_fn: Callable[[Worktree], str] | None = None,
) -> None:
wt_list = list(worktrees)
if not wt_list:
return
max_left_len = 0
wt_entries = []
for wt in wt_list:
display_name = title_fn(wt) if title_fn else wt.name
data = WorktreePrinter.format_worktree_data(wt)
parts = []
if data.sync.behind > 0:
parts.append(colorize(f"{data.sync.behind} behind", Colors.RED))
if data.sync.new > 0:
parts.append(colorize(f"{data.sync.new} new", Colors.BLUE))
build_entries = []
for bd in data.build_dirs:
suffix = " *" if bd.active else ""
label = f"{bd.path}{suffix}:"
max_left_len = max(max_left_len, 4 + len(label))
time_str = format_time_ago(bd.last_build_sec)
build_entries.append((label, bd.config, time_str, bd.active))
wt_entries.append((display_name, parts, build_entries))
wt_entries.sort(key=lambda item: item[0])
left_width = max_left_len + 4
for name, parts, builds in wt_entries:
header = colorize(name, Colors.BOLD)
if parts:
header = f"{header} ({', '.join(parts)})"
print(header)
for i, (label, cfg, time_str, is_active) in enumerate(builds):
is_last = i == len(builds) - 1
prefix = "└── " if is_last else "├── "
left_part = f"{prefix}{label}"
line = f"{left_part:<{left_width}}{cfg} ({time_str})"
if is_active:
line = colorize(line, Colors.GREEN)
print(line)
if builds:
print()
@staticmethod
def format_worktree_data(
wt: Worktree, include_inactive: bool = False
) -> WorktreePrintData:
"""Collects worktree status, build configs, and lease details into
WorktreePrintData.
"""
lease = wt.get_lease_info()
task_name = lease.task_id if lease and lease.task_id else None
if include_inactive:
display_name = wt.name
else:
display_name = task_name if task_name else wt.name
_, behind, new = wt.get_sync_status()
try:
selected_dir = wt.selected_build_dir()
except (FileNotFoundError, ValueError, OSError):
selected_dir = None
build_entries: list[BuildDirPrintData] = []
for bd in wt.build_dirs():
try:
out_rel = str(bd.path.relative_to(wt.path))
except ValueError:
out_rel = str(bd.path)
is_active = selected_dir is not None and bd.path == selected_dir
cfg = bd.get_build_config()
time_ago_sec = bd.get_build_time_ago_sec()
build_entries.append(
BuildDirPrintData(
path=out_rel,
active=is_active,
config=cfg,
last_build=format_time_ago_plain(time_ago_sec),
last_build_sec=time_ago_sec,
)
)
return WorktreePrintData(
name=display_name,
task=task_name,
sync=SyncPrintData(behind=behind, new=new),
build_dirs=build_entries,
)
@staticmethod
def print_worktrees_json(
worktrees: Iterable[Worktree], include_inactive: bool = False
) -> None:
"""Serializes and outputs worktree entries as JSON to stdout."""
entries = []
for wt in worktrees:
d = asdict(
WorktreePrinter.format_worktree_data(
wt, include_inactive=include_inactive
)
)
for bd in d["build_dirs"]:
bd.pop("last_build_sec", None)
entries.append(d)
print(json.dumps(entries))