use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::Result;
use crate::cli::{GlobalArgs, ListArgs};
use crate::commands::is_backup_timestamp_name;
use crate::concurrency::{collect_mapped_parallel_tallied, sort_by_parallel};
use crate::ndjson_types::{ListEntry, ListSummary};
use crate::output::NdjsonWriter;
struct ListRecord {
path: String,
kind: &'static str,
size: Option<u64>,
modified: Option<String>,
bytes: u64,
ext: Option<String>,
}
fn rel_path_for(path: &Path, workspace: &Path, roots: &[PathBuf]) -> String {
path.strip_prefix(workspace)
.ok()
.or_else(|| roots.iter().find_map(|r| path.strip_prefix(r).ok()))
.unwrap_or(path)
.display()
.to_string()
}
fn format_mtime(meta: &std::fs::Metadata) -> Option<String> {
meta.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| {
let (y, mo, da, h, m, s) = crate::atomic::epoch_to_utc(d.as_secs());
format!("{y:04}-{mo:02}-{da:02}T{h:02}:{m:02}:{s:02}Z")
})
}
#[tracing::instrument(skip_all, fields(command = "list"))]
pub fn cmd_list(
args: &ListArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
) -> Result<()> {
let start = Instant::now();
let workspace = global.resolve_workspace()?;
let roots: Vec<PathBuf> = if args.paths.is_empty() {
vec![workspace.clone()]
} else {
args.paths
.iter()
.map(|p| crate::path_safety::validate_path(p, &workspace))
.collect::<Result<Vec<_>>>()?
};
for root in &roots {
if !root.exists() {
return Err(crate::error::AtomwriteError::NotFound { path: root.clone() }.into());
}
}
let mut builder = crate::commands::build_scoped_walker_flavored(
&roots,
global,
if args.all {
crate::commands::WalkerFlavor::AlwaysVisitHidden
} else {
crate::commands::WalkerFlavor::RespectGlobalFlagsByType
},
&args.include,
&args.exclude,
)?;
if let Some(depth) = args.depth {
builder.max_depth(Some(depth));
}
let want_long = args.long;
let want_ext = args.count_by_ext;
let ws = workspace;
let roots_cap = roots;
let failures: std::sync::Arc<std::sync::Mutex<crate::ndjson_types::FailureTally>> =
std::sync::Arc::new(std::sync::Mutex::new(
crate::ndjson_types::FailureTally::default(),
));
let failures_in_walk = std::sync::Arc::clone(&failures);
crate::concurrency::record_ignore_file_failures(&roots_cap, Some(&failures));
let failures_in_map = std::sync::Arc::clone(&failures);
let mut records: Vec<ListRecord> = collect_mapped_parallel_tallied(
&builder,
move |entry| {
let path = entry.path();
if roots_cap.iter().any(|root| root.as_path() == path) {
return None;
}
let rel_path = rel_path_for(path, &ws, &roots_cap);
if rel_path.is_empty() {
return None;
}
let ft = entry.file_type();
let kind: &'static str = if ft.is_some_and(|t| t.is_dir()) {
"dir"
} else if ft.is_some_and(|t| t.is_symlink()) {
"symlink"
} else {
"file"
};
let (size, modified, bytes) = if want_long {
match entry.metadata() {
Ok(meta) => {
let sz = meta.len();
(Some(sz), format_mtime(&meta), sz)
}
Err(e) => {
if let Ok(mut tally) = failures_in_map.lock() {
tally.record(path, crate::concurrency::walk_failure(&e, path));
}
(None, None, 0)
}
}
} else {
let bytes = match entry.metadata() {
Ok(m) => m.len(),
Err(e) => {
if let Ok(mut tally) = failures_in_map.lock() {
tally.record(path, crate::concurrency::walk_failure(&e, path));
}
0
}
};
(None, None, bytes)
};
let ext = if want_ext {
let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let e = if is_backup_timestamp_name(file_name) {
"backup".to_owned()
} else {
path.extension()
.and_then(|e| e.to_str())
.unwrap_or("(none)")
.to_owned()
};
Some(e)
} else {
None
};
Some(ListRecord {
path: rel_path,
kind,
size,
modified,
bytes,
ext,
})
},
Some(failures_in_walk),
);
sort_by_parallel(&mut records, |a, b| a.path.cmp(&b.path));
crate::output::cap_collected(&mut records);
if crate::signal::is_global_shutdown() {
return Ok(());
}
let text_mode = args.format == crate::cli::SearchFormat::Text;
if text_mode {
writer.set_text_verb("list");
}
let records_total = records.len() as u64;
let mut text_written: u64 = 0;
let mut files: u64 = 0;
let mut dirs: u64 = 0;
let mut symlinks: u64 = 0;
let mut total_bytes: u64 = 0;
let mut by_ext: BTreeMap<String, u64> = BTreeMap::new();
for rec in records {
if crate::signal::is_global_shutdown() {
return Ok(());
}
match rec.kind {
"dir" => dirs += 1,
"symlink" => symlinks += 1,
_ => files += 1,
}
if rec.kind == "file" {
total_bytes += rec.bytes;
}
if let Some(ext) = rec.ext {
*by_ext.entry(ext).or_default() += 1;
}
if text_mode {
if crate::output::text_capped() {
continue;
}
writer.write_text_bytes(format!("{}\n", rec.path).as_bytes())?;
if !crate::output::text_capped() {
text_written = text_written.saturating_add(1);
}
continue;
}
writer.write_event(&ListEntry {
r#type: "entry",
path: rec.path,
kind: rec.kind.into(),
size: rec.size,
modified: rec.modified,
})?;
}
if crate::signal::is_global_shutdown() {
return Ok(());
}
let (failed_count, failed_items, first_failure) = match std::sync::Arc::try_unwrap(failures) {
Ok(mutex) => mutex.into_inner().unwrap_or_default().into_parts(),
Err(arc) => {
let taken = arc
.lock()
.map(|mut g| std::mem::take(&mut *g))
.unwrap_or_default();
taken.into_parts()
}
};
let files_failed = (failed_count > 0).then_some(failed_count);
let summary = ListSummary {
r#type: "summary",
files,
dirs,
symlinks,
total_bytes: Some(total_bytes),
files_budget_dropped: Some(crate::output::budget_dropped()).filter(|n| *n > 0),
by_extension: if args.count_by_ext {
Some(by_ext)
} else {
None
},
elapsed_ms: start.elapsed().as_millis() as u64,
files_failed,
partial: crate::ndjson_types::partial_of(files_failed),
errors: (!failed_items.is_empty()).then_some(failed_items),
walker_stopped_early: crate::output::limit_reached().then_some(true),
};
if let Some(err) = first_failure {
return Err(writer.publish_failure(&summary, err));
}
if text_mode {
if crate::output::text_capped() {
let omitted = records_total.saturating_sub(text_written);
crate::output::note_text_omitted("list", omitted);
}
return Ok(());
}
writer.write_terminal(&summary)?;
Ok(())
}