Skip to content

Commit bd37282

Browse files
committed
add branch report command to infrahubctl
1 parent 1429763 commit bd37282

File tree

1 file changed

+150
-1
lines changed

1 file changed

+150
-1
lines changed

infrahub_sdk/ctl/branch.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import logging
2+
from datetime import datetime, timezone
3+
from typing import TYPE_CHECKING
24

35
import typer
46
from rich.console import Console
57
from rich.table import Table
68

79
from ..async_typer import AsyncTyper
8-
from ..utils import calculate_time_diff
10+
from ..utils import calculate_time_diff, decode_json
911
from .client import initialize_client
1012
from .parameters import CONFIG_PARAM
1113
from .utils import catch_exception
1214

15+
if TYPE_CHECKING:
16+
from ..client import InfrahubClient
17+
1318
app = AsyncTyper()
1419
console = Console()
1520

@@ -18,6 +23,42 @@
1823
ENVVAR_CONFIG_FILE = "INFRAHUBCTL_CONFIG"
1924

2025

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+
2162
@app.callback()
2263
def callback() -> None:
2364
"""
@@ -143,3 +184,111 @@ async def validate(branch_name: str, _: str = CONFIG_PARAM) -> None:
143184
client = initialize_client()
144185
await client.branch.validate(branch_name=branch_name)
145186
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

Comments
 (0)