use aurora_core::{AuroraResult, Pipeline, Value, AuroraError};
use std::process::Command;
use std::io::{self, Write, BufRead};
fn check_git_installed() -> AuroraResult<()> {
let status = Command::new("git").arg("--version").output()
.map_err(|_| AuroraError::CommandNotFound(
"Git не установлен. Установи: sudo pacman -S git (Arch) / sudo apt install git (Debian) / brew install git (macOS)".into()
))?;
if !status.status.success() {
return Err(AuroraError::CommandNotFound(
"Git не установлен. Установи: sudo pacman -S git (Arch) / sudo apt install git (Debian) / brew install git (macOS)".into()
));
}
Ok(())
}
fn check_git_repo() -> AuroraResult<bool> {
Ok(Command::new("git").args(["rev-parse", "--git-dir"]).output()
.ok()
.is_some_and(|o| o.status.success()))
}
fn require_git_repo(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
if !check_git_repo()? {
return Err(AuroraError::NotFound(
localizer.t(
"Не git-репозиторий. Запусти `aurora dev init` или `git init`.",
"Not a git repository. Run `aurora dev init` or `git init` first."
).to_string()
));
}
Ok(())
}
fn check_git_config() -> AuroraResult<Option<(String, String)>> {
let name = Command::new("git").args(["config", "user.name"]).output().ok()
.and_then(|o| if o.status.success() {
String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
} else { None });
let email = Command::new("git").args(["config", "user.email"]).output().ok()
.and_then(|o| if o.status.success() {
String::from_utf8(o.stdout).ok().map(|s| s.trim().to_string())
} else { None });
match (name, email) {
(Some(n), Some(e)) => Ok(Some((n, e))),
_ => Ok(None),
}
}
fn prompt(prompt_text: &str) -> AuroraResult<String> {
print!("{prompt_text}");
io::stdout().flush().ok();
let mut line = String::new();
io::stdin().lock().read_line(&mut line)
.map_err(|_| AuroraError::ModuleError("Не удалось прочитать ввод".into()))?;
Ok(line.trim().to_string())
}
fn prompt_yes_no(prompt_text: &str) -> AuroraResult<bool> {
let answer = prompt(&format!("{prompt_text} [y/N]: "))?;
Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes") || answer.eq_ignore_ascii_case("да"))
}
fn run_cmd(args: &[&str]) -> AuroraResult<(bool, String, String)> {
let output = Command::new("git").args(args).output()
.map_err(|e| AuroraError::ModuleError(format!("git failed: {e}")))?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok((output.status.success(), stdout, stderr))
}
const GITIGNORE_TEMPLATE: &str = r#".gitignore # Локальный — не засоряет репозиторий
# ===== Файлы сборки / зависимостей =====
target/ # Rust: результат компиляции (cargo build)
build/ # C/C++/Make: папка сборки
dist/ # Собранные файлы для публикации
node_modules/ # JavaScript: зависимости npm (их НЕ хранят в git)
vendor/ # PHP / Go: сторонние библиотеки
__pycache__/ # Python: кеш байткода
*.pyc # Python: скомпилированные файлы
.env # Локальные переменные (пароли, ключи API)
# ===== IDE и редакторы =====
.idea/ # JetBrains (IntelliJ IDEA, PyCharm, GoLand)
.vscode/ # VS Code — только если там не общие настройки
*.swp # Vim: временные файлы
*.swo # Vim: временные файлы
.DS_Store # macOS: служебные файлы Finder
Thumbs.db # Windows: кеш миниатюр
# ===== Логи и временные файлы =====
*.log # Лог-файлы (их не коммитят)
*.tmp # Временные файлы
*.bak # Бэкапы
"#;
fn ensure_git_repo(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
if check_git_repo()? {
return Ok(());
}
println!();
println!("{}", localizer.t(
"⚠ Git-репозиторий не найден! Хочешь создать его?",
"⚠ Git repository not found! Would you like to create one?"
));
println!(" {}", localizer.t(
"Это создаст папку .git и начнёт отслеживать изменения в проекте.",
"This creates a .git folder and starts tracking changes in your project."
));
if !prompt_yes_no(localizer.t("Создать репозиторий", "Create repository"))? {
return Err(AuroraError::ModuleError(
localizer.t(
"Коммит отменён — нет репозитория. Создай вручную: git init",
"Commit cancelled — no repository. Create manually: git init"
).to_string()
));
}
let (ok, _, err) = run_cmd(&["init"])?;
if !ok {
return Err(AuroraError::ModuleError(
format!("git init failed: {err}")
));
}
println!("{}", localizer.t("✓ Репозиторий создан!", "✓ Repository created!"));
println!();
if prompt_yes_no(localizer.t(
"Создать файл .gitignore? Он скрывает мусор (log, tmp, __pycache__...) из репозитория.",
"Create .gitignore? It hides junk files (logs, tmp, __pycache__...) from the repo."
))? {
let gitignore_path = std::path::Path::new(".gitignore");
if gitignore_path.exists() {
println!(" .gitignore уже существует — пропускаем.");
} else {
std::fs::write(gitignore_path, GITIGNORE_TEMPLATE)
.map_err(|e| AuroraError::ModuleError(format!("Не удалось создать .gitignore: {e}")))?;
println!("{}", localizer.t(
"✓ .gitignore создан! Отредактируй его под свой проект.",
"✓ .gitignore created! Edit it for your project."
));
}
}
Ok(())
}
fn ensure_git_config(localizer: &aurora_locale::Localizer) -> AuroraResult<()> {
if let Some((name, email)) = check_git_config()? {
println!(" {}: {name} <{email}>", localizer.t("Git настроен как", "Git configured as"));
return Ok(());
}
println!();
println!("{}", localizer.t(
"⚠ Git не знает твоё имя и email — они нужны для подписи коммитов.",
"⚠ Git doesn't know your name and email — needed to sign commits."
));
let name = prompt(&localizer.t("Введи имя (как в подписи к коммиту): ", "Enter your name (as commit signature): "))?;
if name.is_empty() {
return Err(AuroraError::ModuleError(
localizer.t("Имя не может быть пустым. Коммит отменён.", "Name cannot be empty. Commit cancelled.").to_string()
));
}
let email = prompt(&localizer.t("Введи email: ", "Enter your email: "))?;
if email.is_empty() {
return Err(AuroraError::ModuleError(
localizer.t("Email не может быть пустым. Коммит отменён.", "Email cannot be empty. Commit cancelled.").to_string()
));
}
let (ok1, _, _) = run_cmd(&["config", "user.name", &name])?;
let (ok2, _, _) = run_cmd(&["config", "user.email", &email])?;
if ok1 && ok2 {
println!("{}", localizer.t("✓ Git настроен!", "✓ Git configured!"));
}
Ok(())
}
fn ensure_staged(localizer: &aurora_locale::Localizer) -> AuroraResult<bool> {
let (_, stdout, _) = run_cmd(&["status", "--short"])?;
if stdout.trim().is_empty() {
println!("{}", localizer.t(
"Нет изменений для коммита. Нечего коммитить.",
"No changes to commit. Nothing to commit."
));
return Ok(false);
}
let lines: Vec<&str> = stdout.lines()
.filter(|l| !l.trim().is_empty())
.collect();
let staged: Vec<&&str> = lines.iter().filter(|l| {
let c = l.as_bytes().first().copied().unwrap_or(b' ');
c != b' ' && c != b'?'
}).collect();
let unstaged: Vec<&&str> = lines.iter().filter(|l| {
let c = l.as_bytes().first().copied().unwrap_or(b' ');
c == b' '
}).collect();
let untracked: Vec<&&str> = lines.iter().filter(|l| {
l.starts_with("??")
}).collect();
if !staged.is_empty() {
return Ok(true);
}
if !untracked.is_empty() || !unstaged.is_empty() {
let total = untracked.len() + unstaged.len();
println!(" {total} {}", localizer.t(
"файлов не добавлены в коммит. Нужно git add",
"files not staged. Need git add"
));
if prompt_yes_no(localizer.t(
"Добавить ВСЕ (git add -A)?",
"Add ALL (git add -A)?"
))? {
run_cmd(&["add", "-A"])?;
println!("{}", localizer.t(
"✓ Все файлы добавлены!",
"✓ All files staged!"
));
return Ok(true);
}
println!(" {}", localizer.t(
"Коммит отменён. Добавь файлы: git add <file>",
"Commit cancelled. Stage files: git add <file>"
));
return Ok(false);
}
Ok(false)
}
pub fn dev_commit(msg: &str, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
ensure_git_repo(localizer)?;
ensure_git_config(localizer)?;
if !ensure_staged(localizer)? {
return Ok(Pipeline::single(Value::String(
localizer.t("Коммит отменён.", "Commit cancelled.").to_string()
)));
}
let commit_msg = if msg.is_empty() || msg == "commit" {
println!();
println!("{}", localizer.t(
"Опиши, что изменилось (описание коммита):",
"Describe what changed (commit message):"
));
prompt("> ")?
} else {
msg.to_string()
};
if commit_msg.is_empty() {
return Err(AuroraError::ModuleError(
localizer.t("Сообщение коммита не может быть пустым.", "Commit message cannot be empty.").to_string()
));
}
let (ok, _stdout, stderr) = run_cmd(&["commit", "-m", &commit_msg])?;
if !ok {
return Err(AuroraError::ModuleError(
format!("{}: {stderr}",
localizer.t("Git commit не удался", "Git commit failed")
)
));
}
println!();
println!("{}", localizer.t("✓ Закоммичено!", "✓ Committed!"));
println!(" {commit_msg}");
Ok(Pipeline::table(
vec!["status".into(), "message".into()],
vec![vec![
Value::String("committed".into()),
Value::String(commit_msg),
]],
))
}
pub fn dev_log(oneline: bool, graph: bool, n: Option<usize>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let mut cmd = Command::new("git");
cmd.arg("log");
if graph { cmd.arg("--graph"); }
if let Some(count) = n { cmd.arg(format!("-{count}")); }
if oneline {
cmd.arg("--oneline");
} else {
cmd.arg("--format=COMMIT%x1e%H%x1f%an%x1f%ai%x1f%s");
}
let output = cmd.output()
.map_err(|e| AuroraError::ModuleError(format!("git log failed: {e}")))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(AuroraError::ModuleError(format!("git log: {stderr}")));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in stdout.lines() {
if line.is_empty() { continue; }
if oneline {
let clean = if graph { line.split('\x1e').last().unwrap_or(line) } else { line };
if let Some(pos) = clean.find(' ') {
let hash = &clean[..pos];
let msg = clean[pos + 1..].trim();
rows.push(vec![
Value::String(hash.into()),
Value::String(String::new()),
Value::String(String::new()),
Value::String(msg.into()),
]);
}
} else {
let clean = if graph { line.split('\x1e').last().unwrap_or(line) } else { line };
let parts: Vec<&str> = clean.splitn(4, '\x1f').collect();
if parts.len() >= 4 {
rows.push(vec![
Value::String(parts[0].into()),
Value::String(parts[1].into()),
Value::String(parts[2].into()),
Value::String(parts[3].into()),
]);
}
}
}
Ok(Pipeline::table(
vec!["commit".into(), "author".into(), "date".into(), "message".into()],
rows,
))
}
pub fn dev_status() -> AuroraResult<Pipeline> {
check_git_installed()?;
if !check_git_repo()? {
return Ok(Pipeline::single(Value::String(
"Not a git repository. Use 'aurora dev commit' to create one.".to_string()
)));
}
let output = Command::new("git").args(["status", "--short"]).output()
.map_err(|e| AuroraError::ModuleError(format!("git status failed: {e}")))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() || line.len() <= 3 { continue; }
let status = &line[..2];
let file = &line[3..];
rows.push(vec![Value::String(status.trim().into()), Value::String(file.into())]);
}
Ok(Pipeline::table(vec!["status".into(), "file".into()], rows))
}
pub fn dev_diff(stat: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let mut cmd = Command::new("git");
cmd.arg("diff");
if stat { cmd.arg("--stat"); }
let output = cmd.output()
.map_err(|e| AuroraError::ModuleError(format!("git diff failed: {e}")))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut rows: Vec<Vec<Value>> = Vec::new();
if stat {
for line in stdout.lines() {
if line.is_empty() { continue; }
rows.push(vec![Value::String(line.into())]);
}
} else {
rows.push(vec![Value::String(stdout.into())]);
}
Ok(Pipeline::table(vec!["diff".into()], rows))
}
pub fn dev_push(remote: Option<&str>, branch: Option<&str>, upstream: bool, force: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let remote = remote.unwrap_or("origin");
let current_branch = if branch.is_some() {
branch.unwrap().to_string()
} else {
let (ok, stdout, _) = run_cmd(&["rev-parse", "--abbrev-ref", "HEAD"])?;
if !ok {
return Err(AuroraError::ModuleError(
"Не удалось определить текущую ветку".into()
));
}
stdout.trim().to_string()
};
let remote_str = remote.to_string();
let branch_str = current_branch.clone();
if force {
if !prompt_yes_no(&format!(
"{} {}/{}? {}",
localizer.t("⚠ FORCE PUSH на", "⚠ FORCE PUSH to"),
remote_str, branch_str,
localizer.t("История будет перезаписана! Продолжить?", "History will be rewritten! Continue?")
))? {
return Ok(Pipeline::single(Value::String(localizer.t("Отменено.", "Cancelled.").to_string())));
}
}
println!("{} {}/{}", localizer.t("→ Пушим в", "→ Pushing to"), remote, current_branch);
let mut args = vec!["push"];
if upstream { args.push("-u"); }
if force { args.push("--force"); }
args.push(remote);
args.push(¤t_branch);
let (ok, stdout, stderr) = run_cmd(&args)?;
if !ok {
let msg = if stderr.contains("fatal:") && stderr.contains("repository") {
localizer.t(
"Репозиторий не найден. Проверь remote: aurora dev remote list",
"Repository not found. Check remote: aurora dev remote list"
)
} else if stderr.contains("fatal:") && stderr.contains("refspec") {
localizer.t(
"Ветка не найдена на удалённом репозитории. Попробуй --upstream или git push -u origin <ветка>",
"Branch not found on remote. Try --upstream or git push -u origin <branch>"
)
} else {
localizer.t(
"Git push не удался. Проверь remote и права доступа.",
"Git push failed. Check remote and access rights."
)
};
return Err(AuroraError::ModuleError(format!("{msg}\n{stderr}")));
}
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in output.lines() {
let line = line.trim();
if line.is_empty() { continue; }
rows.push(vec![Value::String(line.into())]);
}
println!("{} {}/{}", localizer.t("✓ Запушено в", "✓ Pushed to"), remote, current_branch);
Ok(Pipeline::table(vec!["output".into()], rows))
}
pub fn dev_fetch(remote: Option<&str>, branch: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let remote = remote.unwrap_or("origin");
let mut args = vec!["fetch", remote];
if let Some(b) = branch { args.push(b); }
println!("{} {}", localizer.t("→ Получаю из", "→ Fetching from"), remote);
let (ok, stdout, stderr) = run_cmd(&args)?;
if !ok {
return Err(AuroraError::ModuleError(
format!("git fetch: {stderr}")
));
}
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in output.lines() {
let line = line.trim();
if line.is_empty() { continue; }
rows.push(vec![Value::String(line.into())]);
}
println!("{} {}{}",
localizer.t("✓ Получено из", "✓ Fetched from"),
remote,
branch.map(|b| format!(" ({b})")).unwrap_or_default()
);
Ok(Pipeline::table(vec!["output".into()], rows))
}
pub fn dev_pull(remote: Option<&str>, branch: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let remote = remote.unwrap_or("origin");
let current_branch = if branch.is_some() {
branch.unwrap().to_string()
} else {
let (ok, stdout, _) = run_cmd(&["rev-parse", "--abbrev-ref", "HEAD"])?;
if !ok {
return Err(AuroraError::ModuleError(
"Не удалось определить текущую ветку".into()
));
}
stdout.trim().to_string()
};
println!("{} {}/{}", localizer.t("→ Тяну из", "→ Pulling from"), remote, current_branch);
let (ok, stdout, stderr) = run_cmd(&["pull", remote, ¤t_branch])?;
if !ok {
return Err(AuroraError::ModuleError(format!("git pull: {stderr}")));
}
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in output.lines() {
let line = line.trim();
if line.is_empty() { continue; }
rows.push(vec![Value::String(line.into())]);
}
Ok(Pipeline::table(vec!["output".into()], rows))
}
pub fn dev_clone(url: &str, target: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
let dir = target.unwrap_or("");
println!("{} {} {}",
localizer.t("→ Клонирую", "→ Cloning"),
url,
if dir.is_empty() { "".into() } else { format!("→ {dir}") }
);
let mut args = vec!["clone", url];
if !dir.is_empty() { args.push(dir); }
let (ok, _stdout, stderr) = run_cmd(&args)?;
if !ok {
return Err(AuroraError::ModuleError(format!("git clone: {stderr}")));
}
println!("{}", localizer.t("✓ Репозиторий склонирован!", "✓ Repository cloned!"));
Ok(Pipeline::table(
vec!["url".into(), "target".into()],
vec![vec![Value::String(url.into()), Value::String(if dir.is_empty() { "." } else { dir }.into())]],
))
}
pub fn dev_init(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
if check_git_repo()? {
return Err(AuroraError::ModuleError(
localizer.t("Репозиторий уже инициализирован.", "Repository already initialized.").to_string()
));
}
let (ok, _, stderr) = run_cmd(&["init"])?;
if !ok {
return Err(AuroraError::ModuleError(format!("git init: {stderr}")));
}
println!("{}", localizer.t("✓ Репозиторий создан!", "✓ Repository created!"));
if prompt_yes_no(localizer.t(
"Создать .gitignore? Он скрывает мусор из репозитория.",
"Create .gitignore? It hides junk from the repo."
))? {
let gitignore_path = std::path::Path::new(".gitignore");
if gitignore_path.exists() {
println!(" .gitignore уже существует — пропускаем.");
} else {
std::fs::write(gitignore_path, GITIGNORE_TEMPLATE)
.map_err(|e| AuroraError::ModuleError(format!("Не удалось создать .gitignore: {e}")))?;
println!("{}", localizer.t("✓ .gitignore создан!", "✓ .gitignore created!"));
}
}
if let Some((name, email)) = check_git_config()? {
println!(" {}: {name} <{email}>", localizer.t("Git настроен как", "Git configured as"));
} else if prompt_yes_no(localizer.t(
"Настроить имя и email для git?",
"Set up your git name and email?"
))? {
let name = prompt(&localizer.t("Имя: ", "Name: "))?;
let email = prompt(&localizer.t("Email: ", "Email: "))?;
if !name.is_empty() && !email.is_empty() {
run_cmd(&["config", "user.name", &name])?;
run_cmd(&["config", "user.email", &email])?;
println!("{}", localizer.t("✓ Git настроен!", "✓ Git configured!"));
}
}
Ok(Pipeline::table(
vec!["status".into()],
vec![vec![Value::String("initialized".into())]],
))
}
pub fn dev_add(files: &[String], localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let (ok, stdout, stderr) = if files.is_empty() {
run_cmd(&["add", "-A"])?
} else {
let mut args = vec!["add"];
for f in files { args.push(f); }
run_cmd(&args)?
};
if !ok {
return Err(AuroraError::ModuleError(format!("git add: {stderr}")));
}
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in output.lines() {
let line = line.trim();
if line.is_empty() { continue; }
rows.push(vec![Value::String(line.into())]);
}
println!("{}", localizer.t("✓ Файлы добавлены!", "✓ Files staged!"));
Ok(Pipeline::table(vec!["output".into()], rows))
}
pub fn dev_checkout(target: Option<&str>, create: bool, orphan: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let mut args = vec!["checkout"];
if orphan {
args.push("--orphan");
let branch = target.unwrap_or("");
if branch.is_empty() {
return Err(AuroraError::InvalidInput(
localizer.t("Укажи имя ветки: aurora dev checkout --orphan <имя>", "Specify branch name: aurora dev checkout --orphan <name>").to_string()
));
}
args.push(branch);
let (ok, stdout, stderr) = run_cmd(&args)?;
if !ok { return Err(AuroraError::ModuleError(format!("git checkout --orphan: {stderr}"))); }
println!("{} {}", localizer.t("✓ Создана orphan-ветка", "✓ Created orphan branch"), branch);
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
return Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(output.trim().into())]]));
}
let target = target.unwrap_or("");
if create && !target.is_empty() { args.push("-b"); }
if !target.is_empty() { args.push(target); }
let (ok, stdout, stderr) = run_cmd(&args)?;
if !ok {
let hint = if stderr.contains("already exists") {
localizer.t("Ветка уже существует. Используй dev checkout <ветка> без --create.", "Branch exists. Use dev checkout <branch> without --create.")
} else if stderr.contains("did not match any") {
localizer.t("Ветка не найдена. Используй --create чтобы создать.", "Branch not found. Use --create to create it.")
} else { "" };
return Err(AuroraError::ModuleError(format!("git checkout: {stderr}{}", if hint.is_empty() { "".into() } else { format!("\n{hint}") })));
}
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in output.lines() {
let line = line.trim();
if line.is_empty() { continue; }
rows.push(vec![Value::String(line.into())]);
}
println!("{} {} → {}", localizer.t("✓ Переключено на", "✓ Switched to"), target,
if create { localizer.t("(новая ветка)", "(new branch)") } else { "" });
Ok(Pipeline::table(vec!["output".into()], rows))
}
pub fn dev_stash_push(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let (ok, _stdout, stderr) = run_cmd(&["stash"])?;
if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
println!("{}", localizer.t("✓ Изменения спрятаны!", "✓ Changes stashed!"));
Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(stderr.trim().into())]]))
}
pub fn dev_stash_list(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let (ok, stdout, stderr) = run_cmd(&["stash", "list"])?;
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in output.lines() { let l = line.trim(); if l.is_empty() { continue; } rows.push(vec![Value::String(l.into())]); }
Ok(Pipeline::table(vec!["stash".into()], rows))
}
pub fn dev_stash_pop(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let (ok, stdout, stderr) = run_cmd(&["stash", "pop"])?;
if !ok { return Err(AuroraError::ModuleError(format!("git stash: {stderr}"))); }
println!("{}", localizer.t("✓ Изменения достаны!", "✓ Changes popped!"));
let output = if stdout.trim().is_empty() { &stderr } else { &stdout };
Ok(Pipeline::table(vec!["output".into()], vec![vec![Value::String(output.trim().into())]]))
}
pub fn dev_reset(files: &[String], hard: bool, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
if hard {
if !prompt_yes_no(&localizer.t(
"⚠ HARD RESET — все несохранённые изменения будут потеряны! Продолжить?",
"⚠ HARD RESET — all unsaved changes will be lost! Continue?"
))? {
return Ok(Pipeline::single(Value::String(localizer.t("Отменено.", "Cancelled.").to_string())));
}
let (ok, _, stderr) = run_cmd(&["reset", "--hard"])?;
if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
println!("{}", localizer.t("✓ Жёсткий сброс выполнен!", "✓ Hard reset done!"));
} else if files.is_empty() {
let (ok, _, stderr) = run_cmd(&["reset"])?;
if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
println!("{}", localizer.t("✓ Все файлы сняты с индекса!", "✓ All files unstaged!"));
} else {
let mut args = vec!["reset"];
for f in files { args.push(f); }
let (ok, _, stderr) = run_cmd(&args)?;
if !ok { return Err(AuroraError::ModuleError(format!("git reset: {stderr}"))); }
}
Ok(Pipeline::table(
vec!["status".into()],
vec![vec![Value::String(if hard { "hard reset" } else { "reset" }.into())]],
))
}
pub fn dev_config(key: Option<&str>, value: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
match (key, value) {
(Some(k), Some(v)) => {
let (ok, _, stderr) = run_cmd(&["config", k, v])?;
if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
println!("{} {k} = {v}", localizer.t("✓ Установлено:", "✓ Set:"));
Ok(Pipeline::table(
vec!["key".into(), "value".into()],
vec![vec![Value::String(k.into()), Value::String(v.into())]],
))
}
(Some(k), None) => {
let (ok, stdout, stderr) = run_cmd(&["config", k])?;
if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
Ok(Pipeline::table(
vec!["key".into(), "value".into()],
vec![vec![Value::String(k.into()), Value::String(stdout.trim().into())]],
))
}
(None, None) => {
let (ok, stdout, stderr) = run_cmd(&["config", "--list"])?;
if !ok { return Err(AuroraError::ModuleError(format!("git config: {stderr}"))); }
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() { continue; }
if let Some(eq) = line.find('=') {
rows.push(vec![Value::String(line[..eq].into()), Value::String(line[eq+1..].into())]);
}
}
Ok(Pipeline::table(vec!["key".into(), "value".into()], rows))
}
(None, Some(_)) => Err(AuroraError::InvalidInput(
"нельзя указать значение без ключа. Используй: aurora dev config <key> <value>".into()
)),
}
}
pub fn dev_remote_add(name: Option<&str>, url: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let remote_name = name.unwrap_or("origin");
let repo_url = match url {
Some(u) => u.to_string(),
None => {
println!("{}", localizer.t(
"Введи URL репозитория (например https://github.com/user/repo.git):",
"Enter repository URL (e.g. https://github.com/user/repo.git):"
));
prompt("> ")?
}
};
if repo_url.is_empty() {
return Err(AuroraError::ModuleError(
localizer.t("URL не может быть пустым.", "URL cannot be empty.").to_string()
));
}
let (exists_ok, remotes, _) = run_cmd(&["remote", "get-url", remote_name])?;
if exists_ok {
if !prompt_yes_no(&format!(
"{} {}: {}. {}?",
localizer.t("Remote", "Remote"),
remote_name,
remotes.trim(),
localizer.t("Перезаписать", "Overwrite")
))? {
return Ok(Pipeline::single(Value::String(
localizer.t("Отменено.", "Cancelled.").to_string()
)));
}
run_cmd(&["remote", "set-url", remote_name, &repo_url])?;
println!("{} {} = {repo_url}", localizer.t("✓ Remote обновлён:", "✓ Remote updated:"), remote_name);
} else {
run_cmd(&["remote", "add", remote_name, &repo_url])?;
println!("{} {} = {repo_url}", localizer.t("✓ Remote добавлен:", "✓ Remote added:"), remote_name);
}
Ok(Pipeline::table(
vec!["remote".into(), "url".into()],
vec![vec![Value::String(remote_name.into()), Value::String(repo_url)]],
))
}
pub fn dev_remote_list(localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let (ok, stdout, stderr) = run_cmd(&["remote", "-v"])?;
if !ok {
return Err(AuroraError::ModuleError(
format!("git remote: {stderr}")
));
}
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() { continue; }
let parts: Vec<&str> = line.splitn(2, '\t').collect();
if parts.len() == 2 {
rows.push(vec![
Value::String(parts[0].into()),
Value::String(parts[1].into()),
]);
}
}
if rows.is_empty() {
println!("{}", localizer.t(
"Нет удалённых репозиториев. Добавь: aurora dev remote add <url>",
"No remotes configured. Add one: aurora dev remote add <url>"
));
}
Ok(Pipeline::table(vec!["remote".into(), "url".into()], rows))
}
pub fn dev_remote_remove(name: &str, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
let (ok, _, stderr) = run_cmd(&["remote", "remove", name])?;
if !ok {
let hint = if stderr.contains("No such remote") {
format!(". {}",
localizer.t("Проверь список: aurora dev remote list", "Check the list: aurora dev remote list")
)
} else { String::new() };
return Err(AuroraError::ModuleError(format!("git remote remove: {stderr}{hint}")));
}
println!("{} {}", localizer.t("✓ Remote удалён:", "✓ Remote removed:"), name);
Ok(Pipeline::table(
vec!["remote".into(), "action".into()],
vec![vec![Value::String(name.into()), Value::String("removed".into())]],
))
}
pub fn dev_branch(delete: bool, rename: bool, name: Option<&str>, localizer: &aurora_locale::Localizer) -> AuroraResult<Pipeline> {
check_git_installed()?;
require_git_repo(localizer)?;
if delete {
let branch = name.unwrap_or("");
if branch.is_empty() {
return Err(AuroraError::InvalidInput(
localizer.t("Укажи ветку для удаления: aurora dev branch -D <имя>", "Specify branch to delete: aurora dev branch -D <name>").to_string()
));
}
let (ok, _, stderr) = run_cmd(&["branch", "-D", branch])?;
if !ok { return Err(AuroraError::ModuleError(format!("git branch -D: {stderr}"))); }
println!("{} {}", localizer.t("✓ Ветка удалена:", "✓ Branch deleted:"), branch);
return Ok(Pipeline::table(
vec!["branch".into(), "action".into()],
vec![vec![Value::String(branch.into()), Value::String("deleted".into())]],
));
}
if rename {
let new_name = name.unwrap_or("");
if new_name.is_empty() {
return Err(AuroraError::InvalidInput(
localizer.t("Укажи новое имя: aurora dev branch -m <новое-имя>", "Specify new name: aurora dev branch -m <new-name>").to_string()
));
}
let (ok, _, stderr) = run_cmd(&["branch", "-m", new_name])?;
if !ok { return Err(AuroraError::ModuleError(format!("git branch -m: {stderr}"))); }
println!("{} {}", localizer.t("✓ Ветка переименована в:", "✓ Branch renamed to:"), new_name);
return Ok(Pipeline::table(
vec!["branch".into(), "action".into()],
vec![vec![Value::String(new_name.into()), Value::String("renamed".into())]],
));
}
let output = Command::new("git").arg("branch").output()
.map_err(|e| AuroraError::ModuleError(format!("git branch failed: {e}")))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let mut rows: Vec<Vec<Value>> = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() { continue; }
let current = line.starts_with('*');
let name = if current { &line[2..] } else { line };
rows.push(vec![Value::String(name.trim().into()), Value::Bool(current)]);
}
Ok(Pipeline::table(vec!["branch".into(), "current".into()], rows))
}