305 lines
12 KiB
Python
305 lines
12 KiB
Python
"""Submodule management commands for ai.gpt."""
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
import subprocess
|
|
import json
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
|
|
from ..docs.config import get_ai_root, load_docs_config
|
|
from ..docs.git_utils import (
|
|
check_git_repository,
|
|
get_git_branch,
|
|
get_git_remote_url
|
|
)
|
|
from ..docs.utils import run_command
|
|
|
|
console = Console()
|
|
submodules_app = typer.Typer(help="Submodule management for AI ecosystem")
|
|
|
|
|
|
def get_submodules_from_gitmodules(repo_path: Path) -> Dict[str, str]:
|
|
"""Parse .gitmodules file to get submodule information."""
|
|
gitmodules_path = repo_path / ".gitmodules"
|
|
if not gitmodules_path.exists():
|
|
return {}
|
|
|
|
submodules = {}
|
|
current_name = None
|
|
|
|
with open(gitmodules_path, 'r') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith('[submodule "') and line.endswith('"]'):
|
|
current_name = line[12:-2] # Extract module name
|
|
elif line.startswith('path = ') and current_name:
|
|
path = line[7:] # Extract path
|
|
submodules[current_name] = path
|
|
current_name = None
|
|
|
|
return submodules
|
|
|
|
|
|
def get_branch_for_module(config, module_name: str) -> str:
|
|
"""Get target branch for a module from ai.json."""
|
|
project_info = config.get_project_info(module_name)
|
|
if project_info and project_info.branch:
|
|
return project_info.branch
|
|
return "main" # Default branch
|
|
|
|
|
|
@submodules_app.command("list")
|
|
def list_submodules(
|
|
dir: Optional[Path] = typer.Option(None, "--dir", "-d", help="AI ecosystem root directory"),
|
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed information")
|
|
):
|
|
"""List all submodules and their status."""
|
|
try:
|
|
config = load_docs_config(dir)
|
|
ai_root = get_ai_root(dir)
|
|
|
|
if not check_git_repository(ai_root):
|
|
console.print("[red]Error: Not a git repository[/red]")
|
|
raise typer.Abort()
|
|
|
|
submodules = get_submodules_from_gitmodules(ai_root)
|
|
|
|
if not submodules:
|
|
console.print("[yellow]No submodules found[/yellow]")
|
|
return
|
|
|
|
table = Table(title="Submodules Status")
|
|
table.add_column("Module", style="cyan")
|
|
table.add_column("Path", style="blue")
|
|
table.add_column("Branch", style="green")
|
|
table.add_column("Status", style="yellow")
|
|
|
|
for module_name, module_path in submodules.items():
|
|
full_path = ai_root / module_path
|
|
|
|
if not full_path.exists():
|
|
status = "❌ Missing"
|
|
branch = "N/A"
|
|
else:
|
|
branch = get_git_branch(full_path) or "detached"
|
|
|
|
# Check if submodule is up to date
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "submodule", "status", module_path],
|
|
cwd=ai_root
|
|
)
|
|
|
|
if returncode == 0 and stdout:
|
|
status_char = stdout[0] if stdout else ' '
|
|
if status_char == ' ':
|
|
status = "✅ Clean"
|
|
elif status_char == '+':
|
|
status = "📝 Modified"
|
|
elif status_char == '-':
|
|
status = "❌ Not initialized"
|
|
elif status_char == 'U':
|
|
status = "⚠️ Conflicts"
|
|
else:
|
|
status = "❓ Unknown"
|
|
else:
|
|
status = "❓ Unknown"
|
|
|
|
target_branch = get_branch_for_module(config, module_name)
|
|
branch_display = f"{branch}"
|
|
if branch != target_branch:
|
|
branch_display += f" (target: {target_branch})"
|
|
|
|
table.add_row(module_name, module_path, branch_display, status)
|
|
|
|
console.print(table)
|
|
|
|
if verbose:
|
|
console.print(f"\n[dim]Total submodules: {len(submodules)}[/dim]")
|
|
console.print(f"[dim]Repository root: {ai_root}[/dim]")
|
|
|
|
except Exception as e:
|
|
console.print(f"[red]Error: {e}[/red]")
|
|
raise typer.Abort()
|
|
|
|
|
|
@submodules_app.command("update")
|
|
def update_submodules(
|
|
module: Optional[str] = typer.Option(None, "--module", "-m", help="Update specific submodule"),
|
|
all: bool = typer.Option(False, "--all", "-a", help="Update all submodules"),
|
|
dir: Optional[Path] = typer.Option(None, "--dir", "-d", help="AI ecosystem root directory"),
|
|
dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done"),
|
|
auto_commit: bool = typer.Option(False, "--auto-commit", help="Auto-commit changes"),
|
|
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed output")
|
|
):
|
|
"""Update submodules to latest commits."""
|
|
if not module and not all:
|
|
console.print("[red]Error: Either --module or --all is required[/red]")
|
|
raise typer.Abort()
|
|
|
|
if module and all:
|
|
console.print("[red]Error: Cannot use both --module and --all[/red]")
|
|
raise typer.Abort()
|
|
|
|
try:
|
|
config = load_docs_config(dir)
|
|
ai_root = get_ai_root(dir)
|
|
|
|
if not check_git_repository(ai_root):
|
|
console.print("[red]Error: Not a git repository[/red]")
|
|
raise typer.Abort()
|
|
|
|
submodules = get_submodules_from_gitmodules(ai_root)
|
|
|
|
if not submodules:
|
|
console.print("[yellow]No submodules found[/yellow]")
|
|
return
|
|
|
|
# Determine which modules to update
|
|
if all:
|
|
modules_to_update = list(submodules.keys())
|
|
else:
|
|
if module not in submodules:
|
|
console.print(f"[red]Error: Submodule '{module}' not found[/red]")
|
|
console.print(f"Available modules: {', '.join(submodules.keys())}")
|
|
raise typer.Abort()
|
|
modules_to_update = [module]
|
|
|
|
if dry_run:
|
|
console.print("[yellow]🔍 DRY RUN MODE - No changes will be made[/yellow]")
|
|
|
|
console.print(f"[cyan]Updating {len(modules_to_update)} submodule(s)...[/cyan]")
|
|
|
|
updated_modules = []
|
|
|
|
for module_name in modules_to_update:
|
|
module_path = submodules[module_name]
|
|
full_path = ai_root / module_path
|
|
target_branch = get_branch_for_module(config, module_name)
|
|
|
|
console.print(f"\n[blue]📦 Processing: {module_name}[/blue]")
|
|
|
|
if not full_path.exists():
|
|
console.print(f"[red]❌ Module directory not found: {module_path}[/red]")
|
|
continue
|
|
|
|
# Get current commit
|
|
current_commit = None
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "rev-parse", "HEAD"],
|
|
cwd=full_path
|
|
)
|
|
if returncode == 0:
|
|
current_commit = stdout.strip()[:8]
|
|
|
|
if dry_run:
|
|
console.print(f"[yellow]🔍 Would update {module_name} to branch {target_branch}[/yellow]")
|
|
if current_commit:
|
|
console.print(f"[dim]Current: {current_commit}[/dim]")
|
|
continue
|
|
|
|
# Fetch latest changes
|
|
console.print(f"[dim]Fetching latest changes...[/dim]")
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "fetch", "origin"],
|
|
cwd=full_path
|
|
)
|
|
|
|
if returncode != 0:
|
|
console.print(f"[red]❌ Failed to fetch: {stderr}[/red]")
|
|
continue
|
|
|
|
# Check if update is needed
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "rev-parse", f"origin/{target_branch}"],
|
|
cwd=full_path
|
|
)
|
|
|
|
if returncode != 0:
|
|
console.print(f"[red]❌ Branch {target_branch} not found on remote[/red]")
|
|
continue
|
|
|
|
latest_commit = stdout.strip()[:8]
|
|
|
|
if current_commit == latest_commit:
|
|
console.print(f"[green]✅ Already up to date[/green]")
|
|
continue
|
|
|
|
# Switch to target branch and pull
|
|
console.print(f"[dim]Switching to branch {target_branch}...[/dim]")
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "checkout", target_branch],
|
|
cwd=full_path
|
|
)
|
|
|
|
if returncode != 0:
|
|
console.print(f"[red]❌ Failed to checkout {target_branch}: {stderr}[/red]")
|
|
continue
|
|
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "pull", "origin", target_branch],
|
|
cwd=full_path
|
|
)
|
|
|
|
if returncode != 0:
|
|
console.print(f"[red]❌ Failed to pull: {stderr}[/red]")
|
|
continue
|
|
|
|
# Get new commit
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "rev-parse", "HEAD"],
|
|
cwd=full_path
|
|
)
|
|
new_commit = stdout.strip()[:8] if returncode == 0 else "unknown"
|
|
|
|
# Stage the submodule update
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "add", module_path],
|
|
cwd=ai_root
|
|
)
|
|
|
|
console.print(f"[green]✅ Updated {module_name} ({current_commit} → {new_commit})[/green]")
|
|
updated_modules.append((module_name, current_commit, new_commit))
|
|
|
|
# Summary
|
|
if updated_modules:
|
|
console.print(f"\n[green]🎉 Successfully updated {len(updated_modules)} module(s)[/green]")
|
|
|
|
if verbose:
|
|
for module_name, old_commit, new_commit in updated_modules:
|
|
console.print(f" • {module_name}: {old_commit} → {new_commit}")
|
|
|
|
if auto_commit and not dry_run:
|
|
console.print("[blue]💾 Auto-committing changes...[/blue]")
|
|
commit_message = f"Update submodules\n\n📦 Updated modules: {len(updated_modules)}\n"
|
|
for module_name, old_commit, new_commit in updated_modules:
|
|
commit_message += f"- {module_name}: {old_commit} → {new_commit}\n"
|
|
commit_message += "\n🤖 Generated with ai.gpt submodules update"
|
|
|
|
returncode, stdout, stderr = run_command(
|
|
["git", "commit", "-m", commit_message],
|
|
cwd=ai_root
|
|
)
|
|
|
|
if returncode == 0:
|
|
console.print("[green]✅ Changes committed successfully[/green]")
|
|
else:
|
|
console.print(f"[red]❌ Failed to commit: {stderr}[/red]")
|
|
elif not dry_run:
|
|
console.print("[yellow]💾 Changes staged but not committed[/yellow]")
|
|
console.print("Run with --auto-commit to commit automatically")
|
|
elif not dry_run:
|
|
console.print("[yellow]No modules needed updating[/yellow]")
|
|
|
|
except Exception as e:
|
|
console.print(f"[red]Error: {e}[/red]")
|
|
if verbose:
|
|
console.print_exception()
|
|
raise typer.Abort()
|
|
|
|
|
|
# Export the submodules app
|
|
__all__ = ["submodules_app"] |