Skip to main content

ralph/cli/queue/
sort.rs

1//! Queue sort subcommand.
2//!
3//! Responsibilities:
4//! - Reorder tasks in .ralph/queue.jsonc by priority.
5//! - Support dry-run mode to preview the new order.
6//!
7//! Not handled here:
8//! - Time-based or complex sorting (use `ralph queue list --sort-by` for that).
9//! - Sorting by arbitrary fields (intentionally limited to prevent footguns).
10//!
11//! Invariants/assumptions:
12//! - Only supports priority sorting to avoid dangerous queue reordering.
13//! - Mutates queue.json; use with care in collaborative environments.
14//! - Dry-run mode does NOT create undo snapshots or write to disk.
15
16use anyhow::Result;
17use clap::Args;
18
19use crate::config::Resolved;
20use crate::queue;
21
22use super::{QueueSortBy, QueueSortOrder};
23
24/// Arguments for `ralph queue sort`.
25#[derive(Args)]
26#[command(
27    after_long_help = "Examples:\n  ralph queue sort\n  ralph queue sort --order descending\n  ralph queue sort --order ascending\n  ralph queue sort --dry-run\n  ralph queue list --scheduled --sort-by scheduled_start --order ascending\n\nDry run:\n  Shows the new queue order without modifying files.\n\nNote:\n  - `ralph queue sort` reorders .ralph/queue.jsonc and intentionally supports priority only.\n  - For triage/time-based sorting without mutating files, use `ralph queue list --sort-by ...`."
28)]
29pub struct QueueSortArgs {
30    /// Sort by field (supported: priority only; reorders queue file).
31    #[arg(long, value_enum, default_value_t = QueueSortBy::Priority)]
32    pub sort_by: QueueSortBy,
33
34    /// Sort order (default: descending, highest priority first).
35    #[arg(long, value_enum, default_value_t = QueueSortOrder::Descending)]
36    pub order: QueueSortOrder,
37
38    /// Show what would change without writing to disk.
39    #[arg(long)]
40    pub dry_run: bool,
41}
42
43pub(crate) fn handle(resolved: &Resolved, force: bool, args: QueueSortArgs) -> Result<()> {
44    let _queue_lock = queue::acquire_queue_lock(&resolved.repo_root, "queue sort", force)?;
45
46    // Create undo snapshot before mutation (only if not dry-run)
47    if !args.dry_run {
48        crate::undo::create_undo_snapshot(resolved, &format!("queue sort by {}", args.sort_by))?;
49    }
50
51    let mut queue_file = queue::load_queue(&resolved.queue_path)?;
52
53    // Capture original order for dry-run comparison
54    let original_ids: Vec<String> = queue_file.tasks.iter().map(|t| t.id.clone()).collect();
55
56    match args.sort_by {
57        QueueSortBy::Priority => {
58            queue::sort_tasks_by_priority(&mut queue_file, args.order.is_descending());
59        }
60    }
61
62    if args.dry_run {
63        let new_ids: Vec<String> = queue_file.tasks.iter().map(|t| t.id.clone()).collect();
64        if original_ids == new_ids {
65            log::info!("Dry run: queue is already sorted (no changes).");
66        } else {
67            log::info!("Dry run: would reorder {} task(s).", new_ids.len());
68            log::info!("New order: {}", new_ids.join(", "));
69        }
70    } else {
71        queue::save_queue(&resolved.queue_path, &queue_file)?;
72        log::info!("Queue sorted by {} (order: {}).", args.sort_by, args.order);
73    }
74    Ok(())
75}