use std::path::Path;
use crate::error::Result;
use crate::object::{read_commit, read_tree};
use crate::refs::resolve_ref;
use crate::repo::Repo;
use crate::types::{EntryKind, Tree, TreeEntry};
#[derive(Debug, Clone)]
pub struct LsTreeEntry {
pub path: String,
pub entry: TreeEntry,
}
pub fn ls_tree(repo: &Repo, ref_name: &str, path: Option<&Path>) -> Result<Vec<LsTreeEntry>> {
let commit_hash = resolve_ref(repo, ref_name)?;
let commit = read_commit(repo, &commit_hash)?;
let tree = read_tree(repo, &commit.tree)?;
match path {
Some(p) => ls_tree_at_path(repo, &tree, p),
None => ls_tree_flat(repo, &tree, ""),
}
}
fn ls_tree_at_path(repo: &Repo, tree: &Tree, path: &Path) -> Result<Vec<LsTreeEntry>> {
let path_str = path.to_string_lossy();
let components: Vec<&str> = path_str.split('/').filter(|s| !s.is_empty()).collect();
if components.is_empty() {
return ls_tree_flat(repo, tree, "");
}
let mut current_tree = tree.clone();
let mut current_path = String::new();
for (i, component) in components.iter().enumerate() {
match current_tree.get(component) {
Some(entry) => {
if i < components.len() - 1 {
if let EntryKind::Directory { hash, .. } = &entry.kind {
current_tree = read_tree(repo, hash)?;
if current_path.is_empty() {
current_path = component.to_string();
} else {
current_path = format!("{}/{}", current_path, component);
}
} else {
return Ok(vec![]);
}
} else {
if let EntryKind::Directory { hash, .. } = &entry.kind {
let subtree = read_tree(repo, hash)?;
let prefix = if current_path.is_empty() {
component.to_string()
} else {
format!("{}/{}", current_path, component)
};
return ls_tree_flat(repo, &subtree, &prefix);
} else {
let full_path = if current_path.is_empty() {
component.to_string()
} else {
format!("{}/{}", current_path, component)
};
return Ok(vec![LsTreeEntry {
path: full_path,
entry: entry.clone(),
}]);
}
}
}
None => {
return Ok(vec![]);
}
}
}
ls_tree_flat(repo, ¤t_tree, ¤t_path)
}
fn ls_tree_flat(_repo: &Repo, tree: &Tree, prefix: &str) -> Result<Vec<LsTreeEntry>> {
let mut entries = Vec::new();
for entry in tree.entries() {
let path = if prefix.is_empty() {
entry.name.clone()
} else {
format!("{}/{}", prefix, entry.name)
};
entries.push(LsTreeEntry {
path,
entry: entry.clone(),
});
}
Ok(entries)
}
pub fn ls_tree_recursive(repo: &Repo, ref_name: &str) -> Result<Vec<LsTreeEntry>> {
let commit_hash = resolve_ref(repo, ref_name)?;
let commit = read_commit(repo, &commit_hash)?;
let tree = read_tree(repo, &commit.tree)?;
let mut entries = Vec::new();
ls_tree_recursive_impl(repo, &tree, "", &mut entries)?;
Ok(entries)
}
fn ls_tree_recursive_impl(
repo: &Repo,
tree: &Tree,
prefix: &str,
entries: &mut Vec<LsTreeEntry>,
) -> Result<()> {
for entry in tree.entries() {
let path = if prefix.is_empty() {
entry.name.clone()
} else {
format!("{}/{}", prefix, entry.name)
};
entries.push(LsTreeEntry {
path: path.clone(),
entry: entry.clone(),
});
if let EntryKind::Directory { hash, .. } = &entry.kind {
let subtree = read_tree(repo, hash)?;
ls_tree_recursive_impl(repo, &subtree, &path, entries)?;
}
}
Ok(())
}
impl std::fmt::Display for LsTreeEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mode = match &self.entry.kind {
EntryKind::Regular { .. } => "100644",
EntryKind::Symlink { .. } => "120000",
EntryKind::Directory { .. } => "040000",
EntryKind::BlockDevice { .. } => "060000",
EntryKind::CharDevice { .. } => "020000",
EntryKind::Fifo { .. } => "010000",
EntryKind::Socket { .. } => "140000",
EntryKind::Hardlink { .. } => "100644", };
let type_str = self.entry.kind.type_name();
let hash_str = match self.entry.kind.hash() {
Some(h) => h.to_hex()[..12].to_string(),
None => "-".repeat(12),
};
write!(f, "{} {} {} {}", mode, type_str, hash_str, self.path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ops::commit::commit;
use std::fs;
use tempfile::tempdir;
fn test_repo() -> (tempfile::TempDir, Repo) {
let dir = tempdir().unwrap();
let repo_path = dir.path().join("repo");
let repo = Repo::init(&repo_path).unwrap();
(dir, repo)
}
#[test]
fn test_ls_tree_root() {
let (dir, repo) = test_repo();
let source = dir.path().join("source");
fs::create_dir(&source).unwrap();
fs::write(source.join("file.txt"), "content").unwrap();
fs::create_dir(source.join("subdir")).unwrap();
commit(&repo, &source, "test", None, None).unwrap();
let entries = ls_tree(&repo, "test", None).unwrap();
assert_eq!(entries.len(), 2);
assert!(entries.iter().any(|e| e.entry.name == "file.txt"));
assert!(entries.iter().any(|e| e.entry.name == "subdir"));
}
#[test]
fn test_ls_tree_subdir() {
let (dir, repo) = test_repo();
let source = dir.path().join("source");
fs::create_dir_all(source.join("subdir")).unwrap();
fs::write(source.join("subdir/a.txt"), "a").unwrap();
fs::write(source.join("subdir/b.txt"), "b").unwrap();
commit(&repo, &source, "test", None, None).unwrap();
let entries = ls_tree(&repo, "test", Some(Path::new("subdir"))).unwrap();
assert_eq!(entries.len(), 2);
assert!(entries.iter().any(|e| e.path == "subdir/a.txt"));
assert!(entries.iter().any(|e| e.path == "subdir/b.txt"));
}
#[test]
fn test_ls_tree_recursive() {
let (dir, repo) = test_repo();
let source = dir.path().join("source");
fs::create_dir_all(source.join("a/b")).unwrap();
fs::write(source.join("file.txt"), "content").unwrap();
fs::write(source.join("a/nested.txt"), "nested").unwrap();
fs::write(source.join("a/b/deep.txt"), "deep").unwrap();
commit(&repo, &source, "test", None, None).unwrap();
let entries = ls_tree_recursive(&repo, "test").unwrap();
assert!(entries.iter().any(|e| e.path == "file.txt"));
assert!(entries.iter().any(|e| e.path == "a"));
assert!(entries.iter().any(|e| e.path == "a/nested.txt"));
assert!(entries.iter().any(|e| e.path == "a/b"));
assert!(entries.iter().any(|e| e.path == "a/b/deep.txt"));
}
#[test]
fn test_ls_tree_entry_display() {
let (dir, repo) = test_repo();
let source = dir.path().join("source");
fs::create_dir(&source).unwrap();
fs::write(source.join("file.txt"), "content").unwrap();
commit(&repo, &source, "test", None, None).unwrap();
let entries = ls_tree(&repo, "test", None).unwrap();
let display = format!("{}", entries[0]);
assert!(display.contains("100644"));
assert!(display.contains("regular"));
assert!(display.contains("file.txt"));
}
}