362 lines
12 KiB
Python
Executable File
362 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Command Line Interface for AP Git Worktree Automation
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import List
|
|
|
|
# Handle imports - support both package and direct execution
|
|
try:
|
|
from .worktree_manager import WorktreeManager
|
|
from .parallel_agent import ParallelAgent, ParallelTask, IssueParallelizer
|
|
from .task_runner import TaskRunner
|
|
except ImportError:
|
|
from worktree_manager import WorktreeManager
|
|
from parallel_agent import ParallelAgent, ParallelTask, IssueParallelizer
|
|
from task_runner import TaskRunner
|
|
|
|
|
|
def cmd_worktree_list(args):
|
|
"""List active worktrees"""
|
|
wm = WorktreeManager()
|
|
worktrees = wm.list_worktrees()
|
|
|
|
if args.json:
|
|
print(json.dumps([{
|
|
"path": w.path,
|
|
"branch": w.branch,
|
|
"commit": w.commit[:8] if w.commit else "",
|
|
"is_main": w.is_main,
|
|
"last_modified": w.last_modified.isoformat()
|
|
} for w in worktrees], indent=2))
|
|
else:
|
|
print(f"{'PATH':<50} {'BRANCH':<30} {'TYPE':<10}")
|
|
print("-" * 90)
|
|
for w in worktrees:
|
|
wtype = "main" if w.is_main else "worktree"
|
|
branch = w.branch[:28] if len(w.branch) > 28 else w.branch
|
|
path = w.path[:48] if len(w.path) > 48 else w.path
|
|
print(f"{path:<50} {branch:<30} {wtype:<10}")
|
|
|
|
|
|
def cmd_worktree_create(args):
|
|
"""Create a new worktree"""
|
|
wm = WorktreeManager()
|
|
|
|
try:
|
|
path = wm.create_worktree(
|
|
issue_number=args.issue,
|
|
description=args.description,
|
|
base_branch=args.base or "main"
|
|
)
|
|
print(f"✓ Created worktree for issue {args.issue}")
|
|
print(f" Path: {path}")
|
|
|
|
# Show status
|
|
status = wm.get_worktree_status(args.issue)
|
|
if status.get("exists"):
|
|
print(f" Branch: {status['branch']}")
|
|
except Exception as e:
|
|
print(f"✗ Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def cmd_worktree_clean(args):
|
|
"""Clean stale worktrees"""
|
|
wm = WorktreeManager()
|
|
|
|
if args.all:
|
|
# List all non-main worktrees and remove them
|
|
worktrees = wm.list_worktrees()
|
|
removed = []
|
|
for wt in worktrees:
|
|
if not wt.is_main:
|
|
import re
|
|
match = re.search(r'issue-(\d+)', wt.path)
|
|
if match:
|
|
wm.remove_worktree(match.group(1))
|
|
removed.append(wt.path)
|
|
else:
|
|
max_age = args.days or 7
|
|
removed = wm.clean_stale_worktrees(max_age)
|
|
|
|
print(f"✓ Removed {len(removed)} worktrees")
|
|
for r in removed:
|
|
print(f" - {r}")
|
|
|
|
|
|
def cmd_worktree_remove(args):
|
|
"""Remove a specific worktree"""
|
|
wm = WorktreeManager()
|
|
|
|
if wm.remove_worktree(args.issue):
|
|
print(f"✓ Removed worktree for issue {args.issue}")
|
|
else:
|
|
print(f"✗ No worktree found for issue {args.issue}")
|
|
sys.exit(1)
|
|
|
|
|
|
def cmd_worktree_status(args):
|
|
"""Show worktree status"""
|
|
wm = WorktreeManager()
|
|
status = wm.get_worktree_status(args.issue)
|
|
|
|
if not status.get("exists"):
|
|
print(f"No worktree found for issue {args.issue}")
|
|
sys.exit(1)
|
|
|
|
if args.json:
|
|
print(json.dumps(status, indent=2))
|
|
else:
|
|
print(f"Issue: {args.issue}")
|
|
print(f"Path: {status['path']}")
|
|
print(f"Branch: {status['branch']}")
|
|
print(f"Last commit: {status['last_commit']}")
|
|
print(f"Uncommitted changes: {'Yes' if status['has_uncommitted_changes'] else 'No'}")
|
|
if status['files_changed']:
|
|
print("Changed files:")
|
|
for f in status['files_changed']:
|
|
print(f" {f}")
|
|
|
|
|
|
def cmd_parallel_run(args):
|
|
"""Run tasks in parallel"""
|
|
agent = ParallelAgent(max_workers=args.workers)
|
|
|
|
# Load tasks from file if provided
|
|
if args.tasks_file:
|
|
with open(args.tasks_file) as f:
|
|
task_data = json.load(f)
|
|
|
|
tasks = []
|
|
for td in task_data:
|
|
task = ParallelTask(
|
|
task_id=td["task_id"],
|
|
issue_number=td["issue_number"],
|
|
task_type=td.get("task_type", "command"),
|
|
payload=td["payload"],
|
|
dependencies=td.get("dependencies", []),
|
|
priority=td.get("priority", 0),
|
|
timeout=td.get("timeout", 300),
|
|
auto_commit=td.get("auto_commit", False),
|
|
metadata=td.get("metadata", {})
|
|
)
|
|
tasks.append(task)
|
|
|
|
results = agent.run_parallel(tasks)
|
|
else:
|
|
# Simple command execution across issues
|
|
if not args.issues:
|
|
print("Error: No issues specified. Use --issues or --tasks-file")
|
|
sys.exit(1)
|
|
|
|
tasks = []
|
|
for issue in args.issues:
|
|
task = ParallelTask(
|
|
task_id=f"task-{issue}",
|
|
issue_number=issue,
|
|
task_type="command",
|
|
payload=args.command,
|
|
dependencies=[],
|
|
auto_commit=args.auto_commit
|
|
)
|
|
tasks.append(task)
|
|
|
|
results = agent.run_parallel(tasks)
|
|
|
|
# Output results
|
|
if args.json:
|
|
print(json.dumps(results, indent=2))
|
|
else:
|
|
print(f"\n✓ Parallel execution complete")
|
|
print(f" Total tasks: {results['stats']['total_tasks']}")
|
|
print(f" Successful: {results['stats']['successful']}")
|
|
print(f" Failed: {results['stats']['failed']}")
|
|
print(f" Conflicts: {results['stats']['conflicts']}")
|
|
|
|
if results['conflicts']:
|
|
print("\n⚠ Conflicts detected:")
|
|
for c in results['conflicts']:
|
|
print(f" - {c['details']}")
|
|
|
|
# Save report if requested
|
|
if args.output:
|
|
report = agent.generate_report()
|
|
Path(args.output).write_text(report)
|
|
print(f"\n✓ Report saved to {args.output}")
|
|
|
|
|
|
def cmd_demo(args):
|
|
"""Run a demo with 3 parallel issues"""
|
|
print("=" * 60)
|
|
print("ALLEGRO-PRIMUS Git Worktree Automation Demo")
|
|
print("=" * 60)
|
|
|
|
# Setup
|
|
wm = WorktreeManager()
|
|
parallelizer = IssueParallelizer()
|
|
|
|
# Define 3 issues to work on
|
|
issues = [
|
|
("42", "Fix login validation bug"),
|
|
("43", "Add user profile API endpoint"),
|
|
("44", "Update documentation for v2.0")
|
|
]
|
|
|
|
print("\n1. Registering issues for parallel work...")
|
|
for num, desc in issues:
|
|
parallelizer.register_issue(num, desc)
|
|
print(f" Issue #{num}: {desc}")
|
|
|
|
print("\n2. Creating worktrees...")
|
|
for num, desc in issues:
|
|
try:
|
|
path = wm.create_worktree(num, desc)
|
|
print(f" ✓ Issue #{num}: {path}")
|
|
except Exception as e:
|
|
print(f" ✗ Issue #{num}: {e}")
|
|
|
|
print("\n3. Listing worktrees...")
|
|
worktrees = wm.list_worktrees()
|
|
for wt in worktrees:
|
|
if not wt.is_main:
|
|
print(f" {wt.branch} @ {wt.path}")
|
|
|
|
print("\n4. Adding tasks to queue...")
|
|
# Add different tasks for each issue
|
|
parallelizer.add_task_to_issue(
|
|
"42",
|
|
["echo", "Running tests for login validation..."],
|
|
auto_commit=False
|
|
)
|
|
parallelizer.add_task_to_issue(
|
|
"43",
|
|
["echo", "Building API endpoint..."],
|
|
auto_commit=False
|
|
)
|
|
parallelizer.add_task_to_issue(
|
|
"44",
|
|
["echo", "Generating documentation..."],
|
|
auto_commit=False
|
|
)
|
|
print(" ✓ 3 tasks queued")
|
|
|
|
print("\n5. Running tasks in parallel...")
|
|
results = parallelizer.run()
|
|
|
|
print(f"\n6. Results:")
|
|
print(f" Total tasks: {results['stats']['total_tasks']}")
|
|
print(f" Successful: {results['stats']['successful']}")
|
|
print(f" Failed: {results['stats']['failed']}")
|
|
print(f" Worktrees used: {results['stats']['worktrees_used']}")
|
|
|
|
# Show individual results
|
|
for r in results['results']:
|
|
status = "✓" if r['success'] else "✗"
|
|
print(f"\n {status} Task {r['task_id']}")
|
|
print(f" Output: {r['stdout'].strip()[:50]}...")
|
|
print(f" Duration: {r['execution_time']:.2f}s")
|
|
|
|
print("\n7. Checking worktree status...")
|
|
for num, _ in issues:
|
|
status = wm.get_worktree_status(num)
|
|
print(f" Issue #{num}: {status.get('branch', 'N/A')}")
|
|
|
|
if not args.keep:
|
|
print("\n8. Cleaning up worktrees...")
|
|
for num, _ in issues:
|
|
if wm.remove_worktree(num):
|
|
print(f" ✓ Removed issue #{num}")
|
|
else:
|
|
print("\n8. Keeping worktrees (--keep specified)")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("Demo complete!")
|
|
print("=" * 60)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
prog="ap-git",
|
|
description="Allegro-Primus Git Worktree Automation"
|
|
)
|
|
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
|
|
# Worktree commands
|
|
worktree_parser = subparsers.add_parser("worktree", help="Worktree operations")
|
|
worktree_subparsers = worktree_parser.add_subparsers(dest="worktree_cmd")
|
|
|
|
# worktree list
|
|
wt_list = worktree_subparsers.add_parser("list", help="List worktrees")
|
|
wt_list.add_argument("--json", action="store_true", help="Output as JSON")
|
|
wt_list.set_defaults(func=cmd_worktree_list)
|
|
|
|
# worktree create
|
|
wt_create = worktree_subparsers.add_parser("create", help="Create worktree")
|
|
wt_create.add_argument("issue", help="Issue number")
|
|
wt_create.add_argument("--description", "-d", help="Issue description")
|
|
wt_create.add_argument("--base", "-b", default="main", help="Base branch")
|
|
wt_create.set_defaults(func=cmd_worktree_create)
|
|
|
|
# worktree clean
|
|
wt_clean = worktree_subparsers.add_parser("clean", help="Clean stale worktrees")
|
|
wt_clean.add_argument("--days", "-d", type=int, help="Max age in days")
|
|
wt_clean.add_argument("--all", action="store_true", help="Remove all worktrees")
|
|
wt_clean.set_defaults(func=cmd_worktree_clean)
|
|
|
|
# worktree remove
|
|
wt_remove = worktree_subparsers.add_parser("remove", help="Remove worktree")
|
|
wt_remove.add_argument("issue", help="Issue number")
|
|
wt_remove.set_defaults(func=cmd_worktree_remove)
|
|
|
|
# worktree status
|
|
wt_status = worktree_subparsers.add_parser("status", help="Worktree status")
|
|
wt_status.add_argument("issue", help="Issue number")
|
|
wt_status.add_argument("--json", action="store_true", help="Output as JSON")
|
|
wt_status.set_defaults(func=cmd_worktree_status)
|
|
|
|
# Parallel commands
|
|
parallel_parser = subparsers.add_parser("parallel", help="Parallel execution")
|
|
parallel_subparsers = parallel_parser.add_subparsers(dest="parallel_cmd")
|
|
|
|
# parallel run
|
|
parallel_run = parallel_subparsers.add_parser("run", help="Run tasks in parallel")
|
|
parallel_run.add_argument("--tasks-file", "-f", help="JSON file with tasks")
|
|
parallel_run.add_argument("--issues", "-i", nargs="+", help="Issue numbers")
|
|
parallel_run.add_argument("--command", "-c", nargs="+", help="Command to run")
|
|
parallel_run.add_argument("--workers", "-w", type=int, default=4, help="Max workers")
|
|
parallel_run.add_argument("--output", "-o", help="Output report file")
|
|
parallel_run.add_argument("--json", action="store_true", help="Output as JSON")
|
|
parallel_run.add_argument("--auto-commit", action="store_true", help="Auto-commit on success")
|
|
parallel_run.set_defaults(func=cmd_parallel_run)
|
|
|
|
# Demo command
|
|
demo_parser = subparsers.add_parser("demo", help="Run demo")
|
|
demo_parser.add_argument("--keep", action="store_true", help="Keep worktrees after demo")
|
|
demo_parser.set_defaults(func=cmd_demo)
|
|
|
|
# Parse and execute
|
|
args = parser.parse_args()
|
|
|
|
if not args.command:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
if args.command == "worktree" and not args.worktree_cmd:
|
|
worktree_parser.print_help()
|
|
sys.exit(1)
|
|
|
|
if args.command == "parallel" and not args.parallel_cmd:
|
|
parallel_parser.print_help()
|
|
sys.exit(1)
|
|
|
|
args.func(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|