-
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 all 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
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,24 @@ | ||
| 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 ..branch import BranchData | ||
| from ..diff import DiffTreeData | ||
| from ..protocols import CoreProposedChange | ||
| 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 +27,108 @@ | |
| ENVVAR_CONFIG_FILE = "INFRAHUBCTL_CONFIG" | ||
|
|
||
|
|
||
| def format_timestamp(timestamp: str) -> str: | ||
| """Format ISO timestamp to 'YYYY-MM-DD HH:MM:SS'. | ||
| Args: | ||
| timestamp (str): ISO fromatted timestamp | ||
|
|
||
| Returns: | ||
| (str): the datetime as string formatted as 'YYYY-MM-DD HH:MM:SS' | ||
|
|
||
| Raises: | ||
| Any execptions returned from formatting the timestamp are propogated to the caller | ||
| """ | ||
| dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) | ||
| return dt.strftime("%Y-%m-%d %H:%M:%S") | ||
|
Comment on lines
+30
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix typos in docstring. The docstring contains several typographical errors. def format_timestamp(timestamp: str) -> str:
- """Format ISO timestamp to 'YYYY-MM-DD HH:MM:SS'.
+ """Format ISO timestamp to 'YYYY-MM-DD HH:MM:SS'.
Args:
- timestamp (str): ISO fromatted timestamp
+ timestamp (str): ISO formatted timestamp
Returns:
(str): the datetime as string formatted as 'YYYY-MM-DD HH:MM:SS'
Raises:
- Any execptions returned from formatting the timestamp are propogated to the caller
+ Any exceptions raised from formatting the timestamp are propagated to the caller
""" |
||
|
|
||
|
|
||
| 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 | ||
|
|
||
|
|
||
| def generate_branch_report_table( | ||
| branch: BranchData, diff_tree: DiffTreeData | None, git_files_changed: bool | None | ||
| ) -> Table: | ||
| branch_table = Table(show_header=False, box=None) | ||
| branch_table.add_column(justify="left") | ||
| branch_table.add_column(justify="right") | ||
|
|
||
| branch_table.add_row("Created at", format_timestamp(branch.branched_from)) | ||
|
|
||
| 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") | ||
|
|
||
| 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") | ||
|
|
||
| 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", "-") | ||
|
|
||
| return branch_table | ||
|
|
||
|
|
||
| def generate_proposed_change_tables(proposed_changes: list[CoreProposedChange]) -> list[Table]: | ||
| proposed_change_tables: list[Table] = [] | ||
|
|
||
| for pc in proposed_changes: | ||
| # Create proposal table | ||
| proposed_change_table = Table(show_header=False, box=None) | ||
| proposed_change_table.add_column(justify="left") | ||
| proposed_change_table.add_column(justify="right") | ||
|
|
||
| # Extract data from node | ||
| proposed_change_table.add_row("Name", pc.name.value) | ||
| proposed_change_table.add_row("State", str(pc.state.value)) | ||
| proposed_change_table.add_row("Is draft", "Yes" if pc.is_draft.value else "No") | ||
| proposed_change_table.add_row("Created by", pc.created_by.peer.name.value) # type: ignore[union-attr] | ||
| proposed_change_table.add_row("Created at", format_timestamp(str(pc.created_by.updated_at))) | ||
| proposed_change_table.add_row("Approvals", str(len(pc.approved_by.peers))) | ||
| proposed_change_table.add_row("Rejections", str(len(pc.rejected_by.peers))) | ||
|
|
||
| proposed_change_tables.append(proposed_change_table) | ||
|
|
||
| return proposed_change_tables | ||
|
|
||
|
|
||
| @app.callback() | ||
| def callback() -> None: | ||
| """ | ||
|
|
@@ -143,3 +254,66 @@ 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( | ||
| 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.""" | ||
|
|
||
| client = initialize_client() | ||
|
|
||
| # Fetch branch metadata first (needed for diff creation) | ||
| branch = await client.branch.get(branch_name=branch_name) | ||
|
|
||
| if branch.is_default: | ||
| console.print("[red]Cannot create a report for the default branch!") | ||
| sys.exit(1) | ||
|
|
||
| # 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") | ||
|
|
||
| diff_tree = await client.get_diff_tree(branch=branch_name) | ||
|
|
||
| git_files_changed = await check_git_files_changed(client, branch=branch_name) | ||
|
|
||
| proposed_changes = await client.filters( | ||
| kind=CoreProposedChange, # type: ignore[type-abstract] | ||
| source_branch__value=branch_name, | ||
| include=["created_by"], | ||
| prefetch_relationships=True, | ||
| property=True, | ||
| ) | ||
|
|
||
| branch_table = generate_branch_report_table(branch=branch, diff_tree=diff_tree, git_files_changed=git_files_changed) | ||
| proposed_change_tables = generate_proposed_change_tables(proposed_changes=proposed_changes) | ||
|
|
||
| console.print() | ||
| console.print(f"[bold]Branch: {branch_name}[/bold]") | ||
|
|
||
| console.print(branch_table) | ||
| console.print() | ||
|
|
||
| if not proposed_changes: | ||
| console.print("No proposed changes for this branch") | ||
| console.print() | ||
|
|
||
| for proposed_change, proposed_change_table in zip(proposed_changes, proposed_change_tables, strict=True): | ||
| console.print(f"Proposed change: {proposed_change.name.value}") | ||
| console.print(proposed_change_table) | ||
| 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.