-
Notifications
You must be signed in to change notification settings - Fork 7
Add infrahubctl branch report command
#637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
wvandeun
merged 23 commits into
infrahub-develop
from
wvd-20251114-infp388-branch-cleanup-mechanism
Dec 1, 2025
Merged
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
1f4f5a0
add optional graphql variable types and add datetime
wvandeun 16fe512
add get_diff_tree method to SDK client to get a diff object
wvandeun ab9ae5b
add updated_at meta data property for attributes and relationships
wvandeun a1e9b7f
add branch report command to infrahubctl
wvandeun b1040bd
fix
wvandeun ab8df56
add tests
wvandeun 5c4aca9
ruff
wvandeun d98a7c4
update documentation
wvandeun 0b48ff7
fix mypy
wvandeun b3f9a2f
fix: replace | union syntax with Union for Python 3.9 compatibility
wvandeun 22a5de6
fix tests
wvandeun 78c71b3
fail branch report command on main branch
wvandeun a75c77e
linting
wvandeun 634d2d2
linting
wvandeun 101f45a
remove deprecated raise_for_error argument
wvandeun c850da3
Merge remote-tracking branch 'origin/infrahub-develop' into wvd-20251…
wvandeun 32db310
remove excpetion handling in format_timestamp
wvandeun b997e3a
remove typing ignore statements
wvandeun c1ce7df
refactor creating branch report tables
wvandeun d5ff419
fix created_at metadata for proposed change in branch report
wvandeun 898819d
update tests
wvandeun 665eb4c
update tests
wvandeun b38fa29
update tests
wvandeun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,21 @@ | ||
| import logging | ||
| import sys | ||
| from datetime import datetime, timezone | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| import typer | ||
| from rich.console import Console | ||
| from rich.table import Table | ||
|
|
||
| from ..async_typer import AsyncTyper | ||
| from ..utils import calculate_time_diff | ||
| from ..utils import calculate_time_diff, decode_json | ||
| from .client import initialize_client | ||
| from .parameters import CONFIG_PARAM | ||
| from .utils import catch_exception | ||
|
|
||
| if TYPE_CHECKING: | ||
| from ..client import InfrahubClient | ||
|
|
||
| app = AsyncTyper() | ||
| console = Console() | ||
|
|
||
|
|
@@ -18,6 +24,42 @@ | |
| ENVVAR_CONFIG_FILE = "INFRAHUBCTL_CONFIG" | ||
|
|
||
|
|
||
| def format_timestamp(timestamp: str) -> str: | ||
| """Format ISO timestamp to 'YYYY-MM-DD HH:MM:SS'.""" | ||
| try: | ||
| dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) | ||
| return dt.strftime("%Y-%m-%d %H:%M:%S") | ||
| except (ValueError, AttributeError): | ||
| return timestamp | ||
wvandeun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| async def check_git_files_changed(client: "InfrahubClient", branch: str) -> bool: | ||
| """Check if there are any Git file changes in a branch. | ||
|
|
||
| Args: | ||
| client: Infrahub client instance | ||
| branch: Branch name to check | ||
|
|
||
| Returns: | ||
| True if files have changed, False otherwise | ||
|
|
||
| Raises: | ||
| Any exceptions from the API call are propagated to the caller | ||
| """ | ||
| url = f"{client.address}/api/diff/files?branch={branch}" | ||
| resp = await client._get(url=url, timeout=client.default_timeout) | ||
| resp.raise_for_status() | ||
| data = decode_json(response=resp) | ||
|
|
||
| # Check if any repository has files | ||
| if branch in data: | ||
| for repo_data in data[branch].values(): | ||
| if isinstance(repo_data, dict) and "files" in repo_data and len(repo_data["files"]) > 0: | ||
| return True | ||
|
|
||
| return False | ||
|
|
||
|
|
||
| @app.callback() | ||
| def callback() -> None: | ||
| """ | ||
|
|
@@ -143,3 +185,115 @@ async def validate(branch_name: str, _: str = CONFIG_PARAM) -> None: | |
| client = initialize_client() | ||
| await client.branch.validate(branch_name=branch_name) | ||
| console.print(f"Branch '{branch_name}' is valid.") | ||
|
|
||
|
|
||
| @app.command() | ||
| @catch_exception(console=console) | ||
| async def report( # noqa: PLR0915 | ||
wvandeun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| branch_name: str = typer.Argument(..., help="Branch name to generate report for"), | ||
| update_diff: bool = typer.Option(False, "--update-diff", help="Update diff before generating report"), | ||
| _: str = CONFIG_PARAM, | ||
| ) -> None: | ||
| """Generate branch cleanup status report.""" | ||
|
|
||
| if branch_name == "main": | ||
| console.print("[red]Cannot create a report for the main branch!") | ||
| sys.exit(1) | ||
wvandeun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| client = initialize_client() | ||
|
|
||
| # Fetch branch metadata first (needed for diff creation) | ||
| branch = await client.branch.get(branch_name=branch_name) | ||
|
|
||
| # Update diff if requested | ||
| if update_diff: | ||
| console.print("Updating diff...") | ||
| # Create diff from branch creation to now | ||
| from_time = datetime.fromisoformat(branch.branched_from.replace("Z", "+00:00")) | ||
| to_time = datetime.now(timezone.utc) | ||
| await client.create_diff( | ||
| branch=branch_name, | ||
| name=f"report-{branch_name}", | ||
| from_time=from_time, | ||
| to_time=to_time, | ||
| ) | ||
| console.print("Diff updated\n") | ||
|
|
||
| # Fetch diff tree (with metadata) | ||
| diff_tree = await client.get_diff_tree(branch=branch_name) | ||
|
|
||
| # Check if Git files have changed | ||
| git_files_changed = None | ||
| git_files_changed = await check_git_files_changed(client, branch=branch_name) | ||
|
|
||
| # Print branch title | ||
| console.print() | ||
| console.print(f"[bold]Branch: {branch_name}[/bold]") | ||
|
|
||
| # Create branch metadata table | ||
| branch_table = Table(show_header=False, box=None) | ||
| branch_table.add_column(justify="left") | ||
| branch_table.add_column(justify="right") | ||
|
|
||
| # Add branch metadata rows | ||
| branch_table.add_row("Created at", format_timestamp(branch.branched_from)) | ||
|
|
||
| # Add status | ||
| status_value = branch.status.value if hasattr(branch.status, "value") else str(branch.status) | ||
| branch_table.add_row("Status", status_value) | ||
|
|
||
| branch_table.add_row("Synced with Git", "Yes" if branch.sync_with_git else "No") | ||
|
|
||
| # Add Git files changed | ||
| if git_files_changed is not None: | ||
| branch_table.add_row("Git files changed", "Yes" if git_files_changed else "No") | ||
| else: | ||
| branch_table.add_row("Git files changed", "N/A") | ||
|
|
||
| branch_table.add_row("Has schema changes", "Yes" if branch.has_schema_changes else "No") | ||
wvandeun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| # Add diff information | ||
| if diff_tree: | ||
| branch_table.add_row("Diff last updated", format_timestamp(diff_tree["to_time"])) | ||
| branch_table.add_row("Amount of additions", str(diff_tree["num_added"])) | ||
| branch_table.add_row("Amount of deletions", str(diff_tree["num_removed"])) | ||
| branch_table.add_row("Amount of updates", str(diff_tree["num_updated"])) | ||
| branch_table.add_row("Amount of conflicts", str(diff_tree["num_conflicts"])) | ||
| else: | ||
| branch_table.add_row("Diff last updated", "No diff available") | ||
| branch_table.add_row("Amount of additions", "-") | ||
| branch_table.add_row("Amount of deletions", "-") | ||
| branch_table.add_row("Amount of updates", "-") | ||
| branch_table.add_row("Amount of conflicts", "-") | ||
|
|
||
| console.print(branch_table) | ||
| console.print() | ||
|
|
||
| # Fetch proposed changes for the branch | ||
| proposed_changes = await client.filters( | ||
| kind="CoreProposedChange", source_branch__value=branch_name, include=["created_by"], prefetch_relationships=True | ||
| ) | ||
|
|
||
| # Print proposed changes section | ||
| if proposed_changes: | ||
| for pc in proposed_changes: | ||
| # Create proposal table | ||
| proposal_table = Table(show_header=False, box=None) | ||
| proposal_table.add_column(justify="left") | ||
| proposal_table.add_column(justify="right") | ||
|
|
||
| # Extract data from node | ||
| proposal_table.add_row("Name", pc.name.value) # type: ignore[union-attr] | ||
| proposal_table.add_row("State", str(pc.state.value)) # type: ignore[union-attr] | ||
| proposal_table.add_row("Is draft", "Yes" if pc.is_draft.value else "No") # type: ignore[union-attr] | ||
| proposal_table.add_row("Created by", pc.created_by.peer.name.value) # type: ignore[union-attr] | ||
|
||
| proposal_table.add_row("Created at", format_timestamp(str(pc.created_by.updated_at))) # type: ignore[union-attr] | ||
wvandeun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| proposal_table.add_row("Approvals", str(len(pc.approved_by.peers))) # type: ignore[union-attr] | ||
| proposal_table.add_row("Rejections", str(len(pc.rejected_by.peers))) # type: ignore[union-attr] | ||
wvandeun marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| console.print(f"Proposed change: {pc.name.value}") # type: ignore[union-attr] | ||
| console.print(proposal_table) | ||
| console.print() | ||
| else: | ||
| console.print("No proposed changes for this branch") | ||
| console.print() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.