use std::io::IsTerminal;
use std::path::Path;
use serde::Serialize;
use crate::context::{open_repo, void_err_to_cli};
use crate::observer::ProgressObserver;
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug, Clone, Serialize)]
pub struct StatusOutput {
pub staged: StagedChanges,
pub unstaged: UnstagedChanges,
pub untracked: Vec<String>,
pub clean: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct StagedChanges {
pub added: Vec<String>,
pub modified: Vec<String>,
pub deleted: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnstagedChanges {
pub modified: Vec<String>,
pub deleted: Vec<String>,
}
mod colors {
pub const GREEN: &str = "\x1b[32m";
pub const RED: &str = "\x1b[31m";
pub const RESET: &str = "\x1b[0m";
pub fn green(use_colors: bool) -> &'static str {
if use_colors {
GREEN
} else {
""
}
}
pub fn red(use_colors: bool) -> &'static str {
if use_colors {
RED
} else {
""
}
}
pub fn reset(use_colors: bool) -> &'static str {
if use_colors {
RESET
} else {
""
}
}
}
pub fn run(cwd: &Path, paths: Vec<String>, opts: &CliOptions) -> Result<(), CliError> {
let paths: Vec<String> = paths.into_iter().filter(|p| !p.trim().is_empty()).collect();
run_command("status", opts, |ctx| {
ctx.progress("Checking status...");
let repo = open_repo(cwd)?;
let observer: std::sync::Arc<ProgressObserver> = if ctx.use_json() {
std::sync::Arc::new(ProgressObserver::new_hidden())
} else {
std::sync::Arc::new(ProgressObserver::new("Scanning files..."))
};
let result = repo
.status_with_options(paths, Some(observer.clone()))
.map_err(void_err_to_cli)?;
observer.finish();
let use_colors = std::io::stderr().is_terminal();
if !ctx.use_json() {
print_human_status(&result, use_colors, ctx);
}
let clean = result.staged_added.is_empty()
&& result.staged_modified.is_empty()
&& result.staged_deleted.is_empty()
&& result.unstaged_modified.is_empty()
&& result.unstaged_deleted.is_empty()
&& result.untracked.is_empty();
Ok(StatusOutput {
staged: StagedChanges {
added: result.staged_added,
modified: result.staged_modified,
deleted: result.staged_deleted,
},
unstaged: UnstagedChanges {
modified: result.unstaged_modified,
deleted: result.unstaged_deleted,
},
untracked: result.untracked,
clean,
})
})
}
fn print_human_status(
result: &void_core::workspace::stage::StatusResult,
use_colors: bool,
ctx: &mut crate::output::CommandContext,
) {
let has_staged = !result.staged_added.is_empty()
|| !result.staged_modified.is_empty()
|| !result.staged_deleted.is_empty();
let has_unstaged = !result.unstaged_modified.is_empty() || !result.unstaged_deleted.is_empty();
let has_untracked = !result.untracked.is_empty();
if !has_staged && !has_unstaged && !has_untracked {
ctx.info("nothing to commit, working tree clean");
return;
}
if has_staged {
ctx.info("Changes to be committed:");
for path in &result.staged_added {
ctx.info(format!(
" {}new file: {}{}",
colors::green(use_colors),
path,
colors::reset(use_colors)
));
}
for path in &result.staged_modified {
ctx.info(format!(
" {}modified: {}{}",
colors::green(use_colors),
path,
colors::reset(use_colors)
));
}
for path in &result.staged_deleted {
ctx.info(format!(
" {}deleted: {}{}",
colors::green(use_colors),
path,
colors::reset(use_colors)
));
}
ctx.info("");
}
if has_unstaged {
ctx.info("Changes not staged for commit:");
for path in &result.unstaged_modified {
ctx.info(format!(
" {}modified: {}{}",
colors::red(use_colors),
path,
colors::reset(use_colors)
));
}
for path in &result.unstaged_deleted {
ctx.info(format!(
" {}deleted: {}{}",
colors::red(use_colors),
path,
colors::reset(use_colors)
));
}
ctx.info("");
}
if has_untracked {
ctx.info("Untracked files:");
for path in &result.untracked {
ctx.info(format!(
" {}{}{}",
colors::red(use_colors),
path,
colors::reset(use_colors)
));
}
ctx.info("");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_status_output_serialization() {
let output = StatusOutput {
staged: StagedChanges {
added: vec!["new_file.rs".to_string()],
modified: vec!["modified_file.rs".to_string()],
deleted: vec!["deleted_file.rs".to_string()],
},
unstaged: UnstagedChanges {
modified: vec!["changed.rs".to_string()],
deleted: vec!["removed.rs".to_string()],
},
untracked: vec!["untrackedfile.txt".to_string()],
clean: false,
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"staged\""));
assert!(json.contains("\"unstaged\""));
assert!(json.contains("\"untracked\""));
assert!(json.contains("\"added\""));
assert!(json.contains("\"modified\""));
assert!(json.contains("\"deleted\""));
assert!(json.contains("\"clean\":false"));
assert!(json.contains("new_file.rs"));
assert!(json.contains("modified_file.rs"));
assert!(json.contains("deleted_file.rs"));
assert!(json.contains("changed.rs"));
assert!(json.contains("removed.rs"));
assert!(json.contains("untrackedfile.txt"));
}
#[test]
fn test_empty_status_output_serialization() {
let output = StatusOutput {
staged: StagedChanges {
added: vec![],
modified: vec![],
deleted: vec![],
},
unstaged: UnstagedChanges {
modified: vec![],
deleted: vec![],
},
untracked: vec![],
clean: true,
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"staged\""));
assert!(json.contains("\"unstaged\""));
assert!(json.contains("\"untracked\":[]"));
assert!(json.contains("\"clean\":true"));
}
#[test]
fn test_colors_with_tty() {
assert_eq!(colors::green(true), "\x1b[32m");
assert_eq!(colors::red(true), "\x1b[31m");
assert_eq!(colors::reset(true), "\x1b[0m");
}
#[test]
fn test_colors_without_tty() {
assert_eq!(colors::green(false), "");
assert_eq!(colors::red(false), "");
assert_eq!(colors::reset(false), "");
}
}