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

1from pathlib import Path 

2 

3import click 

4 

5from workstack.cli.core import discover_repo_context, ensure_workstacks_dir 

6from workstack.core.context import WorkstackContext 

7 

8 

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). 

20 

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 """ 

24 

25 click.echo("Debug mode is enabled by default while this feature is in development.\n") 

26 

27 def debug_print(msg: str) -> None: 

28 if debug: 

29 click.echo(click.style(msg, fg="bright_black")) 

30 

31 repo = discover_repo_context(ctx, Path.cwd()) 

32 workstacks_dir = ensure_workstacks_dir(repo) 

33 

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} 

38 

39 debug_print(f"Found {len(branches)} worktrees\n") 

40 

41 # Track workstacks eligible for deletion 

42 deletable: list[tuple[str, str, str, int]] = [] 

43 

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 

50 

51 # Skip detached HEAD 

52 if branch is None: 

53 debug_print(f"Skipping detached HEAD: {wt_path}") 

54 continue 

55 

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 

63 

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) 

67 

68 debug_print(f" → state={state}, pr_number={pr_number}, title={title}\n") 

69 

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)) 

74 

75 # Display results 

76 if not deletable: 

77 click.echo("No workstacks found that are safe to delete.") 

78 return 

79 

80 click.echo("Workstacks safe to delete:\n") 

81 

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") 

88 

89 click.echo(f" {name_part} {branch_part} - {state_part} ({pr_part})") 

90 click.echo(f"{cmd_part}\n")