use comfy_table::{Attribute, Cell, ContentArrangement, Table, presets};
use strum::IntoEnumIterator;
use crate::{
cli::Args,
gitinfo::{repoinfo::RepoInfo, status::Status},
};
pub fn repositories_table(repos: &mut [RepoInfo], args: &Args) {
if repos.is_empty() {
log::info!("No repositories found.");
return;
}
repos.sort_by_key(|r| r.repo_path.to_ascii_lowercase());
let repos_iter: Box<dyn Iterator<Item = &RepoInfo>> = if args.non_clean {
Box::new(repos.iter().filter(|r| r.status != Status::Clean))
} else {
Box::new(repos.iter())
};
let mut table = Table::new();
let preset = if args.condensed {
presets::UTF8_FULL_CONDENSED
} else {
presets::UTF8_FULL
};
table
.load_preset(preset)
.set_content_arrangement(ContentArrangement::Dynamic);
let mut header = vec![
Cell::new("Directory").add_attribute(Attribute::Bold),
Cell::new("Branch").add_attribute(Attribute::Bold),
Cell::new("Local").add_attribute(Attribute::Bold),
Cell::new("Commits").add_attribute(Attribute::Bold),
Cell::new("Status").add_attribute(Attribute::Bold),
];
if args.remote {
header.push(Cell::new("Remote").add_attribute(Attribute::Bold));
}
if args.path {
header.push(Cell::new("Path").add_attribute(Attribute::Bold));
}
table.set_header(header);
for repo in repos_iter {
let display_path = if repo.is_worktree {
format!("⎇ {}", repo.repo_path)
} else {
repo.repo_path.clone()
};
let name_cell = Cell::new(&display_path).fg(repo.status.comfy_color());
let mut row = vec![
name_cell,
Cell::new(&repo.branch),
Cell::new(repo.format_local_status()),
Cell::new(repo.commits),
Cell::new(repo.format_status_with_stash_and_ff()).fg(repo.status.comfy_color()),
];
if args.remote {
row.push(Cell::new(repo.remote_url.as_deref().unwrap_or("-")));
}
if args.path {
row.push(Cell::new(repo.path.display()));
}
table.add_row(row);
}
println!("{table}");
}
pub fn legend(condensed: bool) {
let mut table = Table::new();
let preset = if condensed {
presets::UTF8_FULL_CONDENSED
} else {
presets::UTF8_FULL
};
table
.load_preset(preset)
.set_content_arrangement(ContentArrangement::Dynamic);
table.set_header(vec![
Cell::new("Status").add_attribute(Attribute::Bold),
Cell::new("Description").add_attribute(Attribute::Bold),
]);
Status::iter().for_each(|status| {
table.add_row(vec![status.as_cell(), Cell::new(status.description())]);
});
println!("{table}");
println!("The counts in brackets indicate the number of changed files.");
println!("The counts in brackets with an asterisk (*) indicate the number of stashes.");
println!("↑↑ indicates that the repository was fast-forwarded");
println!("⎇ indicates a Git worktree");
}
pub fn summary(repos: &[RepoInfo], failed: usize) {
let total = repos.len();
let clean = repos.iter().filter(|r| r.status == Status::Clean).count();
let dirty = repos
.iter()
.filter(|r| matches!(r.status, Status::Dirty(_)))
.count();
let unpushed = repos.iter().filter(|r| r.has_unpushed).count();
let with_stashes = repos.iter().filter(|r| r.stash_count > 0).count();
let local_only = repos.iter().filter(|r| r.is_local_only).count();
let fast_forwarded = repos.iter().filter(|r| r.fast_forwarded).count();
println!("\nSummary:");
println!(" Total repositories: {total}");
println!(" Clean: {clean}");
println!(" With changes: {dirty}");
println!(" With unpushed: {unpushed}");
println!(" With stashes: {with_stashes}");
println!(" Local-only branches: {local_only}");
println!(" Fast-forwarded: {fast_forwarded}");
if failed > 0 {
println!(" Failed to process: {failed}");
}
}
pub fn failed_summary(failed_repos: &[String]) {
if !failed_repos.is_empty() {
log::warn!("Failed to process the following repositories:");
for repo in failed_repos {
log::warn!(" - {repo}");
}
}
}
pub fn json_output(repos: &[RepoInfo], failed_repos: &[String]) {
let output = serde_json::json!({
"repositories": repos,
"failed": failed_repos
});
println!("{output}");
}