use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::config::{Config, IGNORED_DIRS, IGNORED_FILES, SUPPORTED_EXTENSIONS};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
pub path: PathBuf,
pub relative_path: PathBuf,
pub content: String,
pub file_type: FileType,
pub size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FileType {
Rust,
Markdown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanResult {
pub files: Vec<FileInfo>,
pub project_structure: ProjectStructure,
pub metadata: ProjectMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectStructure {
pub tree: String,
pub total_files: usize,
pub total_size: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectMetadata {
pub name: String,
pub description: Option<String>,
pub dependencies: Vec<String>,
pub rust_version: Option<String>,
}
pub struct RepositoryScanner {
config: Config,
}
impl RepositoryScanner {
pub fn new(config: Config) -> Self {
Self { config }
}
pub async fn scan(&self) -> Result<ScanResult> {
let mut files = Vec::new();
let mut total_size = 0u64;
for entry in WalkDir::new(&self.config.repo_path)
.into_iter()
.filter_entry(|e| self.should_include_path(e.path()))
{
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(file_info) = self.process_file(path).await? {
total_size += file_info.size;
files.push(file_info);
}
}
}
let project_structure = self.build_project_structure(&files, total_size)?;
let metadata = self.extract_project_metadata().await?;
Ok(ScanResult {
files,
project_structure,
metadata,
})
}
fn should_include_path(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
if !self.config.include_hidden && path_str.contains("/.") {
return false;
}
for ignored_dir in IGNORED_DIRS {
if path_str.contains(ignored_dir) {
return false;
}
}
if path.is_file() {
let filename = path.file_name().unwrap_or_default().to_string_lossy();
if IGNORED_FILES.contains(&filename.as_ref()) {
return false;
}
if let Some(ext) = path.extension() {
let ext_str = format!(".{}", ext.to_string_lossy());
return SUPPORTED_EXTENSIONS.contains(&ext_str.as_str());
}
return false;
}
true
}
async fn process_file(&self, path: &Path) -> Result<Option<FileInfo>> {
let content = fs::read_to_string(path)?;
let metadata = fs::metadata(path)?;
let file_type = match path.extension().and_then(|ext| ext.to_str()) {
Some("rs") => FileType::Rust,
Some("md") => FileType::Markdown,
_ => return Ok(None),
};
let relative_path = path
.strip_prefix(&self.config.repo_path)
.unwrap_or(path)
.to_path_buf();
Ok(Some(FileInfo {
path: path.to_path_buf(),
relative_path,
content,
file_type,
size: metadata.len(),
}))
}
fn build_project_structure(
&self,
files: &[FileInfo],
total_size: u64,
) -> Result<ProjectStructure> {
let mut tree = String::new();
let mut paths: Vec<_> = files.iter().map(|f| &f.relative_path).collect();
paths.sort();
tree.push_str("```\n");
for (i, path) in paths.iter().enumerate() {
let depth = path.components().count() - 1;
let indent = "│ ".repeat(depth);
let connector = if i == paths.len() - 1 {
"└── "
} else {
"├── "
};
tree.push_str(&format!("{}{}{}\n", indent, connector, path.display()));
}
tree.push_str("```\n");
Ok(ProjectStructure {
tree,
total_files: files.len(),
total_size,
})
}
async fn extract_project_metadata(&self) -> Result<ProjectMetadata> {
let cargo_toml_path = self.config.repo_path.join("Cargo.toml");
let readme_path = self.config.repo_path.join("README.md");
let mut metadata = ProjectMetadata {
name: self
.config
.repo_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
description: None,
dependencies: Vec::new(),
rust_version: None,
};
if cargo_toml_path.exists() {
let cargo_content = fs::read_to_string(&cargo_toml_path)?;
self.parse_cargo_toml(&cargo_content, &mut metadata)?;
}
if readme_path.exists() {
let readme_content = fs::read_to_string(&readme_path)?;
metadata.description = self.extract_description_from_readme(&readme_content);
}
Ok(metadata)
}
fn parse_cargo_toml(&self, content: &str, metadata: &mut ProjectMetadata) -> Result<()> {
let lines: Vec<&str> = content.lines().collect();
let mut in_package = false;
let mut in_dependencies = false;
for line in lines {
let line = line.trim();
if line.starts_with("[package]") {
in_package = true;
in_dependencies = false;
continue;
}
if line.starts_with("[dependencies") {
in_package = false;
in_dependencies = true;
continue;
}
if line.starts_with("[") {
in_package = false;
in_dependencies = false;
continue;
}
if in_package {
if line.starts_with("name") {
if let Some(name) = line.split('=').nth(1) {
metadata.name = name.trim().trim_matches('"').to_string();
}
} else if line.starts_with("version") {
if let Some(version) = line.split('=').nth(1) {
metadata.rust_version = Some(version.trim().trim_matches('"').to_string());
}
}
}
if in_dependencies && !line.is_empty() {
if let Some(dep_name) = line.split('=').next() {
metadata.dependencies.push(dep_name.trim().to_string());
}
}
}
Ok(())
}
fn extract_description_from_readme(&self, content: &str) -> Option<String> {
let lines: Vec<&str> = content.lines().collect();
let mut description = String::new();
for line in lines.iter().take(10) {
if line.starts_with('#') {
continue;
}
if !line.trim().is_empty() {
description.push_str(line);
description.push('\n');
if description.len() > 200 {
break;
}
}
}
if description.trim().is_empty() {
None
} else {
Some(description.trim().to_string())
}
}
}