use std::collections::BTreeMap;
use std::process::Command;
use std::path::PathBuf;
use gix;
use anyhow;
pub struct GitTree;
enum TreeNode {
File,
Dir(BTreeMap<String, TreeNode>),
}
impl GitTree {
pub fn get_git_root() -> anyhow::Result<PathBuf> {
let repo = gix::discover(".")?;
let root_path = repo.work_dir()
.ok_or_else(|| anyhow::anyhow!("Repository has no working directory (might be bare)"))?
.canonicalize()?;
Ok(root_path)
}
pub fn get_tree() -> Result<String, anyhow::Error> {
let root = Self::get_git_root()?;
let mut cmd = Command::new("git");
let cmd = cmd.arg("ls-files")
.arg("-o")
.arg("--exclude-standard")
.arg("-c")
.current_dir(&root);
let output = cmd.output()?;
if !output.status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
String::from_utf8_lossy(&output.stderr).to_string()
).into());
}
let files: Vec<String> = String::from_utf8_lossy(&output.stdout)
.lines()
.map(String::from)
.collect();
let mut tree = BTreeMap::new();
for path in files {
let parts: Vec<&str> = path.split('/').collect();
let file_name = parts.last().unwrap();
let dir_parts = &parts[..parts.len() - 1];
let mut current = &mut tree;
for &part in dir_parts {
current = current
.entry(part.to_string())
.or_insert(TreeNode::Dir(BTreeMap::new()))
.as_dir_mut()
.unwrap();
}
current.insert(file_name.to_string(), TreeNode::File);
}
let mut result = String::from(".\n");
Self::build_tree_string(&tree, "", &mut result);
Ok(result)
}
fn build_tree_string(
tree: &BTreeMap<String, TreeNode>,
prefix: &str,
result: &mut String,
) {
for (i, (name, node)) in tree.iter().enumerate() {
let is_last_entry = i == tree.len() - 1;
let connector = if is_last_entry { "└── " } else { "├── " };
let next_prefix = if is_last_entry { " " } else { "│ " };
result.push_str(&format!("{}{}{}
", prefix, connector, name));
if let TreeNode::Dir(subtree) = node {
Self::build_tree_string(subtree, &format!("{}{}", prefix, next_prefix), result);
}
}
}
}
impl TreeNode {
fn as_dir_mut(&mut self) -> Option<&mut BTreeMap<String, TreeNode>> {
match self {
TreeNode::Dir(map) => Some(map),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_get_git_root() -> Result<(), anyhow::Error> {
let original_dir = env::current_dir()?;
let temp_dir = tempfile::tempdir()?;
let _repo = gix::init(temp_dir.path())?;
let subdir = temp_dir.path().join("src").join("nested");
std::fs::create_dir_all(&subdir)?;
env::set_current_dir(&subdir)?;
let root = GitTree::get_git_root()?;
assert_eq!(root, temp_dir.path().canonicalize()?);
env::set_current_dir(original_dir)?;
Ok(())
}
}