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
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Steps in order:
- [draft fix-pr](#draft-fix-pr) — fix the current CI failures on a PR locally without pushing
- [draft continue](#draft-continue) — resume a stopped or failed run
- [draft delete](#draft-delete) — remove a single run
- [draft prune](#draft-prune) — bulk-delete finished runs
- [draft prune](#draft-prune) — bulk-delete non-running runs in the current project

### draft init

Expand Down Expand Up @@ -170,7 +170,7 @@ Continue reuses the config file recorded on creation. There is no way to switch

### draft delete

Remove a single run's state directory and its linked git worktree.
Remove a single run's state directory and its linked git worktree. On success, the output line names the resolved project, for example `deleted run 260521-212506 (project: draft)`.

```shell
draft delete <run-id>
Expand All @@ -183,13 +183,12 @@ draft delete <run-id>

### draft prune

Bulk-delete finished runs. By default operates on the current project.
Bulk-delete every run in scope except those actively running. By default operates on the current project.

```shell
draft prune
draft prune --yes
draft prune --dry-run
draft prune --all
draft prune --project NAME
draft prune --all-projects
draft prune --delete-branch
Expand All @@ -199,7 +198,6 @@ draft prune --delete-branch

- `--yes`, `-y` — skip the confirmation prompt
- `--dry-run` — print the selection and exit without deleting
- `--all` — include every non-active run regardless of finished status (not only successful ones)
- `--project NAME` — operate on the named project instead of the current one
- `--all-projects` — operate across every project under `~/.draft/runs/`; mutually exclusive with `--project`
- `--delete-branch` — also delete the local git branch for each pruned run
Expand Down
2 changes: 1 addition & 1 deletion src/draft/command_delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,5 @@ def run(args) -> int:
if result["branch_deleted"]:
print(f"deleted branch {result['branch']}")

print(f"deleted run {args.run_id}")
print(f"deleted run {args.run_id} (project: {run_dir.parent.name})")
return 0
68 changes: 38 additions & 30 deletions src/draft/command_prune.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

from draft import runs

_STATUS_ORDER = {"done": 0, "stopped": 1, "missing": 2, "corrupt": 3}


def register(subparsers):
p = subparsers.add_parser(
"prune",
help="Bulk-delete finished runs.",
description="Bulk-delete successfully finished runs. By default operates on the current project. See also: draft delete.",
help="Bulk-delete non-running runs in the current project.",
description="Bulk-delete every run in scope except those actively running. By default operates on the current project. See also: draft delete.",
allow_abbrev=False,
)
p.add_argument(
"--yes", "-y", action="store_true", help="Skip the confirmation prompt."
Expand All @@ -19,12 +22,6 @@ def register(subparsers):
action="store_true",
help="Print the selection and exit without deleting.",
)
p.add_argument(
"--all",
dest="include_all",
action="store_true",
help="Include every non-active run regardless of finished status.",
)
p.add_argument(
"--project",
metavar="NAME",
Expand Down Expand Up @@ -76,37 +73,43 @@ def _resolve_project_scope(args) -> list[Path] | int:
return runs.project_runs(name)


def _build_selection(candidates, *, include_all):
from draft.pipelines import CorruptStateError

def _build_selection(candidates):
selection = []
active = []
for run_dir in candidates:
if runs.is_run_active(run_dir):
status = runs.classify_run(run_dir)
if status == "running":
active.append(run_dir)
continue
state = runs.load_state(run_dir)
try:
finished = state is not None and runs.is_run_finished(state)
except CorruptStateError:
finished = True
if include_all or finished:
selection.append((run_dir, state))

selection.sort(key=lambda x: x[0].name, reverse=True)
selection.append((run_dir, state, status))

selection.sort(key=lambda t: t[0].name, reverse=True)
selection.sort(key=lambda t: _STATUS_ORDER[t[2]])
active.sort(key=lambda d: d.name, reverse=True)
return selection, active


def _print_selection(selection):
print("runs to delete:")
for run_dir, state in selection:
for run_dir, state, status in selection:
branch = "<unknown>"
if state is not None:
b = state.get("data", {}).get("branch")
if b:
branch = b
print(f" {run_dir.name} {branch}")
print(f" {run_dir.name} {status:<7} {branch}")


def _count_non_running_in_other_projects(current: str | None) -> int:
count = 0
for name in runs.all_project_names():
if name == current:
continue
for run_dir in runs.project_runs(name):
if runs.classify_run(run_dir) != "running":
count += 1
return count


def _confirm() -> bool:
Expand All @@ -121,12 +124,19 @@ def run(args) -> int:
if isinstance(scope, int):
return scope

selection, active = _build_selection(scope, include_all=args.include_all)
selection, active = _build_selection(scope)

if not selection:
print("no runs to prune")
if active:
print(f"skipped {len(active)} active")
if not getattr(args, "project", None) and not getattr(
args, "all_projects", False
):
current = runs.current_project_name()
other = _count_non_running_in_other_projects(current)
if other > 0:
print(
f"{other} non-running run(s) in other projects; pass --all-projects to include them"
)
print(f"deleted 0; skipped {len(active)} active")
return 0

_print_selection(selection)
Expand All @@ -147,7 +157,7 @@ def run(args) -> int:
return 0

n_deleted = 0
for run_dir, _ in selection:
for run_dir, _, _ in selection:
project = run_dir.parent.name
result = runs.delete_run(run_dir, delete_branch=args.delete_branch)
for warning in result["warnings"]:
Expand All @@ -164,7 +174,5 @@ def run(args) -> int:
pid = int(pid_file.read_text().strip())
print(f"skipped active run {run_dir.name} (pid {pid})")

print(
f"deleted {n_deleted}; skipped {len(active)} active; selected {len(selection)}"
)
print(f"deleted {n_deleted}; skipped {len(active)} active")
return 0
18 changes: 18 additions & 0 deletions src/draft/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,24 @@ def is_run_finished(state: dict) -> bool:
return all(s in state.get("completed", []) for s in expected_steps(state))


def classify_run(run_dir: Path) -> str:
if is_run_active(run_dir):
return "running"
state_path = run_dir / "state.json"
if not state_path.exists():
return "missing"
try:
state = json.loads(state_path.read_text())
except Exception:
return "corrupt"
from draft.pipelines import CorruptStateError

try:
return "done" if is_run_finished(state) else "stopped"
except CorruptStateError:
return "corrupt"


def find_original_run_on_branch(project: str, branch: str) -> Path | None:
candidates = []
for run_dir in project_runs(project):
Expand Down
Loading
Loading