1use aurora_core::{AuroraResult, Pipeline, Value, AuroraError};
2use std::process::Command;
3use std::io::{self, Write, BufRead};
4
5fn check_git_installed() -> AuroraResult<()> {
6 let status = Command::new("git").arg("--version").output()
7 .map_err(|_| AuroraError::CommandNotFound(
8 "Git не установлен. Установи: sudo pacman -S git (Arch) / sudo apt install git (Debian) / brew install git (macOS)".into()
9 ))?;
10 if !status.status.success() {
11 return Err(AuroraError::CommandNotFound(
12 "Git не установлен. Установи: sudo pacman -S git (Arch) / sudo apt install git (Debian) / brew install git (macOS)".into()
13 ));
14 }
15 Ok(())
16}
17
18fn check_git_repo() -> AuroraResult<bool> {
19 Ok(Command::new("git").args(["rev-parse", "--git-dir"]).output()
20 .ok()
21 .is_some_and(|o| o.status.success()))
22}
23
24fn require_git_repo(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
25 if !check_git_repo()? {
26 return Err(AuroraError::NotFound(
27 localizer.t(
28 "Не git-репозиторий. Запусти `aurora dev init` или `git init`.",
29 "Not a git repository. Run `aurora dev init` or `git init` first."
30 ).to_string()
31 ));
32 }
33 Ok(())
34}
35
36fn check_git_config() -> AuroraResult<Option<(String, String)>> {
37 let name = Command::new("git").args(["config", "user.name"]).output().ok()
38 .and_then(|o| if o.status.success() {
39 String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
40 } else { None });
41
42 let email = Command::new("git").args(["config", "user.email"]).output().ok()
43 .and_then(|o| if o.status.success() {
44 String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
45 } else { None });
46
47 match (name, email) {
48 (Some(n), Some(e)) => Ok(Some((n, e))),
49 _ => Ok(None),
50 }
51}
52
53fn prompt(prompt_text: &str) -> AuroraResult<String> {
54 print!("{prompt_text}");
55 io::stdout().flush().ok();
56 let mut line = String::new();
57 io::stdin().lock().read_line(&mut line)
58 .map_err(|_| AuroraError::ModuleError("Не удалось прочитать ввод".into()))?;
59 Ok(line.trim().to_string())
60}
61
62fn prompt_yes_no(prompt_text: &str) -> AuroraResult<bool> {
63 let answer = prompt(&format!("{prompt_text} [y/N]: "))?;
64 Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes") || answer.eq_ignore_ascii_case("да"))
65}
66
67fn run_cmd(args: &[&str]) -> AuroraResult<(bool, String, String)> {
68 let output = Command::new("git").args(args).output()
69 .map_err(|e| AuroraError::ModuleError(format!("git failed: {e}")))?;
70 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
71 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
72 Ok((output.status.success(), stdout, stderr))
73}
74
75const GITIGNORE_TEMPLATE: &str = r#".gitignore # Локальный — не засоряет репозиторий
76
77# ===== Файлы сборки / зависимостей =====
78target/ # Rust: результат компиляции (cargo build)
79build/ # C/C++/Make: папка сборки
80dist/ # Собранные файлы для публикации
81node_modules/ # JavaScript: зависимости npm (их НЕ хранят в git)
82vendor/ # PHP / Go: сторонние библиотеки
83__pycache__/ # Python: кеш байткода
84*.pyc # Python: скомпилированные файлы
85.env # Локальные переменные (пароли, ключи API)
86
87# ===== IDE и редакторы =====
88.idea/ # JetBrains (IntelliJ IDEA, PyCharm, GoLand)
89.vscode/ # VS Code — только если там не общие настройки
90*.swp # Vim: временные файлы
91*.swo # Vim: временные файлы
92.DS_Store # macOS: служебные файлы Finder
93Thumbs.db # Windows: кеш миниатюр
94
95# ===== Логи и временные файлы =====
96*.log # Лог-файлы (их не коммитят)
97*.tmp # Временные файлы
98*.bak # Бэкапы
99"#;
100
101fn ensure_git_repo(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
102 if check_git_repo()? {
103 return Ok(());
104 }
105
106 println!();
107 println!("{}", localizer.t(
108 "⚠ Git-репозиторий не найден! Хочешь создать его?",
109 "⚠ Git repository not found! Would you like to create one?"
110 ));
111 println!(" {}", localizer.t(
112 "Это создаст папку .git и начнёт отслеживать изменения в проекте.",
113 "This creates a .git folder and starts tracking changes in your project."
114 ));
115
116 if !prompt_yes_no(localizer.t("Создать репозиторий", "Create repository"))? {
117 return Err(AuroraError::ModuleError(
118 localizer.t(
119 "Коммит отменён — нет репозитория. Создай вручную: git init",
120 "Commit cancelled — no repository. Create manually: git init"
121 ).to_string()
122 ));
123 }
124
125 let (ok, _, err) = run_cmd(&["init"])?;
126 if !ok {
127 return Err(AuroraError::ModuleError(
128 format!("git init failed: {err}")
129 ));
130 }
131
132 println!("{}", localizer.t("✓ Репозиторий создан!", "✓ Repository created!"));
133 println!();
134
135 if prompt_yes_no(localizer.t(
137 "Создать файл .gitignore? Он скрывает мусор (log, tmp, __pycache__...) из репозитория.",
138 "Create .gitignore? It hides junk files (logs, tmp, __pycache__...) from the repo."
139 ))? {
140 let gitignore_path = std::path::Path::new(".gitignore");
141 if gitignore_path.exists() {
142 println!(" .gitignore уже существует — пропускаем.");
143 } else {
144 std::fs::write(gitignore_path, GITIGNORE_TEMPLATE)
145 .map_err(|e| AuroraError::ModuleError(format!("Не удалось создать .gitignore: {e}")))?;
146 println!("{}", localizer.t(
147 "✓ .gitignore создан! Отредактируй его под свой проект.",
148 "✓ .gitignore created! Edit it for your project."
149 ));
150 }
151 }
152
153 Ok(())
154}
155
156fn ensure_git_config(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
157 if let Some((name, email)) = check_git_config()? {
158 println!(" {}: {name} <{email}>", localizer.t("Git настроен как", "Git configured as"));
159 return Ok(());
160 }
161
162 println!();
163 println!("{}", localizer.t(
164 "⚠ Git не знает твоё имя и email — они нужны для подписи коммитов.",
165 "⚠ Git doesn't know your name and email — needed to sign commits."
166 ));
167
168 let name = prompt(&localizer.t("Введи имя (как в подписи к коммиту): ", "Enter your name (as commit signature): "))?;
169 if name.is_empty() {
170 return Err(AuroraError::ModuleError(
171 localizer.t("Имя не может быть пустым. Коммит отменён.", "Name cannot be empty. Commit cancelled.").to_string()
172 ));
173 }
174
175 let email = prompt(&localizer.t("Введи email: ", "Enter your email: "))?;
176 if email.is_empty() {
177 return Err(AuroraError::ModuleError(
178 localizer.t("Email не может быть пустым. Коммит отменён.", "Email cannot be empty. Commit cancelled.").to_string()
179 ));
180 }
181
182 let (ok1, _, _) = run_cmd(&["config", "user.name", &name])?;
183 let (ok2, _, _) = run_cmd(&["config", "user.email", &email])?;
184
185 if ok1 && ok2 {
186 println!("{}", localizer.t("✓ Git настроен!", "✓ Git configured!"));
187 }
188
189 Ok(())
190}
191
192fn ensure_staged(localizer: &aurora_locale::Localizer) -> AuroraResult<bool> {
193 let (_, stdout, _) = run_cmd(&["status", "--short"])?;
194
195 if stdout.trim().is_empty() {
196 println!("{}", localizer.t(
197 "Нет изменений для коммита. Нечего коммитить.",
198 "No changes to commit. Nothing to commit."
199 ));
200 return Ok(false);
201 }
202
203 let lines: Vec<&str> = stdout.lines()
204 .filter(|l| !l.trim().is_empty())
205 .collect();
206
207 let staged: Vec<&&str> = lines.iter().filter(|l| {
210 let c = l.as_bytes().first().copied().unwrap_or(b' ');
211 c != b' ' && c != b'?'
212 }).collect();
213
214 let unstaged: Vec<&&str> = lines.iter().filter(|l| {
215 let c = l.as_bytes().first().copied().unwrap_or(b' ');
216 c == b' '
217 }).collect();
218
219 let untracked: Vec<&&str> = lines.iter().filter(|l| {
220 l.starts_with("??")
221 }).collect();
222
223 if !staged.is_empty() {
224 return Ok(true);
225 }
226
227 if !untracked.is_empty() || !unstaged.is_empty() {
228 let total = untracked.len() + unstaged.len();
229 println!(" {total} {}", localizer.t(
230 "файлов не добавлены в коммит. Нужно git add",
231 "files not staged. Need git add"
232 ));
233
234 if prompt_yes_no(localizer.t(
235 "Добавить ВСЕ (git add -A)?",
236 "Add ALL (git add -A)?"
237 ))? {
238 run_cmd(&["add", "-A"])?;
239 println!("{}", localizer.t(
240 "✓ Все файлы добавлены!",
241 "✓ All files staged!"
242 ));
243 return Ok(true);
244 }
245
246 println!(" {}", localizer.t(
247 "Коммит отменён. Добавь файлы: git add <file>",
248 "Commit cancelled. Stage files: git add <file>"
249 ));
250 return Ok(false);
251 }
252
253 Ok(false)
254}
255
256pub fn dev_commit(msg: &str, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
257 check_git_installed()?;
258
259 ensure_git_repo(localizer)?;
261
262 ensure_git_config(localizer)?;
264
265 if !ensure_staged(localizer)? {
267 return Ok(Pipeline::single(Value::String(
268 localizer.t("Коммит отменён.", "Commit cancelled.").to_string()
269 )));
270 }
271
272 let commit_msg = if msg.is_empty() || msg == "commit" {
274 println!();
275 println!("{}", localizer.t(
276 "Опиши, что изменилось (описание коммита):",
277 "Describe what changed (commit message):"
278 ));
279 prompt("> ")?
280 } else {
281 msg.to_string()
282 };
283
284 if commit_msg.is_empty() {
285 return Err(AuroraError::ModuleError(
286 localizer.t("Сообщение коммита не может быть пустым.", "Commit message cannot be empty.").to_string()
287 ));
288 }
289
290 let (ok, _stdout, stderr) = run_cmd(&["commit", "-m", &commit_msg])?;
292
293 if !ok {
294 return Err(AuroraError::ModuleError(
295 format!("{}: {stderr}",
296 localizer.t("Git commit не удался", "Git commit failed")
297 )
298 ));
299 }
300
301 println!();
302 println!("{}", localizer.t("✓ Закоммичено!", "✓ Committed!"));
303 println!(" {commit_msg}");
304
305 Ok(Pipeline::table(
306 vec!["status".into(), "message".into()],
307 vec![vec![
308 Value::String("committed".into()),
309 Value::String(commit_msg),
310 ]],
311 ))
312}
313
314pub fn dev_log(oneline: bool, graph: bool, n: Option<usize>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
315 check_git_installed()?;
316 require_git_repo(localizer)?;
317 let mut cmd = Command::new("git");
318 cmd.arg("log");
319 if graph { cmd.arg("--graph"); }
320 if let Some(count) = n { cmd.arg(format!("-{count}")); }
321 if oneline {
322 cmd.arg("--oneline");
323 } else {
324 cmd.arg("--format=COMMIT%x1e%H%x1f%an%x1f%ai%x1f%s");
325 }
326
327 let output = cmd.output()
328 .map_err(|e| AuroraError::ModuleError(format!("git log failed: {e}")))?;
329 if !output.status.success() {
330 let stderr = String::from_utf8_lossy(&output.stderr);
331 return Err(AuroraError::ModuleError(format!("git log: {stderr}")));
332 }
333
334 let stdout = String::from_utf8_lossy(&output.stdout);
335 let mut rows: Vec<Vec<Value>> = Vec::new();
336 for line in stdout.lines() {
337 if line.is_empty() { continue; }
338 if oneline {
339 let clean = if graph { line.split('\x1e').last().unwrap_or(line) } else { line };
340 if let Some(pos) = clean.find(' ') {
341 let hash = &clean[..pos];
342 let msg = clean[pos + 1..].trim();
343 rows.push(vec![
344 Value::String(hash.into()),
345 Value::String(String::new()),
346 Value::String(String::new()),
347 Value::String(msg.into()),
348 ]);
349 }
350 } else {
351 let clean = if graph { line.split('\x1e').last().unwrap_or(line) } else { line };
352 let parts: Vec<&str> = clean.splitn(4, '\x1f').collect();
353 if parts.len() >= 4 {
354 rows.push(vec![
355 Value::String(parts[0].into()),
356 Value::String(parts[1].into()),
357 Value::String(parts[2].into()),
358 Value::String(parts[3].into()),
359 ]);
360 }
361 }
362 }
363 Ok(Pipeline::table(
364 vec!["commit".into(), "author".into(), "date".into(), "message".into()],
365 rows,
366 ))
367}
368
369pub fn dev_status() -> AuroraResult<Pipeline> {
370 check_git_installed()?;
371 if !check_git_repo()? {
372 return Ok(Pipeline::single(Value::String(
373 "Not a git repository. Use 'aurora dev commit' to create one.".to_string()
374 )));
375 }
376 let output = Command::new("git").args(["status", "--short"]).output()
377 .map_err(|e| AuroraError::ModuleError(format!("git status failed: {e}")))?;
378 let stdout = String::from_utf8_lossy(&output.stdout);
379 let mut rows: Vec<Vec<Value>> = Vec::new();
380 for line in stdout.lines() {
381 let line = line.trim();
382 if line.is_empty() || line.len() <= 3 { continue; }
383 let status = &line[..2];
384 let file = &line[3..];
385 rows.push(vec![Value::String(status.trim().into()), Value::String(file.into())]);
386 }
387 Ok(Pipeline::table(vec!["status".into(), "file".into()], rows))
388}
389
390pub fn dev_diff(stat: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
391 check_git_installed()?;
392 require_git_repo(localizer)?;
393 let mut cmd = Command::new("git");
394 cmd.arg("diff");
395 if stat { cmd.arg("--stat"); }
396 let output = cmd.output()
397 .map_err(|e| AuroraError::ModuleError(format!("git diff failed: {e}")))?;
398 let stdout = String::from_utf8_lossy(&output.stdout);
399 let mut rows: Vec<Vec<Value>> = Vec::new();
400 if stat {
401 for line in stdout.lines() {
402 if line.is_empty() { continue; }
403 rows.push(vec![Value::String(line.into())]);
404 }
405 } else {
406 rows.push(vec![Value::String(stdout.into())]);
407 }
408 Ok(Pipeline::table(vec!["diff".into()], rows))
409}
410
411pub fn dev_push(remote: Option<&str>, branch: Option<&str>, upstream: bool, force: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
412 check_git_installed()?;
413 require_git_repo(localizer)?;
414
415 let remote = remote.unwrap_or("origin");
416 let current_branch = if branch.is_some() {
417 branch.unwrap().to_string()
418 } else {
419 let (ok, stdout, _) = run_cmd(&["rev-parse", "--abbrev-ref", "HEAD"])?;
420 if !ok {
421 return Err(AuroraError::ModuleError(
422 "Не удалось определить текущую ветку".into()
423 ));
424 }
425 stdout.trim().to_string()
426 };
427
428 let remote_str = remote.to_string();
429 let branch_str = current_branch.clone();
430
431 if force {
432 if !prompt_yes_no(&format!(
433 "{} {}/{}? {}",
434 localizer.t("⚠ FORCE PUSH на", "⚠ FORCE PUSH to"),
435 remote_str, branch_str,
436 localizer.t("История будет перезаписана! Продолжить?", "History will be rewritten! Continue?")
437 ))? {
438 return Ok(Pipeline::single(Value::String(localizer.t("Отменено.", "Cancelled.").to_string())));
439 }
440 }
441
442 println!("{} {}/{}", localizer.t("→ Пушим в", "→ Pushing to"), remote, current_branch);
443
444 let mut args = vec!["push"];
445 if upstream { args.push("-u"); }
446 if force { args.push("--force"); }
447 args.push(remote);
448 args.push(¤t_branch);
449
450 let (ok, stdout, stderr) = run_cmd(&args)?;
451
452 if !ok {
453 let msg = if stderr.contains("fatal:") && stderr.contains("repository") {
454 localizer.t(
455 "Репозиторий не найден. Проверь remote: aurora dev remote list",
456 "Repository not found. Check remote: aurora dev remote list"
457 )
458 } else if stderr.contains("fatal:") && stderr.contains("refspec") {
459 localizer.t(
460 "Ветка не найдена на удалённом репозитории. Попробуй --upstream или git push -u origin <ветка>",
461 "Branch not found on remote. Try --upstream or git push -u origin <branch>"
462 )
463 } else {
464 localizer.t(
465 "Git push не удался. Проверь remote и права доступа.",
466 "Git push failed. Check remote and access rights."
467 )
468 };
469 return Err(AuroraError::ModuleError(format!("{msg}\n{stderr}")));
470 }
471
472 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
473 let mut rows: Vec<Vec<Value>> = Vec::new();
474 for line in output.lines() {
475 let line = line.trim();
476 if line.is_empty() { continue; }
477 rows.push(vec![Value::String(line.into())]);
478 }
479
480 println!("{} {}/{}", localizer.t("✓ Запушено в", "✓ Pushed to"), remote, current_branch);
481 Ok(Pipeline::table(vec!["output".into()], rows))
482}
483
484pub fn dev_fetch(remote: Option<&str>, branch: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
485 check_git_installed()?;
486 require_git_repo(localizer)?;
487
488 let remote = remote.unwrap_or("origin");
489 let mut args = vec!["fetch", remote];
490 if let Some(b) = branch { args.push(b); }
491
492 println!("{} {}", localizer.t("→ Получаю из", "→ Fetching from"), remote);
493 let (ok, stdout, stderr) = run_cmd(&args)?;
494
495 if !ok {
496 return Err(AuroraError::ModuleError(
497 format!("git fetch: {stderr}")
498 ));
499 }
500
501 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
502 let mut rows: Vec<Vec<Value>> = Vec::new();
503 for line in output.lines() {
504 let line = line.trim();
505 if line.is_empty() { continue; }
506 rows.push(vec![Value::String(line.into())]);
507 }
508
509 println!("{} {}{}",
510 localizer.t("✓ Получено из", "✓ Fetched from"),
511 remote,
512 branch.map(|b| format!(" ({b})")).unwrap_or_default()
513 );
514 Ok(Pipeline::table(vec!["output".into()], rows))
515}
516
517pub fn dev_pull(remote: Option<&str>, branch: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
518 check_git_installed()?;
519 require_git_repo(localizer)?;
520
521 let remote = remote.unwrap_or("origin");
522 let current_branch = if branch.is_some() {
523 branch.unwrap().to_string()
524 } else {
525 let (ok, stdout, _) = run_cmd(&["rev-parse", "--abbrev-ref", "HEAD"])?;
526 if !ok {
527 return Err(AuroraError::ModuleError(
528 "Не удалось определить текущую ветку".into()
529 ));
530 }
531 stdout.trim().to_string()
532 };
533
534 println!("{} {}/{}", localizer.t("→ Тяну из", "→ Pulling from"), remote, current_branch);
535
536 let (ok, stdout, stderr) = run_cmd(&["pull", remote, ¤t_branch])?;
537 if !ok {
538 return Err(AuroraError::ModuleError(format!("git pull: {stderr}")));
539 }
540
541 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
542 let mut rows: Vec<Vec<Value>> = Vec::new();
543 for line in output.lines() {
544 let line = line.trim();
545 if line.is_empty() { continue; }
546 rows.push(vec![Value::String(line.into())]);
547 }
548
549 Ok(Pipeline::table(vec!["output".into()], rows))
550}
551
552pub fn dev_clone(url: &str, target: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
553 check_git_installed()?;
554
555 let dir = target.unwrap_or("");
556 println!("{} {} {}",
557 localizer.t("→ Клонирую", "→ Cloning"),
558 url,
559 if dir.is_empty() { "".into() } else { format!("→ {dir}") }
560 );
561
562 let mut args = vec!["clone", url];
563 if !dir.is_empty() { args.push(dir); }
564
565 let (ok, _stdout, stderr) = run_cmd(&args)?;
566 if !ok {
567 return Err(AuroraError::ModuleError(format!("git clone: {stderr}")));
568 }
569
570 println!("{}", localizer.t("✓ Репозиторий склонирован!", "✓ Repository cloned!"));
571 Ok(Pipeline::table(
572 vec!["url".into(), "target".into()],
573 vec![vec![Value::String(url.into()), Value::String(if dir.is_empty() { "." } else { dir }.into())]],
574 ))
575}
576
577pub fn dev_init(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
578 check_git_installed()?;
579
580 if check_git_repo()? {
581 return Err(AuroraError::ModuleError(
582 localizer.t("Репозиторий уже инициализирован.", "Repository already initialized.").to_string()
583 ));
584 }
585
586 let (ok, _, stderr) = run_cmd(&["init"])?;
587 if !ok {
588 return Err(AuroraError::ModuleError(format!("git init: {stderr}")));
589 }
590
591 println!("{}", localizer.t("✓ Репозиторий создан!", "✓ Repository created!"));
592
593 if prompt_yes_no(localizer.t(
595 "Создать .gitignore? Он скрывает мусор из репозитория.",
596 "Create .gitignore? It hides junk from the repo."
597 ))? {
598 let gitignore_path = std::path::Path::new(".gitignore");
599 if gitignore_path.exists() {
600 println!(" .gitignore уже существует — пропускаем.");
601 } else {
602 std::fs::write(gitignore_path, GITIGNORE_TEMPLATE)
603 .map_err(|e| AuroraError::ModuleError(format!("Не удалось создать .gitignore: {e}")))?;
604 println!("{}", localizer.t("✓ .gitignore создан!", "✓ .gitignore created!"));
605 }
606 }
607
608 if let Some((name, email)) = check_git_config()? {
610 println!(" {}: {name} <{email}>", localizer.t("Git настроен как", "Git configured as"));
611 } else if prompt_yes_no(localizer.t(
612 "Настроить имя и email для git?",
613 "Set up your git name and email?"
614 ))? {
615 let name = prompt(&localizer.t("Имя: ", "Name: "))?;
616 let email = prompt(&localizer.t("Email: ", "Email: "))?;
617 if !name.is_empty() && !email.is_empty() {
618 run_cmd(&["config", "user.name", &name])?;
619 run_cmd(&["config", "user.email", &email])?;
620 println!("{}", localizer.t("✓ Git настроен!", "✓ Git configured!"));
621 }
622 }
623
624 Ok(Pipeline::table(
625 vec!["status".into()],
626 vec![vec![Value::String("initialized".into())]],
627 ))
628}
629
630pub fn dev_add(files: &[String], localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
631 check_git_installed()?;
632 require_git_repo(localizer)?;
633
634 let (ok, stdout, stderr) = if files.is_empty() {
635 run_cmd(&["add", "-A"])?
636 } else {
637 let mut args = vec!["add"];
638 for f in files { args.push(f); }
639 run_cmd(&args)?
640 };
641
642 if !ok {
643 return Err(AuroraError::ModuleError(format!("git add: {stderr}")));
644 }
645
646 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
647 let mut rows: Vec<Vec<Value>> = Vec::new();
648 for line in output.lines() {
649 let line = line.trim();
650 if line.is_empty() { continue; }
651 rows.push(vec![Value::String(line.into())]);
652 }
653
654 println!("{}", localizer.t("✓ Файлы добавлены!", "✓ Files staged!"));
655 Ok(Pipeline::table(vec!["output".into()], rows))
656}
657
658pub fn dev_checkout(target: Option<&str>, create: bool, orphan: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
659 check_git_installed()?;
660 require_git_repo(localizer)?;
661
662 let mut args = vec!["checkout"];
663 if orphan {
664 args.push("--orphan");
665 let branch = target.unwrap_or("");
666 if branch.is_empty() {
667 return Err(AuroraError::InvalidInput(
668 localizer.t("Укажи имя ветки: aurora dev checkout --orphan <имя>", "Specify branch name: aurora dev checkout --orphan <name>").to_string()
669 ));
670 }
671 args.push(branch);
672 let (ok, stdout, stderr) = run_cmd(&args)?;
673 if !ok { return Err(AuroraError::ModuleError(format!("git checkout --orphan: {stderr}"))); }
674 println!("{} {}", localizer.t("✓ Создана orphan-ветка", "✓ Created orphan branch"), branch);
675 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
676 return Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(output.trim().into())]]));
677 }
678
679 let target = target.unwrap_or("");
680 if create && !target.is_empty() { args.push("-b"); }
681 if !target.is_empty() { args.push(target); }
682
683 let (ok, stdout, stderr) = run_cmd(&args)?;
684 if !ok {
685 let hint = if stderr.contains("already exists") {
686 localizer.t("Ветка уже существует. Используй dev checkout <ветка> без --create.", "Branch exists. Use dev checkout <branch> without --create.")
687 } else if stderr.contains("did not match any") {
688 localizer.t("Ветка не найдена. Используй --create чтобы создать.", "Branch not found. Use --create to create it.")
689 } else { "" };
690 return Err(AuroraError::ModuleError(format!("git checkout: {stderr}{}", if hint.is_empty() { "".into() } else { format!("\n{hint}") })));
691 }
692
693 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
694 let mut rows: Vec<Vec<Value>> = Vec::new();
695 for line in output.lines() {
696 let line = line.trim();
697 if line.is_empty() { continue; }
698 rows.push(vec![Value::String(line.into())]);
699 }
700
701 println!("{} {} → {}", localizer.t("✓ Переключено на", "✓ Switched to"), target,
702 if create { localizer.t("(новая ветка)", "(new branch)") } else { "" });
703 Ok(Pipeline::table(vec!["output".into()], rows))
704}
705
706pub fn dev_stash_push(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
707 check_git_installed()?;
708 require_git_repo(localizer)?;
709 let (ok, _stdout, stderr) = run_cmd(&["stash"])?;
710 if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
711 println!("{}", localizer.t("✓ Изменения спрятаны!", "✓ Changes stashed!"));
712 Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(stderr.trim().into())]]))
713}
714
715pub fn dev_stash_list(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
716 check_git_installed()?;
717 require_git_repo(localizer)?;
718 let (ok, stdout, stderr) = run_cmd(&["stash", "list"])?;
719 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
720 if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
721 let mut rows: Vec<Vec<Value>> = Vec::new();
722 for line in output.lines() { let l = line.trim(); if l.is_empty() { continue; } rows.push(vec![Value::String(l.into())]); }
723 Ok(Pipeline::table(vec!["stash".into()], rows))
724}
725
726pub fn dev_stash_pop(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
727 check_git_installed()?;
728 require_git_repo(localizer)?;
729 let (ok, stdout, stderr) = run_cmd(&["stash", "pop"])?;
730 if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
731 println!("{}", localizer.t("✓ Изменения достаны!", "✓ Changes popped!"));
732 let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
733 Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(output.trim().into())]]))
734}
735
736pub fn dev_reset(files: &[String], hard: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
737 check_git_installed()?;
738 require_git_repo(localizer)?;
739
740 if hard {
741 if !prompt_yes_no(&localizer.t(
742 "⚠ HARD RESET — все несохранённые изменения будут потеряны! Продолжить?",
743 "⚠ HARD RESET — all unsaved changes will be lost! Continue?"
744 ))? {
745 return Ok(Pipeline::single(Value::String(localizer.t("Отменено.", "Cancelled.").to_string())));
746 }
747 let (ok, _, stderr) = run_cmd(&["reset", "--hard"])?;
748 if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
749 println!("{}", localizer.t("✓ Жёсткий сброс выполнен!", "✓ Hard reset done!"));
750 } else if files.is_empty() {
751 let (ok, _, stderr) = run_cmd(&["reset"])?;
752 if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
753 println!("{}", localizer.t("✓ Все файлы сняты с индекса!", "✓ All files unstaged!"));
754 } else {
755 let mut args = vec!["reset"];
756 for f in files { args.push(f); }
757 let (ok, _, stderr) = run_cmd(&args)?;
758 if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
759 }
760
761 Ok(Pipeline::table(
762 vec!["status".into()],
763 vec![vec![Value::String(if hard { "hard reset" } else { "reset" }.into())]],
764 ))
765}
766
767pub fn dev_config(key: Option<&str>, value: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
768 check_git_installed()?;
769
770 match (key, value) {
771 (Some(k), Some(v)) => {
772 let (ok, _, stderr) = run_cmd(&["config", k, v])?;
773 if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
774 println!("{} {k} = {v}", localizer.t("✓ Установлено:", "✓ Set:"));
775 Ok(Pipeline::table(
776 vec!["key".into(), "value".into()],
777 vec![vec![Value::String(k.into()), Value::String(v.into())]],
778 ))
779 }
780 (Some(k), None) => {
781 let (ok, stdout, stderr) = run_cmd(&["config", k])?;
782 if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
783 Ok(Pipeline::table(
784 vec!["key".into(), "value".into()],
785 vec![vec![Value::String(k.into()), Value::String(stdout.trim().into())]],
786 ))
787 }
788 (None, None) => {
789 let (ok, stdout, stderr) = run_cmd(&["config", "--list"])?;
790 if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
791 let mut rows: Vec<Vec<Value>> = Vec::new();
792 for line in stdout.lines() {
793 let line = line.trim();
794 if line.is_empty() { continue; }
795 if let Some(eq) = line.find('=') {
796 rows.push(vec![Value::String(line[..eq].into()), Value::String(line[eq+1..].into())]);
797 }
798 }
799 Ok(Pipeline::table(vec!["key".into(), "value".into()], rows))
800 }
801 (None, Some(_)) => Err(AuroraError::InvalidInput(
802 "нельзя указать значение без ключа. Используй: aurora dev config <key> <value>".into()
803 )),
804 }
805}
806
807pub fn dev_remote_add(name: Option<&str>, url: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
808 check_git_installed()?;
809 require_git_repo(localizer)?;
810
811 let remote_name = name.unwrap_or("origin");
812
813 let repo_url = match url {
814 Some(u) => u.to_string(),
815 None => {
816 println!("{}", localizer.t(
817 "Введи URL репозитория (например https://github.com/user/repo.git):",
818 "Enter repository URL (e.g. https://github.com/user/repo.git):"
819 ));
820 prompt("> ")?
821 }
822 };
823
824 if repo_url.is_empty() {
825 return Err(AuroraError::ModuleError(
826 localizer.t("URL не может быть пустым.", "URL cannot be empty.").to_string()
827 ));
828 }
829
830 let (exists_ok, remotes, _) = run_cmd(&["remote", "get-url", remote_name])?;
832 if exists_ok {
833 if !prompt_yes_no(&format!(
834 "{} {}: {}. {}?",
835 localizer.t("Remote", "Remote"),
836 remote_name,
837 remotes.trim(),
838 localizer.t("Перезаписать", "Overwrite")
839 ))? {
840 return Ok(Pipeline::single(Value::String(
841 localizer.t("Отменено.", "Cancelled.").to_string()
842 )));
843 }
844 run_cmd(&["remote", "set-url", remote_name, &repo_url])?;
845 println!("{} {} = {repo_url}", localizer.t("✓ Remote обновлён:", "✓ Remote updated:"), remote_name);
846 } else {
847 run_cmd(&["remote", "add", remote_name, &repo_url])?;
848 println!("{} {} = {repo_url}", localizer.t("✓ Remote добавлен:", "✓ Remote added:"), remote_name);
849 }
850
851 Ok(Pipeline::table(
852 vec!["remote".into(), "url".into()],
853 vec![vec![Value::String(remote_name.into()), Value::String(repo_url)]],
854 ))
855}
856
857pub fn dev_remote_list(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
858 check_git_installed()?;
859 require_git_repo(localizer)?;
860
861 let (ok, stdout, stderr) = run_cmd(&["remote", "-v"])?;
862 if !ok {
863 return Err(AuroraError::ModuleError(
864 format!("git remote: {stderr}")
865 ));
866 }
867
868 let mut rows: Vec<Vec<Value>> = Vec::new();
869 for line in stdout.lines() {
870 let line = line.trim();
871 if line.is_empty() { continue; }
872 let parts: Vec<&str> = line.splitn(2, '\t').collect();
873 if parts.len() == 2 {
874 rows.push(vec![
875 Value::String(parts[0].into()),
876 Value::String(parts[1].into()),
877 ]);
878 }
879 }
880
881 if rows.is_empty() {
882 println!("{}", localizer.t(
883 "Нет удалённых репозиториев. Добавь: aurora dev remote add <url>",
884 "No remotes configured. Add one: aurora dev remote add <url>"
885 ));
886 }
887
888 Ok(Pipeline::table(vec!["remote".into(), "url".into()], rows))
889}
890
891pub fn dev_remote_remove(name: &str, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
892 check_git_installed()?;
893 require_git_repo(localizer)?;
894
895 let (ok, _, stderr) = run_cmd(&["remote", "remove", name])?;
896 if !ok {
897 let hint = if stderr.contains("No such remote") {
898 format!(". {}",
899 localizer.t("Проверь список: aurora dev remote list", "Check the list: aurora dev remote list")
900 )
901 } else { String::new() };
902 return Err(AuroraError::ModuleError(format!("git remote remove: {stderr}{hint}")));
903 }
904
905 println!("{} {}", localizer.t("✓ Remote удалён:", "✓ Remote removed:"), name);
906 Ok(Pipeline::table(
907 vec!["remote".into(), "action".into()],
908 vec![vec![Value::String(name.into()), Value::String("removed".into())]],
909 ))
910}
911
912pub fn dev_branch(delete: bool, rename: bool, name: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
913 check_git_installed()?;
914 require_git_repo(localizer)?;
915
916 if delete {
917 let branch = name.unwrap_or("");
918 if branch.is_empty() {
919 return Err(AuroraError::InvalidInput(
920 localizer.t("Укажи ветку для удаления: aurora dev branch -D <имя>", "Specify branch to delete: aurora dev branch -D <name>").to_string()
921 ));
922 }
923 let (ok, _, stderr) = run_cmd(&["branch", "-D", branch])?;
924 if !ok { return Err(AuroraError::ModuleError(format!("git branch -D: {stderr}"))); }
925 println!("{} {}", localizer.t("✓ Ветка удалена:", "✓ Branch deleted:"), branch);
926 return Ok(Pipeline::table(
927 vec!["branch".into(), "action".into()],
928 vec![vec![Value::String(branch.into()), Value::String("deleted".into())]],
929 ));
930 }
931
932 if rename {
933 let new_name = name.unwrap_or("");
934 if new_name.is_empty() {
935 return Err(AuroraError::InvalidInput(
936 localizer.t("Укажи новое имя: aurora dev branch -m <новое-имя>", "Specify new name: aurora dev branch -m <new-name>").to_string()
937 ));
938 }
939 let (ok, _, stderr) = run_cmd(&["branch", "-m", new_name])?;
940 if !ok { return Err(AuroraError::ModuleError(format!("git branch -m: {stderr}"))); }
941 println!("{} {}", localizer.t("✓ Ветка переименована в:", "✓ Branch renamed to:"), new_name);
942 return Ok(Pipeline::table(
943 vec!["branch".into(), "action".into()],
944 vec![vec![Value::String(new_name.into()), Value::String("renamed".into())]],
945 ));
946 }
947
948 let output = Command::new("git").arg("branch").output()
949 .map_err(|e| AuroraError::ModuleError(format!("git branch failed: {e}")))?;
950 let stdout = String::from_utf8_lossy(&output.stdout);
951 let mut rows: Vec<Vec<Value>> = Vec::new();
952 for line in stdout.lines() {
953 let line = line.trim();
954 if line.is_empty() { continue; }
955 let current = line.starts_with('*');
956 let name = if current { &line[2..] } else { line };
957 rows.push(vec![Value::String(name.trim().into()), Value::Bool(current)]);
958 }
959 Ok(Pipeline::table(vec!["branch".into(), "current".into()], rows))
960}