1use crate::core::find_repo_root;
2use crate::response::{BatchOperationResult, BatchResponse};
3use serde::Deserialize;
4use std::io::{self, BufRead};
5
6const MAX_LINE_BYTES: usize = 1_048_576;
10
11fn read_capped_line<R: BufRead>(
16 reader: &mut R,
17 max_bytes: usize,
18) -> io::Result<Option<(Vec<u8>, bool)>> {
19 let mut buf = Vec::new();
20 let mut oversized = false;
21 let mut saw_any = false;
22 loop {
23 let available = match reader.fill_buf() {
24 Ok(b) => b,
25 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue,
26 Err(e) => return Err(e),
27 };
28 if available.is_empty() {
29 if !saw_any {
30 return Ok(None);
31 }
32 return Ok(Some((buf, oversized)));
33 }
34 saw_any = true;
35 let newline = available.iter().position(|&b| b == b'\n');
36 let chunk_len = newline.map_or(available.len(), |idx| idx);
37 let room = max_bytes.saturating_sub(buf.len());
38 let take = room.min(chunk_len);
39 buf.extend_from_slice(&available[..take]);
40 if chunk_len > room {
41 oversized = true;
42 }
43 match newline {
44 Some(idx) => {
45 reader.consume(idx + 1);
46 return Ok(Some((buf, oversized)));
47 }
48 None => {
49 let consumed = available.len();
50 reader.consume(consumed);
51 }
52 }
53 }
54}
55
56#[derive(Debug, Deserialize)]
58struct BatchOperation {
59 command: String,
60 #[serde(default)]
61 args: serde_json::Value,
62}
63
64pub fn execute(atomic: bool, dry_run: bool) -> Result<BatchResponse, crate::errors::LitError> {
65 let _repo_root = find_repo_root()?;
66
67 let stdin = io::stdin();
68 let mut reader = stdin.lock();
69 let mut operations: Vec<BatchOperation> = Vec::new();
70 while let Some((raw, oversized)) = read_capped_line(&mut reader, MAX_LINE_BYTES)
71 .map_err(|e| crate::errors::LitError::io(e.to_string()))?
72 {
73 if oversized {
76 continue;
77 }
78 let line = String::from_utf8_lossy(&raw);
79 let trimmed = line.trim();
80 if trimmed.is_empty() {
81 continue;
82 }
83 if let Ok(op) = serde_json::from_str(trimmed) {
84 operations.push(op);
85 }
86 }
87
88 if operations.is_empty() {
89 return Err("No operations provided on stdin (expected JSONL)".into());
90 }
91
92 let total = operations.len();
93 let mut results = Vec::with_capacity(total);
94 let mut succeeded = 0usize;
95 let mut failed = 0usize;
96
97 for (i, op) in operations.iter().enumerate() {
98 if dry_run {
99 results.push(BatchOperationResult {
100 index: i,
101 command: op.command.clone(),
102 status: "ok".to_string(),
103 result: Some(serde_json::json!({"dry_run": true})),
104 error: None,
105 });
106 succeeded += 1;
107 continue;
108 }
109
110 match execute_single_operation(op) {
111 Ok(value) => {
112 results.push(BatchOperationResult {
113 index: i,
114 command: op.command.clone(),
115 status: "ok".to_string(),
116 result: Some(value),
117 error: None,
118 });
119 succeeded += 1;
120 }
121 Err(e) => {
122 results.push(BatchOperationResult {
123 index: i,
124 command: op.command.clone(),
125 status: "error".to_string(),
126 result: None,
127 error: Some(e.internal_message().to_string()),
128 });
129 failed += 1;
130
131 if atomic {
132 for (j, op) in operations.iter().enumerate().skip(i + 1) {
135 results.push(BatchOperationResult {
136 index: j,
137 command: op.command.clone(),
138 status: "skipped".to_string(),
139 result: None,
140 error: Some("Skipped due to atomic rollback".to_string()),
141 });
142 }
143 break;
144 }
145 }
146 }
147 }
148
149 Ok(BatchResponse {
150 total,
151 succeeded,
152 failed,
153 atomic,
154 dry_run,
155 results,
156 })
157}
158
159fn execute_single_operation(
160 op: &BatchOperation,
161) -> Result<serde_json::Value, crate::errors::LitError> {
162 match op.command.as_str() {
163 "add" => {
164 let files: Vec<String> = op
165 .args
166 .get("files")
167 .and_then(|v| serde_json::from_value(v.clone()).ok())
168 .unwrap_or_default();
169 if files.is_empty() {
170 return Err("'files' argument required for add".into());
171 }
172 let resp = crate::commands::add::execute(files)?;
173 serde_json::to_value(&resp).map_err(|e| e.to_string().into())
174 }
175 "commit" => {
176 let message = op
177 .args
178 .get("message")
179 .and_then(|v| v.as_str())
180 .ok_or("'message' argument required for commit")?
181 .to_string();
182 let author = op
183 .args
184 .get("author")
185 .and_then(|v| v.as_str())
186 .map(|s| s.to_string());
187 let resp = crate::commands::commit::execute(message, author)?;
188 serde_json::to_value(&resp).map_err(|e| e.to_string().into())
189 }
190 "status" => {
191 let resp = crate::commands::status::execute()?;
192 serde_json::to_value(&resp).map_err(|e| e.to_string().into())
193 }
194 "branch" => {
195 let name = op
196 .args
197 .get("name")
198 .and_then(|v| v.as_str())
199 .map(|s| s.to_string());
200 let delete = op
201 .args
202 .get("delete")
203 .and_then(|v| v.as_bool())
204 .unwrap_or(false);
205 let all = op
206 .args
207 .get("all")
208 .and_then(|v| v.as_bool())
209 .unwrap_or(false);
210 let resp = crate::commands::branch::execute(name, delete, all)?;
211 serde_json::to_value(&resp).map_err(|e| e.to_string().into())
212 }
213 "checkout" => {
214 let target = op
215 .args
216 .get("target")
217 .and_then(|v| v.as_str())
218 .ok_or("'target' argument required for checkout")?
219 .to_string();
220 let b = op
221 .args
222 .get("create")
223 .and_then(|v| v.as_bool())
224 .unwrap_or(false);
225 let resp = crate::commands::checkout::execute(target, b)?;
226 serde_json::to_value(&resp).map_err(|e| e.to_string().into())
227 }
228 "log" => {
229 let count = op.args.get("count").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
230 let oneline = op
231 .args
232 .get("oneline")
233 .and_then(|v| v.as_bool())
234 .unwrap_or(false);
235 let resp = crate::commands::log::execute(count, oneline)?;
236 serde_json::to_value(&resp).map_err(|e| e.to_string().into())
237 }
238 "snapshot" => {
239 let message = op
240 .args
241 .get("message")
242 .and_then(|v| v.as_str())
243 .ok_or("'message' argument required for snapshot")?
244 .to_string();
245 let author = op
246 .args
247 .get("author")
248 .and_then(|v| v.as_str())
249 .map(|s| s.to_string());
250 let metadata = op.args.get("metadata").cloned();
251 let resp = crate::commands::snapshot::execute(message, author, metadata)?;
252 serde_json::to_value(&resp).map_err(|e| e.to_string().into())
253 }
254 _ => Err(format!("Unknown batch command: '{}'", op.command).into()),
255 }
256}