Skip to main content

sift_queue/cli/commands/
collect.rs

1use crate::cli::help::{HelpDoc, HelpSection};
2use crate::collect::{detect_format, render_title, Format};
3use crate::queue::{parse_priority_value, NewItem, Queue, Source};
4use crate::CollectArgs;
5use anyhow::Result;
6use clap::builder::{StyledStr, Styles};
7use std::io::{IsTerminal, Read};
8use std::path::PathBuf;
9
10pub fn after_help(styles: &Styles) -> StyledStr {
11    HelpDoc::new()
12        .section(
13            HelpSection::new("Examples:")
14                .item(
15                    "rg --json PATTERN | sq collect --by-file --title-template \"review: {{filepath}}\"",
16                    "Group ripgrep matches by file with a custom title",
17                )
18                .item(
19                    "rg --json -n -C2 PATTERN | sq collect --by-file",
20                    "Preserve line numbers and nearby context in the collected text source",
21                ),
22        )
23        .section(
24            HelpSection::new("Templates:")
25                .item("{{filepath}}", "Full file path for the grouped result")
26                .item("{{filename}}", "Basename of {{filepath}}")
27                .item(
28                    "{{match_count}}",
29                    "Number of rg match events collected for the file",
30                )
31                .text("Default title template: {{match_count}}:{{filepath}}"),
32        )
33        .section(
34            HelpSection::new("Dependencies:")
35                .text("Use --blocked-by <id1,id2> to declare blockers for every created item."),
36        )
37        .render(styles)
38}
39
40/// Execute the `sq collect` command.
41pub fn execute(args: &CollectArgs, queue_path: PathBuf) -> Result<i32> {
42    if !args.by_file {
43        eprintln!("Error: collect requires a split mode (currently only --by-file is supported)");
44        return Ok(1);
45    }
46
47    if args.title.is_some() && args.title_template.is_some() {
48        eprintln!("Error: --title and --title-template are mutually exclusive");
49        return Ok(1);
50    }
51
52    if std::io::stdin().is_terminal() {
53        eprintln!("Error: sq collect --by-file expects piped stdin");
54        eprintln!("Try: rg --json PATTERN | sq collect --by-file");
55        return Ok(1);
56    }
57
58    let mut input = String::new();
59    std::io::stdin().read_to_string(&mut input)?;
60
61    if input.trim().is_empty() {
62        eprintln!("Error: no stdin input received");
63        eprintln!("Try: rg --json PATTERN | sq collect --by-file");
64        return Ok(1);
65    }
66
67    let format = match args.stdin_format.as_deref() {
68        Some("rg-json") => Format::RgJson,
69        Some(other) => {
70            eprintln!("Error: Unsupported stdin format: {}", other);
71            eprintln!("Currently supported: rg --json");
72            return Ok(1);
73        }
74        None => match detect_format(&input) {
75            Some(format) => format,
76            None => {
77                eprintln!("Error: could not detect a supported stdin format");
78                eprintln!("Currently supported: rg --json");
79                return Ok(1);
80            }
81        },
82    };
83
84    let grouped = match format {
85        Format::RgJson => match crate::collect::rg::parse_json(&input) {
86            Ok(items) => items,
87            Err(err) => {
88                eprintln!("Error: {}", err);
89                return Ok(1);
90            }
91        },
92    };
93
94    let priority = match &args.priority {
95        Some(value) => match parse_priority_value(value) {
96            Ok(priority) => Some(priority),
97            Err(err) => {
98                eprintln!("Error: {}", err);
99                return Ok(1);
100            }
101        },
102        None => None,
103    };
104
105    let metadata = match &args.metadata {
106        Some(json_str) => match serde_json::from_str(json_str) {
107            Ok(v) => v,
108            Err(e) => {
109                eprintln!("Error: Invalid JSON for metadata: {}", e);
110                return Ok(1);
111            }
112        },
113        None => serde_json::Value::Object(serde_json::Map::new()),
114    };
115
116    let blocked_by: Vec<String> = match &args.blocked_by {
117        Some(ids) => ids
118            .split(',')
119            .map(|s| s.trim().to_string())
120            .filter(|s| !s.is_empty())
121            .collect(),
122        None => Vec::new(),
123    };
124
125    let mut new_items = Vec::with_capacity(grouped.len());
126    for grouped_item in &grouped {
127        let title = match render_title(
128            args.title.as_deref(),
129            args.title_template.as_deref(),
130            grouped_item,
131        ) {
132            Ok(title) => title,
133            Err(err) => {
134                eprintln!("Error: {}", err);
135                return Ok(1);
136            }
137        };
138
139        new_items.push(NewItem {
140            sources: vec![
141                Source {
142                    type_: "file".to_string(),
143                    path: Some(grouped_item.filepath.clone()),
144                    content: None,
145                },
146                Source {
147                    type_: "text".to_string(),
148                    path: None,
149                    content: Some(grouped_item.text.clone()),
150                },
151            ],
152            title: Some(title),
153            description: args.description.clone(),
154            priority,
155            metadata: metadata.clone(),
156            blocked_by: blocked_by.clone(),
157        });
158    }
159
160    let queue = Queue::new(queue_path);
161    let items = queue.push_many_with_description(new_items)?;
162
163    if args.json {
164        let items = queue.items_with_computed_status(items);
165        let json_values: Vec<serde_json::Value> = items.iter().map(|i| i.to_json_value()).collect();
166        let json = serde_json::to_string_pretty(&json_values)?;
167        println!("{}", json);
168    } else {
169        for item in &items {
170            println!("{}", item.id);
171        }
172        eprintln!("Added {} item(s)", items.len());
173    }
174
175    Ok(0)
176}