fix: workspaces rm

This commit is contained in:
Waylon Walker 2025-11-25 20:38:16 -06:00
parent 42d034eef2
commit c152b4a899

View file

@ -669,6 +669,60 @@ def has_unpushed_commits(repo_path: Path, branch: str) -> bool:
return count > 0
def is_branch_integrated_into_main(
repo_path: Path,
branch: str,
main_ref: str = "origin/main",
) -> bool:
"""
Heuristic: is `branch`'s content already in `main_ref`?
Returns True if:
- branch tip is an ancestor of main_ref (normal merge / FF / rebase-before-merge), OR
- the branch tip's tree hash appears somewhere in main_ref's history
(common for squash merges and cherry-picks).
This does not know about PRs, only Git history.
"""
# Make sure we have latest main (best-effort; ignore fetch errors)
_code, _out, _err = run_cmd(["git", "fetch", "--quiet", "origin"], cwd=repo_path)
# 1) Simple case: branch is ancestor of main_ref
code, _out, _err = run_cmd(
["git", "merge-base", "--is-ancestor", branch, main_ref],
cwd=repo_path,
)
if code == 0:
return True
# 2) Squash / cherry-pick heuristic: same tree exists on main_ref
code, out, err = run_cmd(
["git", "show", "-s", "--format=%T", branch],
cwd=repo_path,
)
if code != 0:
console.print(
f"[red]Failed to get tree for branch {branch} in {repo_path.name}:[/red]\n{err}"
)
return False
branch_tree = out.strip()
if not branch_tree:
return False
code, out, err = run_cmd(
["git", "log", "--format=%T", main_ref],
cwd=repo_path,
)
if code != 0:
console.print(
f"[red]Failed to walk {main_ref} in {repo_path.name}:[/red]\n{err}"
)
return False
main_trees = {line.strip() for line in out.splitlines() if line.strip()}
return branch_tree in main_trees
@app.command("rm")
def remove_workspace(
ctx: typer.Context,
@ -687,6 +741,11 @@ def remove_workspace(
"-f",
help="Force removal even if there is dirty or unpushed work.",
),
main_ref: str = typer.Option(
"origin/main",
"--main-ref",
help="Ref to consider as the integration target (default: origin/main).",
),
):
"""
Remove a workspace:
@ -732,13 +791,19 @@ def remove_workspace(
for wt in worktrees:
status = get_git_status(wt)
branch = get_current_branch(wt) or "?"
repo = find_repo_for_worktree(wt, repos_dir)
integrated = False
if repo is not None and branch != "?":
integrated = is_branch_integrated_into_main(wt, branch, main_ref=main_ref)
if status.dirty:
problems.append(f"{wt.name}: dirty working tree on '{branch}'")
repo = find_repo_for_worktree(wt, repos_dir)
if repo is not None and branch != "?":
# Only care about "unpushe­d commits" if the branch is NOT integrated into main.
if repo is not None and branch != "?" and not integrated:
if has_unpushed_commits(wt, branch):
problems.append(f"{wt.name}: unpushed commits on '{branch}'")
if problems and not force:
console.print(
"[red]Refusing to remove workspace; found dirty or unpushed work:[/red]"
@ -770,10 +835,13 @@ def remove_workspace(
continue
console.print(f"Removing worktree for [bold]{display_name}[/bold]...")
code, _out, err = run_cmd(
["git", "worktree", "remove", "--force" if force else "--detach", str(wt)],
cwd=repo,
)
args = ["git", "worktree", "remove"]
if force:
args.append("--force")
args.append(str(wt))
code, _out, err = run_cmd(args, cwd=repo)
if code != 0:
console.print(
f"[red]Failed to remove worktree {display_name} via git:[/red]\n{err}"
@ -785,9 +853,8 @@ def remove_workspace(
)
shutil.rmtree(wt)
elif wt.exists():
# git worktree remove should delete the directory; if not, clean up.
# git worktree remove *should* delete the directory; if not, clean up.
shutil.rmtree(wt)
# Finally, remove the workspace directory itself
shutil.rmtree(ws_dir)
console.print(
@ -1435,3 +1502,5 @@ app.add_typer(tmux_app)
if __name__ == "__main__":
app()