sift_queue/cli/commands/
collect.rs1use 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
40pub 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}