Coverage for src/workstack/cli/commands/gc.py: 100%
47 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-19 09:31 -0400
1from pathlib import Path
3import click
5from workstack.cli.core import discover_repo_context, ensure_workstacks_dir
6from workstack.core.context import WorkstackContext
9@click.command("gc")
10@click.option(
11 "--debug",
12 is_flag=True,
13 # debug=True: Feature in development, keep debug output enabled by default for user feedback
14 default=True,
15 help="Show commands being executed.",
16)
17@click.pass_obj
18def gc_cmd(ctx: WorkstackContext, debug: bool) -> None:
19 """List workstacks that are safe to delete (merged/closed PRs).
21 Checks each worktree's branch for PRs that have been merged or closed on GitHub.
22 Does not actually delete anything - just prints what could be deleted.
23 """
25 click.echo("Debug mode is enabled by default while this feature is in development.\n")
27 def debug_print(msg: str) -> None:
28 if debug:
29 click.echo(click.style(msg, fg="bright_black"))
31 repo = discover_repo_context(ctx, Path.cwd())
32 workstacks_dir = ensure_workstacks_dir(repo)
34 # Get all worktree branches
35 debug_print("$ git worktree list --porcelain")
36 worktrees = ctx.git_ops.list_worktrees(repo.root)
37 branches = {wt.path: wt.branch for wt in worktrees}
39 debug_print(f"Found {len(branches)} worktrees\n")
41 # Track workstacks eligible for deletion
42 deletable: list[tuple[str, str, str, int]] = []
44 # Check each worktree (skip root repo)
45 for wt_path, branch in branches.items():
46 # Skip root repo
47 if wt_path == repo.root:
48 debug_print(f"Skipping root repo: {wt_path}")
49 continue
51 # Skip detached HEAD
52 if branch is None:
53 debug_print(f"Skipping detached HEAD: {wt_path}")
54 continue
56 # Check if this is a managed workstack
57 if not wt_path.parent == workstacks_dir:
58 debug_print(
59 f"Skipping non-managed worktree: {wt_path} "
60 f"(parent: {wt_path.parent}, expected: {workstacks_dir})"
61 )
62 continue
64 # Get PR status
65 debug_print(f"Checking PR status for {wt_path.name} [{branch}]...")
66 state, pr_number, title = ctx.github_ops.get_pr_status(repo.root, branch, debug=debug)
68 debug_print(f" → state={state}, pr_number={pr_number}, title={title}\n")
70 # Check if PR is merged or closed
71 if state in ("MERGED", "CLOSED") and pr_number is not None:
72 name = wt_path.name
73 deletable.append((name, branch, state, pr_number))
75 # Display results
76 if not deletable:
77 click.echo("No workstacks found that are safe to delete.")
78 return
80 click.echo("Workstacks safe to delete:\n")
82 for name, branch, state, pr_number in deletable:
83 name_part = click.style(name, fg="cyan", bold=True)
84 branch_part = click.style(f"[{branch}]", fg="yellow")
85 state_part = click.style(state.lower(), fg="green" if state == "MERGED" else "red")
86 pr_part = click.style(f"PR #{pr_number}", fg="bright_black")
87 cmd_part = click.style(f"workstack rm {name}", fg="bright_black")
89 click.echo(f" {name_part} {branch_part} - {state_part} ({pr_part})")
90 click.echo(f" → {cmd_part}\n")