use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[cfg(test)]
mod path_prefix_test;
use crate::paths::remove_path_prefix;
use crate::telemetry::{LogMessage, log_with_context};
use crate::traverse::common::{build_walk, is_hidden_path};
#[derive(Debug, Clone)]
pub struct TreeOptions {
pub case_sensitive: bool,
pub respect_gitignore: bool,
pub depth: Option<usize>,
pub omit_path_prefix: Option<PathBuf>,
}
impl Default for TreeOptions {
fn default() -> Self {
Self {
case_sensitive: false,
respect_gitignore: true,
depth: Some(20),
omit_path_prefix: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type")]
pub enum Entry {
#[serde(rename = "file")]
File { name: String },
#[serde(rename = "directory")]
Directory { name: String },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DirectoryTree {
pub dir: String,
pub entries: Vec<Entry>,
}
pub fn generate_tree(directory: &Path, options: &TreeOptions) -> Result<Vec<DirectoryTree>> {
let walker = build_walk(
directory,
options.respect_gitignore,
options.case_sensitive,
options.depth,
)?;
let mut dirs_map: HashMap<String, Vec<Entry>> = HashMap::new();
let root_dir_path = if let Some(prefix) = &options.omit_path_prefix {
let processed_path = remove_path_prefix(directory, prefix);
processed_path
} else {
directory.to_path_buf()
};
let root_dir_key = root_dir_path.to_string_lossy().to_string();
dirs_map.insert(root_dir_key.clone(), Vec::new());
for result in walker {
let entry = match result {
Ok(entry) => entry,
Err(err) => {
log_with_context(
log::Level::Warn,
LogMessage {
message: format!("Error walking directory: {}", err),
module: "tree",
context: Some(vec![("directory", directory.display().to_string())]),
},
);
continue;
}
};
let path = entry.path();
if path == directory {
continue;
}
if options.respect_gitignore && is_hidden_path(path) {
continue;
}
let processed_path = if let Some(prefix) = &options.omit_path_prefix {
remove_path_prefix(path, prefix)
} else {
path.to_path_buf()
};
if let Some(parent) = path.parent() {
if parent == directory {
if path.is_file() {
let entry = Entry::File {
name: path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
};
dirs_map
.entry(root_dir_key.clone())
.or_default()
.push(entry);
} else if path.is_dir() {
let dir_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let entry = Entry::Directory {
name: dir_name.clone(),
};
dirs_map
.entry(root_dir_key.clone())
.or_default()
.push(entry);
let sub_dir_key = processed_path.to_string_lossy().to_string();
dirs_map.insert(sub_dir_key, Vec::new());
}
} else {
let processed_parent = if let Some(processed_parent) = processed_path.parent() {
processed_parent.to_path_buf()
} else {
if let Some(prefix) = &options.omit_path_prefix {
remove_path_prefix(parent, prefix)
} else {
parent.to_path_buf()
}
};
let parent_key = processed_parent.to_string_lossy().to_string();
if !dirs_map.contains_key(&parent_key) {
dirs_map.insert(parent_key.clone(), Vec::new());
}
if path.is_file() {
let entry = Entry::File {
name: path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
};
dirs_map.entry(parent_key).or_default().push(entry);
} else if path.is_dir() {
let dir_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let entry = Entry::Directory { name: dir_name };
dirs_map.entry(parent_key).or_default().push(entry);
let sub_dir_key = processed_path.to_string_lossy().to_string();
dirs_map.insert(sub_dir_key, Vec::new());
}
}
}
}
let mut result: Vec<DirectoryTree> = dirs_map
.into_iter()
.filter(|(_, entries)| !entries.is_empty()) .map(|(dir, entries)| DirectoryTree { dir, entries })
.collect();
if result.is_empty() {
let root_dir_path = if let Some(prefix) = &options.omit_path_prefix {
remove_path_prefix(directory, prefix)
} else {
directory.to_path_buf()
};
result.push(DirectoryTree {
dir: root_dir_path.to_string_lossy().to_string(),
entries: vec![Entry::Directory {
name: ".".to_string(),
}],
});
}
result.sort_by(|a, b| a.dir.cmp(&b.dir));
Ok(result)
}