Source code for dbx_python_cli.commands.list

"""List command for listing repositories."""

import json
import subprocess

import typer

from dbx_python_cli.utils.repo import (
    get_base_dir,
    get_config,
    get_group_dir,
    get_repo_groups,
    is_flat_mode,
    list_repos,
)

# Create a Typer app that will act as a single command
app = typer.Typer(
    help="List repositories",
    no_args_is_help=False,
    invoke_without_command=True,
    context_settings={
        "allow_interspersed_args": False,
        "help_option_names": ["-h", "--help"],
    },
)


def _repo_branch_status(repo_path):
    """Return (branch, ahead, behind) for a cloned repo using cached remote state."""
    try:
        branch = (
            subprocess.run(
                ["git", "-C", str(repo_path), "branch", "--show-current"],
                capture_output=True,
                text=True,
                check=True,
            ).stdout.strip()
            or "(detached)"
        )
    except subprocess.CalledProcessError:
        return "(unknown)", None, None

    try:
        remotes = (
            subprocess.run(
                ["git", "-C", str(repo_path), "remote"],
                capture_output=True,
                text=True,
                check=True,
            )
            .stdout.strip()
            .split()
        )
    except subprocess.CalledProcessError:
        return branch, None, None

    if "upstream" not in remotes:
        return branch, None, None

    try:
        ahead = int(
            subprocess.run(
                [
                    "git",
                    "-C",
                    str(repo_path),
                    "rev-list",
                    "--count",
                    f"upstream/{branch}..HEAD",
                ],
                capture_output=True,
                text=True,
                check=True,
            ).stdout.strip()
        )
        behind = int(
            subprocess.run(
                [
                    "git",
                    "-C",
                    str(repo_path),
                    "rev-list",
                    "--count",
                    f"HEAD..upstream/{branch}",
                ],
                capture_output=True,
                text=True,
                check=True,
            ).stdout.strip()
        )
        return branch, ahead, behind
    except subprocess.CalledProcessError:
        return branch, None, None


def _format_upstream_status(ahead, behind):
    if ahead is None:
        return typer.style("no upstream remote", dim=True)
    if ahead == 0 and behind == 0:
        return typer.style("up to date", fg=typer.colors.GREEN)
    parts = []
    if behind:
        parts.append(typer.style(f"↓{behind} behind", fg=typer.colors.RED))
    if ahead:
        parts.append(typer.style(f"↑{ahead} ahead", fg=typer.colors.YELLOW))
    return "  ".join(parts)


[docs] @app.callback() def list_callback( ctx: typer.Context, group: list[str] = typer.Option( None, "--group", "-g", help="Filter to one or more groups (can be repeated or comma-separated)", ), branches: bool = typer.Option( False, "--branches", "-b", help="Show current branch and upstream sync status for each repo", ), ): """List all repositories (cloned and available). Shows a tree view of all repository groups with status indicators: - ✓ = cloned - ○ = available to clone - ? = cloned but not in config Examples:: dbx list # List all repositories dbx list -g django # Filter to django group dbx list -g django --branches # Show branch + upstream status """ verbose = ctx.obj.get("verbose", False) if ctx.obj else False try: config = get_config() base_dir = get_base_dir(config) if verbose: typer.echo(f"[verbose] Using base directory: {base_dir}") typer.echo(f"[verbose] Config:\n{json.dumps(config, indent=4)}\n") except Exception as e: typer.echo(f"❌ Error: {e}", err=True) raise typer.Exit(1) # Parse comma-separated group names group_filter = set() for g in group or []: group_filter.update(name.strip() for name in g.split(",") if name.strip()) if branches: flat = is_flat_mode(config) groups = get_repo_groups(config) target_groups = { k: v for k, v in groups.items() if not group_filter or k in group_filter } if not target_groups: typer.echo(f"❌ Group(s) not found: {', '.join(group_filter)}", err=True) raise typer.Exit(1) typer.echo(f"{typer.style('Base directory:', bold=True)} {base_dir}\n") repo_col = 28 branch_col = 22 for group_name, group_cfg in target_groups.items(): group_dir = get_group_dir(base_dir, group_name, flat) typer.echo(typer.style(f"{group_name}/", bold=True)) for repo_url in group_cfg.get("repos", []): repo_name = repo_url.split("/")[-1].replace(".git", "") repo_path = group_dir / repo_name if repo_path.exists() and (repo_path / ".git").exists(): icon = typer.style("✓", fg=typer.colors.GREEN) branch, ahead, behind = _repo_branch_status(repo_path) upstream_str = _format_upstream_status(ahead, behind) name_col = f" {icon} {repo_name}".ljust(repo_col) branch_part = branch.ljust(branch_col) typer.echo(f"{name_col} {branch_part} {upstream_str}") else: icon = typer.style("○", fg=typer.colors.YELLOW) typer.echo(f" {icon} {typer.style(repo_name, dim=True)}") typer.echo() return formatted_output = list_repos(base_dir, config=config) if not formatted_output: typer.echo(f"Base directory: {base_dir}\n") typer.echo("No repositories found.") typer.echo("\nClone repositories using: dbx clone -g <group>") return typer.echo(f"{typer.style('Base directory:', bold=True)} {base_dir}\n") typer.echo(f"{typer.style('Repository status:', bold=True)}\n") typer.echo(formatted_output) cloned_label = typer.style("✓", fg=typer.colors.GREEN) available_label = typer.style("○", fg=typer.colors.YELLOW) unknown_label = typer.style("?", fg=typer.colors.MAGENTA) typer.echo( f"\nLegend: {cloned_label} = cloned, {available_label} = available to clone, {unknown_label} = cloned but not in config" )