|
1 | 1 | import logging |
| 2 | +from datetime import datetime, timezone |
| 3 | +from typing import TYPE_CHECKING |
2 | 4 |
|
3 | 5 | import typer |
4 | 6 | from rich.console import Console |
5 | 7 | from rich.table import Table |
6 | 8 |
|
7 | 9 | from ..async_typer import AsyncTyper |
8 | | -from ..utils import calculate_time_diff |
| 10 | +from ..utils import calculate_time_diff, decode_json |
9 | 11 | from .client import initialize_client |
10 | 12 | from .parameters import CONFIG_PARAM |
11 | 13 | from .utils import catch_exception |
12 | 14 |
|
| 15 | +if TYPE_CHECKING: |
| 16 | + from ..client import InfrahubClient |
| 17 | + |
13 | 18 | app = AsyncTyper() |
14 | 19 | console = Console() |
15 | 20 |
|
|
18 | 23 | ENVVAR_CONFIG_FILE = "INFRAHUBCTL_CONFIG" |
19 | 24 |
|
20 | 25 |
|
| 26 | +def format_timestamp(timestamp: str) -> str: |
| 27 | + """Format ISO timestamp to 'YYYY-MM-DD HH:MM:SS'.""" |
| 28 | + try: |
| 29 | + dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) |
| 30 | + return dt.strftime("%Y-%m-%d %H:%M:%S") |
| 31 | + except (ValueError, AttributeError): |
| 32 | + return timestamp |
| 33 | + |
| 34 | + |
| 35 | +async def check_git_files_changed(client: "InfrahubClient", branch: str) -> bool: |
| 36 | + """Check if there are any Git file changes in a branch. |
| 37 | +
|
| 38 | + Args: |
| 39 | + client: Infrahub client instance |
| 40 | + branch: Branch name to check |
| 41 | +
|
| 42 | + Returns: |
| 43 | + True if files have changed, False otherwise |
| 44 | +
|
| 45 | + Raises: |
| 46 | + Any exceptions from the API call are propagated to the caller |
| 47 | + """ |
| 48 | + url = f"{client.address}/api/diff/files?branch={branch}" |
| 49 | + resp = await client._get(url=url, timeout=client.default_timeout) |
| 50 | + resp.raise_for_status() |
| 51 | + data = decode_json(response=resp) |
| 52 | + |
| 53 | + # Check if any repository has files |
| 54 | + if branch in data: |
| 55 | + for repo_data in data[branch].values(): |
| 56 | + if isinstance(repo_data, dict) and "files" in repo_data and len(repo_data["files"]) > 0: |
| 57 | + return True |
| 58 | + |
| 59 | + return False |
| 60 | + |
| 61 | + |
21 | 62 | @app.callback() |
22 | 63 | def callback() -> None: |
23 | 64 | """ |
@@ -143,3 +184,111 @@ async def validate(branch_name: str, _: str = CONFIG_PARAM) -> None: |
143 | 184 | client = initialize_client() |
144 | 185 | await client.branch.validate(branch_name=branch_name) |
145 | 186 | console.print(f"Branch '{branch_name}' is valid.") |
| 187 | + |
| 188 | + |
| 189 | +@app.command() |
| 190 | +@catch_exception(console=console) |
| 191 | +async def report( # noqa: PLR0915 |
| 192 | + branch_name: str = typer.Argument(..., help="Branch name to generate report for"), |
| 193 | + update_diff: bool = typer.Option(False, "--update-diff", help="Update diff before generating report"), |
| 194 | + _: str = CONFIG_PARAM, |
| 195 | +) -> None: |
| 196 | + """Generate branch cleanup status report.""" |
| 197 | + |
| 198 | + client = initialize_client() |
| 199 | + |
| 200 | + # Fetch branch metadata first (needed for diff creation) |
| 201 | + branch = await client.branch.get(branch_name=branch_name) |
| 202 | + |
| 203 | + # Update diff if requested |
| 204 | + if update_diff: |
| 205 | + console.print("Updating diff...") |
| 206 | + # Create diff from branch creation to now |
| 207 | + from_time = datetime.fromisoformat(branch.branched_from.replace("Z", "+00:00")) |
| 208 | + to_time = datetime.now(timezone.utc) |
| 209 | + await client.create_diff( |
| 210 | + branch=branch_name, |
| 211 | + name=f"report-{branch_name}", |
| 212 | + from_time=from_time, |
| 213 | + to_time=to_time, |
| 214 | + ) |
| 215 | + console.print("Diff updated\n") |
| 216 | + |
| 217 | + # Fetch diff tree (with metadata) |
| 218 | + diff_tree = await client.get_diff_tree(branch=branch_name) |
| 219 | + |
| 220 | + # Check if Git files have changed |
| 221 | + git_files_changed = None |
| 222 | + git_files_changed = await check_git_files_changed(client, branch=branch_name) |
| 223 | + |
| 224 | + # Print branch title |
| 225 | + console.print() |
| 226 | + console.print(f"[bold]Branch: {branch_name}[/bold]") |
| 227 | + |
| 228 | + # Create branch metadata table |
| 229 | + branch_table = Table(show_header=False, box=None) |
| 230 | + branch_table.add_column(justify="left") |
| 231 | + branch_table.add_column(justify="right") |
| 232 | + |
| 233 | + # Add branch metadata rows |
| 234 | + branch_table.add_row("Created at", format_timestamp(branch.branched_from)) |
| 235 | + |
| 236 | + # Add status |
| 237 | + status_value = branch.status.value if hasattr(branch.status, "value") else str(branch.status) |
| 238 | + branch_table.add_row("Status", status_value) |
| 239 | + |
| 240 | + branch_table.add_row("Synced with Git", "Yes" if branch.sync_with_git else "No") |
| 241 | + |
| 242 | + # Add Git files changed |
| 243 | + if git_files_changed is not None: |
| 244 | + branch_table.add_row("Git files changed", "Yes" if git_files_changed else "No") |
| 245 | + else: |
| 246 | + branch_table.add_row("Git files changed", "N/A") |
| 247 | + |
| 248 | + branch_table.add_row("Has schema changes", "Yes" if branch.has_schema_changes else "No") |
| 249 | + |
| 250 | + # Add diff information |
| 251 | + if diff_tree: |
| 252 | + branch_table.add_row("Diff last updated", format_timestamp(diff_tree["to_time"])) |
| 253 | + branch_table.add_row("Amount of additions", str(diff_tree["num_added"])) |
| 254 | + branch_table.add_row("Amount of deletions", str(diff_tree["num_removed"])) |
| 255 | + branch_table.add_row("Amount of updates", str(diff_tree["num_updated"])) |
| 256 | + branch_table.add_row("Amount of conflicts", str(diff_tree["num_conflicts"])) |
| 257 | + else: |
| 258 | + branch_table.add_row("Diff last updated", "No diff available") |
| 259 | + branch_table.add_row("Amount of additions", "-") |
| 260 | + branch_table.add_row("Amount of deletions", "-") |
| 261 | + branch_table.add_row("Amount of updates", "-") |
| 262 | + branch_table.add_row("Amount of conflicts", "-") |
| 263 | + |
| 264 | + console.print(branch_table) |
| 265 | + console.print() |
| 266 | + |
| 267 | + # Fetch proposed changes for the branch |
| 268 | + proposed_changes = await client.filters( |
| 269 | + kind="CoreProposedChange", source_branch__value=branch_name, include=["created_by"], prefetch_relationships=True |
| 270 | + ) |
| 271 | + |
| 272 | + # Print proposed changes section |
| 273 | + if proposed_changes: |
| 274 | + for pc in proposed_changes: |
| 275 | + # Create proposal table |
| 276 | + proposal_table = Table(show_header=False, box=None) |
| 277 | + proposal_table.add_column(justify="left") |
| 278 | + proposal_table.add_column(justify="right") |
| 279 | + |
| 280 | + # Extract data from node |
| 281 | + proposal_table.add_row("Name", pc.name.value) # type: ignore[union-attr] |
| 282 | + proposal_table.add_row("State", str(pc.state.value)) # type: ignore[union-attr] |
| 283 | + proposal_table.add_row("Is draft", "Yes" if pc.is_draft.value else "No") # type: ignore[union-attr] |
| 284 | + proposal_table.add_row("Created by", pc.created_by.peer.name.value) # type: ignore[union-attr] |
| 285 | + proposal_table.add_row("Created at", format_timestamp(str(pc.created_by.updated_at))) # type: ignore[union-attr] |
| 286 | + proposal_table.add_row("Approvals", str(len(pc.approved_by.peers))) # type: ignore[union-attr] |
| 287 | + proposal_table.add_row("Rejections", str(len(pc.rejected_by.peers))) # type: ignore[union-attr] |
| 288 | + |
| 289 | + console.print(f"Proposed change: {pc.name.value}") # type: ignore[union-attr] |
| 290 | + console.print(proposal_table) |
| 291 | + console.print() |
| 292 | + else: |
| 293 | + console.print("No proposed changes for this branch") |
| 294 | + console.print() |
0 commit comments