Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions src/shelfai/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,24 @@ def _choose_template_name(registry, default: Optional[str] = None) -> str:
)


# ──────────────────────────────────────────────
# shelfai git (subcommand group)
# ──────────────────────────────────────────────

git_app = typer.Typer(
name="git",
help="Inspect git history for ShelfAI chunks and manage git hooks.",
no_args_is_help=True,
)
app.add_typer(git_app, name="git")


def _git_integration(repo_path: str):
from shelfai.core.git_integration import GitIntegration

return GitIntegration(repo_path=repo_path)


# ──────────────────────────────────────────────
# shelfai init
# ──────────────────────────────────────────────
Expand Down Expand Up @@ -3301,3 +3319,116 @@ def sync_configure(
}
config_path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8")
console.print(f"[green]Updated sync settings in {config_path}[/green]")


@git_app.command("history")
def git_history(
chunk_id: Optional[str] = typer.Argument(
None,
help="Chunk file or chunk id to inspect. If omitted, shows all tracked chunk history.",
),
limit: int = typer.Option(20, "--limit", "-n", help="Maximum number of commits to show"),
repo_path: str = typer.Option(".", "--repo", "-r", help="Path to the git repository"),
):
"""Show git history for chunk(s)."""
git = _git_integration(repo_path)
if not git.is_git_repo():
console.print(f"[red]Not a git repository: {Path(repo_path).resolve()}[/red]")
raise typer.Exit(1)

if chunk_id:
history = git.get_chunk_history(chunk_id, limit=limit)
console.print(f"\n[bold]Git history for[/bold] {history.chunk_id}\n")
console.print(f"Total changes: {history.total_changes}")
console.print(f"Last changed: {history.last_changed or 'unknown'}")
console.print(f"Last author: {history.last_author or 'unknown'}\n")
for commit in history.commits:
console.print(f"- {commit['hash']} {commit['date']} {commit['author']}")
console.print(f" {commit['message']}")
return

changelog = git.get_shelf_changelog(limit=limit)
if not changelog:
console.print("[dim]No tracked chunk changes found.[/dim]")
return

for item in changelog:
console.print(f"- {item['commit']} {item['date']}")
console.print(f" {item['message']}")
console.print(f" chunks: {', '.join(item['chunks_changed'])}")


@git_app.command("changelog")
def git_changelog(
since: Optional[str] = typer.Option(None, "--since", help="Only include commits after this date or commit"),
limit: int = typer.Option(20, "--limit", "-n", help="Maximum number of commits to show"),
repo_path: str = typer.Option(".", "--repo", "-r", help="Path to the git repository"),
):
"""Shelf changelog for chunk files."""
git = _git_integration(repo_path)
if not git.is_git_repo():
console.print(f"[red]Not a git repository: {Path(repo_path).resolve()}[/red]")
raise typer.Exit(1)

changelog = git.get_shelf_changelog(since=since, limit=limit)
if not changelog:
console.print("[dim]No tracked chunk changes found.[/dim]")
return

for item in changelog:
console.print(f"- {item['commit']} {item['date']}")
console.print(f" {item['message']}")
console.print(f" chunks: {', '.join(item['chunks_changed'])}")


@git_app.command("blame")
def git_blame(
chunk_id: str = typer.Argument(..., help="Chunk file or chunk id to blame"),
repo_path: str = typer.Option(".", "--repo", "-r", help="Path to the git repository"),
):
"""Git blame for a chunk."""
git = _git_integration(repo_path)
if not git.is_git_repo():
console.print(f"[red]Not a git repository: {Path(repo_path).resolve()}[/red]")
raise typer.Exit(1)

blame = git.get_chunk_blame(chunk_id)
if not blame:
console.print("[dim]No blame data available.[/dim]")
return

for row in blame:
console.print(
f"{row['line_start']:>4}-{row['line_end']:<4} {row['commit'][:8]} {row['author']} {row['date']}"
)
console.print(f" {row['content']}")


@git_app.command("install-hooks")
def git_install_hooks(
shelf_path: str = typer.Option("./shelf", "--shelf", "-s", help="Path to the shelf directory"),
repo_path: str = typer.Option(".", "--repo", "-r", help="Path to the git repository"),
):
"""Install git hooks for ShelfAI validation and rechunking."""
git = _git_integration(repo_path)
try:
git.install_hooks(shelf_path)
except RuntimeError as exc:
console.print(f"[red]{exc}[/red]")
raise typer.Exit(1)

console.print(f"[green]Installed ShelfAI hooks in {Path(repo_path).resolve()}[/green]")


@git_app.command("uninstall-hooks")
def git_uninstall_hooks(
repo_path: str = typer.Option(".", "--repo", "-r", help="Path to the git repository"),
):
"""Remove ShelfAI git hooks."""
git = _git_integration(repo_path)
if not git.is_git_repo():
console.print(f"[red]Not a git repository: {Path(repo_path).resolve()}[/red]")
raise typer.Exit(1)

git.uninstall_hooks()
console.print(f"[green]Removed ShelfAI hooks from {Path(repo_path).resolve()}[/green]")
15 changes: 1 addition & 14 deletions src/shelfai/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,8 @@
from shelfai.core.diff_report import ChunkDiff, ShelfDiffReport, compare_before_after, compare_shelves
from shelfai.core.migrate import MigrationPlan, MigrationStep, ShelfMigrator
from shelfai.core.priority import ChunkPriority, PriorityManager
"GitIntegration",

__all__ = [
"AnnotationManager",
"ChunkAnnotation",
"ChunkCompiler",
"CompileConfig",
"CompiledContext",
"ChunkDiff",
"ChunkPriority",
"ConditionalLoader",
"LoadCondition",
"LoadContext",
"MigrationPlan",
"MigrationStep",
"PriorityManager",
"ShelfDiffReport",
"ShelfMigrator",
"compare_before_after",
Expand Down
Loading
Loading